mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-21 06:31:37 +00:00
77281 lines
3.3 MiB
Executable File
77281 lines
3.3 MiB
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# GAM7
|
|
#
|
|
# Copyright 2024, 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 Google Workspace domain and accounts.
|
|
|
|
For more information, see:
|
|
https://github.com/GAM-team/GAM
|
|
https://github.com/GAM-team/GAM/wiki
|
|
"""
|
|
|
|
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
|
|
__version__ = '7.06.05'
|
|
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
|
|
|
#pylint: disable=wrong-import-position
|
|
import base64
|
|
import calendar as calendarlib
|
|
import codecs
|
|
import collections
|
|
import configparser
|
|
import csv
|
|
import datetime
|
|
from email.charset import add_charset, QP
|
|
from email.generator import Generator
|
|
from email.header import decode_header, Header
|
|
from email import message_from_string
|
|
from email.mime.application import MIMEApplication
|
|
from email.mime.audio import MIMEAudio
|
|
from email.mime.base import MIMEBase
|
|
from email.mime.image import MIMEImage
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
from email.utils import formatdate
|
|
from email.policy import SMTP as policySMTP
|
|
import hashlib
|
|
from html.entities import name2codepoint
|
|
from html.parser import HTMLParser
|
|
import http.client as http_client
|
|
import importlib
|
|
from importlib.metadata import version as lib_version
|
|
import io
|
|
import ipaddress
|
|
import json
|
|
import logging
|
|
from logging.handlers import RotatingFileHandler
|
|
import mimetypes
|
|
import multiprocessing
|
|
import os
|
|
import platform
|
|
import queue
|
|
import random
|
|
import re
|
|
from secrets import SystemRandom
|
|
import shlex
|
|
import signal
|
|
import smtplib
|
|
import socket
|
|
import sqlite3
|
|
import ssl
|
|
import string
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
from tempfile import TemporaryFile
|
|
try:
|
|
import termios
|
|
except ImportError:
|
|
# termios does not exist for Windows
|
|
pass
|
|
import threading
|
|
import time
|
|
from traceback import print_exc
|
|
import types
|
|
from urllib.parse import quote, quote_plus, unquote, urlencode, urlparse, parse_qs
|
|
import uuid
|
|
import warnings
|
|
import webbrowser
|
|
import wsgiref.simple_server
|
|
import wsgiref.util
|
|
import zipfile
|
|
|
|
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
|
|
|
|
# 10/2024 - I don't recall why we did this but PyInstaller
|
|
# 6.10.0+ does not like it. Only run this when we're not
|
|
# Frozen.
|
|
if not getattr(sys, 'frozen', False):
|
|
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from pathvalidate import sanitize_filename, sanitize_filepath
|
|
|
|
import google.oauth2.credentials
|
|
import google.oauth2.id_token
|
|
import google.auth
|
|
from google.auth.jwt import Credentials as JWTCredentials
|
|
import google.oauth2.service_account
|
|
import google_auth_oauthlib.flow
|
|
import google_auth_httplib2
|
|
import httplib2
|
|
|
|
httplib2.RETRIES = 5
|
|
|
|
from passlib.hash import sha512_crypt
|
|
from filelock import FileLock
|
|
|
|
if platform.system() == 'Linux':
|
|
import distro
|
|
|
|
from gamlib import glaction
|
|
from gamlib import glapi as API
|
|
from gamlib import glcfg as GC
|
|
from gamlib import glclargs
|
|
from gamlib import glentity
|
|
from gamlib import glgapi as GAPI
|
|
from gamlib import glgdata as GDATA
|
|
from gamlib import glglobals as GM
|
|
from gamlib import glindent
|
|
from gamlib import glmsgs as Msg
|
|
from gamlib import glskus as SKU
|
|
from gamlib import gluprop as UProp
|
|
from gamlib import glverlibs
|
|
|
|
import gdata.apps.service
|
|
import gdata.apps.audit
|
|
import gdata.apps.audit.service
|
|
import gdata.apps.contacts
|
|
import gdata.apps.contacts.service
|
|
# Import local library, does not include discovery documents
|
|
import googleapiclient
|
|
import googleapiclient.discovery
|
|
import googleapiclient.errors
|
|
import googleapiclient.http
|
|
from iso8601 import iso8601
|
|
|
|
IS08601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S%:z'
|
|
RFC2822_TIME_FORMAT = '%a, %d %b %Y %H:%M:%S %z'
|
|
|
|
def ISOformatTimeStamp(timestamp):
|
|
return timestamp.isoformat('T', 'seconds')
|
|
|
|
def currentISOformatTimeStamp(timespec='milliseconds'):
|
|
return datetime.datetime.now(GC.Values[GC.TIMEZONE]).isoformat('T', timespec)
|
|
|
|
Act = glaction.GamAction()
|
|
Cmd = glclargs.GamCLArgs()
|
|
Ent = glentity.GamEntity()
|
|
Ind = glindent.GamIndent()
|
|
|
|
# Finding path method varies between Python source, PyInstaller and StaticX
|
|
if os.environ.get('STATICX_PROG_PATH', False):
|
|
# StaticX static executable
|
|
GM.Globals[GM.GAM_PATH] = os.path.dirname(os.environ['STATICX_PROG_PATH'])
|
|
GM.Globals[GM.GAM_TYPE] = 'staticx'
|
|
elif getattr(sys, 'frozen', False):
|
|
# Pyinstaller executable
|
|
GM.Globals[GM.GAM_PATH] = os.path.dirname(sys.executable)
|
|
GM.Globals[GM.GAM_TYPE] = 'pyinstaller'
|
|
else:
|
|
# Source code
|
|
GM.Globals[GM.GAM_PATH] = os.path.dirname(os.path.realpath(__file__))
|
|
GM.Globals[GM.GAM_TYPE] = 'pythonsource'
|
|
|
|
GIT_USER = 'GAM-team'
|
|
GAM = 'GAM'
|
|
GAM_URL = f'https://github.com/{GIT_USER}/{GAM}'
|
|
GAM_USER_AGENT = (f'{GAM} {__version__} - {GAM_URL} / '
|
|
f'{__author__} / '
|
|
f'Python {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} {sys.version_info[3]} / '
|
|
f'{platform.platform()} {platform.machine()} /'
|
|
)
|
|
GAM_RELEASES = f'https://github.com/{GIT_USER}/{GAM}/releases'
|
|
GAM_WIKI = f'https://github.com/{GIT_USER}/{GAM}/wiki'
|
|
GAM_LATEST_RELEASE = f'https://api.github.com/repos/{GIT_USER}/{GAM}/releases/latest'
|
|
GAM_PROJECT_CREATION = 'GAM Project Creation'
|
|
GAM_PROJECT_CREATION_CLIENT_ID = '297408095146-fug707qsjv4ikron0hugpevbrjhkmsk7.apps.googleusercontent.com'
|
|
|
|
TRUE = 'true'
|
|
FALSE = 'false'
|
|
TRUE_VALUES = [TRUE, 'on', 'yes', 'enabled', '1']
|
|
FALSE_VALUES = [FALSE, 'off', 'no', 'disabled', '0']
|
|
TRUE_FALSE = [TRUE, FALSE]
|
|
ERROR = 'ERROR'
|
|
ERROR_PREFIX = ERROR+': '
|
|
WARNING = 'WARNING'
|
|
WARNING_PREFIX = WARNING+': '
|
|
ONE_KILO_10_BYTES = 1000
|
|
ONE_MEGA_10_BYTES = 1000000
|
|
ONE_GIGA_10_BYTES = 1000000000
|
|
ONE_KILO_BYTES = 1024
|
|
ONE_MEGA_BYTES = 1048576
|
|
ONE_GIGA_BYTES = 1073741824
|
|
SECONDS_PER_MINUTE = 60
|
|
SECONDS_PER_HOUR = 3600
|
|
SECONDS_PER_DAY = 86400
|
|
SECONDS_PER_WEEK = 604800
|
|
MAX_GOOGLE_SHEET_CELLS = 10000000 # See https://support.google.com/drive/answer/37603
|
|
MAX_LOCAL_GOOGLE_TIME_OFFSET = 30
|
|
SHARED_DRIVE_MAX_FILES_FOLDERS = 500000
|
|
UTF8 = 'utf-8'
|
|
UTF8_SIG = 'utf-8-sig'
|
|
EV_GAMCFGDIR = 'GAMCFGDIR'
|
|
EV_GAMCFGSECTION = 'GAMCFGSECTION'
|
|
EV_OLDGAMPATH = 'OLDGAMPATH'
|
|
FN_GAM_CFG = 'gam.cfg'
|
|
FN_LAST_UPDATE_CHECK_TXT = 'lastupdatecheck.txt'
|
|
FN_GAMCOMMANDS_TXT = 'GamCommands.txt'
|
|
MY_DRIVE = 'My Drive'
|
|
TEAM_DRIVE = 'Drive'
|
|
ROOT = 'root'
|
|
ROOTID = 'rootid'
|
|
ORPHANS = 'Orphans'
|
|
SHARED_WITHME = 'SharedWithMe'
|
|
SHARED_DRIVES = 'SharedDrives'
|
|
|
|
LOWERNUMERIC_CHARS = string.ascii_lowercase+string.digits
|
|
ALPHANUMERIC_CHARS = LOWERNUMERIC_CHARS+string.ascii_uppercase
|
|
URL_SAFE_CHARS = ALPHANUMERIC_CHARS+'-._~'
|
|
PASSWORD_SAFE_CHARS = ALPHANUMERIC_CHARS+'!#$%&()*-./:;<=>?@[\\]^_{|}~'
|
|
FILENAME_SAFE_CHARS = ALPHANUMERIC_CHARS+'-_.() '
|
|
CHAT_MESSAGEID_CHARS = string.ascii_lowercase+string.digits+'-'
|
|
|
|
ADMIN_ACCESS_OPTIONS = {'adminaccess', 'asadmin'}
|
|
OWNER_ACCESS_OPTIONS = {'owneraccess', 'asowner'}
|
|
|
|
# Python 3 values
|
|
DEFAULT_CSV_READ_MODE = 'r'
|
|
DEFAULT_FILE_APPEND_MODE = 'a'
|
|
DEFAULT_FILE_READ_MODE = 'r'
|
|
DEFAULT_FILE_WRITE_MODE = 'w'
|
|
|
|
# Google API constants
|
|
APPLICATION_VND_GOOGLE_APPS = 'application/vnd.google-apps.'
|
|
MIMETYPE_GA_DOCUMENT = f'{APPLICATION_VND_GOOGLE_APPS}document'
|
|
MIMETYPE_GA_DRAWING = f'{APPLICATION_VND_GOOGLE_APPS}drawing'
|
|
MIMETYPE_GA_FILE = f'{APPLICATION_VND_GOOGLE_APPS}file'
|
|
MIMETYPE_GA_FOLDER = f'{APPLICATION_VND_GOOGLE_APPS}folder'
|
|
MIMETYPE_GA_FORM = f'{APPLICATION_VND_GOOGLE_APPS}form'
|
|
MIMETYPE_GA_FUSIONTABLE = f'{APPLICATION_VND_GOOGLE_APPS}fusiontable'
|
|
MIMETYPE_GA_JAM = f'{APPLICATION_VND_GOOGLE_APPS}jam'
|
|
MIMETYPE_GA_MAP = f'{APPLICATION_VND_GOOGLE_APPS}map'
|
|
MIMETYPE_GA_PRESENTATION = f'{APPLICATION_VND_GOOGLE_APPS}presentation'
|
|
MIMETYPE_GA_SCRIPT = f'{APPLICATION_VND_GOOGLE_APPS}script'
|
|
MIMETYPE_GA_SCRIPT_JSON = f'{APPLICATION_VND_GOOGLE_APPS}script+json'
|
|
MIMETYPE_GA_SHORTCUT = f'{APPLICATION_VND_GOOGLE_APPS}shortcut'
|
|
MIMETYPE_GA_3P_SHORTCUT = f'{APPLICATION_VND_GOOGLE_APPS}drive-sdk'
|
|
MIMETYPE_GA_SITE = f'{APPLICATION_VND_GOOGLE_APPS}site'
|
|
MIMETYPE_GA_SPREADSHEET = f'{APPLICATION_VND_GOOGLE_APPS}spreadsheet'
|
|
MIMETYPE_TEXT_CSV = 'text/csv'
|
|
MIMETYPE_TEXT_HTML = 'text/html'
|
|
MIMETYPE_TEXT_PLAIN = 'text/plain'
|
|
|
|
GOOGLE_NAMESERVERS = ['8.8.8.8', '8.8.4.4']
|
|
GOOGLE_TIMECHECK_LOCATION = 'admin.googleapis.com'
|
|
NEVER_DATE = '1970-01-01'
|
|
NEVER_DATETIME = '1970-01-01 00:00'
|
|
NEVER_TIME = '1970-01-01T00:00:00.000Z'
|
|
NEVER_TIME_NOMS = '1970-01-01T00:00:00Z'
|
|
NEVER_END_DATE = '1969-12-31'
|
|
NEVER_START_DATE = NEVER_DATE
|
|
PROJECTION_CHOICE_MAP = {'basic': 'BASIC', 'full': 'FULL'}
|
|
REFRESH_EXPIRY = '1970-01-01T00:00:01Z'
|
|
REPLACE_GROUP_PATTERN = re.compile(r'\\(\d+)')
|
|
UNKNOWN = 'Unknown'
|
|
|
|
# Queries
|
|
ME_IN_OWNERS = "'me' in owners"
|
|
ME_IN_OWNERS_AND = ME_IN_OWNERS+" and "
|
|
AND_ME_IN_OWNERS = " and "+ME_IN_OWNERS
|
|
NOT_ME_IN_OWNERS = "not "+ME_IN_OWNERS
|
|
NOT_ME_IN_OWNERS_AND = NOT_ME_IN_OWNERS+" and "
|
|
AND_NOT_ME_IN_OWNERS = " and "+NOT_ME_IN_OWNERS
|
|
ANY_FOLDERS = "mimeType = '"+MIMETYPE_GA_FOLDER+"'"
|
|
MY_FOLDERS = ME_IN_OWNERS_AND+ANY_FOLDERS
|
|
NON_TRASHED = "trashed = false"
|
|
WITH_PARENTS = "'{0}' in parents"
|
|
ANY_NON_TRASHED_WITH_PARENTS = "trashed = false and '{0}' in parents"
|
|
ANY_NON_TRASHED_FOLDER_NAME = "mimeType = '"+MIMETYPE_GA_FOLDER+"' and name = '{0}' and trashed = false"
|
|
MY_NON_TRASHED_FOLDER_NAME = ME_IN_OWNERS_AND+ANY_NON_TRASHED_FOLDER_NAME
|
|
MY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS = ME_IN_OWNERS_AND+"mimeType = '"+MIMETYPE_GA_FOLDER+"' and name = '{0}' and trashed = false and '{1}' in parents"
|
|
ANY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS = "mimeType = '"+MIMETYPE_GA_FOLDER+"' and name = '{0}' and trashed = false and '{1}' in parents"
|
|
WITH_ANY_FILE_NAME = "name = '{0}'"
|
|
WITH_MY_FILE_NAME = ME_IN_OWNERS_AND+WITH_ANY_FILE_NAME
|
|
WITH_OTHER_FILE_NAME = NOT_ME_IN_OWNERS_AND+WITH_ANY_FILE_NAME
|
|
AND_NOT_SHORTCUT = " and mimeType != '"+MIMETYPE_GA_SHORTCUT+"'"
|
|
|
|
# Program return codes
|
|
UNKNOWN_ERROR_RC = 1
|
|
USAGE_ERROR_RC = 2
|
|
SOCKET_ERROR_RC = 3
|
|
GOOGLE_API_ERROR_RC = 4
|
|
NETWORK_ERROR_RC = 5
|
|
FILE_ERROR_RC = 6
|
|
MEMORY_ERROR_RC = 7
|
|
KEYBOARD_INTERRUPT_RC = 8
|
|
HTTP_ERROR_RC = 9
|
|
SCOPES_NOT_AUTHORIZED_RC = 10
|
|
DATA_ERROR_RC = 11
|
|
API_ACCESS_DENIED_RC = 12
|
|
CONFIG_ERROR_RC = 13
|
|
SYSTEM_ERROR_RC = 14
|
|
NO_SCOPES_FOR_API_RC = 15
|
|
CLIENT_SECRETS_JSON_REQUIRED_RC = 16
|
|
OAUTH2SERVICE_JSON_REQUIRED_RC = 16
|
|
OAUTH2_TXT_REQUIRED_RC = 16
|
|
INVALID_JSON_RC = 17
|
|
JSON_ALREADY_EXISTS_RC = 17
|
|
AUTHENTICATION_TOKEN_REFRESH_ERROR_RC = 18
|
|
HARD_ERROR_RC = 19
|
|
# Information
|
|
ENTITY_IS_A_USER_RC = 20
|
|
ENTITY_IS_A_USER_ALIAS_RC = 21
|
|
ENTITY_IS_A_GROUP_RC = 22
|
|
ENTITY_IS_A_GROUP_ALIAS_RC = 23
|
|
ENTITY_IS_AN_UNMANAGED_ACCOUNT_RC = 24
|
|
ORGUNIT_NOT_EMPTY_RC = 25
|
|
CHECK_USER_GROUPS_ERROR_RC = 29
|
|
ORPHANS_COLLECTED_RC = 30
|
|
# Warnings/Errors
|
|
ACTION_FAILED_RC = 50
|
|
ACTION_NOT_PERFORMED_RC = 51
|
|
INVALID_ENTITY_RC = 52
|
|
BAD_REQUEST_RC = 53
|
|
ENTITY_IS_NOT_UNIQUE_RC = 54
|
|
DATA_NOT_AVALIABLE_RC = 55
|
|
ENTITY_DOES_NOT_EXIST_RC = 56
|
|
ENTITY_DUPLICATE_RC = 57
|
|
ENTITY_IS_NOT_AN_ALIAS_RC = 58
|
|
ENTITY_IS_UKNOWN_RC = 59
|
|
NO_ENTITIES_FOUND_RC = 60
|
|
INVALID_DOMAIN_RC = 61
|
|
INVALID_DOMAIN_VALUE_RC = 62
|
|
INVALID_TOKEN_RC = 63
|
|
JSON_LOADS_ERROR_RC = 64
|
|
MULTIPLE_DELETED_USERS_FOUND_RC = 65
|
|
MULTIPLE_PROJECT_FOLDERS_FOUND_RC = 65
|
|
STDOUT_STDERR_ERROR_RC = 66
|
|
INSUFFICIENT_PERMISSIONS_RC = 67
|
|
REQUEST_COMPLETED_NO_RESULTS_RC = 71
|
|
REQUEST_NOT_COMPLETED_RC = 72
|
|
SERVICE_NOT_APPLICABLE_RC = 73
|
|
TARGET_DRIVE_SPACE_ERROR_RC = 74
|
|
USER_REQUIRED_TO_CHANGE_PASSWORD_ERROR_RC = 75
|
|
USER_SUSPENDED_ERROR_RC = 76
|
|
NO_CSV_DATA_TO_UPLOAD_RC = 77
|
|
NO_SA_ACCESS_CONTEXT_MANAGER_EDITOR_ROLE_RC = 78
|
|
ACCESS_POLICY_ERROR_RC = 79
|
|
YUBIKEY_CONNECTION_ERROR_RC = 80
|
|
YUBIKEY_INVALID_KEY_TYPE_RC = 81
|
|
YUBIKEY_INVALID_SLOT_RC = 82
|
|
YUBIKEY_INVALID_PIN_RC = 83
|
|
YUBIKEY_APDU_ERROR_RC = 84
|
|
YUBIKEY_VALUE_ERROR_RC = 85
|
|
YUBIKEY_MULTIPLE_CONNECTED_RC = 86
|
|
YUBIKEY_NOT_FOUND_RC = 87
|
|
|
|
# Multiprocessing lock
|
|
mplock = None
|
|
|
|
# stdin/stdout/stderr
|
|
def readStdin(prompt):
|
|
return input(prompt)
|
|
|
|
def stdErrorExit(e):
|
|
try:
|
|
sys.stderr.write(f'\n{ERROR_PREFIX}{str(e)}\n')
|
|
except IOError:
|
|
pass
|
|
sys.exit(STDOUT_STDERR_ERROR_RC)
|
|
|
|
def writeStdout(data):
|
|
try:
|
|
GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout).write(data)
|
|
except IOError as e:
|
|
stdErrorExit(e)
|
|
|
|
def flushStdout():
|
|
try:
|
|
GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout).flush()
|
|
except IOError as e:
|
|
stdErrorExit(e)
|
|
|
|
def writeStderr(data):
|
|
flushStdout()
|
|
try:
|
|
GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, sys.stderr).write(data)
|
|
except IOError as e:
|
|
stdErrorExit(e)
|
|
|
|
def flushStderr():
|
|
try:
|
|
GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, sys.stderr).flush()
|
|
except IOError as e:
|
|
stdErrorExit(e)
|
|
|
|
# Error messages
|
|
def setSysExitRC(sysRC):
|
|
GM.Globals[GM.SYSEXITRC] = sysRC
|
|
|
|
def stderrErrorMsg(message):
|
|
writeStderr(f'\n{ERROR_PREFIX}{message}\n')
|
|
|
|
def stderrWarningMsg(message):
|
|
writeStderr(f'\n{WARNING_PREFIX}{message}\n')
|
|
|
|
def systemErrorExit(sysRC, message):
|
|
if message:
|
|
stderrErrorMsg(message)
|
|
sys.exit(sysRC)
|
|
|
|
def printErrorMessage(sysRC, message):
|
|
setSysExitRC(sysRC)
|
|
writeStderr(f'\n{Ind.Spaces()}{ERROR_PREFIX}{message}\n')
|
|
|
|
def printWarningMessage(sysRC, message):
|
|
setSysExitRC(sysRC)
|
|
writeStderr(f'\n{Ind.Spaces()}{WARNING_PREFIX}{message}\n')
|
|
|
|
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 sys.platform.startswith('win')
|
|
|
|
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')
|
|
|
|
def executeBatch(dbatch):
|
|
dbatch.execute()
|
|
if GC.Values[GC.INTER_BATCH_WAIT] > 0:
|
|
time.sleep(GC.Values[GC.INTER_BATCH_WAIT])
|
|
|
|
def _stripControlCharsFromName(name):
|
|
for cc in ['\x00', '\r', '\n']:
|
|
name = name.replace(cc, '')
|
|
return name
|
|
|
|
class LazyLoader(types.ModuleType):
|
|
"""Lazily import a module, mainly to avoid pulling in large dependencies.
|
|
|
|
`contrib`, and `ffmpeg` are examples of modules that are large and not always
|
|
needed, and this allows them to only be loaded when they are used.
|
|
"""
|
|
|
|
# The lint error here is incorrect.
|
|
def __init__(self, local_name, parent_module_globals, name):
|
|
self._local_name = local_name
|
|
self._parent_module_globals = parent_module_globals
|
|
|
|
super().__init__(name)
|
|
|
|
def _load(self):
|
|
# Import the target module and insert it into the parent's namespace
|
|
module = importlib.import_module(self.__name__)
|
|
self._parent_module_globals[self._local_name] = module
|
|
|
|
# Update this object's dict so that if someone keeps a reference to the
|
|
# LazyLoader, lookups are efficient (__getattr__ is only called on lookups
|
|
# that fail).
|
|
self.__dict__.update(module.__dict__)
|
|
|
|
return module
|
|
|
|
def __getattr__(self, item):
|
|
module = self._load()
|
|
return getattr(module, item)
|
|
|
|
def __dir__(self):
|
|
module = self._load()
|
|
return dir(module)
|
|
|
|
yubikey = LazyLoader('yubikey', globals(), 'gam.gamlib.yubikey')
|
|
|
|
# gam yubikey resetpvi [yubikey_serialnumber <String>]
|
|
def doResetYubiKeyPIV():
|
|
new_data = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'yubikeyserialnumber':
|
|
new_data['yubikey_serial_number'] = getInteger()
|
|
else:
|
|
unknownArgumentExit()
|
|
yk = yubikey.YubiKey(new_data)
|
|
yk.serial_number = yk.get_serial_number()
|
|
yk.reset_piv()
|
|
|
|
class _DeHTMLParser(HTMLParser): #pylint: disable=abstract-method
|
|
def __init__(self):
|
|
HTMLParser.__init__(self)
|
|
self.__text = []
|
|
|
|
def handle_data(self, data):
|
|
self.__text.append(data)
|
|
|
|
def handle_charref(self, name):
|
|
self.__text.append(chr(int(name[1:], 16)) if name.startswith('x') else chr(int(name)))
|
|
|
|
def handle_entityref(self, name):
|
|
cp = name2codepoint.get(name)
|
|
if cp:
|
|
self.__text.append(chr(cp))
|
|
else:
|
|
self.__text.append('&'+name)
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
if tag == 'p':
|
|
self.__text.append('\n\n')
|
|
elif tag == 'br':
|
|
self.__text.append('\n')
|
|
elif tag == 'a':
|
|
for attr in attrs:
|
|
if attr[0] == 'href':
|
|
self.__text.append(f'({attr[1]}) ')
|
|
break
|
|
elif tag == 'div':
|
|
if not attrs:
|
|
self.__text.append('\n')
|
|
elif tag in {'http:', 'https'}:
|
|
self.__text.append(f' ({tag}//{attrs[0][0]}) ')
|
|
|
|
def handle_startendtag(self, tag, attrs):
|
|
if tag == 'br':
|
|
self.__text.append('\n\n')
|
|
|
|
def text(self):
|
|
return re.sub(r'\n{2}\n+', '\n\n', re.sub(r'\n +', '\n', ''.join(self.__text))).strip()
|
|
|
|
def dehtml(text):
|
|
parser = _DeHTMLParser()
|
|
parser.feed(str(text))
|
|
parser.close()
|
|
return parser.text()
|
|
|
|
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'
|
|
|
|
# Format a key value list
|
|
# key, value -> "key: value" + ", " if not last item
|
|
# key, '' -> "key:" + ", " if not last item
|
|
# key, None -> "key" + " " if not last item
|
|
def formatKeyValueList(prefixStr, kvList, suffixStr):
|
|
msg = prefixStr
|
|
i = 0
|
|
l = len(kvList)
|
|
while i < l:
|
|
if isinstance(kvList[i], (bool, float, int)):
|
|
msg += str(kvList[i])
|
|
else:
|
|
msg += kvList[i]
|
|
i += 1
|
|
if i < l:
|
|
val = kvList[i]
|
|
if (val is not None) or (i == l-1):
|
|
msg += ':'
|
|
if (val is not None) and (not isinstance(val, str) or val):
|
|
msg += ' '
|
|
if isinstance(val, (bool, float, int)):
|
|
msg += str(val)
|
|
else:
|
|
msg += val
|
|
i += 1
|
|
if i < l:
|
|
msg += ', '
|
|
else:
|
|
i += 1
|
|
if i < l:
|
|
msg += ' '
|
|
msg += suffixStr
|
|
return msg
|
|
|
|
# Something's wrong with CustomerID??
|
|
def accessErrorMessage(cd, errMsg=None):
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
try:
|
|
callGAPI(cd.customers(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerKey=GC.Values[GC.CUSTOMER_ID], fields='id')
|
|
except (GAPI.badRequest, GAPI.invalidInput):
|
|
return formatKeyValueList('',
|
|
[Ent.Singular(Ent.CUSTOMER_ID), GC.Values[GC.CUSTOMER_ID],
|
|
Msg.INVALID],
|
|
'')
|
|
except GAPI.resourceNotFound:
|
|
return formatKeyValueList('',
|
|
[Ent.Singular(Ent.CUSTOMER_ID), GC.Values[GC.CUSTOMER_ID],
|
|
Msg.DOES_NOT_EXIST],
|
|
'')
|
|
except GAPI.forbidden:
|
|
return formatKeyValueList('',
|
|
Ent.FormatEntityValueList([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID],
|
|
Ent.DOMAIN, GC.Values[GC.DOMAIN],
|
|
Ent.USER, GM.Globals[GM.ADMIN]])+[Msg.ACCESS_FORBIDDEN],
|
|
'')
|
|
if errMsg:
|
|
return formatKeyValueList('',
|
|
[Ent.Singular(Ent.CUSTOMER_ID), GC.Values[GC.CUSTOMER_ID],
|
|
errMsg],
|
|
'')
|
|
return None
|
|
|
|
def accessErrorExit(cd, errMsg=None):
|
|
systemErrorExit(INVALID_DOMAIN_RC, accessErrorMessage(cd or buildGAPIObject(API.DIRECTORY), errMsg))
|
|
|
|
def accessErrorExitNonDirectory(api, errMsg):
|
|
systemErrorExit(API_ACCESS_DENIED_RC,
|
|
formatKeyValueList('',
|
|
Ent.FormatEntityValueList([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID],
|
|
Ent.DOMAIN, GC.Values[GC.DOMAIN],
|
|
Ent.API, api])+[errMsg],
|
|
''))
|
|
|
|
def ClientAPIAccessDeniedExit(errMsg=None):
|
|
stderrErrorMsg(Msg.API_ACCESS_DENIED)
|
|
if errMsg:
|
|
stderrErrorMsg(errMsg)
|
|
missingScopes = API.getClientScopesSet(GM.Globals[GM.CURRENT_CLIENT_API])-GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]
|
|
if missingScopes:
|
|
writeStderr(Msg.API_CHECK_CLIENT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2_CLIENT_ID],
|
|
','.join(sorted(missingScopes))))
|
|
systemErrorExit(API_ACCESS_DENIED_RC, None)
|
|
|
|
def SvcAcctAPIAccessDenied():
|
|
_getSvcAcctData()
|
|
if (GM.Globals[GM.CURRENT_SVCACCT_API] == API.GMAIL and
|
|
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] and
|
|
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES][0] == API.GMAIL_SEND_SCOPE):
|
|
systemErrorExit(OAUTH2SERVICE_JSON_REQUIRED_RC, Msg.NO_SVCACCT_ACCESS_ALLOWED)
|
|
stderrErrorMsg(Msg.API_ACCESS_DENIED)
|
|
apiOrScopes = API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]) if GM.Globals[GM.CURRENT_SVCACCT_API] else ','.join(sorted(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]))
|
|
writeStderr(Msg.API_CHECK_SVCACCT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id'],
|
|
apiOrScopes,
|
|
GM.Globals[GM.CURRENT_SVCACCT_USER] or _getAdminEmail()))
|
|
def SvcAcctAPIAccessDeniedExit():
|
|
SvcAcctAPIAccessDenied()
|
|
systemErrorExit(API_ACCESS_DENIED_RC, None)
|
|
|
|
def SvcAcctAPIDisabledExit():
|
|
if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]:
|
|
ClientAPIAccessDeniedExit()
|
|
if GM.Globals[GM.CURRENT_SVCACCT_API]:
|
|
stderrErrorMsg(Msg.SERVICE_ACCOUNT_API_DISABLED.format(API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API])))
|
|
systemErrorExit(API_ACCESS_DENIED_RC, None)
|
|
systemErrorExit(API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED)
|
|
|
|
def APIAccessDeniedExit():
|
|
if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]:
|
|
ClientAPIAccessDeniedExit()
|
|
if GM.Globals[GM.CURRENT_SVCACCT_API]:
|
|
SvcAcctAPIAccessDeniedExit()
|
|
systemErrorExit(API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED)
|
|
|
|
def checkEntityDNEorAccessErrorExit(cd, entityType, entityName, i=0, count=0):
|
|
message = accessErrorMessage(cd)
|
|
if message:
|
|
systemErrorExit(INVALID_DOMAIN_RC, message)
|
|
entityDoesNotExistWarning(entityType, entityName, i, count)
|
|
|
|
def checkEntityAFDNEorAccessErrorExit(cd, entityType, entityName, i=0, count=0):
|
|
message = accessErrorMessage(cd)
|
|
if message:
|
|
systemErrorExit(INVALID_DOMAIN_RC, message)
|
|
entityActionFailedWarning([entityType, entityName], Msg.DOES_NOT_EXIST, i, count)
|
|
|
|
def checkEntityItemValueAFDNEorAccessErrorExit(cd, entityType, entityName, itemType, itemValue, i=0, count=0):
|
|
message = accessErrorMessage(cd)
|
|
if message:
|
|
systemErrorExit(INVALID_DOMAIN_RC, message)
|
|
entityActionFailedWarning([entityType, entityName, itemType, itemValue], Msg.DOES_NOT_EXIST, i, count)
|
|
|
|
def invalidClientSecretsJsonExit(errMsg):
|
|
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.CLIENT_SECRETS_JSON_FILE), GC.Values[GC.CLIENT_SECRETS_JSON], errMsg))
|
|
writeStderr(Msg.INSTRUCTIONS_CLIENT_SECRETS_JSON)
|
|
systemErrorExit(CLIENT_SECRETS_JSON_REQUIRED_RC, None)
|
|
|
|
def invalidOauth2serviceJsonExit(errMsg):
|
|
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.OAUTH2SERVICE_JSON_FILE), GC.Values[GC.OAUTH2SERVICE_JSON], errMsg))
|
|
writeStderr(Msg.INSTRUCTIONS_OAUTH2SERVICE_JSON)
|
|
systemErrorExit(OAUTH2SERVICE_JSON_REQUIRED_RC, None)
|
|
|
|
def invalidOauth2TxtExit(errMsg):
|
|
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.OAUTH2_TXT_FILE), GC.Values[GC.OAUTH2_TXT], errMsg))
|
|
writeStderr(Msg.EXECUTE_GAM_OAUTH_CREATE)
|
|
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, None)
|
|
|
|
def expiredRevokedOauth2TxtExit():
|
|
stderrErrorMsg(Msg.IS_EXPIRED_OR_REVOKED.format(Ent.Singular(Ent.OAUTH2_TXT_FILE), GC.Values[GC.OAUTH2_TXT]))
|
|
writeStderr(Msg.EXECUTE_GAM_OAUTH_CREATE)
|
|
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, None)
|
|
|
|
def invalidDiscoveryJsonExit(fileName, errMsg):
|
|
stderrErrorMsg(Msg.DOES_NOT_EXIST_OR_HAS_INVALID_FORMAT.format(Ent.Singular(Ent.DISCOVERY_JSON_FILE), fileName, errMsg))
|
|
systemErrorExit(INVALID_JSON_RC, None)
|
|
|
|
def entityActionFailedExit(entityValueList, errMsg, i=0, count=0):
|
|
systemErrorExit(ACTION_FAILED_RC, formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.Failed(), errMsg],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityDoesNotExistExit(entityType, entityName, i=0, count=0, errMsg=None):
|
|
Cmd.Backup()
|
|
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
|
|
systemErrorExit(ENTITY_DOES_NOT_EXIST_RC, formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(entityType), entityName, errMsg or Msg.DOES_NOT_EXIST],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityDoesNotHaveItemExit(entityValueList, i=0, count=0):
|
|
Cmd.Backup()
|
|
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
|
|
systemErrorExit(ENTITY_DOES_NOT_EXIST_RC, formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Msg.DOES_NOT_EXIST],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityIsNotUniqueExit(entityType, entityName, valueType, valueList, i=0, count=0):
|
|
Cmd.Backup()
|
|
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
|
|
systemErrorExit(ENTITY_IS_NOT_UNIQUE_RC, formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(entityType), entityName, Msg.IS_NOT_UNIQUE.format(Ent.Plural(valueType), ','.join(valueList))],
|
|
currentCountNL(i, count)))
|
|
|
|
def usageErrorExit(message, extraneous=False):
|
|
writeStderr(Cmd.CommandLineWithBadArgumentMarked(extraneous))
|
|
stderrErrorMsg(message)
|
|
writeStderr(Msg.HELP_SYNTAX.format(os.path.join(GM.Globals[GM.GAM_PATH], FN_GAMCOMMANDS_TXT)))
|
|
writeStderr(Msg.HELP_WIKI.format(GAM_WIKI))
|
|
sys.exit(USAGE_ERROR_RC)
|
|
|
|
def csvFieldErrorExit(fieldName, fieldNames, backupArg=False, checkForCharset=False):
|
|
if backupArg:
|
|
Cmd.Backup()
|
|
if checkForCharset and Cmd.Previous() == 'charset':
|
|
Cmd.Backup()
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.HEADER_NOT_FOUND_IN_CSV_HEADERS.format(fieldName, ','.join(fieldNames)))
|
|
|
|
def csvDataAlreadySavedErrorExit():
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.CSV_DATA_ALREADY_SAVED)
|
|
|
|
# The last thing shown is unknown
|
|
def unknownArgumentExit():
|
|
Cmd.Backup()
|
|
usageErrorExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1])
|
|
|
|
# Argument describes what's expected
|
|
def expectedArgumentExit(problem, argument):
|
|
usageErrorExit(f'{problem}: {Msg.EXPECTED} <{argument}>')
|
|
|
|
def blankArgumentExit(argument):
|
|
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_BLANK][1], f'{Msg.NON_BLANK} {argument}')
|
|
|
|
def emptyArgumentExit(argument):
|
|
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_EMPTY][1], f'{Msg.NON_EMPTY} {argument}')
|
|
|
|
def invalidArgumentExit(argument):
|
|
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1], argument)
|
|
|
|
def missingArgumentExit(argument):
|
|
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_MISSING][1], argument)
|
|
|
|
def deprecatedArgument(argument):
|
|
Cmd.Backup()
|
|
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
|
|
Cmd.Advance()
|
|
stderrWarningMsg(f'{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_DEPRECATED][1]}: {Msg.IGNORED} <{argument}>')
|
|
|
|
def deprecatedArgumentExit(argument):
|
|
usageErrorExit(f'{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_DEPRECATED][1]}: <{argument}>')
|
|
|
|
def deprecatedCommandExit():
|
|
systemErrorExit(USAGE_ERROR_RC, Msg.SITES_COMMAND_DEPRECATED.format(Cmd.CommandDeprecated()))
|
|
|
|
# Choices is the valid set of choices that was expected
|
|
def formatChoiceList(choices):
|
|
choiceList = [c if c else "''" for c in choices]
|
|
if len(choiceList) <= 5:
|
|
return '|'.join(choiceList)
|
|
return '|'.join(sorted(choiceList))
|
|
|
|
def invalidChoiceExit(choice, choices, backupArg):
|
|
if backupArg:
|
|
Cmd.Backup()
|
|
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID_CHOICE][1].format(choice), formatChoiceList(choices))
|
|
|
|
def missingChoiceExit(choices):
|
|
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_MISSING][1], formatChoiceList(choices))
|
|
|
|
# Check if argument present
|
|
def checkArgumentPresent(choices, required=False):
|
|
choiceList = choices if isinstance(choices, (list, set)) else [choices]
|
|
if Cmd.ArgumentsRemaining():
|
|
choice = Cmd.Current().strip().lower().replace('_', '')
|
|
if choice:
|
|
if choice in choiceList:
|
|
Cmd.Advance()
|
|
return True
|
|
if not required:
|
|
return False
|
|
invalidChoiceExit(choice, choiceList, False)
|
|
elif not required:
|
|
return False
|
|
missingChoiceExit(choiceList)
|
|
|
|
# Check that there are no extraneous arguments at the end of the command line
|
|
def checkForExtraneousArguments():
|
|
if Cmd.ArgumentsRemaining():
|
|
usageErrorExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_EXTRANEOUS][[1, 0][Cmd.MultipleArgumentsRemaining()]], extraneous=True)
|
|
|
|
# Check that an argument remains, get an argument, downshift, delete underscores
|
|
def checkGetArgument():
|
|
if Cmd.ArgumentsRemaining():
|
|
argument = Cmd.Current().lower()
|
|
if argument:
|
|
Cmd.Advance()
|
|
return argument.replace('_', '')
|
|
missingArgumentExit(Cmd.OB_ARGUMENT)
|
|
|
|
# Get an argument, downshift, delete underscores
|
|
def getArgument():
|
|
argument = Cmd.Current().lower()
|
|
if argument:
|
|
Cmd.Advance()
|
|
return argument.replace('_', '')
|
|
missingArgumentExit(Cmd.OB_ARGUMENT)
|
|
|
|
# Get an argument, downshift, delete underscores
|
|
# An empty argument is allowed
|
|
def getArgumentEmptyAllowed():
|
|
argument = Cmd.Current().lower()
|
|
Cmd.Advance()
|
|
return argument.replace('_', '')
|
|
|
|
def getACLRoles(aclRolesMap):
|
|
roles = []
|
|
for role in getString(Cmd.OB_ROLE_LIST, minLen=0).strip().lower().replace(',', ' ').split():
|
|
if role == 'all':
|
|
for arole in aclRolesMap:
|
|
roles.append(aclRolesMap[arole])
|
|
elif role in aclRolesMap:
|
|
roles.append(aclRolesMap[role])
|
|
else:
|
|
invalidChoiceExit(role, aclRolesMap, True)
|
|
return set(roles)
|
|
|
|
def getBoolean(defaultValue=True):
|
|
if Cmd.ArgumentsRemaining():
|
|
boolean = Cmd.Current().strip().lower()
|
|
if boolean in TRUE_VALUES:
|
|
Cmd.Advance()
|
|
return True
|
|
if boolean in FALSE_VALUES:
|
|
Cmd.Advance()
|
|
return False
|
|
if defaultValue is not None:
|
|
if not Cmd.Current().strip(): # If current argument is empty, skip over it
|
|
Cmd.Advance()
|
|
return defaultValue
|
|
invalidChoiceExit(boolean, TRUE_FALSE, False)
|
|
if defaultValue is not None:
|
|
return defaultValue
|
|
missingChoiceExit(TRUE_FALSE)
|
|
|
|
def getCharSet():
|
|
if checkArgumentPresent('charset'):
|
|
return getString(Cmd.OB_CHAR_SET)
|
|
return GC.Values[GC.CHARSET]
|
|
|
|
DEFAULT_CHOICE = 'defaultChoice'
|
|
CHOICE_ALIASES = 'choiceAliases'
|
|
MAP_CHOICE = 'mapChoice'
|
|
NO_DEFAULT = 'NoDefault'
|
|
|
|
def getChoice(choices, **opts):
|
|
if Cmd.ArgumentsRemaining():
|
|
choice = Cmd.Current().strip().lower()
|
|
if choice or '' in choices:
|
|
if choice in opts.get(CHOICE_ALIASES, []):
|
|
choice = opts[CHOICE_ALIASES][choice]
|
|
if choice not in choices:
|
|
choice = choice.replace('_', '').replace('-', '')
|
|
if choice in opts.get(CHOICE_ALIASES, []):
|
|
choice = opts[CHOICE_ALIASES][choice]
|
|
if choice in choices:
|
|
Cmd.Advance()
|
|
return choice if not opts.get(MAP_CHOICE, False) else choices[choice]
|
|
if opts.get(DEFAULT_CHOICE, NO_DEFAULT) != NO_DEFAULT:
|
|
return opts[DEFAULT_CHOICE]
|
|
invalidChoiceExit(choice, choices, False)
|
|
elif opts.get(DEFAULT_CHOICE, NO_DEFAULT) != NO_DEFAULT:
|
|
return opts[DEFAULT_CHOICE]
|
|
missingChoiceExit(choices)
|
|
|
|
def getChoiceAndValue(item, choices, delimiter):
|
|
if not Cmd.ArgumentsRemaining() or Cmd.Current().find(delimiter) == -1:
|
|
return (None, None)
|
|
choice, value = Cmd.Current().strip().split(delimiter, 1)
|
|
choice = choice.strip().lower()
|
|
value = value.strip()
|
|
if choice in choices:
|
|
if value:
|
|
Cmd.Advance()
|
|
return (choice, value)
|
|
missingArgumentExit(item)
|
|
invalidChoiceExit(choice, choices, False)
|
|
|
|
SUSPENDED_ARGUMENTS = {'notsuspended', 'suspended', 'issuspended'}
|
|
SUSPENDED_CHOICE_MAP = {'notsuspended': False, 'suspended': True}
|
|
def _getIsSuspended(myarg):
|
|
if myarg in SUSPENDED_CHOICE_MAP:
|
|
return SUSPENDED_CHOICE_MAP[myarg]
|
|
return getBoolean()
|
|
|
|
ARCHIVED_ARGUMENTS = {'notarchived', 'archived', 'isarchived'}
|
|
ARCHIVED_CHOICE_MAP = {'notarchived': False, 'archived': True}
|
|
def _getIsArchived(myarg):
|
|
if myarg in ARCHIVED_CHOICE_MAP:
|
|
return ARCHIVED_CHOICE_MAP[myarg]
|
|
return getBoolean()
|
|
|
|
def _getOptionalIsSuspendedIsArchived():
|
|
isSuspended = isArchived = None
|
|
while True:
|
|
if Cmd.PeekArgumentPresent(SUSPENDED_ARGUMENTS):
|
|
isSuspended = getChoice(SUSPENDED_CHOICE_MAP, defaultChoice=None, mapChoice=True)
|
|
if isSuspended is None:
|
|
isSuspended = getBoolean()
|
|
elif Cmd.PeekArgumentPresent(ARCHIVED_ARGUMENTS):
|
|
isArchived = getChoice(ARCHIVED_CHOICE_MAP, defaultChoice=None, mapChoice=True)
|
|
if isArchived is None:
|
|
isArchived = getBoolean()
|
|
else:
|
|
break
|
|
return isSuspended, isArchived
|
|
|
|
CALENDAR_COLOR_MAP = {
|
|
'amethyst': 24, 'avocado': 10, 'banana': 12, 'basil': 8, 'birch': 20, 'blueberry': 16,
|
|
'cherryblossom': 22, 'citron': 11, 'cobalt': 15, 'cocoa': 1, 'eucalyptus': 7, 'flamingo': 2,
|
|
'grape': 23, 'graphite': 19, 'lavender': 17, 'mango': 6, 'peacock': 14, 'pistachio': 9,
|
|
'pumpkin': 5, 'radicchio': 21, 'sage': 13, 'tangerine': 4, 'tomato': 3, 'wisteria': 18,
|
|
}
|
|
|
|
CALENDAR_EVENT_COLOR_MAP = {
|
|
'banana': 5, 'basil': 10, 'blueberry': 9, 'flamingo': 4, 'graphite': 8, 'grape': 3,
|
|
'lavender': 1, 'peacock': 7, 'sage': 2, 'tangerine': 6, 'tomato': 11,
|
|
}
|
|
|
|
GOOGLE_COLOR_MAP = {
|
|
'asparagus': '#7bd148', 'bluevelvet': '#9a9cff', 'bubblegum': '#f691b2', 'cardinal': '#f83a22',
|
|
'chocolateicecream': '#ac725e', 'denim': '#9fc6e7', 'desertsand': '#fbe983', 'earthworm': '#cca6ac',
|
|
'macaroni': '#fad165', 'marsorange': '#ff7537', 'mountaingray': '#cabdbf', 'mountaingrey': '#cabdbf',
|
|
'mouse': '#8f8f8f', 'oldbrickred': '#d06b64', 'pool': '#9fe1e7', 'purpledino': '#b99aff',
|
|
'purplerain': '#cd74e6', 'rainysky': '#4986e7', 'seafoam': '#92e1c0', 'slimegreen': '#b3dc6c',
|
|
'spearmint': '#42d692', 'toyeggplant': '#a47ae2', 'vernfern': '#16a765', 'wildstrawberries': '#fa573c',
|
|
'yellowcab': '#ffad46',
|
|
}
|
|
|
|
WEB_COLOR_MAP = {
|
|
'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', 'aqua': '#00ffff', 'aquamarine': '#7fffd4',
|
|
'azure': '#f0ffff', 'beige': '#f5f5dc', 'bisque': '#ffe4c4', 'black': '#000000',
|
|
'blanchedalmond': '#ffebcd', 'blue': '#0000ff', 'blueviolet': '#8a2be2', 'brown': '#a52a2a',
|
|
'burlywood': '#deb887', 'cadetblue': '#5f9ea0', 'chartreuse': '#7fff00', 'chocolate': '#d2691e',
|
|
'coral': '#ff7f50', 'cornflowerblue': '#6495ed', 'cornsilk': '#fff8dc', 'crimson': '#dc143c',
|
|
'cyan': '#00ffff', 'darkblue': '#00008b', 'darkcyan': '#008b8b', 'darkgoldenrod': '#b8860b',
|
|
'darkgray': '#a9a9a9', 'darkgrey': '#a9a9a9', 'darkgreen': '#006400', 'darkkhaki': '#bdb76b',
|
|
'darkmagenta': '#8b008b', 'darkolivegreen': '#556b2f', 'darkorange': '#ff8c00', 'darkorchid': '#9932cc',
|
|
'darkred': '#8b0000', 'darksalmon': '#e9967a', 'darkseagreen': '#8fbc8f', 'darkslateblue': '#483d8b',
|
|
'darkslategray': '#2f4f4f', 'darkslategrey': '#2f4f4f', 'darkturquoise': '#00ced1', 'darkviolet': '#9400d3',
|
|
'deeppink': '#ff1493', 'deepskyblue': '#00bfff', 'dimgray': '#696969', 'dimgrey': '#696969',
|
|
'dodgerblue': '#1e90ff', 'firebrick': '#b22222', 'floralwhite': '#fffaf0', 'forestgreen': '#228b22',
|
|
'fuchsia': '#ff00ff', 'gainsboro': '#dcdcdc', 'ghostwhite': '#f8f8ff', 'gold': '#ffd700',
|
|
'goldenrod': '#daa520', 'gray': '#808080', 'grey': '#808080', 'green': '#008000',
|
|
'greenyellow': '#adff2f', 'honeydew': '#f0fff0', 'hotpink': '#ff69b4', 'indianred': '#cd5c5c',
|
|
'indigo': '#4b0082', 'ivory': '#fffff0', 'khaki': '#f0e68c', 'lavender': '#e6e6fa',
|
|
'lavenderblush': '#fff0f5', 'lawngreen': '#7cfc00', 'lemonchiffon': '#fffacd', 'lightblue': '#add8e6',
|
|
'lightcoral': '#f08080', 'lightcyan': '#e0ffff', 'lightgoldenrodyellow': '#fafad2', 'lightgray': '#d3d3d3',
|
|
'lightgrey': '#d3d3d3', 'lightgreen': '#90ee90', 'lightpink': '#ffb6c1', 'lightsalmon': '#ffa07a',
|
|
'lightseagreen': '#20b2aa', 'lightskyblue': '#87cefa', 'lightslategray': '#778899', 'lightslategrey': '#778899',
|
|
'lightsteelblue': '#b0c4de', 'lightyellow': '#ffffe0', 'lime': '#00ff00', 'limegreen': '#32cd32',
|
|
'linen': '#faf0e6', 'magenta': '#ff00ff', 'maroon': '#800000', 'mediumaquamarine': '#66cdaa',
|
|
'mediumblue': '#0000cd', 'mediumorchid': '#ba55d3', 'mediumpurple': '#9370db', 'mediumseagreen': '#3cb371',
|
|
'mediumslateblue': '#7b68ee', 'mediumspringgreen': '#00fa9a', 'mediumturquoise': '#48d1cc', 'mediumvioletred': '#c71585',
|
|
'midnightblue': '#191970', 'mintcream': '#f5fffa', 'mistyrose': '#ffe4e1', 'moccasin': '#ffe4b5',
|
|
'navajowhite': '#ffdead', 'navy': '#000080', 'oldlace': '#fdf5e6', 'olive': '#808000',
|
|
'olivedrab': '#6b8e23', 'orange': '#ffa500', 'orangered': '#ff4500', 'orchid': '#da70d6',
|
|
'palegoldenrod': '#eee8aa', 'palegreen': '#98fb98', 'paleturquoise': '#afeeee', 'palevioletred': '#db7093',
|
|
'papayawhip': '#ffefd5', 'peachpuff': '#ffdab9', 'peru': '#cd853f', 'pink': '#ffc0cb',
|
|
'plum': '#dda0dd', 'powderblue': '#b0e0e6', 'purple': '#800080', 'red': '#ff0000',
|
|
'rosybrown': '#bc8f8f', 'royalblue': '#4169e1', 'saddlebrown': '#8b4513', 'salmon': '#fa8072',
|
|
'sandybrown': '#f4a460', 'seagreen': '#2e8b57', 'seashell': '#fff5ee', 'sienna': '#a0522d',
|
|
'silver': '#c0c0c0', 'skyblue': '#87ceeb', 'slateblue': '#6a5acd', 'slategray': '#708090',
|
|
'slategrey': '#708090', 'snow': '#fffafa', 'springgreen': '#00ff7f', 'steelblue': '#4682b4',
|
|
'tan': '#d2b48c', 'teal': '#008080', 'thistle': '#d8bfd8', 'tomato': '#ff6347',
|
|
'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff',
|
|
'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32',
|
|
}
|
|
|
|
COLORHEX_PATTERN = re.compile(r'^#[0-9a-fA-F]{6}$')
|
|
COLORHEX_FORMAT_REQUIRED = 'ColorName|ColorHex'
|
|
|
|
def getColor():
|
|
if Cmd.ArgumentsRemaining():
|
|
color = Cmd.Current().strip().lower()
|
|
if color in GOOGLE_COLOR_MAP:
|
|
Cmd.Advance()
|
|
return GOOGLE_COLOR_MAP[color]
|
|
if color in WEB_COLOR_MAP:
|
|
Cmd.Advance()
|
|
return WEB_COLOR_MAP[color]
|
|
tg = COLORHEX_PATTERN.match(color)
|
|
if tg:
|
|
Cmd.Advance()
|
|
return tg.group(0)
|
|
invalidArgumentExit(COLORHEX_FORMAT_REQUIRED)
|
|
missingArgumentExit(COLORHEX_FORMAT_REQUIRED)
|
|
|
|
LABEL_COLORS = [
|
|
'#000000', '#076239', '#0b804b', '#149e60', '#16a766', '#1a764d', '#1c4587', '#285bac',
|
|
'#2a9c68', '#3c78d8', '#3dc789', '#41236d', '#434343', '#43d692', '#44b984', '#4a86e8',
|
|
'#653e9b', '#666666', '#68dfa9', '#6d9eeb', '#822111', '#83334c', '#89d3b2', '#8e63ce',
|
|
'#999999', '#a0eac9', '#a46a21', '#a479e2', '#a4c2f4', '#aa8831', '#ac2b16', '#b65775',
|
|
'#b694e8', '#b9e4d0', '#c6f3de', '#c9daf8', '#cc3a21', '#cccccc', '#cf8933', '#d0bcf1',
|
|
'#d5ae49', '#e07798', '#e4d7f5', '#e66550', '#eaa041', '#efa093', '#efefef', '#f2c960',
|
|
'#f3f3f3', '#f691b3', '#f6c5be', '#f7a7c0', '#fad165', '#fb4c2f', '#fbc8d9', '#fcda83',
|
|
'#fcdee8', '#fce8b3', '#fef1d1', '#ffad47', '#ffbc6b', '#ffd6a2', '#ffe6c7', '#ffffff',
|
|
]
|
|
LABEL_BACKGROUND_COLORS = [
|
|
'#16a765', '#2da2bb', '#42d692', '#4986e7', '#98d7e4', '#a2dcc1',
|
|
'#b3efd3', '#b6cff5', '#b99aff', '#c2c2c2', '#cca6ac', '#e3d7ff',
|
|
'#e7e7e7', '#ebdbde', '#f2b2a8', '#f691b2', '#fb4c2f', '#fbd3e0',
|
|
'#fbe983', '#fdedc1', '#ff7537', '#ffad46', '#ffc8af', '#ffdeb5',
|
|
]
|
|
LABEL_TEXT_COLORS = [
|
|
'#04502e', '#094228', '#0b4f30', '#0d3472', '#0d3b44', '#3d188e',
|
|
'#464646', '#594c05', '#662e37', '#684e07', '#711a36', '#7a2e0b',
|
|
'#7a4706', '#8a1c0a', '#994a64', '#ffffff',
|
|
]
|
|
|
|
def getLabelColor(colorType):
|
|
if Cmd.ArgumentsRemaining():
|
|
color = Cmd.Current().strip().lower()
|
|
tg = COLORHEX_PATTERN.match(color)
|
|
if tg:
|
|
color = tg.group(0)
|
|
if color in colorType or color in LABEL_COLORS:
|
|
Cmd.Advance()
|
|
return color
|
|
elif color.startswith('custom:'):
|
|
tg = COLORHEX_PATTERN.match(color[7:])
|
|
if tg:
|
|
Cmd.Advance()
|
|
return tg.group(0)
|
|
invalidArgumentExit('|'.join(colorType))
|
|
missingArgumentExit(Cmd.OB_LABEL_COLOR_HEX)
|
|
|
|
# Language codes used in Drive Labels/Youtube
|
|
BCP47_LANGUAGE_CODES_MAP = {
|
|
'ar-sa': 'ar-SA', 'cs-cz': 'cs-CZ', 'da-dk': 'da-DK', 'de-de': 'de-DE', #Arabic Saudi Arabia, Czech Czech Republic, Danish Denmark, German Germany
|
|
'el-gr': 'el-GR', 'en-au': 'en-AU', 'en-gb': 'en-GB', 'en-ie': 'en-IE', #Modern Greek Greece, English Australia, English United Kingdom, English Ireland
|
|
'en-us': 'en-US', 'en-za': 'en-ZA', 'es-es': 'es-ES', 'es-mx': 'es-MX', #English United States, English South Africa, Spanish Spain, Spanish Mexico
|
|
'fi-fi': 'fi-FI', 'fr-ca': 'fr-CA', 'fr-fr': 'fr-FR', 'he-il': 'he-IL', #Finnish Finland, French Canada, French France, Hebrew Israel
|
|
'hi-in': 'hi-IN', 'hu-hu': 'hu-HU', 'id-id': 'id-ID', 'it-it': 'it-IT', #Hindi India, Hungarian Hungary, Indonesian Indonesia, Italian Italy
|
|
'ja-jp': 'ja-JP', 'ko-kr': 'ko-KR', 'nl-be': 'nl-BE', 'nl-nl': 'nl-NL', #Japanese Japan, Korean Republic of Korea, Dutch Belgium, Dutch Netherlands
|
|
'no-no': 'no-NO', 'pl-pl': 'pl-PL', 'pt-br': 'pt-BR', 'pt-pt': 'pt-PT', #Norwegian Norway, Polish Poland, Portuguese Brazil, Portuguese Portugal
|
|
'ro-ro': 'ro-RO', 'ru-ru': 'ru-RU', 'sk-sk': 'sk-SK', 'sv-se': 'sv-SE', #Romanian Romania, Russian Russian Federation, Slovak Slovakia, Swedish Sweden
|
|
'th-th': 'th-TH', 'tr-tr': 'tr-TR', 'zh-cn': 'zh-CN', 'zh-hk': 'zh-HK', #Thai Thailand, Turkish Turkey, Chinese China, Chinese Hong Kong
|
|
'zh-tw': 'zh-TW' #Chinese Taiwan
|
|
}
|
|
|
|
# Valid language codes
|
|
LANGUAGE_CODES_MAP = {
|
|
'ach': 'ach', 'af': 'af', 'ag': 'ga', 'ak': 'ak', 'am': 'am', 'ar': 'ar', 'az': 'az', #Luo, Afrikaans, Irish, Akan, Amharic, Arabica, Azerbaijani
|
|
'be': 'be', 'bem': 'bem', 'bg': 'bg', 'bn': 'bn', 'br': 'br', 'bs': 'bs', 'ca': 'ca', #Belarusian, Bemba, Bulgarian, Bengali, Breton, Bosnian, Catalan
|
|
'chr': 'chr', 'ckb': 'ckb', 'co': 'co', 'crs': 'crs', 'cs': 'cs', 'cy': 'cy', 'da': 'da', #Cherokee, Kurdish (Sorani), Corsican, Seychellois Creole, Czech, Welsh, Danish
|
|
'de': 'de', 'ee': 'ee', 'el': 'el', 'en': 'en', 'en-ca': 'en-CA', 'en-gb': 'en-GB', 'en-us': 'en-US', 'eo': 'eo', #German, Ewe, Greek, English, English (CA), English (UK), English (US), Esperanto
|
|
'es': 'es', 'es-419': 'es-419', 'et': 'et', 'eu': 'eu', 'fa': 'fa', 'fi': 'fi', 'fil': 'fil', 'fo': 'fo', #Spanish, Spanish (Latin American), Estonian, Basque, Persian, Finnish, Filipino, Faroese
|
|
'fr': 'fr', 'fr-ca': 'fr-CA', 'fy': 'fy', 'ga': 'ga', 'gaa': 'gaa', 'gd': 'gd', 'gl': 'gl', #French, French (Canada), Frisian, Irish, Ga, Scots Gaelic, Galician
|
|
'gn': 'gn', 'gu': 'gu', 'ha': 'ha', 'haw': 'haw', 'he': 'he', 'hi': 'hi', 'hr': 'hr', #Guarani, Gujarati, Hausa, Hawaiian, Hebrew, Hindi, Croatian
|
|
'ht': 'ht', 'hu': 'hu', 'hy': 'hy', 'ia': 'ia', 'id': 'id', 'ig': 'ig', 'in': 'in', #Haitian Creole, Hungarian, Armenian, Interlingua, Indonesian, Igbo, in
|
|
'is': 'is', 'it': 'it', 'iw': 'iw', 'ja': 'ja', 'jw': 'jw', 'ka': 'ka', 'kg': 'kg', #Icelandic, Italian, Hebrew, Japanese, Javanese, Georgian, Kongo
|
|
'kk': 'kk', 'km': 'km', 'kn': 'kn', 'ko': 'ko', 'kri': 'kri', 'k': 'k', 'ky': 'ky', #Kazakh, Khmer, Kannada, Korean, Krio (Sierra Leone), Kurdish, Kyrgyz
|
|
'la': 'la', 'lg': 'lg', 'ln': 'ln', 'lo': 'lo', 'loz': 'loz', 'lt': 'lt', 'lua': 'lua', #Latin, Luganda, Lingala, Laothian, Lozi, Lithuanian, Tshiluba
|
|
'lv': 'lv', 'mfe': 'mfe', 'mg': 'mg', 'mi': 'mi', 'mk': 'mk', 'ml': 'ml', 'mn': 'mn', #Latvian, Mauritian Creole, Malagasy, Maori, Macedonian, Malayalam, Mongolian
|
|
'mo': 'mo', 'mr': 'mr', 'ms': 'ms', 'mt': 'mt', 'my': 'my', 'ne': 'ne', 'nl': 'nl', #Moldavian, Marathi, Malay, Maltese, Burmese, Nepali, Dutch
|
|
'nn': 'nn', 'no': 'no', 'nso': 'nso', 'ny': 'ny', 'nyn': 'nyn', 'oc': 'oc', 'om': 'om', #Norwegian (Nynorsk), Norwegian, Northern Sotho, Chichewa, Runyakitara, Occitan, Oromo
|
|
'or': 'or', 'pa': 'pa', 'pcm': 'pcm', 'pl': 'pl', 'ps': 'ps', 'pt-br': 'pt-BR', 'pt-pt': 'pt-PT', #Oriya, Punjabi, Nigerian Pidgin, Polish, Pashto, Portuguese (Brazil), Portuguese (Portugal)
|
|
'q': 'q', 'rm': 'rm', 'rn': 'rn', 'ro': 'ro', 'ru': 'ru', 'rw': 'rw', 'sd': 'sd', #Quechua, Romansh, Kirundi, Romanian, Russian, Kinyarwanda, Sindhi
|
|
'sh': 'sh', 'si': 'si', 'sk': 'sk', 'sl': 'sl', 'sn': 'sn', 'so': 'so', 'sq': 'sq', #Serbo-Croatian, Sinhalese, Slovak, Slovenian, Shona, Somali, Albanian
|
|
'sr': 'sr', 'sr-me': 'sr-ME', 'st': 'st', 'su': 'su', 'sv': 'sv', 'sw': 'sw', 'ta': 'ta', #Serbian, Montenegrin, Sesotho, Sundanese, Swedish, Swahili, Tamil
|
|
'te': 'te', 'tg': 'tg', 'th': 'th', 'ti': 'ti', 'tk': 'tk', 'tl': 'tl', 'tn': 'tn', #Telugu, Tajik, Thai, Tigrinya, Turkmen, Tagalog, Setswana
|
|
'to': 'to', 'tr': 'tr', 'tt': 'tt', 'tum': 'tum', 'tw': 'tw', 'ug': 'ug', 'uk': 'uk', #Tonga, Turkish, Tatar, Tumbuka, Twi, Uighur, Ukrainian
|
|
'ur': 'ur', 'uz': 'uz', 'vi': 'vi', 'wo': 'wo', 'xh': 'xh', 'yi': 'yi', 'yo': 'yo', #Urdu, Uzbek, Vietnamese, Wolof, Xhosa, Yiddish, Yoruba
|
|
'zh-cn': 'zh-CN', 'zh-hk': 'zh-HK', 'zh-tw': 'zh-TW', 'zu': 'zu', #Chinese (Simplified), Chinese (Hong Kong/Traditional), Chinese (Taiwan/Traditional), Zulu
|
|
}
|
|
|
|
LOCALE_CODES_MAP = {
|
|
'': '',
|
|
'ar-eg': 'ar_EG', #Arabic, Egypt
|
|
'az-az': 'az_AZ', #Azerbaijani, Azerbaijan
|
|
'be-by': 'be_BY', #Belarusian, Belarus
|
|
'bg-bg': 'bg_BG', #Bulgarian, Bulgaria
|
|
'bn-in': 'bn_IN', #Bengali, India
|
|
'ca-es': 'ca_ES', #Catalan, Spain
|
|
'cs-cz': 'cs_CZ', #Czech, Czech Republic
|
|
'cy-gb': 'cy_GB', #Welsh, United Kingdom
|
|
'da-dk': 'da_DK', #Danish, Denmark
|
|
'de-ch': 'de_CH', #German, Switzerland
|
|
'de-de': 'de_DE', #German, Germany
|
|
'el-gr': 'el_GR', #Greek, Greece
|
|
'en-au': 'en_AU', #English, Australia
|
|
'en-ca': 'en_CA', #English, Canada
|
|
'en-gb': 'en_GB', #English, United Kingdom
|
|
'en-ie': 'en_IE', #English, Ireland
|
|
'en-us': 'en_US', #English, U.S.A.
|
|
'es-ar': 'es_AR', #Spanish, Argentina
|
|
'es-bo': 'es_BO', #Spanish, Bolivia
|
|
'es-cl': 'es_CL', #Spanish, Chile
|
|
'es-co': 'es_CO', #Spanish, Colombia
|
|
'es-ec': 'es_EC', #Spanish, Ecuador
|
|
'es-es': 'es_ES', #Spanish, Spain
|
|
'es-mx': 'es_MX', #Spanish, Mexico
|
|
'es-py': 'es_PY', #Spanish, Paraguay
|
|
'es-uy': 'es_UY', #Spanish, Uruguay
|
|
'es-ve': 'es_VE', #Spanish, Venezuela
|
|
'fi-fi': 'fi_FI', #Finnish, Finland
|
|
'fil-ph': 'fil_PH', #Filipino, Philippines
|
|
'fr-ca': 'fr_CA', #French, Canada
|
|
'fr-fr': 'fr_FR', #French, France
|
|
'gu-in': 'gu_IN', #Gujarati, India
|
|
'hi-in': 'hi_IN', #Hindi, India
|
|
'hr-hr': 'hr_HR', #Croatian, Croatia
|
|
'hu-hu': 'hu_HU', #Hungarian, Hungary
|
|
'hy-am': 'hy_AM', #Armenian, Armenia
|
|
'in-id': 'in_ID', #Indonesian, Indonesia
|
|
'it-it': 'it_IT', #Italian, Italy
|
|
'iw-il': 'iw_IL', #Hebrew, Israel
|
|
'ja-jp': 'ja_JP', #Japanese, Japan
|
|
'ka-ge': 'ka_GE', #Georgian, Georgia
|
|
'kk-kz': 'kk_KZ', #Kazakh, Kazakhstan
|
|
'kn-in': 'kn_IN', #Kannada, India
|
|
'ko-kr': 'ko_KR', #Korean, Korea
|
|
'lt-lt': 'lt_LT', #Lithuanian, Lithuania
|
|
'lv-lv': 'lv_LV', #Latvian, Latvia
|
|
'ml-in': 'ml_IN', #Malayalam, India
|
|
'mn-mn': 'mn_MN', #Mongolian, Mongolia
|
|
'mr-in': 'mr_IN', #Marathi, India
|
|
'my-mn': 'my_MN', #Burmese, Myanmar
|
|
'nl-nl': 'nl_NL', #Dutch, Netherlands
|
|
'nn-no': 'nn_NO', #Nynorsk, Norway
|
|
'no-no': 'no_NO', #Bokmal, Norway
|
|
'pa-in': 'pa_IN', #Punjabi, India
|
|
'pl-pl': 'pl_PL', #Polish, Poland
|
|
'pt-br': 'pt_BR', #Portuguese, Brazil
|
|
'pt-pt': 'pt_PT', #Portuguese, Portugal
|
|
'ro-ro': 'ro_RO', #Romanian, Romania
|
|
'ru-ru': 'ru_RU', #Russian, Russia
|
|
'sk-sk': 'sk_SK', #Slovak, Slovakia
|
|
'sl-si': 'sl_SI', #Slovenian, Slovenia
|
|
'sr-rs': 'sr_RS', #Serbian, Serbia
|
|
'sv-se': 'sv_SE', #Swedish, Sweden
|
|
'ta-in': 'ta_IN', #Tamil, India
|
|
'te-in': 'te_IN', #Telugu, India
|
|
'th-th': 'th_TH', #Thai, Thailand
|
|
'tr-tr': 'tr_TR', #Turkish, Turkey
|
|
'uk-ua': 'uk_UA', #Ukrainian, Ukraine
|
|
'vi-vn': 'vi_VN', #Vietnamese, Vietnam
|
|
'zh-cn': 'zh_CN', #Simplified Chinese, China
|
|
'zh-hk': 'zh_HK', #Traditional Chinese, Hong Kong SAR China
|
|
'zh-tw': 'zh_TW', #Traditional Chinese, Taiwan
|
|
}
|
|
|
|
def getLanguageCode(languageCodeMap):
|
|
if Cmd.ArgumentsRemaining():
|
|
choice = Cmd.Current().strip().lower().replace('_', '-')
|
|
if choice in languageCodeMap:
|
|
Cmd.Advance()
|
|
return languageCodeMap[choice]
|
|
invalidChoiceExit(choice, languageCodeMap, False)
|
|
missingChoiceExit(languageCodeMap)
|
|
|
|
def addCourseIdScope(courseId):
|
|
if not courseId.isdigit() and courseId[:2] not in {'d:', 'p:'}:
|
|
return f'd:{courseId}'
|
|
return courseId
|
|
|
|
def removeCourseIdScope(courseId):
|
|
if courseId.startswith('d:'):
|
|
return courseId[2:]
|
|
return courseId
|
|
|
|
def addCourseAliasScope(alias):
|
|
if alias[:2] not in {'d:', 'p:'}:
|
|
return f'd:{alias}'
|
|
return alias
|
|
|
|
def removeCourseAliasScope(alias):
|
|
if alias.startswith('d:'):
|
|
return alias[2:]
|
|
return alias
|
|
|
|
def getCourseAlias():
|
|
if Cmd.ArgumentsRemaining():
|
|
courseAlias = Cmd.Current()
|
|
if courseAlias:
|
|
Cmd.Advance()
|
|
return addCourseAliasScope(courseAlias)
|
|
missingArgumentExit(Cmd.OB_COURSE_ALIAS)
|
|
|
|
DELIVERY_SETTINGS_UNDEFINED = 'DSU'
|
|
GROUP_DELIVERY_SETTINGS_MAP = {
|
|
'allmail': 'ALL_MAIL',
|
|
'abridged': 'DAILY',
|
|
'daily': 'DAILY',
|
|
'digest': 'DIGEST',
|
|
'disabled': 'DISABLED',
|
|
'none': 'NONE',
|
|
'nomail': 'NONE',
|
|
}
|
|
|
|
def getDeliverySettings():
|
|
if checkArgumentPresent(['delivery', 'deliverysettings']):
|
|
return getChoice(GROUP_DELIVERY_SETTINGS_MAP, mapChoice=True)
|
|
return getChoice(GROUP_DELIVERY_SETTINGS_MAP, defaultChoice=DELIVERY_SETTINGS_UNDEFINED, mapChoice=True)
|
|
|
|
UID_PATTERN = re.compile(r'u?id: ?(.+)', re.IGNORECASE)
|
|
PEOPLE_PATTERN = re.compile(r'people/([0-9]+)$', re.IGNORECASE)
|
|
|
|
def validateEmailAddressOrUID(emailAddressOrUID, checkPeople=True, ciGroupsAPI=False):
|
|
cg = UID_PATTERN.match(emailAddressOrUID)
|
|
if cg:
|
|
return cg.group(1)
|
|
if checkPeople:
|
|
cg = PEOPLE_PATTERN.match(emailAddressOrUID)
|
|
if cg:
|
|
return cg.group(1)
|
|
if ciGroupsAPI and emailAddressOrUID.startswith('groups/'):
|
|
return emailAddressOrUID
|
|
return emailAddressOrUID.find('@') != 0 and emailAddressOrUID.count('@') <= 1
|
|
|
|
# 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, ciGroupsAPI=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)
|
|
cg = PEOPLE_PATTERN.match(emailAddressOrUID)
|
|
if cg:
|
|
return cg.group(1)
|
|
if ciGroupsAPI and emailAddressOrUID.startswith('groups/'):
|
|
return emailAddressOrUID
|
|
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, allowDash=False):
|
|
if emailAddressOrUID.isdigit() or (allowDash and emailAddressOrUID == '-'):
|
|
return emailAddressOrUID
|
|
return normalizeEmailAddressOrUID(emailAddressOrUID)
|
|
|
|
def getEmailAddress(noUid=False, minLen=1, optional=False, returnUIDprefix=''):
|
|
if Cmd.ArgumentsRemaining():
|
|
emailAddress = Cmd.Current().strip().lower()
|
|
if emailAddress:
|
|
cg = UID_PATTERN.match(emailAddress)
|
|
if cg:
|
|
if not noUid:
|
|
if cg.group(1):
|
|
Cmd.Advance()
|
|
return f'{returnUIDprefix}{cg.group(1)}'
|
|
else:
|
|
invalidArgumentExit('name@domain')
|
|
else:
|
|
atLoc = emailAddress.find('@')
|
|
if atLoc == -1:
|
|
if GC.Values[GC.DOMAIN]:
|
|
emailAddress = f'{emailAddress}@{GC.Values[GC.DOMAIN]}'
|
|
Cmd.Advance()
|
|
return emailAddress
|
|
if atLoc != 0:
|
|
if (atLoc == len(emailAddress)-1) and GC.Values[GC.DOMAIN]:
|
|
emailAddress = f'{emailAddress}{GC.Values[GC.DOMAIN]}'
|
|
Cmd.Advance()
|
|
return emailAddress
|
|
invalidArgumentExit('name@domain')
|
|
if optional:
|
|
Cmd.Advance()
|
|
return None
|
|
if minLen == 0:
|
|
Cmd.Advance()
|
|
return ''
|
|
elif optional:
|
|
return None
|
|
missingArgumentExit([Cmd.OB_EMAIL_ADDRESS_OR_UID, Cmd.OB_EMAIL_ADDRESS][noUid])
|
|
|
|
def getFilename():
|
|
filename = os.path.expanduser(getString(Cmd.OB_FILE_NAME))
|
|
if os.path.isfile(filename):
|
|
return filename
|
|
entityDoesNotExistExit(Ent.FILE, filename)
|
|
|
|
def getPermissionId():
|
|
if Cmd.ArgumentsRemaining():
|
|
emailAddress = Cmd.Current().strip()
|
|
if emailAddress:
|
|
cg = UID_PATTERN.match(emailAddress)
|
|
if cg:
|
|
Cmd.Advance()
|
|
return (False, cg.group(1))
|
|
emailAddress = emailAddress.lower()
|
|
atLoc = emailAddress.find('@')
|
|
if atLoc == -1:
|
|
if emailAddress == 'anyone':
|
|
Cmd.Advance()
|
|
return (False, emailAddress)
|
|
if emailAddress == 'anyonewithlink':
|
|
Cmd.Advance()
|
|
return (False, 'anyoneWithLink')
|
|
if GC.Values[GC.DOMAIN]:
|
|
emailAddress = f'{emailAddress}@{GC.Values[GC.DOMAIN]}'
|
|
Cmd.Advance()
|
|
return (True, emailAddress)
|
|
if atLoc != 0:
|
|
if (atLoc == len(emailAddress)-1) and GC.Values[GC.DOMAIN]:
|
|
emailAddress = f'{emailAddress}{GC.Values[GC.DOMAIN]}'
|
|
Cmd.Advance()
|
|
return (True, emailAddress)
|
|
invalidArgumentExit('name@domain')
|
|
missingArgumentExit(Cmd.OB_DRIVE_FILE_PERMISSION_ID)
|
|
|
|
def getGoogleProduct():
|
|
if Cmd.ArgumentsRemaining():
|
|
product = Cmd.Current().strip()
|
|
if product:
|
|
status, productId = SKU.normalizeProductId(product)
|
|
if not status:
|
|
invalidChoiceExit(productId, SKU.getSortedProductList(), False)
|
|
Cmd.Advance()
|
|
return productId
|
|
missingArgumentExit(Cmd.OB_PRODUCT_ID)
|
|
|
|
def getGoogleProductList():
|
|
if Cmd.ArgumentsRemaining():
|
|
productsList = []
|
|
for product in Cmd.Current().split(','):
|
|
status, productId = SKU.normalizeProductId(product)
|
|
if not status:
|
|
invalidChoiceExit(productId, SKU.getSortedProductList(), False)
|
|
if productId not in productsList:
|
|
productsList.append(productId)
|
|
Cmd.Advance()
|
|
return productsList
|
|
missingArgumentExit(Cmd.OB_PRODUCT_ID_LIST)
|
|
|
|
def getGoogleSKU():
|
|
if Cmd.ArgumentsRemaining():
|
|
sku = Cmd.Current().strip()
|
|
if sku:
|
|
Cmd.Advance()
|
|
return SKU.getProductAndSKU(sku)
|
|
missingArgumentExit(Cmd.OB_SKU_ID)
|
|
|
|
def getGoogleSKUList(allowUnknownProduct=False):
|
|
if Cmd.ArgumentsRemaining():
|
|
skusList = []
|
|
for sku in Cmd.Current().split(','):
|
|
productId, sku = SKU.getProductAndSKU(sku)
|
|
if not productId and not allowUnknownProduct:
|
|
invalidChoiceExit(sku, SKU.getSortedSKUList(), False)
|
|
if (productId, sku) not in skusList:
|
|
skusList.append((productId, sku))
|
|
Cmd.Advance()
|
|
return skusList
|
|
missingArgumentExit(Cmd.OB_SKU_ID_LIST)
|
|
|
|
def floatLimits(minVal, maxVal, item='float'):
|
|
if (minVal is not None) and (maxVal is not None):
|
|
return f'{item} {minVal:.3f}<=x<={maxVal:.3f}'
|
|
if minVal is not None:
|
|
return f'{item} x>={minVal:.3f}'
|
|
if maxVal is not None:
|
|
return f'{item} x<={maxVal:.3f}'
|
|
return f'{item} x'
|
|
|
|
def getFloat(minVal=None, maxVal=None):
|
|
if Cmd.ArgumentsRemaining():
|
|
try:
|
|
number = float(Cmd.Current().strip())
|
|
if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)):
|
|
Cmd.Advance()
|
|
return number
|
|
except ValueError:
|
|
pass
|
|
invalidArgumentExit(floatLimits(minVal, maxVal))
|
|
missingArgumentExit(floatLimits(minVal, maxVal))
|
|
|
|
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(minVal=None, maxVal=None, default=None):
|
|
if Cmd.ArgumentsRemaining():
|
|
try:
|
|
number = int(Cmd.Current().strip())
|
|
if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)):
|
|
Cmd.Advance()
|
|
return number
|
|
except ValueError:
|
|
if default is not None:
|
|
if not Cmd.Current().strip(): # If current argument is empty, skip over it
|
|
Cmd.Advance()
|
|
return default
|
|
invalidArgumentExit(integerLimits(minVal, maxVal))
|
|
elif default is not None:
|
|
return default
|
|
missingArgumentExit(integerLimits(minVal, maxVal))
|
|
|
|
def getIntegerEmptyAllowed(minVal=None, maxVal=None, default=0):
|
|
if Cmd.ArgumentsRemaining():
|
|
number = Cmd.Current().strip()
|
|
if not number:
|
|
Cmd.Advance()
|
|
return default
|
|
try:
|
|
number = int(number)
|
|
if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)):
|
|
Cmd.Advance()
|
|
return number
|
|
except ValueError:
|
|
pass
|
|
invalidArgumentExit(integerLimits(minVal, maxVal))
|
|
return default
|
|
|
|
SORTORDER_CHOICE_MAP = {'ascending': 'ASCENDING', 'descending': 'DESCENDING'}
|
|
|
|
class OrderBy():
|
|
def __init__(self, choiceMap, ascendingKeyword='', descendingKeyword='desc'):
|
|
self.choiceMap = choiceMap
|
|
self.ascendingKeyword = ascendingKeyword
|
|
self.descendingKeyword = descendingKeyword
|
|
self.items = []
|
|
|
|
def GetChoice(self):
|
|
fieldName = getChoice(self.choiceMap, mapChoice=True)
|
|
fieldNameAscending = fieldName
|
|
if self.ascendingKeyword:
|
|
fieldNameAscending += f' {self.ascendingKeyword}'
|
|
if fieldNameAscending in self.items:
|
|
self.items.remove(fieldNameAscending)
|
|
fieldNameDescending = fieldName
|
|
if self.descendingKeyword:
|
|
fieldNameDescending += f' {self.descendingKeyword}'
|
|
if fieldNameDescending in self.items:
|
|
self.items.remove(fieldNameDescending)
|
|
if getChoice(SORTORDER_CHOICE_MAP, defaultChoice=None, mapChoice=True) != 'DESCENDING':
|
|
self.items.append(fieldNameAscending)
|
|
else:
|
|
self.items.append(fieldNameDescending)
|
|
|
|
def SetItems(self, itemList):
|
|
self.items = itemList.split(',')
|
|
|
|
@property
|
|
def orderBy(self):
|
|
return ','.join(self.items)
|
|
|
|
def getOrderBySortOrder(choiceMap, defaultSortOrderChoice='ASCENDING', mapSortOrderChoice=True):
|
|
return (getChoice(choiceMap, mapChoice=True),
|
|
getChoice(SORTORDER_CHOICE_MAP, defaultChoice=defaultSortOrderChoice, mapChoice=mapSortOrderChoice))
|
|
|
|
def orgUnitPathQuery(path, isSuspended):
|
|
query = "orgUnitPath='{0}'".format(path.replace("'", "\\'")) if path != '/' else ''
|
|
if isSuspended is not None:
|
|
query += f' isSuspended={isSuspended}'
|
|
return query
|
|
|
|
def makeOrgUnitPathAbsolute(path):
|
|
if path == '/':
|
|
return path
|
|
if path.startswith('/'):
|
|
if not path.endswith('/'):
|
|
return path
|
|
return path[:-1]
|
|
if path.startswith('id:'):
|
|
return path
|
|
if path.startswith('uid:'):
|
|
return path[1:]
|
|
if not path.endswith('/'):
|
|
return '/'+path
|
|
return '/'+path[:-1]
|
|
|
|
def makeOrgUnitPathRelative(path):
|
|
if path == '/':
|
|
return path
|
|
if path.startswith('/'):
|
|
if not path.endswith('/'):
|
|
return path[1:]
|
|
return path[1:-1]
|
|
if path.startswith('id:'):
|
|
return path
|
|
if path.startswith('uid:'):
|
|
return path[1:]
|
|
if not path.endswith('/'):
|
|
return path
|
|
return path[:-1]
|
|
|
|
def encodeOrgUnitPath(path):
|
|
# 6.22.19 - Encoding doesn't work
|
|
# % no longer needs encoding and + is handled incorrectly in API with or without encoding
|
|
return 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(pathOnly=False, absolutePath=True, cd=None):
|
|
if Cmd.ArgumentsRemaining():
|
|
path = Cmd.Current().strip()
|
|
if path:
|
|
if pathOnly and (path.startswith('id:') or path.startswith('uid:')) and cd is not None:
|
|
try:
|
|
result = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=path,
|
|
fields='orgUnitPath')
|
|
Cmd.Advance()
|
|
if absolutePath:
|
|
return makeOrgUnitPathAbsolute(result['orgUnitPath'])
|
|
return makeOrgUnitPathRelative(result['orgUnitPath'])
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError,
|
|
GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, path)
|
|
invalidArgumentExit(Cmd.OB_ORGUNIT_PATH)
|
|
Cmd.Advance()
|
|
if absolutePath:
|
|
return makeOrgUnitPathAbsolute(path)
|
|
return makeOrgUnitPathRelative(path)
|
|
missingArgumentExit([Cmd.OB_ORGUNIT_ITEM, Cmd.OB_ORGUNIT_PATH][pathOnly])
|
|
|
|
def getTopLevelOrgId(cd, parentOrgUnitPath):
|
|
if parentOrgUnitPath != '/':
|
|
try:
|
|
result = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(parentOrgUnitPath)),
|
|
fields='orgUnitId')
|
|
return result['orgUnitId']
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
return None
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, parentOrgUnitPath)
|
|
return None
|
|
try:
|
|
result = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath='/', type='allIncludingParent',
|
|
fields='organizationUnits(orgUnitId,orgUnitPath)')
|
|
for orgUnit in result.get('organizationUnits', []):
|
|
if orgUnit['orgUnitPath'] == '/':
|
|
return orgUnit['orgUnitId']
|
|
return None
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
return None
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, parentOrgUnitPath)
|
|
return None
|
|
|
|
def getOrgUnitId(cd=None, orgUnit=None):
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
if orgUnit is None:
|
|
orgUnit = getOrgUnitItem()
|
|
try:
|
|
if orgUnit == '/':
|
|
result = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath='/', type='children',
|
|
fields='organizationUnits(parentOrgUnitId,parentOrgUnitPath)')
|
|
if result.get('organizationUnits', []):
|
|
return (result['organizationUnits'][0]['parentOrgUnitPath'], result['organizationUnits'][0]['parentOrgUnitId'])
|
|
topLevelOrgId = getTopLevelOrgId(cd, '/')
|
|
if topLevelOrgId:
|
|
return (orgUnit, topLevelOrgId)
|
|
return (orgUnit, '/') #Bogus but should never happen
|
|
result = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnit)),
|
|
fields='orgUnitId,orgUnitPath')
|
|
return (result['orgUnitPath'], result['orgUnitId'])
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, orgUnit)
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
accessErrorExit(cd)
|
|
|
|
def getAllParentOrgUnitsForUser(cd, user):
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=user, fields='orgUnitPath', projection='basic')
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden):
|
|
entityDoesNotExistExit(Ent.USER, user)
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
accessErrorExit(cd)
|
|
parentPath = result['orgUnitPath']
|
|
if parentPath == '/':
|
|
orgUnitPath, orgUnitId = getOrgUnitId(cd, '/')
|
|
return {orgUnitId: orgUnitPath}
|
|
parentPath = encodeOrgUnitPath(makeOrgUnitPathRelative(parentPath))
|
|
orgUnits = {}
|
|
while True:
|
|
try:
|
|
result = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=parentPath,
|
|
fields='orgUnitId,orgUnitPath,parentOrgUnitId')
|
|
orgUnits[result['orgUnitId']] = result['orgUnitPath']
|
|
if 'parentOrgUnitId' not in result:
|
|
break
|
|
parentPath = result['parentOrgUnitId']
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, parentPath)
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
accessErrorExit(cd)
|
|
return orgUnits
|
|
|
|
def validateREPattern(patstr, flags=0):
|
|
try:
|
|
return re.compile(patstr, flags)
|
|
except re.error as e:
|
|
Cmd.Backup()
|
|
usageErrorExit(f'{Cmd.OB_RE_PATTERN} {Msg.ERROR}: {e}')
|
|
|
|
def getREPattern(flags=0):
|
|
if Cmd.ArgumentsRemaining():
|
|
patstr = Cmd.Current()
|
|
if patstr:
|
|
Cmd.Advance()
|
|
return validateREPattern(patstr, flags)
|
|
missingArgumentExit(Cmd.OB_RE_PATTERN)
|
|
|
|
def validateREPatternSubstitution(pattern, replacement):
|
|
try:
|
|
re.sub(pattern, replacement, '')
|
|
return (pattern, replacement)
|
|
except re.error as e:
|
|
Cmd.Backup()
|
|
usageErrorExit(f'{Cmd.OB_RE_SUBSTITUTION} {Msg.ERROR}: {e}')
|
|
|
|
def getREPatternSubstitution(flags=0):
|
|
pattern = getREPattern(flags)
|
|
replacement = getString(Cmd.OB_RE_SUBSTITUTION, minLen=0)
|
|
return validateREPatternSubstitution(pattern, replacement)
|
|
|
|
def getSheetEntity(allowBlankSheet):
|
|
if Cmd.ArgumentsRemaining():
|
|
sheet = Cmd.Current()
|
|
if sheet or allowBlankSheet:
|
|
cg = UID_PATTERN.match(sheet)
|
|
if cg:
|
|
if cg.group(1).isdigit():
|
|
Cmd.Advance()
|
|
return {'sheetType': Ent.SHEET_ID, 'sheetValue': int(cg.group(1)), 'sheetId': int(cg.group(1)), 'sheetTitle': ''}
|
|
else:
|
|
Cmd.Advance()
|
|
return {'sheetType': Ent.SHEET, 'sheetValue': sheet, 'sheetId': None, 'sheetTitle': sheet}
|
|
missingArgumentExit(Cmd.OB_SHEET_ENTITY)
|
|
|
|
def getSheetIdFromSheetEntity(spreadsheet, sheetEntity):
|
|
if sheetEntity['sheetType'] == Ent.SHEET_ID:
|
|
for sheet in spreadsheet['sheets']:
|
|
if sheetEntity['sheetId'] == sheet['properties']['sheetId']:
|
|
return sheet['properties']['sheetId']
|
|
else:
|
|
sheetTitleLower = sheetEntity['sheetTitle'].lower()
|
|
for sheet in spreadsheet['sheets']:
|
|
if sheetTitleLower == sheet['properties']['title'].lower():
|
|
return sheet['properties']['sheetId']
|
|
return None
|
|
|
|
def protectedSheetId(spreadsheet, sheetId):
|
|
for sheet in spreadsheet['sheets']:
|
|
for protectedRange in sheet.get('protectedRanges', []):
|
|
if protectedRange.get('range', {}).get('sheetId', -1) == sheetId and not protectedRange.get('requestingUserCanEdit', False):
|
|
return True
|
|
return False
|
|
|
|
def getString(item, checkBlank=False, optional=False, minLen=1, maxLen=None):
|
|
if Cmd.ArgumentsRemaining():
|
|
argstr = Cmd.Current()
|
|
if argstr:
|
|
if checkBlank:
|
|
if argstr.isspace():
|
|
blankArgumentExit(item)
|
|
if (len(argstr) >= minLen) and ((maxLen is None) or (len(argstr) <= maxLen)):
|
|
Cmd.Advance()
|
|
return argstr
|
|
invalidArgumentExit(f'{integerLimits(minLen, maxLen, Msg.STRING_LENGTH)} for {item}')
|
|
if optional or (minLen == 0):
|
|
Cmd.Advance()
|
|
return ''
|
|
emptyArgumentExit(item)
|
|
elif optional:
|
|
return ''
|
|
missingArgumentExit(item)
|
|
|
|
def escapeCRsNLs(value):
|
|
return value.replace('\r', '\\r').replace('\n', '\\n')
|
|
|
|
def unescapeCRsNLs(value):
|
|
return value.replace('\\r', '\r').replace('\\n', '\n')
|
|
|
|
def getStringWithCRsNLs():
|
|
return unescapeCRsNLs(getString(Cmd.OB_STRING, minLen=0))
|
|
|
|
def getStringReturnInList(item):
|
|
argstr = getString(item, minLen=0).strip()
|
|
if argstr:
|
|
return [argstr]
|
|
return []
|
|
|
|
SORF_SIG_ARGUMENTS = {'signature', 'sig', 'textsig', 'htmlsig'}
|
|
SORF_MSG_ARGUMENTS = {'message', 'textmessage', 'htmlmessage'}
|
|
SORF_FILE_ARGUMENTS = {'file', 'textfile', 'htmlfile', 'gdoc', 'ghtml', 'gcsdoc', 'gcshtml'}
|
|
SORF_HTML_ARGUMENTS = {'htmlsig', 'htmlmessage', 'htmlfile', 'ghtml', 'gcshtml'}
|
|
SORF_TEXT_ARGUMENTS = {'text', 'textfile', 'gdoc', 'gcsdoc'}
|
|
SORF_SIG_FILE_ARGUMENTS = SORF_SIG_ARGUMENTS.union(SORF_FILE_ARGUMENTS)
|
|
SORF_MSG_FILE_ARGUMENTS = SORF_MSG_ARGUMENTS.union(SORF_FILE_ARGUMENTS)
|
|
|
|
def getStringOrFile(myarg, minLen=0, unescapeCRLF=False):
|
|
if myarg in SORF_SIG_ARGUMENTS:
|
|
if checkArgumentPresent(SORF_FILE_ARGUMENTS):
|
|
myarg = Cmd.Previous().strip().lower().replace('_', '')
|
|
html = myarg in SORF_HTML_ARGUMENTS
|
|
if myarg in SORF_FILE_ARGUMENTS:
|
|
if myarg in {'file', 'textfile', 'htmlfile'}:
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
encoding = getCharSet()
|
|
return (readFile(filename, encoding=encoding), encoding, html)
|
|
if myarg in {'gdoc', 'ghtml'}:
|
|
f = getGDocData(myarg)
|
|
data = f.read()
|
|
f.close()
|
|
return (data, UTF8, html)
|
|
return (getStorageFileData(myarg), UTF8, html)
|
|
if not unescapeCRLF:
|
|
return (getString(Cmd.OB_STRING, minLen=minLen), UTF8, html)
|
|
return (unescapeCRsNLs(getString(Cmd.OB_STRING, minLen=minLen)), UTF8, html)
|
|
|
|
def getStringWithCRsNLsOrFile():
|
|
if checkArgumentPresent(SORF_FILE_ARGUMENTS):
|
|
return getStringOrFile(Cmd.Previous().strip().lower().replace('_', ''), minLen=0)
|
|
return (unescapeCRsNLs(getString(Cmd.OB_STRING, minLen=0)), UTF8, False)
|
|
|
|
def todaysDate():
|
|
return datetime.datetime(GM.Globals[GM.DATETIME_NOW].year, GM.Globals[GM.DATETIME_NOW].month, GM.Globals[GM.DATETIME_NOW].day,
|
|
tzinfo=GC.Values[GC.TIMEZONE])
|
|
|
|
def todaysTime():
|
|
return datetime.datetime(GM.Globals[GM.DATETIME_NOW].year, GM.Globals[GM.DATETIME_NOW].month, GM.Globals[GM.DATETIME_NOW].day,
|
|
GM.Globals[GM.DATETIME_NOW].hour, GM.Globals[GM.DATETIME_NOW].minute,
|
|
tzinfo=GC.Values[GC.TIMEZONE])
|
|
|
|
def getDelta(argstr, pattern):
|
|
if argstr == 'NOW':
|
|
return todaysTime()
|
|
if argstr == 'TODAY':
|
|
return todaysDate()
|
|
tg = pattern.match(argstr.lower())
|
|
if tg is None:
|
|
return None
|
|
sign = tg.group(1)
|
|
delta = int(tg.group(2))
|
|
unit = tg.group(3)
|
|
if unit == 'y':
|
|
deltaTime = datetime.timedelta(days=delta*365)
|
|
elif unit == 'w':
|
|
deltaTime = datetime.timedelta(weeks=delta)
|
|
elif unit == 'd':
|
|
deltaTime = datetime.timedelta(days=delta)
|
|
elif unit == 'h':
|
|
deltaTime = datetime.timedelta(hours=delta)
|
|
elif unit == 'm':
|
|
deltaTime = datetime.timedelta(minutes=delta)
|
|
baseTime = todaysDate()
|
|
if unit in {'h', 'm'}:
|
|
baseTime = baseTime+datetime.timedelta(hours=GM.Globals[GM.DATETIME_NOW].hour, minutes=GM.Globals[GM.DATETIME_NOW].minute)
|
|
if sign == '-':
|
|
return baseTime-deltaTime
|
|
return baseTime+deltaTime
|
|
|
|
DELTA_DATE_PATTERN = re.compile(r'^([+-])(\d+)([dwy])$')
|
|
DELTA_DATE_FORMAT_REQUIRED = '(+|-)<Number>(d|w|y)'
|
|
def getDeltaDate(argstr):
|
|
deltaDate = getDelta(argstr, DELTA_DATE_PATTERN)
|
|
if deltaDate is None:
|
|
invalidArgumentExit(DELTA_DATE_FORMAT_REQUIRED)
|
|
return deltaDate
|
|
|
|
DELTA_TIME_PATTERN = re.compile(r'^([+-])(\d+)([mhdwy])$')
|
|
DELTA_TIME_FORMAT_REQUIRED = '(+|-)<Number>(m|h|d|w|y)'
|
|
|
|
def getDeltaTime(argstr):
|
|
deltaTime = getDelta(argstr, DELTA_TIME_PATTERN)
|
|
if deltaTime is None:
|
|
invalidArgumentExit(DELTA_TIME_FORMAT_REQUIRED)
|
|
return deltaTime
|
|
|
|
YYYYMMDD_FORMAT = '%Y-%m-%d'
|
|
YYYYMMDD_FORMAT_REQUIRED = 'yyyy-mm-dd'
|
|
|
|
TODAY_NOW = {'TODAY', 'NOW'}
|
|
PLUS_MINUS = {'+', '-'}
|
|
|
|
def getYYYYMMDD(minLen=1, returnTimeStamp=False, returnDateTime=False, alternateValue=None):
|
|
if Cmd.ArgumentsRemaining():
|
|
argstr = Cmd.Current().strip().upper()
|
|
if argstr:
|
|
if alternateValue is not None and argstr == alternateValue.upper():
|
|
Cmd.Advance()
|
|
return None
|
|
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
|
|
if argstr == 'NOW':
|
|
argstr = 'TODAY'
|
|
argstr = getDeltaDate(argstr).strftime(YYYYMMDD_FORMAT)
|
|
elif argstr == 'NEVER':
|
|
argstr = NEVER_DATE
|
|
try:
|
|
dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
|
|
Cmd.Advance()
|
|
if returnTimeStamp:
|
|
return time.mktime(dateTime.timetuple())*1000
|
|
if returnDateTime:
|
|
return dateTime
|
|
return argstr
|
|
except ValueError:
|
|
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
|
|
elif minLen == 0:
|
|
Cmd.Advance()
|
|
return ''
|
|
missingArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
|
|
|
|
HHMM_FORMAT = '%H:%M'
|
|
HHMM_FORMAT_REQUIRED = 'hh:mm'
|
|
|
|
def getHHMM():
|
|
if Cmd.ArgumentsRemaining():
|
|
argstr = Cmd.Current().strip().upper()
|
|
if argstr:
|
|
try:
|
|
datetime.datetime.strptime(argstr, HHMM_FORMAT)
|
|
Cmd.Advance()
|
|
return argstr
|
|
except ValueError:
|
|
invalidArgumentExit(HHMM_FORMAT_REQUIRED)
|
|
missingArgumentExit(HHMM_FORMAT_REQUIRED)
|
|
|
|
YYYYMMDD_HHMM_FORMAT = '%Y-%m-%d %H:%M'
|
|
YYYYMMDD_HHMM_FORMAT_REQUIRED = 'yyyy-mm-dd hh:mm'
|
|
|
|
def getYYYYMMDD_HHMM():
|
|
if Cmd.ArgumentsRemaining():
|
|
argstr = Cmd.Current().strip().upper()
|
|
if argstr:
|
|
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
|
|
argstr = getDeltaTime(argstr).strftime(YYYYMMDD_HHMM_FORMAT)
|
|
elif argstr == 'NEVER':
|
|
argstr = NEVER_DATETIME
|
|
argstr = argstr.replace('T', ' ')
|
|
try:
|
|
datetime.datetime.strptime(argstr, YYYYMMDD_HHMM_FORMAT)
|
|
Cmd.Advance()
|
|
return argstr
|
|
except ValueError:
|
|
invalidArgumentExit(YYYYMMDD_HHMM_FORMAT_REQUIRED)
|
|
missingArgumentExit(YYYYMMDD_HHMM_FORMAT_REQUIRED)
|
|
|
|
YYYYMMDDTHHMMSSZ_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
|
|
YYYYMMDD_PATTERN = re.compile(r'^[0-9]{4}-[0-9]{2}-[0-9]{2}$')
|
|
|
|
def getDateOrDeltaFromNow(returnDateTime=False):
|
|
if Cmd.ArgumentsRemaining():
|
|
argstr = Cmd.Current().strip().upper()
|
|
if argstr:
|
|
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
|
|
if argstr == 'NOW':
|
|
argstr = 'TODAY'
|
|
argDate = getDeltaDate(argstr)
|
|
elif argstr == 'NEVER':
|
|
argDate = datetime.datetime.strptime(NEVER_DATE, YYYYMMDD_FORMAT)
|
|
elif YYYYMMDD_PATTERN.match(argstr):
|
|
try:
|
|
argDate = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
|
|
except ValueError:
|
|
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
|
|
else:
|
|
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
|
|
Cmd.Advance()
|
|
if not returnDateTime:
|
|
return argDate.strftime(YYYYMMDD_FORMAT)
|
|
return (datetime.datetime(argDate.year, argDate.month, argDate.day, tzinfo=GC.Values[GC.TIMEZONE]),
|
|
GC.Values[GC.TIMEZONE], argDate.strftime(YYYYMMDD_FORMAT))
|
|
missingArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
|
|
|
|
YYYYMMDDTHHMMSS_FORMAT_REQUIRED = 'yyyy-mm-ddThh:mm:ss[.fff](Z|(+|-(hh:mm)))'
|
|
TIMEZONE_FORMAT_REQUIRED = 'Z|(+|-(hh:mm))'
|
|
|
|
def getTimeOrDeltaFromNow(returnDateTime=False):
|
|
if Cmd.ArgumentsRemaining():
|
|
argstr = Cmd.Current().strip().upper()
|
|
if argstr:
|
|
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
|
|
argstr = ISOformatTimeStamp(getDeltaTime(argstr))
|
|
elif argstr == 'NEVER':
|
|
argstr = NEVER_TIME
|
|
elif YYYYMMDD_PATTERN.match(argstr):
|
|
try:
|
|
dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
|
|
except ValueError:
|
|
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
|
|
try:
|
|
argstr = ISOformatTimeStamp(dateTime.replace(tzinfo=GC.Values[GC.TIMEZONE]))
|
|
except OverflowError:
|
|
pass
|
|
try:
|
|
fullDateTime, tz = iso8601.parse_date(argstr)
|
|
Cmd.Advance()
|
|
if not returnDateTime:
|
|
return argstr.replace(' ', 'T')
|
|
return (fullDateTime, tz, argstr.replace(' ', 'T'))
|
|
except (iso8601.ParseError, OverflowError):
|
|
pass
|
|
invalidArgumentExit(YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
|
|
missingArgumentExit(YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
|
|
|
|
def getRowFilterDateOrDeltaFromNow(argstr):
|
|
argstr = argstr.strip().upper()
|
|
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
|
|
if argstr == 'NOW':
|
|
argstr = 'TODAY'
|
|
deltaDate = getDelta(argstr, DELTA_DATE_PATTERN)
|
|
if deltaDate is None:
|
|
return (False, DELTA_DATE_FORMAT_REQUIRED)
|
|
argstr = ISOformatTimeStamp(deltaDate.replace(tzinfo=iso8601.UTC))
|
|
elif argstr == 'NEVER' or YYYYMMDD_PATTERN.match(argstr):
|
|
if argstr == 'NEVER':
|
|
argstr = NEVER_DATE
|
|
try:
|
|
dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
|
|
except ValueError:
|
|
return (False, YYYYMMDD_FORMAT_REQUIRED)
|
|
argstr = ISOformatTimeStamp(dateTime.replace(tzinfo=iso8601.UTC))
|
|
try:
|
|
iso8601.parse_date(argstr)
|
|
return (True, argstr.replace(' ', 'T'))
|
|
except (iso8601.ParseError, OverflowError):
|
|
return (False, YYYYMMDD_FORMAT_REQUIRED)
|
|
|
|
def getRowFilterTimeOrDeltaFromNow(argstr):
|
|
argstr = argstr.strip().upper()
|
|
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
|
|
deltaTime = getDelta(argstr, DELTA_TIME_PATTERN)
|
|
if deltaTime is None:
|
|
return (False, DELTA_TIME_FORMAT_REQUIRED)
|
|
argstr = ISOformatTimeStamp(deltaTime)
|
|
elif argstr == 'NEVER':
|
|
argstr = NEVER_TIME
|
|
elif YYYYMMDD_PATTERN.match(argstr):
|
|
try:
|
|
dateTime = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
|
|
except ValueError:
|
|
return (False, YYYYMMDD_FORMAT_REQUIRED)
|
|
argstr = ISOformatTimeStamp(dateTime.replace(tzinfo=GC.Values[GC.TIMEZONE]))
|
|
try:
|
|
iso8601.parse_date(argstr)
|
|
return (True, argstr.replace(' ', 'T'))
|
|
except (iso8601.ParseError, OverflowError):
|
|
return (False, YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
|
|
|
|
def mapQueryRelativeTimes(query, keywords):
|
|
QUOTES = '\'"'
|
|
for kw in keywords:
|
|
pattern = re.compile(rf'({kw})\s*([<>]=?|=|!=)\s*[{QUOTES}]?(now|today|[+-]\d+[mhdwy])', re.IGNORECASE)
|
|
pos = 0
|
|
while True:
|
|
mg = pattern.search(query, pos)
|
|
if not mg:
|
|
break
|
|
if mg.groups()[2] is not None:
|
|
deltaTime = getDelta(mg.group(3).upper(), DELTA_TIME_PATTERN)
|
|
if deltaTime:
|
|
query = query[:mg.start(3)]+ISOformatTimeStamp(deltaTime)+query[mg.end(3):]
|
|
pos = mg.end()
|
|
return query
|
|
|
|
class StartEndTime():
|
|
def __init__(self, startkw='starttime', endkw='endtime', mode='time'):
|
|
self.startTime = self.endTime = self.startDateTime = self.endDateTime = None
|
|
self._startkw = startkw
|
|
self._endkw = endkw
|
|
self._getValueOrDeltaFromNow = getTimeOrDeltaFromNow if mode == 'time' else getDateOrDeltaFromNow
|
|
|
|
def Get(self, myarg):
|
|
if myarg in {'start', self._startkw}:
|
|
self.startDateTime, _, self.startTime = self._getValueOrDeltaFromNow(True)
|
|
elif myarg in {'end', self._endkw}:
|
|
self.endDateTime, _, self.endTime = self._getValueOrDeltaFromNow(True)
|
|
elif myarg == 'yesterday':
|
|
currDate = todaysDate()
|
|
self.startDateTime = currDate+datetime.timedelta(days=-1)
|
|
self.startTime = ISOformatTimeStamp(self.startDateTime)
|
|
self.endDateTime = currDate+datetime.timedelta(seconds=-1)
|
|
self.endTime = ISOformatTimeStamp(self.endDateTime)
|
|
elif myarg == 'today':
|
|
currDate = todaysDate()
|
|
self.startDateTime = currDate
|
|
self.startTime = ISOformatTimeStamp(self.startDateTime)
|
|
elif myarg == 'range':
|
|
self.startDateTime, _, self.startTime = self._getValueOrDeltaFromNow(True)
|
|
self.endDateTime, _, self.endTime = self._getValueOrDeltaFromNow(True)
|
|
else: #elif myarg in {'thismonth', 'previousmonths'}
|
|
if myarg == 'thismonth':
|
|
firstMonth = 0
|
|
else:
|
|
firstMonth = getInteger(minVal=1, maxVal=6)
|
|
currDate = todaysDate()
|
|
self.startDateTime = currDate+relativedelta(months=-firstMonth, day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
self.startTime = ISOformatTimeStamp(self.startDateTime)
|
|
if myarg == 'thismonth':
|
|
self.endDateTime = todaysTime()
|
|
else:
|
|
self.endDateTime = currDate+relativedelta(day=1, hour=23, minute=59, second=59, microsecond=0)+relativedelta(days=-1)
|
|
self.endTime = ISOformatTimeStamp(self.endDateTime)
|
|
if self.startDateTime and self.endDateTime and self.endDateTime < self.startDateTime:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_DATE_TIME_RANGE.format(self._endkw, self.endTime, self._startkw, self.startTime))
|
|
|
|
EVENTID_PATTERN = re.compile(r'^[a-v0-9]{5,1024}$')
|
|
EVENTID_FORMAT_REQUIRED = '[a-v0-9]{5,1024}'
|
|
|
|
def getEventID():
|
|
if Cmd.ArgumentsRemaining():
|
|
tg = EVENTID_PATTERN.match(Cmd.Current().strip())
|
|
if tg:
|
|
Cmd.Advance()
|
|
return tg.group(0)
|
|
invalidArgumentExit(EVENTID_FORMAT_REQUIRED)
|
|
missingArgumentExit(EVENTID_FORMAT_REQUIRED)
|
|
|
|
EVENT_TIME_FORMAT_REQUIRED = 'allday yyyy-mm-dd | '+YYYYMMDDTHHMMSS_FORMAT_REQUIRED
|
|
|
|
def getEventTime():
|
|
if Cmd.ArgumentsRemaining():
|
|
if Cmd.Current().strip().lower() == 'allday':
|
|
Cmd.Advance()
|
|
return {'date': getYYYYMMDD()}
|
|
return {'dateTime': getTimeOrDeltaFromNow()}
|
|
missingArgumentExit(EVENT_TIME_FORMAT_REQUIRED)
|
|
|
|
AGE_TIME_PATTERN = re.compile(r'^(\d+)([mhdw])$')
|
|
AGE_TIME_FORMAT_REQUIRED = '<Number>(m|h|d|w)'
|
|
|
|
def getAgeTime():
|
|
if Cmd.ArgumentsRemaining():
|
|
tg = AGE_TIME_PATTERN.match(Cmd.Current().strip().lower())
|
|
if tg:
|
|
age = int(tg.group(1))
|
|
age_unit = tg.group(2)
|
|
now = int(time.time())
|
|
if age_unit == 'm':
|
|
age = now-(age*SECONDS_PER_MINUTE)
|
|
elif age_unit == 'h':
|
|
age = now-(age*SECONDS_PER_HOUR)
|
|
elif age_unit == 'd':
|
|
age = now-(age*SECONDS_PER_DAY)
|
|
else: # age_unit == 'w':
|
|
age = now-(age*SECONDS_PER_WEEK)
|
|
Cmd.Advance()
|
|
return age*1000
|
|
invalidArgumentExit(AGE_TIME_FORMAT_REQUIRED)
|
|
missingArgumentExit(AGE_TIME_FORMAT_REQUIRED)
|
|
|
|
CALENDAR_REMINDER_METHODS = ['email', 'popup']
|
|
CALENDAR_REMINDER_MAX_MINUTES = 40320
|
|
|
|
def getCalendarReminder(allowClearNone=False):
|
|
methods = CALENDAR_REMINDER_METHODS[:]
|
|
if allowClearNone:
|
|
methods += Cmd.CLEAR_NONE_ARGUMENT
|
|
if Cmd.ArgumentsRemaining():
|
|
method = Cmd.Current().strip()
|
|
if not method.isdigit():
|
|
method = getChoice(methods)
|
|
minutes = getInteger(minVal=0, maxVal=CALENDAR_REMINDER_MAX_MINUTES)
|
|
else:
|
|
minutes = getInteger(minVal=0, maxVal=CALENDAR_REMINDER_MAX_MINUTES)
|
|
method = getChoice(methods)
|
|
return {'method': method, 'minutes': minutes}
|
|
missingChoiceExit(methods)
|
|
|
|
def getCharacter():
|
|
if Cmd.ArgumentsRemaining():
|
|
argstr = codecs.escape_decode(bytes(Cmd.Current(), UTF8))[0].decode(UTF8)
|
|
if argstr:
|
|
if len(argstr) == 1:
|
|
Cmd.Advance()
|
|
return argstr
|
|
invalidArgumentExit(f'{integerLimits(1, 1, Msg.STRING_LENGTH)} for {Cmd.OB_CHARACTER}')
|
|
emptyArgumentExit(Cmd.OB_CHARACTER)
|
|
missingArgumentExit(Cmd.OB_CHARACTER)
|
|
|
|
def getDelimiter():
|
|
if not checkArgumentPresent('delimiter'):
|
|
return None
|
|
return getCharacter()
|
|
|
|
def getJSON(deleteFields):
|
|
if not checkArgumentPresent('file'):
|
|
encoding = getCharSet()
|
|
if not Cmd.ArgumentsRemaining():
|
|
missingArgumentExit(Cmd.OB_JSON_DATA)
|
|
argstr = Cmd.Current()
|
|
# argstr = Cmd.Current().replace(r'\\"', r'\"')
|
|
Cmd.Advance()
|
|
try:
|
|
if encoding == UTF8:
|
|
jsonData = json.loads(argstr)
|
|
else:
|
|
jsonData = json.loads(argstr.encode(encoding).decode(UTF8))
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
Cmd.Backup()
|
|
usageErrorExit(f'{str(e)}: {argstr if encoding == UTF8 else argstr.encode(encoding).decode(UTF8)}')
|
|
else:
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
encoding = getCharSet()
|
|
try:
|
|
jsonData = json.loads(readFile(filename, encoding=encoding))
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.JSON_ERROR.format(str(e), filename))
|
|
for field in deleteFields:
|
|
jsonData.pop(field, None)
|
|
return jsonData
|
|
|
|
def getMatchSkipFields(fieldNames):
|
|
matchFields = {}
|
|
skipFields = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'matchfield', 'skipfield'}:
|
|
matchField = getString(Cmd.OB_FIELD_NAME).strip('~')
|
|
if (not matchField) or (matchField not in fieldNames):
|
|
csvFieldErrorExit(matchField, fieldNames, backupArg=True)
|
|
if myarg == 'matchfield':
|
|
matchFields[matchField] = getREPattern()
|
|
else:
|
|
skipFields[matchField] = getREPattern()
|
|
else:
|
|
Cmd.Backup()
|
|
break
|
|
return (matchFields, skipFields)
|
|
|
|
def checkMatchSkipFields(row, fieldnames, matchFields, skipFields):
|
|
for matchField, matchPattern in iter(matchFields.items()):
|
|
if (matchField not in row) or not matchPattern.search(row[matchField]):
|
|
return False
|
|
for skipField, matchPattern in iter(skipFields.items()):
|
|
if (skipField in row) and matchPattern.search(row[skipField]):
|
|
return False
|
|
if fieldnames and (GC.Values[GC.CSV_INPUT_ROW_FILTER] or GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER]):
|
|
return RowFilterMatch(row, fieldnames,
|
|
GC.Values[GC.CSV_INPUT_ROW_FILTER], GC.Values[GC.CSV_INPUT_ROW_FILTER_MODE],
|
|
GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER], GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER_MODE])
|
|
return True
|
|
|
|
def checkSubkeyField():
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.NO_CSV_FILE_SUBKEYS_SAVED)
|
|
chkSubkeyField = getString(Cmd.OB_FIELD_NAME, checkBlank=True)
|
|
if chkSubkeyField != GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.SUBKEY_FIELD_MISMATCH.format(chkSubkeyField, GM.Globals[GM.CSV_SUBKEY_FIELD]))
|
|
|
|
def checkDataField():
|
|
if not GM.Globals[GM.CSV_DATA_FIELD]:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.NO_CSV_FILE_DATA_SAVED)
|
|
chkDataField = getString(Cmd.OB_FIELD_NAME, checkBlank=True)
|
|
if chkDataField != GM.Globals[GM.CSV_DATA_FIELD]:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.DATA_FIELD_MISMATCH.format(chkDataField, GM.Globals[GM.CSV_DATA_FIELD]))
|
|
|
|
MAX_MESSAGE_BYTES_PATTERN = re.compile(r'^(\d+)([mkb]?)$')
|
|
MAX_MESSAGE_BYTES_FORMAT_REQUIRED = '<Number>[m|k|b]'
|
|
|
|
def getMaxMessageBytes(oneKiloBytes, oneMegaBytes):
|
|
if Cmd.ArgumentsRemaining():
|
|
tg = MAX_MESSAGE_BYTES_PATTERN.match(Cmd.Current().strip().lower())
|
|
if tg:
|
|
mmb = int(tg.group(1))
|
|
mmb_unit = tg.group(2)
|
|
if mmb_unit == 'm':
|
|
mmb *= oneMegaBytes
|
|
elif mmb_unit == 'k':
|
|
mmb *= oneKiloBytes
|
|
Cmd.Advance()
|
|
return mmb
|
|
invalidArgumentExit(MAX_MESSAGE_BYTES_FORMAT_REQUIRED)
|
|
missingArgumentExit(MAX_MESSAGE_BYTES_FORMAT_REQUIRED)
|
|
|
|
# Get domain from email address
|
|
def getEmailAddressDomain(emailAddress):
|
|
atLoc = emailAddress.find('@')
|
|
if atLoc == -1:
|
|
return GC.Values[GC.DOMAIN]
|
|
return emailAddress[atLoc+1:].lower()
|
|
|
|
# Get user name from email address
|
|
def getEmailAddressUsername(emailAddress):
|
|
atLoc = emailAddress.find('@')
|
|
if atLoc == -1:
|
|
return emailAddress.lower()
|
|
return emailAddress[:atLoc].lower()
|
|
|
|
# Split email address into user and domain
|
|
def splitEmailAddress(emailAddress):
|
|
atLoc = emailAddress.find('@')
|
|
if atLoc == -1:
|
|
return (emailAddress.lower(), GC.Values[GC.DOMAIN])
|
|
return (emailAddress[:atLoc].lower(), emailAddress[atLoc+1:].lower())
|
|
|
|
def formatFileSize(fileSize):
|
|
if fileSize == 0:
|
|
return '0kb'
|
|
if fileSize < ONE_KILO_10_BYTES:
|
|
return '1kb'
|
|
if fileSize < ONE_MEGA_10_BYTES:
|
|
return f'{fileSize//ONE_KILO_10_BYTES}kb'
|
|
if fileSize < ONE_GIGA_10_BYTES:
|
|
return f'{fileSize//ONE_MEGA_10_BYTES}mb'
|
|
return f'{fileSize//ONE_GIGA_10_BYTES}gb'
|
|
|
|
def formatLocalTime(dateTimeStr):
|
|
if dateTimeStr in {NEVER_TIME, NEVER_TIME_NOMS}:
|
|
return GC.Values[GC.NEVER_TIME]
|
|
try:
|
|
timestamp, _ = iso8601.parse_date(dateTimeStr)
|
|
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
|
|
if GM.Globals[GM.CONVERT_TO_LOCAL_TIME]:
|
|
return ISOformatTimeStamp(timestamp.astimezone(GC.Values[GC.TIMEZONE]))
|
|
return timestamp.strftime(YYYYMMDDTHHMMSSZ_FORMAT)
|
|
if GM.Globals[GM.CONVERT_TO_LOCAL_TIME]:
|
|
return timestamp.astimezone(GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
|
|
return timestamp.strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
|
|
except (iso8601.ParseError, OverflowError):
|
|
return dateTimeStr
|
|
|
|
def formatLocalSecondsTimestamp(timestamp):
|
|
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
|
|
return ISOformatTimeStamp(datetime.datetime.fromtimestamp(int(timestamp), GC.Values[GC.TIMEZONE]))
|
|
return datetime.datetime.fromtimestamp(int(timestamp), GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
|
|
|
|
def formatLocalTimestamp(timestamp):
|
|
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
|
|
return ISOformatTimeStamp(datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]))
|
|
return datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
|
|
|
|
def formatLocalTimestampUTC(timestamp):
|
|
return ISOformatTimeStamp(datetime.datetime.fromtimestamp(int(timestamp)//1000, iso8601.UTC))
|
|
|
|
def formatLocalDatestamp(timestamp):
|
|
try:
|
|
if not GC.Values[GC.OUTPUT_DATEFORMAT]:
|
|
return datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(YYYYMMDD_FORMAT)
|
|
return datetime.datetime.fromtimestamp(int(timestamp)//1000, GC.Values[GC.TIMEZONE]).strftime(GC.Values[GC.OUTPUT_DATEFORMAT])
|
|
except OverflowError:
|
|
return NEVER_DATE
|
|
|
|
def formatMaxMessageBytes(maxMessageBytes, oneKiloBytes, oneMegaBytes):
|
|
if maxMessageBytes < oneKiloBytes:
|
|
return maxMessageBytes
|
|
if maxMessageBytes < oneMegaBytes:
|
|
return f'{maxMessageBytes//oneKiloBytes}K'
|
|
return f'{maxMessageBytes//oneMegaBytes}M'
|
|
|
|
def formatMilliSeconds(millis):
|
|
seconds, millis = divmod(millis, 1000)
|
|
minutes, seconds = divmod(seconds, 60)
|
|
hours, minutes = divmod(minutes, 60)
|
|
return f'{hours:02d}:{minutes:02d}:{seconds:02d}'
|
|
|
|
def getPhraseDNEorSNA(email):
|
|
return Msg.DOES_NOT_EXIST if getEmailAddressDomain(email) == GC.Values[GC.DOMAIN] else Msg.SERVICE_NOT_APPLICABLE
|
|
|
|
def formatHTTPError(http_status, reason, message):
|
|
return f'{http_status}: {reason} - {message}'
|
|
|
|
def getHTTPError(responses, http_status, reason, message):
|
|
if reason in responses:
|
|
return responses[reason]
|
|
return formatHTTPError(http_status, reason, message)
|
|
|
|
# Warnings
|
|
def badRequestWarning(entityType, itemType, itemValue):
|
|
printWarningMessage(BAD_REQUEST_RC,
|
|
f'{Msg.GOT} 0 {Ent.Plural(entityType)}: {Msg.INVALID} {Ent.Singular(itemType)} - {itemValue}')
|
|
|
|
def emptyQuery(query, entityType):
|
|
return f'{Ent.Singular(Ent.QUERY)} ({query}) {Msg.NO_ENTITIES_FOUND.format(Ent.Plural(entityType))}'
|
|
|
|
def invalidQuery(query):
|
|
return f'{Ent.Singular(Ent.QUERY)} ({query}) {Msg.INVALID}'
|
|
|
|
def invalidMember(query):
|
|
if query:
|
|
badRequestWarning(Ent.GROUP, Ent.QUERY, invalidQuery(query))
|
|
return True
|
|
return False
|
|
|
|
def invalidUserSchema(schema):
|
|
if isinstance(schema, list):
|
|
return f'{Ent.Singular(Ent.USER_SCHEMA)} ({",".join(schema)}) {Msg.INVALID}'
|
|
return f'{Ent.Singular(Ent.USER_SCHEMA)} {schema}) {Msg.INVALID}'
|
|
|
|
def userServiceNotEnabledWarning(entityName, service, i=0, count=0):
|
|
setSysExitRC(SERVICE_NOT_APPLICABLE_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(Ent.USER), entityName, Msg.SERVICE_NOT_ENABLED.format(service)],
|
|
currentCountNL(i, count)))
|
|
|
|
def userAlertsServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'Alerts', i, count)
|
|
|
|
def userAnalyticsServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'Alerts', i, count)
|
|
|
|
def userCalServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'Calendar', i, count)
|
|
|
|
def userChatServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'Chat', i, count)
|
|
|
|
def userContactDelegateServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'Contact Delegate', i, count)
|
|
|
|
def userDriveServiceNotEnabledWarning(user, errMessage, i=0, count=0):
|
|
# if errMessage.find('Drive apps') == -1 and errMessage.find('Active session is invalid') == -1:
|
|
# entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
|
if errMessage.find('Drive apps') >= 0 or errMessage.find('Active session is invalid') >= 0:
|
|
userServiceNotEnabledWarning(user, 'Drive', i, count)
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER, user], errMessage, i, count)
|
|
|
|
def userKeepServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'Keep', i, count)
|
|
|
|
def userGmailServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'Gmail', i, count)
|
|
|
|
def userLookerStudioServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'Looker Studio', i, count)
|
|
|
|
def userPeopleServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'People', i, count)
|
|
|
|
def userTasksServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'Tasks', i, count)
|
|
|
|
def userYouTubeServiceNotEnabledWarning(entityName, i=0, count=0):
|
|
userServiceNotEnabledWarning(entityName, 'YouTube', i, count)
|
|
|
|
def entityServiceNotApplicableWarning(entityType, entityName, i=0, count=0):
|
|
setSysExitRC(SERVICE_NOT_APPLICABLE_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(entityType), entityName, Msg.SERVICE_NOT_APPLICABLE],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityDoesNotExistWarning(entityType, entityName, i=0, count=0):
|
|
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(entityType), entityName, Msg.DOES_NOT_EXIST],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityListDoesNotExistWarning(entityValueList, i=0, count=0):
|
|
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Msg.DOES_NOT_EXIST],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityUnknownWarning(entityType, entityName, i=0, count=0):
|
|
domain = getEmailAddressDomain(entityName)
|
|
if (domain.endswith(GC.Values[GC.DOMAIN])) or (domain.endswith('google.com')):
|
|
entityDoesNotExistWarning(entityType, entityName, i, count)
|
|
else:
|
|
entityServiceNotApplicableWarning(entityType, entityName, i, count)
|
|
|
|
def entityOrEntityUnknownWarning(entity1Type, entity1Name, entity2Type, entity2Name, i=0, count=0):
|
|
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
[f'{Msg.EITHER} {Ent.Singular(entity1Type)}', entity1Name, getPhraseDNEorSNA(entity1Name), None,
|
|
f'{Msg.OR} {Ent.Singular(entity2Type)}', entity2Name, getPhraseDNEorSNA(entity2Name)],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityDoesNotHaveItemWarning(entityValueList, i=0, count=0):
|
|
setSysExitRC(ENTITY_DOES_NOT_EXIST_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Msg.DOES_NOT_EXIST],
|
|
currentCountNL(i, count)))
|
|
|
|
def duplicateAliasGroupUserWarning(cd, entityValueList, i=0, count=0):
|
|
email = entityValueList[1]
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=email, fields='id,primaryEmail')
|
|
if (result['primaryEmail'].lower() == email) or (result['id'] == email):
|
|
kvList = [Ent.USER, email]
|
|
else:
|
|
kvList = [Ent.USER_ALIAS, email, Ent.USER, result['primaryEmail']]
|
|
except (GAPI.userNotFound, GAPI.badRequest,
|
|
GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError, GAPI.systemError):
|
|
try:
|
|
result = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
|
|
groupKey=email, fields='id,email')
|
|
if (result['email'].lower() == email) or (result['id'] == email):
|
|
kvList = [Ent.GROUP, email]
|
|
else:
|
|
kvList = [Ent.GROUP_ALIAS, email, Ent.GROUP, result['email']]
|
|
except (GAPI.groupNotFound,
|
|
GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
|
|
kvList = [Ent.EMAIL, email]
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+
|
|
[Act.Failed(), Msg.DUPLICATE]+
|
|
Ent.FormatEntityValueList(kvList),
|
|
currentCountNL(i, count)))
|
|
setSysExitRC(ENTITY_DUPLICATE_RC)
|
|
return kvList[0]
|
|
|
|
def entityDuplicateWarning(entityValueList, i=0, count=0):
|
|
setSysExitRC(ENTITY_DUPLICATE_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.Failed(), Msg.DUPLICATE],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityActionFailedWarning(entityValueList, errMessage, i=0, count=0):
|
|
setSysExitRC(ACTION_FAILED_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.Failed(), errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityModifierItemValueListActionFailedWarning(entityValueList, modifier, infoTypeValueList, errMessage, i=0, count=0):
|
|
setSysExitRC(ACTION_FAILED_RC)
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList)+[Act.Failed(), errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityModifierActionFailedWarning(entityValueList, modifier, errMessage, i=0, count=0):
|
|
setSysExitRC(ACTION_FAILED_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', Act.Failed(), errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityModifierNewValueActionFailedWarning(entityValueList, modifier, newValue, errMessage, i=0, count=0):
|
|
setSysExitRC(ACTION_FAILED_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', newValue, Act.Failed(), errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityNumEntitiesActionFailedWarning(entityType, entityName, itemType, itemCount, errMessage, i=0, count=0):
|
|
setSysExitRC(ACTION_FAILED_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(entityType), entityName,
|
|
Ent.Choose(itemType, itemCount), itemCount,
|
|
Act.Failed(), errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityActionNotPerformedWarning(entityValueList, errMessage, i=0, count=0):
|
|
setSysExitRC(ACTION_NOT_PERFORMED_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityItemValueListActionNotPerformedWarning(entityValueList, infoTypeValueList, errMessage, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), '']+Ent.FormatEntityValueList(infoTypeValueList)+[errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityModifierItemValueListActionNotPerformedWarning(entityValueList, modifier, infoTypeValueList, errMessage, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.NotPerformed()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList)+[errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityNumEntitiesActionNotPerformedWarning(entityValueList, itemType, itemCount, errMessage, i=0, count=0):
|
|
setSysExitRC(ACTION_NOT_PERFORMED_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Ent.Choose(itemType, itemCount), itemCount, Act.NotPerformed(), errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityBadRequestWarning(entityValueList, errMessage, i=0, count=0):
|
|
setSysExitRC(BAD_REQUEST_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[ERROR, errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
# Getting ... utilities
|
|
def printGettingAllAccountEntities(entityType, query='', qualifier='', accountType=Ent.ACCOUNT):
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
if query:
|
|
Ent.SetGettingQuery(entityType, query)
|
|
elif qualifier:
|
|
Ent.SetGettingQualifier(entityType, qualifier)
|
|
else:
|
|
Ent.SetGetting(entityType)
|
|
writeStderr(f'{Msg.GETTING_ALL} {Ent.PluralGetting()}{Ent.GettingPreQualifier()}{Ent.MayTakeTime(accountType)}\n')
|
|
|
|
def printGotAccountEntities(count):
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
writeStderr(f'{Msg.GOT} {count} {Ent.ChooseGetting(count)}{Ent.GettingPostQualifier()}\n')
|
|
|
|
def setGettingAllEntityItemsForWhom(entityItem, forWhom, query='', qualifier=''):
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
if query:
|
|
Ent.SetGettingQuery(entityItem, query)
|
|
elif qualifier:
|
|
Ent.SetGettingQualifier(entityItem, qualifier)
|
|
else:
|
|
Ent.SetGetting(entityItem)
|
|
Ent.SetGettingForWhom(forWhom)
|
|
|
|
def printGettingAllEntityItemsForWhom(entityItem, forWhom, i=0, count=0, query='', qualifier='', entityType=None):
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
setGettingAllEntityItemsForWhom(entityItem, forWhom, query=query, qualifier=qualifier)
|
|
writeStderr(f'{Msg.GETTING_ALL} {Ent.PluralGetting()}{Ent.GettingPreQualifier()} {Msg.FOR} {forWhom}{Ent.MayTakeTime(entityType)}{currentCountNL(i, count)}')
|
|
|
|
def printGotEntityItemsForWhom(count):
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
writeStderr(f'{Msg.GOT} {count} {Ent.ChooseGetting(count)}{Ent.GettingPostQualifier()} {Msg.FOR} {Ent.GettingForWhom()}\n')
|
|
|
|
def printGettingEntityItem(entityType, entityItem, i=0, count=0):
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
writeStderr(f'{Msg.GETTING} {Ent.Singular(entityType)} {entityItem}{currentCountNL(i, count)}')
|
|
|
|
def printGettingEntityItemForWhom(entityItem, forWhom, i=0, count=0):
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
Ent.SetGetting(entityItem)
|
|
Ent.SetGettingForWhom(forWhom)
|
|
writeStderr(f'{Msg.GETTING} {Ent.PluralGetting()} {Msg.FOR} {forWhom}{currentCountNL(i, count)}')
|
|
|
|
def stderrEntityMessage(entityValueList, message, i=0, count=0):
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[message],
|
|
currentCountNL(i, count)))
|
|
|
|
FIRST_ITEM_MARKER = '%%first_item%%'
|
|
LAST_ITEM_MARKER = '%%last_item%%'
|
|
TOTAL_ITEMS_MARKER = '%%total_items%%'
|
|
|
|
def getPageMessage(showFirstLastItems=False, showDate=None):
|
|
if not GC.Values[GC.SHOW_GETTINGS]:
|
|
return None
|
|
pageMessage = f'{Msg.GOT} {TOTAL_ITEMS_MARKER} {{0}}'
|
|
if showDate:
|
|
pageMessage += f' on {showDate}'
|
|
if showFirstLastItems:
|
|
pageMessage += f': {FIRST_ITEM_MARKER} - {LAST_ITEM_MARKER}'
|
|
else:
|
|
pageMessage += '...'
|
|
if GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
|
|
pageMessage += '\n'
|
|
else:
|
|
GM.Globals[GM.LAST_GOT_MSG_LEN] = 0
|
|
return pageMessage
|
|
|
|
def getPageMessageForWhom(forWhom=None, showFirstLastItems=False, showDate=None, clearLastGotMsgLen=True):
|
|
if not GC.Values[GC.SHOW_GETTINGS]:
|
|
return None
|
|
if forWhom:
|
|
Ent.SetGettingForWhom(forWhom)
|
|
pageMessage = f'{Msg.GOT} {TOTAL_ITEMS_MARKER} {{0}}{Ent.GettingPostQualifier()} {Msg.FOR} {Ent.GettingForWhom()}'
|
|
if showDate:
|
|
pageMessage += f' on {showDate}'
|
|
if showFirstLastItems:
|
|
pageMessage += f': {FIRST_ITEM_MARKER} - {LAST_ITEM_MARKER}'
|
|
else:
|
|
pageMessage += '...'
|
|
if GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
|
|
pageMessage += '\n'
|
|
elif clearLastGotMsgLen:
|
|
GM.Globals[GM.LAST_GOT_MSG_LEN] = 0
|
|
return pageMessage
|
|
|
|
def printLine(message):
|
|
writeStdout(message+'\n')
|
|
|
|
def printBlankLine():
|
|
writeStdout('\n')
|
|
|
|
def printKeyValueList(kvList):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(), kvList, '\n'))
|
|
|
|
def printKeyValueListWithCount(kvList, i, count):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(), kvList, currentCountNL(i, count)))
|
|
|
|
def printKeyValueDict(kvDict):
|
|
for key, value in iter(kvDict.items()):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(), [key, value], '\n'))
|
|
|
|
def printKeyValueWithCRsNLs(key, value):
|
|
if value.find('\n') >= 0 or value.find('\r') >= 0:
|
|
if GC.Values[GC.SHOW_CONVERT_CR_NL]:
|
|
printKeyValueList([key, escapeCRsNLs(value)])
|
|
else:
|
|
printKeyValueList([key, ''])
|
|
Ind.Increment()
|
|
printKeyValueList([Ind.MultiLineText(value)])
|
|
Ind.Decrement()
|
|
else:
|
|
printKeyValueList([key, value])
|
|
|
|
def printJSONKey(key):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(), [key, None], ''))
|
|
|
|
def printJSONValue(value):
|
|
writeStdout(formatKeyValueList(' ', [value], '\n'))
|
|
|
|
def printEntity(entityValueList, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList),
|
|
currentCountNL(i, count)))
|
|
|
|
def printEntityMessage(entityValueList, message, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[message],
|
|
currentCountNL(i, count)))
|
|
|
|
def printEntitiesCount(entityType, entityList):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Plural(entityType), None if entityList is None else f'({len(entityList)})'],
|
|
'\n'))
|
|
|
|
def printEntityKVList(entityValueList, infoKVList, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+infoKVList,
|
|
currentCountNL(i, count)))
|
|
|
|
def performAction(entityType, entityValue, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
[f'{Act.ToPerform()} {Ent.Singular(entityType)} {entityValue}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def performActionNumItems(itemCount, itemType, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
[f'{Act.ToPerform()} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def performActionModifierNumItems(modifier, itemCount, itemType, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
[f'{Act.ToPerform()} {modifier} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def actionPerformedNumItems(itemCount, itemType, i=0, count=0):
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
[f'{itemCount} {Ent.Choose(itemType, itemCount)} {Act.Performed()} '],
|
|
currentCountNL(i, count)))
|
|
|
|
def actionFailedNumItems(itemCount, itemType, errMessage, i=0, count=0):
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
[f'{itemCount} {Ent.Choose(itemType, itemCount)} {Act.Failed()}: {errMessage} '],
|
|
currentCountNL(i, count)))
|
|
|
|
def actionNotPerformedNumItemsWarning(itemCount, itemType, errMessage, i=0, count=0):
|
|
setSysExitRC(ACTION_NOT_PERFORMED_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Choose(itemType, itemCount), itemCount, Act.NotPerformed(), errMessage],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformAction(entityValueList, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionNumItems(entityValueList, itemCount, itemType, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionModifierNumItems(entityValueList, modifier, itemCount, itemType, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionNumItemsModifier(entityValueList, itemCount, itemType, modifier, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {itemCount} {Ent.Choose(itemType, itemCount)} {modifier}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionSubItemModifierNumItems(entityValueList, subitemType, modifier, itemCount, itemType, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {Ent.Plural(subitemType)} {modifier} {itemCount} {Ent.Choose(itemType, itemCount)}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionSubItemModifierNumItemsModifierNewValue(entityValueList, subitemType, modifier1, itemCount, itemType, modifier2, newValue, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+
|
|
[f'{Act.ToPerform()} {Ent.Plural(subitemType)} {modifier1} {itemCount} {Ent.Choose(itemType, itemCount)} {modifier2}', newValue],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionModifierNumItemsModifier(entityValueList, modifier1, itemCount, itemType, modifier2, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier1} {itemCount} {Ent.Choose(itemType, itemCount)} {modifier2}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionModifierItemValueList(entityValueList, modifier, infoTypeValueList, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList),
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionModifierNewValue(entityValueList, modifier, newValue, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', newValue],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionModifierNewValueItemValueList(entityValueList, modifier, newValue, infoTypeValueList, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.ToPerform()} {modifier}', newValue]+Ent.FormatEntityValueList(infoTypeValueList),
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionItemValue(entityValueList, itemType, itemValue, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.ToPerform(), None, Ent.Singular(itemType), itemValue],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityPerformActionInfo(entityValueList, infoValue, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.ToPerform(), infoValue],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityActionPerformed(entityValueList, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.Performed()],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityActionPerformedMessage(entityValueList, message, i=0, count=0):
|
|
if message:
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.Performed(), message],
|
|
currentCountNL(i, count)))
|
|
else:
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.Performed()],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityNumItemsActionPerformed(entityValueList, itemCount, itemType, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{itemCount} {Ent.Choose(itemType, itemCount)} {Act.Performed()}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityModifierActionPerformed(entityValueList, modifier, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', None],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityModifierItemValueListActionPerformed(entityValueList, modifier, infoTypeValueList, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', None]+Ent.FormatEntityValueList(infoTypeValueList),
|
|
currentCountNL(i, count)))
|
|
|
|
def entityModifierNewValueActionPerformed(entityValueList, modifier, newValue, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', newValue],
|
|
currentCountNL(i, count)))
|
|
|
|
def entityModifierNewValueItemValueListActionPerformed(entityValueList, modifier, newValue, infoTypeValueList, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', newValue]+Ent.FormatEntityValueList(infoTypeValueList),
|
|
currentCountNL(i, count)))
|
|
|
|
def entityModifierNewValueKeyValueActionPerformed(entityValueList, modifier, newValue, infoKey, infoValue, i=0, count=0):
|
|
writeStdout(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[f'{Act.Performed()} {modifier}', newValue, infoKey, infoValue],
|
|
currentCountNL(i, count)))
|
|
|
|
def cleanFilename(filename):
|
|
return sanitize_filename(filename, '_')
|
|
|
|
def setFilePath(fileName):
|
|
if fileName.startswith('./') or fileName.startswith('.\\'):
|
|
fileName = os.path.join(os.getcwd(), fileName[2:])
|
|
else:
|
|
fileName = os.path.expanduser(fileName)
|
|
if not os.path.isabs(fileName):
|
|
fileName = os.path.join(GC.Values[GC.DRIVE_DIR], fileName)
|
|
return fileName
|
|
|
|
def uniqueFilename(targetFolder, filetitle, overwrite, extension=None):
|
|
filename = filetitle
|
|
y = 0
|
|
while True:
|
|
if extension is not None and filename.lower()[-len(extension):] != extension.lower():
|
|
filename += extension
|
|
filepath = os.path.join(targetFolder, filename)
|
|
if overwrite or not os.path.isfile(filepath):
|
|
return (filepath, filename)
|
|
y += 1
|
|
filename = f'({y})-{filetitle}'
|
|
|
|
def cleanFilepath(filepath):
|
|
return sanitize_filepath(filepath, platform='auto')
|
|
|
|
def fileErrorMessage(filename, e, entityType=Ent.FILE):
|
|
return f'{Ent.Singular(entityType)}: {filename}, {str(e)}'
|
|
|
|
def fdErrorMessage(f, defaultFilename, e):
|
|
return fileErrorMessage(getattr(f, 'name') if hasattr(f, 'name') else defaultFilename, e)
|
|
|
|
# Set file encoding to handle UTF8 BOM
|
|
def setEncoding(mode, encoding):
|
|
if 'b' in mode:
|
|
return {}
|
|
if not encoding:
|
|
encoding = GM.Globals[GM.SYS_ENCODING]
|
|
if 'r' in mode and encoding.lower().replace('-', '') == 'utf8':
|
|
encoding = UTF8_SIG
|
|
return {'encoding': encoding}
|
|
|
|
def StringIOobject(initbuff=None):
|
|
if initbuff is None:
|
|
return io.StringIO()
|
|
return io.StringIO(initbuff)
|
|
|
|
# Open a file
|
|
def openFile(filename, mode=DEFAULT_FILE_READ_MODE, encoding=None, errors=None, newline=None,
|
|
continueOnError=False, displayError=True, stripUTFBOM=False):
|
|
try:
|
|
if filename != '-':
|
|
kwargs = setEncoding(mode, encoding)
|
|
f = open(os.path.expanduser(filename), mode, errors=errors, newline=newline, **kwargs)
|
|
if stripUTFBOM:
|
|
if 'b' in mode:
|
|
if f.read(3) != b'\xef\xbb\xbf':
|
|
f.seek(0)
|
|
elif not kwargs['encoding'].lower().startswith('utf'):
|
|
if f.read(3).encode('iso-8859-1', 'replace') != codecs.BOM_UTF8:
|
|
f.seek(0)
|
|
else:
|
|
if f.read(1) != '\ufeff':
|
|
f.seek(0)
|
|
return f
|
|
if 'r' in mode:
|
|
return StringIOobject(str(sys.stdin.read()))
|
|
if 'b' not in mode:
|
|
return sys.stdout
|
|
return os.fdopen(os.dup(sys.stdout.fileno()), 'wb')
|
|
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
|
|
if continueOnError:
|
|
if displayError:
|
|
stderrWarningMsg(fileErrorMessage(filename, e))
|
|
setSysExitRC(FILE_ERROR_RC)
|
|
return None
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
|
|
|
# Close a file
|
|
def closeFile(f, forceFlush=False):
|
|
try:
|
|
if forceFlush:
|
|
# Necessary to make sure file is flushed by both Python and OS
|
|
# https://stackoverflow.com/a/13762137/1503886
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
f.close()
|
|
return True
|
|
except IOError as e:
|
|
stderrErrorMsg(fdErrorMessage(f, UNKNOWN, e))
|
|
setSysExitRC(FILE_ERROR_RC)
|
|
return False
|
|
|
|
# Read a file
|
|
def readFile(filename, mode=DEFAULT_FILE_READ_MODE, encoding=None, newline=None,
|
|
continueOnError=False, displayError=True):
|
|
try:
|
|
if filename != '-':
|
|
kwargs = setEncoding(mode, encoding)
|
|
with open(os.path.expanduser(filename), mode, newline=newline, **kwargs) as f:
|
|
return f.read()
|
|
return str(sys.stdin.read())
|
|
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
|
|
if continueOnError:
|
|
if displayError:
|
|
stderrWarningMsg(fileErrorMessage(filename, e))
|
|
setSysExitRC(FILE_ERROR_RC)
|
|
return None
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
|
|
|
# Write a file
|
|
def writeFile(filename, data, mode=DEFAULT_FILE_WRITE_MODE,
|
|
continueOnError=False, displayError=True):
|
|
try:
|
|
if filename != '-':
|
|
kwargs = setEncoding(mode, None)
|
|
with open(os.path.expanduser(filename), mode, **kwargs) as f:
|
|
f.write(data)
|
|
return True
|
|
GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout).write(data)
|
|
return True
|
|
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
|
|
if continueOnError:
|
|
if displayError:
|
|
stderrErrorMsg(fileErrorMessage(filename, e))
|
|
setSysExitRC(FILE_ERROR_RC)
|
|
return False
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
|
|
|
# Write a file, return error
|
|
def writeFileReturnError(filename, data, mode=DEFAULT_FILE_WRITE_MODE):
|
|
try:
|
|
kwargs = {'encoding': GM.Globals[GM.SYS_ENCODING]} if 'b' not in mode else {}
|
|
with open(os.path.expanduser(filename), mode, **kwargs) as f:
|
|
f.write(data)
|
|
return (True, None)
|
|
except (IOError, LookupError, UnicodeDecodeError, UnicodeError) as e:
|
|
return (False, e)
|
|
|
|
# Delete a file
|
|
def deleteFile(filename, continueOnError=False, displayError=True):
|
|
if os.path.isfile(filename):
|
|
try:
|
|
os.remove(filename)
|
|
except OSError as e:
|
|
if continueOnError:
|
|
if displayError:
|
|
stderrWarningMsg(fileErrorMessage(filename, e))
|
|
return
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
|
|
|
def getGDocSheetDataRetryWarning(entityValueList, errMsg, i=0, count=0):
|
|
action = Act.Get()
|
|
Act.Set(Act.RETRIEVE_DATA)
|
|
stderrWarningMsg(formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMsg, 'Retry', ''],
|
|
currentCountNL(i, count)))
|
|
Act.Set(action)
|
|
|
|
def getGDocSheetDataFailedExit(entityValueList, errMsg, i=0, count=0):
|
|
Act.Set(Act.RETRIEVE_DATA)
|
|
systemErrorExit(ACTION_FAILED_RC, formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMsg],
|
|
currentCountNL(i, count)))
|
|
|
|
GDOC_FORMAT_MIME_TYPES = {
|
|
'gcsv': MIMETYPE_TEXT_CSV,
|
|
'gdoc': MIMETYPE_TEXT_PLAIN,
|
|
'ghtml': MIMETYPE_TEXT_HTML,
|
|
}
|
|
|
|
# gdoc <EmailAddress> <DriveFileIDEntity>|<DriveFileNameEntity>
|
|
def getGDocData(gformat):
|
|
mimeType = GDOC_FORMAT_MIME_TYPES[gformat]
|
|
user = getEmailAddress()
|
|
fileIdEntity = getDriveFileEntity(queryShortcutsOK=False)
|
|
user, drive, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity)
|
|
if not drive:
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
if jcount == 0:
|
|
getGDocSheetDataFailedExit([Ent.USER, user], Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)))
|
|
if jcount > 1:
|
|
getGDocSheetDataFailedExit([Ent.USER, user], Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])))
|
|
fileId = fileIdEntity['list'][0]
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='name,mimeType,exportLinks',
|
|
supportsAllDrives=True)
|
|
# Google Doc
|
|
if 'exportLinks' in result:
|
|
if mimeType not in result['exportLinks']:
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
|
|
Msg.INVALID_MIMETYPE.format(result['mimeType'], mimeType))
|
|
f = TemporaryFile(mode='w+', encoding=UTF8)
|
|
_, content = drive._http.request(uri=result['exportLinks'][mimeType], method='GET')
|
|
f.write(content.decode(UTF8_SIG))
|
|
f.seek(0)
|
|
return f
|
|
# Drive File
|
|
if result['mimeType'] != mimeType:
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
|
|
Msg.INVALID_MIMETYPE.format(result['mimeType'], mimeType))
|
|
fb = TemporaryFile(mode='wb+')
|
|
request = drive.files().get_media(fileId=fileId)
|
|
downloader = googleapiclient.http.MediaIoBaseDownload(fb, request)
|
|
done = False
|
|
while not done:
|
|
_, done = downloader.next_chunk()
|
|
f = TemporaryFile(mode='w+', encoding=UTF8)
|
|
fb.seek(0)
|
|
f.write(fb.read().decode(UTF8_SIG))
|
|
fb.close()
|
|
f.seek(0)
|
|
return f
|
|
except GAPI.fileNotFound:
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DOCUMENT, fileId], Msg.DOES_NOT_EXIST)
|
|
except (IOError, httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
if f:
|
|
f.close()
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DOCUMENT, fileId], str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e))
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
|
|
HTML_TITLE_PATTERN = re.compile(r'.*<title>(.+)</title>')
|
|
|
|
# gsheet <EmailAddress> <DriveFileIDEntity>|<DriveFileNameEntity> <SheetEntity>
|
|
def getGSheetData():
|
|
user = getEmailAddress()
|
|
fileIdEntity = getDriveFileEntity(queryShortcutsOK=False)
|
|
sheetEntity = getSheetEntity(False)
|
|
user, drive, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity)
|
|
if not drive:
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
if jcount == 0:
|
|
getGDocSheetDataFailedExit([Ent.USER, user], Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)))
|
|
if jcount > 1:
|
|
getGDocSheetDataFailedExit([Ent.USER, user], Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])))
|
|
_, sheet = buildGAPIServiceObject(API.SHEETS, user)
|
|
if not sheet:
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
fileId = fileIdEntity['list'][0]
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='name,mimeType', supportsAllDrives=True)
|
|
if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
|
|
Msg.INVALID_MIMETYPE.format(result['mimeType'], MIMETYPE_GA_SPREADSHEET))
|
|
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=fileId, fields='spreadsheetUrl,sheets(properties(sheetId,title))')
|
|
sheetId = getSheetIdFromSheetEntity(spreadsheet, sheetEntity)
|
|
if sheetId is None:
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], Msg.NOT_FOUND)
|
|
spreadsheetUrl = f'{re.sub("/edit.*$", "/export", spreadsheet["spreadsheetUrl"])}?format=csv&id={fileId}&gid={sheetId}'
|
|
f = TemporaryFile(mode='w+', encoding=UTF8)
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
sys.stderr.write(f'Debug: spreadsheetUrl: {spreadsheetUrl}\n')
|
|
triesLimit = 3
|
|
for n in range(1, triesLimit+1):
|
|
_, content = drive._http.request(uri=spreadsheetUrl, method='GET')
|
|
# Check for HTML error message instead of data
|
|
if content[0:15] != b'<!DOCTYPE html>':
|
|
break
|
|
tg = HTML_TITLE_PATTERN.match(content[0:600].decode('utf-8'))
|
|
errMsg = tg.group(1) if tg else 'Unknown error'
|
|
getGDocSheetDataRetryWarning([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg, n, triesLimit)
|
|
time.sleep(20)
|
|
else:
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']], errMsg)
|
|
f.write(content.decode(UTF8_SIG))
|
|
f.seek(0)
|
|
return f
|
|
except GAPI.fileNotFound:
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, fileId], Msg.DOES_NOT_EXIST)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, fileId, sheetEntity['sheetType'], sheetEntity['sheetValue']], str(e))
|
|
except (IOError, httplib2.HttpLib2Error) as e:
|
|
if f:
|
|
f.close()
|
|
getGDocSheetDataFailedExit([Ent.USER, user, Ent.SPREADSHEET, fileId, sheetEntity['sheetType'], sheetEntity['sheetValue']], str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e))
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
|
|
|
|
BUCKET_OBJECT_PATTERNS = [
|
|
{'pattern': re.compile(r'https://storage.(?:googleapis|cloud.google).com/(.+?)/(.+)'), 'unquote': True},
|
|
{'pattern': re.compile(r'gs://(.+?)/(.+)'), 'unquote': False},
|
|
{'pattern': re.compile(r'(.+?)/(.+)'), 'unquote': False},
|
|
]
|
|
|
|
def getBucketObjectName():
|
|
uri = getString(Cmd.OB_STRING)
|
|
for pattern in BUCKET_OBJECT_PATTERNS:
|
|
mg = re.search(pattern['pattern'], uri)
|
|
if mg:
|
|
bucket = mg.group(1)
|
|
s_object = mg.group(2) if not pattern['unquote'] else unquote(mg.group(2))
|
|
return (bucket, s_object, f'{bucket}/{s_object}')
|
|
systemErrorExit(ACTION_NOT_PERFORMED_RC, f'Invalid <StorageBucketObjectName>: {uri}')
|
|
|
|
GCS_FORMAT_MIME_TYPES = {
|
|
'gcscsv': MIMETYPE_TEXT_CSV,
|
|
'gcsdoc': MIMETYPE_TEXT_PLAIN,
|
|
'gcshtml': MIMETYPE_TEXT_HTML,
|
|
}
|
|
|
|
# gcscsv|gcshtml|gcsdoc <StorageBucketObjectName>
|
|
def getStorageFileData(gcsformat, returnData=True):
|
|
mimeType = GCS_FORMAT_MIME_TYPES[gcsformat]
|
|
bucket, s_object, bucketObject = getBucketObjectName()
|
|
s = buildGAPIObject(API.STORAGEREAD)
|
|
try:
|
|
result = callGAPI(s.objects(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
bucket=bucket, object=s_object, projection='noAcl', fields='contentType')
|
|
except GAPI.notFound:
|
|
entityDoesNotExistExit(Ent.CLOUD_STORAGE_FILE, bucketObject)
|
|
except GAPI.forbidden as e:
|
|
entityActionFailedExit([Ent.CLOUD_STORAGE_FILE, bucketObject], str(e))
|
|
if result['contentType'] != mimeType:
|
|
getGDocSheetDataFailedExit([Ent.CLOUD_STORAGE_FILE, bucketObject],
|
|
Msg.INVALID_MIMETYPE.format(result['contentType'], mimeType))
|
|
fb = TemporaryFile(mode='wb+')
|
|
try:
|
|
request = s.objects().get_media(bucket=bucket, object=s_object)
|
|
downloader = googleapiclient.http.MediaIoBaseDownload(fb, request)
|
|
done = False
|
|
while not done:
|
|
_, done = downloader.next_chunk()
|
|
fb.seek(0)
|
|
if returnData:
|
|
data = fb.read().decode(UTF8)
|
|
fb.close()
|
|
return data
|
|
f = TemporaryFile(mode='w+', encoding=UTF8)
|
|
f.write(fb.read().decode(UTF8_SIG))
|
|
fb.close()
|
|
f.seek(0)
|
|
return f
|
|
except googleapiclient.http.HttpError as e:
|
|
mg = HTTP_ERROR_PATTERN.match(str(e))
|
|
getGDocSheetDataFailedExit([Ent.CLOUD_STORAGE_FILE, bucketObject], mg.group(1) if mg else str(e))
|
|
|
|
# <CSVFileInput>
|
|
def openCSVFileReader(filename, fieldnames=None):
|
|
filenameLower = filename.lower()
|
|
if filenameLower == 'gsheet':
|
|
f = getGSheetData()
|
|
getCharSet()
|
|
elif filenameLower in {'gcsv', 'gdoc'}:
|
|
f = getGDocData(filenameLower)
|
|
getCharSet()
|
|
elif filenameLower in {'gcscsv', 'gcsdoc'}:
|
|
f = getStorageFileData(filenameLower, False)
|
|
getCharSet()
|
|
else:
|
|
encoding = getCharSet()
|
|
f = openFile(filename, mode=DEFAULT_CSV_READ_MODE, encoding=encoding)
|
|
if checkArgumentPresent('warnifnodata'):
|
|
loc = f.tell()
|
|
try:
|
|
if not f.readline() or not f.readline():
|
|
stderrWarningMsg(fileErrorMessage(filename, Msg.NO_CSV_FILE_DATA_FOUND))
|
|
sys.exit(NO_ENTITIES_FOUND_RC)
|
|
f.seek(loc)
|
|
except (IOError, UnicodeDecodeError, UnicodeError) as e:
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
|
if checkArgumentPresent('columndelimiter'):
|
|
columnDelimiter = getCharacter()
|
|
else:
|
|
columnDelimiter = GC.Values[GC.CSV_INPUT_COLUMN_DELIMITER]
|
|
if checkArgumentPresent('noescapechar'):
|
|
noEscapeChar = getBoolean()
|
|
else:
|
|
noEscapeChar = GC.Values[GC.CSV_INPUT_NO_ESCAPE_CHAR]
|
|
if checkArgumentPresent('quotechar'):
|
|
quotechar = getCharacter()
|
|
else:
|
|
quotechar = GC.Values[GC.CSV_INPUT_QUOTE_CHAR]
|
|
if not checkArgumentPresent('endcsv') and checkArgumentPresent('fields'):
|
|
fieldnames = shlexSplitList(getString(Cmd.OB_FIELD_NAME_LIST))
|
|
try:
|
|
csvFile = csv.DictReader(f, fieldnames=fieldnames,
|
|
delimiter=columnDelimiter,
|
|
escapechar='\\' if not noEscapeChar else None,
|
|
quotechar=quotechar)
|
|
return (f, csvFile, csvFile.fieldnames if csvFile.fieldnames is not None else [])
|
|
except (csv.Error, UnicodeDecodeError, UnicodeError) as e:
|
|
systemErrorExit(FILE_ERROR_RC, e)
|
|
|
|
def incrAPICallsRetryData(errMsg, delta):
|
|
GM.Globals[GM.API_CALLS_RETRY_DATA].setdefault(errMsg, [0, 0.0])
|
|
GM.Globals[GM.API_CALLS_RETRY_DATA][errMsg][0] += 1
|
|
GM.Globals[GM.API_CALLS_RETRY_DATA][errMsg][1] += delta
|
|
|
|
def initAPICallsRateCheck():
|
|
GM.Globals[GM.RATE_CHECK_COUNT] = 0
|
|
GM.Globals[GM.RATE_CHECK_START] = time.time()
|
|
|
|
def checkAPICallsRate():
|
|
GM.Globals[GM.RATE_CHECK_COUNT] += 1
|
|
if GM.Globals[GM.RATE_CHECK_COUNT] >= GC.Values[GC.API_CALLS_RATE_LIMIT]:
|
|
current = time.time()
|
|
delta = int(current-GM.Globals[GM.RATE_CHECK_START])
|
|
if 0 <= delta < 60:
|
|
delta = (60-delta)+3
|
|
error_message = f'API calls per 60 seconds limit {GC.Values[GC.API_CALLS_RATE_LIMIT]} exceeded'
|
|
writeStderr(f'{WARNING_PREFIX}{error_message}: Backing off: {delta} seconds\n')
|
|
flushStderr()
|
|
time.sleep(delta)
|
|
if GC.Values[GC.SHOW_API_CALLS_RETRY_DATA]:
|
|
incrAPICallsRetryData(error_message, delta)
|
|
GM.Globals[GM.RATE_CHECK_START] = time.time()
|
|
else:
|
|
GM.Globals[GM.RATE_CHECK_START] = current
|
|
GM.Globals[GM.RATE_CHECK_COUNT] = 0
|
|
|
|
def openGAMCommandLog(Globals, name):
|
|
try:
|
|
Globals[GM.CMDLOG_LOGGER] = logging.getLogger(name)
|
|
Globals[GM.CMDLOG_LOGGER].setLevel(logging.INFO)
|
|
Globals[GM.CMDLOG_HANDLER] = RotatingFileHandler(GC.Values[GC.CMDLOG],
|
|
maxBytes=1024*GC.Values[GC.CMDLOG_MAX_KILO_BYTES],
|
|
backupCount=GC.Values[GC.CMDLOG_MAX_BACKUPS],
|
|
encoding=GC.Values[GC.CHARSET])
|
|
Globals[GM.CMDLOG_LOGGER].addHandler(Globals[GM.CMDLOG_HANDLER])
|
|
except Exception as e:
|
|
Globals[GM.CMDLOG_LOGGER] = None
|
|
systemErrorExit(CONFIG_ERROR_RC, Msg.LOGGING_INITIALIZATION_ERROR.format(str(e)))
|
|
|
|
def writeGAMCommandLog(Globals, logCmd, sysRC):
|
|
Globals[GM.CMDLOG_LOGGER].info(f'{currentISOformatTimeStamp()},{sysRC},{logCmd}')
|
|
|
|
def closeGAMCommandLog(Globals):
|
|
try:
|
|
Globals[GM.CMDLOG_HANDLER].flush()
|
|
Globals[GM.CMDLOG_HANDLER].close()
|
|
Globals[GM.CMDLOG_LOGGER].removeHandler(Globals[GM.CMDLOG_HANDLER])
|
|
except Exception:
|
|
pass
|
|
Globals[GM.CMDLOG_LOGGER] = None
|
|
|
|
# Set global variables from config file
|
|
# Return True if there are additional commands on the command line
|
|
def SetGlobalVariables():
|
|
|
|
def _stringInQuotes(value):
|
|
return (len(value) > 1) and (((value.startswith('"') and value.endswith('"'))) or ((value.startswith("'") and value.endswith("'"))))
|
|
|
|
def _stripStringQuotes(value):
|
|
if _stringInQuotes(value):
|
|
return value[1:-1]
|
|
return value
|
|
|
|
def _quoteStringIfLeadingTrailingBlanks(value):
|
|
if not value:
|
|
return "''"
|
|
if _stringInQuotes(value):
|
|
return value
|
|
if (value[0] != ' ') and (value[-1] != ' '):
|
|
return value
|
|
return f"'{value}'"
|
|
|
|
def _getDefault(itemName, itemEntry, oldGamPath):
|
|
if GC.VAR_SIGFILE in itemEntry:
|
|
GC.Defaults[itemName] = itemEntry[GC.VAR_SFFT][os.path.isfile(os.path.join(oldGamPath, itemEntry[GC.VAR_SIGFILE]))]
|
|
elif GC.VAR_ENVVAR in itemEntry:
|
|
value = os.environ.get(itemEntry[GC.VAR_ENVVAR], GC.Defaults[itemName])
|
|
if itemEntry[GC.VAR_TYPE] in [GC.TYPE_INTEGER, GC.TYPE_FLOAT]:
|
|
try:
|
|
number = int(value) if itemEntry[GC.VAR_TYPE] == GC.TYPE_INTEGER else float(value)
|
|
minVal, maxVal = itemEntry[GC.VAR_LIMITS]
|
|
if (minVal is not None) and (number < minVal):
|
|
number = minVal
|
|
elif (maxVal is not None) and (number > maxVal):
|
|
number = maxVal
|
|
except ValueError:
|
|
number = GC.Defaults[itemName]
|
|
value = str(number)
|
|
elif itemEntry[GC.VAR_TYPE] == GC.TYPE_STRING:
|
|
value = _quoteStringIfLeadingTrailingBlanks(value)
|
|
GC.Defaults[itemName] = value
|
|
|
|
def _selectSection():
|
|
value = getString(Cmd.OB_SECTION_NAME, minLen=0)
|
|
if (not value) or (value.upper() == configparser.DEFAULTSECT):
|
|
return configparser.DEFAULTSECT
|
|
if GM.Globals[GM.PARSER].has_section(value):
|
|
return value
|
|
Cmd.Backup()
|
|
usageErrorExit(formatKeyValueList('', [Ent.Singular(Ent.SECTION), value, Msg.NOT_FOUND], ''))
|
|
|
|
def _showSections():
|
|
printKeyValueList([Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE]])
|
|
Ind.Increment()
|
|
for section in [configparser.DEFAULTSECT]+sorted(GM.Globals[GM.PARSER].sections()):
|
|
printKeyValueList([f'{section}{" *" if section == sectionName else ""}'])
|
|
Ind.Decrement()
|
|
|
|
def _checkMakeDir(itemName):
|
|
if not os.path.isdir(GC.Defaults[itemName]):
|
|
try:
|
|
os.makedirs(GC.Defaults[itemName])
|
|
printKeyValueList([Act.PerformedName(Act.CREATE), GC.Defaults[itemName]])
|
|
except OSError as e:
|
|
if not os.path.isdir(GC.Defaults[itemName]):
|
|
systemErrorExit(FILE_ERROR_RC, e)
|
|
|
|
def _copyCfgFile(srcFile, targetDir, oldGamPath):
|
|
if (not srcFile) or os.path.isabs(srcFile):
|
|
return
|
|
dstFile = os.path.join(GC.Defaults[targetDir], srcFile)
|
|
if os.path.isfile(dstFile):
|
|
return
|
|
srcFile = os.path.join(oldGamPath, srcFile)
|
|
if not os.path.isfile(srcFile):
|
|
return
|
|
data = readFile(srcFile, continueOnError=True, displayError=False)
|
|
if (data is not None) and writeFile(dstFile, data, continueOnError=True):
|
|
printKeyValueList([Act.PerformedName(Act.COPY), srcFile, Msg.TO, dstFile])
|
|
|
|
def _printValueError(sectionName, itemName, value, errMessage, sysRC=CONFIG_ERROR_RC):
|
|
kvlMsg = formatKeyValueList('',
|
|
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
|
|
Ent.Singular(Ent.SECTION), sectionName,
|
|
Ent.Singular(Ent.ITEM), itemName,
|
|
Ent.Singular(Ent.VALUE), value,
|
|
errMessage],
|
|
'')
|
|
if sysRC != 0:
|
|
status['errors'] = True
|
|
printErrorMessage(sysRC, kvlMsg)
|
|
else:
|
|
writeStderr(formatKeyValueList(Ind.Spaces(), [WARNING, kvlMsg], '\n'))
|
|
|
|
def _getCfgBoolean(sectionName, itemName):
|
|
value = GM.Globals[GM.PARSER].get(sectionName, itemName).lower()
|
|
if value in TRUE_VALUES:
|
|
return True
|
|
if value in FALSE_VALUES:
|
|
return False
|
|
_printValueError(sectionName, itemName, value, f'{Msg.EXPECTED}: {formatChoiceList(TRUE_FALSE)}')
|
|
return False
|
|
|
|
def _getCfgCharacter(sectionName, itemName):
|
|
value = codecs.escape_decode(bytes(_stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)), UTF8))[0].decode(UTF8)
|
|
if not value and (itemName == 'csv_output_field_delimiter'):
|
|
return ' '
|
|
if not value and (itemName in {'csv_input_escape_char', 'csv_output_escape_char'}):
|
|
return None
|
|
if len(value) == 1:
|
|
return value
|
|
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.EXPECTED}: {integerLimits(1, 1, Msg.STRING_LENGTH)}')
|
|
return ''
|
|
|
|
def _getCfgChoice(sectionName, itemName):
|
|
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)).lower()
|
|
choices = GC.VAR_INFO[itemName][GC.VAR_CHOICES]
|
|
if value in choices:
|
|
return choices[value]
|
|
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.EXPECTED}: {",".join(choices)}')
|
|
return ''
|
|
|
|
def _getCfgLocale(sectionName, itemName):
|
|
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)).lower().replace('_', '-')
|
|
if value in LOCALE_CODES_MAP:
|
|
return LOCALE_CODES_MAP[value]
|
|
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.EXPECTED}: {",".join(LOCALE_CODES_MAP)}')
|
|
return ''
|
|
|
|
def _getCfgNumber(sectionName, itemName):
|
|
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
|
|
minVal, maxVal = GC.VAR_INFO[itemName][GC.VAR_LIMITS]
|
|
try:
|
|
number = int(value) if GC.VAR_INFO[itemName][GC.VAR_TYPE] == GC.TYPE_INTEGER else float(value)
|
|
if ((minVal is None) or (number >= minVal)) and ((maxVal is None) or (number <= maxVal)):
|
|
return number
|
|
if (minVal is not None) and (number < minVal):
|
|
number = minVal
|
|
else:
|
|
number = maxVal
|
|
_printValueError(sectionName, itemName, value, f'{Msg.EXPECTED}: {integerLimits(minVal, maxVal)}, {Msg.USED}: {number}', sysRC=0)
|
|
return number
|
|
except ValueError:
|
|
pass
|
|
_printValueError(sectionName, itemName, value, f'{Msg.EXPECTED}: {integerLimits(minVal, maxVal)}')
|
|
return 0
|
|
|
|
def _getCfgHeaderFilter(sectionName, itemName):
|
|
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
|
|
headerFilters = []
|
|
if not value or (len(value) == 2 and _stringInQuotes(value)):
|
|
return headerFilters
|
|
splitStatus, filters = shlexSplitListStatus(value)
|
|
if splitStatus:
|
|
for filterStr in filters:
|
|
try:
|
|
headerFilters.append(re.compile(filterStr, re.IGNORECASE))
|
|
except re.error as e:
|
|
_printValueError(sectionName, itemName, f'"{filterStr}"', f'{Msg.INVALID_RE}: {e}')
|
|
else:
|
|
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.INVALID_LIST}: {filters}')
|
|
return headerFilters
|
|
|
|
def _getCfgHeaderFilterFromForce(sectionName, itemName):
|
|
headerFilters = []
|
|
for filterStr in GC.Values[itemName]:
|
|
try:
|
|
headerFilters.append(re.compile(fr'^{filterStr}$'))
|
|
except re.error as e:
|
|
_printValueError(sectionName, itemName, f'"{filterStr}"', f'{Msg.INVALID_RE}: {e}')
|
|
return headerFilters
|
|
|
|
ROW_FILTER_ANY_ALL_PATTERN = re.compile(r'^(any:|all:)(.+)$', re.IGNORECASE)
|
|
ROW_FILTER_COMP_PATTERN = re.compile(r'^(date|time|count|length)\s*([<>]=?|=|!=)(.+)$', re.IGNORECASE)
|
|
ROW_FILTER_RANGE_PATTERN = re.compile(r'^(daterange|timerange|countrange|lengthrange)(=|!=)(\S+)/(\S+)$', re.IGNORECASE)
|
|
ROW_FILTER_TIMEOFDAYRANGE_PATTERN = re.compile(r'^(timeofdayrange)(=|!=)(\d\d):(\d\d)/(\d\d):(\d\d)$', re.IGNORECASE)
|
|
ROW_FILTER_BOOL_PATTERN = re.compile(r'^(boolean):(.+)$', re.IGNORECASE)
|
|
ROW_FILTER_TEXT_PATTERN = re.compile(r'^(text)([<>]=?|=|!=)(.*)$', re.IGNORECASE)
|
|
ROW_FILTER_TEXTRANGE_PATTERN = re.compile(r'^(textrange)(=|!=)(.*)/(.*)$', re.IGNORECASE)
|
|
ROW_FILTER_RE_PATTERN = re.compile(r'^(regex|regexcs|notregex|notregexcs):(.*)$', re.IGNORECASE)
|
|
ROW_FILTER_DATA_PATTERN = re.compile(r'^(data|notdata):(list|file|csvfile) +(.+)$', re.IGNORECASE)
|
|
REGEX_CHARS = '^$*+|$[{('
|
|
|
|
def _getCfgRowFilter(sectionName, itemName):
|
|
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
|
|
rowFilters = []
|
|
if not value:
|
|
return rowFilters
|
|
if value.startswith('{'):
|
|
try:
|
|
filterDict = json.loads(value.encode('unicode-escape').decode(UTF8))
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.FAILED_TO_PARSE_AS_JSON}: {str(e)}')
|
|
return rowFilters
|
|
else:
|
|
filterDict = {}
|
|
status, filterList = shlexSplitListStatus(value)
|
|
if not status:
|
|
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.FAILED_TO_PARSE_AS_LIST}: {str(filterList)}')
|
|
return rowFilters
|
|
for filterVal in filterList:
|
|
if not filterVal:
|
|
continue
|
|
try:
|
|
filterTokens = shlexSplitList(filterVal, ':')
|
|
column = filterTokens[0]
|
|
filterStr = ':'.join(filterTokens[1:])
|
|
except ValueError:
|
|
_printValueError(sectionName, itemName, f'"{filterVal}"', f'{Msg.EXPECTED}: column:filter')
|
|
continue
|
|
filterDict[column] = filterStr
|
|
for column, filterStr in iter(filterDict.items()):
|
|
for c in REGEX_CHARS:
|
|
if c in column:
|
|
columnPat = column
|
|
break
|
|
else:
|
|
columnPat = f'^{column}$'
|
|
try:
|
|
columnPat = re.compile(columnPat, re.IGNORECASE)
|
|
except re.error as e:
|
|
_printValueError(sectionName, itemName, f'"{column}"', f'{Msg.INVALID_RE}: {e}')
|
|
continue
|
|
anyMatch = True
|
|
mg = ROW_FILTER_ANY_ALL_PATTERN.match(filterStr)
|
|
if mg:
|
|
anyMatch = mg.group(1).lower() == 'any:'
|
|
filterStr = mg.group(2)
|
|
mg = ROW_FILTER_COMP_PATTERN.match(filterStr)
|
|
if mg:
|
|
filterType = mg.group(1).lower()
|
|
if filterType in {'date', 'time'}:
|
|
if filterType == 'date':
|
|
valid, filterValue = getRowFilterDateOrDeltaFromNow(mg.group(3))
|
|
else:
|
|
valid, filterValue = getRowFilterTimeOrDeltaFromNow(mg.group(3))
|
|
if valid:
|
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue))
|
|
else:
|
|
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue}')
|
|
else: # filterType in {'count', 'length'}:
|
|
if mg.group(3).isdigit():
|
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3))))
|
|
else:
|
|
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <Number>')
|
|
continue
|
|
mg = ROW_FILTER_TEXT_PATTERN.match(filterStr)
|
|
if mg:
|
|
filterType = mg.group(1).lower()
|
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), mg.group(3)))
|
|
continue
|
|
mg = ROW_FILTER_TEXTRANGE_PATTERN.match(filterStr)
|
|
if mg:
|
|
filterType = mg.group(1).lower()
|
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), mg.group(3), mg.group(4)))
|
|
continue
|
|
mg = ROW_FILTER_RANGE_PATTERN.match(filterStr)
|
|
if mg:
|
|
filterType = mg.group(1).lower()
|
|
if filterType in {'daterange', 'timerange'}:
|
|
if filterType == 'daterange':
|
|
valid1, filterValue1 = getRowFilterDateOrDeltaFromNow(mg.group(3))
|
|
valid2, filterValue2 = getRowFilterDateOrDeltaFromNow(mg.group(4))
|
|
else:
|
|
valid1, filterValue1 = getRowFilterTimeOrDeltaFromNow(mg.group(3))
|
|
valid2, filterValue2 = getRowFilterTimeOrDeltaFromNow(mg.group(4))
|
|
if valid1 and valid2:
|
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), filterValue1, filterValue2))
|
|
else:
|
|
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: {filterValue1}/{filterValue2}')
|
|
else: #countrange|lengthrange
|
|
if mg.group(3).isdigit() and mg.group(4).isdigit():
|
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), int(mg.group(3)), int(mg.group(4))))
|
|
else:
|
|
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <Number>/<Number>')
|
|
continue
|
|
mg = ROW_FILTER_TIMEOFDAYRANGE_PATTERN.match(filterStr)
|
|
if mg:
|
|
filterType = mg.group(1).lower()
|
|
startHour = int(mg.group(3))
|
|
startMinute = int(mg.group(4))
|
|
endHour = int(mg.group(5))
|
|
endMinute = int(mg.group(6))
|
|
if startHour > 23 or startMinute > 59 or endHour > 23 or endMinute > 59 or \
|
|
endHour < startHour or (endHour == startHour and endMinute < startMinute):
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_TIMEOFDAY_RANGE.format(f'{startHour:02d}:{startMinute:02d}', f'{endHour:02d}:{endMinute:02d}'))
|
|
rowFilters.append((columnPat, anyMatch, filterType, mg.group(2), f'{startHour:02d}:{startMinute:02d}', f'{endHour:02d}:{endMinute:02d}'))
|
|
continue
|
|
mg = ROW_FILTER_BOOL_PATTERN.match(filterStr)
|
|
if mg:
|
|
filterType = mg.group(1).lower()
|
|
filterValue = mg.group(2).lower()
|
|
if filterValue in TRUE_VALUES:
|
|
rowFilters.append((columnPat, anyMatch, filterType, True))
|
|
elif filterValue in FALSE_VALUES:
|
|
rowFilters.append((columnPat, anyMatch, filterType, False))
|
|
else:
|
|
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <Boolean>')
|
|
continue
|
|
mg = ROW_FILTER_RE_PATTERN.match(filterStr)
|
|
if mg:
|
|
filterType = mg.group(1).lower()
|
|
try:
|
|
if filterType.endswith('cs'):
|
|
filterType = filterType[0:-2]
|
|
flags = 0
|
|
else:
|
|
flags = re.IGNORECASE
|
|
rowFilters.append((columnPat, anyMatch, filterType, re.compile(mg.group(2), flags)))
|
|
except re.error as e:
|
|
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.INVALID_RE}: {e}')
|
|
continue
|
|
mg = ROW_FILTER_DATA_PATTERN.match(filterStr)
|
|
if mg:
|
|
filterType = mg.group(1).lower()
|
|
filterSubType = mg.group(2).lower()
|
|
if filterSubType == 'list':
|
|
rowFilters.append((columnPat, anyMatch, filterType, set(shlexSplitList(mg.group(3)))))
|
|
continue
|
|
Cmd.MergeArguments(shlexSplitList(mg.group(3), ' '))
|
|
if filterSubType == 'file':
|
|
rowFilters.append((columnPat, anyMatch, filterType, getEntitiesFromFile(False, returnSet=True)))
|
|
else: #elif filterSubType == 'csvfile':
|
|
rowFilters.append((columnPat, anyMatch, filterType, getEntitiesFromCSVFile(False, returnSet=True)))
|
|
Cmd.RestoreArguments()
|
|
continue
|
|
_printValueError(sectionName, itemName, f'"{column}": "{filterStr}"', f'{Msg.EXPECTED}: <RowValueFilter>')
|
|
return rowFilters
|
|
|
|
def _getCfgSection(sectionName, itemName):
|
|
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName))
|
|
if (not value) or (value.upper() == configparser.DEFAULTSECT):
|
|
return configparser.DEFAULTSECT
|
|
if GM.Globals[GM.PARSER].has_section(value):
|
|
return value
|
|
_printValueError(sectionName, itemName, value, Msg.NOT_FOUND)
|
|
return configparser.DEFAULTSECT
|
|
|
|
def _getCfgPassword(sectionName, itemName):
|
|
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
|
|
if isinstance(value, bytes):
|
|
return value
|
|
value = _stripStringQuotes(value)
|
|
if value.startswith("b'") and value.endswith("'"):
|
|
return bytes(value[2:-1], UTF8)
|
|
if value:
|
|
return value
|
|
return ''
|
|
|
|
def _validateLicenseSKUs(sectionName, itemName, skuList):
|
|
GM.Globals[GM.LICENSE_SKUS] = []
|
|
for sku in skuList.split(','):
|
|
if '/' not in sku:
|
|
productId, sku = SKU.getProductAndSKU(sku)
|
|
if not productId:
|
|
_printValueError(sectionName, itemName, sku, f'{Msg.EXPECTED}: {",".join(SKU.getSortedSKUList())}')
|
|
else:
|
|
(productId, sku) = sku.split('/')
|
|
if (productId, sku) not in GM.Globals[GM.LICENSE_SKUS]:
|
|
GM.Globals[GM.LICENSE_SKUS].append((productId, sku))
|
|
|
|
def _getCfgString(sectionName, itemName):
|
|
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName))
|
|
if itemName == GC.DOMAIN:
|
|
value = value.strip()
|
|
minLen, maxLen = GC.VAR_INFO[itemName].get(GC.VAR_LIMITS, (None, None))
|
|
if ((minLen is None) or (len(value) >= minLen)) and ((maxLen is None) or (len(value) <= maxLen)):
|
|
if itemName == GC.LICENSE_SKUS and value:
|
|
_validateLicenseSKUs(sectionName, itemName, value)
|
|
return value
|
|
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.EXPECTED}: {integerLimits(minLen, maxLen, Msg.STRING_LENGTH)}')
|
|
return ''
|
|
|
|
def _getCfgStringList(sectionName, itemName):
|
|
value = GM.Globals[GM.PARSER].get(sectionName, itemName)
|
|
stringlist = []
|
|
if not value or (len(value) == 2 and _stringInQuotes(value)):
|
|
return stringlist
|
|
splitStatus, stringlist = shlexSplitListStatus(value)
|
|
if not splitStatus:
|
|
_printValueError(sectionName, itemName, f'"{value}"', f'{Msg.INVALID_LIST}: {stringlist}')
|
|
return stringlist
|
|
|
|
def _getCfgTimezone(sectionName, itemName):
|
|
value = _stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName).lower())
|
|
if value == 'utc':
|
|
GM.Globals[GM.CONVERT_TO_LOCAL_TIME] = False
|
|
return iso8601.UTC
|
|
GM.Globals[GM.CONVERT_TO_LOCAL_TIME] = True
|
|
if value == 'local':
|
|
return iso8601.Local
|
|
try:
|
|
return iso8601.parse_timezone_str(value)
|
|
except (iso8601.ParseError, OverflowError):
|
|
_printValueError(sectionName, itemName, value, f'{Msg.EXPECTED}: {TIMEZONE_FORMAT_REQUIRED}')
|
|
GM.Globals[GM.CONVERT_TO_LOCAL_TIME] = False
|
|
return iso8601.UTC
|
|
|
|
def _getCfgDirectory(sectionName, itemName):
|
|
dirPath = os.path.expanduser(_stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)))
|
|
if (not dirPath) and (itemName in {GC.GMAIL_CSE_INCERT_DIR, GC.GMAIL_CSE_INKEY_DIR}):
|
|
return dirPath
|
|
if (not dirPath) or (not os.path.isabs(dirPath) and dirPath != '.'):
|
|
if (sectionName != configparser.DEFAULTSECT) and (GM.Globals[GM.PARSER].has_option(sectionName, itemName)):
|
|
dirPath = os.path.join(os.path.expanduser(_stripStringQuotes(GM.Globals[GM.PARSER].get(configparser.DEFAULTSECT, itemName))), dirPath)
|
|
if not os.path.isabs(dirPath):
|
|
dirPath = os.path.join(GM.Globals[GM.GAM_CFG_PATH], dirPath)
|
|
return dirPath
|
|
|
|
def _getCfgFile(sectionName, itemName):
|
|
value = os.path.expanduser(_stripStringQuotes(GM.Globals[GM.PARSER].get(sectionName, itemName)))
|
|
if value and not os.path.isabs(value):
|
|
value = os.path.expanduser(os.path.join(_getCfgDirectory(sectionName, GC.CONFIG_DIR), value))
|
|
elif not value and itemName == GC.CACERTS_PEM:
|
|
if hasattr(sys, '_MEIPASS'):
|
|
value = os.path.join(sys._MEIPASS, GC.FN_CACERTS_PEM) #pylint: disable=no-member
|
|
else:
|
|
value = os.path.join(GM.Globals[GM.GAM_PATH], GC.FN_CACERTS_PEM)
|
|
return value
|
|
|
|
def _readGamCfgFile(config, fileName):
|
|
try:
|
|
with open(fileName, DEFAULT_FILE_READ_MODE, encoding=GM.Globals[GM.SYS_ENCODING]) as f:
|
|
config.read_file(f)
|
|
except (configparser.DuplicateOptionError, configparser.DuplicateSectionError,
|
|
configparser.MissingSectionHeaderError, configparser.ParsingError) as e:
|
|
systemErrorExit(CONFIG_ERROR_RC, formatKeyValueList('',
|
|
[Ent.Singular(Ent.CONFIG_FILE), fileName,
|
|
Msg.INVALID, str(e)],
|
|
''))
|
|
except IOError as e:
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(fileName, e, Ent.CONFIG_FILE))
|
|
|
|
def _writeGamCfgFile(config, fileName, action):
|
|
GM.Globals[GM.SECTION] = None # No need to save section for inner gams
|
|
try:
|
|
with open(fileName, DEFAULT_FILE_WRITE_MODE, encoding=GM.Globals[GM.SYS_ENCODING]) as f:
|
|
config.write(f)
|
|
printKeyValueList([Ent.Singular(Ent.CONFIG_FILE), fileName, Act.PerformedName(action)])
|
|
except IOError as e:
|
|
stderrErrorMsg(fileErrorMessage(fileName, e, Ent.CONFIG_FILE))
|
|
|
|
def _verifyValues(sectionName, inputFilterSectionName, outputFilterSectionName):
|
|
printKeyValueList([Ent.Singular(Ent.SECTION), sectionName]) # Do not use printEntity
|
|
Ind.Increment()
|
|
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
|
|
sectName = sectionName
|
|
if itemName in GC.CSV_INPUT_ROW_FILTER_ITEMS:
|
|
if inputFilterSectionName:
|
|
sectName = inputFilterSectionName
|
|
elif itemName in GC.CSV_OUTPUT_ROW_FILTER_ITEMS:
|
|
if outputFilterSectionName:
|
|
sectName = outputFilterSectionName
|
|
cfgValue = GM.Globals[GM.PARSER].get(sectName, itemName)
|
|
varType = itemEntry[GC.VAR_TYPE]
|
|
if varType == GC.TYPE_CHOICE:
|
|
for choice, value in iter(itemEntry[GC.VAR_CHOICES].items()):
|
|
if cfgValue == value:
|
|
cfgValue = choice
|
|
break
|
|
elif varType not in [GC.TYPE_BOOLEAN, GC.TYPE_INTEGER, GC.TYPE_FLOAT, GC.TYPE_PASSWORD]:
|
|
cfgValue = _quoteStringIfLeadingTrailingBlanks(cfgValue)
|
|
if varType == GC.TYPE_FILE:
|
|
expdValue = _getCfgFile(sectName, itemName)
|
|
if cfgValue not in ("''", expdValue):
|
|
cfgValue = f'{cfgValue} ; {expdValue}'
|
|
elif varType == GC.TYPE_DIRECTORY:
|
|
expdValue = _getCfgDirectory(sectName, itemName)
|
|
if cfgValue not in ("''", expdValue):
|
|
cfgValue = f'{cfgValue} ; {expdValue}'
|
|
elif (itemName == GC.SECTION) and (sectName != configparser.DEFAULTSECT):
|
|
continue
|
|
printLine(f'{Ind.Spaces()}{itemName} = {cfgValue}')
|
|
Ind.Decrement()
|
|
|
|
def _chkCfgDirectories(sectionName):
|
|
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
|
|
if itemEntry[GC.VAR_TYPE] == GC.TYPE_DIRECTORY:
|
|
dirPath = GC.Values[itemName]
|
|
if (not dirPath) and (itemName in {GC.GMAIL_CSE_INCERT_DIR, GC.GMAIL_CSE_INKEY_DIR}):
|
|
return
|
|
if (itemName != GC.CACHE_DIR or not GC.Values[GC.NO_CACHE]) and not os.path.isdir(dirPath):
|
|
writeStderr(formatKeyValueList(WARNING_PREFIX,
|
|
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
|
|
Ent.Singular(Ent.SECTION), sectionName,
|
|
Ent.Singular(Ent.ITEM), itemName,
|
|
Ent.Singular(Ent.VALUE), dirPath,
|
|
Msg.INVALID_PATH],
|
|
'\n'))
|
|
|
|
def _chkCfgFiles(sectionName):
|
|
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
|
|
if itemEntry[GC.VAR_TYPE] == GC.TYPE_FILE:
|
|
fileName = GC.Values[itemName]
|
|
if (not fileName) and (itemName in {GC.EXTRA_ARGS, GC.CMDLOG}):
|
|
continue
|
|
if itemName == GC.CLIENT_SECRETS_JSON: # Added 6.57.01
|
|
continue
|
|
if GC.Values[GC.ENABLE_DASA] and itemName == GC.OAUTH2_TXT:
|
|
continue
|
|
if not os.path.isfile(fileName):
|
|
writeStderr(formatKeyValueList([WARNING_PREFIX, ERROR_PREFIX][itemName == GC.CACERTS_PEM],
|
|
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
|
|
Ent.Singular(Ent.SECTION), sectionName,
|
|
Ent.Singular(Ent.ITEM), itemName,
|
|
Ent.Singular(Ent.VALUE), fileName,
|
|
Msg.NOT_FOUND],
|
|
'\n'))
|
|
if itemName == GC.CACERTS_PEM:
|
|
status['errors'] = True
|
|
elif not os.access(fileName, itemEntry[GC.VAR_ACCESS]):
|
|
if itemEntry[GC.VAR_ACCESS] == os.R_OK | os.W_OK:
|
|
accessMsg = Msg.NEED_READ_WRITE_ACCESS
|
|
elif itemEntry[GC.VAR_ACCESS] == os.R_OK:
|
|
accessMsg = Msg.NEED_READ_ACCESS
|
|
else:
|
|
accessMsg = Msg.NEED_WRITE_ACCESS
|
|
writeStderr(formatKeyValueList(ERROR_PREFIX,
|
|
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
|
|
Ent.Singular(Ent.SECTION), sectionName,
|
|
Ent.Singular(Ent.ITEM), itemName,
|
|
Ent.Singular(Ent.VALUE), fileName,
|
|
accessMsg],
|
|
'\n'))
|
|
status['errors'] = True
|
|
|
|
def _setCSVFile(fileName, mode, encoding, writeHeader, multi):
|
|
if fileName != '-':
|
|
fileName = setFilePath(fileName)
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] = fileName
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_MODE] = mode
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_ENCODING] = encoding
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER] = writeHeader
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS] = multi
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = None
|
|
|
|
def _setSTDFile(stdtype, fileName, mode, multi):
|
|
if stdtype == GM.STDOUT:
|
|
GM.Globals[GM.SAVED_STDOUT] = None
|
|
GM.Globals[stdtype][GM.REDIRECT_STD] = False
|
|
if fileName == 'null':
|
|
GM.Globals[stdtype][GM.REDIRECT_FD] = open(os.devnull, mode, encoding=UTF8)
|
|
elif fileName == '-':
|
|
GM.Globals[stdtype][GM.REDIRECT_STD] = True
|
|
if stdtype == GM.STDOUT:
|
|
GM.Globals[stdtype][GM.REDIRECT_FD] = os.fdopen(os.dup(sys.stdout.fileno()), mode, encoding=GM.Globals[GM.SYS_ENCODING])
|
|
else:
|
|
GM.Globals[stdtype][GM.REDIRECT_FD] = os.fdopen(os.dup(sys.stderr.fileno()), mode, encoding=GM.Globals[GM.SYS_ENCODING])
|
|
else:
|
|
fileName = setFilePath(fileName)
|
|
if multi and mode == DEFAULT_FILE_WRITE_MODE:
|
|
deleteFile(fileName)
|
|
mode = DEFAULT_FILE_APPEND_MODE
|
|
GM.Globals[stdtype][GM.REDIRECT_FD] = openFile(fileName, mode)
|
|
GM.Globals[stdtype][GM.REDIRECT_MULTI_FD] = GM.Globals[stdtype][GM.REDIRECT_FD] if not multi else StringIOobject()
|
|
if (stdtype == GM.STDOUT) and (GC.Values[GC.DEBUG_LEVEL] > 0):
|
|
GM.Globals[GM.SAVED_STDOUT] = sys.stdout
|
|
sys.stdout = GM.Globals[stdtype][GM.REDIRECT_MULTI_FD]
|
|
GM.Globals[stdtype][GM.REDIRECT_NAME] = fileName
|
|
GM.Globals[stdtype][GM.REDIRECT_MODE] = mode
|
|
GM.Globals[stdtype][GM.REDIRECT_MULTIPROCESS] = multi
|
|
GM.Globals[stdtype][GM.REDIRECT_QUEUE] = 'stdout' if stdtype == GM.STDOUT else 'stderr'
|
|
|
|
MULTIPROCESS_EXIT_COMP_PATTERN = re.compile(r'^rc([<>]=?|=|!=)(.+)$', re.IGNORECASE)
|
|
MULTIPROCESS_EXIT_RANGE_PATTERN = re.compile(r'^rcrange(=|!=)(\S+)/(\S+)$', re.IGNORECASE)
|
|
|
|
def _setMultiprocessExit():
|
|
rcStr = getString(Cmd.OB_STRING)
|
|
mg = MULTIPROCESS_EXIT_COMP_PATTERN.match(rcStr)
|
|
if mg:
|
|
if not mg.group(2).isdigit():
|
|
usageErrorExit(f'{Msg.EXPECTED}: rc<Operator><Value>')
|
|
GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION] = {'comp': mg.group(1), 'value': int(mg.group(2))}
|
|
return
|
|
mg = MULTIPROCESS_EXIT_RANGE_PATTERN.match(rcStr)
|
|
if mg:
|
|
if not mg.group(2).isdigit() or not mg.group(3).isdigit():
|
|
usageErrorExit(f'{Msg.EXPECTED}: rcrange<Operator><Value>/Value>')
|
|
GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION] = {'range': mg.group(1), 'low': int(mg.group(2)), 'high': int(mg.group(3))}
|
|
return
|
|
usageErrorExit(f'{Msg.EXPECTED}: (rc<Operator><Value>)|(rcrange<Operator><Value>/Value>)')
|
|
|
|
if not GM.Globals[GM.PARSER]:
|
|
homePath = os.path.expanduser('~')
|
|
GM.Globals[GM.GAM_CFG_PATH] = os.environ.get(EV_GAMCFGDIR, None)
|
|
if GM.Globals[GM.GAM_CFG_PATH]:
|
|
GM.Globals[GM.GAM_CFG_PATH] = os.path.expanduser(GM.Globals[GM.GAM_CFG_PATH])
|
|
else:
|
|
GM.Globals[GM.GAM_CFG_PATH] = os.path.join(homePath, '.gam')
|
|
GC.Defaults[GC.CONFIG_DIR] = GM.Globals[GM.GAM_CFG_PATH]
|
|
GC.Defaults[GC.CACHE_DIR] = os.path.join(GM.Globals[GM.GAM_CFG_PATH], 'gamcache')
|
|
GC.Defaults[GC.DRIVE_DIR] = os.path.join(homePath, 'Downloads')
|
|
GM.Globals[GM.GAM_CFG_FILE] = os.path.join(GM.Globals[GM.GAM_CFG_PATH], FN_GAM_CFG)
|
|
if not os.path.isfile(GM.Globals[GM.GAM_CFG_FILE]):
|
|
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
|
|
if itemEntry[GC.VAR_TYPE] == GC.TYPE_DIRECTORY:
|
|
_getDefault(itemName, itemEntry, None)
|
|
oldGamPath = os.environ.get(EV_OLDGAMPATH, GC.Defaults[GC.CONFIG_DIR])
|
|
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
|
|
if itemEntry[GC.VAR_TYPE] != GC.TYPE_DIRECTORY:
|
|
_getDefault(itemName, itemEntry, oldGamPath)
|
|
GM.Globals[GM.PARSER] = configparser.RawConfigParser(defaults=collections.OrderedDict(sorted(list(GC.Defaults.items()), key=lambda t: t[0])))
|
|
_checkMakeDir(GC.CONFIG_DIR)
|
|
_checkMakeDir(GC.CACHE_DIR)
|
|
_checkMakeDir(GC.DRIVE_DIR)
|
|
for itemName, itemEntry in iter(GC.VAR_INFO.items()):
|
|
if itemEntry[GC.VAR_TYPE] == GC.TYPE_FILE:
|
|
srcFile = os.path.expanduser(_stripStringQuotes(GM.Globals[GM.PARSER].get(configparser.DEFAULTSECT, itemName)))
|
|
_copyCfgFile(srcFile, GC.CONFIG_DIR, oldGamPath)
|
|
_writeGamCfgFile(GM.Globals[GM.PARSER], GM.Globals[GM.GAM_CFG_FILE], Act.INITIALIZE)
|
|
else:
|
|
GM.Globals[GM.PARSER] = configparser.RawConfigParser(defaults=collections.OrderedDict(sorted(list(GC.Defaults.items()), key=lambda t: t[0])))
|
|
_readGamCfgFile(GM.Globals[GM.PARSER], GM.Globals[GM.GAM_CFG_FILE])
|
|
status = {'errors': False}
|
|
inputFilterSectionName = outputFilterSectionName = None
|
|
GM.Globals[GM.GAM_CFG_SECTION] = os.environ.get(EV_GAMCFGSECTION, None)
|
|
if GM.Globals[GM.GAM_CFG_SECTION]:
|
|
sectionName = GM.Globals[GM.GAM_CFG_SECTION]
|
|
GM.Globals[GM.SECTION] = sectionName # Save section for inner gams
|
|
if not GM.Globals[GM.PARSER].has_section(sectionName):
|
|
usageErrorExit(formatKeyValueList('', [EV_GAMCFGSECTION, sectionName, Msg.NOT_FOUND], ''))
|
|
if checkArgumentPresent(Cmd.SELECT_CMD):
|
|
Cmd.Backup()
|
|
usageErrorExit(formatKeyValueList('', [EV_GAMCFGSECTION, sectionName, 'select', Msg.NOT_ALLOWED], ''))
|
|
else:
|
|
sectionName = _getCfgSection(configparser.DEFAULTSECT, GC.SECTION)
|
|
# select <SectionName> [save] [verify]
|
|
if checkArgumentPresent(Cmd.SELECT_CMD):
|
|
sectionName = _selectSection()
|
|
GM.Globals[GM.SECTION] = sectionName # Save section for inner gams
|
|
while Cmd.ArgumentsRemaining():
|
|
if checkArgumentPresent('save'):
|
|
GM.Globals[GM.PARSER].set(configparser.DEFAULTSECT, GC.SECTION, sectionName)
|
|
_writeGamCfgFile(GM.Globals[GM.PARSER], GM.Globals[GM.GAM_CFG_FILE], Act.SAVE)
|
|
elif checkArgumentPresent('verify'):
|
|
_verifyValues(sectionName, inputFilterSectionName, outputFilterSectionName)
|
|
else:
|
|
break
|
|
GM.Globals[GM.GAM_CFG_SECTION_NAME] = sectionName
|
|
# showsections
|
|
if checkArgumentPresent(Cmd.SHOWSECTIONS_CMD):
|
|
_showSections()
|
|
# selectfilter|selectoutputfilter|selectinputfilter <SectionName>
|
|
while True:
|
|
filterCommand = getChoice([Cmd.SELECTFILTER_CMD, Cmd.SELECTOUTPUTFILTER_CMD, Cmd.SELECTINPUTFILTER_CMD], defaultChoice=None)
|
|
if filterCommand is None:
|
|
break
|
|
if filterCommand != Cmd.SELECTINPUTFILTER_CMD:
|
|
outputFilterSectionName = _selectSection()
|
|
else:
|
|
inputFilterSectionName = _selectSection()
|
|
# Handle todrive_nobrowser and todrive_noemail if not present
|
|
value = GM.Globals[GM.PARSER].get(configparser.DEFAULTSECT, GC.TODRIVE_NOBROWSER)
|
|
if value == '':
|
|
GM.Globals[GM.PARSER].set(configparser.DEFAULTSECT, GC.TODRIVE_NOBROWSER, str(_getCfgBoolean(configparser.DEFAULTSECT, GC.NO_BROWSER)).lower())
|
|
value = GM.Globals[GM.PARSER].get(configparser.DEFAULTSECT, GC.TODRIVE_NOEMAIL)
|
|
if value == '':
|
|
GM.Globals[GM.PARSER].set(configparser.DEFAULTSECT, GC.TODRIVE_NOEMAIL, str(not _getCfgBoolean(configparser.DEFAULTSECT, GC.NO_BROWSER)).lower())
|
|
# Handle todrive_sheet_timestamp and todrive_sheet_timeformat if not present
|
|
for section in [sectionName, configparser.DEFAULTSECT]:
|
|
value = GM.Globals[GM.PARSER].get(section, GC.TODRIVE_SHEET_TIMESTAMP)
|
|
if value == 'copy':
|
|
GM.Globals[GM.PARSER].set(section, GC.TODRIVE_SHEET_TIMESTAMP, str(_getCfgBoolean(section, GC.TODRIVE_TIMESTAMP)).lower())
|
|
value = GM.Globals[GM.PARSER].get(section, GC.TODRIVE_SHEET_TIMEFORMAT)
|
|
if value == 'copy':
|
|
GM.Globals[GM.PARSER].set(section, GC.TODRIVE_SHEET_TIMEFORMAT, _getCfgString(section, GC.TODRIVE_TIMEFORMAT))
|
|
# Fix mistyped keyword cmdlog_max__backups
|
|
for section in [configparser.DEFAULTSECT, sectionName]:
|
|
if GM.Globals[GM.PARSER].has_option(section, GC.CMDLOG_MAX__BACKUPS):
|
|
GM.Globals[GM.PARSER].set(section, GC.CMDLOG_MAX_BACKUPS, GM.Globals[GM.PARSER].get(section, GC.CMDLOG_MAX__BACKUPS))
|
|
GM.Globals[GM.PARSER].remove_option(section, GC.CMDLOG_MAX__BACKUPS)
|
|
# config (<VariableName> [=] <Value>)* [save] [verify]
|
|
if checkArgumentPresent(Cmd.CONFIG_CMD):
|
|
while Cmd.ArgumentsRemaining():
|
|
if checkArgumentPresent('save'):
|
|
_writeGamCfgFile(GM.Globals[GM.PARSER], GM.Globals[GM.GAM_CFG_FILE], Act.SAVE)
|
|
elif checkArgumentPresent('verify'):
|
|
_verifyValues(sectionName, inputFilterSectionName, outputFilterSectionName)
|
|
else:
|
|
itemName = getChoice(GC.VAR_INFO, defaultChoice=None)
|
|
if itemName is None:
|
|
break
|
|
itemEntry = GC.VAR_INFO[itemName]
|
|
checkArgumentPresent('=')
|
|
varType = itemEntry[GC.VAR_TYPE]
|
|
if varType == GC.TYPE_BOOLEAN:
|
|
value = TRUE if getBoolean(None) else FALSE
|
|
elif varType == GC.TYPE_CHARACTER:
|
|
value = getCharacter()
|
|
elif varType == GC.TYPE_CHOICE:
|
|
value = getChoice(itemEntry[GC.VAR_CHOICES])
|
|
elif varType == GC.TYPE_INTEGER:
|
|
minVal, maxVal = itemEntry[GC.VAR_LIMITS]
|
|
value = str(getInteger(minVal=minVal, maxVal=maxVal))
|
|
elif varType == GC.TYPE_FLOAT:
|
|
minVal, maxVal = itemEntry[GC.VAR_LIMITS]
|
|
value = str(getFloat(minVal=minVal, maxVal=maxVal))
|
|
elif varType == GC.TYPE_LOCALE:
|
|
value = getLanguageCode(LOCALE_CODES_MAP)
|
|
elif varType == GC.TYPE_PASSWORD:
|
|
minLen, maxLen = itemEntry[GC.VAR_LIMITS]
|
|
value = getString(Cmd.OB_STRING, checkBlank=True, minLen=minLen, maxLen=maxLen)
|
|
if value and value.startswith("b'") and value.endswith("'"):
|
|
value = bytes(value[2:-1], UTF8)
|
|
elif varType == GC.TYPE_TIMEZONE:
|
|
value = getString(Cmd.OB_STRING, checkBlank=True)
|
|
else: # GC.TYPE_STRING, GC.TYPE_STRINGLIST
|
|
minLen, maxLen = itemEntry.get(GC.VAR_LIMITS, (0, None))
|
|
value = _quoteStringIfLeadingTrailingBlanks(getString(Cmd.OB_STRING, minLen=minLen, maxLen=maxLen))
|
|
GM.Globals[GM.PARSER].set(sectionName, itemName, value)
|
|
prevExtraArgsTxt = GC.Values.get(GC.EXTRA_ARGS, None)
|
|
prevOauth2serviceJson = GC.Values.get(GC.OAUTH2SERVICE_JSON, None)
|
|
# Assign global variables, directories, timezone first as other variables depend on them
|
|
for itemName, itemEntry in sorted(iter(GC.VAR_INFO.items())):
|
|
varType = itemEntry[GC.VAR_TYPE]
|
|
if varType == GC.TYPE_DIRECTORY:
|
|
GC.Values[itemName] = _getCfgDirectory(sectionName, itemName)
|
|
elif varType == GC.TYPE_TIMEZONE:
|
|
GC.Values[itemName] = _getCfgTimezone(sectionName, itemName)
|
|
GM.Globals[GM.DATETIME_NOW] = datetime.datetime.now(GC.Values[GC.TIMEZONE])
|
|
# Everything else except row filters
|
|
for itemName, itemEntry in sorted(iter(GC.VAR_INFO.items())):
|
|
varType = itemEntry[GC.VAR_TYPE]
|
|
if varType == GC.TYPE_BOOLEAN:
|
|
GC.Values[itemName] = _getCfgBoolean(sectionName, itemName)
|
|
elif varType == GC.TYPE_CHARACTER:
|
|
GC.Values[itemName] = _getCfgCharacter(sectionName, itemName)
|
|
elif varType == GC.TYPE_CHOICE:
|
|
GC.Values[itemName] = _getCfgChoice(sectionName, itemName)
|
|
elif varType in [GC.TYPE_INTEGER, GC.TYPE_FLOAT]:
|
|
GC.Values[itemName] = _getCfgNumber(sectionName, itemName)
|
|
elif varType == GC.TYPE_HEADERFILTER:
|
|
GC.Values[itemName] = _getCfgHeaderFilter(sectionName, itemName)
|
|
elif varType == GC.TYPE_LOCALE:
|
|
GC.Values[itemName] = _getCfgLocale(sectionName, itemName)
|
|
elif varType == GC.TYPE_PASSWORD:
|
|
GC.Values[itemName] = _getCfgPassword(sectionName, itemName)
|
|
elif varType == GC.TYPE_STRING:
|
|
GC.Values[itemName] = _getCfgString(sectionName, itemName)
|
|
elif varType in {GC.TYPE_STRINGLIST, GC.TYPE_HEADERFORCE, GC.TYPE_HEADERORDER}:
|
|
GC.Values[itemName] = _getCfgStringList(sectionName, itemName)
|
|
elif varType == GC.TYPE_FILE:
|
|
GC.Values[itemName] = _getCfgFile(sectionName, itemName)
|
|
# Row filters
|
|
for itemName, itemEntry in sorted(iter(GC.VAR_INFO.items())):
|
|
varType = itemEntry[GC.VAR_TYPE]
|
|
if varType == GC.TYPE_ROWFILTER:
|
|
GC.Values[itemName] = _getCfgRowFilter(sectionName, itemName)
|
|
# Process selectfilter|selectoutputfilter|selectinputfilter
|
|
if inputFilterSectionName:
|
|
GC.Values[GC.CSV_INPUT_ROW_FILTER] = _getCfgRowFilter(inputFilterSectionName, GC.CSV_INPUT_ROW_FILTER)
|
|
GC.Values[GC.CSV_INPUT_ROW_FILTER_MODE] = _getCfgChoice(inputFilterSectionName, GC.CSV_INPUT_ROW_FILTER_MODE)
|
|
GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER] = _getCfgRowFilter(inputFilterSectionName, GC.CSV_INPUT_ROW_DROP_FILTER)
|
|
GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER_MODE] = _getCfgChoice(inputFilterSectionName, GC.CSV_INPUT_ROW_DROP_FILTER_MODE)
|
|
GC.Values[GC.CSV_INPUT_ROW_LIMIT] = _getCfgNumber(inputFilterSectionName, GC.CSV_INPUT_ROW_LIMIT)
|
|
if outputFilterSectionName:
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_FORCE] = _getCfgStringList(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_FORCE)
|
|
if GC.Values[GC.CSV_OUTPUT_HEADER_FORCE]:
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER] = _getCfgHeaderFilterFromForce(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_FORCE)
|
|
else:
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER] = _getCfgHeaderFilter(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_FILTER)
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER] = _getCfgHeaderFilter(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_DROP_FILTER)
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_ORDER] = _getCfgStringList(outputFilterSectionName, GC.CSV_OUTPUT_HEADER_ORDER)
|
|
GC.Values[GC.CSV_OUTPUT_ROW_FILTER] = _getCfgRowFilter(outputFilterSectionName, GC.CSV_OUTPUT_ROW_FILTER)
|
|
GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE] = _getCfgChoice(outputFilterSectionName, GC.CSV_OUTPUT_ROW_FILTER_MODE)
|
|
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER] = _getCfgRowFilter(outputFilterSectionName, GC.CSV_OUTPUT_ROW_DROP_FILTER)
|
|
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE] = _getCfgChoice(outputFilterSectionName, GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE)
|
|
GC.Values[GC.CSV_OUTPUT_ROW_LIMIT] = _getCfgNumber(outputFilterSectionName, GC.CSV_OUTPUT_ROW_LIMIT)
|
|
GC.Values[GC.CSV_OUTPUT_SORT_HEADERS] = _getCfgStringList(outputFilterSectionName, GC.CSV_OUTPUT_SORT_HEADERS)
|
|
elif GC.Values[GC.CSV_OUTPUT_HEADER_FORCE]:
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER] = _getCfgHeaderFilterFromForce(sectionName, GC.CSV_OUTPUT_HEADER_FORCE)
|
|
if status['errors']:
|
|
sys.exit(CONFIG_ERROR_RC)
|
|
# Global values cleanup
|
|
GC.Values[GC.DOMAIN] = GC.Values[GC.DOMAIN].lower()
|
|
if not GC.Values[GC.SMTP_FQDN]:
|
|
GC.Values[GC.SMTP_FQDN] = None
|
|
# Inherit debug_level, output_dateformat, output_timeformat if not locally defined
|
|
if GM.Globals[GM.PID] != 0:
|
|
if GC.Values[GC.DEBUG_LEVEL] == 0:
|
|
GC.Values[GC.DEBUG_LEVEL] = GM.Globals[GM.DEBUG_LEVEL]
|
|
if not GC.Values[GC.OUTPUT_DATEFORMAT]:
|
|
GC.Values[GC.OUTPUT_DATEFORMAT] = GM.Globals[GM.OUTPUT_DATEFORMAT]
|
|
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
|
|
GC.Values[GC.OUTPUT_TIMEFORMAT] = GM.Globals[GM.OUTPUT_TIMEFORMAT]
|
|
# Define lockfile: oauth2.txt.lock
|
|
GM.Globals[GM.OAUTH2_TXT_LOCK] = f'{GC.Values[GC.OAUTH2_TXT]}.lock'
|
|
# Override httplib2 settings
|
|
httplib2.debuglevel = GC.Values[GC.DEBUG_LEVEL]
|
|
# Reset global variables if required
|
|
if prevExtraArgsTxt != GC.Values[GC.EXTRA_ARGS]:
|
|
GM.Globals[GM.EXTRA_ARGS_LIST] = [('prettyPrint', GC.Values[GC.DEBUG_LEVEL] > 0)]
|
|
if GC.Values[GC.EXTRA_ARGS]:
|
|
ea_config = configparser.ConfigParser()
|
|
ea_config.optionxform = str
|
|
ea_config.read(GC.Values[GC.EXTRA_ARGS])
|
|
GM.Globals[GM.EXTRA_ARGS_LIST].extend(ea_config.items('extra-args'))
|
|
if prevOauth2serviceJson != GC.Values[GC.OAUTH2SERVICE_JSON]:
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA] = {}
|
|
GM.Globals[GM.OAUTH2SERVICE_CLIENT_ID] = None
|
|
Cmd.SetEncoding(GM.Globals[GM.SYS_ENCODING])
|
|
# multiprocessexit (rc<Operator><Number>)|(rcrange=<Number>/<Number>)|(rcrange!=<Number>/<Number>)
|
|
if checkArgumentPresent(Cmd.MULTIPROCESSEXIT_CMD):
|
|
_setMultiprocessExit()
|
|
# redirect csv <FileName> [multiprocess] [append] [noheader] [charset <CharSet>]
|
|
# [columndelimiter <Character>] [quotechar <Character>]] [noescapechar [<Boolean>]]
|
|
# [sortheaders <StringList>] [timestampcolumn <String>] [transpose [<Boolean>]]
|
|
# [todrive <ToDriveAttribute>*]
|
|
# redirect stdout <FileName> [multiprocess] [append]
|
|
# redirect stdout null
|
|
# redirect stderr <FileName> [multiprocess] [append]
|
|
# redirect stderr stdout
|
|
# redirect stderr null
|
|
while checkArgumentPresent(Cmd.REDIRECT_CMD):
|
|
myarg = getChoice(['csv', 'stdout', 'stderr'])
|
|
filename = re.sub(r'{{Section}}', sectionName, getString(Cmd.OB_FILE_NAME, checkBlank=True))
|
|
if myarg == 'csv':
|
|
multi = checkArgumentPresent('multiprocess')
|
|
mode = DEFAULT_FILE_APPEND_MODE if checkArgumentPresent('append') else DEFAULT_FILE_WRITE_MODE
|
|
writeHeader = not checkArgumentPresent('noheader')
|
|
encoding = getCharSet()
|
|
if checkArgumentPresent('columndelimiter'):
|
|
GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER] = GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER] = getCharacter()
|
|
if checkArgumentPresent('quotechar'):
|
|
GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR] = GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR] = getCharacter()
|
|
if checkArgumentPresent('noescapechar'):
|
|
GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR] = GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR] = getBoolean()
|
|
if checkArgumentPresent('sortheaders'):
|
|
GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS] = GC.Values[GC.CSV_OUTPUT_SORT_HEADERS] = getString(Cmd.OB_STRING_LIST, minLen=0).replace(',', ' ').split()
|
|
if checkArgumentPresent('timestampcolumn'):
|
|
GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN] = GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN] = getString(Cmd.OB_STRING, minLen=0)
|
|
if checkArgumentPresent('transpose'):
|
|
GM.Globals[GM.CSV_OUTPUT_TRANSPOSE] = getBoolean()
|
|
_setCSVFile(filename, mode, encoding, writeHeader, multi)
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF] = CSVPrintFile()
|
|
if checkArgumentPresent('todrive'):
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].GetTodriveParameters()
|
|
GM.Globals[GM.CSV_TODRIVE] = GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].todrive.copy()
|
|
elif myarg == 'stdout':
|
|
if filename.lower() == 'null':
|
|
multi = checkArgumentPresent('multiprocess')
|
|
_setSTDFile(GM.STDOUT, 'null', DEFAULT_FILE_WRITE_MODE, multi)
|
|
else:
|
|
multi = checkArgumentPresent('multiprocess')
|
|
mode = DEFAULT_FILE_APPEND_MODE if checkArgumentPresent('append') else DEFAULT_FILE_WRITE_MODE
|
|
_setSTDFile(GM.STDOUT, filename, mode, multi)
|
|
else: # myarg == 'stderr'
|
|
if filename.lower() == 'null':
|
|
multi = checkArgumentPresent('multiprocess')
|
|
_setSTDFile(GM.STDERR, 'null', DEFAULT_FILE_WRITE_MODE, multi)
|
|
elif filename.lower() != 'stdout':
|
|
multi = checkArgumentPresent('multiprocess')
|
|
mode = DEFAULT_FILE_APPEND_MODE if checkArgumentPresent('append') else DEFAULT_FILE_WRITE_MODE
|
|
_setSTDFile(GM.STDERR, filename, mode, multi)
|
|
else:
|
|
multi = checkArgumentPresent('multiprocess')
|
|
if not GM.Globals[GM.STDOUT]:
|
|
_setSTDFile(GM.STDOUT, '-', DEFAULT_FILE_WRITE_MODE, multi)
|
|
GM.Globals[GM.STDERR] = GM.Globals[GM.STDOUT].copy()
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_NAME] = 'stdout'
|
|
if not GM.Globals[GM.STDOUT]:
|
|
_setSTDFile(GM.STDOUT, '-', DEFAULT_FILE_WRITE_MODE, False)
|
|
if not GM.Globals[GM.STDERR]:
|
|
_setSTDFile(GM.STDERR, '-', DEFAULT_FILE_WRITE_MODE, False)
|
|
# If both csv and stdout are redirected to - with same multiprocess setting and csv doesn't have any todrive parameters, collapse csv onto stdout
|
|
if (GM.Globals[GM.PID] == 0 and GM.Globals[GM.CSVFILE] and
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] == '-' and GM.Globals[GM.STDOUT][GM.REDIRECT_NAME] == '-' and
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS] == GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS] and
|
|
GM.Globals[GM.CSVFILE].get(GM.REDIRECT_QUEUE_CSVPF) and not GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF].todrive):
|
|
_setCSVFile('-', GM.Globals[GM.STDOUT].get(GM.REDIRECT_MODE, DEFAULT_FILE_WRITE_MODE), GC.Values[GC.CHARSET],
|
|
GM.Globals[GM.CSVFILE].get(GM.REDIRECT_WRITE_HEADER, True), GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS])
|
|
elif not GM.Globals[GM.CSVFILE]:
|
|
_setCSVFile('-', GM.Globals[GM.STDOUT].get(GM.REDIRECT_MODE, DEFAULT_FILE_WRITE_MODE), GC.Values[GC.CHARSET], True, False)
|
|
initAPICallsRateCheck()
|
|
# Main process
|
|
# Clear input row filters/limit from parser, children can define but shouldn't inherit global value
|
|
# Clear output header/row filters/limit from parser, children can define or they will inherit global value if not defined
|
|
if GM.Globals[GM.PID] == 0:
|
|
for itemName, itemEntry in sorted(iter(GC.VAR_INFO.items())):
|
|
varType = itemEntry[GC.VAR_TYPE]
|
|
if varType in {GC.TYPE_HEADERFILTER, GC.TYPE_HEADERFORCE, GC.TYPE_HEADERORDER, GC.TYPE_ROWFILTER}:
|
|
GM.Globals[GM.PARSER].set(sectionName, itemName, '')
|
|
elif (varType == GC.TYPE_INTEGER) and itemName in {GC.CSV_INPUT_ROW_LIMIT, GC.CSV_OUTPUT_ROW_LIMIT}:
|
|
GM.Globals[GM.PARSER].set(sectionName, itemName, '0')
|
|
# Child process
|
|
# Inherit main process output header/row filters/limit, print defaults if not locally defined
|
|
else:
|
|
if not GC.Values[GC.CSV_OUTPUT_HEADER_FILTER]:
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER] = GM.Globals[GM.CSV_OUTPUT_HEADER_FILTER][:]
|
|
if not GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER]:
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER] = GM.Globals[GM.CSV_OUTPUT_HEADER_DROP_FILTER][:]
|
|
if not GC.Values[GC.CSV_OUTPUT_HEADER_FORCE]:
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_FORCE] = GM.Globals[GM.CSV_OUTPUT_HEADER_FORCE][:]
|
|
if not GC.Values[GC.CSV_OUTPUT_HEADER_ORDER]:
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_ORDER] = GM.Globals[GM.CSV_OUTPUT_HEADER_ORDER][:]
|
|
if not GC.Values[GC.CSV_OUTPUT_ROW_FILTER]:
|
|
GC.Values[GC.CSV_OUTPUT_ROW_FILTER] = GM.Globals[GM.CSV_OUTPUT_ROW_FILTER][:]
|
|
GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE] = GM.Globals[GM.CSV_OUTPUT_ROW_FILTER_MODE]
|
|
if not GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER]:
|
|
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER] = GM.Globals[GM.CSV_OUTPUT_ROW_DROP_FILTER][:]
|
|
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE] = GM.Globals[GM.CSV_OUTPUT_ROW_DROP_FILTER_MODE]
|
|
if not GC.Values[GC.CSV_OUTPUT_ROW_LIMIT]:
|
|
GC.Values[GC.CSV_OUTPUT_ROW_LIMIT] = GM.Globals[GM.CSV_OUTPUT_ROW_LIMIT]
|
|
if not GC.Values[GC.PRINT_AGU_DOMAINS]:
|
|
GC.Values[GC.PRINT_AGU_DOMAINS] = GM.Globals[GM.PRINT_AGU_DOMAINS]
|
|
if not GC.Values[GC.PRINT_CROS_OUS]:
|
|
GC.Values[GC.PRINT_CROS_OUS] = GM.Globals[GM.PRINT_CROS_OUS]
|
|
if not GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]:
|
|
GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN] = GM.Globals[GM.PRINT_CROS_OUS_AND_CHILDREN]
|
|
GC.Values[GC.SHOW_GETTINGS] = GM.Globals[GM.SHOW_GETTINGS]
|
|
GC.Values[GC.SHOW_GETTINGS_GOT_NL] = GM.Globals[GM.SHOW_GETTINGS_GOT_NL]
|
|
# customer_id, domain and admin_email must be set when enable_dasa = true
|
|
if GC.Values[GC.ENABLE_DASA]:
|
|
errors = 0
|
|
for itemName in [GC.CUSTOMER_ID, GC.DOMAIN, GC.ADMIN_EMAIL]:
|
|
if not GC.Values[itemName] or (itemName == GC.CUSTOMER_ID and GC.Values[itemName] == GC.MY_CUSTOMER):
|
|
stderrErrorMsg(formatKeyValueList('',
|
|
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
|
|
Ent.Singular(Ent.SECTION), sectionName,
|
|
itemName, GC.Values[itemName] or '""',
|
|
GC.ENABLE_DASA, GC.Values[GC.ENABLE_DASA],
|
|
Msg.NOT_COMPATIBLE],
|
|
'\n'))
|
|
errors += 1
|
|
if errors:
|
|
sys.exit(USAGE_ERROR_RC)
|
|
# If no select/options commands were executed or some were and there are more arguments on the command line,
|
|
# warn if the json files are missing and return True
|
|
if (Cmd.Location() == 1) or (Cmd.ArgumentsRemaining()):
|
|
_chkCfgDirectories(sectionName)
|
|
_chkCfgFiles(sectionName)
|
|
if status['errors']:
|
|
sys.exit(CONFIG_ERROR_RC)
|
|
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]
|
|
# Set environment variables so GData API can find cacerts.pem
|
|
os.environ['REQUESTS_CA_BUNDLE'] = GC.Values[GC.CACERTS_PEM]
|
|
os.environ['DEFAULT_CA_BUNDLE_PATH'] = GC.Values[GC.CACERTS_PEM]
|
|
os.environ['HTTPLIB2_CA_CERTS'] = GC.Values[GC.CACERTS_PEM]
|
|
os.environ['SSL_CERT_FILE'] = GC.Values[GC.CACERTS_PEM]
|
|
httplib2.CA_CERTS = GC.Values[GC.CACERTS_PEM]
|
|
# Needs to be set so oauthlib doesn't puke when Google changes our scopes
|
|
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = 'true'
|
|
# Set up command logging at top level only
|
|
if (GM.Globals[GM.PID] == 0) and GC.Values[GC.CMDLOG]:
|
|
openGAMCommandLog(GM.Globals, 'mainlog')
|
|
return True
|
|
# We're done, nothing else to do
|
|
return False
|
|
|
|
def handleServerError(e):
|
|
errMsg = str(e)
|
|
if 'setting tls' not in errMsg:
|
|
systemErrorExit(NETWORK_ERROR_RC, errMsg)
|
|
stderrErrorMsg(errMsg)
|
|
writeStderr(Msg.DISABLE_TLS_MIN_MAX)
|
|
systemErrorExit(NETWORK_ERROR_RC, None)
|
|
|
|
def getHttpObj(cache=None, timeout=None, override_min_tls=None, override_max_tls=None):
|
|
tls_minimum_version = override_min_tls if override_min_tls else GC.Values[GC.TLS_MIN_VERSION] if GC.Values[GC.TLS_MIN_VERSION] else None
|
|
tls_maximum_version = override_max_tls if override_max_tls else GC.Values[GC.TLS_MAX_VERSION] if GC.Values[GC.TLS_MAX_VERSION] else None
|
|
httpObj = httplib2.Http(cache=cache,
|
|
timeout=timeout,
|
|
ca_certs=GC.Values[GC.CACERTS_PEM],
|
|
disable_ssl_certificate_validation=GC.Values[GC.NO_VERIFY_SSL],
|
|
tls_maximum_version=tls_maximum_version,
|
|
tls_minimum_version=tls_minimum_version)
|
|
httpObj.redirect_codes = set(httpObj.redirect_codes) - {308}
|
|
return httpObj
|
|
|
|
def _force_user_agent(user_agent):
|
|
"""Creates a decorator which can force a user agent in HTTP headers."""
|
|
|
|
def decorator(request_method):
|
|
"""Wraps a request method to insert a user-agent in HTTP headers."""
|
|
|
|
def wrapped_request_method(*args, **kwargs):
|
|
"""Modifies HTTP headers to include a specified user-agent."""
|
|
if kwargs.get('headers') is not None:
|
|
if kwargs['headers'].get('user-agent'):
|
|
if user_agent not in kwargs['headers']['user-agent']:
|
|
# Save the existing user-agent header and tack on our own.
|
|
kwargs['headers']['user-agent'] = f'{user_agent} {kwargs["headers"]["user-agent"]}'
|
|
else:
|
|
kwargs['headers']['user-agent'] = user_agent
|
|
else:
|
|
kwargs['headers'] = {'user-agent': user_agent}
|
|
return request_method(*args, **kwargs)
|
|
|
|
return wrapped_request_method
|
|
|
|
return decorator
|
|
|
|
class transportAgentRequest(google_auth_httplib2.Request):
|
|
"""A Request which forces a user agent."""
|
|
|
|
@_force_user_agent(GAM_USER_AGENT)
|
|
def __call__(self, *args, **kwargs): #pylint: disable=arguments-differ
|
|
"""Inserts the GAM user-agent header in requests."""
|
|
return super().__call__(*args, **kwargs)
|
|
|
|
class transportAuthorizedHttp(google_auth_httplib2.AuthorizedHttp):
|
|
"""An AuthorizedHttp which forces a user agent during requests."""
|
|
|
|
@_force_user_agent(GAM_USER_AGENT)
|
|
def request(self, *args, **kwargs): #pylint: disable=arguments-differ
|
|
"""Inserts the GAM user-agent header in requests."""
|
|
return super().request(*args, **kwargs)
|
|
|
|
def transportCreateRequest(httpObj=None):
|
|
"""Creates a uniform Request object with a default http, if not provided.
|
|
|
|
Args:
|
|
httpObj: Optional httplib2.Http compatible object to be used with the request.
|
|
If not provided, a default HTTP will be used.
|
|
|
|
Returns:
|
|
Request: A google_auth_httplib2.Request compatible Request.
|
|
"""
|
|
if not httpObj:
|
|
httpObj = getHttpObj()
|
|
return transportAgentRequest(httpObj)
|
|
|
|
def doGAMCheckForUpdates(forceCheck):
|
|
def _gamLatestVersionNotAvailable():
|
|
if forceCheck:
|
|
systemErrorExit(NETWORK_ERROR_RC, Msg.GAM_LATEST_VERSION_NOT_AVAILABLE)
|
|
|
|
try:
|
|
_, c = getHttpObj(timeout=10).request(GAM_LATEST_RELEASE, 'GET', headers={'Accept': 'application/vnd.github.v3.text+json'})
|
|
try:
|
|
release_data = json.loads(c)
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
|
|
_gamLatestVersionNotAvailable()
|
|
return
|
|
if not isinstance(release_data, dict) or 'tag_name' not in release_data:
|
|
_gamLatestVersionNotAvailable()
|
|
return
|
|
current_version = __version__
|
|
latest_version = release_data['tag_name']
|
|
if latest_version[0].lower() == 'v':
|
|
latest_version = latest_version[1:]
|
|
printKeyValueList(['Version Check', None])
|
|
Ind.Increment()
|
|
printKeyValueList(['Current', current_version])
|
|
printKeyValueList([' Latest', latest_version])
|
|
Ind.Decrement()
|
|
if forceCheck < 0:
|
|
setSysExitRC(1 if latest_version > current_version else 0)
|
|
return
|
|
except (httplib2.HttpLib2Error, httplib2.ServerNotFoundError,
|
|
google.auth.exceptions.TransportError,
|
|
RuntimeError, ConnectionError, OSError) as e:
|
|
if forceCheck:
|
|
handleServerError(e)
|
|
|
|
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
|
|
|
class signjwtJWTCredentials(google.auth.jwt.Credentials):
|
|
''' Class used for DASA '''
|
|
def _make_jwt(self):
|
|
now = datetime.datetime.utcnow()
|
|
lifetime = datetime.timedelta(seconds=self._token_lifetime)
|
|
expiry = now + lifetime
|
|
payload = {
|
|
"iat": google.auth._helpers.datetime_to_secs(now),
|
|
"exp": google.auth._helpers.datetime_to_secs(expiry),
|
|
"iss": self._issuer,
|
|
"sub": self._subject,
|
|
}
|
|
if self._audience:
|
|
payload["aud"] = self._audience
|
|
payload.update(self._additional_claims)
|
|
jwt = self._signer.sign(payload)
|
|
return jwt, expiry
|
|
|
|
# Some Workforce Identity Federation endpoints such as GitHub Actions
|
|
# only allow TLS 1.2 as of April 2023.
|
|
|
|
def getTLSv1_2Request():
|
|
httpc = getHttpObj(override_min_tls='TLSv1_2')
|
|
return transportCreateRequest(httpc)
|
|
|
|
class signjwtCredentials(google.oauth2.service_account.Credentials):
|
|
''' Class used for DwD '''
|
|
|
|
def _make_authorization_grant_assertion(self):
|
|
now = datetime.datetime.utcnow()
|
|
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
|
|
expiry = now + lifetime
|
|
payload = {
|
|
"iat": google.auth._helpers.datetime_to_secs(now),
|
|
"exp": google.auth._helpers.datetime_to_secs(expiry),
|
|
"iss": self._service_account_email,
|
|
"aud": API.GOOGLE_OAUTH2_TOKEN_ENDPOINT,
|
|
"scope": google.auth._helpers.scopes_to_string(self._scopes or ()),
|
|
}
|
|
payload.update(self._additional_claims)
|
|
# The subject can be a user email for domain-wide delegation.
|
|
if self._subject:
|
|
payload.setdefault("sub", self._subject)
|
|
token = self._signer(payload)
|
|
return token
|
|
|
|
class signjwtSignJwt(google.auth.crypt.Signer):
|
|
''' Signer class for SignJWT '''
|
|
def __init__(self, service_account_info):
|
|
self.service_account_email = service_account_info['client_email']
|
|
self.name = f'projects/-/serviceAccounts/{self.service_account_email}'
|
|
self._key_id = None
|
|
|
|
@property # type: ignore
|
|
def key_id(self):
|
|
return self._key_id
|
|
|
|
def sign(self, message):
|
|
''' Call IAM Credentials SignJWT API to get our signed JWT '''
|
|
try:
|
|
credentials, _ = google.auth.default(scopes=[API.IAM_SCOPE],
|
|
request=getTLSv1_2Request())
|
|
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
|
|
systemErrorExit(API_ACCESS_DENIED_RC, str(e))
|
|
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(override_min_tls='TLSv1_2'))
|
|
iamc = getService(API.IAM_CREDENTIALS, httpObj)
|
|
response = callGAPI(iamc.projects().serviceAccounts(), 'signJwt',
|
|
name=self.name, body={'payload': json.dumps(message)})
|
|
signed_jwt = response.get('signedJwt')
|
|
return signed_jwt
|
|
|
|
def handleOAuthTokenError(e, softErrors, displayError=False, i=0, count=0):
|
|
errMsg = str(e).replace('.', '')
|
|
if ((errMsg in API.OAUTH2_TOKEN_ERRORS) or
|
|
errMsg.startswith('Invalid response') or
|
|
errMsg.startswith('invalid_request: Invalid impersonation "sub" field')):
|
|
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
|
|
ClientAPIAccessDeniedExit()
|
|
if softErrors:
|
|
entityActionFailedWarning([Ent.USER, GM.Globals[GM.CURRENT_SVCACCT_USER], Ent.USER, None], errMsg, i, count)
|
|
return None
|
|
systemErrorExit(SERVICE_NOT_APPLICABLE_RC, Msg.SERVICE_NOT_APPLICABLE_THIS_ADDRESS.format(GM.Globals[GM.CURRENT_SVCACCT_USER]))
|
|
if errMsg in API.OAUTH2_UNAUTHORIZED_ERRORS:
|
|
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
|
|
ClientAPIAccessDeniedExit()
|
|
if softErrors:
|
|
if displayError:
|
|
apiOrScopes = API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]) if GM.Globals[GM.CURRENT_SVCACCT_API] else ','.join(sorted(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]))
|
|
userServiceNotEnabledWarning(GM.Globals[GM.CURRENT_SVCACCT_USER], apiOrScopes, i, count)
|
|
return None
|
|
SvcAcctAPIAccessDeniedExit()
|
|
if errMsg in API.REFRESH_PERM_ERRORS:
|
|
if softErrors:
|
|
return None
|
|
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
|
|
expiredRevokedOauth2TxtExit()
|
|
stderrErrorMsg(f'Authentication Token Error - {errMsg}')
|
|
APIAccessDeniedExit()
|
|
|
|
def getOauth2TxtCredentials(exitOnError=True, api=None, noDASA=False, refreshOnly=False, noScopes=False):
|
|
if not noDASA and GC.Values[GC.ENABLE_DASA]:
|
|
jsonData = readFile(GC.Values[GC.OAUTH2SERVICE_JSON], continueOnError=True, displayError=False)
|
|
if jsonData:
|
|
try:
|
|
if api in API.APIS_NEEDING_ACCESS_TOKEN:
|
|
return (False, getSvcAcctCredentials(API.APIS_NEEDING_ACCESS_TOKEN[api], userEmail=None, forceOauth=True))
|
|
jsonDict = json.loads(jsonData)
|
|
api, _, _ = API.getVersion(api)
|
|
audience = f'https://{api}.googleapis.com/'
|
|
key_type = jsonDict.get('key_type', 'default')
|
|
if key_type == 'default':
|
|
return (True, JWTCredentials.from_service_account_info(jsonDict, audience=audience))
|
|
if key_type == 'yubikey':
|
|
yksigner = yubikey.YubiKey(jsonDict)
|
|
return (True, JWTCredentials._from_signer_and_info(yksigner, jsonDict, audience=audience))
|
|
if key_type == 'signjwt':
|
|
sjsigner = signjwtSignJwt(jsonDict)
|
|
return (True, signjwtJWTCredentials._from_signer_and_info(sjsigner, jsonDict, audience=audience))
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
invalidOauth2serviceJsonExit(str(e))
|
|
invalidOauth2serviceJsonExit(Msg.NO_DATA)
|
|
jsonData = readFile(GC.Values[GC.OAUTH2_TXT], continueOnError=True, displayError=False)
|
|
if jsonData:
|
|
try:
|
|
jsonDict = json.loads(jsonData)
|
|
if noScopes:
|
|
jsonDict['scopes'] = []
|
|
if 'client_id' in jsonDict:
|
|
if not refreshOnly:
|
|
if set(jsonDict.get('scopes', API.REQUIRED_SCOPES)) == API.REQUIRED_SCOPES_SET:
|
|
if exitOnError:
|
|
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, Msg.NO_CLIENT_ACCESS_ALLOWED)
|
|
return (False, None)
|
|
else:
|
|
GM.Globals[GM.CREDENTIALS_SCOPES] = set(jsonDict.pop('scopes', API.REQUIRED_SCOPES))
|
|
token_expiry = jsonDict.get('token_expiry', REFRESH_EXPIRY)
|
|
if GC.Values[GC.TRUNCATE_CLIENT_ID]:
|
|
# chop off .apps.googleusercontent.com suffix as it's not needed and we need to keep things short for the Auth URL.
|
|
jsonDict['client_id'] = re.sub(r'\.apps\.googleusercontent\.com$', '', jsonDict['client_id'])
|
|
creds = google.oauth2.credentials.Credentials.from_authorized_user_info(jsonDict)
|
|
if 'id_token_jwt' not in jsonDict:
|
|
creds.token = jsonDict['token']
|
|
creds._id_token = jsonDict['id_token']
|
|
GM.Globals[GM.DECODED_ID_TOKEN] = jsonDict['decoded_id_token']
|
|
else:
|
|
creds.token = jsonDict['access_token']
|
|
creds._id_token = jsonDict['id_token_jwt']
|
|
GM.Globals[GM.DECODED_ID_TOKEN] = jsonDict['id_token']
|
|
creds.expiry = datetime.datetime.strptime(token_expiry, YYYYMMDDTHHMMSSZ_FORMAT)
|
|
return (not noScopes, creds)
|
|
if jsonDict and exitOnError:
|
|
invalidOauth2TxtExit(Msg.INVALID)
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
if exitOnError:
|
|
invalidOauth2TxtExit(str(e))
|
|
if exitOnError:
|
|
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, Msg.NO_CLIENT_ACCESS_ALLOWED)
|
|
return (False, None)
|
|
|
|
def _getValueFromOAuth(field, credentials=None):
|
|
if not GM.Globals[GM.DECODED_ID_TOKEN]:
|
|
request = transportCreateRequest()
|
|
if credentials is None:
|
|
credentials = getClientCredentials(refreshOnly=True)
|
|
elif credentials.expired:
|
|
credentials.refresh(request)
|
|
try:
|
|
GM.Globals[GM.DECODED_ID_TOKEN] = google.oauth2.id_token.verify_oauth2_token(credentials.id_token, request,
|
|
clock_skew_in_seconds=GC.Values[GC.CLOCK_SKEW_IN_SECONDS])
|
|
except ValueError as e:
|
|
if 'Token used too early' in str(e):
|
|
stderrErrorMsg(Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
|
|
systemErrorExit(SYSTEM_ERROR_RC, str(e))
|
|
return GM.Globals[GM.DECODED_ID_TOKEN].get(field, UNKNOWN)
|
|
|
|
def _getAdminEmail():
|
|
if GC.Values[GC.ADMIN_EMAIL]:
|
|
return GC.Values[GC.ADMIN_EMAIL]
|
|
return _getValueFromOAuth('email')
|
|
|
|
def writeClientCredentials(creds, filename):
|
|
creds_data = {
|
|
'client_id': creds.client_id,
|
|
'client_secret': creds.client_secret,
|
|
'id_token': creds.id_token,
|
|
'refresh_token': creds.refresh_token,
|
|
'scopes': sorted(creds.scopes or GM.Globals[GM.CREDENTIALS_SCOPES]),
|
|
'token': creds.token,
|
|
'token_expiry': creds.expiry.strftime(YYYYMMDDTHHMMSSZ_FORMAT),
|
|
'token_uri': creds.token_uri,
|
|
}
|
|
expected_iss = ['https://accounts.google.com', 'accounts.google.com']
|
|
if _getValueFromOAuth('iss', creds) not in expected_iss:
|
|
systemErrorExit(OAUTH2_TXT_REQUIRED_RC, f'Wrong OAuth 2.0 credentials issuer. Got {_getValueFromOAuth("iss", creds)} expected one of {", ".join(expected_iss)}')
|
|
request = transportCreateRequest()
|
|
try:
|
|
creds_data['decoded_id_token'] = google.oauth2.id_token.verify_oauth2_token(creds.id_token, request,
|
|
clock_skew_in_seconds=GC.Values[GC.CLOCK_SKEW_IN_SECONDS])
|
|
except ValueError as e:
|
|
if 'Token used too early' in str(e):
|
|
stderrErrorMsg(Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
|
|
systemErrorExit(SYSTEM_ERROR_RC, str(e))
|
|
GM.Globals[GM.DECODED_ID_TOKEN] = creds_data['decoded_id_token']
|
|
if filename != '-':
|
|
writeFile(filename, json.dumps(creds_data, indent=2, sort_keys=True)+'\n')
|
|
else:
|
|
writeStdout(json.dumps(creds_data, ensure_ascii=False, sort_keys=True, indent=2)+'\n')
|
|
|
|
URL_SHORTENER_ENDPOINT = 'https://gam-shortn.appspot.com/create'
|
|
|
|
def shortenURL(long_url):
|
|
if GC.Values[GC.NO_SHORT_URLS]:
|
|
return long_url
|
|
httpObj = getHttpObj(timeout=10)
|
|
try:
|
|
payload = json.dumps({'long_url': long_url})
|
|
resp, content = httpObj.request(URL_SHORTENER_ENDPOINT, 'POST',
|
|
payload,
|
|
headers={'Content-Type': 'application/json',
|
|
'User-Agent': GAM_USER_AGENT})
|
|
except:
|
|
return long_url
|
|
if resp.status != 200:
|
|
return long_url
|
|
try:
|
|
if isinstance(content, bytes):
|
|
content = content.decode()
|
|
return json.loads(content).get('short_url', long_url)
|
|
except:
|
|
return long_url
|
|
|
|
def runSqliteQuery(db_file, query):
|
|
conn = sqlite3.connect(db_file)
|
|
curr = conn.cursor()
|
|
curr.execute(query)
|
|
return curr.fetchone()[0]
|
|
|
|
def refreshCredentialsWithReauth(credentials):
|
|
def gcloudError():
|
|
writeStderr(f'Failed to run gcloud as {admin_email}. Please make sure it\'s setup')
|
|
e = Msg.REAUTHENTICATION_IS_NEEDED
|
|
handleOAuthTokenError(e, False)
|
|
|
|
writeStderr(Msg.CALLING_GCLOUD_FOR_REAUTH)
|
|
if 'termios' in sys.modules:
|
|
old_settings = termios.tcgetattr(sys.stdin)
|
|
admin_email = _getAdminEmail()
|
|
# First makes sure gcloud has a valid access token and thus
|
|
# should also have a valid RAPT token
|
|
try:
|
|
devnull = open(os.devnull, 'w', encoding=UTF8)
|
|
subprocess.run(['gcloud',
|
|
'auth',
|
|
'print-identity-token',
|
|
'--no-user-output-enabled'],
|
|
stderr=devnull,
|
|
check=False)
|
|
devnull.close()
|
|
# now determine gcloud's config path and token file
|
|
gcloud_path_result = subprocess.run(['gcloud',
|
|
'info',
|
|
'--format=value(config.paths.global_config_dir)'],
|
|
capture_output=True, check=False)
|
|
except KeyboardInterrupt as e:
|
|
# avoids loss of terminal echo on *nix
|
|
if 'termios' in sys.modules:
|
|
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
|
|
printBlankLine()
|
|
raise KeyboardInterrupt from e
|
|
token_path = gcloud_path_result.stdout.decode().strip()
|
|
if not token_path:
|
|
gcloudError()
|
|
token_file = f'{token_path}/access_tokens.db'
|
|
try:
|
|
credentials._rapt_token = runSqliteQuery(token_file,
|
|
f'SELECT rapt_token FROM access_tokens WHERE account_id = "{admin_email}"')
|
|
except TypeError:
|
|
gcloudError()
|
|
if not credentials._rapt_token:
|
|
systemErrorExit(SYSTEM_ERROR_RC,
|
|
'Failed to retrieve reauth token from gcloud. You may need to wait until gcloud is also prompted for reauth.')
|
|
|
|
def getClientCredentials(forceRefresh=False, forceWrite=False, filename=None, api=None, noDASA=False, refreshOnly=False, noScopes=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 = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK])
|
|
with lock:
|
|
writeCreds, credentials = getOauth2TxtCredentials(api=api, noDASA=noDASA, refreshOnly=refreshOnly, noScopes=noScopes)
|
|
if not credentials:
|
|
invalidOauth2TxtExit('')
|
|
if credentials.expired or forceRefresh:
|
|
triesLimit = 3
|
|
for n in range(1, triesLimit+1):
|
|
try:
|
|
credentials.refresh(transportCreateRequest())
|
|
if writeCreds or forceWrite:
|
|
writeClientCredentials(credentials, filename or GC.Values[GC.OAUTH2_TXT])
|
|
break
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
if n != triesLimit:
|
|
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
|
|
continue
|
|
handleServerError(e)
|
|
except google.auth.exceptions.RefreshError as e:
|
|
if isinstance(e.args, tuple):
|
|
e = e.args[0]
|
|
if 'Reauthentication is needed' in str(e):
|
|
if GC.Values[GC.ENABLE_GCLOUD_REAUTH]:
|
|
refreshCredentialsWithReauth(credentials)
|
|
continue
|
|
e = Msg.REAUTHENTICATION_IS_NEEDED
|
|
handleOAuthTokenError(e, False)
|
|
return credentials
|
|
|
|
def waitOnFailure(n, triesLimit, error_code, error_message):
|
|
delta = min(2 ** n, 60)+float(random.randint(1, 1000))/1000
|
|
if n > 3:
|
|
writeStderr(f'Temporary error: {error_code} - {error_message}, Backing off: {int(delta)} seconds, Retry: {n}/{triesLimit}\n')
|
|
flushStderr()
|
|
time.sleep(delta)
|
|
if GC.Values[GC.SHOW_API_CALLS_RETRY_DATA]:
|
|
incrAPICallsRetryData(error_message, delta)
|
|
|
|
def clearServiceCache(service):
|
|
if hasattr(service._http, 'http') and hasattr(service._http.http, 'cache'):
|
|
if service._http.http.cache is None:
|
|
return False
|
|
service._http.http.cache = None
|
|
return True
|
|
if hasattr(service._http, 'cache'):
|
|
if service._http.cache is None:
|
|
return False
|
|
service._http.cache = None
|
|
return True
|
|
return False
|
|
|
|
DISCOVERY_URIS = [googleapiclient.discovery.V1_DISCOVERY_URI, googleapiclient.discovery.V2_DISCOVERY_URI]
|
|
|
|
# Used for API.CLOUDRESOURCEMANAGER, API.SERVICEUSAGE, API.IAM
|
|
def getAPIService(api, httpObj):
|
|
api, version, v2discovery = API.getVersion(api)
|
|
return googleapiclient.discovery.build(api, version, http=httpObj, cache_discovery=False,
|
|
discoveryServiceUrl=DISCOVERY_URIS[v2discovery], static_discovery=False)
|
|
|
|
def getService(api, httpObj):
|
|
### Drive v3beta
|
|
# mapDriveURL = api == API.DRIVE3 and GC.Values[GC.DRIVE_V3_BETA]
|
|
hasLocalJSON = API.hasLocalJSON(api)
|
|
api, version, v2discovery = API.getVersion(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=httpObj)
|
|
if GM.Globals[GM.CACHE_DISCOVERY_ONLY]:
|
|
clearServiceCache(service)
|
|
return service
|
|
if not hasLocalJSON:
|
|
triesLimit = 3
|
|
for n in range(1, triesLimit+1):
|
|
try:
|
|
service = googleapiclient.discovery.build(api, version, http=httpObj, cache_discovery=False,
|
|
discoveryServiceUrl=DISCOVERY_URIS[v2discovery], static_discovery=False)
|
|
GM.Globals[GM.CURRENT_API_SERVICES].setdefault(api, {})
|
|
GM.Globals[GM.CURRENT_API_SERVICES][api][version] = service._rootDesc.copy()
|
|
### Drive v3beta
|
|
# if mapDriveURL:
|
|
# setattr(service, '_baseUrl', getattr(service, '_baseUrl').replace('/v3/', '/v3beta/'))
|
|
if GM.Globals[GM.CACHE_DISCOVERY_ONLY]:
|
|
clearServiceCache(service)
|
|
return service
|
|
except googleapiclient.errors.UnknownApiNameOrVersion as e:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, Msg.UNKNOWN_API_OR_VERSION.format(str(e), __author__))
|
|
except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e:
|
|
if n != triesLimit:
|
|
waitOnFailure(n, triesLimit, INVALID_JSON_RC, str(e))
|
|
continue
|
|
systemErrorExit(INVALID_JSON_RC, str(e))
|
|
except (http_client.ResponseNotReady, OSError, googleapiclient.errors.HttpError) as e:
|
|
errMsg = f'Connection error: {str(e) or repr(e)}'
|
|
if n != triesLimit:
|
|
waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
|
|
continue
|
|
systemErrorExit(SOCKET_ERROR_RC, errMsg)
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
if n != triesLimit:
|
|
httpObj.connections = {}
|
|
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
|
|
continue
|
|
handleServerError(e)
|
|
disc_file, discovery = readDiscoveryFile(f'{api}-{version}')
|
|
try:
|
|
service = googleapiclient.discovery.build_from_document(discovery, http=httpObj)
|
|
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]:
|
|
clearServiceCache(service)
|
|
return service
|
|
except (googleapiclient.errors.InvalidJsonError, KeyError, ValueError) as e:
|
|
invalidDiscoveryJsonExit(disc_file, str(e))
|
|
except IOError as e:
|
|
systemErrorExit(FILE_ERROR_RC, str(e))
|
|
|
|
def defaultSvcAcctScopes():
|
|
scopesList = API.getSvcAcctScopesList(GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY], False)
|
|
saScopes = {}
|
|
for scope in scopesList:
|
|
saScopes.setdefault(scope['api'], [])
|
|
saScopes[scope['api']].append(scope['scope'])
|
|
saScopes[API.DRIVEACTIVITY].append(API.DRIVE_SCOPE)
|
|
saScopes[API.DRIVE2] = saScopes[API.DRIVE3]
|
|
saScopes[API.DRIVETD] = saScopes[API.DRIVE3]
|
|
saScopes[API.SHEETSTD] = saScopes[API.SHEETS]
|
|
return saScopes
|
|
|
|
def _getSvcAcctData():
|
|
if not GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]:
|
|
jsonData = readFile(GC.Values[GC.OAUTH2SERVICE_JSON], continueOnError=True, displayError=True)
|
|
if not jsonData:
|
|
invalidOauth2serviceJsonExit(Msg.NO_DATA)
|
|
try:
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA] = json.loads(jsonData)
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
invalidOauth2serviceJsonExit(str(e))
|
|
if not GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]:
|
|
systemErrorExit(OAUTH2SERVICE_JSON_REQUIRED_RC, Msg.NO_SVCACCT_ACCESS_ALLOWED)
|
|
requiredFields = ['client_email', 'client_id', 'project_id', 'token_uri']
|
|
key_type = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
|
|
if key_type == 'default':
|
|
requiredFields.extend(['private_key', 'private_key_id'])
|
|
missingFields = []
|
|
for field in requiredFields:
|
|
if field not in GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]:
|
|
missingFields.append(field)
|
|
if missingFields:
|
|
invalidOauth2serviceJsonExit(Msg.MISSING_FIELDS.format(','.join(missingFields)))
|
|
# Some old oauth2service.json files have: 'https://accounts.google.com/o/oauth2/auth' which no longer works
|
|
if GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['token_uri'] == 'https://accounts.google.com/o/oauth2/auth':
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['token_uri'] = API.GOOGLE_OAUTH2_TOKEN_ENDPOINT
|
|
if API.OAUTH2SA_SCOPES not in GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]:
|
|
GM.Globals[GM.SVCACCT_SCOPES_DEFINED] = False
|
|
GM.Globals[GM.SVCACCT_SCOPES] = defaultSvcAcctScopes()
|
|
else:
|
|
GM.Globals[GM.SVCACCT_SCOPES_DEFINED] = True
|
|
GM.Globals[GM.SVCACCT_SCOPES] = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].pop(API.OAUTH2SA_SCOPES)
|
|
|
|
def getSvcAcctCredentials(scopesOrAPI, userEmail, softErrors=False, forceOauth=False):
|
|
_getSvcAcctData()
|
|
if isinstance(scopesOrAPI, str):
|
|
GM.Globals[GM.CURRENT_SVCACCT_API] = scopesOrAPI
|
|
if scopesOrAPI not in API.JWT_APIS:
|
|
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] = GM.Globals[GM.SVCACCT_SCOPES].get(scopesOrAPI, [])
|
|
else:
|
|
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] = API.JWT_APIS[scopesOrAPI]
|
|
if scopesOrAPI != API.CHAT_EVENTS and not GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]:
|
|
if softErrors:
|
|
return None
|
|
SvcAcctAPIAccessDeniedExit()
|
|
if scopesOrAPI in {API.PEOPLE, API.PEOPLE_DIRECTORY, API.PEOPLE_OTHERCONTACTS}:
|
|
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES].append(API.USERINFO_PROFILE_SCOPE)
|
|
if scopesOrAPI in {API.PEOPLE_OTHERCONTACTS}:
|
|
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES].append(API.PEOPLE_SCOPE)
|
|
elif scopesOrAPI == API.CHAT_EVENTS:
|
|
for chatAPI in [API.CHAT_SPACES, API.CHAT_MEMBERSHIPS, API.CHAT_MESSAGES]:
|
|
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES].extend(GM.Globals[GM.SVCACCT_SCOPES].get(chatAPI, []))
|
|
else:
|
|
GM.Globals[GM.CURRENT_SVCACCT_API] = ''
|
|
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] = scopesOrAPI
|
|
key_type = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
|
|
if not GM.Globals[GM.CURRENT_SVCACCT_API] or scopesOrAPI not in API.JWT_APIS or forceOauth:
|
|
try:
|
|
if key_type == 'default':
|
|
credentials = google.oauth2.service_account.Credentials.from_service_account_info(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
|
|
elif key_type == 'yubikey':
|
|
yksigner = yubikey.YubiKey(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
|
|
credentials = google.oauth2.service_account.Credentials._from_signer_and_info(yksigner,
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
|
|
elif key_type == 'signjwt':
|
|
sjsigner = signjwtSignJwt(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
|
|
credentials = signjwtCredentials._from_signer_and_info(sjsigner.sign,
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
|
|
except (ValueError, IndexError, KeyError) as e:
|
|
if softErrors:
|
|
return None
|
|
invalidOauth2serviceJsonExit(str(e))
|
|
credentials = credentials.with_scopes(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES])
|
|
else:
|
|
audience = f'https://{scopesOrAPI}.googleapis.com/'
|
|
try:
|
|
if key_type == 'default':
|
|
credentials = JWTCredentials.from_service_account_info(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA],
|
|
audience=audience)
|
|
elif key_type == 'yubikey':
|
|
yksigner = yubikey.YubiKey(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
|
|
credentials = JWTCredentials._from_signer_and_info(yksigner,
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA],
|
|
audience=audience)
|
|
elif key_type == 'signjwt':
|
|
sjsigner = signjwtSignJwt(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
|
|
credentials = signjwtJWTCredentials._from_signer_and_info(sjsigner,
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA],
|
|
audience=audience)
|
|
credentials.project_id = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['project_id']
|
|
except (ValueError, IndexError, KeyError) as e:
|
|
if softErrors:
|
|
return None
|
|
invalidOauth2serviceJsonExit(str(e))
|
|
GM.Globals[GM.CURRENT_SVCACCT_USER] = userEmail
|
|
if userEmail:
|
|
credentials = credentials.with_subject(userEmail)
|
|
GM.Globals[GM.ADMIN] = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email']
|
|
GM.Globals[GM.OAUTH2SERVICE_CLIENT_ID] = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id']
|
|
return credentials
|
|
|
|
def getGDataOAuthToken(gdataObj, credentials=None):
|
|
if not credentials:
|
|
credentials = getClientCredentials(refreshOnly=True)
|
|
try:
|
|
credentials.refresh(transportCreateRequest())
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
handleServerError(e)
|
|
except google.auth.exceptions.RefreshError as e:
|
|
if isinstance(e.args, tuple):
|
|
e = e.args[0]
|
|
handleOAuthTokenError(e, False)
|
|
gdataObj.additional_headers['Authorization'] = f'Bearer {credentials.token}'
|
|
if not GC.Values[GC.DOMAIN]:
|
|
GC.Values[GC.DOMAIN] = GM.Globals[GM.DECODED_ID_TOKEN].get('hd', 'UNKNOWN').lower()
|
|
if not GC.Values[GC.CUSTOMER_ID]:
|
|
GC.Values[GC.CUSTOMER_ID] = GC.MY_CUSTOMER
|
|
GM.Globals[GM.ADMIN] = GM.Globals[GM.DECODED_ID_TOKEN].get('email', 'UNKNOWN').lower()
|
|
GM.Globals[GM.OAUTH2_CLIENT_ID] = credentials.client_id
|
|
gdataObj.domain = GC.Values[GC.DOMAIN]
|
|
gdataObj.source = GAM_USER_AGENT
|
|
return True
|
|
|
|
def checkGDataError(e, service):
|
|
error = e.args
|
|
reason = error[0].get('reason', '')
|
|
body = error[0].get('body', '').decode(UTF8)
|
|
# First check for errors that need special handling
|
|
if reason in ['Token invalid - Invalid token: Stateless token expired', 'Token invalid - Invalid token: Token not found', 'gone']:
|
|
keep_domain = service.domain
|
|
getGDataOAuthToken(service)
|
|
service.domain = keep_domain
|
|
return (GDATA.TOKEN_EXPIRED, reason)
|
|
error_code = getattr(e, 'error_code', 600)
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
writeStdout(f'{ERROR_PREFIX} {error_code}: {reason}, {body}\n')
|
|
if error_code == 600:
|
|
if (body.startswith('Quota exceeded for the current request') or
|
|
body.startswith('Quota exceeded for quota metric') or
|
|
body.startswith('Request rate higher than configured')):
|
|
return (GDATA.QUOTA_EXCEEDED, body)
|
|
if (body.startswith('Photo delete failed') or
|
|
body.startswith('Upload photo failed') or
|
|
body.startswith('Photo query failed')):
|
|
return (GDATA.NOT_FOUND, body)
|
|
if body.startswith(GDATA.API_DEPRECATED_MSG):
|
|
return (GDATA.API_DEPRECATED, body)
|
|
if reason == 'Too Many Requests':
|
|
return (GDATA.QUOTA_EXCEEDED, reason)
|
|
if reason == 'Bad Gateway':
|
|
return (GDATA.BAD_GATEWAY, reason)
|
|
if reason == 'Gateway Timeout':
|
|
return (GDATA.GATEWAY_TIMEOUT, reason)
|
|
if reason == 'Service Unavailable':
|
|
return (GDATA.SERVICE_UNAVAILABLE, reason)
|
|
if reason == 'Service <jotspot> disabled by G Suite admin.':
|
|
return (GDATA.FORBIDDEN, reason)
|
|
if reason == 'Internal Server Error':
|
|
return (GDATA.INTERNAL_SERVER_ERROR, reason)
|
|
if reason == 'Token invalid - Invalid token: Token disabled, revoked, or expired.':
|
|
return (GDATA.TOKEN_INVALID, 'Token disabled, revoked, or expired. Please delete and re-create oauth.txt')
|
|
if reason == 'Token invalid - AuthSub token has wrong scope':
|
|
return (GDATA.INSUFFICIENT_PERMISSIONS, reason)
|
|
if reason.startswith('Only administrators can request entries belonging to'):
|
|
return (GDATA.INSUFFICIENT_PERMISSIONS, reason)
|
|
if reason == 'You are not authorized to access this API':
|
|
return (GDATA.INSUFFICIENT_PERMISSIONS, reason)
|
|
if reason == 'Invalid domain.':
|
|
return (GDATA.INVALID_DOMAIN, reason)
|
|
if reason.startswith('You are not authorized to perform operations on the domain'):
|
|
return (GDATA.INVALID_DOMAIN, reason)
|
|
if reason == 'Bad Request':
|
|
if 'already exists' in body:
|
|
return (GDATA.ENTITY_EXISTS, Msg.DUPLICATE)
|
|
return (GDATA.BAD_REQUEST, body)
|
|
if reason == 'Forbidden':
|
|
return (GDATA.FORBIDDEN, body)
|
|
if reason == 'Not Found':
|
|
return (GDATA.NOT_FOUND, Msg.DOES_NOT_EXIST)
|
|
if reason == 'Not Implemented':
|
|
return (GDATA.NOT_IMPLEMENTED, body)
|
|
if reason == 'Precondition Failed':
|
|
return (GDATA.PRECONDITION_FAILED, reason)
|
|
elif error_code == 602:
|
|
if body.startswith(GDATA.API_DEPRECATED_MSG):
|
|
return (GDATA.API_DEPRECATED, body)
|
|
if reason == 'Bad Request':
|
|
return (GDATA.BAD_REQUEST, body)
|
|
elif error_code == 610:
|
|
if reason == 'Service <jotspot> disabled by G Suite admin.':
|
|
return (GDATA.FORBIDDEN, reason)
|
|
|
|
# We got a "normal" error, define the mapping below
|
|
error_code_map = {
|
|
1000: reason,
|
|
1001: reason,
|
|
1002: 'Unauthorized and forbidden',
|
|
1100: 'User deleted recently',
|
|
1200: 'Domain user limit exceeded',
|
|
1201: 'Domain alias limit exceeded',
|
|
1202: 'Domain suspended',
|
|
1203: 'Domain feature unavailable',
|
|
1300: f'Entity {getattr(e, "invalidInput", "<unknown>")} exists',
|
|
1301: f'Entity {getattr(e, "invalidInput", "<unknown>")} Does Not Exist',
|
|
1302: 'Entity Name Is Reserved',
|
|
1303: f'Entity {getattr(e, "invalidInput", "<unknown>")} name not valid',
|
|
1306: f'{getattr(e, "invalidInput", "<unknown>")} has members. Cannot delete.',
|
|
1317: f'Invalid input {getattr(e, "invalidInput", "<unknown>")}, reason {getattr(e, "reason", "<unknown>")}',
|
|
1400: 'Invalid Given Name',
|
|
1401: 'Invalid Family Name',
|
|
1402: 'Invalid Password',
|
|
1403: 'Invalid Username',
|
|
1404: 'Invalid Hash Function Name',
|
|
1405: 'Invalid Hash Digest Length',
|
|
1406: 'Invalid Email Address',
|
|
1407: 'Invalid Query Parameter Value',
|
|
1408: 'Invalid SSO Signing Key',
|
|
1409: 'Invalid Encryption Public Key',
|
|
1410: 'Feature Unavailable For User',
|
|
1411: 'Invalid Encryption Public Key Format',
|
|
1500: 'Too Many Recipients On Email List',
|
|
1501: 'Too Many Aliases For User',
|
|
1502: 'Too Many Delegates For User',
|
|
1601: 'Duplicate Destinations',
|
|
1602: 'Too Many Destinations',
|
|
1603: 'Invalid Route Address',
|
|
1700: 'Group Cannot Contain Cycle',
|
|
1800: 'Group Cannot Contain Cycle',
|
|
1801: f'Invalid value {getattr(e, "invalidInput", "<unknown>")}',
|
|
}
|
|
return (error_code, error_code_map.get(error_code, f'Unknown Error: {str(e)}'))
|
|
|
|
def callGData(service, function,
|
|
bailOnInternalServerError=False, softErrors=False,
|
|
throwErrors=None, retryErrors=None, triesLimit=0,
|
|
**kwargs):
|
|
if throwErrors is None:
|
|
throwErrors = []
|
|
if retryErrors is None:
|
|
retryErrors = []
|
|
if triesLimit == 0:
|
|
triesLimit = GC.Values[GC.API_CALLS_TRIES_LIMIT]
|
|
allRetryErrors = GDATA.NON_TERMINATING_ERRORS+retryErrors
|
|
method = getattr(service, function)
|
|
if GC.Values[GC.API_CALLS_RATE_CHECK]:
|
|
checkAPICallsRate()
|
|
for n in range(1, triesLimit+1):
|
|
try:
|
|
return method(**kwargs)
|
|
except (gdata.service.RequestError, gdata.apps.service.AppsForYourDomainException) as e:
|
|
error_code, error_message = checkGDataError(e, service)
|
|
if (n != triesLimit) and (error_code in allRetryErrors):
|
|
if (error_code == GDATA.INTERNAL_SERVER_ERROR and
|
|
bailOnInternalServerError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]):
|
|
raise GDATA.ERROR_CODE_EXCEPTION_MAP[error_code](error_message)
|
|
waitOnFailure(n, triesLimit, error_code, error_message)
|
|
continue
|
|
if error_code in throwErrors:
|
|
if error_code in GDATA.ERROR_CODE_EXCEPTION_MAP:
|
|
raise GDATA.ERROR_CODE_EXCEPTION_MAP[error_code](error_message)
|
|
raise
|
|
if softErrors:
|
|
stderrErrorMsg(f'{error_code} - {error_message}{["", ": Giving up."][n > 1]}')
|
|
return None
|
|
if error_code == GDATA.INSUFFICIENT_PERMISSIONS:
|
|
APIAccessDeniedExit()
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, f'{error_code} - {error_message}')
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
if n != triesLimit:
|
|
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
|
|
continue
|
|
handleServerError(e)
|
|
except google.auth.exceptions.RefreshError as e:
|
|
if isinstance(e.args, tuple):
|
|
e = e.args[0]
|
|
handleOAuthTokenError(e, GDATA.SERVICE_NOT_APPLICABLE in throwErrors)
|
|
raise GDATA.ERROR_CODE_EXCEPTION_MAP[GDATA.SERVICE_NOT_APPLICABLE](str(e))
|
|
except (http_client.ResponseNotReady, OSError) as e:
|
|
errMsg = f'Connection error: {str(e) or repr(e)}'
|
|
if n != triesLimit:
|
|
waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
|
|
continue
|
|
if softErrors:
|
|
writeStderr(f'\n{ERROR_PREFIX}{errMsg} - Giving up.\n')
|
|
return None
|
|
systemErrorExit(SOCKET_ERROR_RC, errMsg)
|
|
|
|
def writeGotMessage(msg):
|
|
if GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
|
|
writeStderr(msg)
|
|
else:
|
|
writeStderr('\r')
|
|
msgLen = len(msg)
|
|
if msgLen < GM.Globals[GM.LAST_GOT_MSG_LEN]:
|
|
writeStderr(msg+' '*(GM.Globals[GM.LAST_GOT_MSG_LEN]-msgLen))
|
|
else:
|
|
writeStderr(msg)
|
|
GM.Globals[GM.LAST_GOT_MSG_LEN] = msgLen
|
|
flushStderr()
|
|
|
|
def callGDataPages(service, function,
|
|
pageMessage=None,
|
|
softErrors=False, throwErrors=None, retryErrors=None,
|
|
uri=None,
|
|
**kwargs):
|
|
if throwErrors is None:
|
|
throwErrors = []
|
|
if retryErrors is None:
|
|
retryErrors = []
|
|
nextLink = None
|
|
allResults = []
|
|
totalItems = 0
|
|
while True:
|
|
this_page = callGData(service, function,
|
|
softErrors=softErrors, throwErrors=throwErrors, retryErrors=retryErrors,
|
|
uri=uri,
|
|
**kwargs)
|
|
if this_page:
|
|
nextLink = this_page.GetNextLink()
|
|
pageItems = len(this_page.entry)
|
|
if pageItems == 0:
|
|
nextLink = None
|
|
totalItems += pageItems
|
|
allResults.extend(this_page.entry)
|
|
else:
|
|
nextLink = None
|
|
pageItems = 0
|
|
if pageMessage:
|
|
show_message = pageMessage.replace(TOTAL_ITEMS_MARKER, str(totalItems))
|
|
writeGotMessage(show_message.format(Ent.ChooseGetting(totalItems)))
|
|
if nextLink is None:
|
|
if pageMessage and (pageMessage[-1] != '\n'):
|
|
writeStderr('\r\n')
|
|
flushStderr()
|
|
return allResults
|
|
uri = nextLink.href
|
|
if 'url_params' in kwargs:
|
|
kwargs['url_params'].pop('start-index', None)
|
|
|
|
def checkGAPIError(e, softErrors=False, retryOnHttpError=False, mapNotFound=True):
|
|
def makeErrorDict(code, reason, message):
|
|
return {'error': {'code': code, 'errors': [{'reason': reason, 'message': message}]}}
|
|
|
|
try:
|
|
error = json.loads(e.content.decode(UTF8))
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
writeStdout(f'{ERROR_PREFIX} JSON: {str(error)}\n')
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
|
|
eContent = e.content.decode(UTF8) if isinstance(e.content, bytes) else e.content
|
|
lContent = eContent.lower()
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
writeStdout(f'{ERROR_PREFIX} HTTP: {str(eContent)}\n')
|
|
if eContent[0:15] != '<!DOCTYPE html>':
|
|
if (e.resp['status'] == '403') and (lContent.startswith('request rate higher than configured')):
|
|
return (e.resp['status'], GAPI.QUOTA_EXCEEDED, eContent)
|
|
if (e.resp['status'] == '429') and (lContent.startswith('quota exceeded for quota metric')):
|
|
return (e.resp['status'], GAPI.QUOTA_EXCEEDED, eContent)
|
|
if (e.resp['status'] == '502') and ('bad gateway' in lContent):
|
|
return (e.resp['status'], GAPI.BAD_GATEWAY, eContent)
|
|
if (e.resp['status'] == '503') and (lContent.startswith('quota exceeded for the current request')):
|
|
return (e.resp['status'], GAPI.QUOTA_EXCEEDED, eContent)
|
|
if (e.resp['status'] == '504') and ('gateway timeout' in lContent):
|
|
return (e.resp['status'], GAPI.GATEWAY_TIMEOUT, eContent)
|
|
else:
|
|
tg = HTML_TITLE_PATTERN.match(lContent)
|
|
lContent = tg.group(1) if tg else 'bad request'
|
|
if (e.resp['status'] == '403') and ('invalid domain.' in lContent):
|
|
error = makeErrorDict(403, GAPI.NOT_FOUND, 'Domain not found')
|
|
elif (e.resp['status'] == '403') and ('domain cannot use apis.' in lContent):
|
|
error = makeErrorDict(403, GAPI.DOMAIN_CANNOT_USE_APIS, 'Domain cannot use apis')
|
|
elif (e.resp['status'] == '400') and ('invalidssosigningkey' in lContent):
|
|
error = makeErrorDict(400, GAPI.INVALID, 'InvalidSsoSigningKey')
|
|
elif (e.resp['status'] == '400') and ('unknownerror' in lContent):
|
|
error = makeErrorDict(400, GAPI.INVALID, 'UnknownError')
|
|
elif (e.resp['status'] == '400') and ('featureunavailableforuser' in lContent):
|
|
error = makeErrorDict(400, GAPI.SERVICE_NOT_AVAILABLE, 'Feature Unavailable For User')
|
|
elif (e.resp['status'] == '400') and ('entitydoesnotexist' in lContent):
|
|
error = makeErrorDict(400, GAPI.NOT_FOUND, 'Entity Does Not Exist')
|
|
elif (e.resp['status'] == '400') and ('entitynamenotvalid' in lContent):
|
|
error = makeErrorDict(400, GAPI.INVALID_INPUT, 'Entity Name Not Valid')
|
|
elif (e.resp['status'] == '400') and ('failed to parse Content-Range header' in lContent):
|
|
error = makeErrorDict(400, GAPI.BAD_REQUEST, 'Failed to parse Content-Range header')
|
|
elif (e.resp['status'] == '400') and ('request contains an invalid argument' in lContent):
|
|
error = makeErrorDict(400, GAPI.INVALID_ARGUMENT, 'Request contains an invalid argument')
|
|
elif (e.resp['status'] == '404') and ('not found' in lContent):
|
|
error = makeErrorDict(404, GAPI.NOT_FOUND, lContent)
|
|
elif (e.resp['status'] == '404') and ('bad request' in lContent):
|
|
error = makeErrorDict(404, GAPI.BAD_REQUEST, lContent)
|
|
elif retryOnHttpError:
|
|
return (-1, None, eContent)
|
|
elif softErrors:
|
|
stderrErrorMsg(eContent)
|
|
return (0, None, None)
|
|
else:
|
|
systemErrorExit(HTTP_ERROR_RC, eContent)
|
|
if 'error' in error:
|
|
http_status = error['error']['code']
|
|
reason = ''
|
|
if 'errors' in error['error'] and 'message' in error['error']['errors'][0]:
|
|
message = error['error']['errors'][0]['message']
|
|
if 'reason' in error['error']['errors'][0]:
|
|
reason = error['error']['errors'][0]['reason']
|
|
elif 'errors' in error['error'] and 'Unknown Error' in error['error']['message'] and 'reason' in error['error']['errors'][0]:
|
|
message = error['error']['errors'][0]['reason']
|
|
else:
|
|
message = error['error']['message']
|
|
status = error['error'].get('status', '')
|
|
lmessage = message.lower() if message is not None else ''
|
|
if http_status == 500:
|
|
if not lmessage or status == 'UNKNOWN':
|
|
if not lmessage:
|
|
message = Msg.UNKNOWN
|
|
error = makeErrorDict(http_status, GAPI.UNKNOWN_ERROR, message)
|
|
elif 'backend error' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.BACKEND_ERROR, message)
|
|
elif 'internal error encountered' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.INTERNAL_ERROR, message)
|
|
elif 'role assignment exists: roleassignment' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.DUPLICATE, message)
|
|
elif 'role assignment exists: roleid' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.DUPLICATE, message)
|
|
elif 'operation not supported' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.OPERATION_NOT_SUPPORTED, message)
|
|
elif 'failed status in update settings response' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.INVALID_INPUT, message)
|
|
elif 'cannot delete a field in use.resource.fields' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.FIELD_IN_USE, message)
|
|
elif status == 'INTERNAL':
|
|
error = makeErrorDict(http_status, GAPI.INTERNAL_ERROR, message)
|
|
elif http_status == 502:
|
|
if 'bad gateway' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.BAD_GATEWAY, message)
|
|
elif http_status == 503:
|
|
if message.startswith('quota exceeded for the current request'):
|
|
error = makeErrorDict(http_status, GAPI.QUOTA_EXCEEDED, message)
|
|
elif status == 'UNAVAILABLE' or 'the service is currently unavailable' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.SERVICE_NOT_AVAILABLE, message)
|
|
elif http_status == 504:
|
|
if 'gateway timeout' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.GATEWAY_TIMEOUT, message)
|
|
elif http_status == 400:
|
|
if '@attachmentnotvisible' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.BAD_REQUEST, message)
|
|
elif status == 'INVALID_ARGUMENT':
|
|
error = makeErrorDict(http_status, GAPI.INVALID_ARGUMENT, message)
|
|
elif status == 'FAILED_PRECONDITION' or 'precondition check failed' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.FAILED_PRECONDITION, message)
|
|
elif 'does not match' in lmessage or 'invalid' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.INVALID, message)
|
|
elif http_status == 401:
|
|
if 'active session is invalid' in lmessage and reason == 'authError':
|
|
# message += ' Drive SDK API access disabled'
|
|
# message = Msg.SERVICE_NOT_ENABLED.format('Drive')
|
|
error = makeErrorDict(http_status, GAPI.AUTH_ERROR, message)
|
|
elif status == 'PERMISSION_DENIED':
|
|
error = makeErrorDict(http_status, GAPI.PERMISSION_DENIED, message)
|
|
elif status == 'UNAUTHENTICATED':
|
|
error = makeErrorDict(http_status, GAPI.AUTH_ERROR, message)
|
|
elif http_status == 403:
|
|
if 'quota exceeded for quota metric' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.QUOTA_EXCEEDED, message)
|
|
elif 'the authenticated user cannot access this service' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.SERVICE_NOT_AVAILABLE, message)
|
|
elif status == 'PERMISSION_DENIED' or 'the caller does not have permission' in lmessage or 'permission iam.serviceaccountkeys' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.PERMISSION_DENIED, message)
|
|
elif http_status == 404:
|
|
if status == 'NOT_FOUND' or 'requested entity was not found' in lmessage or 'does not exist' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.NOT_FOUND, message)
|
|
elif http_status == 409:
|
|
if status == 'ALREADY_EXISTS' or 'requested entity already exists' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.ALREADY_EXISTS, message)
|
|
elif status == 'ABORTED' or 'the operation was aborted' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.ABORTED, message)
|
|
elif http_status == 412:
|
|
if 'insufficient archived user licenses' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.INSUFFICIENT_ARCHIVED_USER_LICENSES, message)
|
|
elif http_status == 413:
|
|
if 'request too large' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.UPLOAD_TOO_LARGE, message)
|
|
elif http_status == 429:
|
|
if status == 'RESOURCE_EXHAUSTED' or 'quota exceeded' in lmessage or 'insufficient quota' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.QUOTA_EXCEEDED, message)
|
|
else:
|
|
if 'error_description' in error:
|
|
if error['error_description'] == 'Invalid Value':
|
|
message = error['error_description']
|
|
http_status = 400
|
|
error = makeErrorDict(http_status, GAPI.INVALID, message)
|
|
else:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, str(error))
|
|
else:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, str(error))
|
|
try:
|
|
reason = error['error']['errors'][0]['reason']
|
|
for messageItem in GAPI.REASON_MESSAGE_MAP.get(reason, []):
|
|
if messageItem[0] in message:
|
|
if reason in [GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND] and mapNotFound:
|
|
message = Msg.DOES_NOT_EXIST
|
|
reason = messageItem[1]
|
|
break
|
|
if reason == GAPI.INVALID_SHARING_REQUEST:
|
|
loc = message.find('User message: ')
|
|
if loc != -1:
|
|
message = message[loc+15:]
|
|
else:
|
|
loc = message.find('User message: ""')
|
|
if loc != -1:
|
|
message = message[:loc+14]+f'"{reason}"'
|
|
except KeyError:
|
|
reason = f'{http_status}'
|
|
return (http_status, reason, message)
|
|
|
|
def callGAPI(service, function,
|
|
bailOnInternalError=False, bailOnTransientError=False, bailOnInvalidError=False,
|
|
softErrors=False, mapNotFound=True,
|
|
throwReasons=None, retryReasons=None, triesLimit=0,
|
|
**kwargs):
|
|
if throwReasons is None:
|
|
throwReasons = []
|
|
if retryReasons is None:
|
|
retryReasons = []
|
|
if triesLimit == 0:
|
|
triesLimit = GC.Values[GC.API_CALLS_TRIES_LIMIT]
|
|
allRetryReasons = GAPI.DEFAULT_RETRY_REASONS+retryReasons
|
|
method = getattr(service, function)
|
|
svcparms = dict(list(kwargs.items())+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
if GC.Values[GC.API_CALLS_RATE_CHECK]:
|
|
checkAPICallsRate()
|
|
for n in range(1, triesLimit+1):
|
|
try:
|
|
return method(**svcparms).execute()
|
|
except googleapiclient.errors.HttpError as e:
|
|
http_status, reason, message = checkGAPIError(e, softErrors=softErrors, retryOnHttpError=n < 3, mapNotFound=mapNotFound)
|
|
if http_status == -1:
|
|
# The error detail indicated that we should retry this request
|
|
# We'll refresh credentials and make another pass
|
|
try:
|
|
# service._http.credentials.refresh(getHttpObj())
|
|
service._http.credentials.refresh(transportCreateRequest())
|
|
except TypeError:
|
|
systemErrorExit(HTTP_ERROR_RC, message)
|
|
continue
|
|
if http_status == 0:
|
|
return None
|
|
if (n != triesLimit) and ((reason in allRetryReasons) or
|
|
(GC.Values[GC.RETRY_API_SERVICE_NOT_AVAILABLE] and (reason == GAPI.SERVICE_NOT_AVAILABLE))):
|
|
if (reason in [GAPI.INTERNAL_ERROR, GAPI.BACKEND_ERROR] and
|
|
bailOnInternalError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]):
|
|
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
|
|
if (reason in [GAPI.INVALID] and
|
|
bailOnInvalidError and n == GC.Values[GC.BAIL_ON_INTERNAL_ERROR_TRIES]):
|
|
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
|
|
waitOnFailure(n, triesLimit, reason, message)
|
|
if reason == GAPI.TRANSIENT_ERROR and bailOnTransientError:
|
|
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
|
|
continue
|
|
if reason in throwReasons:
|
|
if reason in GAPI.REASON_EXCEPTION_MAP:
|
|
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
|
|
raise e
|
|
if softErrors:
|
|
stderrErrorMsg(f'{http_status}: {reason} - {message}{["", ": Giving up."][n > 1]}')
|
|
return None
|
|
if reason == GAPI.INSUFFICIENT_PERMISSIONS:
|
|
APIAccessDeniedExit()
|
|
systemErrorExit(HTTP_ERROR_RC, formatHTTPError(http_status, reason, message))
|
|
except googleapiclient.errors.MediaUploadSizeError as e:
|
|
raise e
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
if n != triesLimit:
|
|
service._http.connections = {}
|
|
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
|
|
continue
|
|
handleServerError(e)
|
|
except google.auth.exceptions.RefreshError as e:
|
|
if isinstance(e.args, tuple):
|
|
e = e.args[0]
|
|
handleOAuthTokenError(e, GAPI.SERVICE_NOT_AVAILABLE in throwReasons)
|
|
raise GAPI.REASON_EXCEPTION_MAP[GAPI.SERVICE_NOT_AVAILABLE](str(e))
|
|
except (http_client.ResponseNotReady, OSError) as e:
|
|
errMsg = f'Connection error: {str(e) or repr(e)}'
|
|
if n != triesLimit:
|
|
waitOnFailure(n, triesLimit, SOCKET_ERROR_RC, errMsg)
|
|
continue
|
|
if softErrors:
|
|
writeStderr(f'\n{ERROR_PREFIX}{errMsg} - Giving up.\n')
|
|
return None
|
|
systemErrorExit(SOCKET_ERROR_RC, errMsg)
|
|
except ValueError as e:
|
|
if clearServiceCache(service):
|
|
continue
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
|
|
except TypeError as e:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
|
|
|
|
def _showGAPIpagesResult(results, pageItems, totalItems, pageMessage, messageAttribute, entityType):
|
|
showMessage = pageMessage.replace(TOTAL_ITEMS_MARKER, str(totalItems))
|
|
if pageItems:
|
|
if messageAttribute:
|
|
firstItem = results[0] if pageItems > 0 else {}
|
|
lastItem = results[-1] if pageItems > 1 else firstItem
|
|
if isinstance(messageAttribute, str):
|
|
firstItem = str(firstItem.get(messageAttribute, ''))
|
|
lastItem = str(lastItem.get(messageAttribute, ''))
|
|
else:
|
|
for attr in messageAttribute:
|
|
firstItem = firstItem.get(attr, {})
|
|
lastItem = lastItem.get(attr, {})
|
|
firstItem = str(firstItem)
|
|
lastItem = str(lastItem)
|
|
showMessage = showMessage.replace(FIRST_ITEM_MARKER, firstItem)
|
|
showMessage = showMessage.replace(LAST_ITEM_MARKER, lastItem)
|
|
else:
|
|
showMessage = showMessage.replace(FIRST_ITEM_MARKER, '')
|
|
showMessage = showMessage.replace(LAST_ITEM_MARKER, '')
|
|
writeGotMessage(showMessage.replace('{0}', str(Ent.Choose(entityType, totalItems))))
|
|
|
|
def _processGAPIpagesResult(results, items, allResults, totalItems, pageMessage, messageAttribute, entityType):
|
|
if results:
|
|
pageToken = results.get('nextPageToken')
|
|
if items in results:
|
|
pageItems = len(results[items])
|
|
totalItems += pageItems
|
|
if allResults is not None:
|
|
allResults.extend(results[items])
|
|
else:
|
|
results = {items: []}
|
|
pageItems = 0
|
|
else:
|
|
pageToken = None
|
|
results = {items: []}
|
|
pageItems = 0
|
|
if pageMessage:
|
|
_showGAPIpagesResult(results[items], pageItems, totalItems, pageMessage, messageAttribute, entityType)
|
|
return (pageToken, totalItems)
|
|
|
|
def _finalizeGAPIpagesResult(pageMessage):
|
|
if pageMessage and (pageMessage[-1] != '\n'):
|
|
writeStderr('\r\n')
|
|
flushStderr()
|
|
|
|
def callGAPIpages(service, function, items,
|
|
pageMessage=None, messageAttribute=None, maxItems=0, noFinalize=False,
|
|
throwReasons=None, retryReasons=None,
|
|
pageArgsInBody=False,
|
|
**kwargs):
|
|
if throwReasons is None:
|
|
throwReasons = []
|
|
if retryReasons is None:
|
|
retryReasons = []
|
|
allResults = []
|
|
totalItems = 0
|
|
maxArg = ''
|
|
if maxItems:
|
|
maxResults = kwargs.get('maxResults', 0)
|
|
if maxResults:
|
|
maxArg = 'maxResults'
|
|
else:
|
|
maxResults = kwargs.get('pageSize', 0)
|
|
if maxResults:
|
|
maxArg = 'pageSize'
|
|
if pageArgsInBody:
|
|
kwargs.setdefault('body', {})
|
|
entityType = Ent.Getting() if pageMessage else None
|
|
while True:
|
|
if maxArg and maxItems-totalItems < maxResults:
|
|
kwargs[maxArg] = maxItems-totalItems
|
|
results = callGAPI(service, function,
|
|
throwReasons=throwReasons, retryReasons=retryReasons,
|
|
**kwargs)
|
|
pageToken, totalItems = _processGAPIpagesResult(results, items, allResults, totalItems, pageMessage, messageAttribute, entityType)
|
|
if not pageToken or (maxItems and totalItems >= maxItems):
|
|
if not noFinalize:
|
|
_finalizeGAPIpagesResult(pageMessage)
|
|
return allResults
|
|
if pageArgsInBody:
|
|
kwargs['body']['pageToken'] = pageToken
|
|
else:
|
|
kwargs['pageToken'] = pageToken
|
|
|
|
def yieldGAPIpages(service, function, items,
|
|
pageMessage=None, messageAttribute=None, maxItems=0, noFinalize=False,
|
|
throwReasons=None, retryReasons=None,
|
|
pageArgsInBody=False,
|
|
**kwargs):
|
|
if throwReasons is None:
|
|
throwReasons = []
|
|
if retryReasons is None:
|
|
retryReasons = []
|
|
totalItems = 0
|
|
maxArg = ''
|
|
if maxItems:
|
|
maxResults = kwargs.get('maxResults', 0)
|
|
if maxResults:
|
|
maxArg = 'maxResults'
|
|
else:
|
|
maxResults = kwargs.get('pageSize', 0)
|
|
if maxResults:
|
|
maxArg = 'pageSize'
|
|
if pageArgsInBody:
|
|
kwargs.setdefault('body', {})
|
|
entityType = Ent.Getting() if pageMessage else None
|
|
while True:
|
|
if maxArg and maxItems-totalItems < maxResults:
|
|
kwargs[maxArg] = maxItems-totalItems
|
|
results = callGAPI(service, function,
|
|
throwReasons=throwReasons, retryReasons=retryReasons,
|
|
**kwargs)
|
|
if results:
|
|
pageToken = results.get('nextPageToken')
|
|
if items in results:
|
|
pageItems = len(results[items])
|
|
totalItems += pageItems
|
|
else:
|
|
results = {items: []}
|
|
pageItems = 0
|
|
else:
|
|
pageToken = None
|
|
results = {items: []}
|
|
pageItems = 0
|
|
if pageMessage:
|
|
_showGAPIpagesResult(results[items], pageItems, totalItems, pageMessage, messageAttribute, entityType)
|
|
yield results[items]
|
|
if not pageToken or (maxItems and totalItems >= maxItems):
|
|
if not noFinalize:
|
|
_finalizeGAPIpagesResult(pageMessage)
|
|
return
|
|
if pageArgsInBody:
|
|
kwargs['body']['pageToken'] = pageToken
|
|
else:
|
|
kwargs['pageToken'] = pageToken
|
|
|
|
def callGAPIitems(service, function, items,
|
|
throwReasons=None, retryReasons=None,
|
|
**kwargs):
|
|
if throwReasons is None:
|
|
throwReasons = []
|
|
if retryReasons is None:
|
|
retryReasons = []
|
|
results = callGAPI(service, function,
|
|
throwReasons=throwReasons, retryReasons=retryReasons,
|
|
**kwargs)
|
|
if results:
|
|
return results.get(items, [])
|
|
return []
|
|
|
|
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'):
|
|
json_string = readFile(os.path.join(sys._MEIPASS, disc_filename), continueOnError=True, displayError=True) #pylint: disable=no-member
|
|
elif os.path.isfile(disc_file):
|
|
json_string = readFile(disc_file, continueOnError=True, displayError=True)
|
|
else:
|
|
json_string = None
|
|
if not json_string:
|
|
invalidDiscoveryJsonExit(disc_file, Msg.NO_DATA)
|
|
try:
|
|
discovery = json.loads(json_string)
|
|
return (disc_file, discovery)
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
invalidDiscoveryJsonExit(disc_file, str(e))
|
|
|
|
def buildGAPIObject(api, credentials=None):
|
|
if credentials is None:
|
|
credentials = getClientCredentials(api=api, refreshOnly=True)
|
|
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj(cache=GM.Globals[GM.CACHE_DIR]))
|
|
service = getService(api, httpObj)
|
|
if not GC.Values[GC.ENABLE_DASA]:
|
|
try:
|
|
API_Scopes = set(list(service._rootDesc['auth']['oauth2']['scopes']))
|
|
except KeyError:
|
|
API_Scopes = set(API.VAULT_SCOPES) if api == API.VAULT else set()
|
|
GM.Globals[GM.CURRENT_CLIENT_API] = api
|
|
GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = API_Scopes.intersection(GM.Globals[GM.CREDENTIALS_SCOPES])
|
|
if api not in API.SCOPELESS_APIS and not GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]:
|
|
systemErrorExit(NO_SCOPES_FOR_API_RC, Msg.NO_SCOPES_FOR_API.format(API.getAPIName(api)))
|
|
if not GC.Values[GC.DOMAIN]:
|
|
GC.Values[GC.DOMAIN] = GM.Globals[GM.DECODED_ID_TOKEN].get('hd', 'UNKNOWN').lower()
|
|
if not GC.Values[GC.CUSTOMER_ID]:
|
|
GC.Values[GC.CUSTOMER_ID] = GC.MY_CUSTOMER
|
|
GM.Globals[GM.ADMIN] = GM.Globals[GM.DECODED_ID_TOKEN].get('email', 'UNKNOWN').lower()
|
|
GM.Globals[GM.OAUTH2_CLIENT_ID] = credentials.client_id
|
|
return service
|
|
|
|
def getSaUser(user):
|
|
currentClientAPI = GM.Globals[GM.CURRENT_CLIENT_API]
|
|
currentClientAPIScopes = GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]
|
|
userEmail = convertUIDtoEmailAddress(user) if user else None
|
|
GM.Globals[GM.CURRENT_CLIENT_API] = currentClientAPI
|
|
GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = currentClientAPIScopes
|
|
return userEmail
|
|
|
|
def buildGAPIServiceObject(api, user, i=0, count=0, displayError=True):
|
|
userEmail = getSaUser(user)
|
|
httpObj = getHttpObj(cache=GM.Globals[GM.CACHE_DIR])
|
|
service = getService(api, httpObj)
|
|
if api == API.MEET_BETA:
|
|
api = API.MEET
|
|
credentials = getSvcAcctCredentials(api, userEmail)
|
|
request = transportCreateRequest(httpObj)
|
|
triesLimit = 3
|
|
for n in range(1, triesLimit+1):
|
|
try:
|
|
credentials.refresh(request)
|
|
service._http = transportAuthorizedHttp(credentials, http=httpObj)
|
|
return (userEmail, service)
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
if n != triesLimit:
|
|
httpObj.connections = {}
|
|
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
|
|
continue
|
|
handleServerError(e)
|
|
except google.auth.exceptions.RefreshError as e:
|
|
if isinstance(e.args, tuple):
|
|
e = e.args[0]
|
|
if n < triesLimit:
|
|
if isinstance(e, str):
|
|
eContent = e
|
|
else:
|
|
eContent = e.content.decode(UTF8) if isinstance(e.content, bytes) else e.content
|
|
if eContent[0:15] == '<!DOCTYPE html>':
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
writeStdout(f'{ERROR_PREFIX} HTTP: {str(eContent)}\n')
|
|
lContent = eContent.lower()
|
|
tg = HTML_TITLE_PATTERN.match(lContent)
|
|
lContent = tg.group(1) if tg else ''
|
|
if lContent.startswith('Error 502 (Server Error)'):
|
|
time.sleep(30)
|
|
continue
|
|
handleOAuthTokenError(e, True, displayError, i, count)
|
|
return (userEmail, None)
|
|
|
|
def buildGAPIObjectNoAuthentication(api):
|
|
httpObj = getHttpObj(cache=GM.Globals[GM.CACHE_DIR])
|
|
service = getService(api, httpObj)
|
|
return service
|
|
|
|
def initGDataObject(gdataObj, api):
|
|
GM.Globals[GM.CURRENT_CLIENT_API] = api
|
|
credentials = getClientCredentials(noDASA=True, refreshOnly=True)
|
|
GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = API.getClientScopesSet(api).intersection(GM.Globals[GM.CREDENTIALS_SCOPES])
|
|
if not GM.Globals[GM.CURRENT_CLIENT_API_SCOPES]:
|
|
systemErrorExit(NO_SCOPES_FOR_API_RC, Msg.NO_SCOPES_FOR_API.format(API.getAPIName(api)))
|
|
getGDataOAuthToken(gdataObj, credentials)
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
gdataObj.debug = True
|
|
return gdataObj
|
|
|
|
def getGDataUserCredentials(api, user, i, count):
|
|
userEmail = getSaUser(user)
|
|
credentials = getSvcAcctCredentials(api, userEmail)
|
|
request = transportCreateRequest()
|
|
try:
|
|
credentials.refresh(request)
|
|
return (userEmail, credentials)
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
handleServerError(e)
|
|
except google.auth.exceptions.RefreshError as e:
|
|
if isinstance(e.args, tuple):
|
|
e = e.args[0]
|
|
handleOAuthTokenError(e, True, True, i, count)
|
|
return (userEmail, None)
|
|
|
|
def getContactsObject(contactFeed):
|
|
contactsObject = initGDataObject(gdata.apps.contacts.service.ContactsService(contactFeed=contactFeed),
|
|
API.CONTACTS)
|
|
return (GC.Values[GC.DOMAIN], contactsObject)
|
|
|
|
def getContactsQuery(**kwargs):
|
|
if GC.Values[GC.NO_VERIFY_SSL]:
|
|
ssl._create_default_https_context = ssl._create_unverified_context
|
|
return gdata.apps.contacts.service.ContactsQuery(**kwargs)
|
|
|
|
def getEmailAuditObject():
|
|
return initGDataObject(gdata.apps.audit.service.AuditService(), API.EMAIL_AUDIT)
|
|
|
|
def getUserEmailFromID(uid, cd):
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=uid, fields='primaryEmail')
|
|
return result.get('primaryEmail')
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
return None
|
|
|
|
def getGroupEmailFromID(uid, cd):
|
|
try:
|
|
result = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
|
|
groupKey=uid, fields='email')
|
|
return result.get('email')
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
|
|
return None
|
|
|
|
def getServiceAccountEmailFromID(account_id, sal=None):
|
|
if sal is None:
|
|
sal = buildGAPIObject(API.SERVICEACCOUNTLOOKUP)
|
|
try:
|
|
certs = callGAPI(sal.serviceaccounts(), 'lookup',
|
|
throwReasons = [GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_ARGUMENT],
|
|
account=account_id)
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.resourceNotFound, GAPI.invalidArgument):
|
|
return None
|
|
sa_cn_rx = r'CN=(.+)\.(.+)\.iam\.gservice.*'
|
|
sa_emails = []
|
|
for _, raw_cert in certs.items():
|
|
cert = x509.load_pem_x509_certificate(raw_cert.encode(), default_backend())
|
|
# suppress crytography warning due to long service account email
|
|
with warnings.catch_warnings():
|
|
warnings.filterwarnings('ignore', message='.*Attribute\'s length.*')
|
|
mg = re.match(sa_cn_rx, cert.issuer.rfc4514_string())
|
|
if mg:
|
|
sa_email = f'{mg.group(1)}@{mg.group(2)}.iam.gserviceaccount.com'
|
|
if sa_email not in sa_emails:
|
|
sa_emails.append(sa_email)
|
|
return GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER].join(sa_emails)
|
|
|
|
# Convert UID to email address and type
|
|
def convertUIDtoEmailAddressWithType(emailAddressOrUID, cd=None, sal=None, emailTypes=None,
|
|
checkForCustomerId=False, ciGroupsAPI=False, aliasAllowed=True):
|
|
if emailTypes is None:
|
|
emailTypes = ['user']
|
|
elif not isinstance(emailTypes, list):
|
|
emailTypes = [emailTypes] if emailTypes != 'any' else ['user', 'group']
|
|
if checkForCustomerId and (emailAddressOrUID == GC.Values[GC.CUSTOMER_ID]):
|
|
return (emailAddressOrUID, 'email')
|
|
normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID, ciGroupsAPI=ciGroupsAPI)
|
|
if ciGroupsAPI and emailAddressOrUID.startswith('groups/'):
|
|
return emailAddressOrUID
|
|
if normalizedEmailAddressOrUID.find('@') > 0 and aliasAllowed:
|
|
return (normalizedEmailAddressOrUID, 'email')
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
if 'user' in emailTypes and 'group' in emailTypes:
|
|
# Google User IDs *TEND* to be integers while groups tend to have letters
|
|
# thus we can optimize which check we try first. We'll still check
|
|
# both since there is no guarantee this will always be true.
|
|
if normalizedEmailAddressOrUID.isdigit():
|
|
uid = getUserEmailFromID(normalizedEmailAddressOrUID, cd)
|
|
if uid:
|
|
return (uid, 'user')
|
|
uid = getGroupEmailFromID(normalizedEmailAddressOrUID, cd)
|
|
if uid:
|
|
return (uid, 'group')
|
|
else:
|
|
uid = getGroupEmailFromID(normalizedEmailAddressOrUID, cd)
|
|
if uid:
|
|
return (uid, 'group')
|
|
uid = getUserEmailFromID(normalizedEmailAddressOrUID, cd)
|
|
if uid:
|
|
return (uid, 'user')
|
|
elif 'user' in emailTypes:
|
|
uid = getUserEmailFromID(normalizedEmailAddressOrUID, cd)
|
|
if uid:
|
|
return (uid, 'user')
|
|
elif 'group' in emailTypes:
|
|
uid = getGroupEmailFromID(normalizedEmailAddressOrUID, cd)
|
|
if uid:
|
|
return (uid, 'group')
|
|
if 'resource' in emailTypes:
|
|
try:
|
|
result = callGAPI(cd.resources().calendars(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
calendarResourceId=normalizedEmailAddressOrUID,
|
|
customer=GC.Values[GC.CUSTOMER_ID], fields='resourceEmail')
|
|
if 'resourceEmail' in result:
|
|
return (result['resourceEmail'].lower(), 'resource')
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
pass
|
|
if 'serviceaccount' in emailTypes:
|
|
uid = getServiceAccountEmailFromID(normalizedEmailAddressOrUID, sal)
|
|
if uid:
|
|
return (uid, 'serviceaccount')
|
|
return (normalizedEmailAddressOrUID, 'unknown')
|
|
|
|
NON_EMAIL_MEMBER_PREFIXES = (
|
|
"cbcm-browser.",
|
|
"chrome-os-device.",
|
|
)
|
|
# Convert UID to email address
|
|
def convertUIDtoEmailAddress(emailAddressOrUID, cd=None, emailTypes=None,
|
|
checkForCustomerId=False, ciGroupsAPI=False, aliasAllowed=True):
|
|
if ciGroupsAPI:
|
|
if emailAddressOrUID.startswith(NON_EMAIL_MEMBER_PREFIXES):
|
|
return emailAddressOrUID
|
|
normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID, ciGroupsAPI=ciGroupsAPI)
|
|
if normalizedEmailAddressOrUID.startswith(NON_EMAIL_MEMBER_PREFIXES):
|
|
return normalizedEmailAddressOrUID
|
|
email, _ = convertUIDtoEmailAddressWithType(emailAddressOrUID, cd, emailTypes,
|
|
checkForCustomerId, ciGroupsAPI, aliasAllowed)
|
|
return email
|
|
|
|
# Convert email address to User/Group UID; called immediately after getting email address from command line
|
|
def convertEmailAddressToUID(emailAddressOrUID, cd=None, emailType='user', savedLocation=None):
|
|
normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID)
|
|
if normalizedEmailAddressOrUID.find('@') == -1:
|
|
return normalizedEmailAddressOrUID
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
if emailType != 'group':
|
|
try:
|
|
return callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=normalizedEmailAddressOrUID, fields='id')['id']
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
if emailType == 'user':
|
|
if savedLocation is not None:
|
|
Cmd.SetLocation(savedLocation)
|
|
entityDoesNotExistExit(Ent.USER, normalizedEmailAddressOrUID, errMsg=getPhraseDNEorSNA(normalizedEmailAddressOrUID))
|
|
try:
|
|
return callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=normalizedEmailAddressOrUID, fields='id')['id']
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError):
|
|
if savedLocation is not None:
|
|
Cmd.SetLocation(savedLocation)
|
|
entityDoesNotExistExit([Ent.USER, Ent.GROUP][emailType == 'group'], normalizedEmailAddressOrUID, errMsg=getPhraseDNEorSNA(normalizedEmailAddressOrUID))
|
|
|
|
# Convert User UID from API call to email address
|
|
def convertUserIDtoEmail(uid, cd=None):
|
|
primaryEmail = GM.Globals[GM.MAP_USER_ID_TO_NAME].get(uid)
|
|
if not primaryEmail:
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
try:
|
|
primaryEmail = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=uid, fields='primaryEmail')['primaryEmail']
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
primaryEmail = f'uid:{uid}'
|
|
GM.Globals[GM.MAP_USER_ID_TO_NAME][uid] = primaryEmail
|
|
return primaryEmail
|
|
|
|
# Convert UID to split email address
|
|
# Return (foo@bar.com, foo, bar.com)
|
|
def splitEmailAddressOrUID(emailAddressOrUID):
|
|
normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID)
|
|
atLoc = normalizedEmailAddressOrUID.find('@')
|
|
if atLoc > 0:
|
|
return (normalizedEmailAddressOrUID, normalizedEmailAddressOrUID[:atLoc], normalizedEmailAddressOrUID[atLoc+1:])
|
|
try:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=normalizedEmailAddressOrUID, fields='primaryEmail')
|
|
if 'primaryEmail' in result:
|
|
normalizedEmailAddressOrUID = result['primaryEmail'].lower()
|
|
atLoc = normalizedEmailAddressOrUID.find('@')
|
|
return (normalizedEmailAddressOrUID, normalizedEmailAddressOrUID[:atLoc], normalizedEmailAddressOrUID[atLoc+1:])
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
pass
|
|
return (normalizedEmailAddressOrUID, normalizedEmailAddressOrUID, GC.Values[GC.DOMAIN])
|
|
|
|
# Convert Org Unit Id to Org Unit Path
|
|
def convertOrgUnitIDtoPath(cd, orgUnitId):
|
|
if orgUnitId.lower().startswith('orgunits/'):
|
|
orgUnitId = f'id:{orgUnitId[9:]}'
|
|
orgUnitPath = GM.Globals[GM.MAP_ORGUNIT_ID_TO_NAME].get(orgUnitId)
|
|
if not orgUnitPath:
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
try:
|
|
orgUnitPath = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnitId, fields='orgUnitPath')['orgUnitPath']
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
orgUnitPath = orgUnitId
|
|
GM.Globals[GM.MAP_ORGUNIT_ID_TO_NAME][orgUnitId] = orgUnitPath
|
|
return orgUnitPath
|
|
|
|
def shlexSplitList(entity, dataDelimiter=' ,'):
|
|
lexer = shlex.shlex(entity, posix=True)
|
|
lexer.whitespace = dataDelimiter
|
|
lexer.whitespace_split = True
|
|
try:
|
|
return list(lexer)
|
|
except ValueError as e:
|
|
Cmd.Backup()
|
|
usageErrorExit(str(e))
|
|
|
|
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):
|
|
if myarg in {'query', 'filter'}:
|
|
return [getString(Cmd.OB_QUERY)]
|
|
return shlexSplitList(getString(Cmd.OB_QUERY_LIST))
|
|
|
|
def convertEntityToList(entity, shlexSplit=False, nonListEntityType=False):
|
|
if not entity:
|
|
return []
|
|
if isinstance(entity, (list, set, dict)):
|
|
return list(entity)
|
|
if nonListEntityType:
|
|
return [entity.strip()]
|
|
if not shlexSplit:
|
|
return entity.replace(',', ' ').split()
|
|
return shlexSplitList(entity)
|
|
|
|
GROUP_ROLES_MAP = {
|
|
'owner': Ent.ROLE_OWNER,
|
|
'owners': Ent.ROLE_OWNER,
|
|
'manager': Ent.ROLE_MANAGER,
|
|
'managers': Ent.ROLE_MANAGER,
|
|
'member': Ent.ROLE_MEMBER,
|
|
'members': Ent.ROLE_MEMBER,
|
|
}
|
|
ALL_GROUP_ROLES = {Ent.ROLE_MANAGER, Ent.ROLE_MEMBER, Ent.ROLE_OWNER}
|
|
|
|
def _getRoleVerification(memberRoles, fields):
|
|
if memberRoles and memberRoles.find(Ent.ROLE_MEMBER) != -1:
|
|
return (set(memberRoles.split(',')), None, fields if fields.find('role') != -1 else fields[:-1]+',role)')
|
|
return (set(), memberRoles, fields)
|
|
|
|
def _getCIRoleVerification(memberRoles):
|
|
if memberRoles:
|
|
return set(memberRoles.split(','))
|
|
return set()
|
|
|
|
def _checkMemberStatusIsSuspendedIsArchived(memberStatus, isSuspended, isArchived):
|
|
if isSuspended is None and isArchived is None:
|
|
return True
|
|
if isSuspended is not None and isArchived is not None:
|
|
if isSuspended == isArchived:
|
|
if not isSuspended:
|
|
return memberStatus not in {'SUSPENDED', 'ARCHIVED'}
|
|
return memberStatus in {'SUSPENDED', 'ARCHIVED'}
|
|
if isSuspended:
|
|
return memberStatus == 'SUSPENDED'
|
|
return memberStatus == 'ARCHIVED'
|
|
if isSuspended is not None:
|
|
if (not isSuspended and memberStatus != 'SUSPENDED') or (isSuspended and memberStatus == 'SUSPENDED'):
|
|
return True
|
|
if isArchived is not None:
|
|
if (not isArchived and memberStatus != 'ARCHIVED') or (isArchived and memberStatus == 'ARCHIVED'):
|
|
return True
|
|
return False
|
|
|
|
def _checkMemberIsSuspendedIsArchived(member, isSuspended, isArchived):
|
|
return _checkMemberStatusIsSuspendedIsArchived(member.get('status', 'UNKNOWN'), isSuspended, isArchived)
|
|
|
|
def _checkMemberRole(member, validRoles):
|
|
return not validRoles or member.get('role', Ent.ROLE_MEMBER) in validRoles
|
|
|
|
def _checkMemberRoleIsSuspendedIsArchived(member, validRoles, isSuspended, isArchived):
|
|
return _checkMemberRole(member, validRoles) and _checkMemberIsSuspendedIsArchived(member, isSuspended, isArchived)
|
|
|
|
def _checkMemberCategory(member, memberDisplayOptions):
|
|
member_email = member.get('email', member.get('id', ''))
|
|
if member_email.find('@') > 0:
|
|
_, domain = member_email.lower().split('@', 1)
|
|
category = 'internal' if domain in memberDisplayOptions['internalDomains'] else 'external'
|
|
else:
|
|
category = 'internal'
|
|
if memberDisplayOptions[category]:
|
|
member['category'] = category
|
|
return True
|
|
return False
|
|
|
|
def _checkCIMemberCategory(member, memberDisplayOptions):
|
|
member_email = member.get('preferredMemberKey', {}).get('id', '')
|
|
if member_email.find('@') > 0:
|
|
_, domain = member_email.lower().split('@', 1)
|
|
category = 'internal' if domain in memberDisplayOptions['internalDomains'] else 'external'
|
|
else:
|
|
category = 'internal'
|
|
if memberDisplayOptions[category]:
|
|
member['category'] = category
|
|
return True
|
|
return False
|
|
|
|
def getCIGroupMemberRoleFixType(member):
|
|
''' fixes missing type and returns the highest role of member '''
|
|
if 'type' not in member:
|
|
if member['preferredMemberKey']['id'] == GC.Values[GC.CUSTOMER_ID]:
|
|
member['type'] = Ent.TYPE_CUSTOMER
|
|
else:
|
|
member['type'] = Ent.TYPE_OTHER
|
|
roles = {}
|
|
memberRoles = member.get('roles', [{'name': Ent.MEMBER}])
|
|
for role in memberRoles:
|
|
roles[role['name']] = role
|
|
for a_role in [Ent.ROLE_OWNER, Ent.ROLE_MANAGER, Ent.ROLE_MEMBER]:
|
|
if a_role in roles:
|
|
member['role'] = a_role
|
|
if 'expiryDetail' in roles[a_role]:
|
|
member['expireTime'] = roles[a_role]['expiryDetail']['expireTime']
|
|
return
|
|
member['role'] = memberRoles[0]['name']
|
|
|
|
def getCIGroupTransitiveMemberRoleFixType(groupName, tmember):
|
|
''' map transitive member to normal member '''
|
|
tid = tmember['preferredMemberKey'][0].get('id', GC.Values[GC.CUSTOMER_ID]) if tmember['preferredMemberKey'] else ''
|
|
ttype, tname = tmember['member'].split('/')
|
|
member = {'name': f'{groupName}/membershipd/{tname}', 'preferredMemberKey': {'id': tid}}
|
|
if 'type' not in tmember:
|
|
if tid == GC.Values[GC.CUSTOMER_ID]:
|
|
member['type'] = Ent.TYPE_CUSTOMER
|
|
elif ttype == 'users':
|
|
member['type'] = Ent.TYPE_USER if not tid.endswith('.iam.gserviceaccount.com') else Ent.TYPE_SERVICE_ACCOUNT
|
|
elif ttype == 'groups':
|
|
member['type'] = Ent.TYPE_GROUP
|
|
elif tid.startswith('cbcm-browser.'):
|
|
member['type'] = Ent.TYPE_CBCM_BROWSER
|
|
else:
|
|
member['type'] = Ent.TYPE_OTHER
|
|
else:
|
|
member['type'] = tmember['type']
|
|
if 'roles' in tmember:
|
|
memberRoles = []
|
|
for trole in tmember['roles']:
|
|
if 'role' in trole:
|
|
trole['name'] = trole.pop('role')
|
|
if trole['name'] == 'ADMIN':
|
|
trole['name'] = Ent.ROLE_MANAGER
|
|
memberRoles.append(trole)
|
|
else:
|
|
memberRoles = [{'name': Ent.MEMBER}]
|
|
roles = {}
|
|
for role in memberRoles:
|
|
roles[role['name']] = role
|
|
for a_role in [Ent.ROLE_OWNER, Ent.ROLE_MANAGER, Ent.ROLE_MEMBER]:
|
|
if a_role in roles:
|
|
member['role'] = a_role
|
|
if 'expiryDetail' in roles[a_role]:
|
|
member['expireTime'] = roles[a_role]['expiryDetail']['expireTime']
|
|
break
|
|
else:
|
|
member['role'] = memberRoles[0]['name']
|
|
return member
|
|
|
|
def convertGroupCloudIDToEmail(ci, group, i=0, count=0):
|
|
if not group.startswith('groups/'):
|
|
group = normalizeEmailAddressOrUID(group, ciGroupsAPI=True)
|
|
if not group.startswith('groups/'):
|
|
return (ci, None, group)
|
|
if not ci:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
try:
|
|
ciGroup = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=group, fields='groupKey(id)')
|
|
return (ci, None, ciGroup['groupKey']['id'])
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
action = Act.Get()
|
|
Act.Set(Act.LOOKUP)
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group, Ent.GROUP, None], str(e), i, count)
|
|
Act.Set(action)
|
|
return (ci, None, None)
|
|
|
|
def convertGroupEmailToCloudID(ci, group, i=0, count=0):
|
|
group = normalizeEmailAddressOrUID(group, ciGroupsAPI=True)
|
|
if not group.startswith('groups/') and group.find('@') == -1:
|
|
group = 'groups/'+group
|
|
if group.startswith('groups/'):
|
|
ci, _, groupEmail = convertGroupCloudIDToEmail(ci, group, i, count)
|
|
return (ci, group, groupEmail)
|
|
if not ci:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
try:
|
|
ciGroup = callGAPI(ci.groups(), 'lookup',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
groupKey_id=group, fields='name')
|
|
return (ci, ciGroup['name'], group)
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
action = Act.Get()
|
|
Act.Set(Act.LOOKUP)
|
|
entityActionFailedWarning([Ent.GROUP, group, Ent.CLOUD_IDENTITY_GROUP, None], str(e), i, count)
|
|
Act.Set(action)
|
|
return (ci, None, None)
|
|
|
|
CIGROUP_DISCUSSION_FORUM_LABEL = 'cloudidentity.googleapis.com/groups.discussion_forum'
|
|
CIGROUP_DYNAMIC_LABEL = 'cloudidentity.googleapis.com/groups.dynamic'
|
|
CIGROUP_SECURITY_LABEL = 'cloudidentity.googleapis.com/groups.security'
|
|
CIGROUP_LOCKED_LABEL = 'cloudidentity.googleapis.com/groups.locked'
|
|
|
|
def getCIGroupMembershipGraph(ci, member):
|
|
if not ci:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
parent = 'groups/-'
|
|
try:
|
|
result = callGAPI(ci.groups().memberships(), 'getMembershipGraph',
|
|
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
parent=parent,
|
|
query=f"member_key_id == '{member}' && CIGROUP_DISCUSSION_FORUM_LABEL in labels")
|
|
return (ci, result.get('response', {}))
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
action = Act.Get()
|
|
Act.Set(Act.LOOKUP)
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, parent], str(e))
|
|
Act.Set(action)
|
|
return (ci, None)
|
|
|
|
def checkGroupExists(cd, ci, ciGroupsAPI, group, i=0, count=0):
|
|
group = normalizeEmailAddressOrUID(group, ciGroupsAPI=ciGroupsAPI)
|
|
if not ciGroupsAPI:
|
|
if not group.startswith('groups/'):
|
|
try:
|
|
result = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=group, fields='email')
|
|
return (ci, None, result['email'])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError):
|
|
entityUnknownWarning(Ent.GROUP, group, i, count)
|
|
return (ci, None, None)
|
|
else:
|
|
ci, _, groupEmail = convertGroupCloudIDToEmail(ci, group, i, count)
|
|
return (ci, None, groupEmail)
|
|
else:
|
|
if not group.startswith('groups/') and group.find('@') == -1:
|
|
group = 'groups/'+group
|
|
if group.startswith('groups/'):
|
|
try:
|
|
result = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=group, fields='name,groupKey(id)')
|
|
return (ci, result['name'], result['groupKey']['id'])
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, group, i, count)
|
|
return (ci, None, None)
|
|
else:
|
|
return convertGroupEmailToCloudID(ci, group, i, count)
|
|
|
|
# Turn the entity into a list of Users/CrOS devices
|
|
def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isArchived=None,
|
|
groupMemberType=Ent.TYPE_USER, noListConversion=False, recursive=False, noCLArgs=False):
|
|
def _incrEntityDoesNotExist(entityType):
|
|
entityError['entityType'] = entityType
|
|
entityError[ENTITY_ERROR_DNE] += 1
|
|
|
|
def _showInvalidEntity(entityType, entityName):
|
|
entityError['entityType'] = entityType
|
|
entityError[ENTITY_ERROR_INVALID] += 1
|
|
printErrorMessage(INVALID_ENTITY_RC, formatKeyValueList('', [Ent.Singular(entityType), entityName, Msg.INVALID], ''))
|
|
|
|
def _addGroupUsersToUsers(group, domains, recursive, includeDerivedMembership):
|
|
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, group, entityType=Ent.GROUP)
|
|
validRoles, listRoles, listFields = _getRoleVerification(memberRoles, 'nextPageToken,members(email,type,status)')
|
|
try:
|
|
result = callGAPIpages(cd.members(), 'list', 'members',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
includeDerivedMembership=includeDerivedMembership,
|
|
groupKey=group, roles=listRoles, fields=listFields, maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(Ent.GROUP, group)
|
|
_incrEntityDoesNotExist(Ent.GROUP)
|
|
return
|
|
for member in result:
|
|
if member['type'] == Ent.TYPE_USER:
|
|
email = member['email'].lower()
|
|
if email in entitySet:
|
|
continue
|
|
if _checkMemberRoleIsSuspendedIsArchived(member, validRoles, isSuspended, isArchived):
|
|
if domains:
|
|
_, domain = splitEmailAddress(email)
|
|
if domain not in domains:
|
|
continue
|
|
entitySet.add(email)
|
|
entityList.append(email)
|
|
elif recursive and member['type'] == Ent.TYPE_GROUP:
|
|
_addGroupUsersToUsers(member['email'], domains, recursive, includeDerivedMembership)
|
|
|
|
def _addCIGroupUsersToUsers(groupName, groupEmail, recursive):
|
|
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, groupEmail, entityType=Ent.CLOUD_IDENTITY_GROUP)
|
|
validRoles = _getCIRoleVerification(memberRoles)
|
|
try:
|
|
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
parent=groupName, view='FULL',
|
|
fields='nextPageToken,memberships(name,preferredMemberKey(id),roles(name),type)', pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupEmail)
|
|
_incrEntityDoesNotExist(Ent.CLOUD_IDENTITY_GROUP)
|
|
return
|
|
for member in result:
|
|
getCIGroupMemberRoleFixType(member)
|
|
if member['type'] == Ent.TYPE_USER:
|
|
email = member.get('preferredMemberKey', {}).get('id', '')
|
|
if (email and _checkMemberRole(member, validRoles) and email not in entitySet):
|
|
entitySet.add(email)
|
|
entityList.append(email)
|
|
elif recursive and member['type'] == Ent.TYPE_GROUP and _checkMemberRole(member, validRoles):
|
|
_, gname = member['name'].rsplit('/', 1)
|
|
_addCIGroupUsersToUsers(f'groups/{gname}', f'groups/{gname}', recursive)
|
|
|
|
GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE] = False
|
|
ENTITY_ERROR_DNE = 'doesNotExist'
|
|
ENTITY_ERROR_INVALID = 'invalid'
|
|
entityError = {'entityType': None, ENTITY_ERROR_DNE: 0, ENTITY_ERROR_INVALID: 0}
|
|
entityList = []
|
|
entitySet = set()
|
|
entityLocation = Cmd.Location()
|
|
if entityType in {Cmd.ENTITY_USER, Cmd.ENTITY_USERS}:
|
|
if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] and not GC.Values[GC.DOMAIN]:
|
|
buildGAPIObject(API.DIRECTORY)
|
|
result = convertEntityToList(entity, nonListEntityType=entityType == Cmd.ENTITY_USER)
|
|
for user in result:
|
|
if validateEmailAddressOrUID(user):
|
|
if user not in entitySet:
|
|
entitySet.add(user)
|
|
entityList.append(user)
|
|
else:
|
|
_showInvalidEntity(Ent.USER, user)
|
|
if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
|
|
return entityList
|
|
elif entityType in {Cmd.ENTITY_ALL_USERS, Cmd.ENTITY_ALL_USERS_NS, Cmd.ENTITY_ALL_USERS_NS_SUSP, Cmd.ENTITY_ALL_USERS_SUSP}:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
if entityType == Cmd.ENTITY_ALL_USERS and isSuspended is not None:
|
|
query = f'isSuspended={isSuspended}'
|
|
else:
|
|
query = Cmd.ALL_USERS_QUERY_MAP[entityType]
|
|
printGettingAllAccountEntities(Ent.USER)
|
|
try:
|
|
result = callGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID],
|
|
query=query, orderBy='email', fields='nextPageToken,users(primaryEmail,archived)',
|
|
maxResults=GC.Values[GC.USER_MAX_RESULTS])
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
entityList = [user['primaryEmail'] for user in result if isArchived is None or isArchived == user['archived']]
|
|
printGotAccountEntities(len(entityList))
|
|
elif entityType in {Cmd.ENTITY_DOMAINS, Cmd.ENTITY_DOMAINS_NS, Cmd.ENTITY_DOMAINS_SUSP}:
|
|
if entityType == Cmd.ENTITY_DOMAINS_NS:
|
|
query = 'isSuspended=False'
|
|
elif entityType == Cmd.ENTITY_DOMAINS_SUSP:
|
|
query = 'isSuspended=True'
|
|
elif isSuspended is not None:
|
|
query = f'isSuspended={isSuspended}'
|
|
else:
|
|
query = None
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
domains = convertEntityToList(entity)
|
|
for domain in domains:
|
|
printGettingAllEntityItemsForWhom(Ent.USER, domain, entityType=Ent.DOMAIN)
|
|
try:
|
|
result = callGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN],
|
|
domain=domain,
|
|
query=query, orderBy='email', fields='nextPageToken,users(primaryEmail,archived)',
|
|
maxResults=GC.Values[GC.USER_MAX_RESULTS])
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.DOMAIN, domain)
|
|
_incrEntityDoesNotExist(Ent.DOMAIN)
|
|
continue
|
|
entityList = [user['primaryEmail'] for user in result if isArchived is None or isArchived == user['archived']]
|
|
printGotAccountEntities(len(entityList))
|
|
elif entityType in {Cmd.ENTITY_GROUP, Cmd.ENTITY_GROUPS,
|
|
Cmd.ENTITY_GROUP_NS, Cmd.ENTITY_GROUPS_NS,
|
|
Cmd.ENTITY_GROUP_SUSP, Cmd.ENTITY_GROUPS_SUSP,
|
|
Cmd.ENTITY_GROUP_INDE, Cmd.ENTITY_GROUPS_INDE}:
|
|
if entityType in {Cmd.ENTITY_GROUP_NS, Cmd.ENTITY_GROUPS_NS}:
|
|
isSuspended = False
|
|
elif entityType in {Cmd.ENTITY_GROUP_SUSP, Cmd.ENTITY_GROUPS_SUSP}:
|
|
isSuspended = True
|
|
includeDerivedMembership = entityType in {Cmd.ENTITY_GROUP_INDE, Cmd.ENTITY_GROUPS_INDE}
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
groups = convertEntityToList(entity, nonListEntityType=entityType in {Cmd.ENTITY_GROUP, Cmd.ENTITY_GROUP_NS, Cmd.ENTITY_GROUP_SUSP, Cmd.ENTITY_GROUP_INDE})
|
|
for group in groups:
|
|
if validateEmailAddressOrUID(group, checkPeople=False):
|
|
group = normalizeEmailAddressOrUID(group)
|
|
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, group, entityType=Ent.GROUP)
|
|
validRoles, listRoles, listFields = _getRoleVerification(memberRoles, 'nextPageToken,members(email,id,type,status)')
|
|
try:
|
|
result = callGAPIpages(cd.members(), 'list', 'members',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
includeDerivedMembership=includeDerivedMembership,
|
|
groupKey=group, roles=listRoles, fields=listFields, maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(Ent.GROUP, group)
|
|
_incrEntityDoesNotExist(Ent.GROUP)
|
|
continue
|
|
for member in result:
|
|
email = member['email'].lower() if member['type'] != Ent.TYPE_CUSTOMER else member['id']
|
|
if ((groupMemberType in ('ALL', member['type'])) and
|
|
(not includeDerivedMembership or (member['type'] == Ent.TYPE_USER)) and
|
|
_checkMemberRoleIsSuspendedIsArchived(member, validRoles, isSuspended, isArchived) and
|
|
email not in entitySet):
|
|
entitySet.add(email)
|
|
entityList.append(email)
|
|
else:
|
|
_showInvalidEntity(Ent.GROUP, group)
|
|
elif entityType in {Cmd.ENTITY_GROUP_USERS, Cmd.ENTITY_GROUP_USERS_NS, Cmd.ENTITY_GROUP_USERS_SUSP, Cmd.ENTITY_GROUP_USERS_SELECT}:
|
|
if entityType == Cmd.ENTITY_GROUP_USERS_NS:
|
|
isSuspended = False
|
|
elif entityType == Cmd.ENTITY_GROUP_USERS_SUSP:
|
|
isSuspended = True
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
groups = convertEntityToList(entity)
|
|
includeDerivedMembership = False
|
|
domains = []
|
|
rolesSet = set()
|
|
if not noCLArgs:
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[myarg])
|
|
elif myarg == 'primarydomain':
|
|
domains.append(GC.Values[GC.DOMAIN])
|
|
elif myarg == 'domains':
|
|
domains.extend(getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY))
|
|
elif myarg == 'recursive':
|
|
recursive = True
|
|
includeDerivedMembership = False
|
|
elif myarg == 'includederivedmembership':
|
|
includeDerivedMembership = True
|
|
recursive = False
|
|
elif entityType == Cmd.ENTITY_GROUP_USERS_SELECT and myarg in SUSPENDED_ARGUMENTS:
|
|
isSuspended = _getIsSuspended(myarg)
|
|
elif entityType == Cmd.ENTITY_GROUP_USERS_SELECT and myarg in ARCHIVED_ARGUMENTS:
|
|
isArchived = _getIsArchived(myarg)
|
|
elif myarg == 'end':
|
|
break
|
|
else:
|
|
Cmd.Backup()
|
|
missingArgumentExit('end')
|
|
if rolesSet:
|
|
memberRoles = ','.join(sorted(rolesSet))
|
|
for group in groups:
|
|
if validateEmailAddressOrUID(group, checkPeople=False):
|
|
_addGroupUsersToUsers(normalizeEmailAddressOrUID(group), domains, recursive, includeDerivedMembership)
|
|
else:
|
|
_showInvalidEntity(Ent.GROUP, group)
|
|
elif entityType in {Cmd.ENTITY_CIGROUP, Cmd.ENTITY_CIGROUPS}:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
groups = convertEntityToList(entity, nonListEntityType=entityType in {Cmd.ENTITY_CIGROUP})
|
|
for group in groups:
|
|
if validateEmailAddressOrUID(group, checkPeople=False, ciGroupsAPI=True):
|
|
_, name, groupEmail = convertGroupEmailToCloudID(ci, group)
|
|
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, groupEmail, entityType=Ent.CLOUD_IDENTITY_GROUP)
|
|
validRoles = _getCIRoleVerification(memberRoles)
|
|
try:
|
|
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
parent=name, view='FULL',
|
|
fields='nextPageToken,memberships(preferredMemberKey(id),roles(name),type)',
|
|
pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupEmail)
|
|
_incrEntityDoesNotExist(Ent.CLOUD_IDENTITY_GROUP)
|
|
continue
|
|
for member in result:
|
|
getCIGroupMemberRoleFixType(member)
|
|
email = member.get('preferredMemberKey', {}).get('id', '')
|
|
if (email and (groupMemberType in ('ALL', member['type'])) and
|
|
_checkMemberRole(member, validRoles) and email not in entitySet):
|
|
entitySet.add(email)
|
|
entityList.append(email)
|
|
else:
|
|
_showInvalidEntity(Ent.CLOUD_IDENTITY_GROUP, groupEmail)
|
|
elif entityType in {Cmd.ENTITY_CIGROUP_USERS}:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
groups = convertEntityToList(entity)
|
|
rolesSet = set()
|
|
if not noCLArgs:
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[myarg])
|
|
elif myarg == 'recursive':
|
|
recursive = True
|
|
elif myarg == 'end':
|
|
break
|
|
else:
|
|
Cmd.Backup()
|
|
missingArgumentExit('end')
|
|
if rolesSet:
|
|
memberRoles = ','.join(sorted(rolesSet))
|
|
for group in groups:
|
|
_, name, groupEmail = convertGroupEmailToCloudID(ci, group)
|
|
if name:
|
|
_addCIGroupUsersToUsers(name, groupEmail, recursive)
|
|
else:
|
|
_showInvalidEntity(Ent.GROUP, group)
|
|
elif entityType in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS, Cmd.ENTITY_OU_AND_CHILDREN, Cmd.ENTITY_OUS_AND_CHILDREN,
|
|
Cmd.ENTITY_OU_NS, Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OU_AND_CHILDREN_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS,
|
|
Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OU_AND_CHILDREN_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP}:
|
|
if entityType in {Cmd.ENTITY_OU_NS, Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OU_AND_CHILDREN_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS}:
|
|
isSuspended = False
|
|
elif entityType in {Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OU_AND_CHILDREN_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP}:
|
|
isSuspended = True
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ous = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType in {Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN,
|
|
Cmd.ENTITY_OU_NS, Cmd.ENTITY_OU_AND_CHILDREN_NS,
|
|
Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OU_AND_CHILDREN_SUSP})
|
|
directlyInOU = entityType in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS, Cmd.ENTITY_OU_NS, Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OU_SUSP, Cmd.ENTITY_OUS_SUSP}
|
|
qualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)) if directlyInOU else Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
|
|
fields = 'nextPageToken,users(primaryEmail,orgUnitPath,archived)' if directlyInOU else 'nextPageToken,users(primaryEmail,archived)'
|
|
prevLen = 0
|
|
for ou in ous:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
if ou.startswith('id:'):
|
|
try:
|
|
ou = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID],
|
|
orgUnitPath=ou, fields='orgUnitPath')['orgUnitPath']
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest,
|
|
GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
_incrEntityDoesNotExist(Ent.ORGANIZATIONAL_UNIT)
|
|
continue
|
|
ouLower = ou.lower()
|
|
printGettingAllEntityItemsForWhom(Ent.USER, ou, qualifier=Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)),
|
|
entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
pageMessage = getPageMessageForWhom()
|
|
usersInOU = 0
|
|
try:
|
|
feed = yieldGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=pageMessage, messageAttribute='primaryEmail',
|
|
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
|
|
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(ou, isSuspended), orderBy='email',
|
|
fields=fields, maxResults=GC.Values[GC.USER_MAX_RESULTS])
|
|
for users in feed:
|
|
if directlyInOU:
|
|
for user in users:
|
|
if ouLower == user.get('orgUnitPath', '').lower() and (isArchived is None or isArchived == user['archived']):
|
|
usersInOU += 1
|
|
entityList.append(user['primaryEmail'])
|
|
elif isArchived is None:
|
|
entityList.extend([user['primaryEmail'] for user in users])
|
|
usersInOU += len(users)
|
|
else:
|
|
for user in users:
|
|
if isArchived == user['archived']:
|
|
usersInOU += 1
|
|
entityList.append(user['primaryEmail'])
|
|
setGettingAllEntityItemsForWhom(Ent.USER, ou, qualifier=qualifier)
|
|
printGotEntityItemsForWhom(usersInOU)
|
|
except (GAPI.invalidInput, GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest,
|
|
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
_incrEntityDoesNotExist(Ent.ORGANIZATIONAL_UNIT)
|
|
elif entityType in {Cmd.ENTITY_QUERY, Cmd.ENTITY_QUERIES}:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
queries = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType == Cmd.ENTITY_QUERY)
|
|
prevLen = 0
|
|
for query in queries:
|
|
printGettingAllAccountEntities(Ent.USER, query)
|
|
try:
|
|
result = callGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
|
|
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], query=query, orderBy='email',
|
|
fields='nextPageToken,users(primaryEmail,suspended,archived)',
|
|
maxResults=GC.Values[GC.USER_MAX_RESULTS])
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.invalidInput):
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_QUERY)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
for user in result:
|
|
email = user['primaryEmail']
|
|
if ((isSuspended is None or isSuspended == user['suspended']) and
|
|
(isArchived is None or isArchived == user['archived']) and
|
|
email not in entitySet):
|
|
entitySet.add(email)
|
|
entityList.append(email)
|
|
totalLen = len(entityList)
|
|
printGotAccountEntities(totalLen-prevLen)
|
|
prevLen = totalLen
|
|
elif entityType == Cmd.ENTITY_LICENSES:
|
|
skusList = []
|
|
for item in entity.split(','):
|
|
productId, sku = SKU.getProductAndSKU(item)
|
|
if not productId:
|
|
_incrEntityDoesNotExist(Ent.SKU)
|
|
elif (productId, sku) not in skusList:
|
|
skusList.append((productId, sku))
|
|
if skusList:
|
|
entityList = doPrintLicenses(returnFields=['userId'], skus=skusList)
|
|
elif entityType in {Cmd.ENTITY_COURSEPARTICIPANTS, Cmd.ENTITY_TEACHERS, Cmd.ENTITY_STUDENTS}:
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
if not noListConversion:
|
|
courseIdList = convertEntityToList(entity)
|
|
else:
|
|
courseIdList = [entity]
|
|
_, _, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, GC.Values[GC.USE_COURSE_OWNER_ACCESS])
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
try:
|
|
if entityType in {Cmd.ENTITY_COURSEPARTICIPANTS, Cmd.ENTITY_TEACHERS}:
|
|
printGettingAllEntityItemsForWhom(Ent.TEACHER, removeCourseIdScope(courseId), entityType=Ent.COURSE)
|
|
result = callGAPIpages(courseInfo['croom'].courses().teachers(), 'list', 'teachers',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, fields='nextPageToken,teachers/profile/emailAddress',
|
|
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
for teacher in result:
|
|
email = teacher['profile'].get('emailAddress', None)
|
|
if email and (email not in entitySet):
|
|
entitySet.add(email)
|
|
entityList.append(email)
|
|
if entityType in {Cmd.ENTITY_COURSEPARTICIPANTS, Cmd.ENTITY_STUDENTS}:
|
|
printGettingAllEntityItemsForWhom(Ent.STUDENT, removeCourseIdScope(courseId), entityType=Ent.COURSE)
|
|
result = callGAPIpages(courseInfo['croom'].courses().students(), 'list', 'students',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, fields='nextPageToken,students/profile/emailAddress',
|
|
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
for student in result:
|
|
email = student['profile'].get('emailAddress', None)
|
|
if email and (email not in entitySet):
|
|
entitySet.add(email)
|
|
entityList.append(email)
|
|
except GAPI.notFound:
|
|
entityDoesNotExistWarning(Ent.COURSE, removeCourseIdScope(courseId))
|
|
_incrEntityDoesNotExist(Ent.COURSE)
|
|
except GAPI.serviceNotAvailable as e:
|
|
entityActionNotPerformedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e))
|
|
GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE] = True
|
|
break
|
|
except (GAPI.forbidden, GAPI.badRequest):
|
|
ClientAPIAccessDeniedExit()
|
|
elif entityType == Cmd.ENTITY_CROS:
|
|
buildGAPIObject(API.DIRECTORY)
|
|
result = convertEntityToList(entity)
|
|
for deviceId in result:
|
|
if deviceId not in entitySet:
|
|
entitySet.add(deviceId)
|
|
entityList.append(deviceId)
|
|
elif entityType == Cmd.ENTITY_ALL_CROS:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
printGettingAllAccountEntities(Ent.CROS_DEVICE)
|
|
try:
|
|
result = callGAPIpages(cd.chromeosdevices(), 'list', 'chromeosdevices',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID],
|
|
fields='nextPageToken,chromeosdevices(deviceId)',
|
|
maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
|
|
except (GAPI.invalidInput, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
entityList = [device['deviceId'] for device in result]
|
|
elif entityType in {Cmd.ENTITY_CROS_QUERY, Cmd.ENTITY_CROS_QUERIES, Cmd.ENTITY_CROS_SN}:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
queries = convertEntityToList(entity, shlexSplit=entityType == Cmd.ENTITY_CROS_QUERIES,
|
|
nonListEntityType=entityType == Cmd.ENTITY_CROS_QUERY)
|
|
if entityType == Cmd.ENTITY_CROS_SN:
|
|
queries = [f'id:{query}' for query in queries]
|
|
prevLen = 0
|
|
for query in queries:
|
|
printGettingAllAccountEntities(Ent.CROS_DEVICE, query)
|
|
try:
|
|
result = callGAPIpages(cd.chromeosdevices(), 'list', 'chromeosdevices',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], query=query,
|
|
fields='nextPageToken,chromeosdevices(deviceId)',
|
|
maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
|
|
except GAPI.invalidInput:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_QUERY)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
for device in result:
|
|
deviceId = device['deviceId']
|
|
if deviceId not in entitySet:
|
|
entitySet.add(deviceId)
|
|
entityList.append(deviceId)
|
|
totalLen = len(entityList)
|
|
printGotAccountEntities(totalLen-prevLen)
|
|
prevLen = totalLen
|
|
elif entityType in {Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN, Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN,
|
|
Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY,
|
|
Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ous = convertEntityToList(entity, shlexSplit=True,
|
|
nonListEntityType=entityType in {Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN,
|
|
Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY,
|
|
Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES})
|
|
numOus = len(ous)
|
|
includeChildOrgunits = entityType in {Cmd.ENTITY_CROS_OU_AND_CHILDREN, Cmd.ENTITY_CROS_OUS_AND_CHILDREN,
|
|
Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY,
|
|
Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}
|
|
allQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Choose(Ent.ORGANIZATIONAL_UNIT, numOus)) if not includeChildOrgunits else Msg.IN_THE.format(Ent.Choose(Ent.ORGANIZATIONAL_UNIT, numOus))
|
|
if entityType in {Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY}:
|
|
queries = getQueries('query')
|
|
elif entityType in {Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}:
|
|
queries = getQueries('queries')
|
|
else:
|
|
queries = [None]
|
|
for ou in ous:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)) if not includeChildOrgunits else Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
|
|
for query in queries:
|
|
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, ou,
|
|
query=query, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
try:
|
|
result = callGAPIpages(cd.chromeosdevices(), 'list', 'chromeosdevices',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], query=query,
|
|
orgUnitPath=ou, includeChildOrgunits=includeChildOrgunits,
|
|
fields='nextPageToken,chromeosdevices(deviceId)', maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
|
|
except GAPI.invalidInput:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_QUERY)
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
_incrEntityDoesNotExist(Ent.ORGANIZATIONAL_UNIT)
|
|
continue
|
|
if query is None:
|
|
entityList.extend([device['deviceId'] for device in result])
|
|
else:
|
|
for device in result:
|
|
deviceId = device['deviceId']
|
|
if deviceId not in entitySet:
|
|
entitySet.add(deviceId)
|
|
entityList.append(deviceId)
|
|
Ent.SetGettingQualifier(Ent.CROS_DEVICE, allQualifier)
|
|
Ent.SetGettingForWhom(','.join(ous))
|
|
printGotEntityItemsForWhom(len(entityList))
|
|
elif entityType == Cmd.ENTITY_BROWSER:
|
|
result = convertEntityToList(entity)
|
|
for deviceId in result:
|
|
if deviceId not in entitySet:
|
|
entitySet.add(deviceId)
|
|
entityList.append(deviceId)
|
|
elif entityType in {Cmd.ENTITY_BROWSER_OU, Cmd.ENTITY_BROWSER_OUS}:
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
ous = convertEntityToList(entity, shlexSplit=True, nonListEntityType=entityType == Cmd.ENTITY_BROWSER_OU)
|
|
numOus = len(ous)
|
|
allQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Choose(Ent.ORGANIZATIONAL_UNIT, numOus))
|
|
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
|
|
for ou in ous:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
printGettingAllEntityItemsForWhom(Ent.CHROME_BROWSER, ou, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
try:
|
|
result = callGAPIpages(cbcm.chromebrowsers(), 'list', 'browsers',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customerId, orgUnitPath=ou, projection='BASIC',
|
|
orderBy='id', sortOrder='ASCENDING', fields='nextPageToken,browsers(deviceId)')
|
|
except (GAPI.badRequest, GAPI.invalidOrgunit, GAPI.forbidden):
|
|
checkEntityDNEorAccessErrorExit(None, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
_incrEntityDoesNotExist(Ent.ORGANIZATIONAL_UNIT)
|
|
continue
|
|
entityList.extend([browser['deviceId'] for browser in result])
|
|
Ent.SetGettingQualifier(Ent.CHROME_BROWSER, allQualifier)
|
|
Ent.SetGettingForWhom(','.join(ous))
|
|
printGotEntityItemsForWhom(len(entityList))
|
|
elif entityType in {Cmd.ENTITY_BROWSER_QUERY, Cmd.ENTITY_BROWSER_QUERIES}:
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
queries = convertEntityToList(entity, shlexSplit=entityType == Cmd.ENTITY_BROWSER_QUERIES,
|
|
nonListEntityType=entityType == Cmd.ENTITY_BROWSER_QUERY)
|
|
prevLen = 0
|
|
for query in queries:
|
|
printGettingAllAccountEntities(Ent.CHROME_BROWSER, query)
|
|
try:
|
|
result = callGAPIpages(cbcm.chromebrowsers(), 'list', 'browsers',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customerId, query=query, projection='BASIC',
|
|
orderBy='id', sortOrder='ASCENDING', fields='nextPageToken,browsers(deviceId)')
|
|
except GAPI.invalidInput:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_QUERY)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
|
|
accessErrorExitNonDirectory(API.CBCM, str(e))
|
|
for device in result:
|
|
deviceId = device['deviceId']
|
|
if deviceId not in entitySet:
|
|
entitySet.add(deviceId)
|
|
entityList.append(deviceId)
|
|
totalLen = len(entityList)
|
|
printGotAccountEntities(totalLen-prevLen)
|
|
prevLen = totalLen
|
|
else:
|
|
systemErrorExit(UNKNOWN_ERROR_RC, 'getItemsToModify coding error')
|
|
for errorType in [ENTITY_ERROR_DNE, ENTITY_ERROR_INVALID]:
|
|
if entityError[errorType] > 0:
|
|
Cmd.SetLocation(entityLocation-1)
|
|
writeStderr(Cmd.CommandLineWithBadArgumentMarked(False))
|
|
count = entityError[errorType]
|
|
if errorType == ENTITY_ERROR_DNE:
|
|
stderrErrorMsg(Msg.BAD_ENTITIES_IN_SOURCE.format(count, Ent.Choose(entityError['entityType'], count),
|
|
Msg.DO_NOT_EXIST if count != 1 else Msg.DOES_NOT_EXIST))
|
|
sys.exit(ENTITY_DOES_NOT_EXIST_RC)
|
|
else:
|
|
stderrErrorMsg(Msg.BAD_ENTITIES_IN_SOURCE.format(count, Msg.INVALID, Ent.Choose(entityError['entityType'], count)))
|
|
sys.exit(INVALID_ENTITY_RC)
|
|
return entityList
|
|
|
|
def splitEntityList(entity, dataDelimiter):
|
|
if not entity:
|
|
return []
|
|
if not dataDelimiter:
|
|
return [entity]
|
|
return entity.split(dataDelimiter)
|
|
|
|
def splitEntityListShlex(entity, dataDelimiter):
|
|
if not entity:
|
|
return (True, [])
|
|
if not dataDelimiter:
|
|
return (True, [entity])
|
|
return shlexSplitListStatus(entity, dataDelimiter)
|
|
|
|
def fileDataErrorExit(filename, row, itemName, value, errMessage):
|
|
if itemName:
|
|
systemErrorExit(DATA_ERROR_RC,
|
|
formatKeyValueList('',
|
|
[Ent.Singular(Ent.FILE), filename,
|
|
Ent.Singular(Ent.ROW), row,
|
|
Ent.Singular(Ent.ITEM), itemName,
|
|
Ent.Singular(Ent.VALUE), value,
|
|
errMessage],
|
|
''))
|
|
else:
|
|
systemErrorExit(DATA_ERROR_RC,
|
|
formatKeyValueList('',
|
|
[Ent.Singular(Ent.FILE), filename,
|
|
Ent.Singular(Ent.ROW), row,
|
|
Ent.Singular(Ent.VALUE), value,
|
|
errMessage],
|
|
''))
|
|
|
|
# <FileSelector>
|
|
def getEntitiesFromFile(shlexSplit, returnSet=False):
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
filenameLower = filename.lower()
|
|
if filenameLower not in {'gcsv', 'gdoc', 'gcscsv', 'gcsdoc'}:
|
|
encoding = getCharSet()
|
|
f = openFile(filename, encoding=encoding, stripUTFBOM=True)
|
|
elif filenameLower in {'gcsv', 'gdoc'}:
|
|
f = getGDocData(filenameLower)
|
|
getCharSet()
|
|
else: #filenameLower in {'gcscsv', 'gcsdoc'}:
|
|
f = getStorageFileData(filenameLower)
|
|
getCharSet()
|
|
dataDelimiter = getDelimiter()
|
|
entitySet = set()
|
|
entityList = []
|
|
i = 0
|
|
for row in f:
|
|
i += 1
|
|
if shlexSplit:
|
|
splitStatus, itemList = splitEntityListShlex(row.strip(), dataDelimiter)
|
|
if not splitStatus:
|
|
fileDataErrorExit(filename, i, None, row.strip(), f'{Msg.INVALID_LIST}: {itemList}')
|
|
else:
|
|
itemList = splitEntityList(row.strip(), dataDelimiter)
|
|
for item in itemList:
|
|
item = item.strip()
|
|
if item and (item not in entitySet):
|
|
entitySet.add(item)
|
|
entityList.append(item)
|
|
closeFile(f)
|
|
return entityList if not returnSet else entitySet
|
|
|
|
# <CSVFileSelector>
|
|
def getEntitiesFromCSVFile(shlexSplit, returnSet=False):
|
|
fileFieldName = getString(Cmd.OB_FILE_NAME_FIELD_NAME)
|
|
if platform.system() == 'Windows' and not fileFieldName.startswith('-:'):
|
|
drive, fileFieldName = os.path.splitdrive(fileFieldName)
|
|
else:
|
|
drive = ''
|
|
if fileFieldName.find(':') == -1:
|
|
Cmd.Backup()
|
|
invalidArgumentExit(Cmd.OB_FILE_NAME_FIELD_NAME)
|
|
fileFieldNameList = fileFieldName.split(':')
|
|
filename = drive+fileFieldNameList[0]
|
|
f, csvFile, fieldnames = openCSVFileReader(filename)
|
|
for fieldName in fileFieldNameList[1:]:
|
|
if fieldName not in fieldnames:
|
|
csvFieldErrorExit(fieldName, fieldnames, backupArg=True, checkForCharset=True)
|
|
matchFields, skipFields = getMatchSkipFields(fieldnames)
|
|
dataDelimiter = getDelimiter()
|
|
entitySet = set()
|
|
entityList = []
|
|
i = 1
|
|
for row in csvFile:
|
|
i += 1
|
|
if checkMatchSkipFields(row, None, matchFields, skipFields):
|
|
for fieldName in fileFieldNameList[1:]:
|
|
if shlexSplit:
|
|
splitStatus, itemList = splitEntityListShlex(row[fieldName].strip(), dataDelimiter)
|
|
if not splitStatus:
|
|
fileDataErrorExit(filename, i, fieldName, row[fieldName].strip(), f'{Msg.INVALID_LIST}: {itemList}')
|
|
else:
|
|
itemList = splitEntityList(row[fieldName].strip(), dataDelimiter)
|
|
for item in itemList:
|
|
item = item.strip()
|
|
if item and (item not in entitySet):
|
|
entitySet.add(item)
|
|
entityList.append(item)
|
|
closeFile(f)
|
|
return entityList if not returnSet else entitySet
|
|
|
|
# <CSVFileSelector>
|
|
# keyfield <FieldName> [keypattern <RESearchPattern>] [keyvalue <RESubstitution>] [delimiter <Character>]
|
|
# subkeyfield <FieldName> [keypattern <RESearchPattern>] [keyvalue <RESubstitution>] [delimiter <Character>]
|
|
# (matchfield|skipfield <FieldName> <RESearchPattern>)*
|
|
# [datafield <FieldName>(:<FieldName>)* [delimiter <Character>]]
|
|
def getEntitiesFromCSVbyField():
|
|
|
|
def getKeyFieldInfo(keyword, required, globalKeyField):
|
|
if not checkArgumentPresent(keyword, required=required):
|
|
GM.Globals[globalKeyField] = None
|
|
return (None, None, None, None)
|
|
keyField = GM.Globals[globalKeyField] = getString(Cmd.OB_FIELD_NAME)
|
|
if keyField not in fieldnames:
|
|
csvFieldErrorExit(keyField, fieldnames, backupArg=True)
|
|
if checkArgumentPresent('keypattern'):
|
|
keyPattern = getREPattern()
|
|
else:
|
|
keyPattern = None
|
|
if checkArgumentPresent('keyvalue'):
|
|
keyValue = getString(Cmd.OB_STRING)
|
|
else:
|
|
keyValue = keyField
|
|
keyDelimiter = getDelimiter()
|
|
return (keyField, keyPattern, keyValue, keyDelimiter)
|
|
|
|
def getKeyList(row, keyField, keyPattern, keyValue, keyDelimiter, matchFields, skipFields):
|
|
item = row[keyField].strip()
|
|
if not item:
|
|
return []
|
|
if not checkMatchSkipFields(row, None, matchFields, skipFields):
|
|
return []
|
|
if keyPattern:
|
|
keyList = [keyPattern.sub(keyValue, keyItem.strip()) for keyItem in splitEntityList(item, keyDelimiter)]
|
|
else:
|
|
keyList = [re.sub(keyField, keyItem.strip(), keyValue) for keyItem in splitEntityList(item, keyDelimiter)]
|
|
return [key for key in keyList if key]
|
|
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
f, csvFile, fieldnames = openCSVFileReader(filename)
|
|
mainKeyField, mainKeyPattern, mainKeyValue, mainKeyDelimiter = getKeyFieldInfo('keyfield', True, GM.CSV_KEY_FIELD)
|
|
subKeyField, subKeyPattern, subKeyValue, subKeyDelimiter = getKeyFieldInfo('subkeyfield', False, GM.CSV_SUBKEY_FIELD)
|
|
matchFields, skipFields = getMatchSkipFields(fieldnames)
|
|
if checkArgumentPresent('datafield'):
|
|
if GM.Globals[GM.CSV_DATA_DICT]:
|
|
csvDataAlreadySavedErrorExit()
|
|
GM.Globals[GM.CSV_DATA_FIELD] = getString(Cmd.OB_FIELD_NAME, checkBlank=True)
|
|
dataFields = GM.Globals[GM.CSV_DATA_FIELD].split(':')
|
|
for dataField in dataFields:
|
|
if dataField not in fieldnames:
|
|
csvFieldErrorExit(dataField, fieldnames, backupArg=True)
|
|
dataDelimiter = getDelimiter()
|
|
else:
|
|
GM.Globals[GM.CSV_DATA_FIELD] = None
|
|
dataFields = []
|
|
dataDelimiter = None
|
|
entitySet = set()
|
|
entityList = []
|
|
csvDataKeys = {}
|
|
GM.Globals[GM.CSV_DATA_DICT] = {}
|
|
if not subKeyField:
|
|
for row in csvFile:
|
|
mainKeyList = getKeyList(row, mainKeyField, mainKeyPattern, mainKeyValue, mainKeyDelimiter, matchFields, skipFields)
|
|
if not mainKeyList:
|
|
continue
|
|
for mainKey in mainKeyList:
|
|
if mainKey not in entitySet:
|
|
entitySet.add(mainKey)
|
|
entityList.append(mainKey)
|
|
if GM.Globals[GM.CSV_DATA_FIELD]:
|
|
csvDataKeys[mainKey] = set()
|
|
GM.Globals[GM.CSV_DATA_DICT][mainKey] = []
|
|
for dataField in dataFields:
|
|
if dataField in row:
|
|
dataList = splitEntityList(row[dataField].strip(), dataDelimiter)
|
|
for dataValue in dataList:
|
|
dataValue = dataValue.strip()
|
|
if not dataValue:
|
|
continue
|
|
for mainKey in mainKeyList:
|
|
if dataValue not in csvDataKeys[mainKey]:
|
|
csvDataKeys[mainKey].add(dataValue)
|
|
GM.Globals[GM.CSV_DATA_DICT][mainKey].append(dataValue)
|
|
else:
|
|
csvSubKeys = {}
|
|
for row in csvFile:
|
|
mainKeyList = getKeyList(row, mainKeyField, mainKeyPattern, mainKeyValue, mainKeyDelimiter, matchFields, skipFields)
|
|
if not mainKeyList:
|
|
continue
|
|
for mainKey in mainKeyList:
|
|
if mainKey not in entitySet:
|
|
entitySet.add(mainKey)
|
|
entityList.append(mainKey)
|
|
csvSubKeys[mainKey] = set()
|
|
csvDataKeys[mainKey] = {}
|
|
GM.Globals[GM.CSV_DATA_DICT][mainKey] = {}
|
|
subKeyList = getKeyList(row, subKeyField, subKeyPattern, subKeyValue, subKeyDelimiter, {}, {})
|
|
if not subKeyList:
|
|
continue
|
|
for mainKey in mainKeyList:
|
|
for subKey in subKeyList:
|
|
if subKey not in csvSubKeys[mainKey]:
|
|
csvSubKeys[mainKey].add(subKey)
|
|
if GM.Globals[GM.CSV_DATA_FIELD]:
|
|
csvDataKeys[mainKey][subKey] = set()
|
|
GM.Globals[GM.CSV_DATA_DICT][mainKey][subKey] = []
|
|
for dataField in dataFields:
|
|
if dataField in row:
|
|
dataList = splitEntityList(row[dataField].strip(), dataDelimiter)
|
|
for dataValue in dataList:
|
|
dataValue = dataValue.strip()
|
|
if not dataValue:
|
|
continue
|
|
for mainKey in mainKeyList:
|
|
for subKey in subKeyList:
|
|
if dataValue not in csvDataKeys[mainKey][subKey]:
|
|
csvDataKeys[mainKey][subKey].add(dataValue)
|
|
GM.Globals[GM.CSV_DATA_DICT][mainKey][subKey].append(dataValue)
|
|
closeFile(f)
|
|
return entityList
|
|
|
|
# Typically used to map courseparticipants to students or teachers
|
|
def mapEntityType(entityType, typeMap):
|
|
if (typeMap is not None) and (entityType in typeMap):
|
|
return typeMap[entityType]
|
|
return entityType
|
|
|
|
def getEntityArgument(entityList):
|
|
if entityList is None:
|
|
return (0, 0, entityList)
|
|
if isinstance(entityList, dict):
|
|
clLoc = Cmd.Location()
|
|
Cmd.SetLocation(GM.Globals[GM.ENTITY_CL_DELAY_START])
|
|
entityList = getItemsToModify(**entityList)
|
|
Cmd.SetLocation(clLoc)
|
|
return (0, len(entityList), entityList)
|
|
|
|
def getEntityToModify(defaultEntityType=None, browserAllowed=False, crosAllowed=False, userAllowed=True,
|
|
typeMap=None, isSuspended=None, isArchived=None, groupMemberType=Ent.TYPE_USER, delayGet=False):
|
|
if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
|
|
crosAllowed = False
|
|
selectorChoices = Cmd.SERVICE_ACCOUNT_ONLY_ENTITY_SELECTORS[:]
|
|
else:
|
|
selectorChoices = Cmd.BASE_ENTITY_SELECTORS[:]
|
|
if userAllowed:
|
|
selectorChoices += Cmd.USER_ENTITY_SELECTORS+Cmd.USER_CSVDATA_ENTITY_SELECTORS
|
|
if crosAllowed:
|
|
selectorChoices += Cmd.CROS_ENTITY_SELECTORS+Cmd.CROS_CSVDATA_ENTITY_SELECTORS
|
|
if browserAllowed:
|
|
selectorChoices = Cmd.BROWSER_ENTITY_SELECTORS
|
|
entitySelector = getChoice(selectorChoices, defaultChoice=None)
|
|
if entitySelector:
|
|
choices = []
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_ALL:
|
|
if userAllowed:
|
|
choices += Cmd.USER_ENTITY_SELECTOR_ALL_SUBTYPES
|
|
if crosAllowed:
|
|
choices += Cmd.CROS_ENTITY_SELECTOR_ALL_SUBTYPES
|
|
entityType = Cmd.ENTITY_SELECTOR_ALL_SUBTYPES_MAP[getChoice(choices)]
|
|
if not delayGet:
|
|
return (Cmd.ENTITY_USERS if entityType != Cmd.ENTITY_ALL_CROS else Cmd.ENTITY_CROS,
|
|
getItemsToModify(entityType, None))
|
|
GM.Globals[GM.ENTITY_CL_DELAY_START] = Cmd.Location()
|
|
buildGAPIObject(API.DIRECTORY)
|
|
return (Cmd.ENTITY_USERS if entityType != Cmd.ENTITY_ALL_CROS else Cmd.ENTITY_CROS,
|
|
{'entityType': entityType, 'entity': None})
|
|
if userAllowed:
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_FILE:
|
|
return (Cmd.ENTITY_USERS, getItemsToModify(Cmd.ENTITY_USERS, getEntitiesFromFile(False)))
|
|
if entitySelector in [Cmd.ENTITY_SELECTOR_CSV, Cmd.ENTITY_SELECTOR_CSVFILE]:
|
|
return (Cmd.ENTITY_USERS, getItemsToModify(Cmd.ENTITY_USERS, getEntitiesFromCSVFile(False)))
|
|
if crosAllowed:
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_CROSFILE:
|
|
return (Cmd.ENTITY_CROS, getEntitiesFromFile(False))
|
|
if entitySelector in [Cmd.ENTITY_SELECTOR_CROSCSV, Cmd.ENTITY_SELECTOR_CROSCSVFILE]:
|
|
return (Cmd.ENTITY_CROS, getEntitiesFromCSVFile(False))
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_CROSFILE_SN:
|
|
return (Cmd.ENTITY_CROS, getItemsToModify(Cmd.ENTITY_CROS_SN, getEntitiesFromFile(False)))
|
|
if entitySelector in [Cmd.ENTITY_SELECTOR_CROSCSV_SN, Cmd.ENTITY_SELECTOR_CROSCSVFILE_SN]:
|
|
return (Cmd.ENTITY_CROS, getItemsToModify(Cmd.ENTITY_CROS_SN, getEntitiesFromCSVFile(False)))
|
|
if browserAllowed:
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_FILE:
|
|
return (Cmd.ENTITY_BROWSER, getEntitiesFromFile(False))
|
|
if entitySelector in [Cmd.ENTITY_SELECTOR_CSV, Cmd.ENTITY_SELECTOR_CSVFILE]:
|
|
return (Cmd.ENTITY_BROWSER, getEntitiesFromCSVFile(False))
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_DATAFILE:
|
|
if userAllowed:
|
|
choices += Cmd.USER_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USERS]
|
|
if crosAllowed:
|
|
choices += Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES
|
|
entityType = mapEntityType(getChoice(choices), typeMap)
|
|
return (Cmd.ENTITY_USERS if entityType not in Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES else Cmd.ENTITY_CROS,
|
|
getItemsToModify(entityType, getEntitiesFromFile(shlexSplit=entityType in [Cmd.ENTITY_OUS, Cmd.ENTITY_OUS_AND_CHILDREN,
|
|
Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS,
|
|
Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP,
|
|
Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN])))
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_CSVDATAFILE:
|
|
if userAllowed:
|
|
choices += Cmd.USER_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USERS]
|
|
if crosAllowed:
|
|
choices += Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES
|
|
entityType = mapEntityType(getChoice(choices), typeMap)
|
|
return (Cmd.ENTITY_USERS if entityType not in Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES else Cmd.ENTITY_CROS,
|
|
getItemsToModify(entityType, getEntitiesFromCSVFile(shlexSplit=entityType in [Cmd.ENTITY_OUS, Cmd.ENTITY_OUS_AND_CHILDREN,
|
|
Cmd.ENTITY_OUS_NS, Cmd.ENTITY_OUS_AND_CHILDREN_NS,
|
|
Cmd.ENTITY_OUS_SUSP, Cmd.ENTITY_OUS_AND_CHILDREN_SUSP,
|
|
Cmd.ENTITY_CROS_OUS, Cmd.ENTITY_CROS_OUS_AND_CHILDREN])))
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_CSVKMD:
|
|
if userAllowed:
|
|
choices += Cmd.USER_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USERS]
|
|
if crosAllowed:
|
|
choices += Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES
|
|
entityType = mapEntityType(getChoice(choices, choiceAliases=Cmd.ENTITY_ALIAS_MAP), typeMap)
|
|
return (Cmd.ENTITY_USERS if entityType not in Cmd.CROS_ENTITY_SELECTOR_DATAFILE_CSVKMD_SUBTYPES else Cmd.ENTITY_CROS,
|
|
getItemsToModify(entityType, getEntitiesFromCSVbyField()))
|
|
if entitySelector in [Cmd.ENTITY_SELECTOR_CSVDATA, Cmd.ENTITY_SELECTOR_CROSCSVDATA]:
|
|
checkDataField()
|
|
return (Cmd.ENTITY_USERS if entitySelector == Cmd.ENTITY_SELECTOR_CSVDATA else Cmd.ENTITY_CROS,
|
|
GM.Globals[GM.CSV_DATA_DICT])
|
|
entityChoices = []
|
|
if userAllowed:
|
|
entityChoices += Cmd.USER_ENTITIES if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] else [Cmd.ENTITY_USER, Cmd.ENTITY_USERS]
|
|
if crosAllowed:
|
|
entityChoices += Cmd.CROS_ENTITIES
|
|
if browserAllowed:
|
|
entityChoices += Cmd.BROWSER_ENTITIES
|
|
entityType = mapEntityType(getChoice(entityChoices, choiceAliases=Cmd.ENTITY_ALIAS_MAP, defaultChoice=defaultEntityType), typeMap)
|
|
if not entityType:
|
|
invalidChoiceExit(Cmd.Current(), selectorChoices+entityChoices, False)
|
|
if entityType not in Cmd.CROS_ENTITIES+Cmd.BROWSER_ENTITIES:
|
|
entityClass = Cmd.ENTITY_USERS
|
|
if entityType == Cmd.ENTITY_OAUTHUSER:
|
|
return (entityClass, [_getAdminEmail()])
|
|
entityItem = getString(Cmd.OB_USER_ENTITY, minLen=0)
|
|
elif entityType in Cmd.CROS_ENTITIES:
|
|
entityClass = Cmd.ENTITY_CROS
|
|
entityItem = getString(Cmd.OB_CROS_ENTITY, minLen=0)
|
|
else:
|
|
entityClass = Cmd.ENTITY_BROWSER
|
|
entityItem = getString(Cmd.OB_BROWSER_ENTITY, minLen=0)
|
|
if not delayGet:
|
|
if entityClass == Cmd.ENTITY_USERS:
|
|
return (entityClass, getItemsToModify(entityType, entityItem,
|
|
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType))
|
|
return (entityClass, getItemsToModify(entityType, entityItem))
|
|
GM.Globals[GM.ENTITY_CL_DELAY_START] = Cmd.Location()
|
|
if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
|
|
buildGAPIObject(API.DIRECTORY)
|
|
if entityClass == Cmd.ENTITY_USERS:
|
|
if entityType in [Cmd.ENTITY_GROUP_USERS,
|
|
Cmd.ENTITY_GROUP_USERS_NS, Cmd.ENTITY_GROUP_USERS_SUSP,
|
|
Cmd.ENTITY_GROUP_USERS_SELECT,
|
|
Cmd.ENTITY_CIGROUP_USERS]:
|
|
# Skip over sub-arguments
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in GROUP_ROLES_MAP or myarg in {'primarydomain', 'recursive', 'includederivedmembership'}:
|
|
pass
|
|
elif myarg == 'domains':
|
|
Cmd.Advance()
|
|
elif ((entityType == Cmd.ENTITY_GROUP_USERS_SELECT) and
|
|
(myarg in SUSPENDED_ARGUMENTS) or (myarg in ARCHIVED_ARGUMENTS)):
|
|
if myarg in {'issuspended', 'isarchived'}:
|
|
if Cmd.PeekArgumentPresent(TRUE_VALUES) or Cmd.PeekArgumentPresent(FALSE_VALUES):
|
|
Cmd.Advance()
|
|
elif myarg == 'end':
|
|
break
|
|
else:
|
|
Cmd.Backup()
|
|
missingArgumentExit('end')
|
|
return (entityClass,
|
|
{'entityType': entityType, 'entity': entityItem, 'isSuspended': isSuspended, 'isArchived': isArchived,
|
|
'groupMemberType': groupMemberType})
|
|
if entityClass == Cmd.ENTITY_CROS:
|
|
if entityType in {Cmd.ENTITY_CROS_OU_QUERY, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERY, Cmd.ENTITY_CROS_OUS_QUERY, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERY,
|
|
Cmd.ENTITY_CROS_OU_QUERIES, Cmd.ENTITY_CROS_OU_AND_CHILDREN_QUERIES, Cmd.ENTITY_CROS_OUS_QUERIES, Cmd.ENTITY_CROS_OUS_AND_CHILDREN_QUERIES}:
|
|
Cmd.Advance()
|
|
return (entityClass,
|
|
{'entityType': entityType, 'entity': entityItem})
|
|
|
|
def getEntitySelector():
|
|
return getChoice(Cmd.ENTITY_LIST_SELECTORS, defaultChoice=None)
|
|
|
|
def getEntitySelection(entitySelector, shlexSplit):
|
|
if entitySelector in [Cmd.ENTITY_SELECTOR_FILE]:
|
|
return getEntitiesFromFile(shlexSplit)
|
|
if entitySelector in [Cmd.ENTITY_SELECTOR_CSV, Cmd.ENTITY_SELECTOR_CSVFILE]:
|
|
return getEntitiesFromCSVFile(shlexSplit)
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_CSVKMD:
|
|
return getEntitiesFromCSVbyField()
|
|
if entitySelector in [Cmd.ENTITY_SELECTOR_CSVSUBKEY]:
|
|
checkSubkeyField()
|
|
return GM.Globals[GM.CSV_DATA_DICT]
|
|
if entitySelector in [Cmd.ENTITY_SELECTOR_CSVDATA]:
|
|
checkDataField()
|
|
return GM.Globals[GM.CSV_DATA_DICT]
|
|
return []
|
|
|
|
def getEntityList(item, shlexSplit=False):
|
|
entitySelector = getEntitySelector()
|
|
if entitySelector:
|
|
return getEntitySelection(entitySelector, shlexSplit)
|
|
return convertEntityToList(getString(item, minLen=0), shlexSplit=shlexSplit)
|
|
|
|
def getNormalizedEmailAddressEntity(shlexSplit=False, noUid=True, noLower=False):
|
|
return [normalizeEmailAddressOrUID(emailAddress, noUid=noUid, noLower=noLower) for emailAddress in getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY, shlexSplit)]
|
|
|
|
def getUserObjectEntity(clObject, itemType, shlexSplit=False):
|
|
entity = {'item': itemType, 'list': getEntityList(clObject, shlexSplit), 'dict': None}
|
|
if isinstance(entity['list'], dict):
|
|
entity['dict'] = entity['list']
|
|
return entity
|
|
|
|
def _validateUserGetObjectList(user, i, count, entity, api=API.GMAIL, showAction=True):
|
|
if entity['dict']:
|
|
entityList = entity['dict'][user]
|
|
else:
|
|
entityList = entity['list']
|
|
user, svc = buildGAPIServiceObject(api, user, i, count)
|
|
if not svc:
|
|
return (user, None, [], 0)
|
|
jcount = len(entityList)
|
|
if showAction:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, entity['item'], i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (user, svc, entityList, jcount)
|
|
|
|
def _validateUserGetMessageIds(user, i, count, entity):
|
|
if entity:
|
|
if entity['dict']:
|
|
entityList = entity['dict'][user]
|
|
else:
|
|
entityList = entity['list']
|
|
else:
|
|
entityList = []
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
return (user, None, None)
|
|
return (user, gmail, entityList)
|
|
|
|
def checkUserExists(cd, user, entityType=Ent.USER, i=0, count=0):
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
return callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=user, fields='primaryEmail')['primaryEmail']
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
entityUnknownWarning(entityType, user, i, count)
|
|
return None
|
|
|
|
def checkUserSuspended(cd, user, entityType=Ent.USER, i=0, count=0):
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
return callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=user, fields='suspended')['suspended']
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
entityUnknownWarning(entityType, user, i, count)
|
|
return None
|
|
|
|
# Add attachements to an email message
|
|
def _addAttachmentsToMessage(message, attachments):
|
|
for attachment in attachments:
|
|
try:
|
|
attachFilename = attachment[0]
|
|
attachContentType, attachEncoding = mimetypes.guess_type(attachFilename)
|
|
if attachContentType is None or attachEncoding is not None:
|
|
attachContentType = 'application/octet-stream'
|
|
main_type, sub_type = attachContentType.split('/', 1)
|
|
if main_type == 'text':
|
|
msg = MIMEText(readFile(attachFilename, 'r', attachment[1]), _subtype=sub_type, _charset=UTF8)
|
|
elif main_type == 'image':
|
|
msg = MIMEImage(readFile(attachFilename, 'rb'), _subtype=sub_type)
|
|
elif main_type == 'audio':
|
|
msg = MIMEAudio(readFile(attachFilename, 'rb'), _subtype=sub_type)
|
|
elif main_type == 'application':
|
|
msg = MIMEApplication(readFile(attachFilename, 'rb'), _subtype=sub_type)
|
|
else:
|
|
msg = MIMEBase(main_type, sub_type)
|
|
msg.set_payload(readFile(attachFilename, 'rb'))
|
|
msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(attachFilename))
|
|
message.attach(msg)
|
|
except (IOError, UnicodeDecodeError) as e:
|
|
usageErrorExit(f'{attachFilename}: {str(e)}')
|
|
|
|
# Add embedded images to an email message
|
|
def _addEmbeddedImagesToMessage(message, embeddedImages):
|
|
for embeddedImage in embeddedImages:
|
|
try:
|
|
imageFilename = embeddedImage[0]
|
|
imageContentType, imageEncoding = mimetypes.guess_type(imageFilename)
|
|
if imageContentType is None or imageEncoding is not None:
|
|
imageContentType = 'application/octet-stream'
|
|
main_type, sub_type = imageContentType.split('/', 1)
|
|
if main_type == 'image':
|
|
msg = MIMEImage(readFile(imageFilename, 'rb'), _subtype=sub_type)
|
|
else:
|
|
msg = MIMEBase(main_type, sub_type)
|
|
msg.set_payload(readFile(imageFilename, 'rb'))
|
|
msg.add_header('Content-Disposition', 'attachment', filename=os.path.basename(imageFilename))
|
|
msg.add_header('Content-ID', f'<{embeddedImage[1]}>')
|
|
message.attach(msg)
|
|
except (IOError, UnicodeDecodeError) as e:
|
|
usageErrorExit(f'{imageFilename}: {str(e)}')
|
|
|
|
NAME_EMAIL_ADDRESS_PATTERN = re.compile(r'^.*<(.+)>$')
|
|
|
|
# Send an email
|
|
def send_email(msgSubject, msgBody, msgTo, i=0, count=0, clientAccess=False, msgFrom=None, msgReplyTo=None,
|
|
html=False, charset=UTF8, attachments=None, embeddedImages=None,
|
|
msgHeaders=None, ccRecipients=None, bccRecipients=None, mailBox=None):
|
|
def checkResult(entityType, recipients):
|
|
if not recipients:
|
|
return
|
|
toSent = set(recipients.split(','))
|
|
toFailed = {}
|
|
for addr, err in iter(result.items()):
|
|
if addr in toSent:
|
|
toSent.remove(addr)
|
|
toFailed[addr] = f'{err[0]}: {err[1]}'
|
|
if toSent:
|
|
entityActionPerformed([entityType, ','.join(toSent), Ent.MESSAGE, msgSubject], i, count)
|
|
for addr, errMsg in iter(toFailed.items()):
|
|
entityActionFailedWarning([entityType, addr, Ent.MESSAGE, msgSubject], errMsg, i, count)
|
|
|
|
def cleanAddr(emailAddr):
|
|
match = NAME_EMAIL_ADDRESS_PATTERN.match(emailAddr)
|
|
if match:
|
|
return match.group(1)
|
|
return emailAddr
|
|
|
|
if msgFrom is None:
|
|
msgFrom = _getAdminEmail()
|
|
# Force ASCII for RFC compliance
|
|
# xmlcharref seems to work to display at least
|
|
# some unicode in HTML body and is ignored in
|
|
# plain text body.
|
|
# msgBody = msgBody.encode('ascii', 'xmlcharrefreplace').decode(UTF8)
|
|
if not attachments and not embeddedImages:
|
|
message = MIMEText(msgBody, ['plain', 'html'][html], charset)
|
|
else:
|
|
message = MIMEMultipart()
|
|
msg = MIMEText(msgBody, ['plain', 'html'][html], charset)
|
|
message.attach(msg)
|
|
if attachments:
|
|
_addAttachmentsToMessage(message, attachments)
|
|
if embeddedImages:
|
|
_addEmbeddedImagesToMessage(message, embeddedImages)
|
|
message['Subject'] = msgSubject
|
|
message['From'] = msgFrom
|
|
if msgReplyTo is not None:
|
|
message['Reply-To'] = msgReplyTo
|
|
if ccRecipients:
|
|
message['Cc'] = ccRecipients.lower()
|
|
if bccRecipients:
|
|
message['Bcc'] = bccRecipients.lower()
|
|
if msgHeaders:
|
|
for header, value in iter(msgHeaders.items()):
|
|
if header not in {'Subject', 'From', 'To', 'Reply-To', 'Cc', 'Bcc'}:
|
|
message[header] = value
|
|
if mailBox is None:
|
|
mailBox = msgFrom
|
|
mailBoxAddr = normalizeEmailAddressOrUID(cleanAddr(mailBox), noUid=True, noLower=True)
|
|
action = Act.Get()
|
|
Act.Set(Act.SENDEMAIL)
|
|
if not GC.Values[GC.SMTP_HOST]:
|
|
if not clientAccess:
|
|
userId, gmail = buildGAPIServiceObject(API.GMAIL, mailBoxAddr)
|
|
if not gmail:
|
|
return
|
|
else:
|
|
userId = mailBoxAddr
|
|
gmail = buildGAPIObject(API.GMAIL)
|
|
message['To'] = msgTo if msgTo else userId
|
|
try:
|
|
result = callGAPI(gmail.users().messages(), 'send',
|
|
throwReasons=[GAPI.SERVICE_NOT_AVAILABLE, GAPI.AUTH_ERROR, GAPI.DOMAIN_POLICY,
|
|
GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
|
userId=userId, body={'raw': base64.urlsafe_b64encode(message.as_bytes()).decode()}, fields='id')
|
|
entityActionPerformedMessage([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], f"{result['id']}", i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], str(e), i, count)
|
|
else:
|
|
message['To'] = msgTo if msgTo else mailBoxAddr
|
|
server = None
|
|
try:
|
|
server = smtplib.SMTP(GC.Values[GC.SMTP_HOST], 587, GC.Values[GC.SMTP_FQDN])
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
server.set_debuglevel(1)
|
|
server.starttls(context=ssl.create_default_context(cafile=GC.Values[GC.CACERTS_PEM]))
|
|
if GC.Values[GC.SMTP_USERNAME] and GC.Values[GC.SMTP_PASSWORD]:
|
|
if isinstance(GC.Values[GC.SMTP_PASSWORD], bytes):
|
|
server.login(GC.Values[GC.SMTP_USERNAME], base64.b64decode(GC.Values[GC.SMTP_PASSWORD]).decode(UTF8))
|
|
else:
|
|
server.login(GC.Values[GC.SMTP_USERNAME], GC.Values[GC.SMTP_PASSWORD])
|
|
result = server.send_message(message)
|
|
checkResult(Ent.RECIPIENT, message['To'])
|
|
checkResult(Ent.RECIPIENT_CC, ccRecipients)
|
|
checkResult(Ent.RECIPIENT_BCC, bccRecipients)
|
|
except smtplib.SMTPException as e:
|
|
entityActionFailedWarning([Ent.RECIPIENT, msgTo, Ent.MESSAGE, msgSubject], str(e), i, count)
|
|
if server:
|
|
try:
|
|
server.quit()
|
|
except Exception:
|
|
pass
|
|
Act.Set(action)
|
|
|
|
def addFieldToFieldsList(fieldName, fieldsChoiceMap, fieldsList):
|
|
fields = fieldsChoiceMap[fieldName.lower()]
|
|
if isinstance(fields, list):
|
|
fieldsList.extend(fields)
|
|
else:
|
|
fieldsList.append(fields)
|
|
|
|
def _getFieldsList():
|
|
return getString(Cmd.OB_FIELD_NAME_LIST).lower().replace('_', '').replace(',', ' ').split()
|
|
|
|
def _getRawFields(requiredField=None):
|
|
rawFields = getString(Cmd.OB_FIELDS)
|
|
if requiredField is None or requiredField in rawFields:
|
|
return rawFields
|
|
return f'{requiredField},{rawFields}'
|
|
|
|
def CheckInputRowFilterHeaders(titlesList, rowFilter, rowDropFilter):
|
|
status = True
|
|
for filterVal in rowFilter:
|
|
columns = [t for t in titlesList if filterVal[0].match(t)]
|
|
if not columns:
|
|
stderrErrorMsg(Msg.COLUMN_DOES_NOT_MATCH_ANY_INPUT_COLUMNS.format(GC.CSV_INPUT_ROW_FILTER, filterVal[0].pattern))
|
|
status = False
|
|
for filterVal in rowDropFilter:
|
|
columns = [t for t in titlesList if filterVal[0].match(t)]
|
|
if not columns:
|
|
stderrErrorMsg(Msg.COLUMN_DOES_NOT_MATCH_ANY_INPUT_COLUMNS.format(GC.CSV_INPUT_ROW_DROP_FILTER, filterVal[0].pattern))
|
|
status = False
|
|
if not status:
|
|
sys.exit(USAGE_ERROR_RC)
|
|
|
|
def RowFilterMatch(row, titlesList, rowFilter, rowFilterModeAll, rowDropFilter, rowDropFilterModeAll):
|
|
def rowRegexFilterMatch(filterPattern):
|
|
if anyMatch:
|
|
for column in columns:
|
|
if filterPattern.search(str(row.get(column, ''))):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not filterPattern.search(str(row.get(column, ''))):
|
|
return False
|
|
return True
|
|
|
|
def rowNotRegexFilterMatch(filterPattern):
|
|
if anyMatch:
|
|
for column in columns:
|
|
if filterPattern.search(str(row.get(column, ''))):
|
|
return False
|
|
return True
|
|
for column in columns:
|
|
if not filterPattern.search(str(row.get(column, ''))):
|
|
return True
|
|
|
|
return False
|
|
|
|
def stripTimeFromDateTime(rowDate):
|
|
if YYYYMMDD_PATTERN.match(rowDate):
|
|
try:
|
|
rowTime = datetime.datetime.strptime(rowDate, YYYYMMDD_FORMAT)
|
|
except ValueError:
|
|
return None
|
|
else:
|
|
try:
|
|
rowTime, _ = iso8601.parse_date(rowDate)
|
|
except (iso8601.ParseError, OverflowError):
|
|
return None
|
|
return ISOformatTimeStamp(datetime.datetime(rowTime.year, rowTime.month, rowTime.day, tzinfo=iso8601.UTC))
|
|
|
|
def rowDateTimeFilterMatch(dateMode, op, filterDate):
|
|
def checkMatch(rowDate):
|
|
if not rowDate or not isinstance(rowDate, str):
|
|
return False
|
|
if rowDate == GC.Values[GC.NEVER_TIME]:
|
|
rowDate = NEVER_TIME
|
|
if dateMode:
|
|
rowDate = stripTimeFromDateTime(rowDate)
|
|
if not rowDate:
|
|
return False
|
|
if op == '<':
|
|
return rowDate < filterDate
|
|
if op == '<=':
|
|
return rowDate <= filterDate
|
|
if op == '>':
|
|
return rowDate > filterDate
|
|
if op == '>=':
|
|
return rowDate >= filterDate
|
|
if op == '!=':
|
|
return rowDate != filterDate
|
|
return rowDate == filterDate
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, '')):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, '')):
|
|
return False
|
|
return True
|
|
|
|
def rowDateTimeRangeFilterMatch(dateMode, op, filterDateL, filterDateR):
|
|
def checkMatch(rowDate):
|
|
if not rowDate or not isinstance(rowDate, str):
|
|
return False
|
|
if rowDate == GC.Values[GC.NEVER_TIME]:
|
|
rowDate = NEVER_TIME
|
|
if dateMode:
|
|
rowDate = stripTimeFromDateTime(rowDate)
|
|
if not rowDate:
|
|
return False
|
|
if op == '!=':
|
|
return not filterDateL <= rowDate <= filterDateR
|
|
return filterDateL <= rowDate <= filterDateR
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, '')):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, '')):
|
|
return False
|
|
return True
|
|
|
|
def getHourMinuteFromDateTime(rowDate):
|
|
if YYYYMMDD_PATTERN.match(rowDate):
|
|
return None
|
|
try:
|
|
rowTime, _ = iso8601.parse_date(rowDate)
|
|
except (iso8601.ParseError, OverflowError):
|
|
return None
|
|
return f'{rowTime.hour:02d}:{rowTime.minute:02d}'
|
|
|
|
def rowTimeOfDayRangeFilterMatch(op, startHourMinute, endHourMinute):
|
|
def checkMatch(rowDate):
|
|
if not rowDate or not isinstance(rowDate, str) or rowDate == GC.Values[GC.NEVER_TIME]:
|
|
return False
|
|
rowHourMinute = getHourMinuteFromDateTime(rowDate)
|
|
if not rowHourMinute:
|
|
return False
|
|
if op == '!=':
|
|
return not startHourMinute <= rowHourMinute <= endHourMinute
|
|
return startHourMinute <= rowHourMinute <= endHourMinute
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, '')):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, '')):
|
|
return False
|
|
return True
|
|
|
|
def rowCountFilterMatch(op, filterCount):
|
|
def checkMatch(rowCount):
|
|
if isinstance(rowCount, str):
|
|
##### Blank = 0
|
|
if not rowCount:
|
|
rowCount = '0'
|
|
elif not rowCount.isdigit():
|
|
return False
|
|
rowCount = int(rowCount)
|
|
elif not isinstance(rowCount, int):
|
|
return False
|
|
if op == '<':
|
|
return rowCount < filterCount
|
|
if op == '<=':
|
|
return rowCount <= filterCount
|
|
if op == '>':
|
|
return rowCount > filterCount
|
|
if op == '>=':
|
|
return rowCount >= filterCount
|
|
if op == '!=':
|
|
return rowCount != filterCount
|
|
return rowCount == filterCount
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, 0)):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, 0)):
|
|
return False
|
|
return True
|
|
|
|
def rowCountRangeFilterMatch(op, filterCountL, filterCountR):
|
|
def checkMatch(rowCount):
|
|
if isinstance(rowCount, str):
|
|
if not rowCount.isdigit():
|
|
return False
|
|
rowCount = int(rowCount)
|
|
elif not isinstance(rowCount, int):
|
|
return False
|
|
if op == '!=':
|
|
return not filterCountL <= rowCount <= filterCountR
|
|
return filterCountL <= rowCount <= filterCountR
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, 0)):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, 0)):
|
|
return False
|
|
return True
|
|
|
|
def rowLengthFilterMatch(op, filterLength):
|
|
def checkMatch(rowString):
|
|
if not isinstance(rowString, str):
|
|
return False
|
|
rowLength = len(rowString)
|
|
if op == '<':
|
|
return rowLength < filterLength
|
|
if op == '<=':
|
|
return rowLength <= filterLength
|
|
if op == '>':
|
|
return rowLength > filterLength
|
|
if op == '>=':
|
|
return rowLength >= filterLength
|
|
if op == '!=':
|
|
return rowLength != filterLength
|
|
return rowLength == filterLength
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, '')):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, '')):
|
|
return False
|
|
return True
|
|
|
|
def rowLengthRangeFilterMatch(op, filterLengthL, filterLengthR):
|
|
def checkMatch(rowString):
|
|
if not isinstance(rowString, str):
|
|
return False
|
|
rowLength = len(rowString)
|
|
if op == '!=':
|
|
return not filterLengthL <= rowLength <= filterLengthR
|
|
return filterLengthL <= rowLength <= filterLengthR
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, '')):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, '')):
|
|
return False
|
|
return True
|
|
|
|
def rowBooleanFilterMatch(filterBoolean):
|
|
def checkMatch(rowBoolean):
|
|
if isinstance(rowBoolean, bool):
|
|
return rowBoolean == filterBoolean
|
|
if isinstance(rowBoolean, str):
|
|
if rowBoolean.lower() in TRUE_FALSE:
|
|
return rowBoolean.capitalize() == str(filterBoolean)
|
|
##### Blank = False
|
|
if not rowBoolean:
|
|
return not filterBoolean
|
|
return False
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, False)):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, False)):
|
|
return False
|
|
return True
|
|
|
|
def rowDataFilterMatch(filterData):
|
|
if anyMatch:
|
|
for column in columns:
|
|
if str(row.get(column, '')) in filterData:
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not str(row.get(column, '')) in filterData:
|
|
return False
|
|
return True
|
|
|
|
def rowNotDataFilterMatch(filterData):
|
|
if anyMatch:
|
|
for column in columns:
|
|
if str(row.get(column, '')) in filterData:
|
|
return False
|
|
return True
|
|
for column in columns:
|
|
if not str(row.get(column, '')) in filterData:
|
|
return True
|
|
return False
|
|
|
|
def rowTextFilterMatch(op, filterText):
|
|
def checkMatch(rowText):
|
|
if not isinstance(rowText, str):
|
|
rowText = str(rowText)
|
|
if op == '<':
|
|
return rowText < filterText
|
|
if op == '<=':
|
|
return rowText <= filterText
|
|
if op == '>':
|
|
return rowText > filterText
|
|
if op == '>=':
|
|
return rowText >= filterText
|
|
if op == '!=':
|
|
return rowText != filterText
|
|
return rowText == filterText
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, '')):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, '')):
|
|
return False
|
|
return True
|
|
|
|
def rowTextRangeFilterMatch(op, filterTextL, filterTextR):
|
|
def checkMatch(rowText):
|
|
if not isinstance(rowText, str):
|
|
rowText = str(rowText)
|
|
if op == '!=':
|
|
return not filterTextL <= rowText <= filterTextR
|
|
return filterTextL <= rowText <= filterTextR
|
|
|
|
if anyMatch:
|
|
for column in columns:
|
|
if checkMatch(row.get(column, '')):
|
|
return True
|
|
return False
|
|
for column in columns:
|
|
if not checkMatch(row.get(column, '')):
|
|
return False
|
|
return True
|
|
|
|
def filterMatch(filterVal):
|
|
if filterVal[2] == 'regex':
|
|
if rowRegexFilterMatch(filterVal[3]):
|
|
return True
|
|
elif filterVal[2] == 'notregex':
|
|
if rowNotRegexFilterMatch(filterVal[3]):
|
|
return True
|
|
elif filterVal[2] in {'date', 'time'}:
|
|
if rowDateTimeFilterMatch(filterVal[2] == 'date', filterVal[3], filterVal[4]):
|
|
return True
|
|
elif filterVal[2] in {'daterange', 'timerange'}:
|
|
if rowDateTimeRangeFilterMatch(filterVal[2] == 'date', filterVal[3], filterVal[4], filterVal[5]):
|
|
return True
|
|
elif filterVal[2] == 'timeofdayrange':
|
|
if rowTimeOfDayRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
|
|
return True
|
|
elif filterVal[2] == 'count':
|
|
if rowCountFilterMatch(filterVal[3], filterVal[4]):
|
|
return True
|
|
elif filterVal[2] == 'countrange':
|
|
if rowCountRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
|
|
return True
|
|
elif filterVal[2] == 'length':
|
|
if rowLengthFilterMatch(filterVal[3], filterVal[4]):
|
|
return True
|
|
elif filterVal[2] == 'lengthrange':
|
|
if rowLengthRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
|
|
return True
|
|
elif filterVal[2] == 'boolean':
|
|
if rowBooleanFilterMatch(filterVal[3]):
|
|
return True
|
|
elif filterVal[2] == 'data':
|
|
if rowDataFilterMatch(filterVal[3]):
|
|
return True
|
|
elif filterVal[2] == 'notdata':
|
|
if rowNotDataFilterMatch(filterVal[3]):
|
|
return True
|
|
elif filterVal[2] == 'text':
|
|
if rowTextFilterMatch(filterVal[3], filterVal[4]):
|
|
return True
|
|
elif filterVal[2] == 'textrange':
|
|
if rowTextRangeFilterMatch(filterVal[3], filterVal[4], filterVal[5]):
|
|
return True
|
|
return False
|
|
|
|
if rowFilter:
|
|
anyMatches = False
|
|
for filterVal in rowFilter:
|
|
columns = [t for t in titlesList if filterVal[0].match(t)]
|
|
if not columns:
|
|
columns = [None]
|
|
anyMatch = filterVal[1]
|
|
if filterMatch(filterVal):
|
|
if not rowFilterModeAll: # Any - any match selects
|
|
anyMatches = True
|
|
break
|
|
else:
|
|
if rowFilterModeAll: # All - any match failure doesn't select
|
|
return False
|
|
if not rowFilterModeAll and not anyMatches: # Any - no matches doesn't select
|
|
return False
|
|
if rowDropFilter:
|
|
allMatches = True
|
|
for filterVal in rowDropFilter:
|
|
columns = [t for t in titlesList if filterVal[0].match(t)]
|
|
if not columns:
|
|
columns = [None]
|
|
anyMatch = filterVal[1]
|
|
if filterMatch(filterVal):
|
|
if not rowDropFilterModeAll: # Any - any match drops
|
|
return False
|
|
else:
|
|
if rowDropFilterModeAll: # All - any match failure doesn't drop
|
|
allMatches = False
|
|
break
|
|
if rowDropFilterModeAll and allMatches: # All - all matches drops
|
|
return False
|
|
return True
|
|
|
|
# myarg is command line argument
|
|
# fieldChoiceMap maps myarg to API field names
|
|
#FIELD_CHOICE_MAP = {
|
|
# 'foo': 'foo',
|
|
# 'foobar': 'fooBar',
|
|
# }
|
|
# fieldsList is the list of API fields
|
|
def getFieldsList(myarg, fieldsChoiceMap, fieldsList, initialField=None, fieldsArg='fields', onlyFieldsArg=False):
|
|
def addInitialField():
|
|
if isinstance(initialField, list):
|
|
fieldsList.extend(initialField)
|
|
else:
|
|
fieldsList.append(initialField)
|
|
|
|
def addMappedFields(mappedFields):
|
|
if isinstance(mappedFields, list):
|
|
fieldsList.extend(mappedFields)
|
|
else:
|
|
fieldsList.append(mappedFields)
|
|
|
|
if not onlyFieldsArg and myarg in fieldsChoiceMap:
|
|
if not fieldsList and initialField is not None:
|
|
addInitialField()
|
|
addMappedFields(fieldsChoiceMap[myarg])
|
|
elif myarg == fieldsArg:
|
|
if not fieldsList and initialField is not None:
|
|
addInitialField()
|
|
for field in _getFieldsList():
|
|
if field in fieldsChoiceMap:
|
|
addMappedFields(fieldsChoiceMap[field])
|
|
else:
|
|
invalidChoiceExit(field, fieldsChoiceMap, True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def getFieldsFromFieldsList(fieldsList):
|
|
if fieldsList:
|
|
return ','.join(set(fieldsList)).replace('.', '/')
|
|
return None
|
|
|
|
def getItemFieldsFromFieldsList(item, fieldsList, returnItemIfNoneList=False):
|
|
if fieldsList:
|
|
return f'nextPageToken,{item}({",".join(set(fieldsList))})'.replace('.', '/')
|
|
if not returnItemIfNoneList:
|
|
return None
|
|
return f'nextPageToken,{item}'
|
|
|
|
class CSVPrintFile():
|
|
|
|
def __init__(self, titles=None, sortTitles=None, indexedTitles=None):
|
|
self.rows = []
|
|
self.rowCount = 0
|
|
self.outputTranspose = GM.Globals[GM.CSV_OUTPUT_TRANSPOSE]
|
|
self.todrive = GM.Globals[GM.CSV_TODRIVE]
|
|
self.titlesSet = set()
|
|
self.titlesList = []
|
|
self.JSONtitlesSet = set()
|
|
self.JSONtitlesList = []
|
|
self.sortHeaders = []
|
|
self.SetHeaderForce(GC.Values[GC.CSV_OUTPUT_HEADER_FORCE])
|
|
if not self.headerForce and titles is not None:
|
|
self.SetTitles(titles)
|
|
self.SetJSONTitles(titles)
|
|
self.SetHeaderOrder(GC.Values[GC.CSV_OUTPUT_HEADER_ORDER])
|
|
if GM.Globals.get(GM.CSV_OUTPUT_COLUMN_DELIMITER) is None:
|
|
GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER] = GC.Values.get(GC.CSV_OUTPUT_COLUMN_DELIMITER, ',')
|
|
self.SetColumnDelimiter(GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER])
|
|
if GM.Globals.get(GM.CSV_OUTPUT_QUOTE_CHAR) is None:
|
|
GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR] = GC.Values.get(GC.CSV_OUTPUT_QUOTE_CHAR, '"')
|
|
if GM.Globals.get(GM.CSV_OUTPUT_NO_ESCAPE_CHAR) is None:
|
|
GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR] = GC.Values.get(GC.CSV_OUTPUT_NO_ESCAPE_CHAR, False)
|
|
self.SetNoEscapeChar(GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR])
|
|
self.SetQuoteChar(GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR])
|
|
# if GM.Globals.get(GM.CSV_OUTPUT_SORT_HEADERS) is None:
|
|
if not GM.Globals.get(GM.CSV_OUTPUT_SORT_HEADERS):
|
|
GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS] = GC.Values.get(GC.CSV_OUTPUT_SORT_HEADERS, [])
|
|
self.SetSortHeaders(GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS])
|
|
# if GM.Globals.get(GM.CSV_OUTPUT_TIMESTAMP_COLUMN) is None:
|
|
if not GM.Globals.get(GM.CSV_OUTPUT_TIMESTAMP_COLUMN):
|
|
GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN] = GC.Values.get(GC.CSV_OUTPUT_TIMESTAMP_COLUMN, '')
|
|
self.SetTimestampColumn(GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN])
|
|
self.SetFormatJSON(False)
|
|
self.SetMapDrive3Titles(False)
|
|
self.SetNodataFields(False, None, None, None, False)
|
|
self.SetFixPaths(False)
|
|
self.SetShowPermissionsLast(False)
|
|
self.sortTitlesSet = set()
|
|
self.sortTitlesList = []
|
|
if sortTitles is not None:
|
|
if not isinstance(sortTitles, str) or sortTitles != 'sortall':
|
|
self.SetSortTitles(sortTitles)
|
|
else:
|
|
self.SetSortAllTitles()
|
|
self.SetIndexedTitles(indexedTitles if indexedTitles is not None else [])
|
|
self.SetHeaderFilter(GC.Values[GC.CSV_OUTPUT_HEADER_FILTER])
|
|
self.SetHeaderDropFilter(GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER])
|
|
self.SetRowFilter(GC.Values[GC.CSV_OUTPUT_ROW_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
|
|
self.SetRowDropFilter(GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE])
|
|
self.SetRowLimit(GC.Values[GC.CSV_OUTPUT_ROW_LIMIT])
|
|
self.SetZeroBlankMimeTypeCounts(False)
|
|
|
|
def AddTitle(self, title):
|
|
self.titlesSet.add(title)
|
|
self.titlesList.append(title)
|
|
|
|
def AddTitles(self, titles):
|
|
for title in titles if isinstance(titles, list) else [titles]:
|
|
if title not in self.titlesSet:
|
|
self.AddTitle(title)
|
|
|
|
def SetTitles(self, titles):
|
|
self.titlesSet = set()
|
|
self.titlesList = []
|
|
self.AddTitles(titles)
|
|
|
|
def RemoveTitles(self, titles):
|
|
for title in titles if isinstance(titles, list) else [titles]:
|
|
if title in self.titlesSet:
|
|
self.titlesSet.remove(title)
|
|
self.titlesList.remove(title)
|
|
|
|
def MoveTitlesToEnd(self, titles):
|
|
self.RemoveTitles(titles)
|
|
self.AddTitles(titles)
|
|
|
|
def MapTitles(self, ov, nv):
|
|
if ov in self.titlesSet:
|
|
self.titlesSet.remove(ov)
|
|
self.titlesSet.add(nv)
|
|
for i, v in enumerate(self.titlesList):
|
|
if v == ov:
|
|
self.titlesList[i] = nv
|
|
break
|
|
|
|
def AddSortTitle(self, title):
|
|
self.sortTitlesSet.add(title)
|
|
self.sortTitlesList.append(title)
|
|
|
|
def AddSortTitles(self, titles):
|
|
for title in titles if isinstance(titles, list) else [titles]:
|
|
if title not in self.sortTitlesSet:
|
|
self.AddSortTitle(title)
|
|
|
|
def SetSortTitles(self, titles):
|
|
self.sortTitlesSet = set()
|
|
self.sortTitlesList = []
|
|
self.AddSortTitles(titles)
|
|
|
|
def SetSortAllTitles(self):
|
|
self.sortTitlesList = self.titlesList[:]
|
|
self.sortTitlesSet = set(self.sortTitlesList)
|
|
|
|
def SetMapDrive3Titles(self, mapDrive3Titles):
|
|
self.mapDrive3Titles = mapDrive3Titles
|
|
|
|
def MapDrive3TitlesToDrive2(self):
|
|
_mapDrive3TitlesToDrive2(self.titlesList, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
|
|
self.titlesSet = set(self.titlesList)
|
|
|
|
def AddJSONTitle(self, title):
|
|
self.JSONtitlesSet.add(title)
|
|
self.JSONtitlesList.append(title)
|
|
|
|
def AddJSONTitles(self, titles):
|
|
for title in titles if isinstance(titles, list) else [titles]:
|
|
if title not in self.JSONtitlesSet:
|
|
self.AddJSONTitle(title)
|
|
|
|
def RemoveJSONTitles(self, titles):
|
|
for title in titles if isinstance(titles, list) else [titles]:
|
|
if title in self.JSONtitlesSet:
|
|
self.JSONtitlesSet.remove(title)
|
|
self.JSONtitlesList.remove(title)
|
|
|
|
def MoveJSONTitlesToEnd(self, titles):
|
|
for title in titles if isinstance(titles, list) else [titles]:
|
|
if title in self.JSONtitlesList:
|
|
self.JSONtitlesList.remove(title)
|
|
self.JSONtitlesList.append(title)
|
|
|
|
def SetJSONTitles(self, titles):
|
|
self.JSONtitlesSet = set()
|
|
self.JSONtitlesList = []
|
|
self.AddJSONTitles(titles)
|
|
|
|
# fieldName is command line argument
|
|
# fieldNameMap maps fieldName to API field names; CSV file header will be API field name
|
|
#ARGUMENT_TO_PROPERTY_MAP = {
|
|
# 'admincreated': 'adminCreated',
|
|
# 'aliases': ['aliases', 'nonEditableAliases'],
|
|
# }
|
|
# fieldsList is the list of API fields
|
|
def AddField(self, fieldName, fieldNameMap, fieldsList):
|
|
fields = fieldNameMap[fieldName.lower()]
|
|
if isinstance(fields, list):
|
|
for field in fields:
|
|
if field not in fieldsList:
|
|
fieldsList.append(field)
|
|
self.AddTitles(field.replace('.', GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]))
|
|
elif fields not in fieldsList:
|
|
fieldsList.append(fields)
|
|
self.AddTitles(fields.replace('.', GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]))
|
|
|
|
def addInitialField(self, initialField, fieldsChoiceMap, fieldsList):
|
|
if isinstance(initialField, list):
|
|
for field in initialField:
|
|
self.AddField(field, fieldsChoiceMap, fieldsList)
|
|
else:
|
|
self.AddField(initialField, fieldsChoiceMap, fieldsList)
|
|
|
|
def GetFieldsListTitles(self, fieldName, fieldsChoiceMap, fieldsList, initialField=None):
|
|
if fieldName in fieldsChoiceMap:
|
|
if not fieldsList and initialField is not None:
|
|
self.addInitialField(initialField, fieldsChoiceMap, fieldsList)
|
|
self.AddField(fieldName, fieldsChoiceMap, fieldsList)
|
|
elif fieldName == 'fields':
|
|
if not fieldsList and initialField is not None:
|
|
self.addInitialField(initialField, fieldsChoiceMap, fieldsList)
|
|
for field in _getFieldsList():
|
|
if field in fieldsChoiceMap:
|
|
self.AddField(field, fieldsChoiceMap, fieldsList)
|
|
else:
|
|
invalidChoiceExit(field, fieldsChoiceMap, True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
TDSHEET_ENTITY_MAP = {'tdsheet': 'sheetEntity', 'tdbackupsheet': 'backupSheetEntity', 'tdcopysheet': 'copySheetEntity'}
|
|
TDSHARE_ACL_ROLES_MAP = {
|
|
'commenter': 'commenter',
|
|
'contributor': 'writer',
|
|
'editor': 'writer',
|
|
'read': 'reader',
|
|
'reader': 'reader',
|
|
'viewer': 'reader',
|
|
'writer': 'writer',
|
|
}
|
|
|
|
|
|
def GetTodriveParameters(self):
|
|
def invalidTodriveFileIdExit(entityValueList, message, location):
|
|
Cmd.SetLocation(location-1)
|
|
usageErrorExit(formatKeyValueList('', Ent.FormatEntityValueList([Ent.DRIVE_FILE_ID, self.todrive['fileId']]+entityValueList)+[message], ''))
|
|
|
|
def invalidTodriveParentExit(entityType, message):
|
|
Cmd.SetLocation(tdparentLocation-1)
|
|
if not localParent:
|
|
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(entityType),
|
|
formatKeyValueList('',
|
|
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
|
|
Ent.Singular(Ent.ITEM), GC.TODRIVE_PARENT,
|
|
Ent.Singular(Ent.VALUE), self.todrive['parent'],
|
|
message],
|
|
'')))
|
|
else:
|
|
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(entityType), message))
|
|
|
|
def invalidTodriveUserExit(entityType, message):
|
|
Cmd.SetLocation(tduserLocation-1)
|
|
if not localUser:
|
|
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(entityType),
|
|
formatKeyValueList('',
|
|
[Ent.Singular(Ent.CONFIG_FILE), GM.Globals[GM.GAM_CFG_FILE],
|
|
Ent.Singular(Ent.ITEM), GC.TODRIVE_USER,
|
|
Ent.Singular(Ent.VALUE), self.todrive['user'],
|
|
message],
|
|
'')))
|
|
else:
|
|
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(entityType), message))
|
|
|
|
def getDriveObject():
|
|
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
|
|
_, drive = buildGAPIServiceObject(API.DRIVETD, self.todrive['user'])
|
|
if not drive:
|
|
invalidTodriveUserExit(Ent.USER, Msg.NOT_FOUND)
|
|
else:
|
|
drive = buildGAPIObject(API.DRIVE3)
|
|
return drive
|
|
|
|
CELL_WRAP_MAP = {'clip': 'CLIP', 'overflow': 'OVERFLOW_CELL', 'overflowcell': 'OVERFLOW_CELL', 'wrap': 'WRAP'}
|
|
CELL_NUMBER_FORMAT_MAP = {'text': 'TEXT', 'number': 'NUMBER'}
|
|
|
|
localUser = localParent = False
|
|
tdfileidLocation = tdparentLocation = tdaddsheetLocation = tdupdatesheetLocation = tduserLocation = Cmd.Location()
|
|
tdsheetLocation = {}
|
|
for sheetEntity in iter(self.TDSHEET_ENTITY_MAP.values()):
|
|
tdsheetLocation[sheetEntity] = Cmd.Location()
|
|
self.todrive = {'user': GC.Values[GC.TODRIVE_USER], 'title': None, 'description': None,
|
|
'sheetEntity': None, 'addsheet': False, 'updatesheet': False, 'sheettitle': None,
|
|
'cellwrap': None, 'cellnumberformat': None, 'clearfilter': GC.Values[GC.TODRIVE_CLEARFILTER],
|
|
'backupSheetEntity': None, 'copySheetEntity': None,
|
|
'locale': GC.Values[GC.TODRIVE_LOCALE], 'timeZone': GC.Values[GC.TODRIVE_TIMEZONE],
|
|
'timestamp': GC.Values[GC.TODRIVE_TIMESTAMP], 'timeformat': GC.Values[GC.TODRIVE_TIMEFORMAT],
|
|
'noescapechar': GC.Values[GC.TODRIVE_NO_ESCAPE_CHAR],
|
|
'daysoffset': None, 'hoursoffset': None,
|
|
'sheettimestamp': GC.Values[GC.TODRIVE_SHEET_TIMESTAMP], 'sheettimeformat': GC.Values[GC.TODRIVE_SHEET_TIMEFORMAT],
|
|
'sheetdaysoffset': None, 'sheethoursoffset': None,
|
|
'fileId': None, 'parentId': None, 'parent': GC.Values[GC.TODRIVE_PARENT], 'retaintitle': False,
|
|
'localcopy': GC.Values[GC.TODRIVE_LOCALCOPY], 'uploadnodata': GC.Values[GC.TODRIVE_UPLOAD_NODATA],
|
|
'nobrowser': GC.Values[GC.TODRIVE_NOBROWSER], 'noemail': GC.Values[GC.TODRIVE_NOEMAIL], 'returnidonly': False,
|
|
'alert': [], 'share': [], 'notify': False, 'subject': None, 'from': None}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'tduser':
|
|
self.todrive['user'] = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
tduserLocation = Cmd.Location()
|
|
localUser = True
|
|
elif myarg == 'tdtitle':
|
|
self.todrive['title'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'tddescription':
|
|
self.todrive['description'] = getString(Cmd.OB_STRING)
|
|
elif myarg in self.TDSHEET_ENTITY_MAP:
|
|
sheetEntity = self.TDSHEET_ENTITY_MAP[myarg]
|
|
tdsheetLocation[sheetEntity] = Cmd.Location()
|
|
self.todrive[sheetEntity] = getSheetEntity(True)
|
|
elif myarg == 'tdaddsheet':
|
|
tdaddsheetLocation = Cmd.Location()
|
|
self.todrive['addsheet'] = getBoolean()
|
|
if self.todrive['addsheet']:
|
|
self.todrive['updatesheet'] = False
|
|
elif myarg == 'tdupdatesheet':
|
|
tdupdatesheetLocation = Cmd.Location()
|
|
self.todrive['updatesheet'] = getBoolean()
|
|
if self.todrive['updatesheet']:
|
|
self.todrive['addsheet'] = False
|
|
elif myarg == 'tdcellwrap':
|
|
self.todrive['cellwrap'] = getChoice(CELL_WRAP_MAP, mapChoice=True)
|
|
elif myarg == 'tdcellnumberformat':
|
|
self.todrive['cellnumberformat'] = getChoice(CELL_NUMBER_FORMAT_MAP, mapChoice=True)
|
|
elif myarg == 'tdclearfilter':
|
|
self.todrive['clearfilter'] = getBoolean()
|
|
elif myarg == 'tdlocale':
|
|
self.todrive['locale'] = getLanguageCode(LOCALE_CODES_MAP)
|
|
elif myarg == 'tdtimezone':
|
|
self.todrive['timeZone'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'tdtimestamp':
|
|
self.todrive['timestamp'] = getBoolean()
|
|
elif myarg == 'tdtimeformat':
|
|
self.todrive['timeformat'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'tdsheettitle':
|
|
self.todrive['sheettitle'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'tdsheettimestamp':
|
|
self.todrive['sheettimestamp'] = getBoolean()
|
|
elif myarg == 'tdsheettimeformat':
|
|
self.todrive['sheettimeformat'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'tddaysoffset':
|
|
self.todrive['daysoffset'] = getInteger(minVal=0)
|
|
elif myarg == 'tdhoursoffset':
|
|
self.todrive['hoursoffset'] = getInteger(minVal=0)
|
|
elif myarg == 'tdsheetdaysoffset':
|
|
self.todrive['sheetdaysoffset'] = getInteger(minVal=0)
|
|
elif myarg == 'tdsheethoursoffset':
|
|
self.todrive['sheethoursoffset'] = getInteger(minVal=0)
|
|
elif myarg == 'tdfileid':
|
|
self.todrive['fileId'] = getString(Cmd.OB_DRIVE_FILE_ID)
|
|
tdfileidLocation = Cmd.Location()
|
|
elif myarg == 'tdretaintitle':
|
|
self.todrive['retaintitle'] = getBoolean()
|
|
elif myarg == 'tdparent':
|
|
self.todrive['parent'] = escapeDriveFileName(getString(Cmd.OB_DRIVE_FOLDER_NAME, minLen=0))
|
|
tdparentLocation = Cmd.Location()
|
|
localParent = True
|
|
elif myarg == 'tdlocalcopy':
|
|
self.todrive['localcopy'] = getBoolean()
|
|
elif myarg == 'tduploadnodata':
|
|
self.todrive['uploadnodata'] = getBoolean()
|
|
elif myarg == 'tdnobrowser':
|
|
self.todrive['nobrowser'] = getBoolean()
|
|
elif myarg == 'tdnoemail':
|
|
self.todrive['noemail'] = getBoolean()
|
|
elif myarg == 'tdreturnidonly':
|
|
self.todrive['returnidonly'] = getBoolean()
|
|
elif myarg == 'tdnoescapechar':
|
|
self.todrive['noescapechar'] = getBoolean()
|
|
elif myarg == 'tdalert':
|
|
self.todrive['alert'].append({'emailAddress': normalizeEmailAddressOrUID(getString(Cmd.OB_EMAIL_ADDRESS))})
|
|
elif myarg == 'tdshare':
|
|
self.todrive['share'].append({'emailAddress': normalizeEmailAddressOrUID(getString(Cmd.OB_EMAIL_ADDRESS)),
|
|
'type': 'user',
|
|
'role': getChoice(self.TDSHARE_ACL_ROLES_MAP, mapChoice=True)})
|
|
elif myarg == 'tdnotify':
|
|
self.todrive['notify'] = getBoolean()
|
|
elif myarg == 'tdsubject':
|
|
self.todrive['subject'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'tdfrom':
|
|
self.todrive['from'] = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
else:
|
|
Cmd.Backup()
|
|
break
|
|
if self.todrive['addsheet']:
|
|
if not self.todrive['fileId']:
|
|
Cmd.SetLocation(tdaddsheetLocation-1)
|
|
missingArgumentExit('tdfileid')
|
|
if self.todrive['sheetEntity'] and self.todrive['sheetEntity']['sheetId']:
|
|
Cmd.SetLocation(tdsheetLocation[sheetEntity]-1)
|
|
invalidArgumentExit('tdsheet <String>')
|
|
if self.todrive['updatesheet'] and (not self.todrive['fileId'] or not self.todrive['sheetEntity']):
|
|
Cmd.SetLocation(tdupdatesheetLocation-1)
|
|
missingArgumentExit('tdfileid and tdsheet')
|
|
if self.todrive['sheetEntity'] and self.todrive['sheetEntity']['sheetId'] and (not self.todrive['fileId'] or not self.todrive['updatesheet']):
|
|
Cmd.SetLocation(tdsheetLocation['sheetEntity']-1)
|
|
missingArgumentExit('tdfileid and tdupdatesheet')
|
|
if not self.todrive['user'] or GC.Values[GC.TODRIVE_CLIENTACCESS]:
|
|
self.todrive['user'] = _getAdminEmail()
|
|
if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY] and not GC.Values[GC.TODRIVE_CLIENTACCESS]:
|
|
user = checkUserExists(buildGAPIObject(API.DIRECTORY), self.todrive['user'])
|
|
if not user:
|
|
invalidTodriveUserExit(Ent.USER, Msg.NOT_FOUND)
|
|
self.todrive['user'] = user
|
|
else:
|
|
self.todrive['user'] = normalizeEmailAddressOrUID(self.todrive['user'])
|
|
if self.todrive['fileId']:
|
|
drive = getDriveObject()
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=self.todrive['fileId'], fields='id,mimeType,capabilities(canEdit)', supportsAllDrives=True)
|
|
if result['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
invalidTodriveFileIdExit([], Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FILE)), tdfileidLocation)
|
|
if not result['capabilities']['canEdit']:
|
|
invalidTodriveFileIdExit([], Msg.NOT_WRITABLE, tdfileidLocation)
|
|
if self.todrive['sheetEntity']:
|
|
if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
|
|
invalidTodriveFileIdExit([], f'{Msg.NOT_A} {Ent.Singular(Ent.SPREADSHEET)}', tdfileidLocation)
|
|
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
|
|
_, sheet = buildGAPIServiceObject(API.SHEETSTD, self.todrive['user'])
|
|
if sheet is None:
|
|
invalidTodriveUserExit(Ent.USER, Msg.NOT_FOUND)
|
|
else:
|
|
sheet = buildGAPIObject(API.SHEETS)
|
|
try:
|
|
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=self.todrive['fileId'],
|
|
fields='spreadsheetUrl,sheets(properties(sheetId,title),protectedRanges(range(sheetId),requestingUserCanEdit))')
|
|
for sheetEntity in iter(self.TDSHEET_ENTITY_MAP.values()):
|
|
if self.todrive[sheetEntity]:
|
|
sheetId = getSheetIdFromSheetEntity(spreadsheet, self.todrive[sheetEntity])
|
|
if sheetId is None:
|
|
if not self.todrive['addsheet'] and ((sheetEntity != 'sheetEntity') or (self.todrive[sheetEntity]['sheetType'] == Ent.SHEET_ID)):
|
|
invalidTodriveFileIdExit([self.todrive[sheetEntity]['sheetType'], self.todrive[sheetEntity]['sheetValue']], Msg.NOT_FOUND, tdsheetLocation[sheetEntity])
|
|
else:
|
|
if self.todrive['addsheet']:
|
|
invalidTodriveFileIdExit([self.todrive[sheetEntity]['sheetType'], self.todrive[sheetEntity]['sheetValue']], Msg.ALREADY_EXISTS, tdsheetLocation[sheetEntity])
|
|
if protectedSheetId(spreadsheet, sheetId):
|
|
invalidTodriveFileIdExit([self.todrive[sheetEntity]['sheetType'], self.todrive[sheetEntity]['sheetValue']], Msg.NOT_WRITABLE, tdsheetLocation[sheetEntity])
|
|
self.todrive[sheetEntity]['sheetId'] = sheetId
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
invalidTodriveFileIdExit([], str(e), tdfileidLocation)
|
|
except GAPI.fileNotFound:
|
|
invalidTodriveFileIdExit([], Msg.NOT_FOUND, tdfileidLocation)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
invalidTodriveUserExit(Ent.USER, str(e))
|
|
elif not self.todrive['parent'] or self.todrive['parent'] == ROOT:
|
|
self.todrive['parentId'] = ROOT
|
|
else:
|
|
drive = getDriveObject()
|
|
if self.todrive['parent'].startswith('id:'):
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND, GAPI.INVALID],
|
|
fileId=self.todrive['parent'][3:], fields='id,mimeType,capabilities(canEdit)', supportsAllDrives=True)
|
|
except GAPI.fileNotFound:
|
|
invalidTodriveParentExit(Ent.DRIVE_FOLDER_ID, Msg.NOT_FOUND)
|
|
except GAPI.invalid as e:
|
|
invalidTodriveParentExit(Ent.DRIVE_FOLDER_ID, str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
invalidTodriveUserExit(Ent.USER, str(e))
|
|
if result['mimeType'] != MIMETYPE_GA_FOLDER:
|
|
invalidTodriveParentExit(Ent.DRIVE_FOLDER_ID, Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FOLDER)))
|
|
if not result['capabilities']['canEdit']:
|
|
invalidTodriveParentExit(Ent.DRIVE_FOLDER_ID, Msg.NOT_WRITABLE)
|
|
self.todrive['parentId'] = result['id']
|
|
else:
|
|
try:
|
|
results = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=f"name = '{self.todrive['parent']}'",
|
|
fields='nextPageToken,files(id,mimeType,capabilities(canEdit))',
|
|
pageSize=1, supportsAllDrives=True)
|
|
except GAPI.invalidQuery:
|
|
invalidTodriveParentExit(Ent.DRIVE_FOLDER_NAME, Msg.NOT_FOUND)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
invalidTodriveUserExit(Ent.USER, str(e))
|
|
if not results:
|
|
invalidTodriveParentExit(Ent.DRIVE_FOLDER_NAME, Msg.NOT_FOUND)
|
|
if results[0]['mimeType'] != MIMETYPE_GA_FOLDER:
|
|
invalidTodriveParentExit(Ent.DRIVE_FOLDER_NAME, Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FOLDER)))
|
|
if not results[0]['capabilities']['canEdit']:
|
|
invalidTodriveParentExit(Ent.DRIVE_FOLDER_NAME, Msg.NOT_WRITABLE)
|
|
self.todrive['parentId'] = results[0]['id']
|
|
|
|
def SortTitles(self):
|
|
if not self.sortTitlesList:
|
|
return
|
|
restoreTitles = []
|
|
for title in self.sortTitlesList:
|
|
if title in self.titlesSet:
|
|
self.titlesList.remove(title)
|
|
restoreTitles.append(title)
|
|
self.titlesList.sort()
|
|
for title in restoreTitles[::-1]:
|
|
self.titlesList.insert(0, title)
|
|
|
|
def RemoveIndexedTitles(self, titles):
|
|
for title in titles if isinstance(titles, list) else [titles]:
|
|
if title in self.indexedTitles:
|
|
self.indexedTitles.remove(title)
|
|
|
|
def SetIndexedTitles(self, indexedTitles):
|
|
self.indexedTitles = indexedTitles
|
|
|
|
def SortIndexedTitles(self, titlesList):
|
|
for field in self.indexedTitles:
|
|
fieldDotN = re.compile(fr'({field}){GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}(\d+)(.*)')
|
|
indexes = []
|
|
subtitles = []
|
|
for i, v in enumerate(titlesList):
|
|
mg = fieldDotN.match(v)
|
|
if mg:
|
|
indexes.append(i)
|
|
subtitles.append(mg.groups(''))
|
|
for i, ii in enumerate(indexes):
|
|
titlesList[ii] = [f'{subtitle[0]}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subtitle[1]}{subtitle[2]}' for subtitle in sorted(subtitles, key=lambda k: (int(k[1]), k[2]))][i]
|
|
|
|
@staticmethod
|
|
def FixPathsTitles(titlesList):
|
|
# Put paths before path.0
|
|
try:
|
|
index = titlesList.index(f'path{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0')
|
|
titlesList.remove('paths')
|
|
titlesList.insert(index, 'paths')
|
|
except ValueError:
|
|
pass
|
|
|
|
def FixNodataTitles(self):
|
|
if self.mapNodataFields:
|
|
titles = []
|
|
addPermissionsTitle = not self.oneItemPerRow
|
|
for field in self.nodataFields:
|
|
if field.find('(') != -1:
|
|
field, subFields = field.split('(', 1)
|
|
if field in self.driveListFields:
|
|
if field != 'permissions':
|
|
titles.append(field)
|
|
elif addPermissionsTitle:
|
|
titles.append(field)
|
|
addPermissionsTitle = False
|
|
titles.extend([f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}' for subField in subFields[:-1].split(',') if subField])
|
|
else:
|
|
titles.extend([f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}' for subField in subFields[:-1].split(',') if subField])
|
|
elif field.find('.') != -1:
|
|
field, subField = field.split('.', 1)
|
|
if field in self.driveListFields:
|
|
if field != 'permissions':
|
|
titles.append(field)
|
|
elif addPermissionsTitle:
|
|
titles.append(field)
|
|
addPermissionsTitle = False
|
|
titles.append(f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}')
|
|
else:
|
|
titles.append(f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}')
|
|
elif field.lower() in self.driveSubfieldsChoiceMap:
|
|
if field in self.driveListFields:
|
|
if field != 'permissions':
|
|
titles.append(field)
|
|
elif addPermissionsTitle:
|
|
titles.append(field)
|
|
addPermissionsTitle = False
|
|
for subField in iter(self.driveSubfieldsChoiceMap[field.lower()].values()):
|
|
if not isinstance(subField, list):
|
|
titles.append(f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}')
|
|
else:
|
|
titles.extend([f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}0{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subSubField}' for subSubField in subField])
|
|
else:
|
|
for subField in iter(self.driveSubfieldsChoiceMap[field.lower()].values()):
|
|
if not isinstance(subField, list):
|
|
titles.append(f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subField}')
|
|
else:
|
|
titles.extend([f'{field}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{subSubField}' for subSubField in subField])
|
|
else:
|
|
titles.append(field)
|
|
if self.oneItemPerRow:
|
|
for i, title in enumerate(titles):
|
|
if title.startswith('permissions.0'):
|
|
titles[i] = title.replace('permissions.0', 'permission')
|
|
if not self.formatJSON:
|
|
self.SetTitles(titles)
|
|
self.SetSortTitles(['Owner', 'id', 'name', 'title'])
|
|
self.SortTitles()
|
|
else:
|
|
self.SetJSONTitles(titles)
|
|
else:
|
|
self.SetTitles(self.nodataFields)
|
|
self.SetJSONTitles(self.nodataFields)
|
|
|
|
def MovePermsToEnd(self):
|
|
# Put permissions at end of titles
|
|
try:
|
|
last = len(self.titlesList)
|
|
start = end = self.titlesList.index('permissions')
|
|
while end < last and self.titlesList[end].startswith('permissions'):
|
|
end += 1
|
|
self.titlesList = self.titlesList[:start]+self.titlesList[end:]+self.titlesList[start:end]
|
|
except ValueError:
|
|
pass
|
|
|
|
def SetColumnDelimiter(self, columnDelimiter):
|
|
self.columnDelimiter = columnDelimiter
|
|
|
|
def SetNoEscapeChar(self, noEscapeChar):
|
|
self.noEscapeChar = noEscapeChar
|
|
|
|
def SetQuoteChar(self, quoteChar):
|
|
self.quoteChar = quoteChar
|
|
|
|
def SetTimestampColumn(self, timestampColumn):
|
|
self.timestampColumn = timestampColumn
|
|
if not GC.Values[GC.OUTPUT_TIMEFORMAT]:
|
|
self.todaysTime = ISOformatTimeStamp(todaysTime())
|
|
else:
|
|
self.todaysTime = todaysTime().strftime(GC.Values[GC.OUTPUT_TIMEFORMAT])
|
|
|
|
def SetSortHeaders(self, sortHeaders):
|
|
self.sortHeaders = sortHeaders
|
|
|
|
def SetFormatJSON(self, formatJSON):
|
|
self.formatJSON = formatJSON
|
|
|
|
def SetNodataFields(self, mapNodataFields, nodataFields, driveListFields, driveSubfieldsChoiceMap, oneItemPerRow):
|
|
self.mapNodataFields = mapNodataFields
|
|
self.nodataFields = nodataFields
|
|
self.driveListFields = driveListFields
|
|
self.driveSubfieldsChoiceMap = driveSubfieldsChoiceMap
|
|
self.oneItemPerRow = oneItemPerRow
|
|
|
|
def SetFixPaths(self, fixPaths):
|
|
self.fixPaths = fixPaths
|
|
|
|
def SetShowPermissionsLast(self, showPermissionsLast):
|
|
self.showPermissionsLast = showPermissionsLast
|
|
|
|
def FixCourseAliasesTitles(self):
|
|
# Put Aliases.* after Aliases
|
|
try:
|
|
aliasesIndex = self.sortTitlesList.index('Aliases')
|
|
index = self.titlesList.index('Aliases.0')
|
|
tempSortTitlesList = self.sortTitlesList[:]
|
|
self.SetSortTitles(tempSortTitlesList[:aliasesIndex+1])
|
|
while self.titlesList[index].startswith('Aliases.'):
|
|
self.AddSortTitle(self.titlesList[index])
|
|
index += 1
|
|
self.AddSortTitles(tempSortTitlesList[aliasesIndex+1:])
|
|
except ValueError:
|
|
pass
|
|
|
|
def RearrangeCourseTitles(self, ttitles, stitles):
|
|
# Put teachers and students after courseMaterialSets if present, otherwise at end
|
|
for title in ttitles['list']:
|
|
if title in self.titlesList:
|
|
self.titlesList.remove(title)
|
|
for title in stitles['list']:
|
|
if title in self.titlesList:
|
|
self.titlesList.remove(title)
|
|
try:
|
|
cmsIndex = self.titlesList.index('courseMaterialSets')
|
|
self.titlesList = self.titlesList[:cmsIndex]+ttitles['list']+stitles['list']+self.titlesList[cmsIndex:]
|
|
except ValueError:
|
|
self.titlesList.extend(ttitles['list'])
|
|
self.titlesList.extend(stitles['list'])
|
|
|
|
def SortRows(self, title, reverse):
|
|
if title in self.titlesSet:
|
|
self.rows.sort(key=lambda k: k[title], reverse=reverse)
|
|
|
|
def SortRowsTwoTitles(self, title1, title2, reverse):
|
|
if title1 in self.titlesSet and title2 in self.titlesSet:
|
|
self.rows.sort(key=lambda k: (k[title1], k[title2]), reverse=reverse)
|
|
|
|
def SetRowFilter(self, rowFilter, rowFilterMode):
|
|
self.rowFilter = rowFilter
|
|
self.rowFilterMode = rowFilterMode
|
|
|
|
def SetRowDropFilter(self, rowDropFilter, rowDropFilterMode):
|
|
self.rowDropFilter = rowDropFilter
|
|
self.rowDropFilterMode = rowDropFilterMode
|
|
|
|
def SetRowLimit(self, rowLimit):
|
|
self.rowLimit = rowLimit
|
|
|
|
def AppendRow(self, row):
|
|
if self.timestampColumn:
|
|
row[self.timestampColumn] = self.todaysTime
|
|
if not self.rowLimit or self.rowCount < self.rowLimit:
|
|
self.rowCount +=1
|
|
self.rows.append(row)
|
|
|
|
def WriteRowNoFilter(self, row):
|
|
self.AppendRow(row)
|
|
|
|
def WriteRow(self, row):
|
|
if RowFilterMatch(row, self.titlesList, self.rowFilter, self.rowFilterMode, self.rowDropFilter, self.rowDropFilterMode):
|
|
self.AppendRow(row)
|
|
|
|
def WriteRowTitles(self, row):
|
|
for title in row:
|
|
if title not in self.titlesSet:
|
|
self.AddTitle(title)
|
|
if RowFilterMatch(row, self.titlesList, self.rowFilter, self.rowFilterMode, self.rowDropFilter, self.rowDropFilterMode):
|
|
self.AppendRow(row)
|
|
|
|
def WriteRowTitlesNoFilter(self, row):
|
|
for title in row:
|
|
if title not in self.titlesSet:
|
|
self.AddTitle(title)
|
|
self.AppendRow(row)
|
|
|
|
def WriteRowTitlesJSONNoFilter(self, row):
|
|
for title in row:
|
|
if title not in self.JSONtitlesSet:
|
|
self.AddJSONTitle(title)
|
|
self.AppendRow(row)
|
|
|
|
def CheckRowTitles(self, row):
|
|
if not self.rowFilter and not self.rowDropFilter:
|
|
return True
|
|
for title in row:
|
|
if title not in self.titlesSet:
|
|
self.AddTitle(title)
|
|
return RowFilterMatch(row, self.titlesList, self.rowFilter, self.rowFilterMode, self.rowDropFilter, self.rowDropFilterMode)
|
|
|
|
def UpdateMimeTypeCounts(self, row, mimeTypeInfo, sizeField):
|
|
saveList = self.titlesList[:]
|
|
saveSet = set(self.titlesSet)
|
|
for title in row:
|
|
if title not in self.titlesSet:
|
|
self.AddTitle(title)
|
|
if RowFilterMatch(row, self.titlesList, self.rowFilter, self.rowFilterMode, self.rowDropFilter, self.rowDropFilterMode):
|
|
mimeTypeInfo.setdefault(row['mimeType'], {'count': 0, 'size': 0})
|
|
mimeTypeInfo[row['mimeType']]['count'] += 1
|
|
mimeTypeInfo[row['mimeType']]['size'] += int(row.get(sizeField, '0'))
|
|
self.titlesList = saveList[:]
|
|
self.titlesSet = set(saveSet)
|
|
|
|
def SetZeroBlankMimeTypeCounts(self, zeroBlankMimeTypeCounts):
|
|
self.zeroBlankMimeTypeCounts = zeroBlankMimeTypeCounts
|
|
|
|
def ZeroBlankMimeTypeCounts(self):
|
|
for row in self.rows:
|
|
for title in self.titlesList:
|
|
if title not in self.sortTitlesSet and title not in row:
|
|
row[title] = 0
|
|
|
|
def CheckOutputRowFilterHeaders(self):
|
|
for filterVal in self.rowFilter:
|
|
columns = [t for t in self.titlesList if filterVal[0].match(t)]
|
|
if not columns:
|
|
stderrWarningMsg(Msg.COLUMN_DOES_NOT_MATCH_ANY_OUTPUT_COLUMNS.format(GC.CSV_OUTPUT_ROW_FILTER, filterVal[0].pattern))
|
|
for filterVal in self.rowDropFilter:
|
|
columns = [t for t in self.titlesList if filterVal[0].match(t)]
|
|
if not columns:
|
|
stderrWarningMsg(Msg.COLUMN_DOES_NOT_MATCH_ANY_OUTPUT_COLUMNS.format(GC.CSV_OUTPUT_ROW_DROP_FILTER, filterVal[0].pattern))
|
|
|
|
def SetHeaderFilter(self, headerFilter):
|
|
self.headerFilter = headerFilter
|
|
|
|
def SetHeaderDropFilter(self, headerDropFilter):
|
|
self.headerDropFilter = headerDropFilter
|
|
|
|
def SetHeaderForce(self, headerForce):
|
|
self.headerForce = headerForce
|
|
self.SetTitles(headerForce)
|
|
self.SetJSONTitles(headerForce)
|
|
|
|
def SetHeaderOrder(self, headerOrder):
|
|
self.headerOrder = headerOrder
|
|
|
|
def orderHeaders(self, titlesList):
|
|
for title in self.headerOrder:
|
|
if title in titlesList:
|
|
titlesList.remove(title)
|
|
return self.headerOrder+titlesList
|
|
|
|
@staticmethod
|
|
def HeaderFilterMatch(filters, title):
|
|
for filterStr in filters:
|
|
if filterStr.match(title):
|
|
return True
|
|
return False
|
|
|
|
def FilterHeaders(self):
|
|
if self.headerDropFilter:
|
|
self.titlesList = [t for t in self.titlesList if not self.HeaderFilterMatch(self.headerDropFilter, t)]
|
|
if self.headerFilter:
|
|
self.titlesList = [t for t in self.titlesList if self.HeaderFilterMatch(self.headerFilter, t)]
|
|
self.titlesSet = set(self.titlesList)
|
|
if not self.titlesSet:
|
|
systemErrorExit(USAGE_ERROR_RC, Msg.NO_COLUMNS_SELECTED_WITH_CSV_OUTPUT_HEADER_FILTER.format(GC.CSV_OUTPUT_HEADER_FILTER, GC.CSV_OUTPUT_HEADER_DROP_FILTER))
|
|
|
|
def FilterJSONHeaders(self):
|
|
if self.headerDropFilter:
|
|
self.JSONtitlesList = [t for t in self.JSONtitlesList if not self.HeaderFilterMatch(self.headerDropFilter, t)]
|
|
if self.headerFilter:
|
|
self.JSONtitlesList = [t for t in self.JSONtitlesList if self.HeaderFilterMatch(self.headerFilter, t)]
|
|
self.JSONtitlesSet = set(self.JSONtitlesList)
|
|
if not self.JSONtitlesSet:
|
|
systemErrorExit(USAGE_ERROR_RC, Msg.NO_COLUMNS_SELECTED_WITH_CSV_OUTPUT_HEADER_FILTER.format(GC.CSV_OUTPUT_HEADER_FILTER, GC.CSV_OUTPUT_HEADER_DROP_FILTER))
|
|
|
|
def writeCSVfile(self, list_type):
|
|
|
|
def todriveCSVErrorExit(entityValueList, errMsg):
|
|
systemErrorExit(ACTION_FAILED_RC, formatKeyValueList(Ind.Spaces(),
|
|
Ent.FormatEntityValueList(entityValueList)+[Act.NotPerformed(), errMsg],
|
|
currentCountNL(0, 0)))
|
|
|
|
@staticmethod
|
|
def itemgetter(*items):
|
|
if len(items) == 1:
|
|
item = items[0]
|
|
def g(obj):
|
|
return obj.get(item, '')
|
|
else:
|
|
def g(obj):
|
|
return tuple(obj.get(item, '') for item in items)
|
|
return g
|
|
|
|
def writeCSVData(writer):
|
|
try:
|
|
if GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER]:
|
|
writer.writerow(dict((item, item) for item in writer.fieldnames))
|
|
if not self.sortHeaders:
|
|
writer.writerows(self.rows)
|
|
else:
|
|
for row in sorted(self.rows, key=itemgetter(*self.sortHeaders)):
|
|
writer.writerow(row)
|
|
return True
|
|
except IOError as e:
|
|
stderrErrorMsg(e)
|
|
return False
|
|
|
|
def setDialect(lineterminator, noEscapeChar):
|
|
writerDialect = {
|
|
'delimiter': self.columnDelimiter,
|
|
'doublequote': True,
|
|
'escapechar': '\\' if not noEscapeChar else None,
|
|
'lineterminator': lineterminator,
|
|
'quotechar': self.quoteChar,
|
|
'quoting': csv.QUOTE_MINIMAL,
|
|
'skipinitialspace': False,
|
|
'strict': False}
|
|
return writerDialect
|
|
|
|
def normalizeSortHeaders():
|
|
if self.sortHeaders:
|
|
writerKeyMap = {}
|
|
for k in titlesList:
|
|
writerKeyMap[k.lower()] = k
|
|
self.sortHeaders = [writerKeyMap[k.lower()] for k in self.sortHeaders if k.lower() in writerKeyMap]
|
|
|
|
def writeCSVToStdout():
|
|
csvFile = StringIOobject()
|
|
writerDialect = setDialect('\n', self.noEscapeChar)
|
|
writer = csv.DictWriter(csvFile, titlesList, extrasaction=extrasaction, **writerDialect)
|
|
if writeCSVData(writer):
|
|
try:
|
|
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].write(csvFile.getvalue())
|
|
except IOError as e:
|
|
stderrErrorMsg(fdErrorMessage(GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD], 'stdout', e))
|
|
setSysExitRC(FILE_ERROR_RC)
|
|
closeFile(csvFile)
|
|
|
|
def writeCSVToFile():
|
|
csvFile = openFile(GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME], GM.Globals[GM.CSVFILE][GM.REDIRECT_MODE], newline='',
|
|
encoding=GM.Globals[GM.CSVFILE][GM.REDIRECT_ENCODING], errors='backslashreplace',
|
|
continueOnError=True)
|
|
if csvFile:
|
|
writerDialect = setDialect(str(GC.Values[GC.CSV_OUTPUT_LINE_TERMINATOR]), self.noEscapeChar)
|
|
writer = csv.DictWriter(csvFile, titlesList, extrasaction=extrasaction, **writerDialect)
|
|
writeCSVData(writer)
|
|
closeFile(csvFile)
|
|
|
|
def writeCSVToDrive():
|
|
numRows = len(self.rows)
|
|
numColumns = len(titlesList)
|
|
if numRows == 0 and not self.todrive['uploadnodata']:
|
|
printKeyValueList([Msg.NO_CSV_DATA_TO_UPLOAD])
|
|
setSysExitRC(NO_CSV_DATA_TO_UPLOAD_RC)
|
|
return
|
|
if self.todrive['addsheet'] or self.todrive['updatesheet']:
|
|
csvFile = TemporaryFile(mode='w+', encoding=UTF8)
|
|
else:
|
|
csvFile = StringIOobject()
|
|
writerDialect = setDialect('\n', self.todrive['noescapechar'])
|
|
writer = csv.DictWriter(csvFile, titlesList, extrasaction=extrasaction, **writerDialect)
|
|
if writeCSVData(writer):
|
|
if ((self.todrive['title'] is None) or
|
|
(not self.todrive['title'] and not self.todrive['timestamp'])):
|
|
title = f'{GC.Values[GC.DOMAIN]} - {list_type}'
|
|
else:
|
|
title = self.todrive['title']
|
|
if ((self.todrive['sheettitle'] is None) or
|
|
(not self.todrive['sheettitle'] and not self.todrive['sheettimestamp'])):
|
|
if ((self.todrive['sheetEntity'] is None) or
|
|
(not self.todrive['sheetEntity']['sheetTitle'])):
|
|
sheetTitle = title
|
|
else:
|
|
sheetTitle = self.todrive['sheetEntity']['sheetTitle']
|
|
else:
|
|
sheetTitle = self.todrive['sheettitle']
|
|
tdbasetime = tdtime = datetime.datetime.now(GC.Values[GC.TIMEZONE])
|
|
if self.todrive['daysoffset'] is not None or self.todrive['hoursoffset'] is not None:
|
|
tdtime = tdbasetime+relativedelta(days=-self.todrive['daysoffset'] if self.todrive['daysoffset'] is not None else 0,
|
|
hours=-self.todrive['hoursoffset'] if self.todrive['hoursoffset'] is not None else 0)
|
|
if self.todrive['timestamp']:
|
|
if title:
|
|
title += ' - '
|
|
if not self.todrive['timeformat']:
|
|
title += ISOformatTimeStamp(tdtime)
|
|
else:
|
|
title += tdtime.strftime(self.todrive['timeformat'])
|
|
if self.todrive['sheettimestamp']:
|
|
if self.todrive['sheetdaysoffset'] is not None or self.todrive['sheethoursoffset'] is not None:
|
|
tdtime = tdbasetime+relativedelta(days=-self.todrive['sheetdaysoffset'] if self.todrive['sheetdaysoffset'] is not None else 0,
|
|
hours=-self.todrive['sheethoursoffset'] if self.todrive['sheethoursoffset'] is not None else 0)
|
|
if sheetTitle:
|
|
sheetTitle += ' - '
|
|
if not self.todrive['sheettimeformat']:
|
|
sheetTitle += ISOformatTimeStamp(tdtime)
|
|
else:
|
|
sheetTitle += tdtime.strftime(self.todrive['sheettimeformat'])
|
|
action = Act.Get()
|
|
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
|
|
user, drive = buildGAPIServiceObject(API.DRIVETD, self.todrive['user'])
|
|
if not drive:
|
|
closeFile(csvFile)
|
|
return
|
|
else:
|
|
user = self.todrive['user']
|
|
drive = buildGAPIObject(API.DRIVE3)
|
|
importSize = csvFile.tell()
|
|
# Add/Update sheet
|
|
try:
|
|
if self.todrive['addsheet'] or self.todrive['updatesheet']:
|
|
Act.Set(Act.CREATE if self.todrive['addsheet'] else Act.UPDATE)
|
|
result = callGAPI(drive.about(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='maxImportSizes')
|
|
if numRows*numColumns > MAX_GOOGLE_SHEET_CELLS or importSize > int(result['maxImportSizes'][MIMETYPE_GA_SPREADSHEET]):
|
|
todriveCSVErrorExit([Ent.USER, user], Msg.RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET)
|
|
fields = ','.join(['id', 'mimeType', 'webViewLink', 'name', 'capabilities(canEdit)'])
|
|
body = {'description': self.todrive['description']}
|
|
if body['description'] is None:
|
|
body['description'] = Cmd.QuotedArgumentList(Cmd.AllArguments())
|
|
if not self.todrive['retaintitle']:
|
|
body['name'] = title
|
|
result = callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR],
|
|
fileId=self.todrive['fileId'], body=body, fields=fields, supportsAllDrives=True)
|
|
entityValueList = [Ent.USER, user, Ent.DRIVE_FILE_ID, self.todrive['fileId']]
|
|
if not result['capabilities']['canEdit']:
|
|
todriveCSVErrorExit(entityValueList, Msg.NOT_WRITABLE)
|
|
if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
|
|
todriveCSVErrorExit(entityValueList, f'{Msg.NOT_A} {Ent.Singular(Ent.SPREADSHEET)}')
|
|
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
|
|
_, sheet = buildGAPIServiceObject(API.SHEETSTD, user)
|
|
if sheet is None:
|
|
return
|
|
else:
|
|
sheet = buildGAPIObject(API.SHEETS)
|
|
csvFile.seek(0)
|
|
spreadsheet = None
|
|
if self.todrive['updatesheet']:
|
|
for sheetEntity in iter(self.TDSHEET_ENTITY_MAP.values()):
|
|
if self.todrive[sheetEntity]:
|
|
entityValueList = [Ent.USER, user, Ent.SPREADSHEET, title, self.todrive[sheetEntity]['sheetType'], self.todrive[sheetEntity]['sheetValue']]
|
|
if spreadsheet is None:
|
|
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=self.todrive['fileId'],
|
|
fields='spreadsheetUrl,sheets(properties(sheetId,title),protectedRanges(range(sheetId),requestingUserCanEdit))')
|
|
sheetId = getSheetIdFromSheetEntity(spreadsheet, self.todrive[sheetEntity])
|
|
if sheetId is None:
|
|
if ((sheetEntity != 'sheetEntity') or (self.todrive[sheetEntity]['sheetType'] == Ent.SHEET_ID)):
|
|
todriveCSVErrorExit(entityValueList, Msg.NOT_FOUND)
|
|
self.todrive['addsheet'] = True
|
|
else:
|
|
if protectedSheetId(spreadsheet, sheetId):
|
|
todriveCSVErrorExit(entityValueList, Msg.NOT_WRITABLE)
|
|
self.todrive[sheetEntity]['sheetId'] = sheetId
|
|
if self.todrive['addsheet']:
|
|
body = {'requests': [{'addSheet': {'properties': {'title': sheetTitle, 'sheetType': 'GRID'}}}]}
|
|
try:
|
|
addresult = callGAPI(sheet.spreadsheets(), 'batchUpdate',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=self.todrive['fileId'], body=body)
|
|
self.todrive['sheetEntity'] = {'sheetId': addresult['replies'][0]['addSheet']['properties']['sheetId']}
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
todriveCSVErrorExit(entityValueList, str(e))
|
|
body = {'requests': []}
|
|
if not self.todrive['addsheet']:
|
|
if self.todrive['backupSheetEntity']:
|
|
body['requests'].append({'copyPaste': {'source': {'sheetId': self.todrive['sheetEntity']['sheetId']},
|
|
'destination': {'sheetId': self.todrive['backupSheetEntity']['sheetId']}, 'pasteType': 'PASTE_NORMAL'}})
|
|
if self.todrive['clearfilter']:
|
|
body['requests'].append({'clearBasicFilter': {'sheetId': self.todrive['sheetEntity']['sheetId']}})
|
|
if self.todrive['sheettitle']:
|
|
body['requests'].append({'updateSheetProperties':
|
|
{'properties': {'sheetId': self.todrive['sheetEntity']['sheetId'], 'title': sheetTitle}, 'fields': 'title'}})
|
|
body['requests'].append({'updateCells': {'range': {'sheetId': self.todrive['sheetEntity']['sheetId']}, 'fields': '*'}})
|
|
if self.todrive['cellwrap']:
|
|
body['requests'].append({'repeatCell': {'range': {'sheetId': self.todrive['sheetEntity']['sheetId']},
|
|
'fields': 'userEnteredFormat.wrapStrategy',
|
|
'cell': {'userEnteredFormat': {'wrapStrategy': self.todrive['cellwrap']}}}})
|
|
if self.todrive['cellnumberformat']:
|
|
body['requests'].append({'repeatCell': {'range': {'sheetId': self.todrive['sheetEntity']['sheetId']},
|
|
'fields': 'userEnteredFormat.numberFormat',
|
|
'cell': {'userEnteredFormat': {'numberFormat': {'type': self.todrive['cellnumberformat']}}}}})
|
|
body['requests'].append({'pasteData': {'coordinate': {'sheetId': self.todrive['sheetEntity']['sheetId'], 'rowIndex': '0', 'columnIndex': '0'},
|
|
'data': csvFile.read(), 'type': 'PASTE_NORMAL', 'delimiter': self.columnDelimiter}})
|
|
if self.todrive['copySheetEntity']:
|
|
body['requests'].append({'copyPaste': {'source': {'sheetId': self.todrive['sheetEntity']['sheetId']},
|
|
'destination': {'sheetId': self.todrive['copySheetEntity']['sheetId']}, 'pasteType': 'PASTE_NORMAL'}})
|
|
try:
|
|
callGAPI(sheet.spreadsheets(), 'batchUpdate',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=self.todrive['fileId'], body=body)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
todriveCSVErrorExit(entityValueList, str(e))
|
|
closeFile(csvFile)
|
|
# Create/update file
|
|
else:
|
|
if GC.Values[GC.TODRIVE_CONVERSION]:
|
|
result = callGAPI(drive.about(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='maxImportSizes')
|
|
if numRows*len(titlesList) > MAX_GOOGLE_SHEET_CELLS or importSize > int(result['maxImportSizes'][MIMETYPE_GA_SPREADSHEET]):
|
|
printKeyValueList([WARNING, Msg.RESULTS_TOO_LARGE_FOR_GOOGLE_SPREADSHEET])
|
|
mimeType = 'text/csv'
|
|
else:
|
|
mimeType = MIMETYPE_GA_SPREADSHEET
|
|
else:
|
|
mimeType = 'text/csv'
|
|
fields = ','.join(['id', 'mimeType', 'webViewLink'])
|
|
body = {'description': self.todrive['description'], 'mimeType': mimeType}
|
|
if body['description'] is None:
|
|
body['description'] = Cmd.QuotedArgumentList(Cmd.AllArguments())
|
|
if not self.todrive['fileId'] or not self.todrive['retaintitle']:
|
|
body['name'] = title
|
|
try:
|
|
if not self.todrive['fileId']:
|
|
Act.Set(Act.CREATE)
|
|
body['parents'] = [self.todrive['parentId']]
|
|
result = callGAPI(drive.files(), 'create',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.INTERNAL_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
|
|
body=body,
|
|
media_body=googleapiclient.http.MediaIoBaseUpload(io.BytesIO(csvFile.getvalue().encode()), mimetype='text/csv', resumable=True),
|
|
fields=fields, supportsAllDrives=True)
|
|
else:
|
|
Act.Set(Act.UPDATE)
|
|
result = callGAPI(drive.files(), 'update',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.INTERNAL_ERROR],
|
|
fileId=self.todrive['fileId'],
|
|
body=body,
|
|
media_body=googleapiclient.http.MediaIoBaseUpload(io.BytesIO(csvFile.getvalue().encode()), mimetype='text/csv', resumable=True),
|
|
fields=fields, supportsAllDrives=True)
|
|
spreadsheetId = result['id']
|
|
except GAPI.internalError as e:
|
|
entityActionFailedWarning([Ent.DRIVE_FILE, body['name']], Msg.UPLOAD_CSV_FILE_INTERNAL_ERROR.format(str(e), str(numRows)))
|
|
closeFile(csvFile)
|
|
return
|
|
closeFile(csvFile)
|
|
if not self.todrive['fileId'] and self.todrive['share']:
|
|
Act.Set(Act.SHARE)
|
|
for share in self.todrive['share']:
|
|
if share['emailAddress'] != user:
|
|
try:
|
|
callGAPI(drive.permissions(), 'create',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS,
|
|
fileId=spreadsheetId, sendNotificationEmail=False, body=share, fields='', supportsAllDrives=True)
|
|
entityActionPerformed([Ent.USER, user, Ent.SPREADSHEET, title,
|
|
Ent.TARGET_USER, share['emailAddress'], Ent.ROLE, share['role']])
|
|
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
|
|
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
|
|
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
|
|
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
|
|
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
|
|
GAPI.ownerOnTeamDriveItemNotSupported,
|
|
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
|
|
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
|
|
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
|
|
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
|
|
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility,
|
|
GAPI.invalidSharingRequest, GAPI.fileNeverWritable, GAPI.abusiveContentRestriction) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, title,
|
|
Ent.TARGET_USER, share['emailAddress'], Ent.ROLE, share['role']],
|
|
str(e))
|
|
if ((result['mimeType'] == MIMETYPE_GA_SPREADSHEET) and
|
|
(self.todrive['sheetEntity'] or self.todrive['locale'] or self.todrive['timeZone'] or
|
|
self.todrive['sheettitle'] or self.todrive['cellwrap'] or self.todrive['cellnumberformat'])):
|
|
if not GC.Values[GC.TODRIVE_CLIENTACCESS]:
|
|
_, sheet = buildGAPIServiceObject(API.SHEETSTD, user)
|
|
if sheet is None:
|
|
return
|
|
else:
|
|
sheet = buildGAPIObject(API.SHEETS)
|
|
try:
|
|
body = {'requests': []}
|
|
if self.todrive['sheetEntity'] or self.todrive['sheettitle'] or self.todrive['cellwrap']:
|
|
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=spreadsheetId, fields='sheets/properties')
|
|
spreadsheet['sheets'][0]['properties']['title'] = sheetTitle
|
|
body['requests'].append({'updateSheetProperties':
|
|
{'properties': spreadsheet['sheets'][0]['properties'], 'fields': 'title'}})
|
|
if self.todrive['cellwrap']:
|
|
body['requests'].append({'repeatCell': {'range': {'sheetId': spreadsheet['sheets'][0]['properties']['sheetId']},
|
|
'fields': 'userEnteredFormat.wrapStrategy',
|
|
'cell': {'userEnteredFormat': {'wrapStrategy': self.todrive['cellwrap']}}}})
|
|
if self.todrive['locale']:
|
|
body['requests'].append({'updateSpreadsheetProperties':
|
|
{'properties': {'locale': self.todrive['locale']}, 'fields': 'locale'}})
|
|
if self.todrive['timeZone']:
|
|
body['requests'].append({'updateSpreadsheetProperties':
|
|
{'properties': {'timeZone': self.todrive['timeZone']}, 'fields': 'timeZone'}})
|
|
if body['requests']:
|
|
callGAPI(sheet.spreadsheets(), 'batchUpdate',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=spreadsheetId, body=body)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
|
|
todriveCSVErrorExit([Ent.USER, user, Ent.SPREADSHEET, title], str(e))
|
|
Act.Set(action)
|
|
file_url = result['webViewLink']
|
|
msg_txt = f'{Msg.DATA_UPLOADED_TO_DRIVE_FILE}:\n{file_url}'
|
|
if not self.todrive['returnidonly']:
|
|
printKeyValueList([msg_txt])
|
|
else:
|
|
if self.todrive['fileId']:
|
|
writeStdout(f'{self.todrive["fileId"]}\n')
|
|
else:
|
|
writeStdout(f'{spreadsheetId}\n')
|
|
if not self.todrive['subject']:
|
|
subject = title
|
|
else:
|
|
subject = self.todrive['subject'].replace('#file#', title).replace('#sheet#', sheetTitle)
|
|
if not self.todrive['noemail']:
|
|
send_email(subject, msg_txt, user, clientAccess=GC.Values[GC.TODRIVE_CLIENTACCESS], msgFrom=self.todrive['from'])
|
|
if self.todrive['notify']:
|
|
for recipient in self.todrive['share']+self.todrive['alert']:
|
|
if recipient['emailAddress'] != user:
|
|
send_email(subject, msg_txt, recipient['emailAddress'], clientAccess=GC.Values[GC.TODRIVE_CLIENTACCESS], msgFrom=self.todrive['from'])
|
|
if not self.todrive['nobrowser']:
|
|
webbrowser.open(file_url)
|
|
except (GAPI.forbidden, GAPI.insufficientPermissions):
|
|
printWarningMessage(INSUFFICIENT_PERMISSIONS_RC, Msg.INSUFFICIENT_PERMISSIONS_TO_PERFORM_TASK)
|
|
except (GAPI.fileNotFound, GAPI.unknownError, GAPI.internalError, GAPI.storageQuotaExceeded) as e:
|
|
if not self.todrive['fileId']:
|
|
entityActionFailedWarning([Ent.DRIVE_FOLDER, self.todrive['parentId']], str(e))
|
|
else:
|
|
entityActionFailedWarning([Ent.DRIVE_FILE, self.todrive['fileId']], str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), 0, 0)
|
|
else:
|
|
closeFile(csvFile)
|
|
|
|
if GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] is not None:
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE].put((GM.REDIRECT_QUEUE_NAME, list_type))
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE].put((GM.REDIRECT_QUEUE_TODRIVE, self.todrive))
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE].put((GM.REDIRECT_QUEUE_CSVPF,
|
|
(self.titlesList, self.sortTitlesList, self.indexedTitles,
|
|
self.formatJSON, self.JSONtitlesList,
|
|
self.columnDelimiter, self.noEscapeChar, self.quoteChar,
|
|
self.sortHeaders, self.timestampColumn,
|
|
self.mapDrive3Titles,
|
|
self.fixPaths,
|
|
self.mapNodataFields,
|
|
self.nodataFields,
|
|
self.driveListFields,
|
|
self.driveSubfieldsChoiceMap,
|
|
self.oneItemPerRow,
|
|
self.showPermissionsLast,
|
|
self.zeroBlankMimeTypeCounts)))
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE].put((GM.REDIRECT_QUEUE_DATA, self.rows))
|
|
return
|
|
if self.zeroBlankMimeTypeCounts:
|
|
self.ZeroBlankMimeTypeCounts()
|
|
if self.rowFilter or self.rowDropFilter:
|
|
self.CheckOutputRowFilterHeaders()
|
|
if self.headerFilter or self.headerDropFilter:
|
|
if not self.formatJSON:
|
|
self.FilterHeaders()
|
|
else:
|
|
self.FilterJSONHeaders()
|
|
extrasaction = 'ignore'
|
|
else:
|
|
extrasaction = 'raise'
|
|
if not self.formatJSON:
|
|
if not self.headerForce:
|
|
self.SortTitles()
|
|
self.SortIndexedTitles(self.titlesList)
|
|
if self.fixPaths:
|
|
self.FixPathsTitles(self.titlesList)
|
|
if self.showPermissionsLast:
|
|
self.MovePermsToEnd()
|
|
if not self.rows and self.nodataFields is not None:
|
|
self.FixNodataTitles()
|
|
if self.mapDrive3Titles:
|
|
self. MapDrive3TitlesToDrive2()
|
|
else:
|
|
self.titlesList = self.headerForce
|
|
if self.timestampColumn:
|
|
self.AddTitle(self.timestampColumn)
|
|
if self.headerOrder:
|
|
self.titlesList = self.orderHeaders(self.titlesList)
|
|
titlesList = self.titlesList
|
|
else:
|
|
if not self.headerForce:
|
|
if self.fixPaths:
|
|
self.FixPathsTitles(self.JSONtitlesList)
|
|
if not self.rows and self.nodataFields is not None:
|
|
self.FixNodataTitles()
|
|
else:
|
|
self.JSONtitlesList = self.headerForce
|
|
if self.timestampColumn:
|
|
for i, v in enumerate(self.JSONtitlesList):
|
|
if v.startswith('JSON'):
|
|
self.JSONtitlesList.insert(i, self.timestampColumn)
|
|
self.JSONtitlesSet.add(self.timestampColumn)
|
|
break
|
|
else:
|
|
self.AddJSONTitle(self.timestampColumn)
|
|
if self.headerOrder:
|
|
self.JSONtitlesList = self.orderHeaders(self.JSONtitlesList)
|
|
titlesList = self.JSONtitlesList
|
|
normalizeSortHeaders()
|
|
if self.outputTranspose:
|
|
newRows = []
|
|
pivotKey = titlesList[0]
|
|
newTitlesList = [pivotKey]
|
|
newTitlesSet = set(newTitlesList)
|
|
for title in titlesList[1:]:
|
|
newRow = {pivotKey: title}
|
|
for row in self.rows:
|
|
pivotValue = row[pivotKey]
|
|
if pivotValue not in newTitlesSet:
|
|
newTitlesSet.add(pivotValue)
|
|
newTitlesList.append(pivotValue)
|
|
newRow[pivotValue] = row.get(title)
|
|
newRows.append(newRow)
|
|
titlesList = newTitlesList
|
|
self.rows = newRows
|
|
if (not self.todrive) or self.todrive['localcopy']:
|
|
if GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] == '-':
|
|
if GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]:
|
|
writeCSVToStdout()
|
|
else:
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_NAME] = GM.Globals[GM.STDOUT][GM.REDIRECT_NAME]
|
|
writeCSVToFile()
|
|
else:
|
|
writeCSVToFile()
|
|
if self.todrive:
|
|
writeCSVToDrive()
|
|
if GM.Globals[GM.CSVFILE][GM.REDIRECT_MODE] == DEFAULT_FILE_APPEND_MODE:
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER] = False
|
|
|
|
def writeEntityNoHeaderCSVFile(entityType, entityList):
|
|
csvPF = CSVPrintFile(entityType)
|
|
_, _, entityList = getEntityArgument(entityList)
|
|
if entityType == Ent.USER:
|
|
for entity in entityList:
|
|
csvPF.WriteRowNoFilter({entityType: normalizeEmailAddressOrUID(entity)})
|
|
else:
|
|
for entity in entityList:
|
|
csvPF.WriteRowNoFilter({entityType: entity})
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_WRITE_HEADER] = False
|
|
csvPF.writeCSVfile(Ent.Plural(entityType))
|
|
|
|
def getTodriveOnly(csvPF):
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
DEFAULT_SKIP_OBJECTS = {'kind', 'etag', 'etags', '@type'}
|
|
|
|
# Clean a JSON object
|
|
def cleanJSON(topStructure, listLimit=None, skipObjects=None, timeObjects=None):
|
|
def _clean(structure, key, subSkipObjects):
|
|
if not isinstance(structure, (dict, list)):
|
|
if key not in timeObjects:
|
|
if isinstance(structure, str) and GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]:
|
|
return escapeCRsNLs(structure)
|
|
return structure
|
|
if isinstance(structure, str) and not structure.isdigit():
|
|
return formatLocalTime(structure)
|
|
return formatLocalTimestamp(structure)
|
|
if isinstance(structure, list):
|
|
listLen = len(structure)
|
|
listLen = min(listLen, listLimit or listLen)
|
|
return [_clean(v, '', DEFAULT_SKIP_OBJECTS) for v in structure[0:listLen]]
|
|
return {k: _clean(v, k, DEFAULT_SKIP_OBJECTS) for k, v in sorted(iter(structure.items())) if k not in subSkipObjects}
|
|
|
|
timeObjects = timeObjects or set()
|
|
return _clean(topStructure, '', DEFAULT_SKIP_OBJECTS.union(skipObjects or set()))
|
|
|
|
# Flatten a JSON object
|
|
def flattenJSON(topStructure, flattened=None,
|
|
listLimit=None, skipObjects=None, timeObjects=None, noLenObjects=None, simpleLists=None, delimiter=None):
|
|
def _flatten(structure, key, path):
|
|
if not isinstance(structure, (dict, list)):
|
|
if key not in timeObjects:
|
|
if isinstance(structure, str):
|
|
if GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL] and (structure.find('\n') >= 0 or structure.find('\r') >= 0):
|
|
flattened[path] = escapeCRsNLs(structure)
|
|
else:
|
|
flattened[path] = structure
|
|
else:
|
|
flattened[path] = structure
|
|
else:
|
|
if isinstance(structure, str) and not structure.isdigit():
|
|
flattened[path] = formatLocalTime(structure)
|
|
else:
|
|
flattened[path] = formatLocalTimestamp(structure)
|
|
elif isinstance(structure, list):
|
|
listLen = len(structure)
|
|
listLen = min(listLen, listLimit or listLen)
|
|
if key in simpleLists:
|
|
flattened[path] = delimiter.join(structure[:listLen])
|
|
else:
|
|
if key not in noLenObjects:
|
|
flattened[path] = listLen
|
|
for i in range(listLen):
|
|
_flatten(structure[i], '', f'{path}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}')
|
|
else:
|
|
if structure:
|
|
for k, v in sorted(iter(structure.items())):
|
|
if k not in DEFAULT_SKIP_OBJECTS:
|
|
_flatten(v, k, f'{path}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}')
|
|
else:
|
|
flattened[path] = ''
|
|
|
|
flattened = flattened or {}
|
|
allSkipObjects = DEFAULT_SKIP_OBJECTS.union(skipObjects or set())
|
|
timeObjects = timeObjects or set()
|
|
noLenObjects = noLenObjects or set()
|
|
simpleLists = simpleLists or set()
|
|
for k, v in sorted(iter(topStructure.items())):
|
|
if k not in allSkipObjects:
|
|
_flatten(v, k, k)
|
|
return flattened
|
|
|
|
# Show a json object
|
|
def showJSON(showName, showValue, skipObjects=None, timeObjects=None,
|
|
simpleLists=None, dictObjectsKey=None, sortDictKeys=True):
|
|
def _show(objectName, objectValue, subObjectKey, level, subSkipObjects):
|
|
if objectName in subSkipObjects:
|
|
return
|
|
if objectName is not None:
|
|
printJSONKey(objectName)
|
|
subObjectKey = dictObjectsKey.get(objectName)
|
|
if isinstance(objectValue, list):
|
|
if objectName in simpleLists:
|
|
printJSONValue(' '.join(objectValue))
|
|
return
|
|
if len(objectValue) == 1 and isinstance(objectValue[0], (str, bool, float, int)):
|
|
if objectName is not None:
|
|
printJSONValue(objectValue[0])
|
|
else:
|
|
printKeyValueList([objectValue[0]])
|
|
return
|
|
if objectName is not None:
|
|
printBlankLine()
|
|
Ind.Increment()
|
|
for subValue in objectValue:
|
|
if isinstance(subValue, (str, bool, float, int)):
|
|
printKeyValueList([subValue])
|
|
else:
|
|
_show(None, subValue, subObjectKey, level+1, DEFAULT_SKIP_OBJECTS)
|
|
if objectName is not None:
|
|
Ind.Decrement()
|
|
elif isinstance(objectValue, dict):
|
|
indentAfterFirst = unindentAfterLast = False
|
|
if objectName is not None:
|
|
printBlankLine()
|
|
Ind.Increment()
|
|
elif level > 0:
|
|
indentAfterFirst = unindentAfterLast = True
|
|
subObjects = sorted(objectValue) if sortDictKeys else objectValue.keys()
|
|
if subObjectKey and (subObjectKey in subObjects):
|
|
subObjects.remove(subObjectKey)
|
|
subObjects.insert(0, subObjectKey)
|
|
subObjectKey = None
|
|
for subObject in subObjects:
|
|
if subObject not in subSkipObjects:
|
|
_show(subObject, objectValue[subObject], subObjectKey, level+1, DEFAULT_SKIP_OBJECTS)
|
|
if indentAfterFirst:
|
|
Ind.Increment()
|
|
indentAfterFirst = False
|
|
if objectName is not None or ((not indentAfterFirst) and unindentAfterLast):
|
|
Ind.Decrement()
|
|
else:
|
|
if objectName not in timeObjects:
|
|
if isinstance(objectValue, str) and (objectValue.find('\n') >= 0 or objectValue.find('\r') >= 0):
|
|
if GC.Values[GC.SHOW_CONVERT_CR_NL]:
|
|
printJSONValue(escapeCRsNLs(objectValue))
|
|
else:
|
|
printBlankLine()
|
|
Ind.Increment()
|
|
printKeyValueList([Ind.MultiLineText(objectValue)])
|
|
Ind.Decrement()
|
|
else:
|
|
printJSONValue(objectValue if objectValue is not None else '')
|
|
else:
|
|
if isinstance(objectValue, str) and not objectValue.isdigit():
|
|
printJSONValue(formatLocalTime(objectValue))
|
|
else:
|
|
printJSONValue(formatLocalTimestamp(objectValue))
|
|
|
|
timeObjects = timeObjects or set()
|
|
simpleLists = simpleLists or set()
|
|
dictObjectsKey = dictObjectsKey or {}
|
|
_show(showName, showValue, None, 0, DEFAULT_SKIP_OBJECTS.union(skipObjects or set()))
|
|
|
|
class FormatJSONQuoteChar():
|
|
|
|
def __init__(self, csvPF=None, formatJSONOnly=False):
|
|
self.SetCsvPF(csvPF)
|
|
self.SetFormatJSON(False)
|
|
self.SetQuoteChar(GM.Globals.get(GM.CSV_OUTPUT_QUOTE_CHAR, GC.Values.get(GC.CSV_OUTPUT_QUOTE_CHAR, '"')))
|
|
if not formatJSONOnly:
|
|
return
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'formatjson':
|
|
self.SetFormatJSON(True)
|
|
return
|
|
unknownArgumentExit()
|
|
|
|
def SetCsvPF(self, csvPF):
|
|
self.csvPF = csvPF
|
|
|
|
def SetFormatJSON(self, formatJSON):
|
|
self.formatJSON = formatJSON
|
|
if self.csvPF:
|
|
self.csvPF.SetFormatJSON(formatJSON)
|
|
|
|
def GetFormatJSON(self, myarg):
|
|
if myarg == 'formatjson':
|
|
self.SetFormatJSON(True)
|
|
return
|
|
unknownArgumentExit()
|
|
|
|
def SetQuoteChar(self, quoteChar):
|
|
self.quoteChar = quoteChar
|
|
if self.csvPF:
|
|
self.csvPF.SetQuoteChar(quoteChar)
|
|
|
|
def GetQuoteChar(self, myarg):
|
|
if self.csvPF and myarg == 'quotechar':
|
|
self.SetQuoteChar(getCharacter())
|
|
return
|
|
unknownArgumentExit()
|
|
|
|
def GetFormatJSONQuoteChar(self, myarg, addTitle=False, noExit=False):
|
|
if myarg == 'formatjson':
|
|
self.SetFormatJSON(True)
|
|
if self.csvPF and addTitle:
|
|
self.csvPF.AddJSONTitles('JSON')
|
|
return True
|
|
if self.csvPF and myarg == 'quotechar':
|
|
self.SetQuoteChar(getCharacter())
|
|
return True
|
|
if noExit:
|
|
return False
|
|
unknownArgumentExit()
|
|
|
|
# Batch processing request_id fields
|
|
RI_ENTITY = 0
|
|
RI_I = 1
|
|
RI_COUNT = 2
|
|
RI_J = 3
|
|
RI_JCOUNT = 4
|
|
RI_ITEM = 5
|
|
RI_ROLE = 6
|
|
RI_OPTION = 7
|
|
|
|
def batchRequestID(entityName, i, count, j, jcount, item, role=None, option=None):
|
|
if role is None and option is None:
|
|
return f'{entityName}\n{i}\n{count}\n{j}\n{jcount}\n{item}'
|
|
return f'{entityName}\n{i}\n{count}\n{j}\n{jcount}\n{item}\n{role}\n{option}'
|
|
|
|
TIME_OFFSET_UNITS = [('day', SECONDS_PER_DAY), ('hour', SECONDS_PER_HOUR), ('minute', SECONDS_PER_MINUTE), ('second', 1)]
|
|
|
|
def getLocalGoogleTimeOffset(testLocation=GOOGLE_TIMECHECK_LOCATION):
|
|
# If local time is well off, it breaks https because the server certificate will be seen as too old or new and thus invalid; http doesn't have that issue.
|
|
# Try with http first, if time is close (<MAX_LOCAL_GOOGLE_TIME_OFFSET seconds), retry with https as it should be OK
|
|
httpObj = getHttpObj()
|
|
for prot in ['http', 'https']:
|
|
try:
|
|
headerData = httpObj.request(f'{prot}://'+testLocation, 'HEAD')
|
|
googleUTC = datetime.datetime.strptime(headerData[0]['date'], '%a, %d %b %Y %H:%M:%S %Z').replace(tzinfo=iso8601.UTC)
|
|
except (httplib2.HttpLib2Error, RuntimeError) as e:
|
|
handleServerError(e)
|
|
except httplib2.socks.HTTPError as e:
|
|
# If user has specified an HTTPS proxy, the http request will probably fail as httplib2
|
|
# turns a GET into a CONNECT which is not valid for an http address
|
|
if prot == 'http':
|
|
continue
|
|
handleServerError(e)
|
|
except (ValueError, KeyError):
|
|
if prot == 'http':
|
|
continue
|
|
systemErrorExit(NETWORK_ERROR_RC, Msg.INVALID_HTTP_HEADER.format(str(headerData)))
|
|
offset = remainder = int(abs((datetime.datetime.now(iso8601.UTC)-googleUTC).total_seconds()))
|
|
if offset < MAX_LOCAL_GOOGLE_TIME_OFFSET and prot == 'http':
|
|
continue
|
|
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(Msg.LESS_THAN_1_SECOND)
|
|
nicetime = ', '.join(timeoff)
|
|
return (offset, nicetime)
|
|
|
|
def _getServerTLSUsed(location):
|
|
url = 'https://'+location
|
|
_, netloc, _, _, _, _ = urlparse(url)
|
|
conn = 'https:'+netloc
|
|
httpObj = getHttpObj()
|
|
triesLimit = 5
|
|
for n in range(1, triesLimit+1):
|
|
try:
|
|
httpObj.request(url, headers={'user-agent': GAM_USER_AGENT})
|
|
cipher_name, tls_ver, _ = httpObj.connections[conn].sock.cipher()
|
|
return tls_ver, cipher_name
|
|
except (httplib2.HttpLib2Error, RuntimeError) as e:
|
|
if n != triesLimit:
|
|
httpObj.connections = {}
|
|
waitOnFailure(n, triesLimit, NETWORK_ERROR_RC, str(e))
|
|
continue
|
|
handleServerError(e)
|
|
|
|
MACOS_CODENAMES = {
|
|
10: {
|
|
6: 'Snow Leopard',
|
|
7: 'Lion',
|
|
8: 'Mountain Lion',
|
|
9: 'Mavericks',
|
|
10: 'Yosemite',
|
|
11: 'El Capitan',
|
|
12: 'Sierra',
|
|
13: 'High Sierra',
|
|
14: 'Mojave',
|
|
15: 'Catalina',
|
|
16: 'Big Sur'
|
|
},
|
|
11: 'Big Sur',
|
|
12: 'Monterey',
|
|
13: 'Ventura',
|
|
14: 'Sonoma',
|
|
15: 'Sequoia',
|
|
}
|
|
|
|
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]
|
|
major_ver = int(mac_ver.split('.')[0]) # macver 10.14.6 == major_ver 10
|
|
minor_ver = int(mac_ver.split('.')[1]) # macver 10.14.6 == minor_ver 14
|
|
if major_ver == 10:
|
|
codename = MACOS_CODENAMES[major_ver].get(minor_ver, '')
|
|
else:
|
|
codename = MACOS_CODENAMES.get(major_ver, '')
|
|
pltfrm = ' '.join([codename, mac_ver])
|
|
else:
|
|
pltfrm = platform.platform()
|
|
return f'{myos} {pltfrm}'
|
|
|
|
# gam checkconnection
|
|
def doCheckConnection():
|
|
hosts = ['api.github.com',
|
|
'raw.githubusercontent.com',
|
|
'accounts.google.com',
|
|
'workspace.google.com',
|
|
'oauth2.googleapis.com',
|
|
'www.googleapis.com']
|
|
fix_hosts = {'calendar-json.googleapis.com': 'www.googleapis.com',
|
|
'storage-api.googleapis.com': 'storage.googleapis.com'}
|
|
api_hosts = ['apps-apis.google.com',
|
|
'sites.google.com',
|
|
'versionhistory.googleapis.com',
|
|
'www.google.com']
|
|
for host in API.PROJECT_APIS:
|
|
host = fix_hosts.get(host, host)
|
|
if host not in api_hosts and host not in hosts:
|
|
api_hosts.append(host)
|
|
hosts.extend(sorted(api_hosts))
|
|
host_count = len(hosts)
|
|
httpObj = getHttpObj(timeout=30)
|
|
httpObj.follow_redirects = False
|
|
headers = {'user-agent': GAM_USER_AGENT}
|
|
okay = createGreenText('OK')
|
|
not_okay = createRedText('ERROR')
|
|
try_count = 0
|
|
success_count = 0
|
|
for host in hosts:
|
|
try_count += 1
|
|
dns_err = None
|
|
ip = 'unknown'
|
|
try:
|
|
ip = socket.getaddrinfo(host, None)[0][-1][0] # works with ipv6
|
|
except socket.gaierror as e:
|
|
dns_err = f'{not_okay}\n DNS failure: {str(e)}\n'
|
|
except Exception as e:
|
|
dns_err = f'{not_okay}\n Unknown DNS failure: {str(e)}\n'
|
|
check_line = f'Checking {host} ({ip}) ({try_count}/{host_count})...'
|
|
writeStdout(f'{check_line:<100}')
|
|
flushStdout()
|
|
if dns_err:
|
|
writeStdout(dns_err)
|
|
continue
|
|
gen_firewall = 'You probably have security software or a firewall on your machine or network that is preventing GAM from making Internet connections. Check your network configuration or try running GAM on a hotspot or home network to see if the problem exists only on your organization\'s network.'
|
|
try:
|
|
if host.startswith('http'):
|
|
url = host
|
|
else:
|
|
url = f'https://{host}:443/'
|
|
httpObj.request(url, 'HEAD', headers=headers)
|
|
success_count += 1
|
|
writeStdout(f'{okay}\n')
|
|
except ConnectionRefusedError:
|
|
writeStdout(f'{not_okay}\n Connection refused. {gen_firewall}\n')
|
|
except ConnectionResetError:
|
|
writeStdout(f'{not_okay}\n Connection reset by peer. {gen_firewall}\n')
|
|
except httplib2.error.ServerNotFoundError:
|
|
writeStdout(f'{not_okay}\n Failed to find server. Your DNS is probably misconfigured.\n')
|
|
except ssl.SSLError as e:
|
|
if e.reason == 'SSLV3_ALERT_HANDSHAKE_FAILURE':
|
|
writeStdout(f'{not_okay}\n GAM expects to connect with TLS 1.3 or newer and that failed. If your firewall / proxy server is not compatible with TLS 1.3 then you can tell GAM to allow TLS 1.2 by setting tls_min_version = TLSv1.2 in gam.cfg.\n')
|
|
elif e.reason == 'CERTIFICATE_VERIFY_FAILED':
|
|
writeStdout(f'{not_okay}\n Certificate verification failed. If you are behind a firewall / proxy server that does TLS / SSL inspection you may need to point GAM at your certificate authority file by setting cacerts_pem = /path/to/your/certauth.pem in gam.cfg.\n')
|
|
elif e.strerror and e.strerror.startswith('TLS/SSL connection has been closed\n'):
|
|
writeStdout(f'{not_okay}\n TLS connection was closed. {gen_firewall}\n')
|
|
else:
|
|
writeStdout(f'{not_okay}\n {str(e)}\n')
|
|
except TimeoutError:
|
|
writeStdout(f'{not_okay}\n Timed out trying to connect to host\n')
|
|
except Exception as e:
|
|
writeStdout(f'{not_okay}\n {str(e)}\n')
|
|
if success_count == host_count:
|
|
writeStdout(createGreenText('All hosts passed!\n'))
|
|
else:
|
|
systemErrorExit(3, createYellowText('Some hosts failed to connect! Please follow the recommendations for those hosts to correct any issues and try again.'))
|
|
|
|
# gam comment
|
|
def doComment():
|
|
writeStdout(Cmd.QuotedArgumentList(Cmd.Remaining())+'\n')
|
|
|
|
# gam version [check|checkrc|simple|extended] [timeoffset] [nooffseterror] [location <HostName>]
|
|
def doVersion(checkForArgs=True):
|
|
forceCheck = 0
|
|
extended = noOffsetError = timeOffset = simple = False
|
|
testLocation = GOOGLE_TIMECHECK_LOCATION
|
|
if checkForArgs:
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'check':
|
|
forceCheck = 1
|
|
elif myarg == 'checkrc':
|
|
forceCheck = -1
|
|
elif myarg == 'simple':
|
|
simple = True
|
|
elif myarg == 'extended':
|
|
extended = timeOffset = True
|
|
elif myarg == 'timeoffset':
|
|
timeOffset = True
|
|
elif myarg == 'nooffseterror':
|
|
noOffsetError = True
|
|
elif myarg == 'location':
|
|
testLocation = getString(Cmd.OB_HOST_NAME)
|
|
else:
|
|
unknownArgumentExit()
|
|
if simple:
|
|
writeStdout(__version__)
|
|
return
|
|
writeStdout((f'{GAM} {__version__} - {GAM_URL} - {GM.Globals[GM.GAM_TYPE]}\n'
|
|
f'{__author__}\n'
|
|
f'Python {sys.version_info[0]}.{sys.version_info[1]}.{sys.version_info[2]} {struct.calcsize("P")*8}-bit {sys.version_info[3]}\n'
|
|
f'{getOSPlatform()} {platform.machine()}\n'
|
|
f'Path: {GM.Globals[GM.GAM_PATH]}\n'
|
|
f'{Ent.Singular(Ent.CONFIG_FILE)}: {GM.Globals[GM.GAM_CFG_FILE]}, {Ent.Singular(Ent.SECTION)}: {GM.Globals[GM.GAM_CFG_SECTION_NAME]}, '
|
|
f'{GC.CUSTOMER_ID}: {GC.Values[GC.CUSTOMER_ID]}, {GC.DOMAIN}: {GC.Values[GC.DOMAIN]}\n'
|
|
f'Time: {ISOformatTimeStamp(todaysTime())}\n'
|
|
))
|
|
if sys.platform.startswith('win') and str(struct.calcsize('P')*8).find('32') != -1 and platform.machine().find('64') != -1:
|
|
printKeyValueList([Msg.UPDATE_GAM_TO_64BIT])
|
|
if timeOffset:
|
|
offsetSeconds, offsetFormatted = getLocalGoogleTimeOffset(testLocation)
|
|
printKeyValueList([Msg.YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE.format(testLocation, offsetFormatted)])
|
|
if offsetSeconds > MAX_LOCAL_GOOGLE_TIME_OFFSET:
|
|
if not noOffsetError:
|
|
systemErrorExit(NETWORK_ERROR_RC, Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
|
|
stderrWarningMsg(Msg.PLEASE_CORRECT_YOUR_SYSTEM_TIME)
|
|
if forceCheck:
|
|
doGAMCheckForUpdates(forceCheck)
|
|
if extended:
|
|
printKeyValueList([ssl.OPENSSL_VERSION])
|
|
tls_ver, cipher_name = _getServerTLSUsed(testLocation)
|
|
for lib in glverlibs.GAM_VER_LIBS:
|
|
try:
|
|
writeStdout(f'{lib} {lib_version(lib)}\n')
|
|
except:
|
|
pass
|
|
printKeyValueList([f'{testLocation} connects using {tls_ver} {cipher_name}'])
|
|
|
|
# gam help
|
|
def doUsage():
|
|
printBlankLine()
|
|
doVersion(checkForArgs=False)
|
|
writeStdout(Msg.HELP_SYNTAX.format(os.path.join(GM.Globals[GM.GAM_PATH], FN_GAMCOMMANDS_TXT)))
|
|
writeStdout(Msg.HELP_WIKI.format(GAM_WIKI))
|
|
|
|
class NullHandler(logging.Handler):
|
|
def emit(self, record):
|
|
pass
|
|
|
|
def initializeLogging():
|
|
nh = NullHandler()
|
|
logging.getLogger().addHandler(nh)
|
|
|
|
def saveNonPickleableValues():
|
|
savedValues = {GM.STDOUT: {}, GM.STDERR: {}, GM.SAVED_STDOUT: None,
|
|
GM.CMDLOG_HANDLER: None, GM.CMDLOG_LOGGER: None}
|
|
savedValues[GM.SAVED_STDOUT] = GM.Globals[GM.SAVED_STDOUT]
|
|
GM.Globals[GM.SAVED_STDOUT] = None
|
|
savedValues[GM.STDOUT][GM.REDIRECT_FD] = GM.Globals[GM.STDOUT].get(GM.REDIRECT_FD, None)
|
|
GM.Globals[GM.STDOUT].pop(GM.REDIRECT_FD, None)
|
|
savedValues[GM.STDERR][GM.REDIRECT_FD] = GM.Globals[GM.STDERR].get(GM.REDIRECT_FD, None)
|
|
GM.Globals[GM.STDERR].pop(GM.REDIRECT_FD, None)
|
|
savedValues[GM.STDOUT][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, None)
|
|
GM.Globals[GM.STDOUT].pop(GM.REDIRECT_MULTI_FD, None)
|
|
savedValues[GM.STDERR][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, None)
|
|
GM.Globals[GM.STDERR].pop(GM.REDIRECT_MULTI_FD, None)
|
|
savedValues[GM.CMDLOG_HANDLER] = GM.Globals[GM.CMDLOG_HANDLER]
|
|
GM.Globals[GM.CMDLOG_HANDLER] = None
|
|
savedValues[GM.CMDLOG_LOGGER] = GM.Globals[GM.CMDLOG_LOGGER]
|
|
GM.Globals[GM.CMDLOG_LOGGER] = None
|
|
return savedValues
|
|
|
|
def restoreNonPickleableValues(savedValues):
|
|
GM.Globals[GM.SAVED_STDOUT] = savedValues[GM.SAVED_STDOUT]
|
|
GM.Globals[GM.STDOUT][GM.REDIRECT_FD] = savedValues[GM.STDOUT][GM.REDIRECT_FD]
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_FD] = savedValues[GM.STDERR][GM.REDIRECT_FD]
|
|
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD] = savedValues[GM.STDOUT][GM.REDIRECT_MULTI_FD]
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = savedValues[GM.STDERR][GM.REDIRECT_MULTI_FD]
|
|
GM.Globals[GM.CMDLOG_HANDLER] = savedValues[GM.CMDLOG_HANDLER]
|
|
GM.Globals[GM.CMDLOG_LOGGER] = savedValues[GM.CMDLOG_LOGGER]
|
|
|
|
def CSVFileQueueHandler(mpQueue, mpQueueStdout, mpQueueStderr, csvPF, datetimeNow, tzinfo, output_timeformat):
|
|
global Cmd
|
|
|
|
def reopenSTDFile(stdtype):
|
|
if GM.Globals[stdtype][GM.REDIRECT_NAME] == 'null':
|
|
GM.Globals[stdtype][GM.REDIRECT_FD] = open(os.devnull, GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=UTF8)
|
|
elif GM.Globals[stdtype][GM.REDIRECT_NAME] == '-':
|
|
GM.Globals[stdtype][GM.REDIRECT_FD] = os.fdopen(os.dup([sys.stderr.fileno(), sys.stdout.fileno()][stdtype == GM.STDOUT]),
|
|
GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=GM.Globals[GM.SYS_ENCODING])
|
|
elif stdtype == GM.STDERR and GM.Globals[stdtype][GM.REDIRECT_NAME] == 'stdout':
|
|
GM.Globals[stdtype][GM.REDIRECT_FD] = GM.Globals[GM.STDOUT][GM.REDIRECT_FD]
|
|
else:
|
|
GM.Globals[stdtype][GM.REDIRECT_FD] = openFile(GM.Globals[stdtype][GM.REDIRECT_NAME], GM.Globals[stdtype][GM.REDIRECT_MODE])
|
|
if stdtype == GM.STDERR and GM.Globals[stdtype][GM.REDIRECT_NAME] == 'stdout':
|
|
GM.Globals[stdtype][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]
|
|
else:
|
|
GM.Globals[stdtype][GM.REDIRECT_MULTI_FD] = GM.Globals[stdtype][GM.REDIRECT_FD] if not GM.Globals[stdtype][GM.REDIRECT_MULTIPROCESS] else StringIOobject()
|
|
|
|
GM.Globals[GM.DATETIME_NOW] = datetimeNow
|
|
GC.Values[GC.TIMEZONE] = tzinfo
|
|
GC.Values[GC.OUTPUT_TIMEFORMAT] = output_timeformat
|
|
# if sys.platform.startswith('win'):
|
|
# signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
if multiprocessing.get_start_method() == 'spawn':
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
Cmd = glclargs.GamCLArgs()
|
|
else:
|
|
csvPF.SetColumnDelimiter(GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER])
|
|
csvPF.SetNoEscapeChar(GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR])
|
|
csvPF.SetQuoteChar(GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR])
|
|
csvPF.SetSortHeaders(GC.Values[GC.CSV_OUTPUT_SORT_HEADERS])
|
|
csvPF.SetTimestampColumn(GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN])
|
|
csvPF.SetHeaderFilter(GC.Values[GC.CSV_OUTPUT_HEADER_FILTER])
|
|
csvPF.SetHeaderDropFilter(GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER])
|
|
csvPF.SetRowFilter(GC.Values[GC.CSV_OUTPUT_ROW_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
|
|
csvPF.SetRowDropFilter(GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE])
|
|
csvPF.SetRowLimit(GC.Values[GC.CSV_OUTPUT_ROW_LIMIT])
|
|
list_type = 'CSV'
|
|
while True:
|
|
dataType, dataItem = mpQueue.get()
|
|
if dataType == GM.REDIRECT_QUEUE_NAME:
|
|
list_type = dataItem
|
|
elif dataType == GM.REDIRECT_QUEUE_TODRIVE:
|
|
csvPF.todrive = dataItem
|
|
elif dataType == GM.REDIRECT_QUEUE_CSVPF:
|
|
csvPF.AddTitles(dataItem[0])
|
|
csvPF.SetSortTitles(dataItem[1])
|
|
csvPF.SetIndexedTitles(dataItem[2])
|
|
csvPF.SetFormatJSON(dataItem[3])
|
|
csvPF.AddJSONTitles(dataItem[4])
|
|
csvPF.SetColumnDelimiter(dataItem[5])
|
|
csvPF.SetNoEscapeChar(dataItem[6])
|
|
csvPF.SetQuoteChar(dataItem[7])
|
|
csvPF.SetSortHeaders(dataItem[8])
|
|
csvPF.SetTimestampColumn(dataItem[9])
|
|
csvPF.SetMapDrive3Titles(dataItem[10])
|
|
csvPF.SetFixPaths(dataItem[11])
|
|
csvPF.SetNodataFields(dataItem[12], dataItem[13], dataItem[14], dataItem[15], dataItem[16])
|
|
csvPF.SetShowPermissionsLast(dataItem[17])
|
|
csvPF.SetZeroBlankMimeTypeCounts(dataItem[18])
|
|
elif dataType == GM.REDIRECT_QUEUE_DATA:
|
|
csvPF.rows.extend(dataItem)
|
|
elif dataType == GM.REDIRECT_QUEUE_ARGS:
|
|
Cmd.InitializeArguments(dataItem)
|
|
elif dataType == GM.REDIRECT_QUEUE_GLOBALS:
|
|
GM.Globals = dataItem
|
|
if multiprocessing.get_start_method() == 'spawn':
|
|
reopenSTDFile(GM.STDOUT)
|
|
reopenSTDFile(GM.STDERR)
|
|
elif dataType == GM.REDIRECT_QUEUE_VALUES:
|
|
GC.Values = dataItem
|
|
csvPF.SetColumnDelimiter(GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER])
|
|
csvPF.SetNoEscapeChar(GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR])
|
|
csvPF.SetQuoteChar(GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR])
|
|
csvPF.SetSortHeaders(GC.Values[GC.CSV_OUTPUT_SORT_HEADERS])
|
|
csvPF.SetTimestampColumn(GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN])
|
|
csvPF.SetHeaderFilter(GC.Values[GC.CSV_OUTPUT_HEADER_FILTER])
|
|
csvPF.SetHeaderDropFilter(GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER])
|
|
csvPF.SetRowFilter(GC.Values[GC.CSV_OUTPUT_ROW_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
|
|
csvPF.SetRowDropFilter(GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER], GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE])
|
|
csvPF.SetRowLimit(GC.Values[GC.CSV_OUTPUT_ROW_LIMIT])
|
|
else: #GM.REDIRECT_QUEUE_EOF
|
|
break
|
|
csvPF.writeCSVfile(list_type)
|
|
if mpQueueStdout:
|
|
mpQueueStdout.put((0, GM.REDIRECT_QUEUE_DATA, GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()))
|
|
else:
|
|
flushStdout()
|
|
if mpQueueStderr and mpQueueStderr is not mpQueueStdout:
|
|
mpQueueStderr.put((0, GM.REDIRECT_QUEUE_DATA, GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()))
|
|
else:
|
|
flushStderr()
|
|
|
|
def initializeCSVFileQueueHandler(mpManager, mpQueueStdout, mpQueueStderr):
|
|
mpQueue = mpManager.Queue()
|
|
mpQueueHandler = multiprocessing.Process(target=CSVFileQueueHandler,
|
|
args=(mpQueue, mpQueueStdout, mpQueueStderr,
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE_CSVPF],
|
|
GM.Globals[GM.DATETIME_NOW], GC.Values[GC.TIMEZONE],
|
|
GC.Values[GC.OUTPUT_TIMEFORMAT]))
|
|
mpQueueHandler.start()
|
|
return (mpQueue, mpQueueHandler)
|
|
|
|
def terminateCSVFileQueueHandler(mpQueue, mpQueueHandler):
|
|
GM.Globals[GM.PARSER] = None
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = None
|
|
if multiprocessing.get_start_method() == 'spawn':
|
|
mpQueue.put((GM.REDIRECT_QUEUE_ARGS, Cmd.AllArguments()))
|
|
savedValues = saveNonPickleableValues()
|
|
mpQueue.put((GM.REDIRECT_QUEUE_GLOBALS, GM.Globals))
|
|
restoreNonPickleableValues(savedValues)
|
|
mpQueue.put((GM.REDIRECT_QUEUE_VALUES, GC.Values))
|
|
mpQueue.put((GM.REDIRECT_QUEUE_EOF, None))
|
|
mpQueueHandler.join()
|
|
|
|
def StdQueueHandler(mpQueue, stdtype, gmGlobals, gcValues):
|
|
|
|
PROCESS_MSG = '{0}: {1:6d}, {2:>5s}: {3}, RC: {4:3d}, Cmd: {5}\n'
|
|
|
|
def _writeData(data):
|
|
fd.write(data)
|
|
|
|
def _writePidData(pid, data):
|
|
try:
|
|
if pid != 0 and GC.Values[GC.SHOW_MULTIPROCESS_INFO]:
|
|
_writeData(PROCESS_MSG.format(pidData[pid]['queue'], pid, 'Start', pidData[pid]['start'], 0, pidData[pid]['cmd']))
|
|
if data[1] is not None:
|
|
_writeData(data[1])
|
|
if GC.Values[GC.SHOW_MULTIPROCESS_INFO]:
|
|
_writeData(PROCESS_MSG.format(pidData[pid]['queue'], pid, 'End', currentISOformatTimeStamp(), data[0], pidData[pid]['cmd']))
|
|
fd.flush()
|
|
except IOError as e:
|
|
systemErrorExit(FILE_ERROR_RC, fdErrorMessage(fd, GM.Globals[stdtype][GM.REDIRECT_NAME], e))
|
|
|
|
# if sys.platform.startswith('win'):
|
|
# signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
if multiprocessing.get_start_method() == 'spawn':
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
GM.Globals = gmGlobals.copy()
|
|
GC.Values = gcValues.copy()
|
|
pid0DataItem = [KEYBOARD_INTERRUPT_RC, None]
|
|
pidData = {}
|
|
if multiprocessing.get_start_method() == 'spawn':
|
|
if GM.Globals[stdtype][GM.REDIRECT_NAME] == 'null':
|
|
fd = open(os.devnull, GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=UTF8)
|
|
elif GM.Globals[stdtype][GM.REDIRECT_NAME] == '-':
|
|
fd = os.fdopen(os.dup([sys.stderr.fileno(), sys.stdout.fileno()][GM.Globals[stdtype][GM.REDIRECT_QUEUE] == 'stdout']),
|
|
GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=GM.Globals[GM.SYS_ENCODING])
|
|
elif GM.Globals[stdtype][GM.REDIRECT_NAME] == 'stdout' and GM.Globals[stdtype][GM.REDIRECT_QUEUE] == 'stderr':
|
|
fd = os.fdopen(os.dup(sys.stdout.fileno()), GM.Globals[stdtype][GM.REDIRECT_MODE], encoding=GM.Globals[GM.SYS_ENCODING])
|
|
else:
|
|
fd = openFile(GM.Globals[stdtype][GM.REDIRECT_NAME], GM.Globals[stdtype][GM.REDIRECT_MODE])
|
|
else:
|
|
fd = GM.Globals[stdtype][GM.REDIRECT_FD]
|
|
while True:
|
|
try:
|
|
pid, dataType, dataItem = mpQueue.get()
|
|
except (EOFError, ValueError):
|
|
break
|
|
if dataType == GM.REDIRECT_QUEUE_START:
|
|
pidData[pid] = {'queue': GM.Globals[stdtype][GM.REDIRECT_QUEUE],
|
|
'start': currentISOformatTimeStamp(),
|
|
'cmd': Cmd.QuotedArgumentList(dataItem)}
|
|
if pid == 0 and GC.Values[GC.SHOW_MULTIPROCESS_INFO]:
|
|
fd.write(PROCESS_MSG.format(pidData[pid]['queue'], pid, 'Start', pidData[pid]['start'], 0, pidData[pid]['cmd']))
|
|
elif dataType == GM.REDIRECT_QUEUE_DATA:
|
|
_writeData(dataItem)
|
|
elif dataType == GM.REDIRECT_QUEUE_END:
|
|
if pid != 0:
|
|
_writePidData(pid, dataItem)
|
|
del pidData[pid]
|
|
else:
|
|
pid0DataItem = dataItem
|
|
else: #GM.REDIRECT_QUEUE_EOF
|
|
break
|
|
for pid in pidData:
|
|
if pid != 0:
|
|
_writePidData(pid, [KEYBOARD_INTERRUPT_RC, None])
|
|
_writePidData(0, pid0DataItem)
|
|
if fd not in [sys.stdout, sys.stderr]:
|
|
try:
|
|
fd.flush()
|
|
fd.close()
|
|
except IOError:
|
|
pass
|
|
GM.Globals[stdtype][GM.REDIRECT_FD] = None
|
|
|
|
def initializeStdQueueHandler(mpManager, stdtype, gmGlobals, gcValues):
|
|
mpQueue = mpManager.Queue()
|
|
mpQueueHandler = multiprocessing.Process(target=StdQueueHandler, args=(mpQueue, stdtype, gmGlobals, gcValues))
|
|
mpQueueHandler.start()
|
|
return (mpQueue, mpQueueHandler)
|
|
|
|
def batchWriteStderr(data):
|
|
try:
|
|
sys.stderr.write(data)
|
|
sys.stderr.flush()
|
|
except IOError as e:
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage('stderr', e))
|
|
|
|
def writeStdQueueHandler(mpQueue, item):
|
|
while True:
|
|
try:
|
|
mpQueue.put(item)
|
|
return
|
|
except Exception as e:
|
|
time.sleep(1)
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},{item[0]}/{GM.Globals[GM.NUM_BATCH_ITEMS]},Error,{str(e)}\n')
|
|
|
|
def terminateStdQueueHandler(mpQueue, mpQueueHandler):
|
|
mpQueue.put((0, GM.REDIRECT_QUEUE_EOF, None))
|
|
mpQueueHandler.join()
|
|
|
|
def ProcessGAMCommandMulti(pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout, mpQueueStderr,
|
|
debugLevel, todrive, printAguDomains,
|
|
printCrosOUs, printCrosOUsAndChildren,
|
|
output_dateformat, output_timeformat,
|
|
csvColumnDelimiter, csvNoEscapeChar, csvQuoteChar,
|
|
csvSortHeaders, csvTimestampColumn,
|
|
csvHeaderFilter, csvHeaderDropFilter,
|
|
csvHeaderForce, csvHeaderOrder,
|
|
csvRowFilter, csvRowFilterMode, csvRowDropFilter, csvRowDropFilterMode,
|
|
csvRowLimit,
|
|
showGettings, showGettingsGotNL,
|
|
args):
|
|
global mplock
|
|
|
|
with mplock:
|
|
initializeLogging()
|
|
# if sys.platform.startswith('win'):
|
|
if multiprocessing.get_start_method() == 'spawn':
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
GM.Globals[GM.API_CALLS_RETRY_DATA] = {}
|
|
GM.Globals[GM.CMDLOG_LOGGER] = None
|
|
GM.Globals[GM.CSVFILE] = {}
|
|
GM.Globals[GM.CSV_DATA_DICT] = {}
|
|
GM.Globals[GM.CSV_KEY_FIELD] = None
|
|
GM.Globals[GM.CSV_SUBKEY_FIELD] = None
|
|
GM.Globals[GM.CSV_DATA_FIELD] = None
|
|
GM.Globals[GM.CSV_OUTPUT_COLUMN_DELIMITER] = csvColumnDelimiter
|
|
GM.Globals[GM.CSV_OUTPUT_NO_ESCAPE_CHAR] = csvNoEscapeChar
|
|
GM.Globals[GM.CSV_OUTPUT_HEADER_DROP_FILTER] = csvHeaderDropFilter[:]
|
|
GM.Globals[GM.CSV_OUTPUT_HEADER_FILTER] = csvHeaderFilter[:]
|
|
GM.Globals[GM.CSV_OUTPUT_HEADER_FORCE] = csvHeaderForce[:]
|
|
GM.Globals[GM.CSV_OUTPUT_HEADER_ORDER] = csvHeaderOrder[:]
|
|
GM.Globals[GM.CSV_OUTPUT_QUOTE_CHAR] = csvQuoteChar
|
|
GM.Globals[GM.CSV_OUTPUT_ROW_DROP_FILTER] = csvRowDropFilter[:]
|
|
GM.Globals[GM.CSV_OUTPUT_ROW_DROP_FILTER_MODE] = csvRowDropFilterMode
|
|
GM.Globals[GM.CSV_OUTPUT_ROW_FILTER] = csvRowFilter[:]
|
|
GM.Globals[GM.CSV_OUTPUT_ROW_FILTER_MODE] = csvRowFilterMode
|
|
GM.Globals[GM.CSV_OUTPUT_ROW_LIMIT] = csvRowLimit
|
|
GM.Globals[GM.CSV_OUTPUT_SORT_HEADERS] = csvSortHeaders[:]
|
|
GM.Globals[GM.CSV_OUTPUT_TIMESTAMP_COLUMN] = csvTimestampColumn
|
|
GM.Globals[GM.CSV_TODRIVE] = todrive.copy()
|
|
GM.Globals[GM.DEBUG_LEVEL] = debugLevel
|
|
GM.Globals[GM.OUTPUT_DATEFORMAT] = output_dateformat
|
|
GM.Globals[GM.OUTPUT_TIMEFORMAT] = output_timeformat
|
|
GM.Globals[GM.NUM_BATCH_ITEMS] = numItems
|
|
GM.Globals[GM.PID] = pid
|
|
GM.Globals[GM.PRINT_AGU_DOMAINS] = printAguDomains[:]
|
|
GM.Globals[GM.PRINT_CROS_OUS] = printCrosOUs[:]
|
|
GM.Globals[GM.PRINT_CROS_OUS_AND_CHILDREN] = printCrosOUsAndChildren[:]
|
|
GM.Globals[GM.SAVED_STDOUT] = None
|
|
GM.Globals[GM.SHOW_GETTINGS] = showGettings
|
|
GM.Globals[GM.SHOW_GETTINGS_GOT_NL] = showGettingsGotNL
|
|
GM.Globals[GM.SYSEXITRC] = 0
|
|
GM.Globals[GM.PARSER] = None
|
|
if mpQueueCSVFile:
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = mpQueueCSVFile
|
|
if mpQueueStdout:
|
|
GM.Globals[GM.STDOUT] = {GM.REDIRECT_NAME: '', GM.REDIRECT_FD: None, GM.REDIRECT_MULTI_FD: StringIOobject()}
|
|
if debugLevel:
|
|
sys.stdout = GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]
|
|
# mpQueueStdout.put((pid, GM.REDIRECT_QUEUE_START, args))
|
|
writeStdQueueHandler(mpQueueStdout,(pid, GM.REDIRECT_QUEUE_START, args))
|
|
else:
|
|
GM.Globals[GM.STDOUT] = {}
|
|
if mpQueueStderr:
|
|
if mpQueueStderr is not mpQueueStdout:
|
|
GM.Globals[GM.STDERR] = {GM.REDIRECT_NAME: '', GM.REDIRECT_FD: None, GM.REDIRECT_MULTI_FD: StringIOobject()}
|
|
# mpQueueStderr.put((pid, GM.REDIRECT_QUEUE_START, args))
|
|
writeStdQueueHandler(mpQueueStderr, (pid, GM.REDIRECT_QUEUE_START, args))
|
|
else:
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]
|
|
else:
|
|
GM.Globals[GM.STDERR] = {}
|
|
sysRC = ProcessGAMCommand(args)
|
|
with mplock:
|
|
if mpQueueStdout:
|
|
# mpQueueStdout.put((pid, GM.REDIRECT_QUEUE_END, [sysRC, GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()]))
|
|
writeStdQueueHandler(mpQueueStdout, (pid, GM.REDIRECT_QUEUE_END, [sysRC, GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()]))
|
|
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].close()
|
|
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD] = None
|
|
if mpQueueStderr and mpQueueStderr is not mpQueueStdout:
|
|
# mpQueueStderr.put((pid, GM.REDIRECT_QUEUE_END, [sysRC, GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()]))
|
|
writeStdQueueHandler(mpQueueStderr, (pid, GM.REDIRECT_QUEUE_END, [sysRC, GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()]))
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].close()
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = None
|
|
return (pid, sysRC, logCmd)
|
|
|
|
ERROR_PLURAL_SINGULAR = [Msg.ERRORS, Msg.ERROR]
|
|
PROCESS_PLURAL_SINGULAR = [Msg.PROCESSES, Msg.PROCESS]
|
|
THREAD_PLURAL_SINGULAR = [Msg.THREADS, Msg.THREAD]
|
|
|
|
def checkChildProcessRC(rc):
|
|
# Comparison
|
|
if 'comp' in GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]:
|
|
op = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['comp']
|
|
value = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['value']
|
|
if op == '<':
|
|
return rc < value
|
|
if op == '<=':
|
|
return rc <= value
|
|
if op == '>':
|
|
return rc > value
|
|
if op == '>=':
|
|
return rc >= value
|
|
if op == '!=':
|
|
return rc != value
|
|
return rc == value
|
|
# Range
|
|
op = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['range']
|
|
low = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['low']
|
|
high = GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION]['high']
|
|
if op == '!=':
|
|
return not low <= rc <= high
|
|
return low <= rc <= high
|
|
|
|
def initGamWorker(l):
|
|
global mplock
|
|
mplock = l
|
|
|
|
def MultiprocessGAMCommands(items, showCmds):
|
|
def poolCallback(result):
|
|
poolProcessResults[0] -= 1
|
|
if showCmds:
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},{result[0]}/{numItems},End,{result[1]},{result[2]}\n')
|
|
if GM.Globals[GM.CMDLOG_LOGGER]:
|
|
GM.Globals[GM.CMDLOG_LOGGER].info(f'{currentISOformatTimeStamp()},{result[1]},{result[2]}')
|
|
if GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION] is not None and checkChildProcessRC(result[1]):
|
|
GM.Globals[GM.MULTIPROCESS_EXIT_PROCESSING] = True
|
|
|
|
def signal_handler(*_):
|
|
nonlocal controlC
|
|
controlC = True
|
|
|
|
def handleControlC(source):
|
|
nonlocal controlC
|
|
batchWriteStderr(f'Control-C (Multiprocess-{source})\n')
|
|
setSysExitRC(KEYBOARD_INTERRUPT_RC)
|
|
batchWriteStderr(Msg.BATCH_CSV_TERMINATE_N_PROCESSES.format(currentISOformatTimeStamp(),
|
|
numItems, poolProcessResults[0],
|
|
PROCESS_PLURAL_SINGULAR[poolProcessResults[0] == 1]))
|
|
pool.terminate()
|
|
controlC = False
|
|
|
|
if not items:
|
|
return
|
|
GM.Globals[GM.NUM_BATCH_ITEMS] = numItems = len(items)
|
|
numPoolProcesses = min(numItems, GC.Values[GC.NUM_THREADS])
|
|
if GC.Values[GC.MULTIPROCESS_POOL_LIMIT] == -1:
|
|
parallelPoolProcesses = -1
|
|
elif GC.Values[GC.MULTIPROCESS_POOL_LIMIT] == 0:
|
|
parallelPoolProcesses = numPoolProcesses
|
|
else:
|
|
parallelPoolProcesses = min(numItems, GC.Values[GC.MULTIPROCESS_POOL_LIMIT])
|
|
# origSigintHandler = signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
mpManager = multiprocessing.Manager()
|
|
l = mpManager.Lock()
|
|
try:
|
|
if multiprocessing.get_start_method() == 'spawn':
|
|
pool = mpManager.Pool(processes=numPoolProcesses, initializer=initGamWorker, initargs=(l,), maxtasksperchild=200)
|
|
else:
|
|
pool = multiprocessing.Pool(processes=numPoolProcesses, initializer=initGamWorker, initargs=(l,), maxtasksperchild=200)
|
|
except IOError as e:
|
|
systemErrorExit(FILE_ERROR_RC, e)
|
|
except AssertionError as e:
|
|
Cmd.SetLocation(0)
|
|
usageErrorExit(str(e))
|
|
if multiprocessing.get_start_method() == 'spawn':
|
|
savedValues = saveNonPickleableValues()
|
|
if GM.Globals[GM.STDOUT][GM.REDIRECT_MULTIPROCESS]:
|
|
mpQueueStdout, mpQueueHandlerStdout = initializeStdQueueHandler(mpManager, GM.STDOUT, GM.Globals, GC.Values)
|
|
mpQueueStdout.put((0, GM.REDIRECT_QUEUE_START, Cmd.AllArguments()))
|
|
else:
|
|
mpQueueStdout = None
|
|
if GM.Globals[GM.STDERR][GM.REDIRECT_MULTIPROCESS]:
|
|
if GM.Globals[GM.STDERR][GM.REDIRECT_NAME] != 'stdout':
|
|
mpQueueStderr, mpQueueHandlerStderr = initializeStdQueueHandler(mpManager, GM.STDERR, GM.Globals, GC.Values)
|
|
mpQueueStderr.put((0, GM.REDIRECT_QUEUE_START, Cmd.AllArguments()))
|
|
else:
|
|
mpQueueStderr = mpQueueStdout
|
|
else:
|
|
mpQueueStderr = None
|
|
if multiprocessing.get_start_method() == 'spawn':
|
|
restoreNonPickleableValues(savedValues)
|
|
if mpQueueStdout:
|
|
mpQueueStdout.put((0, GM.REDIRECT_QUEUE_DATA, GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()))
|
|
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].truncate(0)
|
|
if mpQueueStderr and mpQueueStderr is not mpQueueStdout:
|
|
mpQueueStderr.put((0, GM.REDIRECT_QUEUE_DATA, GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()))
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].truncate(0)
|
|
if GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS]:
|
|
mpQueueCSVFile, mpQueueHandlerCSVFile = initializeCSVFileQueueHandler(mpManager, mpQueueStdout, mpQueueStderr)
|
|
else:
|
|
mpQueueCSVFile = None
|
|
# signal.signal(signal.SIGINT, origSigintHandler)
|
|
controlC = False
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
batchWriteStderr(Msg.USING_N_PROCESSES.format(currentISOformatTimeStamp(),
|
|
numItems, numPoolProcesses,
|
|
PROCESS_PLURAL_SINGULAR[numPoolProcesses == 1]))
|
|
try:
|
|
pid = 0
|
|
poolProcessResults = {pid: 0}
|
|
for item in items:
|
|
if GM.Globals[GM.MULTIPROCESS_EXIT_PROCESSING]:
|
|
break
|
|
if controlC:
|
|
break
|
|
if item[0] == Cmd.COMMIT_BATCH_CMD:
|
|
batchWriteStderr(Msg.COMMIT_BATCH_WAIT_N_PROCESSES.format(currentISOformatTimeStamp(),
|
|
numItems, poolProcessResults[0],
|
|
PROCESS_PLURAL_SINGULAR[poolProcessResults[0] == 1]))
|
|
while poolProcessResults[0] > 0:
|
|
time.sleep(1)
|
|
completedProcesses = []
|
|
for p, result in iter(poolProcessResults.items()):
|
|
if p != 0 and result.ready():
|
|
poolCallback(result.get())
|
|
completedProcesses.append(p)
|
|
for p in completedProcesses:
|
|
del poolProcessResults[p]
|
|
batchWriteStderr(Msg.COMMIT_BATCH_COMPLETE.format(currentISOformatTimeStamp(), numItems, Msg.PROCESSES))
|
|
if len(item) > 1:
|
|
readStdin(f'{currentISOformatTimeStamp()},0/{numItems},{Cmd.QuotedArgumentList(item[1:])}')
|
|
continue
|
|
if item[0] == Cmd.PRINT_CMD:
|
|
batchWriteStderr(Cmd.QuotedArgumentList(item[1:])+'\n')
|
|
continue
|
|
if item[0] == Cmd.SLEEP_CMD:
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},0/{numItems},Sleepiing {item[1]} seconds\n')
|
|
time.sleep(int(item[1]))
|
|
continue
|
|
pid += 1
|
|
if not showCmds and ((pid % 100 == 0) or (pid == numItems)):
|
|
batchWriteStderr(Msg.PROCESSING_ITEM_N_OF_M.format(currentISOformatTimeStamp(), pid, numItems))
|
|
if showCmds or GM.Globals[GM.CMDLOG_LOGGER]:
|
|
logCmd = Cmd.QuotedArgumentList(item)
|
|
if showCmds:
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},Start,0,{logCmd}\n')
|
|
else:
|
|
logCmd = ''
|
|
poolProcessResults[pid] = pool.apply_async(ProcessGAMCommandMulti,
|
|
[pid, numItems, logCmd, mpQueueCSVFile, mpQueueStdout, mpQueueStderr,
|
|
GC.Values[GC.DEBUG_LEVEL], GM.Globals[GM.CSV_TODRIVE],
|
|
GC.Values[GC.PRINT_AGU_DOMAINS],
|
|
GC.Values[GC.PRINT_CROS_OUS], GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN],
|
|
GC.Values[GC.OUTPUT_DATEFORMAT], GC.Values[GC.OUTPUT_TIMEFORMAT],
|
|
GC.Values[GC.CSV_OUTPUT_COLUMN_DELIMITER],
|
|
GC.Values[GC.CSV_OUTPUT_NO_ESCAPE_CHAR],
|
|
GC.Values[GC.CSV_OUTPUT_QUOTE_CHAR],
|
|
GC.Values[GC.CSV_OUTPUT_SORT_HEADERS],
|
|
GC.Values[GC.CSV_OUTPUT_TIMESTAMP_COLUMN],
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_FILTER],
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_DROP_FILTER],
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_FORCE],
|
|
GC.Values[GC.CSV_OUTPUT_HEADER_ORDER],
|
|
GC.Values[GC.CSV_OUTPUT_ROW_FILTER],
|
|
GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE],
|
|
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER],
|
|
GC.Values[GC.CSV_OUTPUT_ROW_DROP_FILTER_MODE],
|
|
GC.Values[GC.CSV_OUTPUT_ROW_LIMIT],
|
|
GC.Values[GC.SHOW_GETTINGS], GC.Values[GC.SHOW_GETTINGS_GOT_NL],
|
|
item])
|
|
poolProcessResults[0] += 1
|
|
if parallelPoolProcesses > 0:
|
|
while poolProcessResults[0] == parallelPoolProcesses:
|
|
completedProcesses = []
|
|
for p, result in iter(poolProcessResults.items()):
|
|
if p != 0 and result.ready():
|
|
poolCallback(result.get())
|
|
completedProcesses.append(p)
|
|
if completedProcesses:
|
|
for p in completedProcesses:
|
|
del poolProcessResults[p]
|
|
break
|
|
time.sleep(1)
|
|
processWaitStart = time.time()
|
|
if not controlC:
|
|
if GC.Values[GC.PROCESS_WAIT_LIMIT] > 0:
|
|
waitRemaining = GC.Values[GC.PROCESS_WAIT_LIMIT]
|
|
else:
|
|
waitRemaining = 'unlimited'
|
|
while poolProcessResults[0] > 0:
|
|
batchWriteStderr(Msg.BATCH_CSV_WAIT_N_PROCESSES.format(currentISOformatTimeStamp(),
|
|
numItems, poolProcessResults[0],
|
|
PROCESS_PLURAL_SINGULAR[poolProcessResults[0] == 1],
|
|
Msg.BATCH_CSV_WAIT_LIMIT.format(waitRemaining)))
|
|
completedProcesses = []
|
|
for p, result in iter(poolProcessResults.items()):
|
|
if p != 0 and result.ready():
|
|
poolCallback(result.get())
|
|
completedProcesses.append(p)
|
|
for p in completedProcesses:
|
|
del poolProcessResults[p]
|
|
if poolProcessResults[0] > 0:
|
|
if controlC:
|
|
handleControlC('SIG')
|
|
break
|
|
time.sleep(5)
|
|
if GC.Values[GC.PROCESS_WAIT_LIMIT] > 0:
|
|
delta = int(time.time()-processWaitStart)
|
|
if delta >= GC.Values[GC.PROCESS_WAIT_LIMIT]:
|
|
batchWriteStderr(Msg.BATCH_CSV_TERMINATE_N_PROCESSES.format(currentISOformatTimeStamp(),
|
|
numItems, poolProcessResults[0],
|
|
PROCESS_PLURAL_SINGULAR[poolProcessResults[0] == 1]))
|
|
pool.terminate()
|
|
break
|
|
waitRemaining = GC.Values[GC.PROCESS_WAIT_LIMIT] - delta
|
|
pool.close()
|
|
else:
|
|
handleControlC('SIG')
|
|
except KeyboardInterrupt:
|
|
handleControlC('KBI')
|
|
pool.join()
|
|
batchWriteStderr(Msg.BATCH_CSV_PROCESSING_COMPLETE.format(currentISOformatTimeStamp(), numItems))
|
|
if mpQueueCSVFile:
|
|
terminateCSVFileQueueHandler(mpQueueCSVFile, mpQueueHandlerCSVFile)
|
|
if mpQueueStdout:
|
|
mpQueueStdout.put((0, GM.REDIRECT_QUEUE_END, [GM.Globals[GM.SYSEXITRC], GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].getvalue()]))
|
|
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD].close()
|
|
GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD] = None
|
|
terminateStdQueueHandler(mpQueueStdout, mpQueueHandlerStdout)
|
|
if mpQueueStderr and mpQueueStderr is not mpQueueStdout:
|
|
mpQueueStderr.put((0, GM.REDIRECT_QUEUE_END, [GM.Globals[GM.SYSEXITRC], GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].getvalue()]))
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD].close()
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = None
|
|
terminateStdQueueHandler(mpQueueStderr, mpQueueHandlerStderr)
|
|
|
|
def threadBatchWorker(showCmds=False, numItems=0):
|
|
while True:
|
|
pid, item, logCmd = GM.Globals[GM.TBATCH_QUEUE].get()
|
|
try:
|
|
sysRC = subprocess.call(item, stdout=GM.Globals[GM.STDOUT].get(GM.REDIRECT_MULTI_FD, sys.stdout),
|
|
stderr=GM.Globals[GM.STDERR].get(GM.REDIRECT_MULTI_FD, sys.stderr))
|
|
if showCmds:
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},End,{sysRC},{logCmd}\n')
|
|
if GM.Globals[GM.MULTIPROCESS_EXIT_CONDITION] is not None and checkChildProcessRC(sysRC):
|
|
GM.Globals[GM.MULTIPROCESS_EXIT_PROCESSING] = True
|
|
except Exception as e:
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},Error,{str(e)},{logCmd}\n')
|
|
GM.Globals[GM.TBATCH_QUEUE].task_done()
|
|
|
|
BATCH_COMMANDS = [Cmd.GAM_CMD, Cmd.COMMIT_BATCH_CMD, Cmd.PRINT_CMD, Cmd.SLEEP_CMD]
|
|
TBATCH_COMMANDS = [Cmd.GAM_CMD, Cmd.COMMIT_BATCH_CMD, Cmd.EXECUTE_CMD, Cmd.PRINT_CMD, Cmd.SLEEP_CMD]
|
|
|
|
def ThreadBatchGAMCommands(items, showCmds):
|
|
if not items:
|
|
return
|
|
pythonCmd = [sys.executable]
|
|
if not getattr(sys, 'frozen', False): # we're not frozen
|
|
pythonCmd.append(os.path.realpath(Cmd.Argument(0)))
|
|
GM.Globals[GM.NUM_BATCH_ITEMS] = numItems = len(items)
|
|
numWorkerThreads = min(numItems, GC.Values[GC.NUM_TBATCH_THREADS])
|
|
# GM.Globals[GM.TBATCH_QUEUE].put() gets blocked when trying to create more items than there are workers
|
|
GM.Globals[GM.TBATCH_QUEUE] = queue.Queue(maxsize=numWorkerThreads)
|
|
batchWriteStderr(Msg.USING_N_PROCESSES.format(currentISOformatTimeStamp(),
|
|
numItems, numWorkerThreads,
|
|
THREAD_PLURAL_SINGULAR[numWorkerThreads == 1]))
|
|
for _ in range(numWorkerThreads):
|
|
t = threading.Thread(target=threadBatchWorker, kwargs={'showCmds': showCmds, 'numItems': numItems})
|
|
t.daemon = True
|
|
t.start()
|
|
pid = 0
|
|
numThreadsInUse = 0
|
|
for item in items:
|
|
if GM.Globals[GM.MULTIPROCESS_EXIT_PROCESSING]:
|
|
break
|
|
if item[0] == Cmd.COMMIT_BATCH_CMD:
|
|
batchWriteStderr(Msg.COMMIT_BATCH_WAIT_N_PROCESSES.format(currentISOformatTimeStamp(),
|
|
numItems, numThreadsInUse,
|
|
THREAD_PLURAL_SINGULAR[numThreadsInUse == 1]))
|
|
GM.Globals[GM.TBATCH_QUEUE].join()
|
|
batchWriteStderr(Msg.COMMIT_BATCH_COMPLETE.format(currentISOformatTimeStamp(), numItems, Msg.THREADS))
|
|
numThreadsInUse = 0
|
|
if len(item) > 1:
|
|
readStdin(f'{currentISOformatTimeStamp()},0/{numItems},{Cmd.QuotedArgumentList(item[1:])}')
|
|
continue
|
|
if item[0] == Cmd.PRINT_CMD:
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},0/{numItems},{Cmd.QuotedArgumentList(item[1:])}\n')
|
|
continue
|
|
if item[0] == Cmd.SLEEP_CMD:
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},0/{numItems},Sleeping {item[1]} seconds\n')
|
|
time.sleep(int(item[1]))
|
|
continue
|
|
pid += 1
|
|
if not showCmds and ((pid % 100 == 0) or (pid == numItems)):
|
|
batchWriteStderr(Msg.PROCESSING_ITEM_N_OF_M.format(currentISOformatTimeStamp(), pid, numItems))
|
|
if showCmds:
|
|
logCmd = Cmd.QuotedArgumentList(item)
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},Start,{Cmd.QuotedArgumentList(item)}\n')
|
|
else:
|
|
logCmd = ''
|
|
if item[0] == Cmd.GAM_CMD:
|
|
GM.Globals[GM.TBATCH_QUEUE].put((pid, pythonCmd+item[1:], logCmd))
|
|
else:
|
|
GM.Globals[GM.TBATCH_QUEUE].put((pid, item[1:], logCmd))
|
|
numThreadsInUse += 1
|
|
GM.Globals[GM.TBATCH_QUEUE].join()
|
|
if showCmds:
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},0/{numItems},Complete\n')
|
|
|
|
def _getShowCommands():
|
|
if checkArgumentPresent('showcmds'):
|
|
return getBoolean()
|
|
return GC.Values[GC.SHOW_COMMANDS]
|
|
|
|
def _getSkipRows():
|
|
if checkArgumentPresent('skiprows'):
|
|
return getInteger(minVal=0)
|
|
# return GC.Values[GC.CSV_INPUT_ROW_SKIP]
|
|
return 0
|
|
|
|
def _getMaxRows():
|
|
if checkArgumentPresent('maxrows'):
|
|
return getInteger(minVal=0)
|
|
return GC.Values[GC.CSV_INPUT_ROW_LIMIT]
|
|
|
|
# gam batch <BatchContent> [showcmds [<Boolean>]]
|
|
def doBatch(threadBatch=False):
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
if (filename == '-') and (GC.Values[GC.DEBUG_LEVEL] > 0):
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.BATCH_CSV_LOOP_DASH_DEBUG_INCOMPATIBLE.format(Cmd.BATCH_CMD))
|
|
filenameLower = filename.lower()
|
|
if filenameLower not in {'gdoc', 'gcsdoc'}:
|
|
encoding = getCharSet()
|
|
f = openFile(filename, encoding=encoding, stripUTFBOM=True)
|
|
elif filenameLower == 'gdoc':
|
|
f = getGDocData(filenameLower)
|
|
getCharSet()
|
|
else: #filenameLower == 'gcsdoc':
|
|
f = getStorageFileData(filenameLower)
|
|
getCharSet()
|
|
showCmds = _getShowCommands()
|
|
checkForExtraneousArguments()
|
|
validCommands = BATCH_COMMANDS if not threadBatch else TBATCH_COMMANDS
|
|
kwValues = {}
|
|
items = []
|
|
errors = 0
|
|
try:
|
|
for line in f:
|
|
if line.startswith('#'):
|
|
continue
|
|
if kwValues:
|
|
for kw, value in iter(kwValues.items()):
|
|
line = line.replace(f'%{kw}%', value)
|
|
try:
|
|
argv = shlex.split(line)
|
|
except ValueError as e:
|
|
writeStderr(f'Command: >>>{line.strip()}<<<\n')
|
|
writeStderr(f'{ERROR_PREFIX}{str(e)}\n')
|
|
errors += 1
|
|
continue
|
|
if argv:
|
|
cmd = argv[0].strip().lower()
|
|
if cmd == Cmd.SET_CMD:
|
|
if len(argv) == 3:
|
|
kwValues[argv[1]] = argv[2]
|
|
else:
|
|
writeStderr(f'Command: >>>{Cmd.QuotedArgumentList([argv[0]])}<<< {Cmd.QuotedArgumentList(argv[1:])}\n')
|
|
writeStderr(f'{ERROR_PREFIX}{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]}: {Msg.EXPECTED} <{Cmd.SET_CMD} keyword value>)>\n')
|
|
errors += 1
|
|
continue
|
|
if cmd == Cmd.CLEAR_CMD:
|
|
if len(argv) == 2:
|
|
kwValues.pop(argv[1], None)
|
|
else:
|
|
writeStderr(f'Command: >>>{Cmd.QuotedArgumentList([argv[0]])}<<< {Cmd.QuotedArgumentList(argv[1:])}\n')
|
|
writeStderr(f'{ERROR_PREFIX}{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]}: {Msg.EXPECTED} <{Cmd.CLEAR_CMD} keyword>)>\n')
|
|
errors += 1
|
|
continue
|
|
if cmd == Cmd.SLEEP_CMD:
|
|
if len(argv) != 2 or not argv[1].isdigit():
|
|
writeStderr(f'Command: >>>{Cmd.QuotedArgumentList([argv[0]])}<<< {Cmd.QuotedArgumentList(argv[1:])}\n')
|
|
writeStderr(f'{ERROR_PREFIX}{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]}: {Msg.EXPECTED} <{Cmd.SLEEP_CMD} integer>)>\n')
|
|
errors += 1
|
|
continue
|
|
if (not cmd) or ((len(argv) == 1) and (cmd not in [Cmd.COMMIT_BATCH_CMD, Cmd.PRINT_CMD])):
|
|
continue
|
|
if cmd in validCommands:
|
|
items.append(argv)
|
|
else:
|
|
writeStderr(f'Command: >>>{Cmd.QuotedArgumentList([argv[0]])}<<< {Cmd.QuotedArgumentList(argv[1:])}\n')
|
|
writeStderr(f'{ERROR_PREFIX}{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]}: {Msg.EXPECTED} <{formatChoiceList(validCommands)}>\n')
|
|
errors += 1
|
|
except IOError as e:
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
|
closeFile(f)
|
|
if errors == 0:
|
|
if not threadBatch:
|
|
MultiprocessGAMCommands(items, showCmds)
|
|
else:
|
|
ThreadBatchGAMCommands(items, showCmds)
|
|
else:
|
|
writeStderr(Msg.BATCH_NOT_PROCESSED_ERRORS.format(ERROR_PREFIX, filename, errors, ERROR_PLURAL_SINGULAR[errors == 1]))
|
|
setSysExitRC(USAGE_ERROR_RC)
|
|
|
|
# gam tbatch <BatchContent> [showcmds [<Boolean>]]
|
|
def doThreadBatch():
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
doBatch(True)
|
|
|
|
def doAutoBatch(entityType, entityList, CL_command):
|
|
remaining = Cmd.Remaining()
|
|
items = []
|
|
initial_argv = [Cmd.GAM_CMD]
|
|
if GM.Globals[GM.SECTION] and not GM.Globals[GM.GAM_CFG_SECTION]:
|
|
initial_argv.extend([Cmd.SELECT_CMD, GM.Globals[GM.SECTION]])
|
|
for entity in entityList:
|
|
items.append(initial_argv+[entityType, entity, CL_command]+remaining)
|
|
MultiprocessGAMCommands(items, GC.Values[GC.SHOW_COMMANDS])
|
|
|
|
# Process command line arguments, find substitutions
|
|
# An argument containing instances of ~~xxx~!~pattern~!~replacement~~ has ~~...~~ replaced by re.sub(pattern, replacement, value of field xxx from the CSV file)
|
|
# For example, ~~primaryEmail~!~^(.+)@(.+)$~!~\1 AT \2~~ would replace foo@bar.com (from the primaryEmail column) with foo AT bar.com
|
|
# 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
|
|
|
|
SUB_PATTERN = re.compile(r'~~(.+?)~~')
|
|
RE_PATTERN = re.compile(r'~~(.+?)~!~(.+?)~!~(.+?)~~')
|
|
SUB_TYPE = 'sub'
|
|
RE_TYPE = 're'
|
|
|
|
# SubFields is a dictionary; the key is the argument number, the value is a list of tuples that mark
|
|
# the substition (type, fieldname, start, end). Type is 'sub' for simple substitution, 're' for regex substitution.
|
|
# Example: update user '~User' address type work unstructured '~~Street~~, ~~City~~, ~~State~~ ~~ZIP~~' primary
|
|
# {2: [('sub', 'User', 0, 5)], 7: [('sub', 'Street', 0, 10), ('sub', 'City', 12, 20), ('sub', 'State', 22, 31), ('sub', 'ZIP', 32, 39)]}
|
|
def getSubFields(initial_argv, fieldNames):
|
|
subFields = {}
|
|
GAM_argv = initial_argv[:]
|
|
GAM_argvI = len(GAM_argv)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = Cmd.Current()
|
|
if not myarg:
|
|
GAM_argv.append(myarg)
|
|
elif SUB_PATTERN.search(myarg):
|
|
pos = 0
|
|
subFields.setdefault(GAM_argvI, [])
|
|
while True:
|
|
submatch = SUB_PATTERN.search(myarg, pos)
|
|
if not submatch:
|
|
break
|
|
rematch = RE_PATTERN.match(submatch.group(0))
|
|
if not rematch:
|
|
fieldName = submatch.group(1)
|
|
if fieldName not in fieldNames:
|
|
csvFieldErrorExit(fieldName, fieldNames)
|
|
subFields[GAM_argvI].append((SUB_TYPE, fieldName, submatch.start(), submatch.end()))
|
|
else:
|
|
fieldName = rematch.group(1)
|
|
if fieldName not in fieldNames:
|
|
csvFieldErrorExit(fieldName, fieldNames)
|
|
try:
|
|
re.compile(rematch.group(2))
|
|
subFields[GAM_argvI].append((RE_TYPE, fieldName, submatch.start(), submatch.end(), rematch.group(2), rematch.group(3)))
|
|
except re.error as e:
|
|
usageErrorExit(f'{Cmd.OB_RE_PATTERN} {Msg.ERROR}: {e}')
|
|
pos = submatch.end()
|
|
GAM_argv.append(myarg)
|
|
elif myarg[0] == '~':
|
|
fieldName = myarg[1:]
|
|
if fieldName in fieldNames:
|
|
subFields[GAM_argvI] = [(SUB_TYPE, fieldName, 0, len(myarg))]
|
|
GAM_argv.append(myarg)
|
|
else:
|
|
csvFieldErrorExit(fieldName, fieldNames)
|
|
else:
|
|
GAM_argv.append(myarg)
|
|
GAM_argvI += 1
|
|
Cmd.Advance()
|
|
return(GAM_argv, subFields)
|
|
|
|
def processSubFields(GAM_argv, row, subFields):
|
|
argv = GAM_argv[:]
|
|
for GAM_argvI, fields in iter(subFields.items()):
|
|
oargv = argv[GAM_argvI][:]
|
|
argv[GAM_argvI] = ''
|
|
pos = 0
|
|
for field in fields:
|
|
argv[GAM_argvI] += oargv[pos:field[2]]
|
|
if field[0] == SUB_TYPE:
|
|
if row[field[1]]:
|
|
argv[GAM_argvI] += row[field[1]]
|
|
else:
|
|
if row[field[1]]:
|
|
argv[GAM_argvI] += re.sub(field[4], field[5], row[field[1]])
|
|
pos = field[3]
|
|
argv[GAM_argvI] += oargv[pos:]
|
|
return argv
|
|
|
|
# gam csv <CSVLoopContent> [warnifnodata]
|
|
# [columndelimiter <Character>] [quotechar <Character>] [fields <FieldNameList>]
|
|
# (matchfield|skipfield <FieldName> <RESearchPattern>)* [showcmds [<Boolean>]]
|
|
# [skiprows <Integer>] [maxrows <Integer>]
|
|
# gam <GAM argument list>
|
|
def doCSV(testMode=False):
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
if (filename == '-') and (GC.Values[GC.DEBUG_LEVEL] > 0):
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.BATCH_CSV_LOOP_DASH_DEBUG_INCOMPATIBLE.format(Cmd.CSV_CMD))
|
|
f, csvFile, fieldnames = openCSVFileReader(filename)
|
|
matchFields, skipFields = getMatchSkipFields(fieldnames)
|
|
showCmds = _getShowCommands()
|
|
skipRows = _getSkipRows()
|
|
maxRows = _getMaxRows()
|
|
checkArgumentPresent(Cmd.GAM_CMD, required=True)
|
|
if not Cmd.ArgumentsRemaining():
|
|
missingArgumentExit(Cmd.OB_GAM_ARGUMENT_LIST)
|
|
initial_argv = [Cmd.GAM_CMD]
|
|
if GM.Globals[GM.SECTION] and not GM.Globals[GM.GAM_CFG_SECTION] and not Cmd.PeekArgumentPresent(Cmd.SELECT_CMD):
|
|
initial_argv.extend([Cmd.SELECT_CMD, GM.Globals[GM.SECTION]])
|
|
GAM_argv, subFields = getSubFields(initial_argv, fieldnames)
|
|
if GC.Values[GC.CSV_INPUT_ROW_FILTER] or GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER]:
|
|
CheckInputRowFilterHeaders(fieldnames, GC.Values[GC.CSV_INPUT_ROW_FILTER], GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER])
|
|
items = []
|
|
i = 0
|
|
for row in csvFile:
|
|
if checkMatchSkipFields(row, fieldnames, matchFields, skipFields):
|
|
i += 1
|
|
if skipRows:
|
|
if i <= skipRows:
|
|
continue
|
|
i = 1
|
|
skipRows = 0
|
|
items.append(processSubFields(GAM_argv, row, subFields))
|
|
if maxRows and i >= maxRows:
|
|
break
|
|
closeFile(f)
|
|
if not testMode:
|
|
MultiprocessGAMCommands(items, showCmds)
|
|
else:
|
|
numItems = min(len(items), 10)
|
|
writeStdout(Msg.CSV_FILE_HEADERS.format(filename))
|
|
Ind.Increment()
|
|
for field in fieldnames:
|
|
writeStdout(f'{Ind.Spaces()}{field}\n')
|
|
Ind.Decrement()
|
|
writeStdout(Msg.CSV_SAMPLE_COMMANDS.format(numItems, GAM))
|
|
Ind.Increment()
|
|
for i in range(numItems):
|
|
writeStdout(f'{Ind.Spaces()}{Cmd.QuotedArgumentList(items[i])}\n')
|
|
Ind.Decrement()
|
|
|
|
def doCSVTest():
|
|
doCSV(testMode=True)
|
|
|
|
# gam loop <CSVLoopContent> [warnifnodata]
|
|
# [columndelimiter <Character>] [quotechar <Character>] [fields <FieldNameList>]
|
|
# (matchfield|skipfield <FieldName> <RESearchPattern>)* [showcmds [<Boolean>]]
|
|
# [skiprows <Integer>] [maxrows <Integer>]
|
|
# gam <GAM argument list>
|
|
def doLoop(loopCmd):
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
if (filename == '-') and (GC.Values[GC.DEBUG_LEVEL] > 0):
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.BATCH_CSV_LOOP_DASH_DEBUG_INCOMPATIBLE.format(Cmd.LOOP_CMD))
|
|
f, csvFile, fieldnames = openCSVFileReader(filename)
|
|
matchFields, skipFields = getMatchSkipFields(fieldnames)
|
|
showCmds = _getShowCommands()
|
|
skipRows = _getSkipRows()
|
|
maxRows = _getMaxRows()
|
|
checkArgumentPresent(Cmd.GAM_CMD, required=True)
|
|
if not Cmd.ArgumentsRemaining():
|
|
missingArgumentExit(Cmd.OB_GAM_ARGUMENT_LIST)
|
|
if GC.Values[GC.CSV_INPUT_ROW_FILTER] or GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER]:
|
|
CheckInputRowFilterHeaders(fieldnames, GC.Values[GC.CSV_INPUT_ROW_FILTER], GC.Values[GC.CSV_INPUT_ROW_DROP_FILTER])
|
|
choice = Cmd.Current().strip().lower()
|
|
if choice == Cmd.LOOP_CMD:
|
|
usageErrorExit(Msg.NESTED_LOOP_CMD_NOT_ALLOWED)
|
|
# gam loop ... gam redirect|select|config ... process gam.cfg on each iteration
|
|
# gam redirect|select|config ... loop ... gam redirect|select|config ... process gam.cfg on each iteration
|
|
# gam loop ... gam !redirect|select|config ... no further processing of gam.cfg
|
|
# gam redirect|select|config ... loop ... gam !redirect|select|config ... no further processing of gam.cfg
|
|
processGamCfg = choice in Cmd.GAM_META_COMMANDS
|
|
GAM_argv, subFields = getSubFields([Cmd.GAM_CMD], fieldnames)
|
|
multi = GM.Globals[GM.CSVFILE][GM.REDIRECT_MULTIPROCESS]
|
|
if multi:
|
|
mpManager = multiprocessing.Manager()
|
|
mpQueue, mpQueueHandler = initializeCSVFileQueueHandler(mpManager, None, None)
|
|
else:
|
|
mpQueue = None
|
|
GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] = mpQueue
|
|
# Set up command logging at top level only
|
|
if GM.Globals[GM.CMDLOG_LOGGER]:
|
|
LoopGlobals = GM.Globals
|
|
else:
|
|
LoopGlobals = {GM.CMDLOG_LOGGER: None, GM.CMDLOG_HANDLER: None}
|
|
if (GM.Globals[GM.PID] > 0) and GC.Values[GC.CMDLOG]:
|
|
openGAMCommandLog(LoopGlobals, 'looplog')
|
|
if LoopGlobals[GM.CMDLOG_LOGGER]:
|
|
writeGAMCommandLog(LoopGlobals, loopCmd, '*')
|
|
if not showCmds:
|
|
i = 0
|
|
for row in csvFile:
|
|
if checkMatchSkipFields(row, fieldnames, matchFields, skipFields):
|
|
i += 1
|
|
if skipRows:
|
|
if i <= skipRows:
|
|
continue
|
|
i = 1
|
|
skipRows = 0
|
|
item = processSubFields(GAM_argv, row, subFields)
|
|
logCmd = Cmd.QuotedArgumentList(item)
|
|
if i % 100 == 0:
|
|
batchWriteStderr(Msg.PROCESSING_ITEM_N.format(currentISOformatTimeStamp(), i))
|
|
sysRC = ProcessGAMCommand(item, processGamCfg=processGamCfg, inLoop=True)
|
|
if (GM.Globals[GM.PID] > 0) and LoopGlobals[GM.CMDLOG_LOGGER]:
|
|
writeGAMCommandLog(LoopGlobals, logCmd, sysRC)
|
|
if (sysRC > 0) and (GM.Globals[GM.SYSEXITRC] <= HARD_ERROR_RC):
|
|
break
|
|
if maxRows and i >= maxRows:
|
|
break
|
|
closeFile(f)
|
|
else:
|
|
items = []
|
|
i = 0
|
|
for row in csvFile:
|
|
if checkMatchSkipFields(row, fieldnames, matchFields, skipFields):
|
|
i += 1
|
|
if skipRows:
|
|
if i <= skipRows:
|
|
continue
|
|
i = 1
|
|
skipRows = 0
|
|
items.append(processSubFields(GAM_argv, row, subFields))
|
|
if maxRows and i >= maxRows:
|
|
break
|
|
closeFile(f)
|
|
numItems = len(items)
|
|
pid = 0
|
|
for item in items:
|
|
pid += 1
|
|
logCmd = Cmd.QuotedArgumentList(item)
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},Start,0,{logCmd}\n')
|
|
sysRC = ProcessGAMCommand(item, processGamCfg=processGamCfg, inLoop=True)
|
|
batchWriteStderr(f'{currentISOformatTimeStamp()},{pid}/{numItems},End,{sysRC},{logCmd}\n')
|
|
if (GM.Globals[GM.PID] > 0) and LoopGlobals[GM.CMDLOG_LOGGER]:
|
|
writeGAMCommandLog(LoopGlobals, logCmd, sysRC)
|
|
if (sysRC > 0) and (GM.Globals[GM.SYSEXITRC] <= HARD_ERROR_RC):
|
|
break
|
|
if (GM.Globals[GM.PID] > 0) and LoopGlobals[GM.CMDLOG_LOGGER]:
|
|
closeGAMCommandLog(LoopGlobals)
|
|
if multi:
|
|
terminateCSVFileQueueHandler(mpQueue, mpQueueHandler)
|
|
|
|
def _doList(entityList, entityType):
|
|
buildGAPIObject(API.DIRECTORY)
|
|
if GM.Globals[GM.CSV_DATA_DICT]:
|
|
keyField = GM.Globals[GM.CSV_KEY_FIELD]
|
|
dataField = GM.Globals[GM.CSV_DATA_FIELD]
|
|
else:
|
|
keyField = 'Entity'
|
|
dataField = 'Data'
|
|
csvPF = CSVPrintFile(keyField)
|
|
if checkArgumentPresent('todrive'):
|
|
csvPF.GetTodriveParameters()
|
|
if entityList is None:
|
|
entityList = getEntityList(Cmd.OB_ENTITY)
|
|
showData = checkArgumentPresent('data')
|
|
if showData:
|
|
if not entityType:
|
|
itemType, itemList = getEntityToModify(crosAllowed=True)
|
|
else:
|
|
itemType = None
|
|
itemList = getEntityList(Cmd.OB_ENTITY)
|
|
entityItemLists = itemList if isinstance(itemList, dict) else None
|
|
csvPF.AddTitle(dataField)
|
|
else:
|
|
entityItemLists = None
|
|
dataDelimiter = getDelimiter()
|
|
checkForExtraneousArguments()
|
|
_, _, entityList = getEntityArgument(entityList)
|
|
for entity in entityList:
|
|
entityEmail = normalizeEmailAddressOrUID(entity)
|
|
if showData:
|
|
if entityItemLists:
|
|
if entity not in entityItemLists:
|
|
csvPF.WriteRow({keyField: entityEmail})
|
|
continue
|
|
itemList = entityItemLists[entity]
|
|
if itemType == Cmd.ENTITY_USERS:
|
|
for i, item in enumerate(itemList):
|
|
itemList[i] = normalizeEmailAddressOrUID(item)
|
|
if dataDelimiter:
|
|
csvPF.WriteRow({keyField: entityEmail, dataField: dataDelimiter.join(itemList)})
|
|
else:
|
|
for item in itemList:
|
|
csvPF.WriteRow({keyField: entityEmail, dataField: item})
|
|
else:
|
|
csvPF.WriteRow({keyField: entityEmail})
|
|
csvPF.writeCSVfile('Entity')
|
|
|
|
# gam list [todrive <ToDriveAttribute>*] <EntityList> [data <CrOSTypeEntity>|<UserTypeEntity> [delimiter <Character>]]
|
|
def doListType():
|
|
_doList(None, None)
|
|
|
|
# gam <CrOSTypeEntity> list [todrive <ToDriveAttribute>*] [data <EntityList> [delimiter <Character>]]
|
|
def doListCrOS(entityList):
|
|
_doList(entityList, Cmd.ENTITY_CROS)
|
|
|
|
# gam <UserTypeEntity> list [todrive <ToDriveAttribute>*] [data <EntityList> [delimiter <Character>]]
|
|
def doListUser(entityList):
|
|
_doList(entityList, Cmd.ENTITY_USERS)
|
|
|
|
def _showCount(entityList, entityType):
|
|
buildGAPIObject(API.DIRECTORY)
|
|
checkForExtraneousArguments()
|
|
_, count, entityList = getEntityArgument(entityList)
|
|
actionPerformedNumItems(count, entityType)
|
|
|
|
# gam <CrOSTypeEntity> show count
|
|
def showCountCrOS(entityList):
|
|
_showCount(entityList, Ent.CHROME_DEVICE)
|
|
|
|
# gam <UserTypeEntity> show count
|
|
def showCountUser(entityList):
|
|
_showCount(entityList, Ent.USER)
|
|
|
|
VALIDEMAIL_PATTERN = re.compile(r'^[^@]+@[^@]+\.[^@]+$')
|
|
|
|
def _getValidateLoginHint(login_hint, projectId=None):
|
|
while True:
|
|
if not login_hint:
|
|
if not projectId:
|
|
login_hint = readStdin(Msg.ENTER_GSUITE_ADMIN_EMAIL_ADDRESS).strip()
|
|
else:
|
|
login_hint = readStdin(Msg.ENTER_MANAGE_GCP_PROJECT_EMAIL_ADDRESS.format(projectId)).strip()
|
|
if login_hint.find('@') == -1 and GC.Values[GC.DOMAIN]:
|
|
login_hint = f'{login_hint}@{GC.Values[GC.DOMAIN]}'
|
|
if VALIDEMAIL_PATTERN.match(login_hint):
|
|
return login_hint
|
|
sys.stdout.write(f'{ERROR_PREFIX}Invalid email address: {login_hint}\n')
|
|
login_hint = None
|
|
|
|
def getOAuthClientIDAndSecret():
|
|
cs_data = readFile(GC.Values[GC.CLIENT_SECRETS_JSON], continueOnError=True, displayError=True)
|
|
if not cs_data:
|
|
invalidClientSecretsJsonExit(Msg.NO_DATA)
|
|
try:
|
|
cs_json = json.loads(cs_data)
|
|
if not cs_json:
|
|
systemErrorExit(CLIENT_SECRETS_JSON_REQUIRED_RC, Msg.NO_CLIENT_ACCESS_CREATE_UPDATE_ALLOWED)
|
|
return (cs_json['installed']['client_id'], cs_json['installed']['client_secret'])
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
invalidClientSecretsJsonExit(str(e))
|
|
|
|
def getScopesFromUser(scopesList, clientAccess, currentScopes=None):
|
|
OAUTH2_CMDS = ['s', 'u', 'e', 'c']
|
|
oauth2_menu = ''
|
|
numScopes = len(scopesList)
|
|
for a_scope in scopesList:
|
|
oauth2_menu += f"[%%s] %2d) {a_scope['name']}"
|
|
if a_scope['subscopes']:
|
|
oauth2_menu += f' (supports {" and ".join(a_scope["subscopes"])})'
|
|
oauth2_menu += '\n'
|
|
oauth2_menu += '''
|
|
Select an unselected scope [ ] by entering a number; yields [*]
|
|
For scopes that support readonly, enter a number and an 'r' to grant read-only access; yields [R]
|
|
For scopes that support action, enter a number and an 'a' to grant action-only access; yields [A]
|
|
Clear read-only access [R] or action-only access [A] from a scope by entering a number; yields [*]
|
|
Unselect a selected scope [*] by entering a number; yields [ ]
|
|
Select all default scopes by entering an 's'; yields [*] for default scopes, [ ] for others
|
|
Unselect all scopes by entering a 'u'; yields [ ] for all scopes
|
|
Exit without changes/authorization by entering an 'e'
|
|
Continue to authorization by entering a 'c'
|
|
'''
|
|
if clientAccess:
|
|
oauth2_menu += ''' Note, if all scopes are selected, Google will probably generate an authorization error
|
|
'''
|
|
menu = oauth2_menu % tuple(range(numScopes))
|
|
selectedScopes = ['*'] * numScopes
|
|
if currentScopes is None and clientAccess:
|
|
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK])
|
|
with lock:
|
|
_, credentials = getOauth2TxtCredentials(exitOnError=False)
|
|
if credentials and credentials.scopes is not None:
|
|
currentScopes = sorted(credentials.scopes)
|
|
if currentScopes is not None:
|
|
if clientAccess:
|
|
i = 0
|
|
for a_scope in scopesList:
|
|
selectedScopes[i] = ' '
|
|
possibleScope = a_scope['scope']
|
|
for currentScope in currentScopes:
|
|
if currentScope == possibleScope:
|
|
selectedScopes[i] = '*'
|
|
break
|
|
if 'readonly' in a_scope['subscopes']:
|
|
if currentScope == possibleScope+'.readonly':
|
|
selectedScopes[i] = 'R'
|
|
break
|
|
if 'action' in a_scope['subscopes']:
|
|
if currentScope == possibleScope+'.action':
|
|
selectedScopes[i] = 'A'
|
|
break
|
|
i += 1
|
|
else:
|
|
i = 0
|
|
for a_scope in scopesList:
|
|
selectedScopes[i] = ' '
|
|
api = a_scope['api']
|
|
possibleScope = a_scope['scope']
|
|
if api in currentScopes:
|
|
for scope in currentScopes[api]:
|
|
if scope == possibleScope:
|
|
selectedScopes[i] = '*'
|
|
break
|
|
if 'readonly' in a_scope['subscopes']:
|
|
if (scope == possibleScope+'.readonly') or (scope == a_scope.get('roscope')):
|
|
selectedScopes[i] = 'R'
|
|
break
|
|
i += 1
|
|
else:
|
|
i = 0
|
|
for a_scope in scopesList:
|
|
if a_scope.get('offByDefault'):
|
|
selectedScopes[i] = ' '
|
|
elif a_scope.get('roByDefault'):
|
|
selectedScopes[i] = 'R'
|
|
else:
|
|
selectedScopes[i] = '*'
|
|
i += 1
|
|
prompt = f'\nPlease enter 0-{numScopes-1}[a|r] or {"|".join(OAUTH2_CMDS)}: '
|
|
while True:
|
|
os.system(['clear', 'cls'][sys.platform.startswith('win')])
|
|
sys.stdout.write(menu % tuple(selectedScopes))
|
|
while True:
|
|
choice = readStdin(prompt)
|
|
if choice:
|
|
selection = choice.lower()
|
|
if selection.find('r') >= 0:
|
|
mode = 'R'
|
|
selection = selection.replace('r', '')
|
|
elif selection.find('a') >= 0:
|
|
mode = 'A'
|
|
selection = selection.replace('a', '')
|
|
else:
|
|
mode = ' '
|
|
if selection and selection.isdigit():
|
|
selection = int(selection)
|
|
if isinstance(selection, int) and selection < numScopes:
|
|
if mode == 'R':
|
|
if 'readonly' not in scopesList[selection]['subscopes']:
|
|
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support read-only mode!\n')
|
|
continue
|
|
elif mode == 'A':
|
|
if 'action' not in scopesList[selection]['subscopes']:
|
|
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support action-only mode!\n')
|
|
continue
|
|
elif selectedScopes[selection] != '*':
|
|
mode = '*'
|
|
else:
|
|
mode = ' '
|
|
selectedScopes[selection] = mode
|
|
break
|
|
if isinstance(selection, str) and selection in OAUTH2_CMDS:
|
|
if selection == 's':
|
|
i = 0
|
|
for a_scope in scopesList:
|
|
selectedScopes[i] = ' ' if a_scope.get('offByDefault', False) else '*'
|
|
i += 1
|
|
elif selection == 'u':
|
|
for i in range(numScopes):
|
|
selectedScopes[i] = ' '
|
|
elif selection == 'e':
|
|
return None
|
|
break
|
|
sys.stdout.write(f'{ERROR_PREFIX}Invalid input "{choice}"\n')
|
|
if selection == 'c':
|
|
break
|
|
return selectedScopes
|
|
|
|
def _localhost_to_ip():
|
|
'''returns IPv4 or IPv6 loopback address which localhost resolves to.
|
|
If localhost does not resolve to valid loopback IP address then returns
|
|
127.0.0.1'''
|
|
# TODO gethostbyname() will only ever return ipv4
|
|
# find a way to support IPv6 here and get preferred IP
|
|
# note that IPv6 may be broken on some systems also :-(
|
|
# for now IPv4 should do.
|
|
local_ip = socket.gethostbyname('localhost')
|
|
# local_ip = socket.getaddrinfo('localhost', None)[0][-1][0] # works with ipv6, makes wsgiref fail
|
|
if not ipaddress.ip_address(local_ip).is_loopback:
|
|
local_ip = '127.0.0.1'
|
|
return local_ip
|
|
|
|
def _waitForHttpClient(d):
|
|
wsgi_app = google_auth_oauthlib.flow._RedirectWSGIApp(Msg.AUTHENTICATION_FLOW_COMPLETE_CLOSE_BROWSER.format(GAM))
|
|
wsgiref.simple_server.WSGIServer.allow_reuse_address = False
|
|
# Convert hostname to IP since apparently binding to the IP
|
|
# reduces odds of firewall blocking us
|
|
local_ip = _localhost_to_ip()
|
|
for port in range(8080, 8099):
|
|
try:
|
|
local_server = wsgiref.simple_server.make_server(
|
|
local_ip,
|
|
port,
|
|
wsgi_app,
|
|
handler_class=wsgiref.simple_server.WSGIRequestHandler
|
|
)
|
|
break
|
|
except OSError:
|
|
pass
|
|
redirect_uri_format = "http://{}:{}/" if d['trailing_slash'] else "http://{}:{}"
|
|
# provide redirect_uri to main process so it can formulate auth_url
|
|
d['redirect_uri'] = redirect_uri_format.format(*local_server.server_address)
|
|
# wait until main process provides auth_url
|
|
# so we can open it in web browser.
|
|
while 'auth_url' not in d:
|
|
time.sleep(0.1)
|
|
if d['open_browser']:
|
|
webbrowser.open(d['auth_url'], new=1, autoraise=True)
|
|
try:
|
|
local_server.handle_request()
|
|
authorization_response = wsgi_app.last_request_uri.replace("http", "https")
|
|
d['code'] = authorization_response
|
|
except:
|
|
pass
|
|
local_server.server_close()
|
|
|
|
def _waitForUserInput(d):
|
|
sys.stdin = open(0, DEFAULT_FILE_READ_MODE, encoding=UTF8)
|
|
d['code'] = readStdin(Msg.ENTER_VERIFICATION_CODE_OR_URL)
|
|
|
|
class _GamOauthFlow(google_auth_oauthlib.flow.InstalledAppFlow):
|
|
def run_dual(self, **kwargs):
|
|
mgr = multiprocessing.Manager()
|
|
d = mgr.dict()
|
|
d['trailing_slash'] = True
|
|
d['open_browser'] = not GC.Values[GC.NO_BROWSER]
|
|
httpClientProcess = multiprocessing.Process(target=_waitForHttpClient, args=(d,))
|
|
userInputProcess = multiprocessing.Process(target=_waitForUserInput, args=(d,))
|
|
httpClientProcess.start()
|
|
# we need to wait until web server starts on avail port
|
|
# so we know redirect_uri to use
|
|
while 'redirect_uri' not in d:
|
|
time.sleep(0.1)
|
|
self.redirect_uri = d['redirect_uri']
|
|
d['auth_url'], _ = super().authorization_url(**kwargs)
|
|
d['auth_url'] = shortenURL(d['auth_url'])
|
|
print(Msg.OAUTH2_GO_TO_LINK_MESSAGE.format(url=d['auth_url']))
|
|
userInputProcess.start()
|
|
userInput = False
|
|
checkHttp = checkUser = True
|
|
alive = 2
|
|
while alive > 0:
|
|
time.sleep(0.1)
|
|
if checkHttp and not httpClientProcess.is_alive():
|
|
if 'code' in d:
|
|
if checkUser:
|
|
userInputProcess.terminate()
|
|
break
|
|
checkHttp = False
|
|
alive -= 1
|
|
if checkUser and not userInputProcess.is_alive():
|
|
userInput = True
|
|
if 'code' in d:
|
|
if checkHttp:
|
|
httpClientProcess.terminate()
|
|
break
|
|
checkUser = False
|
|
alive -= 1
|
|
if 'code' not in d:
|
|
systemErrorExit(SYSTEM_ERROR_RC, Msg.AUTHENTICATION_FLOW_FAILED)
|
|
while True:
|
|
code = d['code']
|
|
if code.startswith('http'):
|
|
parsed_url = urlparse(code)
|
|
parsed_params = parse_qs(parsed_url.query)
|
|
code = parsed_params.get('code', [None])[0]
|
|
try:
|
|
fetch_args = {'code': code}
|
|
if GC.Values[GC.CACERTS_PEM]:
|
|
fetch_args['verify'] = GC.Values[GC.CACERTS_PEM]
|
|
self.fetch_token(**fetch_args)
|
|
break
|
|
except Exception as e:
|
|
if not userInput:
|
|
systemErrorExit(INVALID_TOKEN_RC, str(e))
|
|
stderrErrorMsg(str(e))
|
|
_waitForUserInput(d)
|
|
print(Msg.AUTHENTICATION_FLOW_COMPLETE)
|
|
return self.credentials
|
|
|
|
class Credentials(google.oauth2.credentials.Credentials):
|
|
"""Google OAuth2.0 Credentials with GAM-specific properties and methods."""
|
|
|
|
def __init__(self,
|
|
token,
|
|
refresh_token=None,
|
|
id_token=None,
|
|
token_uri=None,
|
|
client_id=None,
|
|
client_secret=None,
|
|
scopes=None,
|
|
quota_project_id=None,
|
|
expiry=None,
|
|
id_token_data=None,
|
|
filename=None):
|
|
"""A thread-safe OAuth2.0 credentials object.
|
|
|
|
Credentials adds additional utility properties and methods to a
|
|
standard OAuth2.0 credentials object. When used to store credentials on
|
|
disk, it implements a file lock to avoid collision during writes.
|
|
|
|
Args:
|
|
token: Optional String, The OAuth 2.0 access token. Can be None if refresh
|
|
information is provided.
|
|
refresh_token: String, The OAuth 2.0 refresh token. If specified,
|
|
credentials can be refreshed.
|
|
id_token: String, The Open ID Connect ID Token.
|
|
token_uri: String, The OAuth 2.0 authorization server's token endpoint
|
|
URI. Must be specified for refresh, can be left as None if the token can
|
|
not be refreshed.
|
|
client_id: String, The OAuth 2.0 client ID. Must be specified for refresh,
|
|
can be left as None if the token can not be refreshed.
|
|
client_secret: String, The OAuth 2.0 client secret. Must be specified for
|
|
refresh, can be left as None if the token can not be refreshed.
|
|
scopes: Sequence[str], The scopes used to obtain authorization.
|
|
This parameter is used by :meth:`has_scopes`. OAuth 2.0 credentials can
|
|
not request additional scopes after authorization. The scopes must be
|
|
derivable from the refresh token if refresh information is provided
|
|
(e.g. The refresh token scopes are a superset of this or contain a
|
|
wild card scope like
|
|
'https://www.googleapis.com/auth/any-api').
|
|
quota_project_id: String, The project ID used for quota and billing. This
|
|
project may be different from the project used to create the
|
|
credentials.
|
|
expiry: datetime.datetime, The time at which the provided token will
|
|
expire.
|
|
id_token_data: Oauth2.0 ID Token data which was previously fetched for
|
|
this access token against the google.oauth2.id_token library.
|
|
filename: String, Path to a file that will be used to store the
|
|
credentials. If provided, a lock file of the same name and a ".lock"
|
|
extension will be created for concurrency controls. Note: New
|
|
credentials are not saved to disk until write() or refresh() are
|
|
called.
|
|
|
|
Raises:
|
|
TypeError: If id_token_data is not the required dict type.
|
|
"""
|
|
super().__init__(token=token,
|
|
refresh_token=refresh_token,
|
|
id_token=id_token,
|
|
token_uri=token_uri,
|
|
client_id=client_id,
|
|
client_secret=client_secret,
|
|
scopes=scopes,
|
|
quota_project_id=quota_project_id)
|
|
|
|
# Load data not restored by the super class
|
|
self.expiry = expiry
|
|
if id_token_data and not isinstance(id_token_data, dict):
|
|
raise TypeError(f'Expected type id_token_data dict but received {type(id_token_data)}')
|
|
self._id_token_data = id_token_data.copy() if id_token_data else None
|
|
|
|
# If a filename is provided, use a lock file to control concurrent access
|
|
# to the resource. If no filename is provided, use a thread lock that has
|
|
# the same interface as FileLock in order to simplify the implementation.
|
|
if filename:
|
|
# Convert relative paths into absolute
|
|
self._filename = os.path.abspath(filename)
|
|
else:
|
|
self._filename = None
|
|
|
|
# Use a property to prevent external mutation of the filename.
|
|
@property
|
|
def filename(self):
|
|
return self._filename
|
|
|
|
@classmethod
|
|
def from_authorized_user_info_gam(cls, info, filename=None):
|
|
"""Generates Credentials from JSON containing authorized user info.
|
|
|
|
Args:
|
|
info: Dict, authorized user info in Google format.
|
|
filename: String, the filename used to store these credentials on disk. If
|
|
no filename is provided, the credentials will not be saved to disk.
|
|
|
|
Raises:
|
|
ValueError: If missing fields are detected in the info.
|
|
"""
|
|
# We need all of these keys
|
|
keys_needed = {'client_id', 'client_secret'}
|
|
# We need 1 or more of these keys
|
|
keys_need_one_of = {'refresh_token', 'auth_token', 'token'}
|
|
missing = keys_needed.difference(info.keys())
|
|
has_one_of = set(info) & keys_need_one_of
|
|
if missing or not has_one_of:
|
|
raise ValueError(
|
|
'Authorized user info was not in the expected format, missing '
|
|
f'fields {", ".join(missing)} and one of {", ".join(keys_need_one_of)}.')
|
|
|
|
expiry = info.get('token_expiry')
|
|
if expiry:
|
|
# Convert the raw expiry to datetime
|
|
expiry = datetime.datetime.strptime(expiry, YYYYMMDDTHHMMSSZ_FORMAT)
|
|
id_token_data = info.get('decoded_id_token')
|
|
|
|
# Provide backwards compatibility with field names when loading from JSON.
|
|
# Some field names may be different, depending on when/how the credentials
|
|
# were pickled.
|
|
return cls(token=info.get('token', info.get('auth_token', '')),
|
|
refresh_token=info.get('refresh_token', ''),
|
|
id_token=info.get('id_token_jwt', info.get('id_token')),
|
|
token_uri=info.get('token_uri'),
|
|
client_id=info['client_id'],
|
|
client_secret=info['client_secret'],
|
|
scopes=info.get('scopes'),
|
|
quota_project_id=info.get('quota_project_id'),
|
|
expiry=expiry,
|
|
id_token_data=id_token_data,
|
|
filename=filename)
|
|
|
|
@classmethod
|
|
def from_google_oauth2_credentials(cls, credentials, filename=None):
|
|
"""Generates Credentials from a google.oauth2.Credentials object."""
|
|
info = json.loads(credentials.to_json())
|
|
# Add properties which are not exported with the native to_json() output.
|
|
info['id_token'] = credentials.id_token
|
|
if credentials.expiry:
|
|
info['token_expiry'] = credentials.expiry.strftime(YYYYMMDDTHHMMSSZ_FORMAT)
|
|
info['quota_project_id'] = credentials.quota_project_id
|
|
|
|
return cls.from_authorized_user_info_gam(info, filename=filename)
|
|
|
|
@classmethod
|
|
def from_client_secrets(cls,
|
|
client_id,
|
|
client_secret,
|
|
scopes,
|
|
access_type='offline',
|
|
login_hint=None,
|
|
filename=None,
|
|
open_browser=True):
|
|
"""Runs an OAuth Flow from client secrets to generate credentials.
|
|
|
|
Args:
|
|
client_id: String, The OAuth2.0 Client ID.
|
|
client_secret: String, The OAuth2.0 Client Secret.
|
|
scopes: Sequence[str], A list of scopes to include in the credentials.
|
|
access_type: String, 'offline' or 'online'. Indicates whether your
|
|
application can refresh access tokens when the user is not present at
|
|
the browser. Valid parameter values are online, which is the default
|
|
value, and offline. Set the value to offline if your application needs
|
|
to refresh access tokens when the user is not present at the browser.
|
|
This is the method of refreshing access tokens described later in this
|
|
document. This value instructs the Google authorization server to return
|
|
a refresh token and an access token the first time that your application
|
|
exchanges an authorization code for tokens.
|
|
login_hint: String, The email address that will be displayed on the Google
|
|
login page as a hint for the user to login to the correct account.
|
|
filename: String, the path to a file to use to save the credentials.
|
|
open_browser: Boolean: whether or not GAM should try to open the browser
|
|
automatically.
|
|
|
|
Returns:
|
|
Credentials
|
|
"""
|
|
client_config = {
|
|
'installed': {
|
|
'client_id': client_id,
|
|
'client_secret': client_secret,
|
|
'redirect_uris': ['http://localhost'],
|
|
'auth_uri': API.GOOGLE_OAUTH2_ENDPOINT,
|
|
'token_uri': API.GOOGLE_OAUTH2_TOKEN_ENDPOINT,
|
|
}
|
|
}
|
|
|
|
flow = _GamOauthFlow.from_client_config(client_config,
|
|
scopes,
|
|
autogenerate_code_verifier=True)
|
|
flow_kwargs = {'access_type': access_type,
|
|
'open_browser': open_browser}
|
|
if login_hint:
|
|
flow_kwargs['login_hint'] = login_hint
|
|
flow.run_dual(**flow_kwargs)
|
|
return cls.from_google_oauth2_credentials(flow.credentials, filename=filename)
|
|
|
|
def to_json(self, strip=None):
|
|
"""Creates a JSON representation of a Credentials.
|
|
|
|
Args:
|
|
strip: Sequence[str], Optional list of members to exclude from the
|
|
generated JSON.
|
|
|
|
Returns:
|
|
str: A JSON representation of this instance, suitable to pass to
|
|
from_json().
|
|
"""
|
|
expiry = self.expiry.strftime(YYYYMMDDTHHMMSSZ_FORMAT) if self.expiry else None
|
|
prep = {
|
|
'token': self.token,
|
|
'refresh_token': self.refresh_token,
|
|
'token_uri': self.token_uri,
|
|
'client_id': self.client_id,
|
|
'client_secret': self.client_secret,
|
|
'id_token': self.id_token,
|
|
# Google auth doesn't currently give us scopes back on refresh.
|
|
# 'scopes': sorted(self.scopes),
|
|
'token_expiry': expiry,
|
|
'decoded_id_token': self._id_token_data,
|
|
}
|
|
|
|
# Remove empty entries
|
|
prep = {k: v for k, v in prep.items() if v is not None}
|
|
|
|
# Remove entries that explicitly need to be removed
|
|
if strip is not None:
|
|
prep = {k: v for k, v in prep.items() if k not in strip}
|
|
|
|
return json.dumps(prep, indent=2, sort_keys=True)
|
|
|
|
def doOAuthRequest(currentScopes, login_hint, verifyScopes=False):
|
|
client_id, client_secret = getOAuthClientIDAndSecret()
|
|
scopesList = API.getClientScopesList(GC.Values[GC.TODRIVE_CLIENTACCESS])
|
|
if not currentScopes or verifyScopes:
|
|
selectedScopes = getScopesFromUser(scopesList, True, currentScopes)
|
|
if selectedScopes is None:
|
|
return False
|
|
scopes = set(API.REQUIRED_SCOPES)
|
|
i = 0
|
|
for scope in scopesList:
|
|
if selectedScopes[i] == '*':
|
|
if scope['scope']:
|
|
scopes.add(scope['scope'])
|
|
elif selectedScopes[i] == 'R':
|
|
scopes.add(f'{scope["scope"]}.readonly')
|
|
elif selectedScopes[i] == 'A':
|
|
scopes.add(f'{scope["scope"]}.action')
|
|
i += 1
|
|
else:
|
|
scopes = set(currentScopes+API.REQUIRED_SCOPES)
|
|
if API.STORAGE_READWRITE_SCOPE in scopes:
|
|
scopes.discard(API.STORAGE_READONLY_SCOPE)
|
|
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'
|
|
credentials = Credentials.from_client_secrets(
|
|
client_id,
|
|
client_secret,
|
|
scopes=list(scopes),
|
|
access_type='offline',
|
|
login_hint=login_hint,
|
|
open_browser=not GC.Values[GC.NO_BROWSER])
|
|
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK])
|
|
with lock:
|
|
writeClientCredentials(credentials, GC.Values[GC.OAUTH2_TXT])
|
|
entityActionPerformed([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]])
|
|
return True
|
|
|
|
# gam oauth|oauth2 create|request [<EmailAddress>]
|
|
# gam oauth|oauth2 create|request [admin <EmailAddress>] [scope|scopes <APIScopeURLList>]
|
|
def doOAuthCreate():
|
|
if not Cmd.PeekArgumentPresent(['admin', 'scope', 'scopes']):
|
|
login_hint = getEmailAddress(noUid=True, optional=True)
|
|
scopes = None
|
|
checkForExtraneousArguments()
|
|
else:
|
|
login_hint = None
|
|
scopes = []
|
|
scopesList = API.getClientScopesList(GC.Values[GC.TODRIVE_CLIENTACCESS])
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'admin':
|
|
login_hint = getEmailAddress(noUid=True)
|
|
elif myarg in {'scope', 'scopes'}:
|
|
for uscope in getString(Cmd.OB_API_SCOPE_URL_LIST).lower().replace(',', ' ').split():
|
|
if uscope in {'openid', 'email', API.USERINFO_EMAIL_SCOPE, 'profile', API.USERINFO_PROFILE_SCOPE}:
|
|
continue
|
|
for scope in scopesList:
|
|
if ((uscope == scope['scope']) or
|
|
(uscope.endswith('.action') and 'action' in scope['subscopes']) or
|
|
(uscope.endswith('.readonly') and 'readonly' in scope['subscopes'])):
|
|
scopes.append(uscope)
|
|
break
|
|
else:
|
|
invalidChoiceExit(uscope, API.getClientScopesURLs(GC.Values[GC.TODRIVE_CLIENTACCESS]), True)
|
|
else:
|
|
unknownArgumentExit()
|
|
if len(scopes) == 0:
|
|
scopes = None
|
|
doOAuthRequest(scopes, login_hint)
|
|
|
|
def exitIfNoOauth2Txt():
|
|
if not os.path.isfile(GC.Values[GC.OAUTH2_TXT]):
|
|
entityActionNotPerformedWarning([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], Msg.DOES_NOT_EXIST)
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
|
|
# gam oauth|oauth2 delete|revoke
|
|
def doOAuthDelete():
|
|
checkForExtraneousArguments()
|
|
exitIfNoOauth2Txt()
|
|
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK], timeout=10)
|
|
with lock:
|
|
_, credentials = getOauth2TxtCredentials(noScopes=True)
|
|
if not credentials:
|
|
return
|
|
entityType = Ent.OAUTH2_TXT_FILE
|
|
entityName = GC.Values[GC.OAUTH2_TXT]
|
|
sys.stdout.write(f'{Ent.Singular(entityType)}: {entityName}, will be Deleted in 3...')
|
|
sys.stdout.flush()
|
|
time.sleep(1)
|
|
sys.stdout.write('2...')
|
|
sys.stdout.flush()
|
|
time.sleep(1)
|
|
sys.stdout.write('1...')
|
|
sys.stdout.flush()
|
|
time.sleep(1)
|
|
sys.stdout.write('boom!\n')
|
|
sys.stdout.flush()
|
|
httpObj = getHttpObj()
|
|
params = {'token': credentials.refresh_token}
|
|
revoke_uri = f'https://accounts.google.com/o/oauth2/revoke?{urlencode(params)}'
|
|
httpObj.request(revoke_uri, 'GET')
|
|
deleteFile(GC.Values[GC.OAUTH2_TXT], continueOnError=True)
|
|
entityActionPerformed([entityType, entityName])
|
|
|
|
# gam oauth|oauth2 info|verify [showsecret] [accesstoken <AccessToken> idtoken <IDToken>] [showdetails]
|
|
def doOAuthInfo():
|
|
credentials = access_token = id_token = None
|
|
showDetails = showSecret = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'accesstoken':
|
|
access_token = getString(Cmd.OB_ACCESS_TOKEN)
|
|
elif myarg == 'idtoken':
|
|
id_token = getString(Cmd.OB_ID_TOKEN)
|
|
elif myarg == 'showdetails':
|
|
showDetails = True
|
|
elif myarg == 'showsecret':
|
|
showSecret = True
|
|
else:
|
|
unknownArgumentExit()
|
|
exitIfNoOauth2Txt()
|
|
if not access_token and not id_token:
|
|
credentials = getClientCredentials(noScopes=True)
|
|
access_token = credentials.token
|
|
printEntity([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]])
|
|
oa2 = buildGAPIObject(API.OAUTH2)
|
|
try:
|
|
token_info = callGAPI(oa2, 'tokeninfo',
|
|
throwReasons=[GAPI.INVALID],
|
|
access_token=access_token, id_token=id_token)
|
|
except GAPI.invalid as e:
|
|
entityActionFailedExit([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], str(e))
|
|
if 'issued_to' in token_info:
|
|
printKeyValueList(['Client ID', token_info['issued_to']])
|
|
if credentials is not None and showSecret:
|
|
printKeyValueList(['Secret', credentials.client_secret])
|
|
if 'scope' in token_info:
|
|
scopes = token_info['scope'].split(' ')
|
|
printKeyValueList(['Scopes', len(scopes)])
|
|
Ind.Increment()
|
|
for scope in sorted(scopes):
|
|
printKeyValueList([scope])
|
|
Ind.Decrement()
|
|
if 'email' in token_info:
|
|
printKeyValueList(['Google Workspace Admin', f'{token_info["email"]}'])
|
|
if 'expires_in' in token_info:
|
|
printKeyValueList(['Expires', ISOformatTimeStamp((datetime.datetime.now()+datetime.timedelta(seconds=token_info['expires_in'])).replace(tzinfo=GC.Values[GC.TIMEZONE]))])
|
|
if showDetails:
|
|
for k, v in sorted(iter(token_info.items())):
|
|
if k not in ['email', 'expires_in', 'issued_to', 'scope']:
|
|
printKeyValueList([k, v])
|
|
printBlankLine()
|
|
|
|
# gam oauth|oauth2 update [<EmailAddress>]
|
|
# gam oauth|oauth2 update [admin <EmailAddress>]
|
|
def doOAuthUpdate():
|
|
if Cmd.PeekArgumentPresent(['admin']):
|
|
Cmd.Advance()
|
|
login_hint = getEmailAddress(noUid=True)
|
|
else:
|
|
login_hint = getEmailAddress(noUid=True, optional=True)
|
|
checkForExtraneousArguments()
|
|
exitIfNoOauth2Txt()
|
|
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK])
|
|
with lock:
|
|
jsonData = readFile(GC.Values[GC.OAUTH2_TXT], continueOnError=True, displayError=False)
|
|
if not jsonData:
|
|
invalidOauth2TxtExit(Msg.NO_DATA)
|
|
try:
|
|
jsonDict = json.loads(jsonData)
|
|
if 'client_id' in jsonDict:
|
|
if 'scopes' in jsonDict:
|
|
currentScopes = jsonDict['scopes']
|
|
else:
|
|
currentScopes = API.getClientScopesURLs(GC.Values[GC.TODRIVE_CLIENTACCESS])
|
|
else:
|
|
currentScopes = []
|
|
except (AttributeError, IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
invalidOauth2TxtExit(str(e))
|
|
if not doOAuthRequest(currentScopes, login_hint, verifyScopes=True):
|
|
entityActionNotPerformedWarning([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], Msg.USER_CANCELLED)
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
|
|
# gam oauth|oauth2 refresh
|
|
def doOAuthRefresh():
|
|
checkForExtraneousArguments()
|
|
exitIfNoOauth2Txt()
|
|
getClientCredentials(forceRefresh=True, forceWrite=True, filename=GC.Values[GC.OAUTH2_TXT], refreshOnly=True)
|
|
entityActionPerformed([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]])
|
|
|
|
# gam oauth|oauth2 export [<FileName>]
|
|
def doOAuthExport():
|
|
if Cmd.ArgumentsRemaining():
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
checkForExtraneousArguments()
|
|
else:
|
|
filename = GC.Values[GC.OAUTH2_TXT]
|
|
getClientCredentials(forceRefresh=True, forceWrite=True, filename=filename, refreshOnly=True)
|
|
if filename != '-':
|
|
entityModifierNewValueActionPerformed([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], Act.MODIFIER_TO, filename)
|
|
|
|
def getCRMService(login_hint):
|
|
scopes = [API.CLOUD_PLATFORM_SCOPE]
|
|
client_id = GAM_PROJECT_CREATION_CLIENT_ID
|
|
client_secret = 'qM3dP8f_4qedwzWQE1VR4zzU'
|
|
credentials = Credentials.from_client_secrets(
|
|
client_id,
|
|
client_secret,
|
|
scopes=scopes,
|
|
access_type='online',
|
|
login_hint=login_hint,
|
|
open_browser=not GC.Values[GC.NO_BROWSER])
|
|
httpObj = transportAuthorizedHttp(credentials, http=getHttpObj())
|
|
return (httpObj, getAPIService(API.CLOUDRESOURCEMANAGER, httpObj))
|
|
|
|
def enableGAMProjectAPIs(httpObj, projectId, login_hint, checkEnabled, i=0, count=0):
|
|
apis = API.PROJECT_APIS[:]
|
|
projectName = f'projects/{projectId}'
|
|
serveu = getAPIService(API.SERVICEUSAGE, httpObj)
|
|
status = True
|
|
if checkEnabled:
|
|
try:
|
|
services = callGAPIpages(serveu.services(), 'list', 'services',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
|
|
parent=projectName, filter='state:ENABLED',
|
|
fields='nextPageToken,services(name)')
|
|
Act.Set(Act.CHECK)
|
|
jcount = len(services)
|
|
entityPerformActionNumItems([Ent.PROJECT, projectId], jcount, Ent.API, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for service in sorted(services, key=lambda k: k['name']):
|
|
j += 1
|
|
if 'name' in service:
|
|
serviceName = service['name'].split('/')[-1]
|
|
if serviceName in apis:
|
|
printEntityKVList([Ent.API, serviceName], ['Already enabled'], j, jcount)
|
|
apis.remove(serviceName)
|
|
else:
|
|
printEntityKVList([Ent.API, serviceName], ['Already enabled (non-GAM which is fine)'], j, jcount)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId], str(e), i, count)
|
|
status = False
|
|
jcount = len(apis)
|
|
if status and jcount > 0:
|
|
Act.Set(Act.ENABLE)
|
|
entityPerformActionNumItems([Ent.PROJECT, projectId], jcount, Ent.API, i, count)
|
|
failed = 0
|
|
Ind.Increment()
|
|
j = 0
|
|
for api in apis:
|
|
j += 1
|
|
serviceName = f'projects/{projectId}/services/{api}'
|
|
while True:
|
|
try:
|
|
callGAPI(serveu.services(), 'enable',
|
|
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
retryReasons=[GAPI.INTERNAL_ERROR],
|
|
name=serviceName)
|
|
entityActionPerformed([Ent.API, api], j, jcount)
|
|
break
|
|
except GAPI.failedPrecondition as e:
|
|
entityActionFailedWarning([Ent.API, api], str(e), j, jcount)
|
|
readStdin(Msg.ACCEPT_CLOUD_TOS.format(login_hint))
|
|
except (GAPI.forbidden, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.API, api], str(e), j, jcount)
|
|
failed += 1
|
|
break
|
|
Ind.Decrement()
|
|
if not checkEnabled:
|
|
status = failed <= 2
|
|
else:
|
|
status = failed == 0
|
|
return status
|
|
|
|
# gam enable apis [auto|manual]
|
|
def doEnableAPIs():
|
|
automatic = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'auto':
|
|
automatic = True
|
|
elif myarg == 'manual':
|
|
automatic = False
|
|
else:
|
|
unknownArgumentExit()
|
|
request = getTLSv1_2Request()
|
|
try:
|
|
_, projectId = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
|
|
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError):
|
|
projectId = readStdin(Msg.WHAT_IS_YOUR_PROJECT_ID).strip()
|
|
while automatic is None:
|
|
a_or_m = readStdin(Msg.ENABLE_PROJECT_APIS_AUTOMATICALLY_OR_MANUALLY).strip().lower()
|
|
if a_or_m.startswith('a'):
|
|
automatic = True
|
|
break
|
|
if a_or_m.startswith('m'):
|
|
automatic = False
|
|
break
|
|
writeStdout(Msg.PLEASE_ENTER_A_OR_M)
|
|
if automatic:
|
|
login_hint = _getValidateLoginHint(None)
|
|
httpObj, _ = getCRMService(login_hint)
|
|
enableGAMProjectAPIs(httpObj, projectId, login_hint, True)
|
|
else:
|
|
apis = API.PROJECT_APIS[:]
|
|
chunk_size = 20
|
|
writeStdout('Using an account with project access, please use ALL of these URLs to enable 20 APIs at a time:\n\n')
|
|
for chunk in range(0, len(apis), chunk_size):
|
|
apiid = ",".join(apis[chunk:chunk+chunk_size])
|
|
url = f'https://console.cloud.google.com/apis/enableflow?apiid={apiid}&project={projectId}'
|
|
writeStdout(f' {url}\n\n')
|
|
|
|
def _waitForSvcAcctCompletion(i):
|
|
sleep_time = i*5
|
|
if i > 3:
|
|
sys.stdout.write(Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.SVCACCT), sleep_time))
|
|
time.sleep(sleep_time)
|
|
|
|
def _grantRotateRights(iam, projectId, service_account, email, account_type='serviceAccount'):
|
|
body = {'policy': {'bindings': [{'role': 'roles/iam.serviceAccountKeyAdmin',
|
|
'members': [f'{account_type}:{email}']}]}}
|
|
maxRetries = 10
|
|
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, email],
|
|
Msg.HAS_RIGHTS_TO_ROTATE_OWN_PRIVATE_KEY.format(email, service_account))
|
|
for retry in range(1, maxRetries+1):
|
|
try:
|
|
callGAPI(iam.projects().serviceAccounts(), 'setIamPolicy',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT],
|
|
resource=f'projects/{projectId}/serviceAccounts/{service_account}', body=body)
|
|
return True
|
|
except GAPI.invalidArgument as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, service_account], str(e))
|
|
if 'does not exist' not in str(e) or retry == maxRetries:
|
|
return False
|
|
_waitForSvcAcctCompletion(retry)
|
|
except Exception as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, service_account], str(e))
|
|
return False
|
|
|
|
def _createOauth2serviceJSON(httpObj, projectInfo, svcAcctInfo, create_key=True):
|
|
iam = getAPIService(API.IAM, httpObj)
|
|
try:
|
|
service_account = callGAPI(iam.projects().serviceAccounts(), 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
|
|
name=f'projects/{projectInfo["projectId"]}',
|
|
body={'accountId': svcAcctInfo['name'],
|
|
'serviceAccount': {'displayName': svcAcctInfo['displayName'],
|
|
'description': svcAcctInfo['description']}})
|
|
entityActionPerformed([Ent.PROJECT, projectInfo['projectId'], Ent.SVCACCT, service_account['name'].rsplit('/', 1)[-1]])
|
|
except (GAPI.notFound, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectInfo['projectId']], str(e))
|
|
return False
|
|
except GAPI.alreadyExists as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectInfo['projectId'], Ent.SVCACCT, svcAcctInfo['name']], str(e))
|
|
return False
|
|
GM.Globals[GM.SVCACCT_SCOPES_DEFINED] = False
|
|
if create_key and not doProcessSvcAcctKeys(mode='retainexisting', iam=iam,
|
|
projectId=service_account['projectId'],
|
|
clientEmail=service_account['email'],
|
|
clientId=service_account['uniqueId']):
|
|
return False
|
|
sa_email = service_account['name'].rsplit('/', 1)[-1]
|
|
return _grantRotateRights(iam, projectInfo['projectId'], sa_email, sa_email)
|
|
|
|
def _createClientSecretsOauth2service(httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key=True):
|
|
def _checkClientAndSecret(csHttpObj, client_id, client_secret):
|
|
post_data = {'client_id': client_id, 'client_secret': client_secret,
|
|
'code': 'ThisIsAnInvalidCodeOnlyBeingUsedToTestIfClientAndSecretAreValid',
|
|
'redirect_uri': 'http://127.0.0.1:8080', 'grant_type': 'authorization_code'}
|
|
_, content = csHttpObj.request(API.GOOGLE_OAUTH2_TOKEN_ENDPOINT, 'POST', urlencode(post_data),
|
|
headers={'Content-type': 'application/x-www-form-urlencoded'})
|
|
try:
|
|
content = json.loads(content)
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
sys.stderr.write(f'{str(e)}: {content}')
|
|
return False
|
|
if not 'error' in content or not 'error_description' in content:
|
|
sys.stderr.write(f'Unknown error: {content}\n')
|
|
return False
|
|
if content['error'] == 'invalid_grant':
|
|
return True
|
|
if content['error_description'] == 'The OAuth client was not found.':
|
|
sys.stderr.write(Msg.IS_NOT_A_VALID_CLIENT_ID.format(client_id))
|
|
return False
|
|
if content['error_description'] == 'Unauthorized':
|
|
sys.stderr.write(Msg.IS_NOT_A_VALID_CLIENT_SECRET.format(client_secret))
|
|
return False
|
|
sys.stderr.write(f'Unknown error: {content}\n')
|
|
return False
|
|
|
|
if not enableGAMProjectAPIs(httpObj, projectInfo['projectId'], login_hint, False):
|
|
return
|
|
sys.stdout.write(Msg.SETTING_GAM_PROJECT_CONSENT_SCREEN_CREATING_CLIENT)
|
|
console_url = f'https://console.cloud.google.com/auth/clients?project={projectInfo["projectId"]}&authuser={login_hint}'
|
|
csHttpObj = getHttpObj()
|
|
while True:
|
|
sys.stdout.write(Msg.CREATE_CLIENT_INSTRUCTIONS.format(console_url, appInfo['applicationTitle'], appInfo['supportEmail']))
|
|
client_id = readStdin(Msg.ENTER_YOUR_CLIENT_ID).strip()
|
|
if not client_id:
|
|
client_id = readStdin('').strip()
|
|
client_secret = readStdin(Msg.ENTER_YOUR_CLIENT_SECRET).strip()
|
|
if not client_secret:
|
|
client_secret = readStdin('').strip()
|
|
client_valid = _checkClientAndSecret(csHttpObj, client_id, client_secret)
|
|
if client_valid:
|
|
break
|
|
sys.stdout.write('\n')
|
|
cs_data = f'''{{
|
|
"installed": {{
|
|
"auth_provider_x509_cert_url": "{API.GOOGLE_AUTH_PROVIDER_X509_CERT_URL}",
|
|
"auth_uri": "{API.GOOGLE_OAUTH2_ENDPOINT}",
|
|
"client_id": "{client_id}",
|
|
"client_secret": "{client_secret}",
|
|
"created_by": "{login_hint}",
|
|
"project_id": "{projectInfo['projectId']}",
|
|
"token_uri": "{API.GOOGLE_OAUTH2_TOKEN_ENDPOINT}"
|
|
}}
|
|
}}'''
|
|
writeFile(GC.Values[GC.CLIENT_SECRETS_JSON], cs_data, continueOnError=False)
|
|
sys.stdout.write(Msg.TRUST_GAM_CLIENT_ID.format(GAM, client_id))
|
|
readStdin('')
|
|
if not _createOauth2serviceJSON(httpObj, projectInfo, svcAcctInfo, create_key):
|
|
return
|
|
sys.stdout.write(Msg.YOUR_GAM_PROJECT_IS_CREATED_AND_READY_TO_USE)
|
|
|
|
def _getProjects(crm, pfilter, returnNF=False):
|
|
try:
|
|
projects = callGAPIpages(crm.projects(), 'search', 'projects',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
query=pfilter)
|
|
if projects:
|
|
return projects
|
|
if (not pfilter) or pfilter == GAM_PROJECT_FILTER:
|
|
return []
|
|
if pfilter.startswith('id:'):
|
|
projects = [callGAPI(crm.projects(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=f'projects/{pfilter[3:]}')]
|
|
if projects or not returnNF:
|
|
return projects
|
|
return []
|
|
except (GAPI.badRequest, GAPI.invalidArgument) as e:
|
|
entityActionFailedExit([Ent.PROJECT, pfilter], str(e))
|
|
except GAPI.permissionDenied:
|
|
if (not pfilter) or (not pfilter.startswith('id:')) or (not returnNF):
|
|
return []
|
|
return [{'projectId': pfilter[3:], 'state': 'NF'}]
|
|
|
|
def _checkProjectFound(project, i, count):
|
|
if project.get('state', '') != 'NF':
|
|
return True
|
|
entityActionFailedWarning([Ent.PROJECT, project['projectId']], Msg.DOES_NOT_EXIST, i, count)
|
|
return False
|
|
|
|
def convertGCPFolderNameToID(parent, crm):
|
|
folders = callGAPIpages(crm.folders(), 'search', 'folders',
|
|
query=f'displayName="{parent}"')
|
|
if not folders:
|
|
entityActionFailedExit([Ent.PROJECT_FOLDER, parent], Msg.NOT_FOUND)
|
|
jcount = len(folders)
|
|
if jcount > 1:
|
|
entityActionNotPerformedWarning([Ent.PROJECT_FOLDER, parent],
|
|
Msg.PLEASE_SELECT_ENTITY_TO_PROCESS.format(jcount, Ent.Plural(Ent.PROJECT_FOLDER), 'use in create', 'parent <String>'))
|
|
Ind.Increment()
|
|
j = 0
|
|
for folder in folders:
|
|
j += 1
|
|
printKeyValueListWithCount(['Name', folder['name'], 'ID', folder['displayName']], j, jcount)
|
|
Ind.Decrement()
|
|
systemErrorExit(MULTIPLE_PROJECT_FOLDERS_FOUND_RC, None)
|
|
return folders[0]['name']
|
|
|
|
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 _checkProjectId(projectId):
|
|
if not PROJECTID_PATTERN.match(projectId):
|
|
Cmd.Backup()
|
|
invalidArgumentExit(PROJECTID_FORMAT_REQUIRED)
|
|
|
|
PROJECTNAME_PATTERN = re.compile('^[a-zA-Z0-9 '+"'"+'"!-]{4,30}$')
|
|
PROJECTNAME_FORMAT_REQUIRED = '[a-zA-Z0-9 \'"!-]{4,30}'
|
|
def _checkProjectName(projectName):
|
|
if not PROJECTNAME_PATTERN.match(projectName):
|
|
Cmd.Backup()
|
|
invalidArgumentExit(PROJECTNAME_FORMAT_REQUIRED)
|
|
|
|
def _getSvcAcctInfo(myarg, svcAcctInfo):
|
|
if myarg == 'saname':
|
|
svcAcctInfo['name'] = getString(Cmd.OB_STRING, minLen=6, maxLen=30)
|
|
_checkProjectId(svcAcctInfo['name'])
|
|
elif myarg == 'sadisplayname':
|
|
svcAcctInfo['displayName'] = getString(Cmd.OB_STRING, maxLen=100)
|
|
elif myarg == 'sadescription':
|
|
svcAcctInfo['description'] = getString(Cmd.OB_STRING, maxLen=256)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _getAppInfo(myarg, appInfo):
|
|
if myarg == 'appname':
|
|
appInfo['applicationTitle'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'supportemail':
|
|
appInfo['supportEmail'] = getEmailAddress(noUid=True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _generateProjectSvcAcctId(prefix):
|
|
return f'{prefix}-{"".join(random.choice(LOWERNUMERIC_CHARS) for _ in range(5))}'
|
|
|
|
def _getLoginHintProjectInfo(createCmd):
|
|
login_hint = None
|
|
create_key = True
|
|
appInfo = {'applicationTitle': '', 'supportEmail': ''}
|
|
projectInfo = {'projectId': '', 'parent': '', 'name': ''}
|
|
svcAcctInfo = {'name': '', 'displayName': '', 'description': ''}
|
|
if not Cmd.PeekArgumentPresent(['admin', 'appname', 'supportemail', 'project', 'parent',
|
|
'projectname', 'saname', 'sadisplayname', 'sadescription',
|
|
'algorithm', 'localkeysize', 'validityhours', 'yubikey', 'nokey']):
|
|
login_hint = getString(Cmd.OB_EMAIL_ADDRESS, optional=True)
|
|
if login_hint and login_hint.find('@') == -1:
|
|
Cmd.Backup()
|
|
login_hint = None
|
|
projectInfo['projectId'] = getString(Cmd.OB_STRING, optional=True, minLen=6, maxLen=30).strip()
|
|
if projectInfo['projectId']:
|
|
_checkProjectId(projectInfo['projectId'])
|
|
checkForExtraneousArguments()
|
|
else:
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'admin':
|
|
login_hint = getEmailAddress(noUid=True)
|
|
elif myarg == 'nokey':
|
|
create_key = False
|
|
elif myarg == 'project':
|
|
projectInfo['projectId'] = getString(Cmd.OB_STRING, minLen=6, maxLen=30)
|
|
_checkProjectId(projectInfo['projectId'])
|
|
elif createCmd and myarg == 'parent':
|
|
projectInfo['parent'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'projectname':
|
|
projectInfo['name'] = getString(Cmd.OB_STRING, minLen=4, maxLen=30)
|
|
_checkProjectName(projectInfo['name'])
|
|
elif _getSvcAcctInfo(myarg, svcAcctInfo):
|
|
pass
|
|
elif _getAppInfo(myarg, appInfo):
|
|
pass
|
|
elif myarg in {'algorithm', 'localkeysize', 'validityhours', 'yubikey'}:
|
|
Cmd.Backup()
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
if not projectInfo['projectId']:
|
|
if createCmd:
|
|
projectInfo['projectId'] = _generateProjectSvcAcctId('gam-project')
|
|
else:
|
|
projectInfo['projectId'] = readStdin(Msg.WHAT_IS_YOUR_PROJECT_ID).strip()
|
|
if not PROJECTID_PATTERN.match(projectInfo['projectId']):
|
|
systemErrorExit(USAGE_ERROR_RC, f'{Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID][1]} {Cmd.OB_PROJECT_ID}: {Msg.EXPECTED} <{PROJECTID_FORMAT_REQUIRED}>')
|
|
if not projectInfo['name']:
|
|
projectInfo['name'] = 'GAM Project' if not GC.Values[GC.USE_PROJECTID_AS_NAME] else projectInfo['projectId']
|
|
if not svcAcctInfo['name']:
|
|
svcAcctInfo['name'] = projectInfo['projectId']
|
|
if not svcAcctInfo['displayName']:
|
|
svcAcctInfo['displayName'] = projectInfo['name']
|
|
if not svcAcctInfo['description']:
|
|
svcAcctInfo['description'] = svcAcctInfo['displayName']
|
|
login_hint = _getValidateLoginHint(login_hint, projectInfo['projectId'])
|
|
if not appInfo['applicationTitle']:
|
|
appInfo['applicationTitle'] = 'GAM' if not GC.Values[GC.USE_PROJECTID_AS_NAME] else projectInfo['projectId']
|
|
if not appInfo['supportEmail']:
|
|
appInfo['supportEmail'] = login_hint
|
|
httpObj, crm = getCRMService(login_hint)
|
|
if projectInfo['parent'] and not projectInfo['parent'].startswith('organizations/') and not projectInfo['parent'].startswith('folders/'):
|
|
projectInfo['parent'] = convertGCPFolderNameToID(projectInfo['parent'], crm)
|
|
projects = _getProjects(crm, f'id:{projectInfo["projectId"]}')
|
|
if not createCmd:
|
|
if not projects:
|
|
entityActionFailedExit([Ent.USER, login_hint, Ent.PROJECT, projectInfo['projectId']], Msg.DOES_NOT_EXIST)
|
|
if projects[0]['state'] != 'ACTIVE':
|
|
entityActionFailedExit([Ent.USER, login_hint, Ent.PROJECT, projectInfo['projectId']], Msg.NOT_ACTIVE)
|
|
else:
|
|
if projects:
|
|
entityActionFailedExit([Ent.USER, login_hint, Ent.PROJECT, projectInfo['projectId']], Msg.DUPLICATE)
|
|
return (crm, httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key)
|
|
|
|
def _getCurrentProjectId():
|
|
jsonData = readFile(GC.Values[GC.OAUTH2SERVICE_JSON], continueOnError=True, displayError=False)
|
|
if jsonData:
|
|
try:
|
|
return json.loads(jsonData)['project_id']
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
|
|
pass
|
|
jsonData = readFile(GC.Values[GC.CLIENT_SECRETS_JSON], continueOnError=True, displayError=True)
|
|
if not jsonData:
|
|
invalidClientSecretsJsonExit(Msg.NO_DATA)
|
|
try:
|
|
return json.loads(jsonData)['installed']['project_id']
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
invalidClientSecretsJsonExit(str(e))
|
|
|
|
GAM_PROJECT_FILTER = 'id:gam-project-*'
|
|
PROJECTID_FILTER_REQUIRED = '<ProjectIDEntity>'
|
|
PROJECTS_CREATESVCACCT_OPTIONS = {'saname', 'sadisplayname', 'sadescription'}
|
|
PROJECTS_DELETESVCACCT_OPTIONS = {'saemail', 'saname', 'sauniqueid'}
|
|
PROJECTS_PRINTSHOW_OPTIONS = {'showsakeys', 'showiampolicies', 'onememberperrow', 'states', 'todrive', 'delimiter', 'formatjson', 'quotechar'}
|
|
|
|
def _getLoginHintProjects(createSvcAcctCmd=False, deleteSvcAcctCmd=False, printShowCmd=False, readOnly=False):
|
|
if checkArgumentPresent(['admin']):
|
|
login_hint = getEmailAddress(noUid=True)
|
|
else:
|
|
login_hint = getString(Cmd.OB_EMAIL_ADDRESS, optional=True)
|
|
if login_hint and login_hint.find('@') == -1:
|
|
Cmd.Backup()
|
|
login_hint = None
|
|
if readOnly and login_hint and login_hint != _getAdminEmail():
|
|
readOnly = False
|
|
projectIds = None
|
|
pfilter = getString(Cmd.OB_STRING, optional=True)
|
|
if not pfilter:
|
|
pfilter = 'current' if not printShowCmd else GAM_PROJECT_FILTER
|
|
elif printShowCmd and pfilter in PROJECTS_PRINTSHOW_OPTIONS:
|
|
pfilter = GAM_PROJECT_FILTER
|
|
Cmd.Backup()
|
|
elif createSvcAcctCmd and pfilter in PROJECTS_CREATESVCACCT_OPTIONS:
|
|
pfilter = 'current'
|
|
Cmd.Backup()
|
|
elif deleteSvcAcctCmd and pfilter in PROJECTS_DELETESVCACCT_OPTIONS:
|
|
pfilter = 'current'
|
|
Cmd.Backup()
|
|
elif printShowCmd and pfilter.lower() == 'all':
|
|
pfilter = None
|
|
elif pfilter.lower() == 'current':
|
|
pfilter = 'current'
|
|
elif pfilter.lower() == 'gam':
|
|
pfilter = GAM_PROJECT_FILTER
|
|
elif pfilter.lower() == 'filter':
|
|
pfilter = getString(Cmd.OB_STRING)
|
|
elif pfilter.lower() == 'select':
|
|
projectIds = getEntityList(Cmd.OB_PROJECT_ID_ENTITY, False)
|
|
projectId = None
|
|
elif PROJECTID_PATTERN.match(pfilter):
|
|
pfilter = f'id:{pfilter}'
|
|
elif pfilter.startswith('id:') and PROJECTID_PATTERN.match(pfilter[3:]):
|
|
pass
|
|
else:
|
|
Cmd.Backup()
|
|
invalidArgumentExit(['', 'all|'][printShowCmd]+PROJECTID_FILTER_REQUIRED)
|
|
if not printShowCmd and not createSvcAcctCmd and not deleteSvcAcctCmd:
|
|
checkForExtraneousArguments()
|
|
if projectIds is None:
|
|
if pfilter in {'current', 'id:current'}:
|
|
projectId = _getCurrentProjectId()
|
|
else:
|
|
projectId = f'filter {pfilter or "all"}'
|
|
login_hint = _getValidateLoginHint(login_hint, projectId)
|
|
crm = None
|
|
if readOnly:
|
|
_, crm = buildGAPIServiceObject(API.CLOUDRESOURCEMANAGER, None)
|
|
if crm:
|
|
httpObj = crm._http
|
|
if not crm:
|
|
httpObj, crm = getCRMService(login_hint)
|
|
if projectIds is None:
|
|
if pfilter in {'current', 'id:current'}:
|
|
if not printShowCmd:
|
|
projects = [{'projectId': projectId}]
|
|
else:
|
|
projects = _getProjects(crm, f'id:{projectId}', returnNF=True)
|
|
else:
|
|
projects = _getProjects(crm, pfilter, returnNF=printShowCmd)
|
|
else:
|
|
projects = []
|
|
for projectId in projectIds:
|
|
projects.extend(_getProjects(crm, f'id:{projectId}', returnNF=True))
|
|
return (crm, httpObj, login_hint, projects)
|
|
|
|
def _checkForExistingProjectFiles(projectFiles):
|
|
for a_file in projectFiles:
|
|
if os.path.exists(a_file):
|
|
systemErrorExit(JSON_ALREADY_EXISTS_RC, Msg.AUTHORIZATION_FILE_ALREADY_EXISTS.format(a_file, Act.ToPerform()))
|
|
|
|
def getGCPOrg(crm, login_hint, login_domain):
|
|
try:
|
|
getorg = callGAPI(crm.organizations(), 'search',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
query=f'domain:{login_domain}')
|
|
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit([Ent.USER, login_hint, Ent.DOMAIN, login_domain], str(e))
|
|
try:
|
|
organization = getorg['organizations'][0]['name']
|
|
sys.stdout.write(Msg.YOUR_ORGANIZATION_NAME_IS.format(organization))
|
|
return organization
|
|
except (KeyError, IndexError):
|
|
systemErrorExit(3, Msg.YOU_HAVE_NO_RIGHTS_TO_CREATE_PROJECTS_AND_YOU_ARE_NOT_A_SUPER_ADMIN)
|
|
|
|
# gam create gcpfolder <String>
|
|
# gam create gcpfolder [admin <EmailAddress] folder <String>
|
|
def doCreateGCPFolder():
|
|
login_hint = None
|
|
if not Cmd.PeekArgumentPresent(['admin', 'folder']):
|
|
name = getString(Cmd.OB_STRING)
|
|
checkForExtraneousArguments()
|
|
else:
|
|
name = ''
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'admin':
|
|
login_hint = getEmailAddress(noUid=True)
|
|
elif myarg == 'folder':
|
|
name = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not name:
|
|
missingChoiceExit('folder')
|
|
login_hint = _getValidateLoginHint(login_hint)
|
|
login_domain = getEmailAddressDomain(login_hint)
|
|
_, crm = getCRMService(login_hint)
|
|
organization = getGCPOrg(crm, login_hint, login_domain)
|
|
try:
|
|
result = callGAPI(crm.folders(), 'create',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
body={'parent': organization, 'displayName': name})
|
|
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit([Ent.USER, login_hint, Ent.GCP_FOLDER, name], str(e))
|
|
entityActionPerformed([Ent.USER, login_hint, Ent.GCP_FOLDER, name, Ent.GCP_FOLDER_NAME, result['name']])
|
|
|
|
# gam create project [<EmailAddress>] [<ProjectID>]
|
|
# gam create project [admin <EmailAddress>] [project <ProjectID>]
|
|
# [appname <String>] [supportemail <EmailAddress>]
|
|
# [projectname <ProjectName>] [parent <String>]
|
|
# [saname <ServiceAccountName>] [sadisplayname <ServiceAccountDisplayName>] [sadescription <ServiceAccountDescription>]
|
|
# [(algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
|
|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
|
|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)|
|
|
# nokey]
|
|
def doCreateProject():
|
|
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON], GC.Values[GC.CLIENT_SECRETS_JSON]])
|
|
sys.stdout.write(Msg.TRUST_GAM_CLIENT_ID.format(GAM_PROJECT_CREATION, GAM_PROJECT_CREATION_CLIENT_ID))
|
|
readStdin('')
|
|
crm, httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key = _getLoginHintProjectInfo(True)
|
|
login_domain = getEmailAddressDomain(login_hint)
|
|
body = {'projectId': projectInfo['projectId'], 'displayName': projectInfo['name']}
|
|
if projectInfo['parent']:
|
|
body['parent'] = projectInfo['parent']
|
|
while True:
|
|
create_again = False
|
|
sys.stdout.write(Msg.CREATING_PROJECT.format(body['displayName']))
|
|
try:
|
|
create_operation = callGAPI(crm.projects(), 'create',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.ALREADY_EXISTS,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
|
|
body=body)
|
|
except (GAPI.badRequest, GAPI.alreadyExists, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit([Ent.USER, login_hint, Ent.PROJECT, projectInfo['projectId']], str(e))
|
|
operation_name = create_operation['name']
|
|
time.sleep(5) # Google recommends always waiting at least 5 seconds
|
|
for i in range(1, 10):
|
|
sys.stdout.write(Msg.CHECKING_PROJECT_CREATION_STATUS)
|
|
status = callGAPI(crm.operations(), 'get',
|
|
name=operation_name)
|
|
if 'error' in status:
|
|
if status['error'].get('message', '') == 'No permission to create project in organization':
|
|
sys.stdout.write(Msg.NO_RIGHTS_GOOGLE_CLOUD_ORGANIZATION)
|
|
organization = getGCPOrg(crm, login_hint, login_domain)
|
|
org_policy = callGAPI(crm.organizations(), 'getIamPolicy',
|
|
resource=organization)
|
|
if 'bindings' not in org_policy:
|
|
org_policy['bindings'] = []
|
|
sys.stdout.write(Msg.LOOKS_LIKE_NO_ONE_HAS_RIGHTS_TO_YOUR_GOOGLE_CLOUD_ORGANIZATION_ATTEMPTING_TO_GIVE_YOU_CREATE_RIGHTS)
|
|
else:
|
|
sys.stdout.write(Msg.THE_FOLLOWING_RIGHTS_SEEM_TO_EXIST)
|
|
for a_policy in org_policy['bindings']:
|
|
if 'role' in a_policy:
|
|
sys.stdout.write(f' Role: {a_policy["role"]}\n')
|
|
if 'members' in a_policy:
|
|
sys.stdout.write(' Members:\n')
|
|
for member in a_policy['members']:
|
|
sys.stdout.write(f' {member}\n')
|
|
my_role = 'roles/resourcemanager.projectCreator'
|
|
sys.stdout.write(Msg.GIVING_LOGIN_HINT_THE_CREATOR_ROLE.format(login_hint, my_role))
|
|
org_policy['bindings'].append({'role': my_role, 'members': [f'user:{login_hint}']})
|
|
callGAPI(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':
|
|
readStdin(Msg.ACCEPT_CLOUD_TOS.format(login_hint))
|
|
create_again = True
|
|
break
|
|
except (IndexError, KeyError):
|
|
pass
|
|
systemErrorExit(1, str(status)+'\n')
|
|
if status.get('done', False):
|
|
break
|
|
sleep_time = min(2 ** i, 60)
|
|
sys.stdout.write(Msg.PROJECT_STILL_BEING_CREATED_SLEEPING.format(sleep_time))
|
|
time.sleep(sleep_time)
|
|
if create_again:
|
|
continue
|
|
if not status.get('done', False):
|
|
systemErrorExit(1, Msg.FAILED_TO_CREATE_PROJECT.format(status))
|
|
elif 'error' in status:
|
|
systemErrorExit(2, status['error']+'\n')
|
|
break
|
|
# Try to set policy on project to allow Service Account Key Upload
|
|
# orgp = getAPIService(API.ORGPOLICY, httpObj)
|
|
# projectParent = f"projects/{projectInfo['projectId']}"
|
|
# policyName = f'{projectParent}/policies/iam.managed.disableServiceAccountKeyUpload'
|
|
# try:
|
|
# result = callGAPI(orgp.projects().policies(), 'get',
|
|
# throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
|
|
# name=policyName)
|
|
# if result['spec']['rules'][0]['enforce']:
|
|
# callGAPI(orgp.projects().policies(), 'patch',
|
|
# throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
|
|
# name=policyName, body={'spec': {'rules': [{'enforce': False}]}}, updateMask='policy.spec')
|
|
# except GAPI.notFound:
|
|
# callGAPI(orgp.projects().policies(), 'create',
|
|
# throwReasons=[GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
|
|
# parent=projectParent, body={'name': policyName, 'spec': {'rules': [{'enforce': False}]}})
|
|
# except (GAPI.badRequest, GAPI.failedPrecondition, GAPI.permissionDenied):
|
|
# pass
|
|
# Create client_secrets.json and oauth2service.json
|
|
_createClientSecretsOauth2service(httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key)
|
|
|
|
# gam use project [<EmailAddress>] [<ProjectID>]
|
|
# gam use project [admin <EmailAddress>] [project <ProjectID>]
|
|
# [appname <String>] [supportemail <EmailAddress>]
|
|
# [saname <ServiceAccountName>] [sadisplayname <ServiceAccountDisplayName>] [sadescription <ServiceAccountDescription>]
|
|
# [(algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
|
|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
|
|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)]
|
|
def doUseProject():
|
|
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON], GC.Values[GC.CLIENT_SECRETS_JSON]])
|
|
_, httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key = _getLoginHintProjectInfo(False)
|
|
_createClientSecretsOauth2service(httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key)
|
|
|
|
# gam update project [[admin] <EmailAddress>] [<ProjectIDEntity>]
|
|
def doUpdateProject():
|
|
_, httpObj, login_hint, projects = _getLoginHintProjects()
|
|
count = len(projects)
|
|
entityPerformActionNumItems([Ent.USER, login_hint], count, Ent.PROJECT)
|
|
Ind.Increment()
|
|
i = 0
|
|
for project in projects:
|
|
i += 1
|
|
if not _checkProjectFound(project, i, count):
|
|
continue
|
|
projectId = project['projectId']
|
|
Act.Set(Act.UPDATE)
|
|
if not enableGAMProjectAPIs(httpObj, projectId, login_hint, True, i, count):
|
|
continue
|
|
iam = getAPIService(API.IAM, httpObj)
|
|
_getSvcAcctData() # needed to read in GM.OAUTH2SERVICE_JSON_DATA
|
|
_grantRotateRights(iam, projectId,
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email'],
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email'])
|
|
Ind.Decrement()
|
|
|
|
# gam delete project [[admin] <EmailAddress>] [<ProjectIDEntity>]
|
|
def doDeleteProject():
|
|
crm, _, login_hint, projects = _getLoginHintProjects()
|
|
count = len(projects)
|
|
entityPerformActionNumItems([Ent.USER, login_hint], count, Ent.PROJECT)
|
|
Ind.Increment()
|
|
i = 0
|
|
for project in projects:
|
|
i += 1
|
|
if not _checkProjectFound(project, i, count):
|
|
continue
|
|
projectId = project['projectId']
|
|
try:
|
|
callGAPI(crm.projects(), 'delete',
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
|
name=project['name'])
|
|
entityActionPerformed([Ent.PROJECT, projectId])
|
|
except (GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId], str(e))
|
|
Ind.Decrement()
|
|
|
|
PROJECT_TIMEOBJECTS = ['createTime']
|
|
PROJECT_STATE_CHOICE_MAP = {
|
|
'all': {'ACTIVE', 'DELETE_REQUESTED'},
|
|
'active': {'ACTIVE'},
|
|
'deleterequested': {'DELETE_REQUESTED'}
|
|
}
|
|
|
|
# gam print projects [[admin] <EmailAddress>] [all|<ProjectIDEntity>] [todrive <ToDriveAttribute>*]
|
|
# [states all|active|deleterequested] [showiampolicies 0|1|3 [onememberperrow]]
|
|
# [delimiter <Character>] [formatjson [quotechar <Character>]]
|
|
# gam show projects [[admin] <EmailAddress>] [all|<ProjectIDEntity>]
|
|
# [states all|active|deleterequested] [showiampolicies 0|1|3]
|
|
def doPrintShowProjects():
|
|
def _getProjectPolicies(crm, project, policyBody, i, count):
|
|
try:
|
|
policy = callGAPI(crm.projects(), 'getIamPolicy',
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
|
resource=project['name'], body=policyBody)
|
|
return policy
|
|
except (GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.PROJECT, project['projectId'], Ent.IAM_POLICY, None], str(e), i, count)
|
|
return {}
|
|
|
|
readOnly = not Cmd.ArgumentIsAhead('showiampolicies')
|
|
crm, _, login_hint, projects = _getLoginHintProjects(printShowCmd=True, readOnly=readOnly)
|
|
csvPF = CSVPrintFile(['User', 'projectId']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
oneMemberPerRow = False
|
|
showIAMPolicies = -1
|
|
lifecycleStates = PROJECT_STATE_CHOICE_MAP['active']
|
|
policy = None
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'onememberperrow':
|
|
oneMemberPerRow = True
|
|
elif myarg == 'states':
|
|
lifecycleStates = getChoice(PROJECT_STATE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'showiampolicies':
|
|
showIAMPolicies = int(getChoice(['0', '1', '3']))
|
|
policyBody = {'options': {"requestedPolicyVersion": showIAMPolicies}}
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if not csvPF:
|
|
count = len(projects)
|
|
entityPerformActionNumItems([Ent.USER, login_hint], count, Ent.PROJECT)
|
|
Ind.Increment()
|
|
i = 0
|
|
for project in projects:
|
|
i += 1
|
|
if not _checkProjectFound(project, i, count):
|
|
continue
|
|
if project['state'] not in lifecycleStates:
|
|
continue
|
|
projectId = project['projectId']
|
|
if showIAMPolicies >= 0:
|
|
policy = _getProjectPolicies(crm, project, policyBody, i, count)
|
|
printEntity([Ent.PROJECT, projectId], i, count)
|
|
Ind.Increment()
|
|
printKeyValueList(['name', project['name']])
|
|
printKeyValueList(['displayName', project['displayName']])
|
|
for field in ['createTime', 'updateTime', 'deleteTime']:
|
|
if field in project:
|
|
printKeyValueList([field, formatLocalTime(project[field])])
|
|
printKeyValueList(['state', project['state']])
|
|
jcount = len(project.get('labels', []))
|
|
if jcount > 0:
|
|
printKeyValueList(['labels', jcount])
|
|
Ind.Increment()
|
|
for k, v in iter(project['labels'].items()):
|
|
printKeyValueList([k, v])
|
|
Ind.Decrement()
|
|
if 'parent' in project:
|
|
printKeyValueList(['parent', project['parent']])
|
|
if policy:
|
|
printKeyValueList([Ent.Singular(Ent.IAM_POLICY), ''])
|
|
Ind.Increment()
|
|
bindings = policy.get('bindings', [])
|
|
jcount = len(bindings)
|
|
printKeyValueList(['version', policy['version']])
|
|
printKeyValueList(['bindings', jcount])
|
|
Ind.Increment()
|
|
j = 0
|
|
for binding in bindings:
|
|
j += 1
|
|
printKeyValueListWithCount(['role', binding['role']], j, jcount)
|
|
Ind.Increment()
|
|
for member in binding.get('members', []):
|
|
printKeyValueList(['member', member])
|
|
if 'condition' in binding:
|
|
printKeyValueList(['condition', ''])
|
|
Ind.Increment()
|
|
for k, v in iter(binding['condition'].items()):
|
|
printKeyValueList([k, v])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
if not FJQC.formatJSON:
|
|
csvPF.AddTitles(['projectId', 'name', 'displayName', 'createTime', 'updateTime', 'deleteTime', 'state'])
|
|
csvPF.SetSortAllTitles()
|
|
count = len(projects)
|
|
i = 0
|
|
for project in projects:
|
|
i += 1
|
|
if not _checkProjectFound(project, i, count):
|
|
continue
|
|
if project['state'] not in lifecycleStates:
|
|
continue
|
|
projectId = project['projectId']
|
|
if showIAMPolicies >= 0:
|
|
policy = _getProjectPolicies(crm, project, policyBody, i, count)
|
|
if FJQC.formatJSON:
|
|
if policy is not None:
|
|
project['policy'] = policy
|
|
row = flattenJSON(project, flattened={'User': login_hint}, timeObjects=PROJECT_TIMEOBJECTS)
|
|
if csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'User': login_hint, 'projectId': projectId,
|
|
'JSON': json.dumps(cleanJSON(project),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
continue
|
|
row = flattenJSON(project, flattened={'User': login_hint}, timeObjects=PROJECT_TIMEOBJECTS)
|
|
if not policy:
|
|
csvPF.WriteRowTitles(row)
|
|
continue
|
|
row[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}version'] = policy['version']
|
|
for binding in policy.get('bindings', []):
|
|
prow = row.copy()
|
|
prow[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}role'] = binding['role']
|
|
if 'condition' in binding:
|
|
for k, v in iter(binding['condition'].items()):
|
|
prow[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}condition{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = v
|
|
members = binding.get('members', [])
|
|
if not oneMemberPerRow:
|
|
prow[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}members'] = delimiter.join(members)
|
|
csvPF.WriteRowTitles(prow)
|
|
else:
|
|
for member in members:
|
|
mrow = prow.copy()
|
|
mrow[f'policy{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}member'] = member
|
|
csvPF.WriteRowTitles(mrow)
|
|
csvPF.writeCSVfile('Projects')
|
|
|
|
# gam info currentprojectid
|
|
def doInfoCurrentProjectId():
|
|
checkForExtraneousArguments()
|
|
printEntity([Ent.PROJECT_ID, _getCurrentProjectId()])
|
|
|
|
# gam create svcacct [[admin] <EmailAddress>] [<ProjectIDEntity>]
|
|
# [saname <ServiceAccountName>] [sadisplayname <ServiceAccountDisplayName>] [sadescription <ServiceAccountDescription>]
|
|
# [(algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
|
|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
|
|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)]
|
|
def doCreateSvcAcct():
|
|
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON]])
|
|
_, httpObj, login_hint, projects = _getLoginHintProjects(createSvcAcctCmd=True)
|
|
svcAcctInfo = {'name': '', 'displayName': '', 'description': ''}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getSvcAcctInfo(myarg, svcAcctInfo):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
if not svcAcctInfo['name']:
|
|
svcAcctInfo['name'] = _generateProjectSvcAcctId('gam-svcacct')
|
|
if not svcAcctInfo['displayName']:
|
|
svcAcctInfo['displayName'] = svcAcctInfo['name']
|
|
if not svcAcctInfo['description']:
|
|
svcAcctInfo['description'] = svcAcctInfo['displayName']
|
|
count = len(projects)
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, login_hint], Ent.SVCACCT, Act.MODIFIER_TO, count, Ent.PROJECT)
|
|
Ind.Increment()
|
|
i = 0
|
|
for project in projects:
|
|
i += 1
|
|
if not _checkProjectFound(project, i, count):
|
|
continue
|
|
projectInfo = {'projectId': project['projectId']}
|
|
_createOauth2serviceJSON(httpObj, projectInfo, svcAcctInfo)
|
|
Ind.Decrement()
|
|
|
|
# gam delete svcacct [[admin] <EmailAddress>] [<ProjectIDEntity>]
|
|
# (saemail <ServiceAccountEmail>)|(saname <ServiceAccountName>)|(sauniqueid <ServiceAccountUniqueID>)
|
|
def doDeleteSvcAcct():
|
|
_, httpObj, login_hint, projects = _getLoginHintProjects(deleteSvcAcctCmd=True)
|
|
iam = getAPIService(API.IAM, httpObj)
|
|
clientEmail = clientId = clientName = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'saemail':
|
|
clientEmail = getEmailAddress(noUid=True)
|
|
clientName = clientId = None
|
|
elif myarg == 'saname':
|
|
clientName = getString(Cmd.OB_STRING, minLen=6, maxLen=30).strip()
|
|
_checkProjectId(clientName)
|
|
clientEmail = clientId = None
|
|
elif myarg == 'sauniqueid':
|
|
clientId = getInteger(minVal=0)
|
|
clientEmail = clientName = None
|
|
else:
|
|
unknownArgumentExit()
|
|
if not clientEmail and not clientId and not clientName:
|
|
missingArgumentExit('email|name|uniqueid')
|
|
count = len(projects)
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, login_hint], Ent.SVCACCT, Act.MODIFIER_FROM, count, Ent.PROJECT)
|
|
Ind.Increment()
|
|
i = 0
|
|
for project in projects:
|
|
i += 1
|
|
if not _checkProjectFound(project, i, count):
|
|
continue
|
|
projectId = project['projectId']
|
|
try:
|
|
if clientEmail:
|
|
saName = clientEmail
|
|
elif clientName:
|
|
saName = f'{clientName}@{projectId}.iam.gserviceaccount.com'
|
|
else: #clientId
|
|
saName = clientId
|
|
callGAPI(iam.projects().serviceAccounts(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST],
|
|
name=f'projects/{projectId}/serviceAccounts/{saName}')
|
|
entityActionPerformed([Ent.PROJECT, projectId, Ent.SVCACCT, saName], i, count)
|
|
except (GAPI.notFound, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, saName], str(e), i, count)
|
|
Ind.Decrement()
|
|
|
|
def _getSvcAcctKeyProjectClientFields():
|
|
return (GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].get('private_key_id', ''),
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['project_id'],
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email'],
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id'])
|
|
|
|
# gam <UserTypeEntity> check serviceaccount (scope|scopes <APIScopeURLList>)* [usecolor]
|
|
# gam <UserTypeEntity> update serviceaccount (scope|scopes <APIScopeURLList>)* [usecolor]
|
|
def checkServiceAccount(users):
|
|
def printMessage(message):
|
|
writeStdout(Ind.Spaces()+message+'\n')
|
|
|
|
def printPassFail(description, result):
|
|
writeStdout(Ind.Spaces()+f'{description:73} {result}'+'\n')
|
|
|
|
def authorizeScopes(message):
|
|
long_url = ('https://admin.google.com/ac/owl/domainwidedelegation'
|
|
f'?clientScopeToAdd={",".join(checkScopes)}'
|
|
f'&clientIdToAdd={service_account}&overwriteClientId=true')
|
|
if GC.Values[GC.DOMAIN]:
|
|
long_url += f'&dn={GC.Values[GC.DOMAIN]}'
|
|
long_url += f'&authuser={_getAdminEmail()}'
|
|
short_url = shortenURL(long_url)
|
|
printLine(message.format('', short_url))
|
|
|
|
credentials = getSvcAcctCredentials([API.USERINFO_EMAIL_SCOPE], None, forceOauth=True)
|
|
allScopes = API.getSvcAcctScopes(GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY], Act.Get() == Act.UPDATE)
|
|
checkScopesSet = set()
|
|
saScopes = {}
|
|
useColor = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'scope', 'scopes'}:
|
|
for scope in getString(Cmd.OB_API_SCOPE_URL_LIST).lower().replace(',', ' ').split():
|
|
api = API.getSvcAcctScopeAPI(scope)
|
|
if api is not None:
|
|
saScopes.setdefault(api, [])
|
|
saScopes[api].append(scope)
|
|
checkScopesSet.add(scope)
|
|
else:
|
|
invalidChoiceExit(scope, allScopes, True)
|
|
elif myarg == 'usecolor':
|
|
useColor = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if useColor:
|
|
testPass = createGreenText('PASS')
|
|
testFail = createRedText('FAIL')
|
|
testWarn = createYellowText('WARN')
|
|
else:
|
|
testPass = 'PASS'
|
|
testFail = 'FAIL'
|
|
testWarn = 'WARN'
|
|
if Act.Get() == Act.CHECK:
|
|
if not checkScopesSet:
|
|
for scope in iter(GM.Globals[GM.SVCACCT_SCOPES].values()):
|
|
checkScopesSet.update(scope)
|
|
else:
|
|
if not checkScopesSet:
|
|
scopesList = API.getSvcAcctScopesList(GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY], True)
|
|
selectedScopes = getScopesFromUser(scopesList, False, GM.Globals[GM.SVCACCT_SCOPES])
|
|
if selectedScopes is None:
|
|
return False
|
|
i = 0
|
|
for scope in scopesList:
|
|
if selectedScopes[i] == '*':
|
|
saScopes.setdefault(scope['api'], [])
|
|
saScopes[scope['api']].append(scope['scope'])
|
|
checkScopesSet.add(scope['scope'])
|
|
elif selectedScopes[i] == 'R':
|
|
saScopes.setdefault(scope['api'], [])
|
|
if 'roscope' not in scope:
|
|
saScopes[scope['api']].append(f'{scope["scope"]}.readonly')
|
|
checkScopesSet.add(f'{scope["scope"]}.readonly')
|
|
else:
|
|
saScopes[scope['api']].append(scope['roscope'])
|
|
checkScopesSet.add(scope['roscope'])
|
|
i += 1
|
|
if API.DRIVEACTIVITY in saScopes and API.DRIVE3 in saScopes:
|
|
saScopes[API.DRIVEACTIVITY].append(API.DRIVE_SCOPE)
|
|
if API.DRIVE3 in saScopes:
|
|
saScopes[API.DRIVE2] = saScopes[API.DRIVE3]
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA][API.OAUTH2SA_SCOPES] = saScopes
|
|
writeFile(GC.Values[GC.OAUTH2SERVICE_JSON],
|
|
json.dumps(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA], ensure_ascii=False, sort_keys=True, indent=2),
|
|
continueOnError=False)
|
|
checkScopes = sorted(checkScopesSet)
|
|
jcount = len(checkScopes)
|
|
printMessage(Msg.SYSTEM_TIME_STATUS)
|
|
offsetSeconds, offsetFormatted = getLocalGoogleTimeOffset()
|
|
if offsetSeconds <= MAX_LOCAL_GOOGLE_TIME_OFFSET:
|
|
timeStatus = testPass
|
|
else:
|
|
timeStatus = testFail
|
|
Ind.Increment()
|
|
printPassFail(Msg.YOUR_SYSTEM_TIME_DIFFERS_FROM_GOOGLE.format(GOOGLE_TIMECHECK_LOCATION, offsetFormatted), timeStatus)
|
|
Ind.Decrement()
|
|
oa2 = buildGAPIObject(API.OAUTH2)
|
|
printMessage(Msg.SERVICE_ACCOUNT_PRIVATE_KEY_AUTHENTICATION)
|
|
# We are explicitly not doing DwD here, just confirming service account can auth
|
|
auth_error = ''
|
|
try:
|
|
request = transportCreateRequest()
|
|
credentials.refresh(request)
|
|
sa_token_info = callGAPI(oa2, 'tokeninfo', access_token=credentials.token)
|
|
if sa_token_info:
|
|
saTokenStatus = testPass
|
|
else:
|
|
saTokenStatus = testFail
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
handleServerError(e)
|
|
except google.auth.exceptions.RefreshError as e:
|
|
saTokenStatus = testFail
|
|
if isinstance(e.args, tuple):
|
|
e = e.args[0]
|
|
auth_error = ' - '+str(e)
|
|
Ind.Increment()
|
|
printPassFail(f'Authentication{auth_error}', saTokenStatus)
|
|
Ind.Decrement()
|
|
if saTokenStatus == testFail:
|
|
invalidOauth2serviceJsonExit(f'Authentication{auth_error}')
|
|
_getSvcAcctData() # needed to read in GM.OAUTH2SERVICE_JSON_DATA
|
|
if GM.Globals[GM.SVCACCT_SCOPES_DEFINED] and API.IAM not in GM.Globals[GM.SVCACCT_SCOPES]:
|
|
GM.Globals[GM.SVCACCT_SCOPES][API.IAM] = [API.CLOUD_PLATFORM_SCOPE]
|
|
key_type = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
|
|
if key_type == 'default':
|
|
printMessage(Msg.SERVICE_ACCOUNT_CHECK_PRIVATE_KEY_AGE)
|
|
_, iam = buildGAPIServiceObject(API.IAM, None)
|
|
currentPrivateKeyId, projectId, _, clientId = _getSvcAcctKeyProjectClientFields()
|
|
name = f'projects/{projectId}/serviceAccounts/{clientId}/keys/{currentPrivateKeyId}'
|
|
Ind.Increment()
|
|
try:
|
|
key = callGAPI(iam.projects().serviceAccounts().keys(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
|
|
name=name, fields='validAfterTime')
|
|
key_created, _ = iso8601.parse_date(key['validAfterTime'])
|
|
key_age = todaysTime()-key_created
|
|
printPassFail(Msg.SERVICE_ACCOUNT_PRIVATE_KEY_AGE.format(key_age.days), testWarn if key_age.days > 30 else testPass)
|
|
except GAPI.permissionDenied:
|
|
printMessage(Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
|
printPassFail(Msg.SERVICE_ACCOUNT_PRIVATE_KEY_AGE.format('UNKNOWN'), testWarn)
|
|
except (GAPI.badRequest, GAPI.invalid, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.PROJECT, GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['project_id'],
|
|
Ent.SVCACCT, GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email']],
|
|
str(e))
|
|
printPassFail(Msg.SERVICE_ACCOUNT_PRIVATE_KEY_AGE.format('UNKNOWN'), testWarn)
|
|
else:
|
|
printPassFail(Msg.SERVICE_ACCOUNT_SKIPPING_KEY_AGE_CHECK.format(key_type), testPass)
|
|
Ind.Decrement()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
allScopesPass = True
|
|
user = convertUIDtoEmailAddress(user)
|
|
printKeyValueListWithCount([Msg.DOMAIN_WIDE_DELEGATION_AUTHENTICATION, '',
|
|
Ent.Singular(Ent.USER), user,
|
|
Ent.Choose(Ent.SCOPE, jcount), jcount],
|
|
i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for scope in checkScopes:
|
|
j += 1
|
|
# try with and without email scope
|
|
for scopes in [[scope, API.USERINFO_EMAIL_SCOPE], [scope]]:
|
|
try:
|
|
credentials = getSvcAcctCredentials(scopes, user)
|
|
credentials.refresh(request)
|
|
break
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
handleServerError(e)
|
|
except google.auth.exceptions.RefreshError:
|
|
continue
|
|
if credentials.token:
|
|
token_info = callGAPI(oa2, 'tokeninfo', access_token=credentials.token)
|
|
if scope in token_info.get('scope', '').split(' ') and user == token_info.get('email', user).lower():
|
|
scopeStatus = testPass
|
|
else:
|
|
scopeStatus = testFail
|
|
allScopesPass = False
|
|
else:
|
|
scopeStatus = testFail
|
|
allScopesPass = False
|
|
printPassFail(scope, f'{scopeStatus}{currentCount(j, jcount)}')
|
|
Ind.Decrement()
|
|
service_account = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id']
|
|
if allScopesPass:
|
|
if Act.Get() == Act.CHECK:
|
|
printLine(Msg.SCOPE_AUTHORIZATION_PASSED.format(service_account))
|
|
else:
|
|
authorizeScopes(Msg.SCOPE_AUTHORIZATION_UPDATE_PASSED)
|
|
else:
|
|
# Tack on email scope for more accurate checking
|
|
checkScopes.append(API.USERINFO_EMAIL_SCOPE)
|
|
setSysExitRC(SCOPES_NOT_AUTHORIZED_RC)
|
|
authorizeScopes(Msg.SCOPE_AUTHORIZATION_FAILED)
|
|
printBlankLine()
|
|
|
|
# gam check svcacct <UserTypeEntity> (scope|scopes <APIScopeURLList>)*
|
|
# gam update svcacct <UserTypeEntity> (scope|scopes <APIScopeURLList>)*
|
|
def doCheckUpdateSvcAcct():
|
|
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USER)
|
|
checkServiceAccount(entityList)
|
|
|
|
def _getSAKeys(iam, projectId, clientEmail, name, keyTypes):
|
|
try:
|
|
keys = callGAPIitems(iam.projects().serviceAccounts().keys(), 'list', 'keys',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
|
|
name=name, fields='*', keyTypes=keyTypes)
|
|
return (True, keys)
|
|
except GAPI.permissionDenied:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
|
except GAPI.badRequest as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
|
return (False, None)
|
|
|
|
SVCACCT_KEY_TIME_OBJECTS = {'validAfterTime', 'validBeforeTime'}
|
|
|
|
def _showSAKeys(keys, count, currentPrivateKeyId):
|
|
Ind.Increment()
|
|
i = 0
|
|
for key in keys:
|
|
i += 1
|
|
keyName = key.pop('name').rsplit('/', 1)[-1]
|
|
printKeyValueListWithCount(['name', keyName], i, count)
|
|
Ind.Increment()
|
|
for k, v in sorted(iter(key.items())):
|
|
if k not in SVCACCT_KEY_TIME_OBJECTS:
|
|
printKeyValueList([k, v])
|
|
else:
|
|
printKeyValueList([k, formatLocalTime(v)])
|
|
if keyName == currentPrivateKeyId:
|
|
printKeyValueList(['usedToAuthenticateThisRequest', True])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
SVCACCT_DISPLAY_FIELDS = ['displayName', 'description', 'oauth2ClientId', 'uniqueId', 'disabled']
|
|
SVCACCT_KEY_TYPE_CHOICE_MAP = {
|
|
'all': None,
|
|
'system': 'SYSTEM_MANAGED',
|
|
'systemmanaged': 'SYSTEM_MANAGED',
|
|
'user': 'USER_MANAGED',
|
|
'usermanaged': 'USER_MANAGED'
|
|
}
|
|
|
|
# gam print svcaccts [[admin] <EmailAddress>] [all|<ProjectIDEntity>]
|
|
# [showsakeys all|system|user]
|
|
# [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]
|
|
# gam show svcaccts [<EmailAddress>] [all|<ProjectIDEntity>]
|
|
# [showsakeys all|system|user]
|
|
def doPrintShowSvcAccts():
|
|
_, httpObj, login_hint, projects = _getLoginHintProjects(printShowCmd=True, readOnly=False)
|
|
csvPF = CSVPrintFile(['User', 'projectId']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
iam = getAPIService(API.IAM, httpObj)
|
|
keyTypes = None
|
|
showSAKeys = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'showsakeys':
|
|
keyTypes = getChoice(SVCACCT_KEY_TYPE_CHOICE_MAP, mapChoice=True)
|
|
showSAKeys = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
count = len(projects)
|
|
if not csvPF:
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, login_hint], Ent.SVCACCT, Act.MODIFIER_FOR, count, Ent.PROJECT)
|
|
else:
|
|
csvPF.AddTitles(['projectId']+SVCACCT_DISPLAY_FIELDS)
|
|
csvPF.SetSortAllTitles()
|
|
i = 0
|
|
for project in projects:
|
|
i += 1
|
|
if not _checkProjectFound(project, i, count):
|
|
continue
|
|
projectId = project['projectId']
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(Ent.SVCACCT, projectId, i, count)
|
|
if project['state'] != 'ACTIVE':
|
|
entityActionNotPerformedWarning([Ent.PROJECT, projectId], Msg.DELETED, i, count)
|
|
continue
|
|
try:
|
|
svcAccts = callGAPIpages(iam.projects().serviceAccounts(), 'list', 'accounts',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
|
|
name=f'projects/{projectId}')
|
|
jcount = len(svcAccts)
|
|
if not csvPF:
|
|
entityPerformActionNumItems([Ent.PROJECT, projectId], jcount, Ent.SVCACCT, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for svcAcct in svcAccts:
|
|
j += 1
|
|
printKeyValueListWithCount(['email', svcAcct['email']], j, jcount)
|
|
Ind.Increment()
|
|
for field in SVCACCT_DISPLAY_FIELDS:
|
|
if field in svcAcct:
|
|
printKeyValueList([field, svcAcct[field]])
|
|
if showSAKeys:
|
|
name = f"projects/{projectId}/serviceAccounts/{svcAcct['oauth2ClientId']}"
|
|
status, keys = _getSAKeys(iam, projectId, svcAcct['email'], name, keyTypes)
|
|
if status:
|
|
kcount = len(keys)
|
|
if kcount > 0:
|
|
printKeyValueList([Ent.Choose(Ent.SVCACCT_KEY, kcount), kcount])
|
|
_showSAKeys(keys, kcount, '')
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
for svcAcct in svcAccts:
|
|
if showSAKeys:
|
|
name = f"projects/{projectId}/serviceAccounts/{svcAcct['oauth2ClientId']}"
|
|
status, keys = _getSAKeys(iam, projectId, svcAcct['email'], name, keyTypes)
|
|
if status:
|
|
svcAcct['keys'] = keys
|
|
row = flattenJSON(svcAcct, flattened={'User': login_hint}, timeObjects=SVCACCT_KEY_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'User': login_hint, 'projectId': projectId,
|
|
'JSON': json.dumps(cleanJSON(svcAcct, timeObjects=SVCACCT_KEY_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
except (GAPI.notFound, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId], str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Service Accounts')
|
|
|
|
def _generatePrivateKeyAndPublicCert(projectId, clientEmail, name, key_size, b64enc_pub=True, validityHours=0):
|
|
if projectId:
|
|
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.GENERATING_NEW_PRIVATE_KEY)
|
|
else:
|
|
writeStdout(Msg.GENERATING_NEW_PRIVATE_KEY+'\n')
|
|
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()
|
|
|
|
if projectId:
|
|
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.EXTRACTING_PUBLIC_CERTIFICATE)
|
|
else:
|
|
writeStdout(Msg.EXTRACTING_PUBLIC_CERTIFICATE+'\n')
|
|
public_key = private_key.public_key()
|
|
builder = x509.CertificateBuilder()
|
|
# suppress cryptography warnings on service account email length
|
|
with warnings.catch_warnings():
|
|
warnings.filterwarnings('ignore', message='.*Attribute\'s length.*')
|
|
builder = builder.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,
|
|
name,
|
|
_validate=False)]))
|
|
builder = builder.issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,
|
|
name,
|
|
_validate=False)]))
|
|
# Gooogle seems to enforce the not before date strictly. Set the not before
|
|
# date to be UTC two minutes ago which should cover any clock skew.
|
|
now = datetime.datetime.utcnow()
|
|
builder = builder.not_valid_before(now - datetime.timedelta(minutes=2))
|
|
# Google defaults to 12/31/9999 date for end time if there's no
|
|
# policy to restrict key age
|
|
if validityHours:
|
|
expires = now + datetime.timedelta(hours=validityHours) - datetime.timedelta(minutes=2)
|
|
builder = builder.not_valid_after(expires)
|
|
else:
|
|
builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
|
|
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()
|
|
if projectId:
|
|
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.DONE_GENERATING_PRIVATE_KEY_AND_PUBLIC_CERTIFICATE)
|
|
else:
|
|
writeStdout(Msg.DONE_GENERATING_PRIVATE_KEY_AND_PUBLIC_CERTIFICATE+'\n')
|
|
if not b64enc_pub:
|
|
return (private_pem, public_cert_pem)
|
|
publicKeyData = base64.b64encode(public_cert_pem.encode())
|
|
if isinstance(publicKeyData, bytes):
|
|
publicKeyData = publicKeyData.decode()
|
|
return (private_pem, publicKeyData)
|
|
|
|
def _formatOAuth2ServiceData(service_data):
|
|
quotedEmail = quote(service_data.get('client_email', ''))
|
|
service_data['auth_provider_x509_cert_url'] = API.GOOGLE_AUTH_PROVIDER_X509_CERT_URL
|
|
service_data['auth_uri'] = API.GOOGLE_OAUTH2_ENDPOINT
|
|
service_data['client_x509_cert_url'] = f'https://www.googleapis.com/robot/v1/metadata/x509/{quotedEmail}'
|
|
service_data['token_uri'] = API.GOOGLE_OAUTH2_TOKEN_ENDPOINT
|
|
service_data['type'] = 'service_account'
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA] = service_data.copy()
|
|
return json.dumps(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA], indent=2, sort_keys=True)
|
|
|
|
def doProcessSvcAcctKeys(mode=None, iam=None, projectId=None, clientEmail=None, clientId=None):
|
|
def getSAKeyParms(body, new_data):
|
|
nonlocal local_key_size, validityHours
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'algorithm':
|
|
body['keyAlgorithm'] = getChoice(["key_alg_rsa_1024", "key_alg_rsa_2048"]).upper()
|
|
local_key_size = 0
|
|
elif myarg == 'localkeysize':
|
|
local_key_size = int(getChoice(['1024', '2048', '4096']))
|
|
elif myarg == 'validityhours':
|
|
validityHours = getInteger()
|
|
elif myarg == 'yubikey':
|
|
new_data['key_type'] = 'yubikey'
|
|
elif myarg == 'yubikeyslot':
|
|
new_data['yubikey_slot'] = getString(Cmd.OB_STRING).upper()
|
|
elif myarg == 'yubikeypin':
|
|
new_data['yubikey_pin'] = readStdin('Enter your YubiKey PIN: ')
|
|
elif myarg == 'yubikeyserialnumber':
|
|
new_data['yubikey_serial_number'] = getInteger()
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
local_key_size = 2048
|
|
validityHours = 0
|
|
body = {}
|
|
if mode is None:
|
|
mode = getChoice(['retainnone', 'retainexisting', 'replacecurrent'])
|
|
if iam is None or mode == 'upload':
|
|
if iam is None:
|
|
_, iam = buildGAPIServiceObject(API.IAM, None)
|
|
_getSvcAcctData()
|
|
currentPrivateKeyId, projectId, clientEmail, clientId = _getSvcAcctKeyProjectClientFields()
|
|
# dict() ensures we have a real copy, not pointer
|
|
new_data = dict(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA])
|
|
# assume default key type unless we are told otherwise
|
|
new_data['key_type'] = 'default'
|
|
getSAKeyParms(body, new_data)
|
|
else:
|
|
new_data = {
|
|
'client_email': clientEmail,
|
|
'project_id': projectId,
|
|
'client_id': clientId,
|
|
'key_type': 'default'
|
|
}
|
|
getSAKeyParms(body, new_data)
|
|
name = f'projects/{projectId}/serviceAccounts/{clientId}'
|
|
if mode != 'retainexisting':
|
|
try:
|
|
keys = callGAPIitems(iam.projects().serviceAccounts().keys(), 'list', 'keys',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
|
|
name=name, keyTypes='USER_MANAGED')
|
|
except GAPI.permissionDenied:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
|
return False
|
|
except GAPI.badRequest as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
|
return False
|
|
if new_data.get('key_type') == 'yubikey':
|
|
# Use yubikey private key
|
|
new_data['yubikey_key_type'] = f'RSA{local_key_size}'
|
|
new_data.pop('private_key', None)
|
|
yk = yubikey.YubiKey(new_data)
|
|
if 'yubikey_serial_number' not in new_data:
|
|
new_data['yubikey_serial_number'] = yk.get_serial_number()
|
|
yk = yubikey.YubiKey(new_data)
|
|
if 'yubikey_slot' not in new_data:
|
|
new_data['yubikey_slot'] = 'AUTHENTICATION'
|
|
publicKeyData = yk.get_certificate()
|
|
elif local_key_size:
|
|
# Generate private key locally, store in file
|
|
new_data['private_key'], publicKeyData = _generatePrivateKeyAndPublicCert(projectId, clientEmail, name,
|
|
local_key_size, validityHours=validityHours)
|
|
new_data['key_type'] = 'default'
|
|
for key in list(new_data):
|
|
if key.startswith('yubikey_'):
|
|
new_data.pop(key, None)
|
|
if local_key_size:
|
|
Act.Set(Act.UPLOAD)
|
|
maxRetries = 10
|
|
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPLOADING_NEW_PUBLIC_CERTIFICATE_TO_GOOGLE)
|
|
for retry in range(1, maxRetries+1):
|
|
try:
|
|
result = callGAPI(iam.projects().serviceAccounts().keys(), 'upload',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name, body={'publicKeyData': publicKeyData})
|
|
newPrivateKeyId = result['name'].rsplit('/', 1)[-1]
|
|
break
|
|
except GAPI.notFound as e:
|
|
if retry == maxRetries:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
|
return False
|
|
_waitForSvcAcctCompletion(retry)
|
|
except GAPI.permissionDenied:
|
|
if retry == maxRetries:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
|
return False
|
|
_waitForSvcAcctCompletion(retry)
|
|
except GAPI.badRequest as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
|
return False
|
|
except GAPI.failedPrecondition as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
|
if 'iam.disableServiceAccountKeyUpload' not in str(e) and 'iam.managed.disableServiceAccountKeyUpload' not in str(e):
|
|
return False
|
|
if retry == maxRetries or mode != 'upload':
|
|
sys.stdout.write(Msg.ENABLE_SERVICE_ACCOUNT_PRIVATE_KEY_UPLOAD.format(projectId))
|
|
new_data['private_key'] = ''
|
|
newPrivateKeyId = ''
|
|
break
|
|
_waitForSvcAcctCompletion(retry)
|
|
new_data['private_key_id'] = newPrivateKeyId
|
|
oauth2service_data = _formatOAuth2ServiceData(new_data)
|
|
else:
|
|
Act.Set(Act.CREATE)
|
|
maxRetries = 10
|
|
for retry in range(1, maxRetries+1):
|
|
try:
|
|
result = callGAPI(iam.projects().serviceAccounts().keys(), 'create',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
|
|
name=name, body=body)
|
|
newPrivateKeyId = result['name'].rsplit('/', 1)[-1]
|
|
break
|
|
except GAPI.permissionDenied:
|
|
if retry == maxRetries:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
|
return False
|
|
_waitForSvcAcctCompletion(retry)
|
|
except GAPI.badRequest as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
|
return False
|
|
oauth2service_data = base64.b64decode(result['privateKeyData']).decode(UTF8)
|
|
if newPrivateKeyId != '':
|
|
entityActionPerformed([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail, Ent.SVCACCT_KEY, newPrivateKeyId])
|
|
if GM.Globals[GM.SVCACCT_SCOPES_DEFINED]:
|
|
try:
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA] = json.loads(oauth2service_data)
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
invalidOauth2serviceJsonExit(str(e))
|
|
GM.Globals[GM.OAUTH2SERVICE_JSON_DATA][API.OAUTH2SA_SCOPES] = GM.Globals[GM.SVCACCT_SCOPES]
|
|
oauth2service_data = json.dumps(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA], ensure_ascii=False, sort_keys=True, indent=2)
|
|
writeFile(GC.Values[GC.OAUTH2SERVICE_JSON], oauth2service_data, continueOnError=False)
|
|
Act.Set(Act.UPDATE)
|
|
entityActionPerformed([Ent.OAUTH2SERVICE_JSON_FILE, GC.Values[GC.OAUTH2SERVICE_JSON],
|
|
Ent.SVCACCT_KEY, newPrivateKeyId])
|
|
if mode in {'retainexisting', 'upload'}:
|
|
return newPrivateKeyId != ''
|
|
Act.Set(Act.REVOKE)
|
|
count = len(keys) if mode == 'retainnone' else 1
|
|
entityPerformActionNumItems([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], count, Ent.SVCACCT_KEY)
|
|
Ind.Increment()
|
|
i = 0
|
|
for key in keys:
|
|
keyName = key['name'].rsplit('/', 1)[-1]
|
|
if mode == 'retainnone' or keyName == currentPrivateKeyId and keyName != newPrivateKeyId:
|
|
i += 1
|
|
maxRetries = 5
|
|
for retry in range(1, maxRetries+1):
|
|
try:
|
|
callGAPI(iam.projects().serviceAccounts().keys(), 'delete',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
|
|
name=key['name'])
|
|
entityActionPerformed([Ent.SVCACCT_KEY, keyName], i, count)
|
|
break
|
|
except GAPI.permissionDenied:
|
|
if retry == maxRetries:
|
|
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
|
break
|
|
_waitForSvcAcctCompletion(retry)
|
|
except GAPI.badRequest as e:
|
|
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], str(e), i, count)
|
|
break
|
|
if mode != 'retainnone':
|
|
break
|
|
Ind.Decrement()
|
|
return True
|
|
|
|
# gam create sakey|sakeys
|
|
# gam rotate sakey|sakeys retain_existing
|
|
# (algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
|
|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
|
|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)
|
|
def doCreateSvcAcctKeys():
|
|
doProcessSvcAcctKeys(mode='retainexisting')
|
|
|
|
# gam update sakey|sakeys
|
|
# gam rotate sakey|sakeys replace_current
|
|
# (algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
|
|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
|
|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)
|
|
def doUpdateSvcAcctKeys():
|
|
doProcessSvcAcctKeys(mode='replacecurrent')
|
|
|
|
# gam replace sakey|sakeys
|
|
# gam rotate sakey|sakeys retain_none
|
|
# (algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
|
|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
|
|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)
|
|
def doReplaceSvcAcctKeys():
|
|
doProcessSvcAcctKeys(mode='retainnone')
|
|
|
|
# gam upload sakey|sakeys [admin <EmailAddress>]
|
|
# (algorithm KEY_ALG_RSA_1024|KEY_ALG_RSA_2048)|
|
|
# (localkeysize 1024|2048|4096 [validityhours <Number>])|
|
|
# (yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikey_serialnumber <String>)
|
|
def doUploadSvcAcctKeys():
|
|
login_hint = getEmailAddress(noUid=True) if checkArgumentPresent(['admin']) else None
|
|
httpObj, _ = getCRMService(login_hint)
|
|
iam = getAPIService(API.IAM, httpObj)
|
|
if doProcessSvcAcctKeys(mode='upload', iam=iam):
|
|
sa_email = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_email']
|
|
_grantRotateRights(iam, GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['project_id'], sa_email, sa_email)
|
|
sys.stdout.write(Msg.YOUR_GAM_PROJECT_IS_CREATED_AND_READY_TO_USE)
|
|
|
|
# gam delete sakeys <ServiceAccountKeyList>
|
|
def doDeleteSvcAcctKeys():
|
|
_, iam = buildGAPIServiceObject(API.IAM, None)
|
|
doit = False
|
|
keyList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'doit':
|
|
doit = True
|
|
else:
|
|
Cmd.Backup()
|
|
keyList.extend(getString(Cmd.OB_SERVICE_ACCOUNT_KEY_LIST, minLen=0).strip().replace(',', ' ').split())
|
|
currentPrivateKeyId, projectId, clientEmail, clientId = _getSvcAcctKeyProjectClientFields()
|
|
name = f'projects/{projectId}/serviceAccounts/{clientId}'
|
|
try:
|
|
keys = callGAPIitems(iam.projects().serviceAccounts().keys(), 'list', 'keys',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
|
|
name=name, keyTypes='USER_MANAGED')
|
|
except GAPI.permissionDenied:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
|
return
|
|
except GAPI.badRequest as e:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
|
return
|
|
Act.Set(Act.REVOKE)
|
|
count = len(keyList)
|
|
entityPerformActionNumItems([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], count, Ent.SVCACCT_KEY)
|
|
Ind.Increment()
|
|
i = 0
|
|
for dkeyName in keyList:
|
|
i += 1
|
|
for key in keys:
|
|
keyName = key['name'].rsplit('/', 1)[-1]
|
|
if keyName == dkeyName:
|
|
if keyName == currentPrivateKeyId and not doit:
|
|
entityActionNotPerformedWarning([Ent.SVCACCT_KEY, keyName],
|
|
Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION+Msg.ON_CURRENT_PRIVATE_KEY, i, count)
|
|
break
|
|
try:
|
|
callGAPI(iam.projects().serviceAccounts().keys(), 'delete',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.PERMISSION_DENIED],
|
|
name=key['name'])
|
|
entityActionPerformed([Ent.SVCACCT_KEY, keyName], i, count)
|
|
except GAPI.permissionDenied:
|
|
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
|
except GAPI.badRequest as e:
|
|
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], str(e), i, count)
|
|
break
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.SVCACCT_KEY, dkeyName], Msg.NOT_FOUND, i, count)
|
|
Ind.Decrement()
|
|
|
|
# gam show sakeys [all|system|user]
|
|
def doShowSvcAcctKeys():
|
|
_, iam = buildGAPIServiceObject(API.IAM, None)
|
|
keyTypes = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in SVCACCT_KEY_TYPE_CHOICE_MAP:
|
|
keyTypes = SVCACCT_KEY_TYPE_CHOICE_MAP[myarg]
|
|
else:
|
|
unknownArgumentExit()
|
|
currentPrivateKeyId, projectId, clientEmail, clientId = _getSvcAcctKeyProjectClientFields()
|
|
name = f'projects/{projectId}/serviceAccounts/{clientId}'
|
|
status, keys = _getSAKeys(iam, projectId, clientEmail, name, keyTypes)
|
|
if not status:
|
|
return
|
|
count = len(keys)
|
|
entityPerformActionNumItems([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], count, Ent.SVCACCT_KEY)
|
|
if count > 0:
|
|
_showSAKeys(keys, count, currentPrivateKeyId)
|
|
|
|
# gam create gcpserviceaccount|signjwtserviceaccount
|
|
def doCreateGCPServiceAccount():
|
|
checkForExtraneousArguments()
|
|
_checkForExistingProjectFiles([GC.Values[GC.OAUTH2SERVICE_JSON]])
|
|
sa_info = {'key_type': 'signjwt', 'token_uri': API.GOOGLE_OAUTH2_TOKEN_ENDPOINT, 'type': 'service_account'}
|
|
request = getTLSv1_2Request()
|
|
try:
|
|
credentials, sa_info['project_id'] = google.auth.default(scopes=[API.IAM_SCOPE], request=request)
|
|
except (google.auth.exceptions.DefaultCredentialsError, google.auth.exceptions.RefreshError) as e:
|
|
systemErrorExit(API_ACCESS_DENIED_RC, str(e))
|
|
credentials.refresh(request)
|
|
sa_info['client_email'] = credentials.service_account_email
|
|
oa2 = buildGAPIObjectNoAuthentication(API.OAUTH2)
|
|
try:
|
|
token_info = callGAPI(oa2, 'tokeninfo',
|
|
throwReasons=[GAPI.INVALID],
|
|
access_token=credentials.token)
|
|
except GAPI.invalid as e:
|
|
systemErrorExit(API_ACCESS_DENIED_RC, str(e))
|
|
sa_info['client_id'] = token_info['issued_to']
|
|
sa_output = json.dumps(sa_info, ensure_ascii=False, sort_keys=True, indent=2)
|
|
writeStdout(f'Writing SignJWT service account data:\n\n{sa_output}\n')
|
|
writeFile(GC.Values[GC.OAUTH2SERVICE_JSON], sa_output, continueOnError=False)
|
|
|
|
# Audit command utilities
|
|
def getAuditParameters(emailAddressRequired=True, requestIdRequired=True, destUserRequired=False):
|
|
auditObject = getEmailAuditObject()
|
|
emailAddress = getEmailAddress(noUid=True, optional=not emailAddressRequired)
|
|
parameters = {}
|
|
if emailAddress:
|
|
parameters['auditUser'] = emailAddress
|
|
parameters['auditUserName'], auditObject.domain = splitEmailAddress(emailAddress)
|
|
if requestIdRequired:
|
|
parameters['requestId'] = getString(Cmd.OB_REQUEST_ID)
|
|
if destUserRequired:
|
|
destEmailAddress = getEmailAddress(noUid=True)
|
|
parameters['auditDestUser'] = destEmailAddress
|
|
parameters['auditDestUserName'], destDomain = splitEmailAddress(destEmailAddress)
|
|
if auditObject.domain != destDomain:
|
|
Cmd.Backup()
|
|
invalidArgumentExit(f'{parameters["auditDestUserName"]}@{auditObject.domain}')
|
|
return (auditObject, parameters)
|
|
|
|
# Audit monitor command utilities
|
|
def _showMailboxMonitorRequestStatus(request, i=0, count=0):
|
|
printKeyValueListWithCount(['Destination', normalizeEmailAddressOrUID(request['destUserName'])], i, count)
|
|
Ind.Increment()
|
|
printKeyValueList(['Begin', request.get('beginDate', 'immediately')])
|
|
printKeyValueList(['End', request['endDate']])
|
|
printKeyValueList(['Monitor Incoming', request['outgoingEmailMonitorLevel']])
|
|
printKeyValueList(['Monitor Outgoing', request['incomingEmailMonitorLevel']])
|
|
printKeyValueList(['Monitor Chats', request.get('chatMonitorLevel', 'NONE')])
|
|
printKeyValueList(['Monitor Drafts', request.get('draftMonitorLevel', 'NONE')])
|
|
Ind.Decrement()
|
|
|
|
# gam audit monitor create <EmailAddress> <DestEmailAddress> [begin <DateTime>] [end <DateTime>] [incoming_headers] [outgoing_headers] [nochats] [nodrafts] [chat_headers] [draft_headers]
|
|
def doCreateMonitor():
|
|
auditObject, parameters = getAuditParameters(emailAddressRequired=True, requestIdRequired=False, destUserRequired=True)
|
|
#end_date defaults to 30 days in the future...
|
|
end_date = (GM.Globals[GM.DATETIME_NOW]+datetime.timedelta(days=30)).strftime(YYYYMMDD_HHMM_FORMAT)
|
|
begin_date = None
|
|
incoming_headers_only = outgoing_headers_only = drafts_headers_only = chats_headers_only = False
|
|
drafts = chats = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'begin':
|
|
begin_date = getYYYYMMDD_HHMM()
|
|
elif myarg == 'end':
|
|
end_date = getYYYYMMDD_HHMM()
|
|
elif myarg == 'incomingheaders':
|
|
incoming_headers_only = True
|
|
elif myarg == 'outgoingheaders':
|
|
outgoing_headers_only = True
|
|
elif myarg == 'nochats':
|
|
chats = False
|
|
elif myarg == 'nodrafts':
|
|
drafts = False
|
|
elif myarg == 'chatheaders':
|
|
chats_headers_only = True
|
|
elif myarg == 'draftheaders':
|
|
drafts_headers_only = True
|
|
else:
|
|
unknownArgumentExit()
|
|
try:
|
|
request = callGData(auditObject, 'createEmailMonitor',
|
|
throwErrors=[GDATA.INVALID_VALUE, GDATA.INVALID_INPUT, GDATA.DOES_NOT_EXIST, GDATA.INVALID_DOMAIN],
|
|
source_user=parameters['auditUserName'], destination_user=parameters['auditDestUserName'], end_date=end_date, begin_date=begin_date,
|
|
incoming_headers_only=incoming_headers_only, outgoing_headers_only=outgoing_headers_only,
|
|
drafts=drafts, drafts_headers_only=drafts_headers_only, chats=chats, chats_headers_only=chats_headers_only)
|
|
entityActionPerformed([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None])
|
|
Ind.Increment()
|
|
_showMailboxMonitorRequestStatus(request)
|
|
Ind.Decrement()
|
|
except (GDATA.invalidValue, GDATA.invalidInput) as e:
|
|
entityActionFailedWarning([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None], str(e))
|
|
except (GDATA.doesNotExist, GDATA.invalidDomain) as e:
|
|
if str(e).find(parameters['auditUser']) != -1:
|
|
entityUnknownWarning(Ent.USER, parameters['auditUser'])
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None], str(e))
|
|
|
|
# gam audit monitor delete <EmailAddress> <DestEmailAddress>
|
|
def doDeleteMonitor():
|
|
auditObject, parameters = getAuditParameters(emailAddressRequired=True, requestIdRequired=False, destUserRequired=True)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGData(auditObject, 'deleteEmailMonitor',
|
|
throwErrors=[GDATA.INVALID_INPUT, GDATA.DOES_NOT_EXIST, GDATA.INVALID_DOMAIN],
|
|
source_user=parameters['auditUserName'], destination_user=parameters['auditDestUserName'])
|
|
entityActionPerformed([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, parameters['auditDestUser']])
|
|
except GDATA.invalidInput as e:
|
|
entityActionFailedWarning([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None], str(e))
|
|
except (GDATA.doesNotExist, GDATA.invalidDomain) as e:
|
|
if str(e).find(parameters['auditUser']) != -1:
|
|
entityUnknownWarning(Ent.USER, parameters['auditUser'])
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, parameters['auditUser'], Ent.AUDIT_MONITOR_REQUEST, None], str(e))
|
|
|
|
# gam audit monitor list <EmailAddress>
|
|
def doShowMonitors():
|
|
auditObject, parameters = getAuditParameters(emailAddressRequired=True, requestIdRequired=False, destUserRequired=False)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
results = callGData(auditObject, 'getEmailMonitors',
|
|
throwErrors=[GDATA.DOES_NOT_EXIST, GDATA.INVALID_DOMAIN],
|
|
user=parameters['auditUserName'])
|
|
jcount = len(results) if (results) else 0
|
|
entityPerformActionNumItems([Ent.USER, parameters['auditUser']], jcount, Ent.AUDIT_MONITOR_REQUEST)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return
|
|
Ind.Increment()
|
|
j = 0
|
|
for request in results:
|
|
j += 1
|
|
_showMailboxMonitorRequestStatus(request, j, jcount)
|
|
Ind.Decrement()
|
|
except (GDATA.doesNotExist, GDATA.invalidDomain):
|
|
entityUnknownWarning(Ent.USER, parameters['auditUser'])
|
|
|
|
# gam whatis <EmailItem> [noinfo] [noinvitablecheck]
|
|
def doWhatIs():
|
|
def _showPrimaryType(entityType, email):
|
|
printEntity([entityType, email])
|
|
|
|
def _showAliasType(entityType, email, primaryEntityType, primaryEmail):
|
|
printEntity([entityType, email, primaryEntityType, primaryEmail])
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
email = getEmailAddress()
|
|
showInfo = invitableCheck = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'noinfo':
|
|
showInfo = False
|
|
elif myarg == 'noinvitablecheck':
|
|
invitableCheck = False
|
|
else:
|
|
unknownArgumentExit()
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=email, fields='id,primaryEmail')
|
|
if (result['primaryEmail'].lower() == email) or (result['id'] == email):
|
|
if showInfo:
|
|
infoUsers(entityList=[email])
|
|
else:
|
|
_showPrimaryType(Ent.USER, email)
|
|
setSysExitRC(ENTITY_IS_A_USER_RC)
|
|
else:
|
|
if showInfo:
|
|
infoAliases(entityList=[email])
|
|
else:
|
|
_showAliasType(Ent.USER_ALIAS, email, Ent.USER, result['primaryEmail'])
|
|
setSysExitRC(ENTITY_IS_A_USER_ALIAS_RC)
|
|
return
|
|
except (GAPI.userNotFound, GAPI.badRequest):
|
|
pass
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.backendError, GAPI.systemError):
|
|
entityUnknownWarning(Ent.EMAIL, email)
|
|
setSysExitRC(ENTITY_IS_UKNOWN_RC)
|
|
return
|
|
try:
|
|
result = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
|
|
groupKey=email, fields='id,email')
|
|
if (result['email'].lower() == email) or (result['id'] == email):
|
|
if showInfo:
|
|
infoGroups([email])
|
|
else:
|
|
_showPrimaryType(Ent.GROUP, email)
|
|
setSysExitRC(ENTITY_IS_A_GROUP_RC)
|
|
else:
|
|
if showInfo:
|
|
infoAliases(entityList=[email])
|
|
else:
|
|
_showAliasType(Ent.GROUP_ALIAS, email, Ent.GROUP, result['email'])
|
|
setSysExitRC(ENTITY_IS_A_GROUP_ALIAS_RC)
|
|
return
|
|
except (GAPI.groupNotFound, GAPI.forbidden):
|
|
pass
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.badRequest):
|
|
entityUnknownWarning(Ent.EMAIL, email)
|
|
setSysExitRC(ENTITY_IS_UKNOWN_RC)
|
|
return
|
|
if not invitableCheck or not getSvcAcctCredentials(API.CLOUDIDENTITY_USERINVITATIONS, _getAdminEmail(), softErrors=True):
|
|
isInvitableUser = False
|
|
else:
|
|
isInvitableUser, ci = _getIsInvitableUser(None, email)
|
|
if isInvitableUser:
|
|
if showInfo:
|
|
name, user, ci = _getCIUserInvitationsEntity(ci, email)
|
|
infoCIUserInvitations(name, user, ci, None)
|
|
else:
|
|
_showPrimaryType(Ent.USER_INVITATION, email)
|
|
setSysExitRC(ENTITY_IS_AN_UNMANAGED_ACCOUNT_RC)
|
|
else:
|
|
entityUnknownWarning(Ent.EMAIL, email)
|
|
setSysExitRC(ENTITY_IS_UKNOWN_RC)
|
|
|
|
def _adjustTryDate(errMsg, numDateChanges, limitDateChanges, prevTryDate):
|
|
match_date = re.match('Data for dates later than (.*) is not yet available. Please check back later', errMsg)
|
|
if match_date:
|
|
tryDate = match_date.group(1)
|
|
else:
|
|
match_date = re.match('Start date can not be later than (.*)', errMsg)
|
|
if match_date:
|
|
tryDate = match_date.group(1)
|
|
else:
|
|
match_date = re.match('End date greater than LastReportedDate.', errMsg)
|
|
if match_date:
|
|
tryDateTime = datetime.datetime.strptime(prevTryDate, YYYYMMDD_FORMAT)-datetime.timedelta(days=1)
|
|
tryDate = tryDateTime.strftime(YYYYMMDD_FORMAT)
|
|
if (not match_date) or (numDateChanges > limitDateChanges >= 0):
|
|
printWarningMessage(DATA_NOT_AVALIABLE_RC, errMsg)
|
|
return None
|
|
return tryDate
|
|
|
|
def _checkDataRequiredServices(result, tryDate, dataRequiredServices, parameterServices=None, checkUserEmail=False):
|
|
# -1: Data not available:
|
|
# 0: Backup to earlier date
|
|
# 1: Data available
|
|
oneDay = datetime.timedelta(days=1)
|
|
dataWarnings = result.get('warnings', [])
|
|
usageReports = result.get('usageReports', [])
|
|
# move to day before if we don't have at least one usageReport with parameters
|
|
if not usageReports or not usageReports[0].get('parameters', []):
|
|
tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
|
|
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
|
|
for warning in dataWarnings:
|
|
if warning['code'] == 'PARTIAL_DATA_AVAILABLE':
|
|
for app in warning['data']:
|
|
if app['key'] == 'application' and app['value'] != 'docs' and app['value'] in dataRequiredServices:
|
|
tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
|
|
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
|
|
elif warning['code'] == 'DATA_NOT_AVAILABLE':
|
|
for app in warning['data']:
|
|
if app['key'] == 'application' and app['value'] != 'docs' and app['value'] in dataRequiredServices:
|
|
return (-1, tryDate, None)
|
|
if parameterServices:
|
|
requiredServices = parameterServices.copy()
|
|
for item in usageReports[0].get('parameters', []):
|
|
if 'name' not in item:
|
|
continue
|
|
service, _ = item['name'].split(':', 1)
|
|
if service in requiredServices:
|
|
requiredServices.remove(service)
|
|
if not requiredServices:
|
|
break
|
|
else:
|
|
tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
|
|
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
|
|
if checkUserEmail:
|
|
if 'entity' not in usageReports[0] or 'userEmail' not in usageReports[0]['entity']:
|
|
tryDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)-oneDay
|
|
return (0, tryDateTime.strftime(YYYYMMDD_FORMAT), None)
|
|
return (1, tryDate, usageReports)
|
|
|
|
CUSTOMER_REPORT_SERVICES = {
|
|
'accounts',
|
|
'app_maker',
|
|
'apps_scripts',
|
|
'calendar',
|
|
'chat',
|
|
'classroom',
|
|
'cros',
|
|
'device_management',
|
|
'docs',
|
|
'drive',
|
|
'gmail',
|
|
'gplus',
|
|
'meet',
|
|
'sites',
|
|
}
|
|
|
|
USER_REPORT_SERVICES = {
|
|
'accounts',
|
|
'chat',
|
|
'classroom',
|
|
'docs',
|
|
'drive',
|
|
'gmail',
|
|
'gplus',
|
|
}
|
|
|
|
CUSTOMER_USER_CHOICES = {'customer', 'user'}
|
|
|
|
# gam report usageparameters customer|user [todrive <ToDriveAttribute>*]
|
|
def doReportUsageParameters():
|
|
report = getChoice(CUSTOMER_USER_CHOICES)
|
|
csvPF = CSVPrintFile(['parameter'], 'sortall')
|
|
getTodriveOnly(csvPF)
|
|
rep = buildGAPIObject(API.REPORTS)
|
|
if report == 'customer':
|
|
service = rep.customerUsageReports()
|
|
dataRequiredServices = CUSTOMER_REPORT_SERVICES
|
|
kwargs = {}
|
|
else: # 'user'
|
|
service = rep.userUsageReport()
|
|
dataRequiredServices = USER_REPORT_SERVICES
|
|
kwargs = {'userKey': _getAdminEmail()}
|
|
customerId = GC.Values[GC.CUSTOMER_ID]
|
|
if customerId == GC.MY_CUSTOMER:
|
|
customerId = None
|
|
tryDate = todaysDate().strftime(YYYYMMDD_FORMAT)
|
|
allParameters = set()
|
|
while True:
|
|
try:
|
|
result = callGAPI(service, 'get',
|
|
throwReasons=[GAPI.INVALID, GAPI.BAD_REQUEST],
|
|
date=tryDate, customerId=customerId, fields='warnings,usageReports(parameters(name))', **kwargs)
|
|
fullData, tryDate, usageReports = _checkDataRequiredServices(result, tryDate, dataRequiredServices)
|
|
if fullData < 0:
|
|
printWarningMessage(DATA_NOT_AVALIABLE_RC, Msg.NO_USAGE_PARAMETERS_DATA_AVAILABLE)
|
|
return
|
|
if usageReports:
|
|
for parameter in usageReports[0]['parameters']:
|
|
name = parameter.get('name')
|
|
if name:
|
|
allParameters.add(name)
|
|
if fullData == 1:
|
|
break
|
|
except GAPI.badRequest:
|
|
printErrorMessage(BAD_REQUEST_RC, Msg.BAD_REQUEST)
|
|
return
|
|
except GAPI.invalid as e:
|
|
tryDate = _adjustTryDate(str(e), 0, -1, tryDate)
|
|
if not tryDate:
|
|
break
|
|
for parameter in sorted(allParameters):
|
|
csvPF.WriteRow({'parameter': parameter})
|
|
csvPF.writeCSVfile(f'{report.capitalize()} Report Usage Parameters')
|
|
|
|
def getUserOrgUnits(cd, orgUnit, orgUnitId):
|
|
try:
|
|
if orgUnit == orgUnitId:
|
|
orgUnit = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnit, fields='orgUnitPath')['orgUnitPath']
|
|
printGettingAllEntityItemsForWhom(Ent.USER, orgUnit, qualifier=Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)),
|
|
entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
result = callGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
|
|
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnit, None), orderBy='email',
|
|
fields='nextPageToken,users(primaryEmail,orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
|
|
userOrgUnits = {}
|
|
for user in result:
|
|
userOrgUnits[user['primaryEmail']] = user['orgUnitPath']
|
|
return userOrgUnits
|
|
except (GAPI.badRequest, GAPI.invalidInput, GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError,
|
|
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnit)
|
|
|
|
# Convert report mb item to gb
|
|
def convertReportMBtoGB(name, item):
|
|
if item is not None:
|
|
item['intValue'] = f"{int(item['intValue'])/1024:.2f}"
|
|
return name.replace('_in_mb', '_in_gb')
|
|
|
|
REPORTS_PARAMETERS_SIMPLE_TYPES = ['intValue', 'boolValue', 'datetimeValue', 'stringValue']
|
|
|
|
# gam report usage user [todrive <ToDriveAttribute>*]
|
|
# [(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath> [showorgunit])|(select <UserTypeEntity>)]
|
|
# [([start|startdate <Date>] [end|enddate <Date>])|(range <Date> <Date>)|
|
|
# thismonth|(previousmonths <Integer>)]
|
|
# [fields|parameters <String>)]
|
|
# [convertmbtogb]
|
|
# gam report usage customer [todrive <ToDriveAttribute>*]
|
|
# [([start|startdate <Date>] [end|enddate <Date>])|(range <Date> <Date>)|
|
|
# thismonth|(previousmonths <Integer>)]
|
|
# [fields|parameters <String>)]
|
|
# [convertmbtogb]
|
|
def doReportUsage():
|
|
def usageEntitySelectors():
|
|
selectorChoices = Cmd.USER_ENTITY_SELECTORS+Cmd.USER_CSVDATA_ENTITY_SELECTORS
|
|
if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
|
|
selectorChoices += Cmd.SERVICE_ACCOUNT_ONLY_ENTITY_SELECTORS[:]+[Cmd.ENTITY_USER, Cmd.ENTITY_USERS]
|
|
else:
|
|
selectorChoices += Cmd.BASE_ENTITY_SELECTORS[:]+Cmd.USER_ENTITIES[:]
|
|
return selectorChoices
|
|
|
|
def validateYYYYMMDD(argstr):
|
|
if argstr in TODAY_NOW or argstr[0] in PLUS_MINUS:
|
|
if argstr == 'NOW':
|
|
argstr = 'TODAY'
|
|
deltaDate = getDelta(argstr, DELTA_DATE_PATTERN)
|
|
if deltaDate is None:
|
|
Cmd.Backup()
|
|
invalidArgumentExit(DELTA_DATE_FORMAT_REQUIRED)
|
|
return deltaDate
|
|
try:
|
|
argDate = datetime.datetime.strptime(argstr, YYYYMMDD_FORMAT)
|
|
return datetime.datetime(argDate.year, argDate.month, argDate.day, tzinfo=GC.Values[GC.TIMEZONE])
|
|
except ValueError:
|
|
Cmd.Backup()
|
|
invalidArgumentExit(YYYYMMDD_FORMAT_REQUIRED)
|
|
|
|
report = getChoice(CUSTOMER_USER_CHOICES)
|
|
rep = buildGAPIObject(API.REPORTS)
|
|
titles = ['date']
|
|
if report == 'customer':
|
|
fullDataServices = CUSTOMER_REPORT_SERVICES
|
|
userReports = False
|
|
service = rep.customerUsageReports()
|
|
kwargs = [{}]
|
|
else: # 'user'
|
|
fullDataServices = USER_REPORT_SERVICES
|
|
userReports = True
|
|
service = rep.userUsageReport()
|
|
kwargs = [{'userKey': 'all'}]
|
|
titles.append('user')
|
|
csvPF = CSVPrintFile()
|
|
customerId = GC.Values[GC.CUSTOMER_ID]
|
|
if customerId == GC.MY_CUSTOMER:
|
|
customerId = None
|
|
parameters = set()
|
|
convertMbToGb = select = showOrgUnit = False
|
|
userKey = 'all'
|
|
cd = orgUnit = orgUnitId = None
|
|
userOrgUnits = {}
|
|
startEndTime = StartEndTime('startdate', 'enddate', 'date')
|
|
skipDayNumbers = []
|
|
skipDates = set()
|
|
oneDay = datetime.timedelta(days=1)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'start', 'startdate', 'end', 'enddate', 'range', 'thismonth', 'previousmonths'}:
|
|
startEndTime.Get(myarg)
|
|
elif userReports and myarg in {'ou', 'org', 'orgunit'}:
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
orgUnit, orgUnitId = getOrgUnitId(cd)
|
|
select = False
|
|
elif userReports and myarg == 'showorgunit':
|
|
showOrgUnit = True
|
|
elif myarg in {'fields', 'parameters'}:
|
|
for field in getString(Cmd.OB_STRING).replace(',', ' ').split():
|
|
if ':' in field:
|
|
repsvc, _ = field.split(':', 1)
|
|
if repsvc in fullDataServices:
|
|
parameters.add(field)
|
|
else:
|
|
invalidChoiceExit(repsvc, fullDataServices, True)
|
|
else:
|
|
Cmd.Backup()
|
|
invalidArgumentExit('service:parameter')
|
|
elif myarg == 'skipdates':
|
|
for skip in getString(Cmd.OB_STRING).upper().split(','):
|
|
if skip.find(':') == -1:
|
|
skipDates.add(validateYYYYMMDD(skip))
|
|
else:
|
|
skipStart, skipEnd = skip.split(':', 1)
|
|
skipStartDate = validateYYYYMMDD(skipStart)
|
|
skipEndDate = validateYYYYMMDD(skipEnd)
|
|
if skipEndDate < skipStartDate:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_DATE_TIME_RANGE.format(myarg, skipEnd, myarg, skipStart))
|
|
while skipStartDate <= skipEndDate:
|
|
skipDates.add(skipStartDate)
|
|
skipStartDate += oneDay
|
|
elif myarg == 'skipdaysofweek':
|
|
skipdaynames = getString(Cmd.OB_STRING).split(',')
|
|
dow = [d.lower() for d in calendarlib.day_abbr]
|
|
skipDayNumbers = [dow.index(d) for d in skipdaynames if d in dow]
|
|
elif userReports and myarg == 'user':
|
|
userKey = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
orgUnit = orgUnitId = None
|
|
select = False
|
|
elif userReports and (myarg == 'select' or myarg in usageEntitySelectors()):
|
|
if myarg != 'select':
|
|
Cmd.Backup()
|
|
_, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
orgUnit = orgUnitId = None
|
|
select = True
|
|
elif myarg == 'convertmbtogb':
|
|
convertMbToGb = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if startEndTime.endDateTime is None:
|
|
startEndTime.endDateTime = todaysDate()
|
|
if startEndTime.startDateTime is None:
|
|
startEndTime.startDateTime = startEndTime.endDateTime+datetime.timedelta(days=-30)
|
|
startDateTime = startEndTime.startDateTime
|
|
startDate = startDateTime.strftime(YYYYMMDD_FORMAT)
|
|
endDateTime = startEndTime.endDateTime
|
|
endDate = endDateTime.strftime(YYYYMMDD_FORMAT)
|
|
startUseDate = endUseDate = None
|
|
if not orgUnitId:
|
|
showOrgUnit = False
|
|
if userReports:
|
|
if select:
|
|
Ent.SetGetting(Ent.REPORT)
|
|
kwargs = [{'userKey': normalizeEmailAddressOrUID(user)} for user in users]
|
|
elif userKey == 'all':
|
|
if orgUnitId:
|
|
kwargs[0]['orgUnitID'] = orgUnitId
|
|
userOrgUnits = getUserOrgUnits(cd, orgUnit, orgUnitId)
|
|
forWhom = f'users in orgUnit {orgUnit}'
|
|
else:
|
|
forWhom = 'all users'
|
|
printGettingEntityItemForWhom(Ent.REPORT, forWhom)
|
|
else:
|
|
Ent.SetGetting(Ent.REPORT)
|
|
kwargs = [{'userKey': normalizeEmailAddressOrUID(userKey)}]
|
|
printGettingEntityItemForWhom(Ent.REPORT, kwargs[0]['userKey'])
|
|
if showOrgUnit:
|
|
titles.append('orgUnitPath')
|
|
else:
|
|
pageMessage = None
|
|
csvPF.SetTitles(titles)
|
|
csvPF.SetSortAllTitles()
|
|
parameters = ','.join(parameters) if parameters else None
|
|
while startDateTime <= endDateTime:
|
|
if startDateTime.weekday() in skipDayNumbers or startDateTime in skipDates:
|
|
startDateTime += oneDay
|
|
continue
|
|
useDate = startDateTime.strftime(YYYYMMDD_FORMAT)
|
|
startDateTime += oneDay
|
|
try:
|
|
for kwarg in kwargs:
|
|
if userReports:
|
|
if not select and userKey == 'all':
|
|
pageMessage = getPageMessageForWhom(forWhom, showDate=useDate)
|
|
else:
|
|
pageMessage = getPageMessageForWhom(kwarg['userKey'], showDate=useDate)
|
|
try:
|
|
usage = callGAPIpages(service, 'get', 'usageReports',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customerId=customerId, date=useDate,
|
|
parameters=parameters, **kwarg)
|
|
except GAPI.badRequest:
|
|
continue
|
|
for entity in usage:
|
|
row = {'date': useDate}
|
|
if userReports:
|
|
if 'userEmail' in entity['entity']:
|
|
row['user'] = entity['entity']['userEmail']
|
|
if showOrgUnit:
|
|
row['orgUnitPath'] = userOrgUnits.get(row['user'], UNKNOWN)
|
|
else:
|
|
row['user'] = UNKNOWN
|
|
for item in entity.get('parameters', []):
|
|
if 'name' not in item:
|
|
continue
|
|
name = item['name']
|
|
if name == 'cros:device_version_distribution':
|
|
versions = {}
|
|
for version in item['msgValue']:
|
|
versions[version['version_number']] = version['num_devices']
|
|
for k, v in sorted(iter(versions.items()), reverse=True):
|
|
title = f'cros:num_devices_chrome_{k}'
|
|
row[title] = v
|
|
else:
|
|
for ptype in REPORTS_PARAMETERS_SIMPLE_TYPES:
|
|
if ptype in item:
|
|
if ptype != 'datetimeValue':
|
|
if convertMbToGb and name.endswith('_in_mb'):
|
|
name = convertReportMBtoGB(name, item)
|
|
row[name] = item[ptype]
|
|
else:
|
|
row[name] = formatLocalTime(item[ptype])
|
|
break
|
|
else:
|
|
row[name] = ''
|
|
if not startUseDate:
|
|
startUseDate = useDate
|
|
endUseDate = useDate
|
|
csvPF.WriteRowTitles(row)
|
|
except GAPI.invalid as e:
|
|
stderrWarningMsg(str(e))
|
|
break
|
|
except GAPI.invalidInput as e:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
|
|
except GAPI.forbidden as e:
|
|
accessErrorExit(None, str(e))
|
|
if startUseDate:
|
|
reportName = f'{report.capitalize()} Usage Report - {startUseDate}:{endUseDate}'
|
|
else:
|
|
reportName = f'{report.capitalize()} Usage Report - {startDate}:{endDate} - No Data'
|
|
csvPF.writeCSVfile(reportName)
|
|
|
|
NL_SPACES_PATTERN = re.compile(r'\n +')
|
|
DISABLED_REASON_TIME_PATTERN = re.compile(r'.*(\d{4}/\d{2}/\d{2}-\d{2}:\d{2}:\d{2})')
|
|
|
|
REPORT_CHOICE_MAP = {
|
|
'access': 'access_transparency',
|
|
'accesstransparency': 'access_transparency',
|
|
'admin': 'admin',
|
|
'calendar': 'calendar',
|
|
'calendars': 'calendar',
|
|
'chat': 'chat',
|
|
'chrome': 'chrome',
|
|
'contextawareaccess': 'context_aware_access',
|
|
'customer': 'customer',
|
|
'customers': 'customer',
|
|
'datastudio': 'data_studio',
|
|
'devices': 'mobile',
|
|
'doc': 'drive',
|
|
'docs': 'drive',
|
|
'domain': 'customer',
|
|
'drive': 'drive',
|
|
'enterprisegroups': 'groups_enterprise',
|
|
'gcp': 'gcp',
|
|
'gplus': 'gplus',
|
|
'google+': 'gplus',
|
|
'group': 'groups',
|
|
'groups': 'groups',
|
|
'groupsenterprise': 'groups_enterprise',
|
|
'hangoutsmeet': 'meet',
|
|
'jamboard': 'jamboard',
|
|
'keep': 'keep',
|
|
'login': 'login',
|
|
'logins': 'login',
|
|
'lookerstudio': 'data_studio',
|
|
'meet': 'meet',
|
|
'mobile': 'mobile',
|
|
'oauthtoken': 'token',
|
|
'rules': 'rules',
|
|
'saml': 'saml',
|
|
'token': 'token',
|
|
'tokens': 'token',
|
|
'usage': 'usage',
|
|
'usageparameters': 'usageparameters',
|
|
'user': 'user',
|
|
'users': 'user',
|
|
'useraccounts': 'user_accounts',
|
|
'vault': 'vault',
|
|
}
|
|
|
|
REPORT_ACTIVITIES_UPPERCASE_EVENTS = {
|
|
'access_transparency',
|
|
'admin',
|
|
'chrome',
|
|
'context_aware_access',
|
|
'data_studio',
|
|
'gcp',
|
|
'jamboard',
|
|
'mobile'
|
|
}
|
|
|
|
REPORT_ACTIVITIES_TIME_OBJECTS = {'time'}
|
|
|
|
# gam report <ActivityApplictionName> [todrive <ToDriveAttribute>*]
|
|
# [(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath> [showorgunit])|(select <UserTypeEntity>)]
|
|
# [([start <Time>] [end <Time>])|(range <Time> <Time>)|
|
|
# yesterday|today|thismonth|(previousmonths <Integer>)]
|
|
# [filtertime.* <Time>] [filter|filters <String>]
|
|
# [event|events <EventNameList>] [ip <String>]
|
|
# [groupidfilter <String>]
|
|
# [maxactivities <Number>] [maxevents <Number>] [maxresults <Number>]
|
|
# [countsonly [bydate|summary] [eventrowfilter]]
|
|
# (addcsvdata <FieldName> <String>)* [shownoactivities]
|
|
# gam report users|user [todrive <ToDriveAttribute>*]
|
|
# [(user all|<UserItem>)|(orgunit|org|ou <OrgUnitPath> [showorgunit])|(select <UserTypeEntity>)]
|
|
# [allverifyuser <UserItem>]
|
|
# [(date <Date>)|(range <Date> <Date>)|
|
|
# yesterday|today|thismonth|(previousmonths <Integer>)]
|
|
# [nodatechange | (fulldatarequired all|<UserServiceNameList>)]
|
|
# [filtertime.* <Time>] [filter|filters <String>]
|
|
# [(fields|parameters <String>)|(services <UserServiceNameList>)]
|
|
# [aggregatebydate|aggregatebyuser [Boolean]]
|
|
# [maxresults <Number>]
|
|
# [convertmbtogb]
|
|
# gam report customers|customer|domain [todrive <ToDriveAttribute>*]
|
|
# [(date <Date>)|(range <Date> <Date>)|
|
|
# yesterday|today|thismonth|(previousmonths <Integer>)]
|
|
# [nodatechange | (fulldatarequired all|<CustomerServiceNameList>)]
|
|
# [(fields|parameters <String>)|(services <CustomerServiceNameList>)] [noauthorizedapps]
|
|
# [convertmbtogb]
|
|
def doReport():
|
|
def processUserUsage(usage, lastDate):
|
|
if not usage:
|
|
return (True, lastDate)
|
|
if lastDate == usage[0]['date']:
|
|
return (False, lastDate)
|
|
lastDate = usage[0]['date']
|
|
for user_report in usage:
|
|
if 'entity' not in user_report:
|
|
continue
|
|
row = {'date': user_report['date']}
|
|
if 'userEmail' in user_report['entity']:
|
|
row['email'] = user_report['entity']['userEmail']
|
|
if showOrgUnit:
|
|
row['orgUnitPath'] = userOrgUnits.get(row['email'], UNKNOWN)
|
|
else:
|
|
row['email'] = UNKNOWN
|
|
for item in user_report.get('parameters', []):
|
|
if 'name' not in item:
|
|
continue
|
|
name = item['name']
|
|
repsvc, _ = name.split(':', 1)
|
|
if repsvc not in includeServices:
|
|
continue
|
|
if name == 'accounts:disabled_reason' and 'stringValue' in item:
|
|
mg = DISABLED_REASON_TIME_PATTERN.match(item['stringValue'])
|
|
if mg:
|
|
try:
|
|
disabledTime = formatLocalTime(datetime.datetime.strptime(mg.group(1), '%Y/%m/%d-%H:%M:%S').replace(tzinfo=iso8601.UTC).strftime(YYYYMMDDTHHMMSSZ_FORMAT))
|
|
row['accounts:disabled_time'] = disabledTime
|
|
csvPF.AddTitles('accounts:disabled_time')
|
|
except ValueError:
|
|
pass
|
|
elif convertMbToGb and name.endswith('_in_mb'):
|
|
name = convertReportMBtoGB(name, item)
|
|
csvPF.AddTitles(name)
|
|
for ptype in REPORTS_PARAMETERS_SIMPLE_TYPES:
|
|
if ptype in item:
|
|
if ptype != 'datetimeValue':
|
|
row[name] = item[ptype]
|
|
else:
|
|
row[name] = formatLocalTime(item[ptype])
|
|
break
|
|
else:
|
|
row[name] = ''
|
|
csvPF.WriteRow(row)
|
|
return (True, lastDate)
|
|
|
|
def processAggregateUserUsageByUser(usage, lastDate):
|
|
if not usage:
|
|
return (True, lastDate)
|
|
if lastDate == usage[0]['date']:
|
|
return (False, lastDate)
|
|
lastDate = usage[0]['date']
|
|
for user_report in usage:
|
|
if 'entity' not in user_report:
|
|
continue
|
|
if 'userEmail' not in user_report['entity']:
|
|
continue
|
|
email = user_report['entity']['userEmail']
|
|
for item in user_report.get('parameters', []):
|
|
if 'name' not in item:
|
|
continue
|
|
name = item['name']
|
|
repsvc, _ = name.split(':', 1)
|
|
if repsvc not in includeServices:
|
|
continue
|
|
if 'intValue' in item:
|
|
if convertMbToGb and name.endswith('_in_mb'):
|
|
name = convertReportMBtoGB(name, None)
|
|
csvPF.AddTitles(name)
|
|
eventCounts.setdefault(email, {})
|
|
eventCounts[email].setdefault(name, 0)
|
|
eventCounts[email][name] += int(item['intValue'])
|
|
return (True, lastDate)
|
|
|
|
def processAggregateUserUsageByDate(usage, lastDate):
|
|
if not usage:
|
|
return (True, lastDate)
|
|
if lastDate == usage[0]['date']:
|
|
return (False, lastDate)
|
|
lastDate = usage[0]['date']
|
|
for user_report in usage:
|
|
if 'entity' not in user_report:
|
|
continue
|
|
for item in user_report.get('parameters', []):
|
|
if 'name' not in item:
|
|
continue
|
|
name = item['name']
|
|
repsvc, _ = name.split(':', 1)
|
|
if repsvc not in includeServices:
|
|
continue
|
|
if 'intValue' in item:
|
|
if convertMbToGb and name.endswith('_in_mb'):
|
|
name = convertReportMBtoGB(name, None)
|
|
csvPF.AddTitles(name)
|
|
eventCounts.setdefault(lastDate, {})
|
|
eventCounts[lastDate].setdefault(name, 0)
|
|
eventCounts[lastDate][name] += int(item['intValue'])
|
|
return (True, lastDate)
|
|
|
|
def processCustomerUsageOneRow(usage, lastDate):
|
|
if not usage:
|
|
return (True, lastDate)
|
|
if lastDate == usage[0]['date']:
|
|
return (False, lastDate)
|
|
lastDate = usage[0]['date']
|
|
row = {'date': lastDate}
|
|
for item in usage[0].get('parameters', []):
|
|
if 'name' not in item:
|
|
continue
|
|
name = item['name']
|
|
repsvc, _ = name.split(':', 1)
|
|
if repsvc not in includeServices:
|
|
continue
|
|
for ptype in REPORTS_PARAMETERS_SIMPLE_TYPES:
|
|
if ptype in item:
|
|
if convertMbToGb and name.endswith('_in_mb'):
|
|
name = convertReportMBtoGB(name, item)
|
|
csvPF.AddTitles(name)
|
|
if ptype != 'datetimeValue':
|
|
row[name] = item[ptype]
|
|
else:
|
|
row[name] = formatLocalTime(item[ptype])
|
|
break
|
|
else:
|
|
if 'msgValue' in item:
|
|
if name == 'accounts:authorized_apps':
|
|
if noAuthorizedApps:
|
|
continue
|
|
for app in item['msgValue']:
|
|
appName = f'App: {escapeCRsNLs(app["client_name"])}'
|
|
for key in ['num_users', 'client_id']:
|
|
title = f'{appName}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'
|
|
csvPF.AddTitles(title)
|
|
row[title] = app[key]
|
|
elif name == 'cros:device_version_distribution':
|
|
versions = {}
|
|
for version in item['msgValue']:
|
|
versions[version['version_number']] = version['num_devices']
|
|
for k, v in sorted(iter(versions.items()), reverse=True):
|
|
title = f'cros:device_version{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'
|
|
csvPF.AddTitles(title)
|
|
row[title] = v
|
|
else:
|
|
values = []
|
|
for subitem in item['msgValue']:
|
|
if 'count' in subitem:
|
|
mycount = myvalue = None
|
|
for key, value in iter(subitem.items()):
|
|
if key == 'count':
|
|
mycount = value
|
|
else:
|
|
myvalue = value
|
|
if mycount and myvalue:
|
|
values.append(f'{myvalue}:{mycount}')
|
|
value = ' '.join(values)
|
|
elif 'version_number' in subitem and 'num_devices' in subitem:
|
|
values.append(f'{subitem["version_number"]}:{subitem["num_devices"]}')
|
|
else:
|
|
continue
|
|
value = ' '.join(sorted(values, reverse=True))
|
|
csvPF.AddTitles(name)
|
|
row['name'] = value
|
|
csvPF.WriteRow(row)
|
|
return (True, lastDate)
|
|
|
|
def processCustomerUsage(usage, lastDate):
|
|
if not usage:
|
|
return (True, lastDate)
|
|
if lastDate == usage[0]['date']:
|
|
return (False, lastDate)
|
|
lastDate = usage[0]['date']
|
|
for item in usage[0].get('parameters', []):
|
|
if 'name' not in item:
|
|
continue
|
|
name = item['name']
|
|
repsvc, _ = name.split(':', 1)
|
|
if repsvc not in includeServices:
|
|
continue
|
|
for ptype in REPORTS_PARAMETERS_SIMPLE_TYPES:
|
|
if ptype in item:
|
|
if ptype != 'datetimeValue':
|
|
if convertMbToGb and name.endswith('_in_mb'):
|
|
name = convertReportMBtoGB(name, item)
|
|
csvPF.WriteRow({'date': lastDate, 'name': name, 'value': item[ptype]})
|
|
else:
|
|
csvPF.WriteRow({'date': lastDate, 'name': name, 'value': formatLocalTime(item[ptype])})
|
|
break
|
|
else:
|
|
if 'msgValue' in item:
|
|
if name == 'accounts:authorized_apps':
|
|
if noAuthorizedApps:
|
|
continue
|
|
for subitem in item['msgValue']:
|
|
app = {'date': lastDate}
|
|
for an_item in subitem:
|
|
if an_item == 'client_name':
|
|
app['name'] = f'App: {escapeCRsNLs(subitem[an_item])}'
|
|
elif an_item == 'num_users':
|
|
app['value'] = f'{subitem[an_item]} users'
|
|
elif an_item == 'client_id':
|
|
app['client_id'] = subitem[an_item]
|
|
authorizedApps.append(app)
|
|
elif name == 'cros:device_version_distribution':
|
|
values = []
|
|
for subitem in item['msgValue']:
|
|
values.append(f'{subitem["version_number"]}:{subitem["num_devices"]}')
|
|
csvPF.WriteRow({'date': lastDate, 'name': name, 'value': ' '.join(sorted(values, reverse=True))})
|
|
else:
|
|
values = []
|
|
for subitem in item['msgValue']:
|
|
if 'count' in subitem:
|
|
mycount = myvalue = None
|
|
for key, value in iter(subitem.items()):
|
|
if key == 'count':
|
|
mycount = value
|
|
else:
|
|
myvalue = value
|
|
if mycount and myvalue:
|
|
values.append(f'{myvalue}:{mycount}')
|
|
else:
|
|
continue
|
|
csvPF.WriteRow({'date': lastDate, 'name': name, 'value': ' '.join(sorted(values, reverse=True))})
|
|
csvPF.SortRowsTwoTitles('date', 'name', False)
|
|
if authorizedApps:
|
|
csvPF.AddTitle('client_id')
|
|
for row in sorted(authorizedApps, key=lambda k: (k['date'], k['name'].lower())):
|
|
csvPF.WriteRow(row)
|
|
return (True, lastDate)
|
|
|
|
report = getChoice(REPORT_CHOICE_MAP, mapChoice=True)
|
|
if report == 'usage':
|
|
doReportUsage()
|
|
return
|
|
if report == 'usageparameters':
|
|
doReportUsageParameters()
|
|
return
|
|
rep = buildGAPIObject(API.REPORTS)
|
|
customerId = GC.Values[GC.CUSTOMER_ID]
|
|
if customerId == GC.MY_CUSTOMER:
|
|
customerId = None
|
|
csvPF = CSVPrintFile()
|
|
filters = actorIpAddress = groupIdFilter = orgUnit = orgUnitId = None
|
|
showOrgUnit = False
|
|
parameters = set()
|
|
parameterServices = set()
|
|
eventCounts = {}
|
|
eventNames = []
|
|
startEndTime = StartEndTime('start', 'end')
|
|
oneDay = datetime.timedelta(days=1)
|
|
filterTimes = {}
|
|
maxActivities = maxEvents = 0
|
|
maxResults = 1000
|
|
aggregateByDate = aggregateByUser = convertMbToGb = countsOnly = countsByDate = countsSummary = \
|
|
eventRowFilter = exitUserLoop = noAuthorizedApps = normalizeUsers = select = userCustomerRange = False
|
|
limitDateChanges = -1
|
|
allVerifyUser = userKey = 'all'
|
|
cd = orgUnit = orgUnitId = None
|
|
userOrgUnits = {}
|
|
customerReports = userReports = False
|
|
if report == 'customer':
|
|
customerReports = True
|
|
service = rep.customerUsageReports()
|
|
fullDataServices = CUSTOMER_REPORT_SERVICES
|
|
elif report == 'user':
|
|
userReports = True
|
|
service = rep.userUsageReport()
|
|
fullDataServices = USER_REPORT_SERVICES
|
|
else:
|
|
service = rep.activities()
|
|
usageReports = customerReports or userReports
|
|
activityReports = not usageReports
|
|
dataRequiredServices = set()
|
|
addCSVData = {}
|
|
showNoActivities = False
|
|
if usageReports:
|
|
includeServices = set()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'range', 'thismonth', 'previousmonths'}:
|
|
startEndTime.Get(myarg)
|
|
userCustomerRange = True
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
orgUnit, orgUnitId = getOrgUnitId(cd)
|
|
select = False
|
|
elif myarg == 'showorgunit':
|
|
showOrgUnit = True
|
|
elif usageReports and myarg in {'date', 'yesterday', 'today'}:
|
|
startEndTime.Get('start' if myarg == 'date' else myarg)
|
|
startEndTime.endDateTime = startEndTime.startDateTime
|
|
userCustomerRange = False
|
|
elif usageReports and myarg in {'nodatechange', 'limitdatechanges'}:
|
|
if myarg == 'nodatechange':
|
|
limitDateChanges = 0
|
|
else:
|
|
limitDateChanges = getInteger(minVal=-1)
|
|
if (limitDateChanges == 0) and (startEndTime.startDateTime is not None) and (startEndTime.endDateTime == startEndTime.startDateTime):
|
|
userCustomerRange = True
|
|
elif usageReports and myarg in {'fields', 'parameters'}:
|
|
for field in getString(Cmd.OB_STRING).replace(',', ' ').split():
|
|
if ':' in field:
|
|
repsvc, _ = field.split(':', 1)
|
|
if repsvc in fullDataServices:
|
|
parameters.add(field)
|
|
parameterServices.add(repsvc)
|
|
includeServices.add(repsvc)
|
|
else:
|
|
invalidChoiceExit(repsvc, fullDataServices, True)
|
|
else:
|
|
Cmd.Backup()
|
|
invalidArgumentExit('service:parameter')
|
|
elif usageReports and myarg == 'fulldatarequired':
|
|
fdr = getString(Cmd.OB_SERVICE_NAME_LIST, minLen=0).lower()
|
|
if fdr:
|
|
if fdr != 'all':
|
|
for repsvc in fdr.replace(',', ' ').split():
|
|
if repsvc in fullDataServices:
|
|
dataRequiredServices.add(repsvc)
|
|
else:
|
|
invalidChoiceExit(repsvc, fullDataServices, True)
|
|
else:
|
|
dataRequiredServices = fullDataServices
|
|
elif usageReports and myarg in {'service', 'services'}:
|
|
for repsvc in getString(Cmd.OB_SERVICE_NAME_LIST).lower().replace(',', ' ').split():
|
|
if repsvc in fullDataServices:
|
|
parameterServices.add(repsvc)
|
|
includeServices.add(repsvc)
|
|
else:
|
|
invalidChoiceExit(repsvc, fullDataServices, True)
|
|
elif usageReports and myarg == 'convertmbtogb':
|
|
convertMbToGb = True
|
|
elif customerReports and myarg == 'noauthorizedapps':
|
|
noAuthorizedApps = True
|
|
elif activityReports and myarg == 'maxactivities':
|
|
maxActivities = getInteger(minVal=0)
|
|
elif activityReports and myarg == 'maxevents':
|
|
maxEvents = getInteger(minVal=0)
|
|
elif activityReports and myarg in {'start', 'starttime', 'end', 'endtime', 'yesterday', 'today'}:
|
|
startEndTime.Get(myarg)
|
|
elif activityReports and myarg in {'event', 'events'}:
|
|
for event in getString(Cmd.OB_EVENT_NAME_LIST).replace(',', ' ').split():
|
|
event = event.lower() if report not in REPORT_ACTIVITIES_UPPERCASE_EVENTS else event.upper()
|
|
if event not in eventNames:
|
|
eventNames.append(event)
|
|
elif activityReports and myarg == 'ip':
|
|
actorIpAddress = getString(Cmd.OB_STRING)
|
|
elif activityReports and myarg == 'countsonly':
|
|
countsOnly = True
|
|
elif activityReports and myarg == 'bydate':
|
|
countsByDate = True
|
|
elif activityReports and myarg == 'summary':
|
|
countsSummary = True
|
|
elif activityReports and myarg == 'eventrowfilter':
|
|
eventRowFilter = True
|
|
elif activityReports and myarg == 'groupidfilter':
|
|
groupIdFilter = getString(Cmd.OB_STRING)
|
|
elif activityReports and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif activityReports and myarg == 'shownoactivities':
|
|
showNoActivities = True
|
|
elif not customerReports and myarg.startswith('filtertime'):
|
|
filterTimes[myarg] = getTimeOrDeltaFromNow()
|
|
elif not customerReports and myarg in {'filter', 'filters'}:
|
|
filters = getString(Cmd.OB_STRING)
|
|
elif not customerReports and myarg == 'maxresults':
|
|
maxResults = getInteger(minVal=1, maxVal=1000)
|
|
elif not customerReports and myarg == 'user':
|
|
userKey = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
orgUnit = orgUnitId = None
|
|
select = False
|
|
elif not customerReports and myarg == 'select':
|
|
_, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
orgUnit = orgUnitId = None
|
|
select = True
|
|
elif userReports and myarg == 'aggregatebydate':
|
|
aggregateByDate = getBoolean()
|
|
elif userReports and myarg == 'aggregatebyuser':
|
|
aggregateByUser = getBoolean()
|
|
elif userReports and myarg == 'allverifyuser':
|
|
allVerifyUser = getEmailAddress()
|
|
else:
|
|
unknownArgumentExit()
|
|
if aggregateByDate and aggregateByUser:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('aggregateByDate', 'aggregateByUser'))
|
|
if countsOnly and countsByDate and countsSummary:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('bydate', 'summary'))
|
|
parameters = ','.join(parameters) if parameters else None
|
|
if usageReports and not includeServices:
|
|
includeServices = set(fullDataServices)
|
|
if filterTimes and filters is not None:
|
|
for filterTimeName, filterTimeValue in iter(filterTimes.items()):
|
|
filters = filters.replace(f'#{filterTimeName}#', filterTimeValue)
|
|
if not orgUnitId:
|
|
showOrgUnit = False
|
|
if userReports:
|
|
if startEndTime.startDateTime is None:
|
|
startEndTime.startDateTime = startEndTime.endDateTime = todaysDate()
|
|
if select:
|
|
normalizeUsers = True
|
|
orgUnitId = None
|
|
Ent.SetGetting(Ent.REPORT)
|
|
elif userKey == 'all':
|
|
if orgUnitId:
|
|
if showOrgUnit:
|
|
userOrgUnits = getUserOrgUnits(cd, orgUnit, orgUnitId)
|
|
forWhom = f'users in orgUnit {orgUnit}'
|
|
else:
|
|
forWhom = 'all users'
|
|
printGettingEntityItemForWhom(Ent.REPORT, forWhom)
|
|
users = ['all']
|
|
else:
|
|
Ent.SetGetting(Ent.REPORT)
|
|
users = [normalizeEmailAddressOrUID(userKey)]
|
|
orgUnitId = None
|
|
if aggregateByDate:
|
|
titles = ['date']
|
|
elif aggregateByUser:
|
|
titles = ['email'] if not showOrgUnit else ['email', 'orgUnitPath']
|
|
else:
|
|
titles = ['email', 'date'] if not showOrgUnit else ['email', 'orgUnitPath', 'date']
|
|
csvPF.SetTitles(titles)
|
|
csvPF.SetSortAllTitles()
|
|
i = 0
|
|
count = len(users)
|
|
for user in users:
|
|
i += 1
|
|
if normalizeUsers:
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if user != 'all':
|
|
printGettingEntityItemForWhom(Ent.REPORT, user, i, count)
|
|
verifyUser = user
|
|
else:
|
|
verifyUser = allVerifyUser
|
|
startDateTime = startEndTime.startDateTime
|
|
endDateTime = startEndTime.endDateTime
|
|
lastDate = None
|
|
numDateChanges = 0
|
|
while startDateTime <= endDateTime:
|
|
tryDate = startDateTime.strftime(YYYYMMDD_FORMAT)
|
|
try:
|
|
if not userCustomerRange:
|
|
result = callGAPI(service, 'get',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
|
|
userKey=verifyUser, date=tryDate, customerId=customerId,
|
|
orgUnitID=orgUnitId, parameters=parameters,
|
|
fields='warnings,usageReports', maxResults=1)
|
|
prevTryDate = tryDate
|
|
fullData, tryDate, usageReports = _checkDataRequiredServices(result, tryDate,
|
|
dataRequiredServices, parameterServices, True)
|
|
if fullData < 0:
|
|
printWarningMessage(DATA_NOT_AVALIABLE_RC, Msg.NO_REPORT_AVAILABLE.format(report))
|
|
break
|
|
numDateChanges += 1
|
|
if fullData == 0:
|
|
if numDateChanges > limitDateChanges >= 0:
|
|
break
|
|
startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
|
|
continue
|
|
if not select and userKey == 'all':
|
|
pageMessage = getPageMessageForWhom(forWhom, showDate=tryDate)
|
|
else:
|
|
pageMessage = getPageMessageForWhom(user, showDate=tryDate)
|
|
prevTryDate = tryDate
|
|
usage = callGAPIpages(service, 'get', 'usageReports',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userKey=user, date=tryDate, customerId=customerId,
|
|
orgUnitID=orgUnitId, filters=filters, parameters=parameters,
|
|
maxResults=maxResults)
|
|
if aggregateByDate:
|
|
status, lastDate = processAggregateUserUsageByDate(usage, lastDate)
|
|
elif aggregateByUser:
|
|
status, lastDate = processAggregateUserUsageByUser(usage, lastDate)
|
|
else:
|
|
status, lastDate = processUserUsage(usage, lastDate)
|
|
if not status:
|
|
break
|
|
except GAPI.invalid as e:
|
|
if userCustomerRange:
|
|
break
|
|
numDateChanges += 1
|
|
tryDate = _adjustTryDate(str(e), numDateChanges, limitDateChanges, tryDate)
|
|
if not tryDate:
|
|
break
|
|
startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
|
|
continue
|
|
except GAPI.invalidInput as e:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
|
|
except GAPI.badRequest:
|
|
if user != 'all':
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
else:
|
|
printErrorMessage(BAD_REQUEST_RC, Msg.BAD_REQUEST)
|
|
exitUserLoop = True
|
|
break
|
|
except GAPI.forbidden as e:
|
|
accessErrorExit(None, str(e))
|
|
startDateTime += oneDay
|
|
if exitUserLoop:
|
|
break
|
|
if user != 'all' and lastDate is None and GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'date': prevTryDate, 'email': user})
|
|
if aggregateByDate:
|
|
for usageDate, events in iter(eventCounts.items()):
|
|
row = {'date': usageDate}
|
|
for event, count in iter(events.items()):
|
|
if convertMbToGb and event.endswith('_in_gb'):
|
|
count = f'{count/1024:.2f}'
|
|
row[event] = count
|
|
csvPF.WriteRow(row)
|
|
csvPF.SortRows('date', False)
|
|
csvPF.writeCSVfile(f'User Reports Aggregate - {tryDate}')
|
|
elif aggregateByUser:
|
|
for email, events in iter(eventCounts.items()):
|
|
row = {'email': email}
|
|
if showOrgUnit:
|
|
row['orgUnitPath'] = userOrgUnits.get(email, UNKNOWN)
|
|
for event, count in iter(events.items()):
|
|
if convertMbToGb and event.endswith('_in_gb'):
|
|
count = f'{count/1024:.2f}'
|
|
row[event] = count
|
|
csvPF.WriteRow(row)
|
|
csvPF.SortRows('email', False)
|
|
csvPF.writeCSVfile('User Reports Aggregate - User')
|
|
else:
|
|
csvPF.SortRowsTwoTitles('email', 'date', False)
|
|
csvPF.writeCSVfile(f'User Reports - {tryDate}')
|
|
elif customerReports:
|
|
if startEndTime.startDateTime is None:
|
|
startEndTime.startDateTime = startEndTime.endDateTime = todaysDate()
|
|
csvPF.SetTitles('date')
|
|
if not userCustomerRange or (startEndTime.startDateTime == startEndTime.endDateTime):
|
|
csvPF.AddTitles(['name', 'value'])
|
|
authorizedApps = []
|
|
startDateTime = startEndTime.startDateTime
|
|
endDateTime = startEndTime.endDateTime
|
|
lastDate = None
|
|
numDateChanges = 0
|
|
while startDateTime <= endDateTime:
|
|
tryDate = startDateTime.strftime(YYYYMMDD_FORMAT)
|
|
try:
|
|
if not userCustomerRange:
|
|
result = callGAPI(service, 'get',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
|
|
date=tryDate, customerId=customerId, parameters=parameters, fields='warnings,usageReports')
|
|
fullData, tryDate, usageReports = _checkDataRequiredServices(result, tryDate,
|
|
dataRequiredServices, parameterServices)
|
|
if fullData < 0:
|
|
printWarningMessage(DATA_NOT_AVALIABLE_RC, Msg.NO_REPORT_AVAILABLE.format(report))
|
|
break
|
|
numDateChanges += 1
|
|
if fullData == 0:
|
|
if numDateChanges > limitDateChanges >= 0:
|
|
break
|
|
startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
|
|
continue
|
|
usage = callGAPIpages(service, 'get', 'usageReports',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.FORBIDDEN],
|
|
date=tryDate, customerId=customerId, parameters=parameters)
|
|
if not userCustomerRange or (startEndTime.startDateTime == startEndTime.endDateTime):
|
|
status, lastDate = processCustomerUsage(usage, lastDate)
|
|
else:
|
|
status, lastDate = processCustomerUsageOneRow(usage, lastDate)
|
|
if not status:
|
|
break
|
|
except GAPI.invalid as e:
|
|
if userCustomerRange:
|
|
break
|
|
numDateChanges += 1
|
|
tryDate = _adjustTryDate(str(e), numDateChanges, limitDateChanges, tryDate)
|
|
if not tryDate:
|
|
break
|
|
startDateTime = endDateTime = datetime.datetime.strptime(tryDate, YYYYMMDD_FORMAT)
|
|
continue
|
|
except GAPI.invalidInput as e:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
|
|
except GAPI.forbidden as e:
|
|
accessErrorExit(None, str(e))
|
|
startDateTime += oneDay
|
|
csvPF.writeCSVfile(f'Customer Report - {tryDate}')
|
|
else: # activityReports
|
|
csvPF.SetTitles('name')
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if select:
|
|
pageMessage = None
|
|
normalizeUsers = True
|
|
orgUnitId = None
|
|
elif userKey == 'all':
|
|
if orgUnitId:
|
|
if showOrgUnit:
|
|
userOrgUnits = getUserOrgUnits(cd, orgUnit, orgUnitId)
|
|
printGettingEntityItemForWhom(Ent.REPORT, f'users in orgUnit {orgUnit}')
|
|
else:
|
|
printGettingEntityItemForWhom(Ent.REPORT, 'all users')
|
|
pageMessage = getPageMessage()
|
|
users = ['all']
|
|
else:
|
|
Ent.SetGetting(Ent.ACTIVITY)
|
|
pageMessage = getPageMessage()
|
|
users = [normalizeEmailAddressOrUID(userKey)]
|
|
orgUnitId = None
|
|
zeroEventCounts = {}
|
|
if not eventNames:
|
|
eventNames.append(None)
|
|
else:
|
|
for eventName in eventNames:
|
|
zeroEventCounts[eventName] = 0
|
|
i = 0
|
|
count = len(users)
|
|
for user in users:
|
|
i += 1
|
|
if normalizeUsers:
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if select or user != 'all':
|
|
printGettingEntityItemForWhom(Ent.ACTIVITY, user, i, count)
|
|
for eventName in eventNames:
|
|
try:
|
|
feed = callGAPIpages(service, 'list', 'items',
|
|
pageMessage=pageMessage, maxItems=maxActivities,
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.AUTH_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
applicationName=report, userKey=user, customerId=customerId,
|
|
actorIpAddress=actorIpAddress, orgUnitID=orgUnitId,
|
|
startTime=startEndTime.startTime, endTime=startEndTime.endTime,
|
|
eventName=eventName, filters=filters, groupIdFilter=groupIdFilter,
|
|
maxResults=maxResults)
|
|
except GAPI.badRequest:
|
|
if user != 'all':
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
continue
|
|
printErrorMessage(BAD_REQUEST_RC, Msg.BAD_REQUEST)
|
|
break
|
|
except (GAPI.invalid, GAPI.invalidInput, GAPI.serviceNotAvailable) as e:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
|
|
except GAPI.authError:
|
|
accessErrorExit(None)
|
|
for activity in feed:
|
|
events = activity.pop('events')
|
|
actor = activity['actor'].get('email')
|
|
if not actor:
|
|
actor = 'id:'+activity['actor'].get('profileId', UNKNOWN)
|
|
if showOrgUnit:
|
|
activity['actor']['orgUnitPath'] = userOrgUnits.get(actor, UNKNOWN)
|
|
if countsOnly and countsByDate:
|
|
eventTime = activity.get('id', {}).get('time', UNKNOWN)
|
|
if eventTime != UNKNOWN:
|
|
try:
|
|
eventTime, _ = iso8601.parse_date(eventTime)
|
|
except (iso8601.ParseError, OverflowError):
|
|
eventTime = UNKNOWN
|
|
if eventTime != UNKNOWN:
|
|
eventDate = eventTime.strftime(YYYYMMDD_FORMAT)
|
|
else:
|
|
eventDate = UNKNOWN
|
|
if not countsOnly or eventRowFilter:
|
|
activity_row = flattenJSON(activity, timeObjects=REPORT_ACTIVITIES_TIME_OBJECTS)
|
|
purge_parameters = True
|
|
numEvents = 0
|
|
for event in events:
|
|
numEvents += 1
|
|
for item in event.get('parameters', []):
|
|
itemSet = set(item)
|
|
if not itemSet.symmetric_difference({'name'}):
|
|
event[item['name']] = ''
|
|
elif not itemSet.symmetric_difference({'value', 'name'}):
|
|
event[item['name']] = NL_SPACES_PATTERN.sub('', item['value'])
|
|
elif not itemSet.symmetric_difference({'intValue', 'name'}):
|
|
if item['name'] in {'start_time', 'end_time'}:
|
|
val = item.get('intValue')
|
|
if val is not None:
|
|
val = int(val)
|
|
if val >= 62135683200:
|
|
event[item['name']] = ISOformatTimeStamp(datetime.datetime.fromtimestamp(val-62135683200, GC.Values[GC.TIMEZONE]))
|
|
else:
|
|
event[item['name']] = val
|
|
else:
|
|
event[item['name']] = item['intValue']
|
|
elif not itemSet.symmetric_difference({'boolValue', 'name'}):
|
|
event[item['name']] = item['boolValue']
|
|
elif not itemSet.symmetric_difference({'multiValue', 'name'}):
|
|
event[item['name']] = ' '.join(item['multiValue'])
|
|
elif item['name'] == 'scope_data':
|
|
parts = {}
|
|
for message in item['multiMessageValue']:
|
|
for mess in message['parameter']:
|
|
value = mess.get('value', ' '.join(mess.get('multiValue', [])))
|
|
parts[mess['name']] = parts.get(mess['name'], [])+[value]
|
|
for part, v in iter(parts.items()):
|
|
if part == 'scope_name':
|
|
part = 'scope'
|
|
event[part] = ' '.join(v)
|
|
else:
|
|
purge_parameters = False
|
|
if purge_parameters:
|
|
event.pop('parameters', None)
|
|
row = flattenJSON(event)
|
|
row.update(activity_row)
|
|
if not countsOnly:
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRowTitles(row)
|
|
if numEvents >= maxEvents > 0:
|
|
break
|
|
elif csvPF.CheckRowTitles(row):
|
|
eventName = event['name']
|
|
if not countsSummary:
|
|
eventCounts.setdefault(actor, {})
|
|
if not countsByDate:
|
|
eventCounts[actor].setdefault(eventName, 0)
|
|
eventCounts[actor][eventName] += 1
|
|
else:
|
|
eventCounts[actor].setdefault(eventDate, {})
|
|
eventCounts[actor][eventDate].setdefault(eventName, 0)
|
|
eventCounts[actor][eventDate][eventName] += 1
|
|
else:
|
|
eventCounts.setdefault(eventName, 0)
|
|
eventCounts[eventName] += 1
|
|
elif not countsSummary:
|
|
eventCounts.setdefault(actor, {})
|
|
if not countsByDate:
|
|
for event in events:
|
|
eventName = event['name']
|
|
eventCounts[actor].setdefault(eventName, 0)
|
|
eventCounts[actor][eventName] += 1
|
|
else:
|
|
for event in events:
|
|
eventName = event['name']
|
|
eventCounts[actor].setdefault(eventDate, {})
|
|
eventCounts[actor][eventDate].setdefault(eventName, 0)
|
|
eventCounts[actor][eventDate][eventName] += 1
|
|
else:
|
|
for event in events:
|
|
eventCounts.setdefault(event['name'], 0)
|
|
eventCounts[event['name']] += 1
|
|
if not countsOnly:
|
|
if not csvPF.rows and showNoActivities:
|
|
row = {'name': 'NoActivities'}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRowTitles(row)
|
|
else:
|
|
if eventRowFilter:
|
|
csvPF.SetRowFilter([], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
|
|
if not countsSummary:
|
|
titles = ['emailAddress']
|
|
if countsOnly and countsByDate:
|
|
titles.append('date')
|
|
csvPF.SetTitles(titles)
|
|
csvPF.SetSortTitles(titles)
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if eventCounts:
|
|
if not countsByDate:
|
|
for actor, events in iter(eventCounts.items()):
|
|
row = {'emailAddress': actor}
|
|
row.update(zeroEventCounts)
|
|
for event, count in iter(events.items()):
|
|
row[event] = count
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRowTitles(row)
|
|
else:
|
|
for actor, eventDates in iter(eventCounts.items()):
|
|
for eventDate, events in iter(eventDates.items()):
|
|
row = {'emailAddress': actor, 'date': eventDate}
|
|
row.update(zeroEventCounts)
|
|
for event, count in iter(events.items()):
|
|
row[event] = count
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRowTitles(row)
|
|
elif showNoActivities:
|
|
row = {'emailAddress': 'NoActivities'}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
else:
|
|
csvPF.SetTitles(['event', 'count'])
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if eventCounts:
|
|
for event, count in sorted(iter(eventCounts.items())):
|
|
row = {'event': event, 'count': count}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
elif showNoActivities:
|
|
row = {'event': 'NoActivities', 'count': 0}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
csvPF.writeCSVfile(f'{report.capitalize()} Activity Report')
|
|
|
|
# Substitute for #user#, #email#, #usernamne#
|
|
def _substituteForUser(field, user, userName):
|
|
if field.find('#') == -1:
|
|
return field
|
|
return field.replace('#user#', user).replace('#email#', user).replace('#username#', userName)
|
|
|
|
# Tag utilities
|
|
TAG_ADDRESS_ARGUMENT_TO_FIELD_MAP = {
|
|
'country': 'country',
|
|
'countrycode': 'countryCode',
|
|
'customtype': 'customType',
|
|
'extendedaddress': 'extendedAddress',
|
|
'formatted': 'formatted',
|
|
'locality': 'locality',
|
|
'pobox': 'poBox',
|
|
'postalcode': 'postalCode',
|
|
'primary': 'primary',
|
|
'region': 'region',
|
|
'streetaddress': 'streetAddress',
|
|
'type': 'type',
|
|
}
|
|
|
|
TAG_EMAIL_ARGUMENT_TO_FIELD_MAP = {
|
|
'domain': 'domain',
|
|
'primaryemail': 'primaryEmail',
|
|
'username': 'username',
|
|
}
|
|
|
|
TAG_EXTERNALID_ARGUMENT_TO_FIELD_MAP = {
|
|
'customtype': 'customType',
|
|
'type': 'type',
|
|
'value': 'value',
|
|
}
|
|
|
|
TAG_GENDER_ARGUMENT_TO_FIELD_MAP = {
|
|
'addressmeas': 'addressMeAs',
|
|
'customgender': 'customGender',
|
|
'type': 'type',
|
|
}
|
|
|
|
TAG_IM_ARGUMENT_TO_FIELD_MAP = {
|
|
'customprotocol': 'customProtocol',
|
|
'customtype': 'customType',
|
|
'im': 'im',
|
|
'protocol': 'protocol',
|
|
'primary': 'primary',
|
|
'type': 'type',
|
|
}
|
|
|
|
TAG_KEYWORD_ARGUMENT_TO_FIELD_MAP = {
|
|
'customtype': 'customType',
|
|
'type': 'type',
|
|
'value': 'value',
|
|
}
|
|
|
|
TAG_LOCATION_ARGUMENT_TO_FIELD_MAP = {
|
|
'area': 'area',
|
|
'buildingid': 'buildingId',
|
|
'buildingname': 'buildingName',
|
|
'customtype': 'customType',
|
|
'deskcode': 'deskCode',
|
|
'floorname': 'floorName',
|
|
'floorsection': 'floorSection',
|
|
'type': 'type',
|
|
}
|
|
|
|
TAG_NAME_ARGUMENT_TO_FIELD_MAP = {
|
|
'familyname': 'familyName',
|
|
'fullname': 'fullName',
|
|
'givenname': 'givenName',
|
|
}
|
|
|
|
TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP = {
|
|
'costcenter': 'costCenter',
|
|
'costcentre': 'costCenter',
|
|
'customtype': 'customType',
|
|
'department': 'department',
|
|
'description': 'description',
|
|
'domain': 'domain',
|
|
'fulltimeequivalent': 'fullTimeEquivalent',
|
|
'location': 'location',
|
|
'name': 'name',
|
|
'primary': 'primary',
|
|
'symbol': 'symbol',
|
|
'title': 'title',
|
|
'type': 'type',
|
|
}
|
|
|
|
TAG_OTHEREMAIL_ARGUMENT_TO_FIELD_MAP = {
|
|
'address': 'address',
|
|
'customtype': 'customType',
|
|
'primary': 'primary',
|
|
'type': 'type',
|
|
}
|
|
|
|
TAG_PHONE_ARGUMENT_TO_FIELD_MAP = {
|
|
'customtype': 'customType',
|
|
'primary': 'primary',
|
|
'type': 'type',
|
|
'value': 'value',
|
|
}
|
|
|
|
TAG_POSIXACCOUNT_ARGUMENT_TO_FIELD_MAP = {
|
|
'accountid': 'accountId',
|
|
'gecos': 'gecos',
|
|
'gid': 'gid',
|
|
'homedirectory': 'homeDirectory',
|
|
'operatingsystemtype': 'operatingSystemType',
|
|
'primary': 'primary',
|
|
'shell': 'shell',
|
|
'systemid': 'systemId',
|
|
'uid': 'uid',
|
|
'username': 'username',
|
|
}
|
|
|
|
TAG_RELATION_ARGUMENT_TO_FIELD_MAP = {
|
|
'customtype': 'customType',
|
|
'type': 'type',
|
|
'value': 'value',
|
|
}
|
|
|
|
TAG_SSHPUBLICKEY_ARGUMENT_TO_FIELD_MAP = {
|
|
'expirationtimeusec': 'expirationTimeUsec',
|
|
'fingerprint': 'fingerprint',
|
|
'key': 'key',
|
|
}
|
|
|
|
TAG_WEBSITE_ARGUMENT_TO_FIELD_MAP = {
|
|
'customtype': 'customType',
|
|
'primary': 'primary',
|
|
'type': 'type',
|
|
'value': 'value',
|
|
}
|
|
|
|
TAG_FIELD_SUBFIELD_CHOICE_MAP = {
|
|
'address': ('addresses', TAG_ADDRESS_ARGUMENT_TO_FIELD_MAP),
|
|
'addresses': ('addresses', TAG_ADDRESS_ARGUMENT_TO_FIELD_MAP),
|
|
'email': ('primaryEmail', TAG_EMAIL_ARGUMENT_TO_FIELD_MAP),
|
|
'externalid': ('externalIds', TAG_EXTERNALID_ARGUMENT_TO_FIELD_MAP),
|
|
'externalids': ('externalIds', TAG_EXTERNALID_ARGUMENT_TO_FIELD_MAP),
|
|
'gender': ('gender', TAG_GENDER_ARGUMENT_TO_FIELD_MAP),
|
|
'im': ('ims', TAG_IM_ARGUMENT_TO_FIELD_MAP),
|
|
'ims': ('ims', TAG_IM_ARGUMENT_TO_FIELD_MAP),
|
|
'keyword': ('keywords', TAG_KEYWORD_ARGUMENT_TO_FIELD_MAP),
|
|
'keywords': ('keywords', TAG_KEYWORD_ARGUMENT_TO_FIELD_MAP),
|
|
'location': ('locations', TAG_LOCATION_ARGUMENT_TO_FIELD_MAP),
|
|
'locations': ('locations', TAG_LOCATION_ARGUMENT_TO_FIELD_MAP),
|
|
'name': ('name', TAG_NAME_ARGUMENT_TO_FIELD_MAP),
|
|
'organization': ('organizations', TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP),
|
|
'organizations': ('organizations', TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP),
|
|
'organisation': ('organizations', TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP),
|
|
'organisations': ('organizations', TAG_ORGANIZATION_ARGUMENT_TO_FIELD_MAP),
|
|
'otheremail': ('emails', TAG_OTHEREMAIL_ARGUMENT_TO_FIELD_MAP),
|
|
'otheremails': ('emails', TAG_OTHEREMAIL_ARGUMENT_TO_FIELD_MAP),
|
|
'phone': ('phones', TAG_PHONE_ARGUMENT_TO_FIELD_MAP),
|
|
'phones': ('phones', TAG_PHONE_ARGUMENT_TO_FIELD_MAP),
|
|
'photourl': ('thumbnailPhotoUrl', {'': 'thumbnailPhotoUrl'}),
|
|
'posix': ('posixAccounts', TAG_POSIXACCOUNT_ARGUMENT_TO_FIELD_MAP),
|
|
'posixaccounts': ('posixAccounts', TAG_POSIXACCOUNT_ARGUMENT_TO_FIELD_MAP),
|
|
'relation': ('relations', TAG_RELATION_ARGUMENT_TO_FIELD_MAP),
|
|
'relations': ('relations', TAG_RELATION_ARGUMENT_TO_FIELD_MAP),
|
|
'sshkeys': ('sshPublicKeys', TAG_SSHPUBLICKEY_ARGUMENT_TO_FIELD_MAP),
|
|
'sshpublickeys': ('sshPublicKeys', TAG_SSHPUBLICKEY_ARGUMENT_TO_FIELD_MAP),
|
|
'website': ('websites', TAG_WEBSITE_ARGUMENT_TO_FIELD_MAP),
|
|
'websites': ('websites', TAG_WEBSITE_ARGUMENT_TO_FIELD_MAP),
|
|
}
|
|
|
|
def _initTagReplacements():
|
|
return {'cd': None, 'tags': {}, 'subs': False,
|
|
'fieldsSet': set(), 'fields': '',
|
|
'schemasSet': set(), 'customFieldMask': None}
|
|
|
|
def _getTagReplacement(myarg, tagReplacements, allowSubs):
|
|
if myarg == 'replace':
|
|
trregex = None
|
|
elif myarg == 'replaceregex':
|
|
trregex = getREPatternSubstitution(re.IGNORECASE)
|
|
else:
|
|
return False
|
|
matchTag = getString(Cmd.OB_TAG)
|
|
matchReplacement = getString(Cmd.OB_STRING, minLen=0)
|
|
if matchReplacement.startswith('field:'):
|
|
if not allowSubs:
|
|
usageErrorExit(Msg.USER_SUBS_NOT_ALLOWED_TAG_REPLACEMENT)
|
|
tagReplacements['subs'] = True
|
|
field = matchReplacement[6:].strip().lower()
|
|
if field.find('.') != -1:
|
|
args = field.split('.', 3)
|
|
field = args[0]
|
|
subfield = args[1]
|
|
if len(args) == 2:
|
|
matchfield = matchvalue = ''
|
|
elif len(args) == 4:
|
|
matchfield = args[2]
|
|
matchvalue = args[3]
|
|
if matchfield == 'primary':
|
|
matchvalue = matchvalue.lower()
|
|
if matchvalue == TRUE:
|
|
matchvalue = True
|
|
elif matchvalue == FALSE:
|
|
matchvalue = ''
|
|
else:
|
|
invalidChoiceExit(matchvalue, TRUE_FALSE, True)
|
|
else:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_TAG_SPECIFICATION)
|
|
elif field == 'photourl':
|
|
subfield = matchfield = matchvalue = ''
|
|
else:
|
|
field = ''
|
|
if not field or field not in TAG_FIELD_SUBFIELD_CHOICE_MAP:
|
|
invalidChoiceExit(field, TAG_FIELD_SUBFIELD_CHOICE_MAP, True)
|
|
field, subfieldsChoiceMap = TAG_FIELD_SUBFIELD_CHOICE_MAP[field]
|
|
if subfield not in subfieldsChoiceMap:
|
|
invalidChoiceExit(subfield, subfieldsChoiceMap, True)
|
|
subfield = subfieldsChoiceMap[subfield]
|
|
if matchfield:
|
|
if matchfield not in subfieldsChoiceMap:
|
|
invalidChoiceExit(matchfield, subfieldsChoiceMap, True)
|
|
matchfield = subfieldsChoiceMap[matchfield]
|
|
tagReplacements['fieldsSet'].add(field)
|
|
tagReplacements['fields'] = ','.join(tagReplacements['fieldsSet'])
|
|
tagReplacements['tags'][matchTag] = {'field': field, 'subfield': subfield,
|
|
'matchfield': matchfield, 'matchvalue': matchvalue, 'value': '',
|
|
'trregex': trregex}
|
|
if field == 'locations' and subfield == 'buildingName':
|
|
_makeBuildingIdNameMap()
|
|
elif matchReplacement.startswith('schema:'):
|
|
if not allowSubs:
|
|
usageErrorExit(Msg.USER_SUBS_NOT_ALLOWED_TAG_REPLACEMENT)
|
|
tagReplacements['subs'] = True
|
|
matchReplacement = matchReplacement[7:].strip()
|
|
if matchReplacement.find('.') != -1:
|
|
schemaName, schemaField = matchReplacement.split('.', 1)
|
|
else:
|
|
schemaName = ''
|
|
if not schemaName or not schemaField:
|
|
invalidArgumentExit(Cmd.OB_SCHEMA_NAME_FIELD_NAME)
|
|
tagReplacements['fieldsSet'].add('customSchemas')
|
|
tagReplacements['fields'] = ','.join(tagReplacements['fieldsSet'])
|
|
tagReplacements['schemasSet'].add(schemaName)
|
|
tagReplacements['customFieldMask'] = ','.join(tagReplacements['schemasSet'])
|
|
tagReplacements['tags'][matchTag] = {'schema': schemaName, 'schemafield': schemaField, 'value': '',
|
|
'trregex': trregex}
|
|
elif ((matchReplacement.find('#') >= 0) and
|
|
(matchReplacement.find('#user#') >= 0) or (matchReplacement.find('#email#') >= 0) or (matchReplacement.find('#username#') >= 0)):
|
|
if not allowSubs:
|
|
usageErrorExit(Msg.USER_SUBS_NOT_ALLOWED_TAG_REPLACEMENT)
|
|
tagReplacements['subs'] = True
|
|
tagReplacements['tags'][matchTag] = {'template': matchReplacement, 'value': '',
|
|
'trregex': trregex}
|
|
else:
|
|
if trregex is None:
|
|
tagReplacements['tags'][matchTag] = {'value': matchReplacement}
|
|
else:
|
|
tagReplacements['tags'][matchTag] = {'value': re.sub(trregex[0], trregex[1], matchReplacement)}
|
|
return True
|
|
|
|
def _getTagReplacementFieldValues(user, i, count, tagReplacements, results=None):
|
|
if results is None:
|
|
if tagReplacements['fields'] and tagReplacements['fields'] != 'primaryEmail':
|
|
if not tagReplacements['cd']:
|
|
tagReplacements['cd'] = buildGAPIObject(API.DIRECTORY)
|
|
try:
|
|
results = callGAPI(tagReplacements['cd'].users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=user, projection='custom', customFieldMask=tagReplacements['customFieldMask'], fields=tagReplacements['fields'])
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
return
|
|
else:
|
|
results = {'primaryEmail': user}
|
|
userName, domain = splitEmailAddress(user)
|
|
for _, tag in iter(tagReplacements['tags'].items()):
|
|
if tag.get('field'):
|
|
field = tag['field']
|
|
if field == 'primaryEmail':
|
|
subfield = tag['subfield']
|
|
if subfield == 'username':
|
|
tag['value'] = userName
|
|
elif subfield == 'domain':
|
|
tag['value'] = domain
|
|
else:
|
|
tag['value'] = user
|
|
else:
|
|
if field in ['addresses', 'emails', 'ims', 'organizations', 'phones', 'posixAccounts', 'websites']:
|
|
items = results.get(field, [])
|
|
if not tag['matchfield']:
|
|
for data in items:
|
|
if data.get('primary'):
|
|
break
|
|
else:
|
|
if items:
|
|
data = items[0]
|
|
else:
|
|
data = {}
|
|
else:
|
|
for data in items:
|
|
if data.get(tag['matchfield'], '') == tag['matchvalue']:
|
|
break
|
|
else:
|
|
data = {}
|
|
elif field in ['externalIds', 'relations', 'sshPublicKeys']:
|
|
items = results.get(field, [])
|
|
if not tag['matchfield']:
|
|
if items:
|
|
data = items[0]
|
|
else:
|
|
data = {}
|
|
else:
|
|
for data in items:
|
|
if data.get(tag['matchfield'], '') == tag['matchvalue']:
|
|
break
|
|
else:
|
|
data = {}
|
|
elif field in ['keywords', 'locations']:
|
|
items = results.get(field, [])
|
|
if not tag['matchfield']:
|
|
if items:
|
|
data = items[0]
|
|
data['buildingName'] = GM.Globals[GM.MAP_BUILDING_ID_TO_NAME].get(data.get('buildingId', ''), '')
|
|
else:
|
|
data = {}
|
|
else:
|
|
for data in items:
|
|
if data.get(tag['matchfield'], '') == tag['matchvalue']:
|
|
break
|
|
else:
|
|
data = {}
|
|
elif field == 'thumbnailPhotoUrl':
|
|
data = results
|
|
else:
|
|
data = results.get(field, {})
|
|
tag['value'] = str(data.get(tag['subfield'], ''))
|
|
elif tag.get('schema'):
|
|
tag['value'] = str(results.get('customSchemas', {}).get(tag['schema'], {}).get(tag['schemafield'], ''))
|
|
elif tag.get('template'):
|
|
tag['value'] = _substituteForUser(tag['template'], user, userName)
|
|
trregex = tag.get('trregex', None)
|
|
if trregex is not None:
|
|
tag['value'] = re.sub(trregex[0], trregex[1], tag['value'])
|
|
|
|
RTL_PATTERN = re.compile(r'(?s){RTL}.*?{/RTL}')
|
|
RT_PATTERN = re.compile(r'(?s){RT}.*?{/RT}')
|
|
TAG_REPLACE_PATTERN = re.compile(r'{(.+?)}')
|
|
RT_MARKERS = {'RT', '/RT', 'RTL', '/RTL'}
|
|
PC_PATTERN = re.compile(r'(?s){PC}.*?{/PC}')
|
|
UC_PATTERN = re.compile(r'(?s){UC}.*?{/UC}')
|
|
LC_PATTERN = re.compile(r'(?s){LC}.*?{/LC}')
|
|
CASE_MARKERS = {'PC', '/PC', 'UC', '/UC', 'LC', '/LC'}
|
|
SKIP_PATTERNS = [re.compile(r'<head>.*?</head>', flags=re.IGNORECASE),
|
|
re.compile(r'<script>.*?</script>', flags=re.IGNORECASE)]
|
|
|
|
def _processTagReplacements(tagReplacements, message):
|
|
def pcase(trstring):
|
|
new = ''
|
|
# state = True: Upshift 1st letter found
|
|
# state = False: Downshift subsequent letters
|
|
state = True
|
|
for c in trstring:
|
|
if state:
|
|
if c.isalpha():
|
|
new += c.upper()
|
|
state = False
|
|
else:
|
|
new += c
|
|
else:
|
|
if c.isalpha():
|
|
new += c.lower()
|
|
else:
|
|
state = True
|
|
new += c
|
|
return new
|
|
|
|
def ucase(trstring):
|
|
return trstring.upper()
|
|
|
|
def lcase(trstring):
|
|
return trstring.lower()
|
|
|
|
def _processCase(message, casePattern, caseFunc):
|
|
# Find all {xC}.*?{/xC} sequences
|
|
pos = 0
|
|
while True:
|
|
match = casePattern.search(message, pos)
|
|
if not match:
|
|
return message
|
|
start, end = match.span()
|
|
for skipArea in skipAreas:
|
|
if start >= skipArea[0] and end <= skipArea[1]:
|
|
break
|
|
else:
|
|
message = message[:start]+caseFunc(message[start+4:end-5])+message[end:]
|
|
pos = end
|
|
|
|
# Identify areas of message to avoid replacements
|
|
skipAreas = []
|
|
for pattern in SKIP_PATTERNS:
|
|
pos = 0
|
|
while True:
|
|
match = pattern.search(message, pos)
|
|
if not match:
|
|
break
|
|
skipAreas.append(match.span())
|
|
pos = match.end()
|
|
skipTags = set()
|
|
# Find all {tag}, note replacement value and starting location; note tags in skipAreas
|
|
tagFields = []
|
|
tagSubs = {}
|
|
pos = 0
|
|
while True:
|
|
match = TAG_REPLACE_PATTERN.search(message, pos)
|
|
if not match:
|
|
break
|
|
start, end = match.span()
|
|
tag = match.group(1)
|
|
if tag in CASE_MARKERS:
|
|
pass
|
|
elif tag not in RT_MARKERS:
|
|
for skipArea in skipAreas:
|
|
if start >= skipArea[0] and end <= skipArea[1]:
|
|
skipTags.add(tag)
|
|
break
|
|
else:
|
|
tagSubs.setdefault(tag, tagReplacements['tags'].get(tag, {'value': ''})['value'])
|
|
tagFields.append((tagSubs[tag], start))
|
|
pos = end
|
|
# Find all {RT}.*?{/RT} sequences
|
|
# If any non-empty {tag} replacement value falls between them, then mark {RT} and {/RT} to be stripped
|
|
# Otherwise, mark the entire {RT}.*?{/RT} sequence to be stripped
|
|
rtStrips = []
|
|
pos = 0
|
|
while True:
|
|
match = RT_PATTERN.search(message, pos)
|
|
if not match:
|
|
break
|
|
start, end = match.span()
|
|
stripEntireRT = True
|
|
hasTags = False
|
|
for tagField in tagFields:
|
|
if tagField[1] >= end:
|
|
break
|
|
if tagField[1] >= start:
|
|
hasTags = True
|
|
if tagField[0]:
|
|
rtStrips.append((False, start, start+4))
|
|
rtStrips.append((False, end-5, end))
|
|
stripEntireRT = False
|
|
break
|
|
if stripEntireRT:
|
|
if hasTags or start+4 == end-5:
|
|
rtStrips.append((True, start, end))
|
|
else:
|
|
rtStrips.append((False, start, start+4))
|
|
rtStrips.append((False, end-5, end))
|
|
pos = end
|
|
# Find all {RTL}.*?{/RTL} sequences
|
|
# If any non-empty {RT}...{tag}... {/RT} falls between them, then mark {RTL} and {/RTL} to be stripped
|
|
# Otherwise, mark the entire {RTL}.*{/RTL} sequence to be stripped
|
|
rtlStrips = []
|
|
pos = 0
|
|
while True:
|
|
match = RTL_PATTERN.search(message, pos)
|
|
if not match:
|
|
break
|
|
start, end = match.span()
|
|
stripEntireRTL = True
|
|
hasTags = False
|
|
for tagField in tagFields:
|
|
if tagField[1] >= end:
|
|
break
|
|
if tagField[1] >= start:
|
|
hasTags = True
|
|
if tagField[0]:
|
|
rtlStrips.append((False, start, start+5, end-6, end))
|
|
stripEntireRTL = False
|
|
break
|
|
if stripEntireRTL:
|
|
for rtStrip in rtStrips:
|
|
if rtStrip[1] >= end:
|
|
break
|
|
if rtStrip[1] >= start:
|
|
hasTags = True
|
|
if not rtStrip[0]:
|
|
rtlStrips.append((False, start, start+5, end-6, end))
|
|
stripEntireRTL = False
|
|
break
|
|
if stripEntireRTL:
|
|
if hasTags or start+5 == end-6:
|
|
rtlStrips.append((True, start, end))
|
|
else:
|
|
rtlStrips.append((False, start, start+5, end-6, end))
|
|
pos = end
|
|
if rtlStrips:
|
|
allStrips = []
|
|
i = 0
|
|
l = len(rtStrips)
|
|
for rtlStrip in rtlStrips:
|
|
while i < l and rtStrips[i][1] < rtlStrip[1]:
|
|
allStrips.append(rtStrips[i])
|
|
i += 1
|
|
allStrips.append((False, rtlStrip[1], rtlStrip[2]))
|
|
if not rtlStrip[0]:
|
|
while i < l and rtStrips[i][1] < rtlStrip[3]:
|
|
allStrips.append(rtStrips[i])
|
|
i += 1
|
|
allStrips.append((False, rtlStrip[3], rtlStrip[4]))
|
|
else:
|
|
while i < l and rtStrips[i][1] < rtlStrip[2]:
|
|
i += 1
|
|
while i < l:
|
|
allStrips.append(rtStrips[i])
|
|
i += 1
|
|
else:
|
|
allStrips = rtStrips
|
|
# Strip {RTL} {/RTL}, {RT} {/RT}, {RTL}.*?{/RTL}, {RT}.*?{/RT} sequences
|
|
for rtStrip in allStrips[::-1]:
|
|
message = message[:rtStrip[1]]+message[rtStrip[2]:]
|
|
# Strip {RTL} {/RTL}, {RT} {/RT}, {RTL}.*?{/RTL}, {RT}.*?{/RT} sequences
|
|
# Make {tag} replacements; ignore tags in skipAreas
|
|
pos = 0
|
|
while True:
|
|
match = TAG_REPLACE_PATTERN.search(message, pos)
|
|
if not match:
|
|
break
|
|
start, end = match.span()
|
|
tag = match.group(1)
|
|
if tag in CASE_MARKERS:
|
|
pos = end
|
|
elif tag not in RT_MARKERS:
|
|
if tag not in skipTags:
|
|
message = re.sub(match.group(0), tagSubs[tag], message)
|
|
pos = start+1
|
|
else:
|
|
pos = end
|
|
else:
|
|
# Replace invalid RT tags with ERROR(RT)
|
|
message = re.sub(match.group(0), f'ERROR({tag})', message)
|
|
pos = start+1
|
|
# Process case changes
|
|
message = _processCase(message, PC_PATTERN, pcase)
|
|
message = _processCase(message, UC_PATTERN, ucase)
|
|
message = _processCase(message, LC_PATTERN, lcase)
|
|
return message
|
|
|
|
def sendCreateUpdateUserNotification(body, basenotify, tagReplacements, i=0, count=0, msgFrom=None, createMessage=True):
|
|
def _makeSubstitutions(field):
|
|
notify[field] = _substituteForUser(notify[field], body['primaryEmail'], userName)
|
|
notify[field] = notify[field].replace('#domain#', domain)
|
|
notify[field] = notify[field].replace('#givenname#', body['name'].get('givenName', ''))
|
|
notify[field] = notify[field].replace('#familyname#', body['name'].get('familyName', ''))
|
|
|
|
def _makePasswordSubstitutions(field, html):
|
|
if not html:
|
|
notify[field] = notify[field].replace('#password#', notify['password'])
|
|
else:
|
|
notify[field] = notify[field].replace('#password#', notify['password'].replace('&', '&').replace('<', '<').replace('>', '>'))
|
|
|
|
userName, domain = splitEmailAddress(body['primaryEmail'])
|
|
notify = basenotify.copy()
|
|
if not notify['subject']:
|
|
notify['subject'] = Msg.CREATE_USER_NOTIFY_SUBJECT if createMessage else Msg.UPDATE_USER_PASSWORD_CHANGE_NOTIFY_SUBJECT
|
|
_makeSubstitutions('subject')
|
|
if not notify['message']:
|
|
notify['message'] = Msg.CREATE_USER_NOTIFY_MESSAGE if createMessage else Msg.UPDATE_USER_PASSWORD_CHANGE_NOTIFY_MESSAGE
|
|
elif notify['html']:
|
|
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '<br/>')
|
|
else:
|
|
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '\n')
|
|
_makeSubstitutions('message')
|
|
if tagReplacements['subs']:
|
|
_getTagReplacementFieldValues(body['primaryEmail'], i, count, tagReplacements, body if createMessage else None)
|
|
notify['subject'] = _processTagReplacements(tagReplacements, notify['subject'])
|
|
notify['message'] = _processTagReplacements(tagReplacements, notify['message'])
|
|
_makePasswordSubstitutions('subject', False)
|
|
_makePasswordSubstitutions('message', notify['html'])
|
|
if 'from' in notify:
|
|
msgFrom = notify['from']
|
|
msgReplyTo = notify.get('replyto', None)
|
|
mailBox = notify.get('mailbox', None)
|
|
for recipient in notify['recipients']:
|
|
send_email(notify['subject'], notify['message'], recipient, i, count,
|
|
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'], mailBox=mailBox)
|
|
|
|
def getRecipients():
|
|
if checkArgumentPresent('select'):
|
|
_, recipients = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
return [normalizeEmailAddressOrUID(emailAddress, noUid=True, noLower=True) for emailAddress in recipients]
|
|
return getNormalizedEmailAddressEntity(shlexSplit=True, noLower=True)
|
|
|
|
# gam sendemail [recipient|to] <RecipientEntity> [from <EmailAddress>] [mailbox <EmailAddress>] [replyto <EmailAddress>]
|
|
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
|
# [subject <String>]
|
|
# [<MessageContent>]
|
|
# (replace <Tag> <String>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
|
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
|
|
# (embedimage <FileName> <String>)*
|
|
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
|
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
|
# gam <UserTypeEntity> sendemail recipient <RecipientEntity> [replyto <EmailAddress>]
|
|
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
|
# [subject <String>]
|
|
# [<MessageContent>]
|
|
# (replace <Tag> <String>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
|
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
|
|
# (embedimage <FileName> <String>)*
|
|
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
|
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
|
# gam <UserTypeEntity> sendemail from <EmailAddress> [replyto <EmailAddress>]
|
|
# [cc <RecipientEntity>] [bcc <RecipientEntity>] [singlemessage]
|
|
# [subject <String>]
|
|
# [<MessageContent>]
|
|
# (replace <Tag> <String>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
|
# [html [<Boolean>]] (attach <FileName> [charset <CharSet>])*
|
|
# (embedimage <FileName> <String>)*
|
|
# [newuser <EmailAddress> firstname|givenname <String> lastname|familyname <string> password <Password>]
|
|
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
|
def doSendEmail(users=None):
|
|
body = {}
|
|
notify = {'subject': '', 'message': '', 'html': False, 'charset': UTF8, 'password': ''}
|
|
msgFroms = [_getAdminEmail()]
|
|
count = 1
|
|
if users is None:
|
|
checkArgumentPresent({'recipient', 'recipients', 'to'})
|
|
recipients = getRecipients()
|
|
else:
|
|
_, count, entityList = getEntityArgument(users)
|
|
if checkArgumentPresent({'recipient', 'recipients', 'to'}):
|
|
msgFroms = [normalizeEmailAddressOrUID(entity) for entity in entityList]
|
|
recipients = getRecipients()
|
|
else:
|
|
if checkArgumentPresent({'from'}):
|
|
msgFroms = [getString(Cmd.OB_EMAIL_ADDRESS)]
|
|
count = 1
|
|
recipients = [normalizeEmailAddressOrUID(entity) for entity in entityList]
|
|
msgHeaders = {}
|
|
ccRecipients = []
|
|
bccRecipients = []
|
|
mailBox = None
|
|
msgReplyTo = None
|
|
singleMessage = False
|
|
tagReplacements = _initTagReplacements()
|
|
attachments = []
|
|
embeddedImages = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if users is None and myarg == 'from':
|
|
msgFroms = [getString(Cmd.OB_EMAIL_ADDRESS)]
|
|
count = 1
|
|
elif myarg == 'replyto':
|
|
msgReplyTo = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
elif myarg == 'subject':
|
|
notify['subject'] = getString(Cmd.OB_STRING)
|
|
elif myarg in SORF_MSG_FILE_ARGUMENTS:
|
|
notify['message'], notify['charset'], notify['html'] = getStringOrFile(myarg)
|
|
elif myarg == 'cc':
|
|
ccRecipients = getRecipients()
|
|
elif myarg == 'bcc':
|
|
bccRecipients = getRecipients()
|
|
elif myarg == 'mailbox':
|
|
mailBox = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
elif myarg == 'singlemessage':
|
|
singleMessage = True
|
|
elif myarg == 'html':
|
|
notify['html'] = getBoolean()
|
|
elif myarg == 'newuser':
|
|
body['primaryEmail'] = getEmailAddress()
|
|
elif myarg in {'firstname', 'givenname'}:
|
|
body.setdefault('name', {})
|
|
body['name']['givenName'] = getString(Cmd.OB_STRING, minLen=0, maxLen=60)
|
|
elif myarg in {'lastname', 'familyname'}:
|
|
body.setdefault('name', {})
|
|
body['name']['familyName'] = getString(Cmd.OB_STRING, minLen=0, maxLen=60)
|
|
elif myarg in {'password', 'notifypassword'}:
|
|
body['password'] = notify['password'] = getString(Cmd.OB_PASSWORD, maxLen=100)
|
|
elif _getTagReplacement(myarg, tagReplacements, False):
|
|
pass
|
|
elif myarg == 'attach':
|
|
attachments.append((getFilename(), getCharSet()))
|
|
elif myarg == 'embedimage':
|
|
embeddedImages.append((getFilename(), getString(Cmd.OB_STRING)))
|
|
elif myarg in SMTP_HEADERS_MAP:
|
|
if myarg in SMTP_DATE_HEADERS:
|
|
msgDate, _, _ = getTimeOrDeltaFromNow(True)
|
|
msgHeaders[SMTP_HEADERS_MAP[myarg]] = formatdate(time.mktime(msgDate.timetuple()) + msgDate.microsecond/1E6, True)
|
|
else:
|
|
msgHeaders[SMTP_HEADERS_MAP[myarg]] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'header':
|
|
header = getString(Cmd.OB_STRING, minLen=1)
|
|
msgHeaders[SMTP_HEADERS_MAP.get(header.lower(), header)] = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
notify['message'] = notify['message'].replace('\r', '').replace('\\n', '\n')
|
|
if tagReplacements['tags']:
|
|
notify['message'] = _processTagReplacements(tagReplacements, notify['message'])
|
|
if tagReplacements['tags']:
|
|
notify['subject'] = _processTagReplacements(tagReplacements, notify['subject'])
|
|
jcount = len(recipients)
|
|
if body.get('primaryEmail'):
|
|
if (recipients and ('password' in body) and ('name' in body) and ('givenName' in body['name']) and ('familyName' in body['name'])):
|
|
notify['recipients'] = recipients
|
|
sendCreateUpdateUserNotification(body, notify, tagReplacements, msgFrom=msgFroms[0])
|
|
else:
|
|
usageErrorExit(Msg.NEWUSER_REQUIREMENTS, True)
|
|
return
|
|
if ccRecipients or bccRecipients:
|
|
singleMessage = True
|
|
i = 0
|
|
for msgFrom in msgFroms:
|
|
i += 1
|
|
if singleMessage:
|
|
entityPerformActionModifierNumItems([Ent.USER, msgFrom],
|
|
Act.MODIFIER_TO, jcount+len(ccRecipients)+len(bccRecipients), Ent.RECIPIENT, i, count)
|
|
send_email(notify['subject'], notify['message'], ','.join(recipients), i, count,
|
|
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
|
|
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders,
|
|
ccRecipients=','.join(ccRecipients), bccRecipients=','.join(bccRecipients),
|
|
mailBox=mailBox)
|
|
else:
|
|
entityPerformActionModifierNumItems([Ent.USER, msgFrom], Act.MODIFIER_TO, jcount, Ent.RECIPIENT, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for recipient in recipients:
|
|
j += 1
|
|
send_email(notify['subject'], notify['message'], recipient, j, jcount,
|
|
msgFrom=msgFrom, msgReplyTo=msgReplyTo, html=notify['html'], charset=notify['charset'],
|
|
attachments=attachments, embeddedImages=embeddedImages, msgHeaders=msgHeaders, mailBox=mailBox)
|
|
Ind.Decrement()
|
|
|
|
ADDRESS_FIELDS_PRINT_ORDER = ['contactName', 'organizationName', 'addressLine1', 'addressLine2', 'addressLine3', 'locality', 'region', 'postalCode', 'countryCode']
|
|
|
|
def _showCustomerAddressPhoneNumber(customerInfo):
|
|
if 'postalAddress' in customerInfo:
|
|
printKeyValueList(['Address', None])
|
|
Ind.Increment()
|
|
for field in ADDRESS_FIELDS_PRINT_ORDER:
|
|
if field in customerInfo['postalAddress']:
|
|
printKeyValueList([field, customerInfo['postalAddress'][field]])
|
|
Ind.Decrement()
|
|
if 'phoneNumber' in customerInfo:
|
|
printKeyValueList(['Phone', customerInfo['phoneNumber']])
|
|
|
|
ADDRESS_FIELDS_ARGUMENT_MAP = {
|
|
'contact': 'contactName', 'contactname': 'contactName',
|
|
'name': 'organizationName', 'organizationname': 'organizationName', 'organisationname': 'organizationName',
|
|
'address': 'addressLine1', 'address1': 'addressLine1', 'addressline1': 'addressLine1',
|
|
'address2': 'addressLine2', 'addressline2': 'addressLine2',
|
|
'address3': 'addressLine3', 'addressline3': 'addressLine3',
|
|
'city': 'locality', 'locality': 'locality',
|
|
'state': 'region', 'region': 'region',
|
|
'zipcode': 'postalCode', 'postal': 'postalCode', 'postalcode': 'postalCode',
|
|
'country': 'countryCode', 'countrycode': 'countryCode',
|
|
}
|
|
|
|
def _getResoldCustomerAttr():
|
|
body = {}
|
|
customerAuthToken = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in ADDRESS_FIELDS_ARGUMENT_MAP:
|
|
body.setdefault('postalAddress', {})
|
|
body['postalAddress'][ADDRESS_FIELDS_ARGUMENT_MAP[myarg]] = getString(Cmd.OB_STRING, minLen=0, maxLen=255)
|
|
elif myarg in {'email', 'alternateemail'}:
|
|
body['alternateEmail'] = getEmailAddress(noUid=True)
|
|
elif myarg in {'phone', 'phonenumber'}:
|
|
body['phoneNumber'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'customerauthtoken', 'transfertoken'}:
|
|
customerAuthToken = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
return customerAuthToken, body
|
|
|
|
# gam create resoldcustomer <CustomerDomain> (customer_auth_token <String>) <ResoldCustomerAttribute>+
|
|
def doCreateResoldCustomer():
|
|
res = buildGAPIObject(API.RESELLER)
|
|
customerDomain = getString('customerDomain')
|
|
customerAuthToken, body = _getResoldCustomerAttr()
|
|
body['customerDomain'] = customerDomain
|
|
try:
|
|
result = callGAPI(res.customers(), 'insert',
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
body=body, customerAuthToken=customerAuthToken, fields='customerId')
|
|
entityActionPerformed([Ent.CUSTOMER_DOMAIN, body['customerDomain'], Ent.CUSTOMER_ID, result['customerId']])
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CUSTOMER_DOMAIN, body['customerDomain']], str(e))
|
|
|
|
# gam update resoldcustomer <CustomerID> <ResoldCustomerAttribute>+
|
|
def doUpdateResoldCustomer():
|
|
res = buildGAPIObject(API.RESELLER)
|
|
customerId = getString(Cmd.OB_CUSTOMER_ID)
|
|
_, body = _getResoldCustomerAttr()
|
|
try:
|
|
callGAPI(res.customers(), 'patch',
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
customerId=customerId, body=body, fields='')
|
|
entityActionPerformed([Ent.CUSTOMER_ID, customerId])
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId], str(e))
|
|
|
|
# gam info resoldcustomer <CustomerID> [formatjson]
|
|
def doInfoResoldCustomer():
|
|
res = buildGAPIObject(API.RESELLER)
|
|
customerId = getString(Cmd.OB_CUSTOMER_ID)
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
FJQC.GetFormatJSON(myarg)
|
|
try:
|
|
customerInfo = callGAPI(res.customers(), 'get',
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
customerId=customerId)
|
|
if not FJQC.formatJSON:
|
|
printKeyValueList(['Customer ID', customerInfo['customerId']])
|
|
printKeyValueList(['Customer Type', customerInfo['customerType']])
|
|
printKeyValueList(['Customer Domain', customerInfo['customerDomain']])
|
|
if 'customerDomainVerified' in customerInfo:
|
|
printKeyValueList(['Customer Domain Verified', customerInfo['customerDomainVerified']])
|
|
_showCustomerAddressPhoneNumber(customerInfo)
|
|
primaryEmail = customerInfo.get('primaryAdmin', {}).get('primaryEmail')
|
|
if primaryEmail:
|
|
printKeyValueList(['Customer Primary Email', primaryEmail])
|
|
if 'alternateEmail' in customerInfo:
|
|
printKeyValueList(['Customer Alternate Email', customerInfo['alternateEmail']])
|
|
printKeyValueList(['Customer Admin Console URL', customerInfo['resourceUiUrl']])
|
|
else:
|
|
printLine(json.dumps(cleanJSON(customerInfo), ensure_ascii=False, sort_keys=False))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId], str(e))
|
|
|
|
def getCustomerSubscription(res):
|
|
customerId = getString(Cmd.OB_CUSTOMER_ID)
|
|
productId, skuId = SKU.getProductAndSKU(getString(Cmd.OB_SKU_ID))
|
|
if not productId:
|
|
invalidChoiceExit(skuId, SKU.getSortedSKUList(), True)
|
|
try:
|
|
subscriptions = callGAPIpages(res.subscriptions(), 'list', 'subscriptions',
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
customerId=customerId, fields='nextPageToken,subscriptions(skuId,subscriptionId,plan(planName))')
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.SUBSCRIPTION, None], str(e))
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
for subscription in subscriptions:
|
|
if skuId == subscription['skuId']:
|
|
return (customerId, skuId, subscription['subscriptionId'], subscription['plan']['planName'])
|
|
Cmd.Backup()
|
|
usageErrorExit(f'{Ent.FormatEntityValueList([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId])}, {Msg.SUBSCRIPTION_NOT_FOUND}')
|
|
|
|
PLAN_NAME_MAP = {
|
|
'annualmonthlypay': 'ANNUAL_MONTHLY_PAY',
|
|
'annualyearlypay': 'ANNUAL_YEARLY_PAY',
|
|
'flexible': 'FLEXIBLE',
|
|
'trial': 'TRIAL',
|
|
}
|
|
|
|
def _getResoldSubscriptionAttr(customerId):
|
|
body = {'customerId': customerId,
|
|
'plan': {},
|
|
'seats': {},
|
|
'skuId': None,
|
|
}
|
|
customerAuthToken = None
|
|
seats1 = seats2 = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'deal', 'dealcode'}:
|
|
body['dealCode'] = getString('dealCode')
|
|
elif myarg in {'plan', 'planname'}:
|
|
body['plan']['planName'] = getChoice(PLAN_NAME_MAP, mapChoice=True)
|
|
elif myarg in {'purchaseorderid', 'po'}:
|
|
body['purchaseOrderId'] = getString('purchaseOrderId')
|
|
elif myarg == 'seats':
|
|
seats1 = getInteger(minVal=0)
|
|
if Cmd.ArgumentsRemaining() and Cmd.Current().isdigit():
|
|
seats2 = getInteger(minVal=0)
|
|
elif myarg in {'sku', 'skuid'}:
|
|
productId, body['skuId'] = SKU.getProductAndSKU(getString(Cmd.OB_SKU_ID))
|
|
if not productId:
|
|
invalidChoiceExit(body['skuId'], SKU.getSortedSKUList(), True)
|
|
elif myarg in {'customerauthtoken', 'transfertoken'}:
|
|
customerAuthToken = getString('customer_auth_token')
|
|
else:
|
|
unknownArgumentExit()
|
|
for field in ['plan', 'skuId']:
|
|
if not body[field]:
|
|
missingArgumentExit(field.lower())
|
|
if seats1 is None:
|
|
missingArgumentExit('seats')
|
|
if body['plan']['planName'].startswith('ANNUAL'):
|
|
body['seats']['numberOfSeats'] = seats1
|
|
else:
|
|
body['seats']['maximumNumberOfSeats'] = seats1 if seats2 is None else seats2
|
|
return customerAuthToken, body
|
|
|
|
SUBSCRIPTION_SKIP_OBJECTS = {'customerId', 'skuId', 'subscriptionId'}
|
|
SUBSCRIPTION_TIME_OBJECTS = {'creationTime', 'startTime', 'endTime', 'trialEndTime', 'transferabilityExpirationTime'}
|
|
|
|
def _showSubscription(subscription, FJQC=None):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(subscription, timeObjects=SUBSCRIPTION_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
|
|
return
|
|
Ind.Increment()
|
|
printEntity([Ent.SUBSCRIPTION, subscription['subscriptionId']])
|
|
showJSON(None, subscription, SUBSCRIPTION_SKIP_OBJECTS, SUBSCRIPTION_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
# gam create resoldsubscription <CustomerID> (sku <SKUID>)
|
|
# (plan annual_monthly_pay|annual_yearly_pay|flexible|trial) (seats <Number>)
|
|
# [customer_auth_token <String>] [deal <String>] [purchaseorderid <String>]
|
|
def doCreateResoldSubscription():
|
|
res = buildGAPIObject(API.RESELLER)
|
|
customerId = getString(Cmd.OB_CUSTOMER_ID)
|
|
customerAuthToken, body = _getResoldSubscriptionAttr(customerId)
|
|
try:
|
|
subscription = callGAPI(res.subscriptions(), 'insert',
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
customerId=customerId, customerAuthToken=customerAuthToken, body=body)
|
|
entityActionPerformed([Ent.CUSTOMER_ID, customerId, Ent.SKU, subscription['skuId']])
|
|
_showSubscription(subscription)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId], str(e))
|
|
|
|
RENEWAL_TYPE_MAP = {
|
|
'autorenewmonthlypay': 'AUTO_RENEW_MONTHLY_PAY',
|
|
'autorenewyearlypay': 'AUTO_RENEW_YEARLY_PAY',
|
|
'cancel': 'CANCEL',
|
|
'renewcurrentusersmonthlypay': 'RENEW_CURRENT_USERS_MONTHLY_PAY',
|
|
'renewcurrentusersyearlypay': 'RENEW_CURRENT_USERS_YEARLY_PAY',
|
|
'switchtopayasyougo': 'SWITCH_TO_PAY_AS_YOU_GO',
|
|
}
|
|
|
|
# gam update resoldsubscription <CustomerID> <SKUID>
|
|
# activate|suspend|startpaidservice|
|
|
# (renewal auto_renew_monthly_pay|auto_renew_yearly_pay|cancel|renew_current_users_monthly_pay|renew_current_users_yearly_pay|switch_to_pay_as_you_go)|
|
|
# (seats <Number>)|
|
|
# (plan annual_monthly_pay|annual_yearly_pay|flexible|trial [deal <String>] [purchaseorderid <String>] [seats <Number>])
|
|
def doUpdateResoldSubscription():
|
|
def _getSeats():
|
|
seats1 = getInteger(minVal=0)
|
|
if Cmd.ArgumentsRemaining() and Cmd.Current().isdigit():
|
|
seats2 = getInteger(minVal=0)
|
|
else:
|
|
seats2 = None
|
|
if planName.startswith('ANNUAL'):
|
|
return {'numberOfSeats': seats1}
|
|
return {'maximumNumberOfSeats': seats1 if seats2 is None else seats2}
|
|
|
|
res = buildGAPIObject(API.RESELLER)
|
|
function = None
|
|
customerId, skuId, subscriptionId, planName = getCustomerSubscription(res)
|
|
kwargs = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'activate':
|
|
function = 'activate'
|
|
elif myarg == 'suspend':
|
|
function = 'suspend'
|
|
elif myarg == 'startpaidservice':
|
|
function = 'startPaidService'
|
|
elif myarg in {'renewal', 'renewaltype'}:
|
|
function = 'changeRenewalSettings'
|
|
kwargs['body'] = {'renewalType': getChoice(RENEWAL_TYPE_MAP, mapChoice=True)}
|
|
elif myarg == 'seats':
|
|
function = 'changeSeats'
|
|
kwargs['body'] = _getSeats()
|
|
elif myarg == 'plan':
|
|
function = 'changePlan'
|
|
planName = getChoice(PLAN_NAME_MAP, mapChoice=True)
|
|
kwargs['body'] = {'planName': planName}
|
|
while Cmd.ArgumentsRemaining():
|
|
planarg = getArgument()
|
|
if planarg == 'seats':
|
|
kwargs['body']['seats'] = _getSeats()
|
|
elif planarg in {'purchaseorderid', 'po'}:
|
|
kwargs['body']['purchaseOrderId'] = getString('purchaseOrderId')
|
|
elif planarg in {'dealcode', 'deal'}:
|
|
kwargs['body']['dealCode'] = getString('dealCode')
|
|
else:
|
|
unknownArgumentExit()
|
|
else:
|
|
unknownArgumentExit()
|
|
try:
|
|
subscription = callGAPI(res.subscriptions(), function,
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
customerId=customerId, subscriptionId=subscriptionId, **kwargs)
|
|
entityActionPerformed([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId])
|
|
if subscription:
|
|
_showSubscription(subscription)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId], str(e))
|
|
|
|
DELETION_TYPE_MAP = {
|
|
'cancel': 'cancel',
|
|
'downgrade': 'downgrade',
|
|
'transfertodirect': 'transfer_to_direct',
|
|
}
|
|
|
|
# gam delete resoldsubscription <CustomerID> <SKUID> cancel|downgrade|transfer_to_direct
|
|
def doDeleteResoldSubscription():
|
|
res = buildGAPIObject(API.RESELLER)
|
|
customerId, skuId, subscriptionId, _ = getCustomerSubscription(res)
|
|
deletionType = getChoice(DELETION_TYPE_MAP, mapChoice=True)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(res.subscriptions(), 'delete',
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
customerId=customerId, subscriptionId=subscriptionId, deletionType=deletionType)
|
|
entityActionPerformed([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId])
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId], str(e))
|
|
|
|
# gam info resoldsubscription <CustomerID> <SKUID>
|
|
def doInfoResoldSubscription():
|
|
res = buildGAPIObject(API.RESELLER)
|
|
customerId, skuId, subscriptionId, _ = getCustomerSubscription(res)
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
FJQC.GetFormatJSON(myarg)
|
|
try:
|
|
subscription = callGAPI(res.subscriptions(), 'get',
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
customerId=customerId, subscriptionId=subscriptionId)
|
|
if not FJQC.formatJSON:
|
|
printEntity([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId])
|
|
_showSubscription(subscription, FJQC)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId, Ent.SKU, skuId], str(e))
|
|
|
|
PRINT_RESOLD_SUBSCRIPTIONS_TITLES = ['customerId', 'skuId', 'subscriptionId']
|
|
|
|
# gam print resoldsubscriptions [todrive <ToDriveAttribute>*]
|
|
# [customerid <CustomerID>] [customer_auth_token <String>] [customer_prefix <String>]
|
|
# [maxresults <Number>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show resoldsubscriptions
|
|
# [customerid <CustomerID>] [customer_auth_token <String>] [customer_prefix <String>]
|
|
# [maxresults <Number>]
|
|
# [formatjson]
|
|
def doPrintShowResoldSubscriptions():
|
|
res = buildGAPIObject(API.RESELLER)
|
|
kwargs = {'maxResults': 100}
|
|
csvPF = CSVPrintFile(PRINT_RESOLD_SUBSCRIPTIONS_TITLES, 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'customerid':
|
|
kwargs['customerId'] = getString(Cmd.OB_CUSTOMER_ID)
|
|
elif myarg in {'customerauthtoken', 'transfertoken'}:
|
|
kwargs['customerAuthToken'] = getString(Cmd.OB_CUSTOMER_AUTH_TOKEN)
|
|
elif myarg == 'customerprefix':
|
|
kwargs['customerNamePrefix'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'maxresults':
|
|
kwargs['maxResults'] = getInteger(minVal=1, maxVal=100)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
try:
|
|
subscriptions = callGAPIpages(res.subscriptions(), 'list', 'subscriptions',
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
fields='nextPageToken,subscriptions', **kwargs)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.SUBSCRIPTION, None], str(e))
|
|
return
|
|
jcount = len(subscriptions)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, Ent.SUBSCRIPTION)
|
|
Ind.Increment()
|
|
j = 0
|
|
for subscription in subscriptions:
|
|
j += 1
|
|
if not FJQC.formatJSON:
|
|
printEntity([Ent.CUSTOMER_ID, subscription['customerId'], Ent.SKU, subscription['skuId']], j, jcount)
|
|
_showSubscription(subscription, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
for subscription in subscriptions:
|
|
row = flattenJSON(subscription, timeObjects=SUBSCRIPTION_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'customerId': subscription['customerId'],
|
|
'skuId': subscription['skuId'],
|
|
'subscriptionId': subscription['subscriptionId'],
|
|
'JSON': json.dumps(cleanJSON(subscription, timeObjects=SUBSCRIPTION_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
|
|
csvPF.writeCSVfile('Resold Subscriptions')
|
|
|
|
def normalizeChannelResellerID(resellerId):
|
|
if resellerId.startswith('accounts/'):
|
|
return resellerId
|
|
return f'accounts/{resellerId}'
|
|
|
|
def normalizeChannelCustomerID(customerId):
|
|
if customerId.startswith('customers/'):
|
|
return customerId
|
|
return f'customers/{customerId}'
|
|
|
|
def normalizeChannelProductID(productId):
|
|
if productId.startswith('products/'):
|
|
return productId
|
|
return f'products/{productId}'
|
|
|
|
CHANNEL_ENTITY_MAP = {
|
|
Ent.CHANNEL_CUSTOMER:
|
|
{'JSONtitles': ['name', 'domain', 'JSON'],
|
|
'timeObjects': ['createTime', 'updateTime'],
|
|
'items': 'customers',
|
|
'pageSize': 50,
|
|
'maxPageSize': 50,
|
|
'fields': {
|
|
'name': 'name',
|
|
'orgdisplayname': 'orgDisplayName',
|
|
'orgpostaladdress': 'orgPostalAddress',
|
|
'primarycontactinfo': 'primaryContactInfo',
|
|
'alternateemail': 'alternateEmail',
|
|
'domain': 'domain',
|
|
'createtime': 'createTime',
|
|
'updatetime': 'updateTime',
|
|
'cloudidentityid': 'cloudIdentityId',
|
|
'languagecode': 'languageCode',
|
|
'cloudidentityinfo': 'cloudIdentityInfo',
|
|
'channelpartnerid': 'channelPartnerId',
|
|
}
|
|
},
|
|
Ent.CHANNEL_CUSTOMER_ENTITLEMENT:
|
|
{'JSONtitles': ['name', 'offer', 'JSON'],
|
|
'timeObjects': ['createTime', 'updateTime', 'startTime', 'endTime'],
|
|
'items': 'entitlements',
|
|
'pageSize': 100,
|
|
'maxPageSize': 100,
|
|
'fields': {
|
|
'name': 'name',
|
|
'createtime': 'createTime',
|
|
'updatetime': 'updateTime',
|
|
'offer': 'offer',
|
|
'commitmentsettings': 'commitmentSettings',
|
|
'provisioningstate': 'provisioningState',
|
|
'provisionedservice': 'provisionedService',
|
|
'suspensionreasons': 'suspensionReasons',
|
|
'purchaseorderid': 'purchaseOrderId',
|
|
'trialsettings': 'trialSettings',
|
|
'associationinfo': 'associationInfo',
|
|
'parameters': 'parameters',
|
|
}
|
|
},
|
|
Ent.CHANNEL_OFFER:
|
|
{'JSONtitles': ['name', 'sku', 'JSON'],
|
|
'timeObjects': ['startTime', 'endTime'],
|
|
'items': 'offers',
|
|
'pageSize': 1000,
|
|
'maxPageSize': 1000,
|
|
'fields': {
|
|
'name': 'name',
|
|
'marketinginfo': 'marketingInfo',
|
|
'sku': 'sku',
|
|
'plan': 'plan',
|
|
'constraints': 'constraints',
|
|
'pricebyresources': 'priceByResources',
|
|
'starttime': 'startTime',
|
|
'endtime': 'endTime',
|
|
'parameterdefinitions': 'parameterDefinitions',
|
|
}
|
|
},
|
|
Ent.CHANNEL_PRODUCT:
|
|
{'JSONtitles': ['name', 'JSON'],
|
|
'timeObjects': None,
|
|
'items': 'products',
|
|
'pageSize': 1000,
|
|
'maxPageSize': 1000,
|
|
'fields': {
|
|
'name': 'name',
|
|
'marketinginfo': 'marketingInfo',
|
|
}
|
|
},
|
|
Ent.CHANNEL_SKU:
|
|
{'JSOBtitles': ['name', 'JSON'],
|
|
'timeObjects': None,
|
|
'items': 'skus',
|
|
'pageSize': 1000,
|
|
'maxPageSize': 1000,
|
|
'fields': {
|
|
'name': 'name',
|
|
'marketinginfo': 'marketingInfo',
|
|
'product': 'product',
|
|
}
|
|
}
|
|
}
|
|
|
|
def doPrintShowChannelItems(entityType):
|
|
cchan = buildGAPIObject(API.CLOUDCHANNEL)
|
|
if entityType == Ent.CHANNEL_CUSTOMER:
|
|
service = cchan.accounts().customers()
|
|
elif entityType == Ent.CHANNEL_CUSTOMER_ENTITLEMENT:
|
|
service = cchan.accounts().customers().entitlements()
|
|
elif entityType == Ent.CHANNEL_OFFER:
|
|
service = cchan.accounts().offers()
|
|
elif entityType == Ent.CHANNEL_PRODUCT:
|
|
service = cchan.products()
|
|
else: #Ent.CHANNEL_SKU
|
|
service = cchan.products().skus()
|
|
channelEntityMap = CHANNEL_ENTITY_MAP[entityType]
|
|
# csvPF = CSVPrintFile(channelEntityMap['titles'], 'sortall') if Act.csvFormat() else None
|
|
csvPF = CSVPrintFile(['name'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
resellerId = normalizeChannelResellerID(GC.Values[GC.RESELLER_ID] if GC.Values[GC.RESELLER_ID] else GC.Values[GC.CUSTOMER_ID])
|
|
customerId = normalizeChannelCustomerID(GC.Values[GC.CHANNEL_CUSTOMER_ID])
|
|
name = None
|
|
productId = 'products/-'
|
|
kwargs = {'pageSize': channelEntityMap['pageSize']}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'resellerid':
|
|
resellerId = normalizeChannelResellerID(getString(Cmd.OB_RESELLER_ID))
|
|
elif (entityType == Ent.CHANNEL_CUSTOMER_ENTITLEMENT) and myarg in {'customerid', 'channelcustomerid'}:
|
|
customerId = normalizeChannelCustomerID(getString(Cmd.OB_CHANNEL_CUSTOMER_ID))
|
|
elif (entityType == Ent.CHANNEL_CUSTOMER_ENTITLEMENT) and myarg == 'name':
|
|
name = getString(Cmd.OB_STRING)
|
|
parent = name.split('/')
|
|
if (len(parent) != 4) or (parent[0] != 'accounts') or (not parent[1]) or (parent[2] != 'customers') or (not parent[3]):
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_RESELLER_CUSTOMER_NAME)
|
|
elif (entityType in {Ent.CHANNEL_OFFER, Ent.CHANNEL_PRODUCT, Ent.CHANNEL_SKU}) and myarg == 'language':
|
|
kwargs['languageCode'] = getLanguageCode(LANGUAGE_CODES_MAP)
|
|
elif (entityType in {Ent.CHANNEL_CUSTOMER, Ent.CHANNEL_OFFER}) and myarg == 'filter':
|
|
kwargs['filter'] = getString(Cmd.OB_STRING)
|
|
elif (entityType == Ent.CHANNEL_SKU) and myarg == 'productid':
|
|
productId = normalizeChannelProductID(getString(Cmd.OB_PRODUCT_ID))
|
|
elif myarg == 'fields':
|
|
if not fieldsList:
|
|
fieldsList.append('name')
|
|
for field in _getFieldsList():
|
|
if field in channelEntityMap['fields']:
|
|
fieldsList.append(channelEntityMap['fields'][field])
|
|
else:
|
|
invalidChoiceExit(field, list(channelEntityMap['fields']), True)
|
|
elif myarg == 'maxresults':
|
|
kwargs['pageSize'] = getInteger(minVal=1, maxVal=channelEntityMap['maxPageSize'])
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if entityType != Ent.CHANNEL_CUSTOMER_ENTITLEMENT:
|
|
entityName = resellerId
|
|
if entityType in {Ent.CHANNEL_CUSTOMER, Ent.CHANNEL_OFFER}:
|
|
kwargs['parent'] = resellerId
|
|
else:
|
|
kwargs['account'] = resellerId
|
|
if entityType == Ent.CHANNEL_SKU:
|
|
kwargs['parent'] = productId
|
|
else:
|
|
if not name and customerId == 'customers/':
|
|
missingArgumentExit('channelcustomerid')
|
|
entityName = kwargs['parent'] = name if name else f'{resellerId}/{customerId}'
|
|
fields = getItemFieldsFromFieldsList(channelEntityMap['items'], fieldsList)
|
|
# if csvPF and FJQC.formatJSON and not fieldsList:
|
|
# csvPF.SetJSONTitles(channelEntityMap['JSONtitles'])
|
|
try:
|
|
results = callGAPIpages(service, 'list', channelEntityMap['items'],
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.INTERNAL_ERROR, GAPI.NOT_FOUND],
|
|
fields=fields, **kwargs)
|
|
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.badRequest, GAPI.internalError, GAPI.notFound) as e:
|
|
entityActionFailedWarning([entityType, entityName], str(e))
|
|
return
|
|
jcount = len(results)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, entityType)
|
|
Ind.Increment()
|
|
j = 0
|
|
for item in results:
|
|
j += 1
|
|
if not FJQC.formatJSON:
|
|
printEntity([entityType, item['name']], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, item, timeObjects=channelEntityMap['timeObjects'])
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(item, timeObjects=channelEntityMap['timeObjects']),
|
|
ensure_ascii=False, sort_keys=False))
|
|
Ind.Decrement()
|
|
else:
|
|
for item in results:
|
|
row = flattenJSON(item, timeObjects=channelEntityMap['timeObjects'])
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'name': item['name'],
|
|
'JSON': json.dumps(cleanJSON(item, timeObjects=channelEntityMap['timeObjects']),
|
|
ensure_ascii=False, sort_keys=True)}
|
|
# if not fieldsList:
|
|
# if entityType == Ent.CHANNEL_CUSTOMER:
|
|
# row.update({'domain': item['domain']})
|
|
# elif entityType == Ent.CHANNEL_CUSTOMER_ENTITLEMENT:
|
|
# row.update({'offer': item['offer']})
|
|
# elif entityType == Ent.CHANNEL_OFFER:
|
|
# row.update({'sku': item['sku']})
|
|
csvPF.WriteRowNoFilter(row)
|
|
csvPF.writeCSVfile(Ent.Plural(entityType))
|
|
|
|
# gam print channelcustomers [todrive <ToDriveAttribute>*]
|
|
# [resellerid <ResellerID>] [filter <String>]
|
|
# [fields <ChannelCustomerFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show channelcustomers
|
|
# [resellerid <ResellerID>] [filter <String>]
|
|
# [fields <ChannelCustomerFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson]
|
|
def doPrintShowChannelCustomers():
|
|
doPrintShowChannelItems(Ent.CHANNEL_CUSTOMER)
|
|
|
|
# gam print channelcustomercentitlements [todrive <ToDriveAttribute>*]
|
|
# ([resellerid <ResellerID>] [customerid <ChannelCustomerID>])|
|
|
# (name accounts/<ResellerID>/customers/<ChannelCustomerID>)
|
|
# [fields <ChannelCustomerEntitlementFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show channelcustomerentitlements
|
|
# ([resellerid <ResellerID>] [customerid <ChannelCustomerID>])|
|
|
# (name accounts/<ResellerID>/customers/<ChannelCustomerID>)
|
|
# [fields <ChannelCustomerEntitlementFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson]
|
|
def doPrintShowChannelCustomerEntitlements():
|
|
doPrintShowChannelItems(Ent.CHANNEL_CUSTOMER_ENTITLEMENT)
|
|
|
|
# gam print channeloffers [todrive <ToDriveAttribute>*]
|
|
# [resellerid <ResellerID>] [filter <String>] [language <LanguageCode]
|
|
# [fields <ChannelOfferFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show channeloffers
|
|
# [resellerid <ResellerID>] [filter <String>] [language <LanguageCode]
|
|
# [fields <ChannelOfferFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson]
|
|
def doPrintShowChannelOffers():
|
|
doPrintShowChannelItems(Ent.CHANNEL_OFFER)
|
|
|
|
# gam print channelproducts [todrive <ToDriveAttribute>*]
|
|
# [resellerid <ResellerID>] [language <LanguageCode]
|
|
# [fields <ChannelProductFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show channelproducts
|
|
# [resellerid <ResellerID>] [language <LanguageCode]
|
|
# [fields <ChannelProductFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson]
|
|
def doPrintShowChannelProducts():
|
|
doPrintShowChannelItems(Ent.CHANNEL_PRODUCT)
|
|
|
|
# gam print channelskus [todrive <ToDriveAttribute>*]
|
|
# [resellerid <ResellerID>] [language <LanguageCode] [productid <ProductID>]
|
|
# [fields <ChannelSKUFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show channelskus
|
|
# [resellerid <ResellerID>] [language <LanguageCode] [productid <ProductID>]
|
|
# [fields <ChannelSKUFieldList>]
|
|
# [maxresults <Integer>]
|
|
# [formatjson]
|
|
def doPrintShowChannelSKUs():
|
|
doPrintShowChannelItems(Ent.CHANNEL_SKU)
|
|
|
|
ANALYTIC_ENTITY_MAP = {
|
|
Ent.ANALYTIC_ACCOUNT:
|
|
{'titles': ['User', 'name', 'displayName', 'createTime', 'updateTime', 'regionCode', 'deleted'],
|
|
'JSONtitles': ['User', 'name', 'displayName', 'JSON'],
|
|
'timeObjects': ['createTime', 'updateTime'],
|
|
'items': 'accounts',
|
|
'pageSize': 50,
|
|
'maxPageSize': 200,
|
|
},
|
|
Ent.ANALYTIC_ACCOUNT_SUMMARY:
|
|
{'titles': ['User', 'name', 'displayName', 'account'],
|
|
'JSONtitles': ['User', 'name', 'displayName', 'account', 'JSON'],
|
|
'timeObjects': ['createTime', 'updateTime', 'deleteTime', 'expireTime'],
|
|
'items': 'accountSummaries',
|
|
'pageSize': 50,
|
|
'maxPageSize': 200,
|
|
},
|
|
Ent.ANALYTIC_DATASTREAM:
|
|
{'titles': ['User', 'name', 'displayName', 'type', 'createTime', 'updateTime'],
|
|
'JSONtitles': ['User', 'name', 'displayName', 'type', 'JSON'],
|
|
'timeObjects': ['createTime', 'updateTime'],
|
|
'items': 'dataStreams',
|
|
'pageSize': 50,
|
|
'maxPageSize': 200,
|
|
},
|
|
Ent.ANALYTIC_PROPERTY:
|
|
{'titles': ['User', 'name', 'displayName', 'createTime', 'updateTime', 'propertyType', 'parent'],
|
|
'JSONtitles': ['User', 'name', 'displayName', 'propertyType', 'parent', 'JSON'],
|
|
'timeObjects': ['createTime', 'updateTime', 'deleteTime', 'expireTime'],
|
|
'items': 'properties',
|
|
'pageSize': 50,
|
|
'maxPageSize': 200,
|
|
},
|
|
Ent.ANALYTIC_UA_PROPERTY:
|
|
{'titles': ['User', 'accountId', 'name', 'id', 'created', 'updated'],
|
|
'JSONtitles': ['User', 'accountId', 'name', 'id', 'JSON'],
|
|
'timeObjects': ['created', 'updated'],
|
|
'items': 'items',
|
|
'pageSize': 50,
|
|
'maxPageSize': 200,
|
|
},
|
|
}
|
|
|
|
def printShowAnalyticItems(users, entityType):
|
|
analyticEntityMap = ANALYTIC_ENTITY_MAP[entityType]
|
|
csvPF = CSVPrintFile(analyticEntityMap['titles'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
if entityType != Ent.ANALYTIC_UA_PROPERTY:
|
|
kwargs = {'pageSize': analyticEntityMap['pageSize']}
|
|
api = API.ANALYTICS_ADMIN
|
|
else:
|
|
# kwargs = {'webPropertyId': '~all'}
|
|
kwargs = {}
|
|
api = API.ANALYTICS
|
|
if entityType in {Ent.ANALYTIC_ACCOUNT, Ent.ANALYTIC_PROPERTY}:
|
|
kwargs['showDeleted'] = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'maxresults':
|
|
kwargs['pageSize'] = getInteger(minVal=1, maxVal=analyticEntityMap['maxPageSize'])
|
|
elif entityType in {Ent.ANALYTIC_ACCOUNT, Ent.ANALYTIC_PROPERTY} and myarg == 'showdeleted':
|
|
kwargs['showDeleted'] = getBoolean()
|
|
elif entityType == Ent.ANALYTIC_PROPERTY and myarg == 'filter':
|
|
kwargs['filter'] = getString(Cmd.OB_STRING)
|
|
elif entityType == Ent.ANALYTIC_UA_PROPERTY and myarg == 'accountid':
|
|
kwargs['accountId'] = getString(Cmd.OB_STRING).replace('accounts/', '')
|
|
elif entityType == Ent.ANALYTIC_DATASTREAM and myarg == 'parent':
|
|
kwargs['parent'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if entityType == Ent.ANALYTIC_PROPERTY and 'filter' not in kwargs:
|
|
missingArgumentExit('filter')
|
|
if entityType == Ent.ANALYTIC_UA_PROPERTY and 'accountId' not in kwargs:
|
|
missingArgumentExit('accountid')
|
|
if entityType == Ent.ANALYTIC_DATASTREAM and 'parent' not in kwargs:
|
|
missingArgumentExit('parent')
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(analyticEntityMap['JSONtitles'])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, analytics = buildGAPIServiceObject(api, user, i, count)
|
|
if not analytics:
|
|
continue
|
|
if entityType == Ent.ANALYTIC_ACCOUNT:
|
|
service = analytics.accounts()
|
|
elif entityType == Ent.ANALYTIC_ACCOUNT_SUMMARY:
|
|
service = analytics.accountSummaries()
|
|
elif entityType == Ent.ANALYTIC_DATASTREAM:
|
|
service = analytics.properties().dataStreams()
|
|
elif entityType == Ent.ANALYTIC_PROPERTY:
|
|
service = analytics.properties()
|
|
else: #Ent.ANALYTIC_UA_PROPERTY:
|
|
service = analytics.management().webproperties()
|
|
# service = analytics.management().profiles()
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(entityType, user, i, count)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
try:
|
|
results = callGAPIpages(service, 'list', analyticEntityMap['items'],
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.INTERNAL_ERROR,
|
|
GAPI.SERVICE_NOT_AVAILABLE],
|
|
**kwargs)
|
|
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.badRequest, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, None], str(e), i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userAnalyticsServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(results)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, entityType)
|
|
Ind.Increment()
|
|
j = 0
|
|
for item in results:
|
|
j += 1
|
|
if not FJQC.formatJSON:
|
|
printEntity([entityType, item['name']], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, item, timeObjects=analyticEntityMap['timeObjects'])
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(item, timeObjects=analyticEntityMap['timeObjects']),
|
|
ensure_ascii=False, sort_keys=False))
|
|
Ind.Decrement()
|
|
else:
|
|
for item in results:
|
|
row = flattenJSON(item, flattened={'User': user}, timeObjects=analyticEntityMap['timeObjects'])
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
if entityType != Ent.ANALYTIC_UA_PROPERTY:
|
|
row = {'User': user, 'name': item['name'], 'displayName': item['displayName']}
|
|
else:
|
|
row = {'User': user, 'accountId': item['accountId'], 'id': item['id'], 'name': item['name']}
|
|
for field in analyticEntityMap['JSONtitles'][2:-1]:
|
|
row[field] = item[field]
|
|
row['JSON'] = json.dumps(cleanJSON(item, timeObjects=analyticEntityMap['timeObjects']),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
if csvPF:
|
|
csvPF.writeCSVfile(Ent.Plural(entityType))
|
|
|
|
# gam <UserTypeEntity> print analyticaccounts [todrive <ToDriveAttribute>*]
|
|
# [maxresults <Integer>] [showdeleted [<Boolean>]]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show analyticaccounts
|
|
# [maxresults <Integer>] [showdeleted [<Boolean>]]
|
|
# [formatjson]
|
|
def printShowAnalyticAccounts(users):
|
|
printShowAnalyticItems(users, Ent.ANALYTIC_ACCOUNT)
|
|
|
|
# gam <UserTypeEntity> print analyticaccountsummaries [todrive <ToDriveAttribute>*]
|
|
# [maxresults <Integer>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show analyticaccountsummaries
|
|
# [maxresults <Integer>]
|
|
# [formatjson]
|
|
def printShowAnalyticAccountSummaries(users):
|
|
printShowAnalyticItems(users, Ent.ANALYTIC_ACCOUNT_SUMMARY)
|
|
|
|
# gam <UserTypeEntity> print analyticproperties [todrive <ToDriveAttribute>*]
|
|
# filter <String>
|
|
# [maxresults <Integer>] [showdeleted [<Boolean>]]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show analyticproperties
|
|
# filter <String>
|
|
# [maxresults <Integer>] [showdeleted [<Boolean>]]
|
|
# [formatjson]
|
|
def printShowAnalyticProperties(users):
|
|
printShowAnalyticItems(users, Ent.ANALYTIC_PROPERTY)
|
|
|
|
# gam <UserTypeEntity> print analyticuaproperties [todrive <ToDriveAttribute>*]
|
|
# accountid [accounts/]<String>
|
|
# [maxresults <Integer>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show analyticuaproperties
|
|
# accountid [accounts/]<String>
|
|
# [maxresults <Integer>]
|
|
# [formatjson]
|
|
def printShowAnalyticUAProperties(users):
|
|
printShowAnalyticItems(users, Ent.ANALYTIC_UA_PROPERTY)
|
|
|
|
# gam <UserTypeEntity> print analyticdatastreams [todrive <ToDriveAttribute>*]
|
|
# parent <String>
|
|
# [maxresults <Integer>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show analyticdatastreams
|
|
# parent <String>
|
|
# [maxresults <Integer>]
|
|
# [formatjson]
|
|
def printShowAnalyticDatastreams(users):
|
|
printShowAnalyticItems(users, Ent.ANALYTIC_DATASTREAM)
|
|
|
|
# gam create domainalias|aliasdomain <DomainAlias> <DomainName>
|
|
def doCreateDomainAlias():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
body = {'domainAliasName': getString(Cmd.OB_DOMAIN_ALIAS)}
|
|
body['parentDomainName'] = getString(Cmd.OB_DOMAIN_NAME)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cd.domainAliases(), 'insert',
|
|
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.DUPLICATE, GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.NOT_FOUND,
|
|
GAPI.FORBIDDEN, GAPI.CONFLICT],
|
|
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
|
|
entityActionPerformed([Ent.DOMAIN, body['parentDomainName'], Ent.DOMAIN_ALIAS, body['domainAliasName']])
|
|
except GAPI.domainNotFound:
|
|
entityActionFailedWarning([Ent.DOMAIN, body['parentDomainName']], Msg.DOES_NOT_EXIST)
|
|
except GAPI.duplicate:
|
|
entityActionFailedWarning([Ent.DOMAIN, body['parentDomainName'], Ent.DOMAIN_ALIAS, body['domainAliasName']], Msg.DUPLICATE)
|
|
except (GAPI.invalid, GAPI.conflict) as e:
|
|
entityActionFailedWarning([Ent.DOMAIN, body['parentDomainName'], Ent.DOMAIN_ALIAS, body['domainAliasName']], str(e))
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
|
|
accessErrorExit(cd, str(e))
|
|
|
|
# gam delete domainalias|aliasdomain <DomainAlias>
|
|
def doDeleteDomainAlias():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
domainAliasName = getString(Cmd.OB_DOMAIN_ALIAS)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cd.domainAliases(), 'delete',
|
|
throwReasons=[GAPI.DOMAIN_ALIAS_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], domainAliasName=domainAliasName)
|
|
entityActionPerformed([Ent.DOMAIN_ALIAS, domainAliasName])
|
|
except GAPI.domainAliasNotFound:
|
|
entityActionFailedWarning([Ent.DOMAIN_ALIAS, domainAliasName], Msg.DOES_NOT_EXIST)
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
|
|
accessErrorExit(cd, str(e))
|
|
|
|
DOMAIN_TIME_OBJECTS = {'creationTime'}
|
|
DOMAIN_ALIAS_PRINT_ORDER = ['parentDomainName', 'creationTime', 'verified']
|
|
DOMAIN_ALIAS_SKIP_OBJECTS = {'domainAliasName'}
|
|
|
|
def _showDomainAlias(alias, FJQC, aliasSkipObjects, i=0, count=0):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(alias, timeObjects=DOMAIN_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.DOMAIN_ALIAS, alias['domainAliasName']], i, count)
|
|
Ind.Increment()
|
|
if 'creationTime' in alias:
|
|
alias['creationTime'] = formatLocalTimestamp(alias['creationTime'])
|
|
for field in DOMAIN_ALIAS_PRINT_ORDER:
|
|
if field in alias:
|
|
printKeyValueList([field, alias[field]])
|
|
aliasSkipObjects.add(field)
|
|
showJSON(None, alias, aliasSkipObjects)
|
|
Ind.Decrement()
|
|
|
|
# gam info domainalias|aliasdomain <DomainAlias> [formatjson]
|
|
def doInfoDomainAlias():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
domainAliasName = getString(Cmd.OB_DOMAIN_ALIAS)
|
|
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
|
|
try:
|
|
result = callGAPI(cd.domainAliases(), 'get',
|
|
throwReasons=[GAPI.DOMAIN_ALIAS_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], domainAliasName=domainAliasName)
|
|
aliasSkipObjects = DOMAIN_ALIAS_SKIP_OBJECTS
|
|
_showDomainAlias(result, FJQC, aliasSkipObjects)
|
|
except GAPI.domainAliasNotFound:
|
|
entityActionFailedWarning([Ent.DOMAIN_ALIAS, domainAliasName], Msg.DOES_NOT_EXIST)
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
|
|
accessErrorExit(cd, str(e))
|
|
|
|
def _printDomain(domain, csvPF):
|
|
row = {}
|
|
for attr in domain:
|
|
if attr not in DEFAULT_SKIP_OBJECTS:
|
|
if attr in DOMAIN_TIME_OBJECTS:
|
|
row[attr] = formatLocalTimestamp(domain[attr])
|
|
else:
|
|
row[attr] = domain[attr]
|
|
csvPF.AddTitles(attr)
|
|
csvPF.WriteRow(row)
|
|
|
|
DOMAIN_ALIAS_SORT_TITLES = ['domainAliasName', 'parentDomainName', 'creationTime', 'verified']
|
|
|
|
# gam print domainaliases [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [showitemcountonly]
|
|
# gam show domainaliases
|
|
# [formatjson]
|
|
# [showitemcountonly]
|
|
def doPrintShowDomainAliases():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['domainAliasName'], DOMAIN_ALIAS_SORT_TITLES) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
showItemCountOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
try:
|
|
domainAliases = callGAPIitems(cd.domainAliases(), 'list', 'domainAliases',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID])
|
|
count = len(domainAliases)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{count}\n')
|
|
return
|
|
i = 0
|
|
for domainAlias in domainAliases:
|
|
i += 1
|
|
if not csvPF:
|
|
aliasSkipObjects = DOMAIN_ALIAS_SKIP_OBJECTS
|
|
_showDomainAlias(domainAlias, FJQC, aliasSkipObjects, i, count)
|
|
elif not FJQC.formatJSON:
|
|
_printDomain(domainAlias, csvPF)
|
|
else:
|
|
csvPF.WriteRowNoFilter({'domainAliasName': domainAlias['domainAliasName'],
|
|
'JSON': json.dumps(cleanJSON(domainAlias, timeObjects=DOMAIN_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
|
|
accessErrorExit(cd, str(e))
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Domain Aliases')
|
|
|
|
# gam create domain <DomainName>
|
|
def doCreateDomain():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
body = {'domainName': getString(Cmd.OB_DOMAIN_NAME)}
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cd.domains(), 'insert',
|
|
throwReasons=[GAPI.DUPLICATE, GAPI.DOMAIN_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.CONFLICT],
|
|
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
|
|
entityActionPerformed([Ent.DOMAIN, body['domainName']])
|
|
except GAPI.duplicate:
|
|
entityDuplicateWarning([Ent.DOMAIN, body['domainName']])
|
|
except GAPI.conflict as e:
|
|
entityActionFailedWarning([Ent.DOMAIN, body['domainName']], str(e))
|
|
except (GAPI.domainNotFound, GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
|
|
accessErrorExit(cd, str(e))
|
|
|
|
# gam update domain <DomainName> primary
|
|
def doUpdateDomain():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
domainName = getString(Cmd.OB_DOMAIN_NAME)
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'primary':
|
|
body['customerDomain'] = domainName
|
|
else:
|
|
unknownArgumentExit()
|
|
if not body:
|
|
missingArgumentExit('primary')
|
|
try:
|
|
callGAPI(cd.customers(), 'update',
|
|
throwReasons=[GAPI.DOMAIN_NOT_VERIFIED_SECONDARY, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT],
|
|
customerKey=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
|
|
entityActionPerformedMessage([Ent.DOMAIN, domainName], Msg.NOW_THE_PRIMARY_DOMAIN)
|
|
except GAPI.domainNotVerifiedSecondary:
|
|
entityActionFailedWarning([Ent.DOMAIN, domainName], Msg.DOMAIN_NOT_VERIFIED_SECONDARY)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalidInput) as e:
|
|
accessErrorExit(cd, str(e))
|
|
|
|
# gam delete domain <DomainName>
|
|
def doDeleteDomain():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
domainName = getString(Cmd.OB_DOMAIN_NAME)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cd.domains(), 'delete',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], domainName=domainName)
|
|
entityActionPerformed([Ent.DOMAIN, domainName])
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
|
|
accessErrorExit(cd, str(e))
|
|
|
|
CUSTOMER_LICENSE_MAP = {
|
|
'accounts:num_users': 'Total Users',
|
|
'accounts:gsuite_basic_total_licenses': 'G Suite Basic Licenses',
|
|
'accounts:gsuite_basic_used_licenses': 'G Suite Basic Users',
|
|
'accounts:gsuite_enterprise_total_licenses': 'Workspace Enterprise Plus Licenses',
|
|
'accounts:gsuite_enterprise_used_licenses': 'Workspace Enterprise Plus Users',
|
|
'accounts:gsuite_unlimited_total_licenses': 'G Suite Business Licenses',
|
|
'accounts:gsuite_unlimited_used_licenses': 'G Suite Business Users',
|
|
'accounts:vault_total_licenses': 'Google Vault Licenses',
|
|
}
|
|
|
|
def _showCustomerLicenseInfo(customerInfo, FJQC):
|
|
def numUsersAvailable(result):
|
|
usageReports = result.get('usageReports', [])
|
|
if usageReports:
|
|
for item in usageReports[0].get('parameters', []):
|
|
if item['name'] == 'accounts:num_users':
|
|
return usageReports
|
|
return None
|
|
|
|
rep = buildGAPIObject(API.REPORTS)
|
|
parameters = ','.join(CUSTOMER_LICENSE_MAP)
|
|
tryDate = todaysDate().strftime(YYYYMMDD_FORMAT)
|
|
dataRequiredServices = {'accounts'}
|
|
while True:
|
|
try:
|
|
result = callGAPI(rep.customerUsageReports(), 'get',
|
|
throwReasons=[GAPI.INVALID, GAPI.FORBIDDEN],
|
|
date=tryDate, customerId=customerInfo['id'],
|
|
fields='warnings,usageReports', parameters=parameters)
|
|
usageReports = numUsersAvailable(result)
|
|
if usageReports:
|
|
break
|
|
fullData, tryDate, usageReports = _checkDataRequiredServices(result, tryDate, dataRequiredServices)
|
|
if fullData < 0:
|
|
printWarningMessage(DATA_NOT_AVALIABLE_RC, Msg.NO_USER_COUNTS_DATA_AVAILABLE)
|
|
return
|
|
if fullData == 0:
|
|
continue
|
|
break
|
|
except GAPI.invalid as e:
|
|
tryDate = _adjustTryDate(str(e), 0, -1, tryDate)
|
|
if not tryDate:
|
|
return
|
|
continue
|
|
except GAPI.forbidden:
|
|
return
|
|
if not FJQC.formatJSON:
|
|
printKeyValueList([f'User counts as of {tryDate}:'])
|
|
Ind.Increment()
|
|
for item in usageReports[0]['parameters']:
|
|
api_name = CUSTOMER_LICENSE_MAP.get(item['name'])
|
|
api_value = int(item.get('intValue', '0'))
|
|
if api_name and api_value:
|
|
if not FJQC.formatJSON:
|
|
printKeyValueList([api_name, f'{api_value:,}'])
|
|
else:
|
|
customerInfo[item['name']] = api_value
|
|
if not FJQC.formatJSON:
|
|
Ind.Decrement()
|
|
|
|
def setTrueCustomerId(cd=None):
|
|
if GC.Values[GC.CUSTOMER_ID] == GC.MY_CUSTOMER:
|
|
if not cd:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
try:
|
|
customerInfo = callGAPI(cd.customers(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerKey=GC.MY_CUSTOMER,
|
|
fields='id')
|
|
GC.Values[GC.CUSTOMER_ID] = customerInfo['id']
|
|
except (GAPI.badRequest, GAPI.invalidInput, GAPI.resourceNotFound, GAPI.forbidden):
|
|
pass
|
|
|
|
def _getCustomerId():
|
|
customerId = GC.Values[GC.CUSTOMER_ID]
|
|
if customerId != GC.MY_CUSTOMER and customerId[0] != 'C':
|
|
customerId = 'C' + customerId
|
|
return customerId
|
|
|
|
def _getCustomerIdNoC():
|
|
customerId = GC.Values[GC.CUSTOMER_ID]
|
|
if customerId[0] == 'C':
|
|
return customerId[1:]
|
|
return customerId
|
|
|
|
def _getCustomersCustomerIdNoC():
|
|
customerId = GC.Values[GC.CUSTOMER_ID]
|
|
if customerId.startswith('C'):
|
|
customerId = customerId[1:]
|
|
return f'customers/{customerId}'
|
|
|
|
def _getCustomersCustomerIdWithC():
|
|
customerId = GC.Values[GC.CUSTOMER_ID]
|
|
if customerId != GC.MY_CUSTOMER and customerId[0] != 'C':
|
|
customerId = 'C' + customerId
|
|
return f'customers/{customerId}'
|
|
|
|
# gam info customer [formatjson]
|
|
def doInfoCustomer(returnCustomerInfo=None, FJQC=None):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
customerId = _getCustomerId()
|
|
if FJQC is None:
|
|
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
|
|
try:
|
|
customerInfo = callGAPI(cd.customers(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerKey=customerId)
|
|
if 'customerCreationTime' in customerInfo:
|
|
customerInfo['customerCreationTime'] = formatLocalTime(customerInfo['customerCreationTime'])
|
|
else:
|
|
customerInfo['customerCreationTime'] = UNKNOWN
|
|
primaryDomain = {'domainName': UNKNOWN, 'verified': UNKNOWN}
|
|
try:
|
|
domains = callGAPIitems(cd.domains(), 'list', 'domains',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=customerInfo['id'], fields='domains(creationTime,domainName,isPrimary,verified)')
|
|
for domain in domains:
|
|
if domain.get('isPrimary'):
|
|
primaryDomain = domain
|
|
break
|
|
# From Jay Lee
|
|
# If customer has changed primary domain, customerCreationTime is date of current primary being added, not customer create date.
|
|
# We should get all domains and use oldest date
|
|
customerCreationTime = UNKNOWN
|
|
for domain in domains:
|
|
domainCreationTime = formatLocalTimestampUTC(domain['creationTime'])
|
|
if customerCreationTime == UNKNOWN or domainCreationTime < customerCreationTime:
|
|
customerCreationTime = domainCreationTime
|
|
customerInfo['customerCreationTime'] = formatLocalTime(customerCreationTime)
|
|
except (GAPI.badRequest, GAPI.notFound):
|
|
pass
|
|
customerInfo['customerDomain'] = primaryDomain['domainName']
|
|
customerInfo['verified'] = primaryDomain['verified']
|
|
if FJQC.formatJSON:
|
|
_showCustomerLicenseInfo(customerInfo, FJQC)
|
|
if returnCustomerInfo is not None:
|
|
returnCustomerInfo.update(customerInfo)
|
|
return
|
|
printLine(json.dumps(cleanJSON(customerInfo), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printKeyValueList(['Customer ID', customerInfo['id']])
|
|
printKeyValueList(['Primary Domain', customerInfo['customerDomain']])
|
|
printKeyValueList(['Primary Domain Verified', customerInfo['verified']])
|
|
printKeyValueList(['Customer Creation Time', customerInfo['customerCreationTime']])
|
|
printKeyValueList(['Default Language', customerInfo.get('language', 'Unset or Unknown (defaults to en)')])
|
|
_showCustomerAddressPhoneNumber(customerInfo)
|
|
printKeyValueList(['Admin Secondary Email', customerInfo.get('alternateEmail', UNKNOWN)])
|
|
_showCustomerLicenseInfo(customerInfo, FJQC)
|
|
except (GAPI.badRequest, GAPI.invalidInput, GAPI.domainNotFound, GAPI.notFound, GAPI.resourceNotFound):
|
|
accessErrorExit(cd)
|
|
except GAPI.forbidden as e:
|
|
entityActionFailedExit([Ent.CUSTOMER_ID, customerId], str(e))
|
|
|
|
# gam update customer [primary <DomainName>] [adminsecondaryemail|alternateemail <EmailAddress>] [language <LanguageCode] [phone|phonenumber <String>]
|
|
# [contact|contactname <String>] [name|organizationname <String>]
|
|
# [address1|addressline1 <String>] [address2|addressline2 <String>] [address3|addressline3 <String>]
|
|
# [locality <String>] [region <String>] [postalcode <String>] [country|countrycode <String>]
|
|
def doUpdateCustomer():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
customerId = _getCustomerId()
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in ADDRESS_FIELDS_ARGUMENT_MAP:
|
|
body.setdefault('postalAddress', {})
|
|
body['postalAddress'][ADDRESS_FIELDS_ARGUMENT_MAP[myarg]] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'primary':
|
|
body['customerDomain'] = getString(Cmd.OB_DOMAIN_NAME)
|
|
elif myarg in {'adminsecondaryemail', 'alternateemail'}:
|
|
body['alternateEmail'] = getEmailAddress(noUid=True)
|
|
elif myarg in {'phone', 'phonenumber'}:
|
|
body['phoneNumber'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'language':
|
|
body['language'] = getLanguageCode(LANGUAGE_CODES_MAP)
|
|
else:
|
|
unknownArgumentExit()
|
|
if body:
|
|
try:
|
|
callGAPI(cd.customers(), 'patch',
|
|
throwReasons=[GAPI.DOMAIN_NOT_VERIFIED_SECONDARY, GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerKey=customerId, body=body, fields='')
|
|
entityActionPerformed([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID]])
|
|
except GAPI.domainNotVerifiedSecondary:
|
|
entityActionFailedWarning([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID], Ent.DOMAIN, body['customerDomain']], Msg.DOMAIN_NOT_VERIFIED_SECONDARY)
|
|
except (GAPI.invalid, GAPI.invalidInput) as e:
|
|
entityActionFailedWarning([Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID]], str(e))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
# gam info instance [formatjson]
|
|
def doInfoInstance():
|
|
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
|
|
customerInfo = None if not FJQC.formatJSON else {}
|
|
doInfoCustomer(customerInfo, FJQC)
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(customerInfo), ensure_ascii=False, sort_keys=True))
|
|
|
|
DOMAIN_PRINT_ORDER = ['customerDomain', 'creationTime', 'isPrimary', 'verified']
|
|
DOMAIN_SKIP_OBJECTS = {'domainName', 'domainAliases'}
|
|
|
|
def _showDomain(result, FJQC, i=0, count=0):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(result, timeObjects=DOMAIN_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
skipObjects = DOMAIN_SKIP_OBJECTS
|
|
printEntity([Ent.DOMAIN, result['domainName']], i, count)
|
|
Ind.Increment()
|
|
if 'creationTime' in result:
|
|
result['creationTime'] = formatLocalTimestamp(result['creationTime'])
|
|
for field in DOMAIN_PRINT_ORDER:
|
|
if field in result:
|
|
printKeyValueList([field, result[field]])
|
|
skipObjects.add(field)
|
|
field = 'domainAliases'
|
|
aliases = result.get(field)
|
|
if aliases:
|
|
skipObjects.add(field)
|
|
aliasSkipObjects = DOMAIN_ALIAS_SKIP_OBJECTS
|
|
for alias in aliases:
|
|
_showDomainAlias(alias, FJQC, aliasSkipObjects)
|
|
showJSON(None, alias, aliasSkipObjects)
|
|
showJSON(None, result, skipObjects)
|
|
Ind.Decrement()
|
|
|
|
# gam info domain [<DomainName>] [formatjson]
|
|
def doInfoDomain():
|
|
if (not Cmd.ArgumentsRemaining()) or (Cmd.Current().lower() == 'formatjson'):
|
|
doInfoInstance()
|
|
return
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
domainName = getString(Cmd.OB_DOMAIN_NAME)
|
|
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
|
|
try:
|
|
result = callGAPI(cd.domains(), 'get',
|
|
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], domainName=domainName)
|
|
_showDomain(result, FJQC)
|
|
except GAPI.domainNotFound:
|
|
entityActionFailedWarning([Ent.DOMAIN, domainName], Msg.DOES_NOT_EXIST)
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
DOMAIN_SORT_TITLES = ['domainName', 'parentDomainName', 'creationTime', 'type', 'verified']
|
|
|
|
# gam print domains [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [showitemcountonly]
|
|
# gam show domains
|
|
# [formatjson]
|
|
# [showitemcountonly]
|
|
def doPrintShowDomains():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['domainName'], DOMAIN_SORT_TITLES) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
showItemCountOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
try:
|
|
domains = callGAPIitems(cd.domains(), 'list', 'domains',
|
|
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID])
|
|
count = len(domains)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{count}\n')
|
|
return
|
|
i = 0
|
|
for domain in domains:
|
|
i += 1
|
|
if not csvPF:
|
|
_showDomain(domain, FJQC, i, count)
|
|
elif not FJQC.formatJSON:
|
|
domain['type'] = 'primary' if domain.pop('isPrimary') else 'secondary'
|
|
domainAliases = domain.pop('domainAliases', [])
|
|
_printDomain(domain, csvPF)
|
|
for domainAlias in domainAliases:
|
|
domainAlias['type'] = 'alias'
|
|
domainAlias['domainName'] = domainAlias.pop('domainAliasName')
|
|
_printDomain(domainAlias, csvPF)
|
|
else:
|
|
csvPF.WriteRowNoFilter({'domainName': domain['domainName'],
|
|
'JSON': json.dumps(cleanJSON(domain, timeObjects=DOMAIN_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden, GAPI.domainNotFound) as e:
|
|
accessErrorExit(cd, str(e))
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Domains')
|
|
|
|
PRINT_PRIVILEGES_FIELDS = ['serviceId', 'serviceName', 'privilegeName', 'isOuScopable', 'childPrivileges']
|
|
|
|
def _listPrivileges(cd):
|
|
fields = f'items({",".join(PRINT_PRIVILEGES_FIELDS)})'
|
|
try:
|
|
return callGAPIitems(cd.privileges(), 'list', 'items',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
|
|
except (GAPI.badRequest, GAPI.customerNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
# gam print privileges [todrive <ToDriveAttribute>*]
|
|
# gam show privileges
|
|
def doPrintShowPrivileges():
|
|
def _showPrivilege(privilege, i, count):
|
|
printEntity([Ent.PRIVILEGE, privilege['privilegeName']], i, count)
|
|
Ind.Increment()
|
|
printKeyValueList(['serviceId', privilege['serviceId']])
|
|
printKeyValueList(['serviceName', privilege.get('serviceName', UNKNOWN)])
|
|
printKeyValueList(['isOuScopable', privilege['isOuScopable']])
|
|
jcount = len(privilege.get('childPrivileges', []))
|
|
if jcount > 0:
|
|
printKeyValueList(['childPrivileges', jcount])
|
|
Ind.Increment()
|
|
j = 0
|
|
for childPrivilege in privilege['childPrivileges']:
|
|
j += 1
|
|
_showPrivilege(childPrivilege, j, jcount)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(PRINT_PRIVILEGES_FIELDS, 'sortall') if Act.csvFormat() else None
|
|
getTodriveOnly(csvPF)
|
|
privileges = _listPrivileges(cd)
|
|
if not csvPF:
|
|
count = len(privileges)
|
|
performActionNumItems(count, Ent.PRIVILEGE)
|
|
Ind.Increment()
|
|
i = 0
|
|
for privilege in privileges:
|
|
i += 1
|
|
_showPrivilege(privilege, i, count)
|
|
Ind.Decrement()
|
|
else:
|
|
for privilege in privileges:
|
|
csvPF.WriteRowTitles(flattenJSON(privilege))
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Privileges')
|
|
|
|
def makeRoleIdNameMap():
|
|
GM.Globals[GM.MAKE_ROLE_ID_NAME_MAP] = False
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
try:
|
|
result = callGAPIpages(cd.roles(), 'list', 'items',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID],
|
|
fields='nextPageToken,items(roleId,roleName)',
|
|
maxResults=100)
|
|
except (GAPI.badRequest, GAPI.customerNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
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'].lower()] = role['roleId']
|
|
|
|
def role_from_roleid(roleid):
|
|
if GM.Globals[GM.MAKE_ROLE_ID_NAME_MAP]:
|
|
makeRoleIdNameMap()
|
|
return GM.Globals[GM.MAP_ROLE_ID_TO_NAME].get(roleid, roleid)
|
|
|
|
def roleid_from_role(role):
|
|
if GM.Globals[GM.MAKE_ROLE_ID_NAME_MAP]:
|
|
makeRoleIdNameMap()
|
|
return GM.Globals[GM.MAP_ROLE_NAME_TO_ID].get(role.lower(), None)
|
|
|
|
def getRoleId():
|
|
role = getString(Cmd.OB_ROLE_ITEM)
|
|
cg = UID_PATTERN.match(role)
|
|
if cg:
|
|
roleId = cg.group(1)
|
|
else:
|
|
roleId = roleid_from_role(role)
|
|
if not roleId:
|
|
invalidChoiceExit(role, GM.Globals[GM.MAP_ROLE_NAME_TO_ID], True)
|
|
return (role, roleId)
|
|
|
|
# gam create adminrole <String> privileges all|all_ou|<PrivilegesList> [description <String>]
|
|
# gam update adminrole <RoleItem> [name <String>] [privileges all|all_ou|<PrivilegesList>] [description <String>]
|
|
def doCreateUpdateAdminRoles():
|
|
def expandChildPrivileges(privilege):
|
|
for childPrivilege in privilege.get('childPrivileges', []):
|
|
childPrivileges[childPrivilege['privilegeName']] = childPrivilege['serviceId']
|
|
expandChildPrivileges(childPrivilege)
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
updateCmd = Act.Get() == Act.UPDATE
|
|
if not updateCmd:
|
|
body = {'roleName': getString(Cmd.OB_STRING)}
|
|
else:
|
|
body = {}
|
|
_, roleId = getRoleId()
|
|
allPrivileges = {}
|
|
ouPrivileges = {}
|
|
childPrivileges = {}
|
|
for privilege in _listPrivileges(cd):
|
|
allPrivileges[privilege['privilegeName']] = privilege['serviceId']
|
|
if privilege['isOuScopable']:
|
|
ouPrivileges[privilege['privilegeName']] = privilege['serviceId']
|
|
expandChildPrivileges(privilege)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'privileges':
|
|
privs = getString(Cmd.OB_PRIVILEGE_LIST).upper()
|
|
if privs == 'ALL':
|
|
body['rolePrivileges'] = [{'privilegeName': p, 'serviceId': v} for p, v in allPrivileges.items()]
|
|
elif privs == 'ALL_OU':
|
|
body['rolePrivileges'] = [{'privilegeName': p, 'serviceId': v} for p, v in ouPrivileges.items()]
|
|
else:
|
|
body.setdefault('rolePrivileges', [])
|
|
for p in privs.split(','):
|
|
if p in allPrivileges:
|
|
body['rolePrivileges'].append({'privilegeName': p, 'serviceId': allPrivileges[p]})
|
|
elif p in ouPrivileges:
|
|
body['rolePrivileges'].append({'privilegeName': p, 'serviceId': ouPrivileges[p]})
|
|
elif p in childPrivileges:
|
|
body['rolePrivileges'].append({'privilegeName': p, 'serviceId': childPrivileges[p]})
|
|
elif ':' in p:
|
|
priv, serv = p.split(':')
|
|
body['rolePrivileges'].append({'privilegeName': priv, 'serviceId': serv.lower()})
|
|
else:
|
|
invalidChoiceExit(p, list(allPrivileges.keys())+list(ouPrivileges.keys())+list(childPrivileges.keys()), True)
|
|
elif myarg == 'description':
|
|
body['roleDescription'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'name':
|
|
body['roleName'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not updateCmd and not body.get('rolePrivileges'):
|
|
missingArgumentExit('privileges')
|
|
try:
|
|
if not updateCmd:
|
|
result = callGAPI(cd.roles(), 'insert',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN]+[GAPI.DUPLICATE],
|
|
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='roleId,roleName')
|
|
else:
|
|
result = callGAPI(cd.roles(), 'patch',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN]+[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION],
|
|
customer=GC.Values[GC.CUSTOMER_ID], roleId=roleId, body=body, fields='roleId,roleName')
|
|
entityActionPerformed([Ent.ADMIN_ROLE, f"{result['roleName']}({result['roleId']})"])
|
|
except GAPI.duplicate as e:
|
|
entityActionFailedWarning([Ent.ADMIN_ROLE, f"{body['roleName']}"], str(e))
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.ADMIN_ROLE, roleId], str(e))
|
|
except (GAPI.badRequest, GAPI.customerNotFound):
|
|
accessErrorExit(cd)
|
|
|
|
# gam delete adminrole <RoleItem>
|
|
def doDeleteAdminRole():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
role, roleId = getRoleId()
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cd.roles(), 'delete',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN]+[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION],
|
|
customer=GC.Values[GC.CUSTOMER_ID], roleId=roleId)
|
|
entityActionPerformed([Ent.ADMIN_ROLE, f"{role}({roleId})"])
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.ADMIN_ROLE, roleId], str(e))
|
|
except (GAPI.badRequest, GAPI.customerNotFound):
|
|
accessErrorExit(cd)
|
|
|
|
PRINT_ADMIN_ROLES_FIELDS = ['roleId', 'roleName', 'roleDescription', 'isSuperAdminRole', 'isSystemRole']
|
|
|
|
def _showAdminRole(role, i=0, count=0):
|
|
printEntity([Ent.ADMIN_ROLE, role['roleName']], i, count)
|
|
Ind.Increment()
|
|
for field in PRINT_ADMIN_ROLES_FIELDS:
|
|
if field != 'roleName' and field in role:
|
|
printKeyValueList([field, role[field]])
|
|
jcount = len(role.get('rolePrivileges', []))
|
|
if jcount > 0:
|
|
printKeyValueList(['rolePrivileges', jcount])
|
|
Ind.Increment()
|
|
j = 0
|
|
for rolePrivilege in role['rolePrivileges']:
|
|
j += 1
|
|
printKeyValueList(['privilegeName', rolePrivilege['privilegeName']])
|
|
Ind.Increment()
|
|
printKeyValueList(['serviceId', rolePrivilege['serviceId']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam info adminrole <RoleItem> [privileges]
|
|
def doInfoAdminRole():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
fieldsList = PRINT_ADMIN_ROLES_FIELDS[:]
|
|
_, roleId = getRoleId()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'privileges':
|
|
fieldsList.append('rolePrivileges')
|
|
else:
|
|
unknownArgumentExit()
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
try:
|
|
role = callGAPI(cd.roles(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.FAILED_PRECONDITION,
|
|
GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND],
|
|
customer=GC.Values[GC.CUSTOMER_ID], roleId=roleId, fields=fields)
|
|
role.setdefault('isSuperAdminRole', False)
|
|
role.setdefault('isSystemRole', False)
|
|
_showAdminRole(role)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.ADMIN_ROLE, roleId], str(e))
|
|
except (GAPI.badRequest, GAPI.customerNotFound):
|
|
accessErrorExit(cd)
|
|
|
|
# gam print adminroles|roles [todrive <ToDriveAttribute>*]
|
|
# [privileges] [oneitemperrow]
|
|
# gam show adminroles|roles [privileges]
|
|
def doPrintShowAdminRoles():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
fieldsList = PRINT_ADMIN_ROLES_FIELDS[:]
|
|
csvPF = CSVPrintFile(fieldsList, PRINT_ADMIN_ROLES_FIELDS) if Act.csvFormat() else None
|
|
oneItemPerRow = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'privileges':
|
|
fieldsList.append('rolePrivileges')
|
|
elif myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if csvPF:
|
|
if not oneItemPerRow:
|
|
csvPF.AddTitles(['rolePrivileges'])
|
|
else:
|
|
csvPF.AddTitles(['privilegeName', 'serviceId'])
|
|
fields = getItemFieldsFromFieldsList('items', fieldsList)
|
|
printGettingAllAccountEntities(Ent.ADMIN_ROLE)
|
|
try:
|
|
roles = callGAPIpages(cd.roles(), 'list', 'items',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
|
|
except (GAPI.badRequest, GAPI.customerNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
for role in roles:
|
|
role.setdefault('isSuperAdminRole', False)
|
|
role.setdefault('isSystemRole', False)
|
|
if not csvPF:
|
|
count = len(roles)
|
|
performActionNumItems(count, Ent.ADMIN_ROLE)
|
|
Ind.Increment()
|
|
i = 0
|
|
for role in roles:
|
|
i += 1
|
|
_showAdminRole(role, i, count)
|
|
Ind.Decrement()
|
|
else:
|
|
for role in roles:
|
|
if not oneItemPerRow or 'rolePrivileges' not in role:
|
|
csvPF.WriteRowTitles(flattenJSON(role))
|
|
else:
|
|
privileges = role.pop('rolePrivileges')
|
|
baserow = flattenJSON(role)
|
|
for privilege in privileges:
|
|
row = flattenJSON(privilege, flattened=baserow.copy())
|
|
csvPF.WriteRowTitles(row)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Admin Roles')
|
|
|
|
ADMIN_SCOPE_TYPE_CHOICE_MAP = {'customer': 'CUSTOMER', 'orgunit': 'ORG_UNIT', 'org': 'ORG_UNIT', 'ou': 'ORG_UNIT'}
|
|
|
|
SECURITY_GROUP_CONDITION = "api.getAttribute('cloudidentity.googleapis.com/groups.labels', []).hasAny(['groups.security']) && resource.type == 'cloudidentity.googleapis.com/Group'"
|
|
NONSECURITY_GROUP_CONDITION = f'!{SECURITY_GROUP_CONDITION}'
|
|
ADMIN_CONDITION_CHOICE_MAP = {
|
|
'securitygroup': SECURITY_GROUP_CONDITION,
|
|
'nonsecuritygroup': NONSECURITY_GROUP_CONDITION,
|
|
}
|
|
|
|
# gam create admin <EmailAddress>|<UniqueID> <RoleItem> customer|(org_unit <OrgUnitItem>)
|
|
# [condition securitygroup|nonsecuritygroup]
|
|
def doCreateAdmin():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
user = getEmailAddress(returnUIDprefix='uid:')
|
|
body = {'assignedTo': convertEmailAddressToUID(user, cd, emailType='any')}
|
|
role, roleId = getRoleId()
|
|
body['roleId'] = roleId
|
|
body['scopeType'] = getChoice(ADMIN_SCOPE_TYPE_CHOICE_MAP, mapChoice=True)
|
|
if body['scopeType'] == 'ORG_UNIT':
|
|
orgUnit, orgUnitId = getOrgUnitId(cd)
|
|
body['orgUnitId'] = orgUnitId[3:]
|
|
scope = f'ORG_UNIT {orgUnit}'
|
|
else:
|
|
scope = 'CUSTOMER'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'condition':
|
|
body['condition'] = getChoice(ADMIN_CONDITION_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
unknownArgumentExit()
|
|
try:
|
|
result = callGAPI(cd.roleAssignments(), 'insert',
|
|
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND,
|
|
GAPI.FORBIDDEN, GAPI.CUSTOMER_EXCEEDED_ROLE_ASSIGNMENTS_LIMIT, GAPI.SERVICE_NOT_AVAILABLE,
|
|
GAPI.INVALID_ORGUNIT, GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='roleAssignmentId,assigneeType')
|
|
assigneeType = result.get('assigneeType')
|
|
if assigneeType == 'user':
|
|
entityType = Ent.USER
|
|
elif assigneeType == 'group':
|
|
entityType = Ent.GROUP
|
|
else:
|
|
entityType = Ent.ADMINISTRATOR
|
|
entityActionPerformedMessage([Ent.ADMIN_ROLE_ASSIGNMENT, result['roleAssignmentId']],
|
|
f'{Ent.Singular(entityType)} {user}, {Ent.Singular(Ent.ADMIN_ROLE)} {role}, {Ent.Singular(Ent.SCOPE)} {scope}')
|
|
except GAPI.internalError:
|
|
pass
|
|
except (GAPI.badRequest, GAPI.customerNotFound):
|
|
accessErrorExit(cd)
|
|
except (GAPI.forbidden, GAPI.customerExceededRoleAssignmentsLimit, GAPI.serviceNotAvailable, GAPI.conditionNotMet) as e:
|
|
entityActionFailedWarning([Ent.ADMINISTRATOR, user, Ent.ADMIN_ROLE, role], str(e))
|
|
except GAPI.invalidOrgunit:
|
|
entityActionFailedWarning([Ent.ADMINISTRATOR, user], Msg.INVALID_ORGUNIT)
|
|
except GAPI.duplicate:
|
|
entityActionFailedWarning([Ent.ADMINISTRATOR, user, Ent.ADMIN_ROLE, role], Msg.DUPLICATE)
|
|
|
|
# gam delete admin <RoleAssignmentId>
|
|
def doDeleteAdmin():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
roleAssignmentId = getString(Cmd.OB_ROLE_ASSIGNMENT_ID)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cd.roleAssignments(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.OPERATION_NOT_SUPPORTED, GAPI.FORBIDDEN,
|
|
GAPI.INVALID_INPUT, GAPI.SERVICE_NOT_AVAILABLE, GAPI.RESOURCE_NOT_FOUND,
|
|
GAPI.FAILED_PRECONDITION, GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=GC.Values[GC.CUSTOMER_ID], roleAssignmentId=roleAssignmentId)
|
|
entityActionPerformed([Ent.ADMIN_ROLE_ASSIGNMENT, roleAssignmentId])
|
|
except (GAPI.notFound, GAPI.operationNotSupported, GAPI.forbidden,
|
|
GAPI.invalidInput, GAPI.serviceNotAvailable, GAPI.resourceNotFound, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.ADMIN_ROLE_ASSIGNMENT, roleAssignmentId], str(e))
|
|
except (GAPI.badRequest, GAPI.customerNotFound):
|
|
accessErrorExit(cd)
|
|
|
|
ASSIGNEE_EMAILTYPE_TOFIELD_MAP = {
|
|
'user': 'assignedToUser',
|
|
'group': 'assignedToGroup',
|
|
'serviceaccount': 'assignedToServiceAccount',
|
|
}
|
|
PRINT_ADMIN_FIELDS = ['roleAssignmentId', 'roleId', 'assignedTo', 'scopeType', 'orgUnitId']
|
|
PRINT_ADMIN_TITLES = ['roleAssignmentId', 'roleId', 'role',
|
|
'assignedTo', 'assignedToUser', 'assignedToGroup', 'assignedToServiceAccount', 'assignedToUnknown',
|
|
'scopeType', 'orgUnitId', 'orgUnit']
|
|
|
|
# gam print admins [todrive <ToDriveAttribute>*]
|
|
# [user|group <EmailAddress>|<UniqueID>] [role <RoleItem>] [condition]
|
|
# [privileges] [oneitemperrow]
|
|
# gam show admins
|
|
# [user|group <EmailAddress>|<UniqueID>] [role <RoleItem>] [condition] [privileges]
|
|
def doPrintShowAdmins():
|
|
def _getPrivileges(admin):
|
|
if showPrivileges:
|
|
roleId = admin['roleId']
|
|
if roleId not in rolePrivileges:
|
|
try:
|
|
rolePrivileges[roleId] = callGAPI(cd.roles(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.FAILED_PRECONDITION,
|
|
GAPI.SERVICE_NOT_AVAILABLE, GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=GC.Values[GC.CUSTOMER_ID],
|
|
roleId=roleId,
|
|
fields='rolePrivileges')
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.failedPrecondition, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedExit([Ent.USER, userKey, Ent.ADMIN_ROLE, admin['roleId']], str(e))
|
|
rolePrivileges[roleId] = None
|
|
except (GAPI.badRequest, GAPI.customerNotFound):
|
|
accessErrorExit(cd)
|
|
return rolePrivileges[roleId]
|
|
|
|
def _setNamesFromIds(admin, privileges):
|
|
admin['role'] = role_from_roleid(admin['roleId'])
|
|
assignedTo = admin['assignedTo']
|
|
admin['assignedToUnknown'] = False
|
|
if assignedTo not in assignedToIdEmailMap:
|
|
assigneeType = admin.get('assigneeType')
|
|
assignedToField = ASSIGNEE_EMAILTYPE_TOFIELD_MAP.get(assigneeType, None)
|
|
assigneeEmail, assigneeType = convertUIDtoEmailAddressWithType(f'uid:{assignedTo}',
|
|
cd,
|
|
sal,
|
|
emailTypes=list(ASSIGNEE_EMAILTYPE_TOFIELD_MAP.keys()))
|
|
if not assignedToField and assigneeType in ASSIGNEE_EMAILTYPE_TOFIELD_MAP:
|
|
assignedToField = ASSIGNEE_EMAILTYPE_TOFIELD_MAP[assigneeType]
|
|
if assigneeType == 'unknown':
|
|
assignedToField = 'assignedToUnknown'
|
|
assigneeEmail = True
|
|
assignedToIdEmailMap[assignedTo] = {'assignedToField': assignedToField, 'assigneeEmail': assigneeEmail}
|
|
admin[assignedToIdEmailMap[assignedTo]['assignedToField']] = assignedToIdEmailMap[assignedTo]['assigneeEmail']
|
|
if privileges is not None:
|
|
admin.update(privileges)
|
|
if 'orgUnitId' in admin:
|
|
admin['orgUnit'] = convertOrgUnitIDtoPath(cd, f'id:{admin["orgUnitId"]}')
|
|
if 'condition' in admin:
|
|
if admin['condition'] == SECURITY_GROUP_CONDITION:
|
|
admin['condition'] = 'securitygroup'
|
|
elif admin['condition'] == NONSECURITY_GROUP_CONDITION:
|
|
admin['condition'] = 'nonsecuritygroup'
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
sal = buildGAPIObject(API.SERVICEACCOUNTLOOKUP)
|
|
csvPF = CSVPrintFile(PRINT_ADMIN_TITLES) if Act.csvFormat() else None
|
|
roleId = None
|
|
userKey = None
|
|
oneItemPerRow = showPrivileges = False
|
|
kwargs = {}
|
|
rolePrivileges = {}
|
|
fieldsList = PRINT_ADMIN_FIELDS
|
|
assignedToIdEmailMap = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'user', 'group'}:
|
|
userKey = kwargs['userKey'] = getEmailAddress()
|
|
elif myarg == 'role':
|
|
_, roleId = getRoleId()
|
|
elif myarg == 'condition':
|
|
fieldsList.append('condition')
|
|
if csvPF:
|
|
csvPF.AddTitle('condition')
|
|
elif myarg == 'privileges':
|
|
showPrivileges = True
|
|
elif myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if roleId and not kwargs:
|
|
kwargs['roleId'] = roleId
|
|
roleId = None
|
|
fields = getItemFieldsFromFieldsList('items', fieldsList)
|
|
printGettingAllAccountEntities(Ent.ADMIN_ROLE_ASSIGNMENT)
|
|
try:
|
|
admins = callGAPIpages(cd.roleAssignments(), 'list', 'items',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.INVALID, GAPI.USER_NOT_FOUND,
|
|
GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE,
|
|
GAPI.BAD_REQUEST, GAPI.CUSTOMER_NOT_FOUND],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=GC.Values[GC.CUSTOMER_ID], fields=fields, **kwargs)
|
|
except (GAPI.invalid, GAPI.userNotFound):
|
|
entityUnknownWarning(Ent.ADMINISTRATOR, userKey)
|
|
return
|
|
except (GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedExit([Ent.ADMINISTRATOR, userKey, Ent.ADMIN_ROLE, roleId], str(e))
|
|
except (GAPI.badRequest, GAPI.customerNotFound):
|
|
accessErrorExit(cd)
|
|
if not csvPF:
|
|
count = len(admins)
|
|
performActionNumItems(count, Ent.ADMIN_ROLE_ASSIGNMENT)
|
|
Ind.Increment()
|
|
i = 0
|
|
for admin in admins:
|
|
i += 1
|
|
if roleId and roleId != admin['roleId']:
|
|
continue
|
|
_setNamesFromIds(admin, _getPrivileges(admin))
|
|
printEntity([Ent.ADMIN_ROLE_ASSIGNMENT, admin['roleAssignmentId']], i, count)
|
|
Ind.Increment()
|
|
for field in PRINT_ADMIN_TITLES:
|
|
if field in admin:
|
|
if field == 'roleAssignmentId':
|
|
continue
|
|
if field != 'rolePrivileges':
|
|
printKeyValueList([field, admin[field]])
|
|
else:
|
|
showJSON(None, admin[field])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
for admin in admins:
|
|
if roleId and roleId != admin['roleId']:
|
|
continue
|
|
_setNamesFromIds(admin, _getPrivileges(admin))
|
|
if not oneItemPerRow or 'rolePrivileges' not in admin:
|
|
csvPF.WriteRowTitles(flattenJSON(admin))
|
|
else:
|
|
privileges = admin.pop('rolePrivileges')
|
|
baserow = flattenJSON(admin)
|
|
for privilege in privileges:
|
|
row = flattenJSON(privilege, flattened=baserow.copy())
|
|
csvPF.WriteRowTitles(row)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Admins')
|
|
|
|
def getTransferApplications(dt):
|
|
try:
|
|
return callGAPIpages(dt.applications(), 'list', 'applications',
|
|
throwReasons=[GAPI.UNKNOWN_ERROR, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], fields='applications(id,name,transferParams)')
|
|
except (GAPI.unknownError, GAPI.forbidden):
|
|
accessErrorExit(None)
|
|
|
|
def _convertTransferAppIDtoName(apps, appID):
|
|
for app in apps:
|
|
if appID == app['id']:
|
|
return app['name']
|
|
return f'applicationId: {appID}'
|
|
|
|
DRIVE_AND_DOCS_APP_NAME = 'drive and docs'
|
|
GOOGLE_DATA_STUDIO_APP_NAME = 'google data studio'
|
|
|
|
SERVICE_NAME_CHOICE_MAP = {
|
|
'datastudio': GOOGLE_DATA_STUDIO_APP_NAME,
|
|
'drive': DRIVE_AND_DOCS_APP_NAME,
|
|
'googledrive': DRIVE_AND_DOCS_APP_NAME,
|
|
'gdrive': DRIVE_AND_DOCS_APP_NAME,
|
|
'lookerstudio': GOOGLE_DATA_STUDIO_APP_NAME,
|
|
}
|
|
|
|
def _validateTransferAppName(apps, appName):
|
|
appName = appName.strip().lower()
|
|
appName = SERVICE_NAME_CHOICE_MAP.get(appName, appName)
|
|
appNameList = []
|
|
for app in apps:
|
|
if appName == app['name'].lower():
|
|
return (app['name'], app['id'])
|
|
appNameList.append(app['name'].lower())
|
|
invalidChoiceExit(appName, appNameList, True)
|
|
|
|
PRIVACY_LEVEL_CHOICE_MAP = {
|
|
'private': ['PRIVATE'],
|
|
'shared': ['SHARED'],
|
|
'all': ['PRIVATE', 'SHARED'],
|
|
}
|
|
|
|
# gam create datatransfer|transfer <OldOwnerID> <ServiceNameList> <NewOwnerID>
|
|
# [private|shared|all] [release_resources] (<ParameterKey> <ParameterValue>)*
|
|
# [wait <Integer> <Integer>]
|
|
def doCreateDataTransfer():
|
|
def _assignAppParameter(key, value, doubleBackup=False):
|
|
keyValid = False
|
|
for app in apps:
|
|
for params in app.get('transferParams', []):
|
|
if key == params['key']:
|
|
appIndex = appIndicies.get(app['id'])
|
|
if appIndex is not None:
|
|
body['applicationDataTransfers'][appIndex].setdefault('applicationTransferParams', [])
|
|
body['applicationDataTransfers'][appIndex]['applicationTransferParams'].append({'key': key, 'value': value})
|
|
keyValid = True
|
|
break
|
|
if not keyValid:
|
|
Cmd.Backup()
|
|
if doubleBackup:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.NO_DATA_TRANSFER_APP_FOR_PARAMETER.format(key))
|
|
|
|
dt = buildGAPIObject(API.DATATRANSFER)
|
|
apps = getTransferApplications(dt)
|
|
old_owner = getEmailAddress(returnUIDprefix='uid:')
|
|
body = {'oldOwnerUserId': convertEmailAddressToUID(old_owner)}
|
|
appIndicies = {}
|
|
appNameList = []
|
|
waitInterval = waitRetries = 0
|
|
i = 0
|
|
body['applicationDataTransfers'] = []
|
|
for appName in getString(Cmd.OB_SERVICE_NAME_LIST).split(','):
|
|
appName, appId = _validateTransferAppName(apps, appName)
|
|
body['applicationDataTransfers'].append({'applicationId': appId})
|
|
appIndicies[appId] = i
|
|
i += 1
|
|
appNameList.append(appName)
|
|
new_owner = getEmailAddress(returnUIDprefix='uid:')
|
|
body['newOwnerUserId'] = convertEmailAddressToUID(new_owner)
|
|
if body['oldOwnerUserId'] == body['newOwnerUserId']:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.NEW_OWNER_MUST_DIFFER_FROM_OLD_OWNER)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in PRIVACY_LEVEL_CHOICE_MAP:
|
|
_assignAppParameter('PRIVACY_LEVEL', PRIVACY_LEVEL_CHOICE_MAP[myarg])
|
|
elif myarg == 'releaseresources':
|
|
if getBoolean():
|
|
_assignAppParameter('RELEASE_RESOURCES', ['TRUE'])
|
|
elif myarg == 'wait':
|
|
waitInterval = getInteger(minVal=5, maxVal=60)
|
|
waitRetries = getInteger(minVal=0)
|
|
else:
|
|
_assignAppParameter(Cmd.Previous().upper(), getString(Cmd.OB_PARAMETER_VALUE).upper().split(','), True)
|
|
try:
|
|
result = callGAPI(dt.transfers(), 'insert',
|
|
throwReasons=[GAPI.UNKNOWN_ERROR, GAPI.FORBIDDEN],
|
|
body=body, fields='id')
|
|
except (GAPI.unknownError, GAPI.forbidden) as e:
|
|
entityActionFailedExit([Ent.USER, old_owner], str(e))
|
|
entityActionPerformed([Ent.TRANSFER_REQUEST, None])
|
|
Ind.Increment()
|
|
printEntity([Ent.TRANSFER_ID, result['id']])
|
|
printEntity([Ent.SERVICE, ','.join(appNameList)])
|
|
printKeyValueList([Msg.FROM, old_owner])
|
|
printKeyValueList([Msg.TO, new_owner])
|
|
Ind.Decrement()
|
|
if waitRetries == 0:
|
|
return
|
|
retry = 0
|
|
status = 'inProgress'
|
|
dtId = result['id']
|
|
while True:
|
|
writeStderr(Ind.Spaces()+Msg.WAITING_FOR_DATA_TRANSFER_TO_COMPLETE_SLEEPING.format(waitInterval))
|
|
time.sleep(waitInterval)
|
|
try:
|
|
result = callGAPI(dt.transfers(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND],
|
|
dataTransferId=dtId, fields='overallTransferStatusCode')
|
|
if result['overallTransferStatusCode'] == 'completed':
|
|
status = result['overallTransferStatusCode']
|
|
break
|
|
retry += 1
|
|
if retry >= waitRetries:
|
|
break
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([Ent.TRANSFER_ID, dtId], Msg.DOES_NOT_EXIST)
|
|
break
|
|
printEntity([Ent.TRANSFER_ID, dtId, Ent.STATUS, status])
|
|
|
|
def _showTransfer(apps, transfer, i, count):
|
|
printEntity([Ent.TRANSFER_ID, transfer['id']], i, count)
|
|
Ind.Increment()
|
|
printKeyValueList(['Request Time', formatLocalTime(transfer['requestTime'])])
|
|
printKeyValueList(['Old Owner', convertUserIDtoEmail(transfer['oldOwnerUserId'])])
|
|
printKeyValueList(['New Owner', convertUserIDtoEmail(transfer['newOwnerUserId'])])
|
|
printKeyValueList(['Overall Transfer Status', transfer['overallTransferStatusCode']])
|
|
for app in transfer['applicationDataTransfers']:
|
|
printKeyValueList(['Application', _convertTransferAppIDtoName(apps, app['applicationId'])])
|
|
Ind.Increment()
|
|
printKeyValueList(['Status', app['applicationTransferStatus']])
|
|
printKeyValueList(['Parameters'])
|
|
Ind.Increment()
|
|
if 'applicationTransferParams' in app:
|
|
for param in app['applicationTransferParams']:
|
|
key = param['key']
|
|
value = param.get('value', [])
|
|
if value:
|
|
printKeyValueList([key, ','.join(value)])
|
|
else:
|
|
printKeyValueList([key])
|
|
else:
|
|
printKeyValueList(['None'])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam info datatransfer|transfer <TransferID>
|
|
def doInfoDataTransfer():
|
|
dt = buildGAPIObject(API.DATATRANSFER)
|
|
apps = getTransferApplications(dt)
|
|
dtId = getString(Cmd.OB_TRANSFER_ID)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
transfer = callGAPI(dt.transfers(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND],
|
|
dataTransferId=dtId)
|
|
_showTransfer(apps, transfer, 0, 0)
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([Ent.TRANSFER_ID, dtId], Msg.DOES_NOT_EXIST)
|
|
|
|
DATA_TRANSFER_STATUS_MAP = {
|
|
'completed': 'completed',
|
|
'failed': 'failed',
|
|
# 'pending': 'pending',
|
|
'inprogress': 'inProgress',
|
|
}
|
|
DATA_TRANSFER_SORT_TITLES = ['id', 'requestTime', 'oldOwnerUserEmail', 'newOwnerUserEmail',
|
|
'overallTransferStatusCode', 'application', 'applicationId', 'status']
|
|
|
|
# gam print datatransfers|transfers [todrive <ToDriveAttribute>*]
|
|
# [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>]
|
|
# [status <String>] [delimiter <Character>]]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# gam show datatransfers|transfers
|
|
# [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>]
|
|
# [status <String>] [delimiter <Character>]]
|
|
def doPrintShowDataTransfers():
|
|
dt = buildGAPIObject(API.DATATRANSFER)
|
|
apps = getTransferApplications(dt)
|
|
newOwnerUserId = None
|
|
oldOwnerUserId = None
|
|
status = None
|
|
csvPF = CSVPrintFile(['id'], DATA_TRANSFER_SORT_TITLES) if Act.csvFormat() else None
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'olduser', 'oldowner'}:
|
|
oldOwnerUserId = convertEmailAddressToUID(getEmailAddress(returnUIDprefix='uid:'))
|
|
elif myarg in {'newuser', 'newowner'}:
|
|
newOwnerUserId = convertEmailAddressToUID(getEmailAddress(returnUIDprefix='uid:'))
|
|
elif myarg == 'status':
|
|
status = getChoice(DATA_TRANSFER_STATUS_MAP, mapChoice=True)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
try:
|
|
transfers = callGAPIpages(dt.transfers(), 'list', 'dataTransfers',
|
|
throwReasons=[GAPI.UNKNOWN_ERROR, GAPI.FORBIDDEN, GAPI.INVALID_INPUT],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], status=status,
|
|
newOwnerUserId=newOwnerUserId, oldOwnerUserId=oldOwnerUserId)
|
|
except (GAPI.unknownError, GAPI.forbidden, GAPI.invalidInput) as e:
|
|
entityActionFailedExit([Ent.TRANSFER_REQUEST, None], str(e))
|
|
if not csvPF:
|
|
count = len(transfers)
|
|
performActionNumItems(count, Ent.TRANSFER_REQUEST)
|
|
Ind.Increment()
|
|
i = 0
|
|
for transfer in sorted(transfers, key=lambda k: k['requestTime']):
|
|
i += 1
|
|
_showTransfer(apps, transfer, i, count)
|
|
Ind.Decrement()
|
|
else:
|
|
for transfer in sorted(transfers, key=lambda k: k['requestTime']):
|
|
row = {}
|
|
row['id'] = transfer['id']
|
|
row['requestTime'] = formatLocalTime(transfer['requestTime'])
|
|
row['oldOwnerUserEmail'] = convertUserIDtoEmail(transfer['oldOwnerUserId'])
|
|
row['newOwnerUserEmail'] = convertUserIDtoEmail(transfer['newOwnerUserId'])
|
|
row['overallTransferStatusCode'] = transfer['overallTransferStatusCode']
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
for app in transfer['applicationDataTransfers']:
|
|
xrow = row.copy()
|
|
xrow['application'] = _convertTransferAppIDtoName(apps, app['applicationId'])
|
|
xrow['applicationId'] = app['applicationId']
|
|
xrow['status'] = app['applicationTransferStatus']
|
|
for param in app.get('applicationTransferParams', []):
|
|
key = param['key']
|
|
xrow[key] = delimiter.join(param.get('value', [] if key != 'RELEASE_RESOURCES' else ['TRUE']))
|
|
csvPF.WriteRowTitles(xrow)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Data Transfers')
|
|
|
|
# gam show transferapps
|
|
def doShowTransferApps():
|
|
dt = buildGAPIObject(API.DATATRANSFER)
|
|
checkForExtraneousArguments()
|
|
Act.Set(Act.SHOW)
|
|
try:
|
|
apps = callGAPIpages(dt.applications(), 'list', 'applications',
|
|
throwReasons=[GAPI.UNKNOWN_ERROR, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], fields='applications(id,name,transferParams)')
|
|
except (GAPI.unknownError, GAPI.forbidden):
|
|
accessErrorExit(None)
|
|
count = len(apps)
|
|
performActionNumItems(count, Ent.TRANSFER_APPLICATION)
|
|
Ind.Increment()
|
|
i = 0
|
|
for app in apps:
|
|
i += 1
|
|
printKeyValueListWithCount([app['name']], i, count)
|
|
Ind.Increment()
|
|
printKeyValueList(['id', app['id']])
|
|
transferParams = app.get('transferParams', [])
|
|
if transferParams:
|
|
printKeyValueList(['Parameters'])
|
|
Ind.Increment()
|
|
for param in transferParams:
|
|
printKeyValueList(['key', param['key']])
|
|
Ind.Increment()
|
|
printKeyValueList(['value', ','.join(param['value'])])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
def _getOrgInheritance(myarg, body):
|
|
if myarg == 'noinherit':
|
|
Cmd.Backup()
|
|
deprecatedArgumentExit(myarg)
|
|
elif myarg == 'inherit':
|
|
body['blockInheritance'] = False
|
|
elif myarg in {'blockinheritance', 'inheritanceblocked'}:
|
|
location = Cmd.Location()-1
|
|
if getBoolean():
|
|
Cmd.SetLocation(location)
|
|
deprecatedArgumentExit(myarg)
|
|
body['blockInheritance'] = False
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
# gam create org|ou <String> [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)] [buildpath]
|
|
def doCreateOrg():
|
|
|
|
def _createOrg(body, parentPath, fullPath):
|
|
try:
|
|
callGAPI(cd.orgunits(), 'insert',
|
|
throwReasons=[GAPI.INVALID_PARENT_ORGUNIT, GAPI.INVALID_ORGUNIT, GAPI.BACKEND_ERROR, GAPI.BAD_REQUEST, GAPI.INVALID_CUSTOMER_ID, GAPI.LOGIN_REQUIRED],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
|
|
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, fullPath])
|
|
except GAPI.invalidParentOrgunit:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, fullPath, Ent.PARENT_ORGANIZATIONAL_UNIT, parentPath], Msg.ENTITY_DOES_NOT_EXIST.format(Ent.Singular(Ent.PARENT_ORGANIZATIONAL_UNIT)))
|
|
return False
|
|
except (GAPI.invalidOrgunit, GAPI.backendError):
|
|
entityDuplicateWarning([Ent.ORGANIZATIONAL_UNIT, fullPath])
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, fullPath)
|
|
return True
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
name = getOrgUnitItem(pathOnly=True, absolutePath=False)
|
|
parent = ''
|
|
body = {}
|
|
buildPath = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'description':
|
|
body['description'] = getStringWithCRsNLs()
|
|
elif myarg == 'parent':
|
|
parent = getOrgUnitItem()
|
|
elif _getOrgInheritance(myarg, body):
|
|
pass
|
|
elif myarg == 'buildpath':
|
|
buildPath = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if parent.startswith('id:'):
|
|
parentPath = None
|
|
try:
|
|
parentPath = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=parent, fields='orgUnitPath')['orgUnitPath']
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
pass
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
errMsg = accessErrorMessage(cd)
|
|
if errMsg:
|
|
systemErrorExit(INVALID_DOMAIN_RC, errMsg)
|
|
if not parentPath and not buildPath:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, name, Ent.PARENT_ORGANIZATIONAL_UNIT, parent], Msg.ENTITY_DOES_NOT_EXIST.format(Ent.Singular(Ent.PARENT_ORGANIZATIONAL_UNIT)))
|
|
return
|
|
parent = parentPath
|
|
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']
|
|
if _createOrg(body, parent, orgUnitPath) or not buildPath:
|
|
return
|
|
description = body.pop('description', None)
|
|
fullPath = '/'
|
|
getPath = ''
|
|
orgNames = orgUnitPath.split('/')
|
|
n = len(orgNames)-1
|
|
for i in range(1, n+1):
|
|
body['parentOrgUnitPath'] = fullPath
|
|
if fullPath != '/':
|
|
fullPath += '/'
|
|
fullPath += orgNames[i]
|
|
if getPath != '':
|
|
getPath += '/'
|
|
getPath += orgNames[i]
|
|
try:
|
|
callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(getPath), fields='')
|
|
printKeyValueList([Ent.Singular(Ent.ORGANIZATIONAL_UNIT), fullPath, Msg.EXISTS])
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
body['name'] = orgNames[i]
|
|
if i == n and description:
|
|
body['description'] = description
|
|
if not _createOrg(body, body['parentOrgUnitPath'], fullPath):
|
|
return
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, fullPath)
|
|
|
|
def checkOrgUnitPathExists(cd, orgUnitPath, i=0, count=0, showError=False):
|
|
if orgUnitPath == '/':
|
|
_, orgUnitId = getOrgUnitId(cd, orgUnitPath)
|
|
return (True, orgUnitPath, orgUnitId)
|
|
try:
|
|
orgUnit = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnitPath)),
|
|
fields='orgUnitPath,orgUnitId')
|
|
return (True, orgUnit['orgUnitPath'], orgUnit['orgUnitId'])
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
pass
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
errMsg = accessErrorMessage(cd)
|
|
if errMsg:
|
|
systemErrorExit(INVALID_DOMAIN_RC, errMsg)
|
|
if showError:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST, i, count)
|
|
return (False, orgUnitPath, orgUnitPath)
|
|
|
|
def _batchMoveCrOSesToOrgUnit(cd, orgUnitPath, orgUnitId, i, count, items, quickCrOSMove, fromOrgUnitPath=None):
|
|
def _callbackMoveCrOSesToOrgUnit(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
if not fromOrgUnitPath:
|
|
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
entityModifierActionPerformed([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM]], toOrgUnitPath, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason in [GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN]:
|
|
checkEntityItemValueAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
errMsg = getHTTPError({}, http_status, reason, message)
|
|
if not fromOrgUnitPath:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
entityModifierActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath, Ent.CROS_DEVICE, ri[RI_ITEM]], toOrgUnitPath, errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
jcount = len(items)
|
|
if not fromOrgUnitPath:
|
|
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.CROS_DEVICE, i, count)
|
|
else:
|
|
toOrgUnitPath = f'{Act.MODIFIER_TO} {Ent.Singular(Ent.ORGANIZATIONAL_UNIT)}: {orgUnitPath}'
|
|
entityPerformActionNumItemsModifier([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath], jcount, Ent.CROS_DEVICE, toOrgUnitPath, i, count)
|
|
Ind.Increment()
|
|
if not quickCrOSMove:
|
|
svcargs = dict([('customerId', GC.Values[GC.CUSTOMER_ID]),
|
|
('deviceId', None),
|
|
('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
if not GC.Values[GC.UPDATE_CROS_OU_WITH_ID]:
|
|
svcargs['body'] = {'orgUnitPath': orgUnitPath}
|
|
method = getattr(cd.chromeosdevices(), 'update')
|
|
else:
|
|
svcargs['body'] = {'orgUnitPath': orgUnitPath, 'orgUnitId': orgUnitId}
|
|
method = getattr(cd.chromeosdevices(), 'patch')
|
|
dbatch = cd.new_batch_http_request(callback=_callbackMoveCrOSesToOrgUnit)
|
|
bcount = 0
|
|
j = 0
|
|
for deviceId in items:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['deviceId'] = deviceId
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, deviceId))
|
|
bcount += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = cd.new_batch_http_request(callback=_callbackMoveCrOSesToOrgUnit)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
else:
|
|
bcount = 0
|
|
j = 0
|
|
while bcount < jcount:
|
|
kcount = min(jcount-bcount, GC.Values[GC.BATCH_SIZE])
|
|
try:
|
|
deviceIds = items[bcount:bcount+kcount]
|
|
callGAPI(cd.chromeosdevices(), 'moveDevicesToOu',
|
|
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnitPath,
|
|
body={'deviceIds': deviceIds})
|
|
for deviceId in deviceIds:
|
|
j += 1
|
|
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, deviceId], j, jcount)
|
|
bcount += kcount
|
|
except GAPI.invalidOrgunit:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.INVALID_ORGUNIT, i, count)
|
|
break
|
|
except GAPI.invalidInput as e:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, None], str(e), i, count)
|
|
break
|
|
except GAPI.resourceNotFound as e:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, ','.join(deviceIds)], str(e), i, count)
|
|
break
|
|
except (GAPI.badRequest, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath, i, count)
|
|
bcount += kcount
|
|
Ind.Decrement()
|
|
|
|
def _batchMoveUsersToOrgUnit(cd, orgUnitPath, i, count, items, fromOrgUnitPath=None):
|
|
_MOVE_USER_REASON_TO_MESSAGE_MAP = {GAPI.USER_NOT_FOUND: Msg.DOES_NOT_EXIST, GAPI.DOMAIN_NOT_FOUND: Msg.SERVICE_NOT_APPLICABLE, GAPI.FORBIDDEN: Msg.SERVICE_NOT_APPLICABLE}
|
|
def _callbackMoveUsersToOrgUnit(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
if not fromOrgUnitPath:
|
|
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.USER, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
entityModifierActionPerformed([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath, Ent.USER, ri[RI_ITEM]], toOrgUnitPath, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
errMsg = getHTTPError(_MOVE_USER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
|
|
if not fromOrgUnitPath:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.USER, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
entityModifierActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath, Ent.USER, ri[RI_ITEM]], toOrgUnitPath, errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
jcount = len(items)
|
|
if not fromOrgUnitPath:
|
|
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.USER, i, count)
|
|
else:
|
|
toOrgUnitPath = f'{Act.MODIFIER_TO} {Ent.Singular(Ent.ORGANIZATIONAL_UNIT)}: {orgUnitPath}'
|
|
entityPerformActionNumItemsModifier([Ent.ORGANIZATIONAL_UNIT, fromOrgUnitPath], jcount, Ent.USER, toOrgUnitPath, i, count)
|
|
Ind.Increment()
|
|
svcargs = dict([('userKey', None), ('body', {'orgUnitPath': orgUnitPath}), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(cd.users(), 'update')
|
|
dbatch = cd.new_batch_http_request(callback=_callbackMoveUsersToOrgUnit)
|
|
bcount = 0
|
|
j = 0
|
|
for user in items:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['userKey'] = normalizeEmailAddressOrUID(user)
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, svcparms['userKey']))
|
|
bcount += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = cd.new_batch_http_request(callback=_callbackMoveUsersToOrgUnit)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
Ind.Decrement()
|
|
|
|
def _doUpdateOrgs(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
if checkArgumentPresent(['move', 'add']):
|
|
entityType, items = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS, crosAllowed=True)
|
|
orgItemLists = items if isinstance(items, dict) else None
|
|
quickCrOSMove = GC.Values[GC.QUICK_CROS_MOVE]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if entityType == Cmd.ENTITY_CROS and myarg == 'quickcrosmove':
|
|
quickCrOSMove = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
Act.Set(Act.ADD)
|
|
i = 0
|
|
count = len(entityList)
|
|
for orgUnitPath in entityList:
|
|
i += 1
|
|
if orgItemLists:
|
|
items = orgItemLists[orgUnitPath]
|
|
status, orgUnitPath, orgUnitId = checkOrgUnitPathExists(cd, orgUnitPath, i, count, True)
|
|
if not status:
|
|
continue
|
|
if entityType == Cmd.ENTITY_USERS:
|
|
_batchMoveUsersToOrgUnit(cd, orgUnitPath, i, count, items)
|
|
else:
|
|
_batchMoveCrOSesToOrgUnit(cd, orgUnitPath, orgUnitId, i, count, items, quickCrOSMove)
|
|
elif checkArgumentPresent(['sync']):
|
|
entityType, syncMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS, crosAllowed=True)
|
|
cmdEntityType = Cmd.ENTITY_OU if entityType == Cmd.ENTITY_USERS else Cmd.ENTITY_CROS_OU
|
|
orgItemLists = syncMembers if isinstance(syncMembers, dict) else None
|
|
if orgItemLists is None:
|
|
syncMembersSet = set(syncMembers)
|
|
removeToOrgUnitPath = '/'
|
|
removeToOrgUnitId = None
|
|
quickCrOSMove = GC.Values[GC.QUICK_CROS_MOVE]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if entityType == Cmd.ENTITY_CROS and myarg == 'quickcrosmove':
|
|
quickCrOSMove = getBoolean()
|
|
elif myarg == 'removetoou':
|
|
status, removeToOrgUnitPath, removeToOrgUnitId = checkOrgUnitPathExists(cd, getOrgUnitItem())
|
|
if not status:
|
|
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, removeToOrgUnitPath)
|
|
else:
|
|
unknownArgumentExit()
|
|
if entityType == Cmd.ENTITY_CROS and not removeToOrgUnitId:
|
|
_, removeToOrgUnitPath, removeToOrgUnitId = checkOrgUnitPathExists(cd, removeToOrgUnitPath)
|
|
i = 0
|
|
count = len(entityList)
|
|
for orgUnitPath in entityList:
|
|
i += 1
|
|
if orgItemLists:
|
|
syncMembersSet = set(orgItemLists[orgUnitPath])
|
|
status, orgUnitPath, orgUnitId = checkOrgUnitPathExists(cd, orgUnitPath, i, count, True)
|
|
if not status:
|
|
continue
|
|
currentMembersSet = set(getItemsToModify(cmdEntityType, orgUnitPath))
|
|
if entityType == Cmd.ENTITY_USERS:
|
|
Act.Set(Act.ADD)
|
|
_batchMoveUsersToOrgUnit(cd, orgUnitPath, i, count, list(syncMembersSet-currentMembersSet))
|
|
Act.Set(Act.REMOVE)
|
|
_batchMoveUsersToOrgUnit(cd, removeToOrgUnitPath, i, count, list(currentMembersSet-syncMembersSet), orgUnitPath)
|
|
else:
|
|
Act.Set(Act.ADD)
|
|
_batchMoveCrOSesToOrgUnit(cd, orgUnitPath, orgUnitId, i, count, list(syncMembersSet-currentMembersSet), quickCrOSMove)
|
|
Act.Set(Act.REMOVE)
|
|
_batchMoveCrOSesToOrgUnit(cd, removeToOrgUnitPath, removeToOrgUnitId, i, count, list(currentMembersSet-syncMembersSet), quickCrOSMove, orgUnitPath)
|
|
else:
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
body['name'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'description':
|
|
body['description'] = getStringWithCRsNLs()
|
|
elif myarg == 'parent':
|
|
parent = getOrgUnitItem()
|
|
if parent.startswith('id:'):
|
|
body['parentOrgUnitId'] = parent
|
|
else:
|
|
body['parentOrgUnitPath'] = parent
|
|
elif _getOrgInheritance(myarg, body):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
i = 0
|
|
count = len(entityList)
|
|
for orgUnitPath in entityList:
|
|
i += 1
|
|
try:
|
|
callGAPI(cd.orgunits(), 'update',
|
|
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND, GAPI.BACKEND_ERROR, GAPI.INVALID_ORGUNIT_NAME,
|
|
GAPI.CONDITION_NOT_MET, GAPI.BAD_REQUEST, GAPI.INVALID_CUSTOMER_ID, GAPI.LOGIN_REQUIRED],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnitPath)), body=body, fields='')
|
|
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], i, count)
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST, i, count)
|
|
except GAPI.invalidOrgunitName as e:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.NAME, body['name']], str(e), i, count)
|
|
except GAPI.conditionNotMet as e:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], str(e), i, count)
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
|
|
|
|
# gam update orgs|ous <OrgUnitEntity> [name <String>] [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)]
|
|
# gam update orgs|ous <OrgUnitEntity> add|move <CrosTypeEntity> [quickcrosmove [<Boolean>]]
|
|
# gam update orgs|ous <OrgUnitEntity> add|move <UserTypeEntity>
|
|
# gam update orgs|ous <OrgUnitEntity> sync <CrosTypeEntity> [removetoou <OrgUnitItem>] [quickcrosmove [<Boolean>]]
|
|
# gam update orgs|ous <OrgUnitEntity> sync <UserTypeEntity> [removetoou <OrgUnitItem>]
|
|
def doUpdateOrgs():
|
|
_doUpdateOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
|
|
|
|
# gam update org|ou <OrgUnitItem> [name <String>] [description <String>] [parent <OrgUnitItem>] [inherit|(blockinheritance False)]
|
|
# gam update org|ou <OrgUnitItem> add|move <CrosTypeEntity> [quickcrosmove [<Boolean>]]
|
|
# gam update org|ou <OrgUnitItem> add|move <UserTypeEntity>
|
|
# gam update org|ou <OrgUnitItem> sync <CrosTypeEntity> [removetoou <OrgUnitItem>] [quickcrosmove [<Boolean>]]
|
|
# gam update org|ou <OrgUnitItem> sync <UserTypeEntity> [removetoou <OrgUnitItem>]
|
|
def doUpdateOrg():
|
|
_doUpdateOrgs([getOrgUnitItem()])
|
|
|
|
def _doDeleteOrgs(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for orgUnitPath in entityList:
|
|
i += 1
|
|
try:
|
|
orgUnitPath = makeOrgUnitPathAbsolute(orgUnitPath)
|
|
callGAPI(cd.orgunits(), 'delete',
|
|
throwReasons=[GAPI.CONDITION_NOT_MET, GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND, GAPI.BACKEND_ERROR,
|
|
GAPI.INVALID_CUSTOMER_ID, GAPI.SERVICE_NOT_AVAILABLE,
|
|
GAPI.BAD_REQUEST, GAPI.LOGIN_REQUIRED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(makeOrgUnitPathRelative(orgUnitPath)))
|
|
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], i, count)
|
|
except GAPI.conditionNotMet:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.HAS_CHILD_ORGS.format(Ent.Plural(Ent.ORGANIZATIONAL_UNIT)), i, count)
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.invalidCustomerId, GAPI.serviceNotAvailable) as e:
|
|
### Check for my_customer
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CUSTOMER_ID, GC.Values[GC.CUSTOMER_ID]], str(e), i, count)
|
|
except (GAPI.badRequest, GAPI.loginRequired):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
|
|
|
|
# gam delete orgs|ous <OrgUnitEntity>
|
|
def doDeleteOrgs():
|
|
_doDeleteOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
|
|
|
|
# gam delete org|ou <OrgUnitItem>
|
|
def doDeleteOrg():
|
|
_doDeleteOrgs([getOrgUnitItem()])
|
|
|
|
ORG_FIELD_INFO_ORDER = ['orgUnitId', 'name', 'description', 'parentOrgUnitPath', 'parentOrgUnitId', 'blockInheritance']
|
|
ORG_FIELDS_WITH_CRS_NLS = {'description'}
|
|
|
|
def _doInfoOrgs(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
getUsers = True
|
|
isSuspended = None
|
|
entityType = Ent.USER
|
|
showChildren = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'nousers':
|
|
getUsers = False
|
|
elif myarg in SUSPENDED_ARGUMENTS:
|
|
isSuspended = _getIsSuspended(myarg)
|
|
entityType = Ent.USER_SUSPENDED if isSuspended else Ent.USER_NOT_SUSPENDED
|
|
elif myarg in {'children', 'child'}:
|
|
showChildren = True
|
|
else:
|
|
unknownArgumentExit()
|
|
i = 0
|
|
count = len(entityList)
|
|
for origOrgUnitPath in entityList:
|
|
i += 1
|
|
try:
|
|
if origOrgUnitPath == '/':
|
|
_, orgUnitPath = getOrgUnitId(cd, origOrgUnitPath)
|
|
else:
|
|
orgUnitPath = makeOrgUnitPathRelative(origOrgUnitPath)
|
|
result = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=encodeOrgUnitPath(orgUnitPath))
|
|
if 'orgUnitPath' not in result:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, origOrgUnitPath], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
printEntity([Ent.ORGANIZATIONAL_UNIT, result['orgUnitPath']], i, count)
|
|
Ind.Increment()
|
|
for field in ORG_FIELD_INFO_ORDER:
|
|
value = result.get(field, None)
|
|
if value is not None:
|
|
if field not in ORG_FIELDS_WITH_CRS_NLS:
|
|
printKeyValueList([field, value])
|
|
else:
|
|
printKeyValueWithCRsNLs(field, value)
|
|
if getUsers:
|
|
orgUnitPath = result['orgUnitPath']
|
|
users = callGAPIpages(cd.users(), 'list', 'users',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, isSuspended), orderBy='email',
|
|
fields='nextPageToken,users(primaryEmail,orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
|
|
printEntitiesCount(entityType, None)
|
|
usersInOU = 0
|
|
Ind.Increment()
|
|
orgUnitPath = orgUnitPath.lower()
|
|
for user in users:
|
|
if orgUnitPath == user['orgUnitPath'].lower():
|
|
printKeyValueList([user['primaryEmail']])
|
|
usersInOU += 1
|
|
elif showChildren:
|
|
printKeyValueList([f'{user["primaryEmail"]} (child)'])
|
|
usersInOU += 1
|
|
Ind.Decrement()
|
|
printKeyValueList([Msg.TOTAL_ITEMS_IN_ENTITY.format(Ent.Plural(entityType), Ent.Singular(Ent.ORGANIZATIONAL_UNIT)), usersInOU])
|
|
Ind.Decrement()
|
|
except (GAPI.invalidInput, GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
|
|
|
|
# gam info orgs|ous <OrgUnitEntity> [nousers|notsuspended|suspended] [children|child]
|
|
def doInfoOrgs():
|
|
_doInfoOrgs(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
|
|
|
|
# gam info org|ou <OrgUnitItem> [nousers|notsuspended|suspended] [children|child]
|
|
def doInfoOrg():
|
|
_doInfoOrgs([getOrgUnitItem()])
|
|
|
|
ORG_ARGUMENT_TO_FIELD_MAP = {
|
|
'blockinheritance': 'blockInheritance',
|
|
'inheritanceblocked': 'blockInheritance',
|
|
'inherit': 'blockInheritance',
|
|
'description': 'description',
|
|
'id': 'orgUnitId',
|
|
'name': 'name',
|
|
'orgunitid': 'orgUnitId',
|
|
'orgunitpath': 'orgUnitPath',
|
|
'path': 'orgUnitPath',
|
|
'parentorgunitid': 'parentOrgUnitId',
|
|
'parentid': 'parentOrgUnitId',
|
|
'parentorgunitpath': 'parentOrgUnitPath',
|
|
'parent': 'parentOrgUnitPath',
|
|
}
|
|
ORG_FIELD_PRINT_ORDER = ['orgUnitPath', 'orgUnitId', 'name', 'description', 'parentOrgUnitPath', 'parentOrgUnitId', 'blockInheritance']
|
|
PRINT_ORGS_DEFAULT_FIELDS = ['orgUnitPath', 'orgUnitId', 'name', 'parentOrgUnitId']
|
|
|
|
ORG_UNIT_SELECTOR_FIELD = 'orgUnitSelector'
|
|
PRINT_OUS_SELECTOR_CHOICES = [
|
|
Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN,
|
|
Cmd.ENTITY_OU, Cmd.ENTITY_OU_NS, Cmd.ENTITY_OU_SUSP,
|
|
Cmd.ENTITY_OU_AND_CHILDREN, Cmd.ENTITY_OU_AND_CHILDREN_NS, Cmd.ENTITY_OU_AND_CHILDREN_SUSP,
|
|
]
|
|
|
|
def _getOrgUnits(cd, orgUnitPath, fieldsList, listType, showParent, batchSubOrgs, childSelector=None, parentSelector=None):
|
|
def _callbackListOrgUnits(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
orgUnits.extend(response.get('organizationUnits', []))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
errMsg = getHTTPError({}, http_status, reason, message)
|
|
if reason not in GAPI.DEFAULT_RETRY_REASONS:
|
|
if reason in [GAPI.BAD_REQUEST, GAPI.INVALID_CUSTOMER_ID, GAPI.LOGIN_REQUIRED]:
|
|
accessErrorExit(cd)
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, topLevelOrgUnits[int(ri[RI_I])]], errMsg)
|
|
return
|
|
waitOnFailure(1, 10, reason, message)
|
|
try:
|
|
response = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], type='all', orgUnitPath=topLevelOrgUnits[int(ri[RI_I])], fields=listfields)
|
|
orgUnits.extend(response.get('organizationUnits', []))
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, topLevelOrgUnits[int(ri[RI_I])]], Msg.DOES_NOT_EXIST)
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
accessErrorExit(cd)
|
|
|
|
def _batchListOrgUnits():
|
|
svcargs = dict([('customerId', GC.Values[GC.CUSTOMER_ID]), ('orgUnitPath', None), ('type', 'all'), ('fields', listfields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(cd.orgunits(), 'list')
|
|
dbatch = cd.new_batch_http_request(callback=_callbackListOrgUnits)
|
|
bcount = 0
|
|
i = 0
|
|
for orgUnitPath in topLevelOrgUnits:
|
|
svcparms = svcargs.copy()
|
|
svcparms['orgUnitPath'] = orgUnitPath
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID('', i, 0, 0, 0, ''))
|
|
bcount += 1
|
|
i += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = cd.new_batch_http_request(callback=_callbackListOrgUnits)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
|
|
deleteOrgUnitId = deleteParentOrgUnitId = False
|
|
if showParent:
|
|
localFieldsList = fieldsList[:]
|
|
if 'orgUnitId' not in fieldsList:
|
|
localFieldsList.append('orgUnitId')
|
|
deleteOrgUnitId = True
|
|
if 'parentOrgUnitId' not in fieldsList:
|
|
localFieldsList.append('parentOrgUnitId')
|
|
deleteParentOrgUnitId = True
|
|
fields = getFieldsFromFieldsList(localFieldsList)
|
|
else:
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
listfields = f'organizationUnits({fields})'
|
|
if listType == 'all' and orgUnitPath == '/':
|
|
printGettingAllAccountEntities(Ent.ORGANIZATIONAL_UNIT)
|
|
else:
|
|
printGettingAllEntityItemsForWhom(Ent.CHILD_ORGANIZATIONAL_UNIT, orgUnitPath,
|
|
qualifier=' (Direct Children)' if listType == 'children' else '', entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
if listType == 'children':
|
|
batchSubOrgs = False
|
|
try:
|
|
orgs = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], type=listType if not batchSubOrgs else 'children',
|
|
orgUnitPath=orgUnitPath, fields=listfields)
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError):
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.DOES_NOT_EXIST)
|
|
return None
|
|
except (GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
accessErrorExit(cd)
|
|
orgUnits = orgs.get('organizationUnits', [])
|
|
topLevelOrgUnits = [orgUnit['orgUnitPath'] for orgUnit in orgUnits]
|
|
if batchSubOrgs:
|
|
_batchListOrgUnits()
|
|
if showParent:
|
|
parentOrgIds = []
|
|
retrievedOrgIds = []
|
|
if not orgUnits:
|
|
topLevelOrgId = getTopLevelOrgId(cd, orgUnitPath)
|
|
if topLevelOrgId:
|
|
parentOrgIds.append(topLevelOrgId)
|
|
for orgUnit in orgUnits:
|
|
retrievedOrgIds.append(orgUnit['orgUnitId'])
|
|
if orgUnit['parentOrgUnitId'] not in parentOrgIds:
|
|
parentOrgIds.append(orgUnit['parentOrgUnitId'])
|
|
missing_parents = set(parentOrgIds)-set(retrievedOrgIds)
|
|
for missing_parent in missing_parents:
|
|
try:
|
|
result = callGAPI(cd.orgunits(), 'get',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=missing_parent, fields=fields)
|
|
orgUnits.append(result)
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError,
|
|
GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
pass
|
|
if listType == 'all' and orgUnitPath == '/':
|
|
printGotAccountEntities(len(orgUnits))
|
|
else:
|
|
printGotEntityItemsForWhom(len(orgUnits))
|
|
if childSelector is not None:
|
|
for orgUnit in orgUnits:
|
|
orgUnit[ORG_UNIT_SELECTOR_FIELD] = childSelector if orgUnit['orgUnitPath'] != orgUnitPath else parentSelector
|
|
if deleteOrgUnitId or deleteParentOrgUnitId:
|
|
for orgUnit in orgUnits:
|
|
if deleteOrgUnitId:
|
|
orgUnit.pop('orgUnitId', None)
|
|
if deleteParentOrgUnitId:
|
|
orgUnit.pop('parentOrgUnitId', None)
|
|
return orgUnits
|
|
|
|
def getOrgUnitIdToPathMap(cd=None):
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
orgUnits = _getOrgUnits(cd, '/', ['orgUnitPath', 'orgUnitId'], 'all', True, False)
|
|
return {ou['orgUnitId']:ou['orgUnitPath'] for ou in orgUnits}
|
|
|
|
# gam print orgs|ous [todrive <ToDriveAttribute>*]
|
|
# [fromparent <OrgUnitItem>] [showparent] [toplevelonly]
|
|
# [parentselector <OrgUnitSelector> childselector <OrgUnitSelector>]
|
|
# [allfields|<OrgUnitFieldName>*|(fields <OrgUnitFieldNameList>)] [convertcrnl] [batchsuborgs [<Boolean>]]
|
|
# [mincroscount <Number>] [maxcroscount <Number>]
|
|
# [minusercount <Number>] [maxusercount <Number>]
|
|
# [showitemcountonly]
|
|
def doPrintOrgs():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]
|
|
fieldsList = []
|
|
csvPF = CSVPrintFile(sortTitles=ORG_FIELD_PRINT_ORDER)
|
|
orgUnitPath = '/'
|
|
listType = 'all'
|
|
batchSubOrgs = showParent = False
|
|
childSelector = parentSelector = None
|
|
minCrOSCounts = maxCrOSCounts = minUserCounts = maxUserCounts = -1
|
|
crosCounts = {}
|
|
userCounts = {}
|
|
showItemCountOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'fromparent':
|
|
orgUnitPath = getOrgUnitItem()
|
|
elif myarg == 'showparent':
|
|
showParent = getBoolean()
|
|
elif myarg == 'parentselector':
|
|
parentSelector = getChoice(PRINT_OUS_SELECTOR_CHOICES)
|
|
elif myarg == 'childselector':
|
|
childSelector = getChoice(PRINT_OUS_SELECTOR_CHOICES)
|
|
elif myarg == 'mincroscount':
|
|
minCrOSCounts = getInteger(minVal=-1)
|
|
elif myarg == 'maxcroscount':
|
|
maxCrOSCounts = getInteger(minVal=-1)
|
|
elif myarg == 'minusercount':
|
|
minUserCounts = getInteger(minVal=-1)
|
|
elif myarg == 'maxusercount':
|
|
maxUserCounts = getInteger(minVal=-1)
|
|
elif myarg == 'batchsuborgs':
|
|
batchSubOrgs = getBoolean()
|
|
elif myarg == 'toplevelonly':
|
|
listType = 'children'
|
|
elif myarg == 'allfields':
|
|
fieldsList = []
|
|
csvPF.SetTitles(fieldsList)
|
|
for field in ORG_FIELD_PRINT_ORDER:
|
|
csvPF.AddField(field, ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
|
|
elif myarg in ORG_ARGUMENT_TO_FIELD_MAP:
|
|
if not fieldsList:
|
|
csvPF.AddField('orgUnitPath', ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
|
|
csvPF.AddField(myarg, ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
|
|
elif myarg == 'fields':
|
|
if not fieldsList:
|
|
csvPF.AddField('orgUnitPath', ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
|
|
for field in _getFieldsList():
|
|
if field in ORG_ARGUMENT_TO_FIELD_MAP:
|
|
csvPF.AddField(field, ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
|
|
else:
|
|
invalidChoiceExit(field, list(ORG_ARGUMENT_TO_FIELD_MAP), True)
|
|
elif myarg in {'convertcrnl', 'converttextnl'}:
|
|
convertCRNL = True
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if childSelector:
|
|
if showParent and parentSelector is None:
|
|
missingArgumentExit('parentselector')
|
|
csvPF.AddTitle(ORG_UNIT_SELECTOR_FIELD)
|
|
csvPF.AddSortTitle(ORG_UNIT_SELECTOR_FIELD)
|
|
showCrOSCounts = (minCrOSCounts >= 0 or maxCrOSCounts >= 0)
|
|
showUserCounts = (minUserCounts >= 0 or maxUserCounts >= 0)
|
|
if not fieldsList:
|
|
for field in PRINT_ORGS_DEFAULT_FIELDS:
|
|
csvPF.AddField(field, ORG_ARGUMENT_TO_FIELD_MAP, fieldsList)
|
|
orgUnits = _getOrgUnits(cd, orgUnitPath, fieldsList, listType, showParent, batchSubOrgs, childSelector, parentSelector)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{0 if orgUnits is None else (len(orgUnits))}\n')
|
|
return
|
|
if orgUnits is None:
|
|
return
|
|
if showUserCounts:
|
|
for orgUnit in orgUnits:
|
|
userCounts[orgUnit['orgUnitPath']] = [0, 0]
|
|
qualifier = Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
|
|
printGettingAllEntityItemsForWhom(Ent.USER, orgUnitPath, qualifier=qualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
pageMessage = getPageMessageForWhom()
|
|
try:
|
|
feed = yieldGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
|
|
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, None), orderBy='email',
|
|
fields='nextPageToken,users(orgUnitPath,suspended)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
|
|
for users in feed:
|
|
for user in users:
|
|
if user['orgUnitPath'] in userCounts:
|
|
userCounts[user['orgUnitPath']][user['suspended']] += 1
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.invalidInput, GAPI.badRequest, GAPI.backendError,
|
|
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
|
|
for orgUnit in sorted(orgUnits, key=lambda k: k['orgUnitPath']):
|
|
orgUnitPath = orgUnit['orgUnitPath']
|
|
if showCrOSCounts:
|
|
crosCounts[orgUnit['orgUnitPath']] = {}
|
|
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
pageMessage = getPageMessageForWhom()
|
|
pageToken = None
|
|
totalItems = 0
|
|
tokenRetries = 0
|
|
while True:
|
|
try:
|
|
feed = callGAPI(cd.chromeosdevices(), 'list', 'chromeosdevices',
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageToken=pageToken,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnitPath, includeChildOrgunits=False,
|
|
fields='nextPageToken,chromeosdevices(status)', maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
|
|
tokenRetries = 0
|
|
pageToken, totalItems = _processGAPIpagesResult(feed, 'chromeosdevices', None, totalItems, pageMessage, None, Ent.CROS_DEVICE)
|
|
if feed:
|
|
for cros in feed.get('chromeosdevices', []):
|
|
crosCounts[orgUnitPath].setdefault(cros['status'], 0)
|
|
crosCounts[orgUnitPath][cros['status']] += 1
|
|
del feed
|
|
if not pageToken:
|
|
_finalizeGAPIpagesResult(pageMessage)
|
|
break
|
|
except GAPI.invalidInput as e:
|
|
message = str(e)
|
|
# Invalid Input: xyz - Check for invalid pageToken!!
|
|
# 0123456789012345
|
|
if message[15:] == pageToken:
|
|
tokenRetries += 1
|
|
if tokenRetries <= 2:
|
|
writeStderr(f'{WARNING_PREFIX}{Msg.LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY}')
|
|
time.sleep(tokenRetries*5)
|
|
continue
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CROS_DEVICE, None], message)
|
|
break
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.badRequest, GAPI.backendError,
|
|
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
|
|
break
|
|
row = {}
|
|
for field in fieldsList:
|
|
if convertCRNL and field in ORG_FIELDS_WITH_CRS_NLS:
|
|
row[field] = escapeCRsNLs(orgUnit.get(field, ''))
|
|
else:
|
|
row[field] = orgUnit.get(field, '')
|
|
if childSelector:
|
|
row[ORG_UNIT_SELECTOR_FIELD] = orgUnit[ORG_UNIT_SELECTOR_FIELD]
|
|
if showCrOSCounts or showUserCounts:
|
|
if showCrOSCounts:
|
|
total = 0
|
|
for k, v in sorted(iter(crosCounts[orgUnitPath].items())):
|
|
row[f'CrOS{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = v
|
|
total += v
|
|
row[f'CrOS{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Total'] = total
|
|
if ((minCrOSCounts != -1 and total < minCrOSCounts) or
|
|
(maxCrOSCounts != -1 and total > maxCrOSCounts)):
|
|
continue
|
|
if showUserCounts:
|
|
row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}NotSuspended'] = userCounts[orgUnitPath][0]
|
|
row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Suspended'] = userCounts[orgUnitPath][1]
|
|
row[f'Users{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}Total'] = total = userCounts[orgUnitPath][0]+userCounts[orgUnitPath][1]
|
|
if ((minUserCounts != -1 and total < minUserCounts) or
|
|
(maxUserCounts != -1 and total > maxUserCounts)):
|
|
continue
|
|
csvPF.WriteRowTitles(row)
|
|
else:
|
|
csvPF.WriteRow(row)
|
|
csvPF.writeCSVfile('Orgs')
|
|
|
|
# gam show orgtree [fromparent <OrgUnitItem>] [batchsuborgs [Boolean>]]
|
|
def doShowOrgTree():
|
|
def addOrgUnitToTree(orgPathList, i, n, tree):
|
|
if orgPathList[i] not in tree:
|
|
tree[orgPathList[i]] = {}
|
|
if i < n:
|
|
addOrgUnitToTree(orgPathList, i+1, n, tree[orgPathList[i]])
|
|
|
|
def printOrgUnit(parentOrgUnit, tree):
|
|
printKeyValueList([parentOrgUnit])
|
|
Ind.Increment()
|
|
for childOrgUnit in sorted(tree[parentOrgUnit]):
|
|
printOrgUnit(childOrgUnit, tree[parentOrgUnit])
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
orgUnitPath = '/'
|
|
fieldsList = ['orgUnitPath']
|
|
listType = 'all'
|
|
batchSubOrgs = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'fromparent':
|
|
orgUnitPath = getOrgUnitItem()
|
|
elif myarg == 'batchsuborgs':
|
|
batchSubOrgs = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
orgUnits = _getOrgUnits(cd, orgUnitPath, fieldsList, listType, False, batchSubOrgs)
|
|
if orgUnits is None:
|
|
return
|
|
orgTree = {}
|
|
for orgUnit in orgUnits:
|
|
orgPath = orgUnit['orgUnitPath'].split('/')
|
|
addOrgUnitToTree(orgPath, 1, len(orgPath)-1, orgTree)
|
|
for org in sorted(orgTree):
|
|
printOrgUnit(org, orgTree)
|
|
|
|
ORG_ITEMS_FIELD_MAP = {
|
|
'browsers': 'browsers',
|
|
'devices': 'devices',
|
|
'shareddrives': 'sharedDrives',
|
|
'subous': 'subOus',
|
|
'users': 'users',
|
|
}
|
|
|
|
# gam check ou|org <OrgUnitItem> [todrive <ToDriveAttribute>*]
|
|
# [<OrgUnitCheckName>*|(fields <OrgUnitCheckNameList>)]
|
|
# [filename <FileName>] [movetoou <OrgUnitItem>]
|
|
# [formatjson [quotechar <Character>]]
|
|
def doCheckOrgUnit():
|
|
def writeCommandInfo(field):
|
|
nonlocal commitBatch
|
|
if commitBatch:
|
|
f.write(f'{Cmd.COMMIT_BATCH_CMD}\n')
|
|
else:
|
|
commitBatch = True
|
|
f.write(f'{Cmd.PRINT_CMD} Move {field} from {orgUnitPath} to {moveToOrgUnitPath}\n')
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['orgUnitPath', 'orgUnitId', 'empty'])
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
f = orgUnitPath = None
|
|
fieldsList = []
|
|
titlesList = []
|
|
status, orgUnitPath, orgUnitId = checkOrgUnitPathExists(cd, getOrgUnitItem())
|
|
if not status:
|
|
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
|
|
orgUnitPathLower = orgUnitPath.lower()
|
|
fileName = 'CleanOuBatch.txt'
|
|
moveToOrgUnitPath = moveToOrgUnitPathLower = None
|
|
commitBatch = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ORG_ITEMS_FIELD_MAP:
|
|
fieldsList.append(myarg)
|
|
elif myarg == 'fields':
|
|
for field in _getFieldsList():
|
|
if field in ORG_ITEMS_FIELD_MAP:
|
|
fieldsList.append(field)
|
|
else:
|
|
invalidChoiceExit(field, list(ORG_ITEMS_FIELD_MAP), True)
|
|
elif myarg == 'filename':
|
|
fileName = setFilePath(getString(Cmd.OB_FILE_NAME))
|
|
elif myarg == 'movetoou':
|
|
movetoouLocation = Cmd.Location()
|
|
status, moveToOrgUnitPath, _ = checkOrgUnitPathExists(cd, getOrgUnitItem())
|
|
moveToOrgUnitPathLower = moveToOrgUnitPath.lower()
|
|
if not status:
|
|
entityDoesNotExistExit(Ent.ORGANIZATIONAL_UNIT, moveToOrgUnitPath)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if not fieldsList:
|
|
fieldsList = ORG_ITEMS_FIELD_MAP.keys()
|
|
if moveToOrgUnitPath is not None:
|
|
Cmd.SetLocation(movetoouLocation)
|
|
if orgUnitPathLower == moveToOrgUnitPathLower:
|
|
usageErrorExit(Msg.OU_AND_MOVETOOU_CANNOT_BE_IDENTICAL.format(orgUnitPath, moveToOrgUnitPath))
|
|
if 'subous' in fieldsList and moveToOrgUnitPathLower.startswith(orgUnitPathLower):
|
|
usageErrorExit(Msg.OU_SUBOUS_CANNOT_BE_MOVED_TO_MOVETOOU.format(orgUnitPath, moveToOrgUnitPath))
|
|
fileName = setFilePath(fileName)
|
|
f = openFile(fileName, DEFAULT_FILE_WRITE_MODE)
|
|
orgUnitItemCounts = {}
|
|
for field in sorted(fieldsList):
|
|
title = ORG_ITEMS_FIELD_MAP[field]
|
|
orgUnitItemCounts[title] = 0
|
|
if not FJQC.formatJSON:
|
|
titlesList.append(title)
|
|
if 'browsers' in fieldsList:
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
printGettingAllEntityItemsForWhom(Ent.CHROME_BROWSER, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
feed = yieldGAPIpages(cbcm.chromebrowsers(), 'list', 'browsers',
|
|
pageMessage=pageMessage, messageAttribute='deviceId',
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customerId, orgUnitPath=orgUnitPath, projection='BASIC',
|
|
fields='nextPageToken,browsers(deviceId)')
|
|
for browsers in feed:
|
|
orgUnitItemCounts['browsers'] += len(browsers)
|
|
if f is not None and orgUnitItemCounts['browsers'] > 0:
|
|
writeCommandInfo('browsers')
|
|
f.write(f'gam move browsers ou {moveToOrgUnitPath} browserou {orgUnitPath}\n')
|
|
except (GAPI.invalidInput, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER, None], str(e))
|
|
except GAPI.invalidOrgunit as e:
|
|
entityActionFailedExit([Ent.CHROME_BROWSER, None], str(e))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound):
|
|
accessErrorExit(None)
|
|
if 'devices' in fieldsList:
|
|
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
pageMessage = getPageMessageForWhom()
|
|
pageToken = None
|
|
totalItems = 0
|
|
tokenRetries = 0
|
|
while True:
|
|
try:
|
|
feed = callGAPI(cd.chromeosdevices(), 'list',
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageToken=pageToken, customerId=GC.Values[GC.CUSTOMER_ID],
|
|
orgUnitPath=orgUnitPath, fields='nextPageToken,chromeosdevices(deviceId)', maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
|
|
tokenRetries = 0
|
|
pageToken, totalItems = _processGAPIpagesResult(feed, 'chromeosdevices', None, totalItems, pageMessage, None, Ent.CROS_DEVICE)
|
|
if feed:
|
|
orgUnitItemCounts['devices'] += len(feed.get('chromeosdevices', []))
|
|
del feed
|
|
if not pageToken:
|
|
_finalizeGAPIpagesResult(pageMessage)
|
|
printGotAccountEntities(totalItems)
|
|
if f is not None and orgUnitItemCounts['devices'] > 0:
|
|
writeCommandInfo('devices')
|
|
f.write(f'gam update ou {moveToOrgUnitPath} add cros_ou {orgUnitPath}\n')
|
|
break
|
|
except GAPI.invalidInput as e:
|
|
message = str(e)
|
|
# Invalid Input: xyz - Check for invalid pageToken!!
|
|
# 0123456789012345
|
|
if message[15:] == pageToken:
|
|
tokenRetries += 1
|
|
if tokenRetries <= 2:
|
|
writeStderr(f'{WARNING_PREFIX}{Msg.LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY}')
|
|
time.sleep(tokenRetries*5)
|
|
continue
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
|
|
break
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
|
|
break
|
|
except GAPI.invalidOrgunit as e:
|
|
entityActionFailedExit([Ent.CROS_DEVICE, None], str(e))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
if 'shareddrives' in fieldsList:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_ORGUNITS_BETA)
|
|
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
sds = callGAPIpages(ci.orgUnits().memberships(), 'list', 'orgMemberships',
|
|
pageMessage=getPageMessageForWhom(),
|
|
parent=f'orgUnits/{orgUnitId[3:]}',
|
|
customer=_getCustomersCustomerIdWithC(),
|
|
filter="type == 'shared_drive'")
|
|
orgUnitItemCounts['sharedDrives'] = len(sds)
|
|
if f is not None and orgUnitItemCounts['sharedDrives'] > 0:
|
|
writeCommandInfo('Shared Drives')
|
|
for sd in sds:
|
|
name = sd['name'].split(';')[1]
|
|
f.write(f'gam update shareddrive {name} ou {moveToOrgUnitPath}\n')
|
|
if 'subous' in fieldsList:
|
|
subOus = _getOrgUnits(cd, orgUnitPath, ['orgUnitPath'], 'children', False, False, None, None)
|
|
orgUnitItemCounts['subOus'] = len(subOus)
|
|
if f is not None and orgUnitItemCounts['subOus'] > 0:
|
|
writeCommandInfo('Sub OrgUnit')
|
|
for ou in subOus:
|
|
f.write(f'gam update ou {ou["orgUnitPath"]} parent {moveToOrgUnitPath}\n')
|
|
if 'users' in fieldsList:
|
|
printGettingAllEntityItemsForWhom(Ent.USER, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
pageMessage = getPageMessageForWhom()
|
|
try:
|
|
feed = yieldGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.ORGUNIT_NOT_FOUND,
|
|
GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], query=orgUnitPathQuery(orgUnitPath, None),
|
|
fields='nextPageToken,users(orgUnitPath)', maxResults=GC.Values[GC.USER_MAX_RESULTS])
|
|
for users in feed:
|
|
for user in users:
|
|
if orgUnitPathLower == user.get('orgUnitPath', '').lower():
|
|
orgUnitItemCounts['users'] += 1
|
|
if f is not None and orgUnitItemCounts['users'] > 0:
|
|
writeCommandInfo('users')
|
|
f.write(f'gam update ou {moveToOrgUnitPath} add ou {orgUnitPath}\n')
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.invalidInput, GAPI.badRequest, GAPI.backendError,
|
|
GAPI.invalidCustomerId, GAPI.loginRequired, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitPath)
|
|
if f is not None:
|
|
closeFile(f)
|
|
writeStderr(Msg.GAM_BATCH_FILE_WRITTEN.format(fileName))
|
|
empty = True
|
|
for count in orgUnitItemCounts.values():
|
|
if count > 0:
|
|
empty = False
|
|
break
|
|
baseRow = {'orgUnitPath': orgUnitPath, 'orgUnitId': orgUnitId, 'empty': empty}
|
|
row = flattenJSON(orgUnitItemCounts, baseRow.copy())
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
baseRow['JSON'] = json.dumps(cleanJSON(orgUnitItemCounts), ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(baseRow)
|
|
csvPF.writeCSVfile(f'OrgUnit {orgUnitPath} Item Counts')
|
|
if not empty and GM.Globals[GM.SYSEXITRC] == 0:
|
|
setSysExitRC(ORGUNIT_NOT_EMPTY_RC)
|
|
|
|
ALIAS_TARGET_TYPES = ['user', 'group', 'target']
|
|
|
|
# gam create aliases|nicknames <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
|
|
# [verifynotinvitable]
|
|
# gam update aliases|nicknames <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
|
|
# [notargetverify] [waitafterdelete <Integer>]
|
|
def doCreateUpdateAliases():
|
|
def verifyAliasTargetExists():
|
|
if targetType != 'group':
|
|
try:
|
|
callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=targetEmail, fields='primaryEmail')
|
|
return 'user'
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
if targetType == 'user':
|
|
return None
|
|
try:
|
|
callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=targetEmail, fields='email')
|
|
return 'group'
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.systemError):
|
|
return None
|
|
|
|
def deleteAliasOnUpdate():
|
|
# User alias
|
|
if targetType != 'group':
|
|
try:
|
|
callGAPI(cd.users().aliases(), 'delete',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
userKey=aliasEmail, alias=aliasEmail)
|
|
printEntityKVList([Ent.USER_ALIAS, aliasEmail], [Act.PerformedName(Act.DELETE)], i, count)
|
|
time.sleep(waitAfterDelete)
|
|
return True
|
|
except GAPI.conditionNotMet as e:
|
|
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail], str(e), i, count)
|
|
return False
|
|
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource):
|
|
if targetType == 'user':
|
|
entityUnknownWarning(Ent.USER_ALIAS, aliasEmail, i, count)
|
|
return False
|
|
# Group alias
|
|
try:
|
|
callGAPI(cd.groups().aliases(), 'delete',
|
|
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
groupKey=aliasEmail, alias=aliasEmail)
|
|
time.sleep(waitAfterDelete)
|
|
return True
|
|
except GAPI.conditionNotMet as e:
|
|
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail], str(e), i, count)
|
|
return False
|
|
except GAPI.forbidden:
|
|
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
|
|
return False
|
|
except (GAPI.groupNotFound, GAPI.badRequest, GAPI.invalid, GAPI.invalidResource):
|
|
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
|
|
return False
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
updateCmd = Act.Get() == Act.UPDATE
|
|
aliasList = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
|
|
targetType = getChoice(ALIAS_TARGET_TYPES)
|
|
targetEmails = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
entityLists = targetEmails if isinstance(targetEmails, dict) else None
|
|
verifyNotInvitable = False
|
|
verifyTarget = updateCmd
|
|
waitAfterDelete = 2
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if (not updateCmd) and myarg == 'verifynotinvitable':
|
|
verifyNotInvitable = True
|
|
elif updateCmd and myarg == 'notargetverify':
|
|
verifyTarget = False
|
|
elif updateCmd and myarg == 'waitafterdelete':
|
|
waitAfterDelete = getInteger(minVal=2, maxVal=10)
|
|
else:
|
|
unknownArgumentExit()
|
|
i = 0
|
|
count = len(aliasList)
|
|
for aliasEmail in aliasList:
|
|
i += 1
|
|
if entityLists:
|
|
targetEmails = entityLists[aliasEmail]
|
|
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True, noLower=True)
|
|
if verifyNotInvitable:
|
|
isInvitableUser, ci = _getIsInvitableUser(ci, aliasEmail)
|
|
if isInvitableUser:
|
|
entityActionNotPerformedWarning([Ent.ALIAS_EMAIL, aliasEmail], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT)
|
|
continue
|
|
body = {'alias': aliasEmail}
|
|
jcount = len(targetEmails)
|
|
if jcount > 0:
|
|
# Only process first target
|
|
targetEmail = normalizeEmailAddressOrUID(targetEmails[0])
|
|
if verifyTarget:
|
|
targetType = verifyAliasTargetExists()
|
|
if targetType is None:
|
|
entityUnknownWarning(Ent.ALIAS_TARGET, targetEmail, i, count)
|
|
continue
|
|
if updateCmd and not deleteAliasOnUpdate():
|
|
continue
|
|
# User alias
|
|
if targetType != 'group':
|
|
try:
|
|
callGAPI(cd.users().aliases(), 'insert',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST,
|
|
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.FORBIDDEN, GAPI.DUPLICATE,
|
|
GAPI.CONDITION_NOT_MET, GAPI.LIMIT_EXCEEDED],
|
|
userKey=targetEmail, body=body, fields='')
|
|
entityActionPerformed([Ent.USER_ALIAS, aliasEmail, Ent.USER, targetEmail], i, count)
|
|
continue
|
|
except (GAPI.conditionNotMet, GAPI.limitExceeded) as e:
|
|
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail, Ent.USER, targetEmail], str(e), i, count)
|
|
continue
|
|
except GAPI.duplicate:
|
|
duplicateAliasGroupUserWarning(cd, [Ent.USER_ALIAS, aliasEmail, Ent.USER, targetEmail], i, count)
|
|
continue
|
|
except (GAPI.invalid, GAPI.invalidInput):
|
|
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail, Ent.USER, targetEmail], Msg.INVALID_ALIAS, i, count)
|
|
continue
|
|
except (GAPI.userNotFound, GAPI.badRequest, GAPI.forbidden):
|
|
if targetType == 'user':
|
|
entityUnknownWarning(Ent.ALIAS_TARGET, targetEmail, i, count)
|
|
continue
|
|
# Group alias
|
|
try:
|
|
callGAPI(cd.groups().aliases(), 'insert',
|
|
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST,
|
|
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.FORBIDDEN, GAPI.DUPLICATE,
|
|
GAPI.CONDITION_NOT_MET, GAPI.LIMIT_EXCEEDED],
|
|
groupKey=targetEmail, body=body, fields='')
|
|
entityActionPerformed([Ent.GROUP_ALIAS, aliasEmail, Ent.GROUP, targetEmail], i, count)
|
|
except (GAPI.conditionNotMet, GAPI.limitExceeded) as e:
|
|
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail, Ent.GROUP, targetEmail], str(e), i, count)
|
|
except GAPI.duplicate:
|
|
duplicateAliasGroupUserWarning(cd, [Ent.GROUP_ALIAS, aliasEmail, Ent.GROUP, targetEmail], i, count)
|
|
except (GAPI.invalid, GAPI.invalidInput):
|
|
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail, Ent.GROUP, targetEmail], Msg.INVALID_ALIAS, i, count)
|
|
except (GAPI.groupNotFound, GAPI.userNotFound, GAPI.badRequest, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.ALIAS_TARGET, targetEmail, i, count)
|
|
|
|
# gam delete aliases|nicknames [user|group|target] <EmailAddressEntity>
|
|
def doDeleteAliases():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
targetType = getChoice(ALIAS_TARGET_TYPES, defaultChoice='target')
|
|
entityList = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for aliasEmail in entityList:
|
|
i += 1
|
|
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True)
|
|
aliasDeleted = False
|
|
if targetType != 'group':
|
|
try:
|
|
result = callGAPI(cd.users().aliases(), 'list',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
userKey=aliasEmail, fields='aliases(alias)')
|
|
for aliasEntry in result.get('aliases', []):
|
|
if aliasEmail == aliasEntry['alias'].lower():
|
|
aliasEmail = aliasEntry['alias']
|
|
callGAPI(cd.users().aliases(), 'delete',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
userKey=aliasEmail, alias=aliasEmail)
|
|
entityActionPerformed([Ent.USER_ALIAS, aliasEmail], i, count)
|
|
aliasDeleted = True
|
|
break
|
|
if aliasDeleted:
|
|
continue
|
|
except GAPI.conditionNotMet as e:
|
|
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail], str(e), i, count)
|
|
continue
|
|
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource):
|
|
pass
|
|
if targetType == 'user':
|
|
entityUnknownWarning(Ent.USER_ALIAS, aliasEmail, i, count)
|
|
continue
|
|
try:
|
|
result = callGAPI(cd.groups().aliases(), 'list',
|
|
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
groupKey=aliasEmail, fields='aliases(alias)')
|
|
for aliasEntry in result.get('aliases', []):
|
|
if aliasEmail == aliasEntry['alias'].lower():
|
|
aliasEmail = aliasEntry['alias']
|
|
callGAPI(cd.groups().aliases(), 'delete',
|
|
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
groupKey=aliasEmail, alias=aliasEmail)
|
|
entityActionPerformed([Ent.GROUP_ALIAS, aliasEmail], i, count)
|
|
aliasDeleted = True
|
|
break
|
|
if aliasDeleted:
|
|
continue
|
|
except GAPI.conditionNotMet as e:
|
|
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail], str(e), i, count)
|
|
continue
|
|
except (GAPI.groupNotFound, GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource):
|
|
pass
|
|
if targetType == 'group':
|
|
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
|
|
continue
|
|
entityUnknownWarning(Ent.ALIAS, aliasEmail, i, count)
|
|
|
|
# gam remove aliases|nicknames <EmailAddress> user|group <EmailAddressEntity>
|
|
def doRemoveAliases():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
targetEmail = getEmailAddress()
|
|
targetType = getChoice(['user', 'group'])
|
|
entityList = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
|
|
checkForExtraneousArguments()
|
|
count = len(entityList)
|
|
i = 0
|
|
if targetType == 'user':
|
|
try:
|
|
for aliasEmail in entityList:
|
|
i += 1
|
|
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True)
|
|
callGAPI(cd.users().aliases(), 'delete',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
userKey=targetEmail, alias=aliasEmail)
|
|
entityActionPerformed([Ent.USER, targetEmail, Ent.USER_ALIAS, aliasEmail], i, count)
|
|
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.conditionNotMet) as e:
|
|
entityActionFailedWarning([Ent.USER, targetEmail, Ent.USER_ALIAS, aliasEmail], str(e), i, count)
|
|
except GAPI.invalidResource:
|
|
entityActionFailedWarning([Ent.USER, targetEmail, Ent.USER_ALIAS, aliasEmail], Msg.DOES_NOT_EXIST, i, count)
|
|
else:
|
|
try:
|
|
for aliasEmail in entityList:
|
|
i += 1
|
|
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True)
|
|
callGAPI(cd.groups().aliases(), 'delete',
|
|
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
groupKey=targetEmail, alias=aliasEmail)
|
|
entityActionPerformed([Ent.GROUP, targetEmail, Ent.GROUP_ALIAS, aliasEmail], i, count)
|
|
except (GAPI.groupNotFound, GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.conditionNotMet) as e:
|
|
entityActionFailedWarning([Ent.GROUP, targetEmail, Ent.GROUP_ALIAS, aliasEmail], str(e), i, count)
|
|
except GAPI.invalidResource:
|
|
entityActionFailedWarning([Ent.GROUP, targetEmail, Ent.GROUP_ALIAS, aliasEmail], Msg.DOES_NOT_EXIST, i, count)
|
|
|
|
def _addUserAliases(cd, user, aliasList, i, count):
|
|
jcount = len(aliasList)
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.USER_ALIAS, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for aliasEmail in aliasList:
|
|
j += 1
|
|
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True, noLower=True)
|
|
body = {'alias': aliasEmail}
|
|
try:
|
|
callGAPI(cd.users().aliases(), 'insert',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST,
|
|
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.FORBIDDEN, GAPI.DUPLICATE,
|
|
GAPI.CONDITION_NOT_MET, GAPI.LIMIT_EXCEEDED],
|
|
userKey=user, body=body, fields='')
|
|
entityActionPerformed([Ent.USER, user, Ent.USER_ALIAS, aliasEmail], j, jcount)
|
|
except (GAPI.conditionNotMet, GAPI.limitExceeded) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.USER_ALIAS, aliasEmail], str(e), j, jcount)
|
|
except GAPI.duplicate:
|
|
duplicateAliasGroupUserWarning(cd, [Ent.USER, user, Ent.USER_ALIAS, aliasEmail], j, jcount)
|
|
except (GAPI.invalid, GAPI.invalidInput):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.USER_ALIAS, aliasEmail], Msg.INVALID_ALIAS, j, jcount)
|
|
except (GAPI.userNotFound, GAPI.badRequest, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delete alias|aliases
|
|
def deleteUsersAliases(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
user_aliases = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=user, fields='id,primaryEmail,aliases')
|
|
user_id = user_aliases['id']
|
|
user_primary = user_aliases['primaryEmail']
|
|
jcount = len(user_aliases['aliases']) if ('aliases' in user_aliases) else 0
|
|
entityPerformActionNumItems([Ent.USER, user_primary], jcount, Ent.ALIAS, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for an_alias in user_aliases['aliases']:
|
|
j += 1
|
|
try:
|
|
callGAPI(cd.users().aliases(), 'delete',
|
|
throwReasons=[GAPI.RESOURCE_ID_NOT_FOUND],
|
|
userKey=user_id, alias=an_alias)
|
|
entityActionPerformed([Ent.USER, user_primary, Ent.ALIAS, an_alias], j, jcount)
|
|
except GAPI.resourceIdNotFound:
|
|
entityActionFailedWarning([Ent.USER, user_primary, Ent.ALIAS, an_alias], Msg.DOES_NOT_EXIST, j, jcount)
|
|
Ind.Decrement()
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
|
|
def infoAliases(entityList):
|
|
|
|
def _showAliasInfo(uid, email, aliasEmail, entityType, aliasEntityType, i, count):
|
|
if email.lower() != aliasEmail:
|
|
printEntity([aliasEntityType, aliasEmail], i, count)
|
|
Ind.Increment()
|
|
printEntity([entityType, email])
|
|
printEntity([Ent.UNIQUE_ID, uid])
|
|
Ind.Decrement()
|
|
else:
|
|
setSysExitRC(ENTITY_IS_NOT_AN_ALIAS_RC)
|
|
printEntityKVList([Ent.EMAIL, aliasEmail],
|
|
[f'Is a {Ent.Singular(entityType)}, not a {Ent.Singular(aliasEntityType)}'],
|
|
i, count)
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
# Ignore info group/user arguments that may have come from whatis
|
|
if (myarg in INFO_GROUP_OPTIONS) or (myarg in INFO_USER_OPTIONS):
|
|
if myarg == 'schemas':
|
|
getString(Cmd.OB_SCHEMA_NAME_LIST)
|
|
else:
|
|
unknownArgumentExit()
|
|
i = 0
|
|
count = len(entityList)
|
|
for aliasEmail in entityList:
|
|
i += 1
|
|
aliasEmail = normalizeEmailAddressOrUID(aliasEmail, noUid=True, noLower=True)
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=aliasEmail, fields='id,primaryEmail')
|
|
_showAliasInfo(result['id'], result['primaryEmail'], aliasEmail, Ent.USER_EMAIL, Ent.USER_ALIAS, i, count)
|
|
continue
|
|
except (GAPI.userNotFound, GAPI.badRequest):
|
|
pass
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.backendError, GAPI.systemError):
|
|
entityUnknownWarning(Ent.USER_ALIAS, aliasEmail, i, count)
|
|
continue
|
|
try:
|
|
result = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
|
|
groupKey=aliasEmail, fields='id,email')
|
|
_showAliasInfo(result['id'], result['email'], aliasEmail, Ent.GROUP_EMAIL, Ent.GROUP_ALIAS, i, count)
|
|
continue
|
|
except GAPI.groupNotFound:
|
|
pass
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
|
|
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
|
|
continue
|
|
entityUnknownWarning(Ent.EMAIL, aliasEmail, i, count)
|
|
|
|
# gam info aliases|nicknames <EmailAddressEntity>
|
|
def doInfoAliases():
|
|
infoAliases(getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY))
|
|
|
|
def initUserGroupDomainQueryFilters():
|
|
if not GC.Values[GC.PRINT_AGU_DOMAINS]:
|
|
return {'list': [{'customer': GC.Values[GC.CUSTOMER_ID]}], 'queries': [None]}
|
|
return {'list': [{'domain': domain.lower()} for domain in GC.Values[GC.PRINT_AGU_DOMAINS].replace(',', ' ').split()], 'queries': [None]}
|
|
|
|
def getUserGroupDomainQueryFilters(myarg, kwargsDict):
|
|
if myarg in {'domain', 'domains'}:
|
|
kwargsDict['list'] = [{'domain': domain.lower()} for domain in getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY)]
|
|
elif myarg in {'query', 'queries'}:
|
|
kwargsDict['queries'] = getQueries(myarg)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def makeUserGroupDomainQueryFilters(kwargsDict):
|
|
kwargsQueries = []
|
|
for kwargs in kwargsDict['list']:
|
|
for query in kwargsDict['queries']:
|
|
kwargsQueries.append((kwargs, query))
|
|
return kwargsQueries
|
|
|
|
def userFilters(kwargs, query, orgUnitPath, isSuspended):
|
|
queryTitle = ''
|
|
if kwargs.get('domain'):
|
|
queryTitle += f'domain={kwargs["domain"]}, '
|
|
if orgUnitPath is not None:
|
|
if query is not None and query.find(orgUnitPath) == -1:
|
|
query += f" orgUnitPath='{orgUnitPath}'"
|
|
else:
|
|
if query is None:
|
|
query = ''
|
|
else:
|
|
query += ' '
|
|
query += f"orgUnitPath='{orgUnitPath}'"
|
|
if isSuspended is not None:
|
|
if query is None:
|
|
query = ''
|
|
else:
|
|
query += ' '
|
|
query += f'isSuspended={isSuspended}'
|
|
if query is not None:
|
|
queryTitle += f'query="{query}", '
|
|
if queryTitle:
|
|
return query, queryTitle[:-2]
|
|
return query, queryTitle
|
|
|
|
# gam print aliases|nicknames [todrive <ToDriveAttribute>*]
|
|
# ([domain|domains <DomainNameEntity>] [(query <QueryUser>)|(queries <QueryUserList>)]
|
|
# [limittoou <OrgUnitItem>])
|
|
# [user|users <EmailAddressList>] [group|groups <EmailAddressList>]
|
|
# [select <UserTypeEntity>]
|
|
# [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
|
# [shownoneditable] [nogroups] [nousers]
|
|
# [onerowpertarget] [delimiter <Character>]
|
|
# [suppressnoaliasrows]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
def doPrintAliases():
|
|
def writeAliases(target, targetEmail, targetType):
|
|
if not oneRowPerTarget:
|
|
for alias in target.get('aliases', []):
|
|
if aliasMatchPattern.match(alias):
|
|
row = {'Alias': alias, 'Target': targetEmail, 'TargetType': targetType}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
if showNonEditable:
|
|
for alias in target.get('nonEditableAliases', []):
|
|
if aliasMatchPattern.match(alias):
|
|
row = {'NonEditableAlias': alias, 'Target': targetEmail, 'TargetType': targetType}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
else:
|
|
aliases = [alias for alias in target.get('aliases', []) if aliasMatchPattern.match(alias)]
|
|
if showNonEditable:
|
|
nealiases = [alias for alias in target.get('nonEditableAliases', []) if aliasMatchPattern.match(alias)]
|
|
else:
|
|
nealiases = []
|
|
if suppressNoAliasRows and not aliases and not nealiases:
|
|
return
|
|
row = {'Target': targetEmail, 'TargetType': targetType, 'Aliases': delimiter.join(aliases)}
|
|
if showNonEditable:
|
|
row['NonEditableAliases'] = delimiter.join(nealiases)
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile()
|
|
userFields = ['primaryEmail', 'aliases']
|
|
groupFields = ['email', 'aliases']
|
|
oneRowPerTarget = showNonEditable = suppressNoAliasRows = False
|
|
kwargsDict = initUserGroupDomainQueryFilters()
|
|
getGroups = getUsers = True
|
|
groups = []
|
|
users = []
|
|
aliasMatchPattern = re.compile(r'^.*$')
|
|
isSuspended = orgUnitPath = None
|
|
addCSVData = {}
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'shownoneditable':
|
|
showNonEditable= True
|
|
userFields.append('nonEditableAliases')
|
|
groupFields.append('nonEditableAliases')
|
|
elif myarg == 'nogroups':
|
|
getGroups = False
|
|
elif myarg == 'nousers':
|
|
getUsers = False
|
|
elif myarg == 'limittoou':
|
|
orgUnitPath = getOrgUnitItem(pathOnly=True, cd=cd)
|
|
orgUnitPathLower = orgUnitPath.lower()
|
|
userFields.append('orgUnitPath')
|
|
getGroups = False
|
|
elif getUserGroupDomainQueryFilters(myarg, kwargsDict):
|
|
pass
|
|
elif myarg == 'select':
|
|
_, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
elif myarg == 'issuspended':
|
|
isSuspended = getBoolean()
|
|
elif myarg in {'user','users'}:
|
|
users.extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0)))
|
|
elif myarg in {'group', 'groups'}:
|
|
groups.extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0)))
|
|
elif myarg == 'aliasmatchpattern':
|
|
aliasMatchPattern = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'onerowpertarget':
|
|
oneRowPerTarget = True
|
|
elif myarg == 'suppressnoaliasrows':
|
|
suppressNoAliasRows = True
|
|
elif myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
else:
|
|
unknownArgumentExit()
|
|
if (users or groups) and kwargsDict['queries'][0] is None:
|
|
getUsers = getGroups = False
|
|
if not oneRowPerTarget:
|
|
titlesList = ['Alias', 'Target', 'TargetType']
|
|
if showNonEditable:
|
|
titlesList.insert(1, 'NonEditableAlias')
|
|
else:
|
|
titlesList = ['Target', 'TargetType', 'Aliases']
|
|
if showNonEditable:
|
|
titlesList.append('NonEditableAliases')
|
|
csvPF.SetTitles(titlesList)
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if getUsers:
|
|
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
|
|
kwargs = kwargsQuery[0]
|
|
query = kwargsQuery[1]
|
|
query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended)
|
|
printGettingAllAccountEntities(Ent.USER, pquery)
|
|
try:
|
|
entityList = callGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='primaryEmail',
|
|
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.INVALID_INPUT, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
|
|
GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
|
|
query=query, orderBy='email',
|
|
fields=f'nextPageToken,users({",".join(userFields)})',
|
|
maxResults=GC.Values[GC.USER_MAX_RESULTS], **kwargs)
|
|
for user in entityList:
|
|
if orgUnitPath is None or orgUnitPathLower == user.get('orgUnitPath', '').lower():
|
|
writeAliases(user, user['primaryEmail'], 'User')
|
|
except (GAPI.invalidOrgunit, GAPI.invalidInput):
|
|
entityActionFailedWarning([Ent.ALIAS, None], invalidQuery(query))
|
|
continue
|
|
except GAPI.domainNotFound as e:
|
|
entityActionFailedWarning([Ent.ALIAS, None, Ent.DOMAIN, kwargs['domain']], str(e))
|
|
continue
|
|
except (GAPI.unknownError, GAPI.failedPrecondition) as e:
|
|
entityActionFailedExit([Ent.USER, None], str(e))
|
|
except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
count = len(users)
|
|
i = 0
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
printGettingEntityItemForWhom(Ent.USER_ALIAS, user, i, count)
|
|
try:
|
|
result = callGAPI(cd.users().aliases(), 'list',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
userKey=user, fields='aliases(alias)')
|
|
aliases = {'aliases': [alias['alias'] for alias in result.get('aliases', [])]}
|
|
writeAliases(aliases, user, 'User')
|
|
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource, GAPI.conditionNotMet) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
if getGroups:
|
|
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
|
|
kwargs = kwargsQuery[0]
|
|
query = kwargsQuery[1]
|
|
query, pquery = groupFilters(kwargs, query)
|
|
printGettingAllAccountEntities(Ent.GROUP, pquery)
|
|
try:
|
|
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
query=query, orderBy='email',
|
|
fields=f'nextPageToken,groups({",".join(groupFields)})', **kwargs)
|
|
for group in entityList:
|
|
writeAliases(group, group['email'], 'Group')
|
|
except (GAPI.invalidMember, GAPI.invalidInput) as e:
|
|
if not invalidMember(query):
|
|
entityActionFailedExit([Ent.GROUP, None], str(e))
|
|
except GAPI.domainNotFound as e:
|
|
entityActionFailedWarning([Ent.ALIAS, None, Ent.DOMAIN, kwargs['domain']], str(e))
|
|
continue
|
|
except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
count = len(groups)
|
|
i = 0
|
|
for group in groups:
|
|
i += 1
|
|
group = normalizeEmailAddressOrUID(group)
|
|
printGettingEntityItemForWhom(Ent.GROUP_ALIAS, group, i, count)
|
|
try:
|
|
result = callGAPI(cd.groups().aliases(), 'list',
|
|
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
|
|
GAPI.CONDITION_NOT_MET],
|
|
groupKey=group, fields='aliases(alias)')
|
|
aliases = {'aliases': [alias['alias'] for alias in result.get('aliases', [])]}
|
|
writeAliases(aliases, group, 'Group')
|
|
except (GAPI.groupNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource, GAPI.conditionNotMet) as e:
|
|
entityActionFailedWarning([Ent.GROUP, group], str(e), i, count)
|
|
csvPF.writeCSVfile('Aliases')
|
|
|
|
# gam print addresses [todrive <ToDriveAttribute>*]
|
|
# [domain <DomainName>]
|
|
def doPrintAddresses():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
|
|
csvPF = CSVPrintFile()
|
|
titlesList = ['Type', 'Email', 'Target']
|
|
userFields = ['primaryEmail', 'aliases', 'nonEditableAliases', 'suspended']
|
|
groupFields = ['email', 'aliases', 'nonEditableAliases']
|
|
domainFields = ['domainName', 'isPrimary', 'domainAliases']
|
|
resourceFields = ['resourceEmail']
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'domain':
|
|
kwargs['domain'] = getString(Cmd.OB_DOMAIN_NAME).lower()
|
|
kwargs.pop('customer', None)
|
|
else:
|
|
unknownArgumentExit()
|
|
csvPF.SetTitles(titlesList)
|
|
printGettingAllAccountEntities(Ent.USER)
|
|
try:
|
|
entityList = callGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='primaryEmail',
|
|
throwReasons=[GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
|
|
GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
|
|
orderBy='email', fields=f'nextPageToken,users({",".join(userFields)})',
|
|
maxResults=GC.Values[GC.USER_MAX_RESULTS], **kwargs)
|
|
except (GAPI.unknownError, GAPI.failedPrecondition) as e:
|
|
entityActionFailedExit([Ent.USER, None], str(e))
|
|
except (GAPI.resourceNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
for user in entityList:
|
|
userEmail = user['primaryEmail']
|
|
prefix = '' if not user['suspended'] else 'Suspended'
|
|
csvPF.WriteRow({'Type': f'{prefix}User', 'Email': userEmail})
|
|
for alias in user.get('aliases', []):
|
|
csvPF.WriteRow({'Type': f'{prefix}UserAlias', 'Email': alias, 'Target': userEmail})
|
|
for alias in user.get('nonEditableAliases', []):
|
|
csvPF.WriteRow({'Type': f'{prefix}UserNEAlias', 'Email': alias, 'Target': userEmail})
|
|
printGettingAllAccountEntities(Ent.GROUP)
|
|
try:
|
|
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email',
|
|
throwReasons=GAPI.GROUP_LIST_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', fields=f'nextPageToken,groups({",".join(groupFields)})', **kwargs)
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
for group in entityList:
|
|
groupEmail = group['email']
|
|
csvPF.WriteRow({'Type': 'Group', 'Email': groupEmail})
|
|
for alias in group.get('aliases', []):
|
|
csvPF.WriteRow({'Type': 'GroupAlias', 'Email': alias, 'Target': groupEmail})
|
|
for alias in group.get('nonEditableAliases', []):
|
|
csvPF.WriteRow({'Type': 'GroupNEAlias', 'Email': alias, 'Target': groupEmail})
|
|
printGettingAllAccountEntities(Ent.RESOURCE_CALENDAR)
|
|
try:
|
|
entityList = callGAPIpages(cd.resources().calendars(), 'list', 'items',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='resourceEmail',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT],
|
|
customer=GC.Values[GC.CUSTOMER_ID], fields=f'nextPageToken,items({",".join(resourceFields)})')
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
except GAPI.invalidInput as e:
|
|
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, ''], str(e))
|
|
return
|
|
for resource in entityList:
|
|
csvPF.WriteRow({'Type': 'Resource', 'Email': resource['resourceEmail']})
|
|
try:
|
|
entityList = callGAPIitems(cd.domains(), 'list', 'domains',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], fields=f'domains({",".join(domainFields)})')
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
for domain in entityList:
|
|
domainEmail = domain['domainName']
|
|
csvPF.WriteRow({'Type': 'DomainPrimary' if domain['isPrimary'] else 'DomainSecondary', 'Email': domainEmail})
|
|
for alias in domain.get('domainAliases', []):
|
|
csvPF.WriteRow({'Type': 'DomainAlias', 'Email': alias['domainAliasName'], 'Target': domainEmail})
|
|
csvPF.SortRowsTwoTitles('Type', 'Email', False)
|
|
csvPF.writeCSVfile('Addresses')
|
|
|
|
# Contact commands utilities
|
|
#
|
|
def _getCreateContactReturnOptions(parameters):
|
|
myarg = getArgument()
|
|
if myarg == 'returnidonly':
|
|
parameters['returnIdOnly'] = True
|
|
elif myarg == 'csv':
|
|
parameters['csvPF'] = CSVPrintFile(parameters['titles'], 'sortall')
|
|
elif parameters['csvPF'] and myarg == 'todrive':
|
|
parameters['csvPF'].GetTodriveParameters()
|
|
elif parameters['csvPF'] and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
parameters['addCSVData'][k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
return False
|
|
return True
|
|
#
|
|
CONTACT_JSON = 'JSON'
|
|
|
|
CONTACT_ID = 'ContactID'
|
|
CONTACT_UPDATED = 'Updated'
|
|
CONTACT_NAME_PREFIX = 'Name Prefix'
|
|
CONTACT_GIVEN_NAME = 'Given Name'
|
|
CONTACT_ADDITIONAL_NAME = 'Additional Name'
|
|
CONTACT_FAMILY_NAME = 'Family Name'
|
|
CONTACT_NAME_SUFFIX = 'Name Suffix'
|
|
CONTACT_NAME = 'Name'
|
|
CONTACT_NICKNAME = 'Nickname'
|
|
CONTACT_MAIDENNAME = 'Maiden Name'
|
|
CONTACT_SHORTNAME = 'Short Name'
|
|
CONTACT_INITIALS = 'Initials'
|
|
CONTACT_BIRTHDAY = 'Birthday'
|
|
CONTACT_GENDER = 'Gender'
|
|
CONTACT_LOCATION = 'Location'
|
|
CONTACT_PRIORITY = 'Priority'
|
|
CONTACT_SENSITIVITY = 'Sensitivity'
|
|
CONTACT_SUBJECT = 'Subject'
|
|
CONTACT_LANGUAGE = 'Language'
|
|
CONTACT_NOTES = 'Notes'
|
|
CONTACT_OCCUPATION = 'Occupation'
|
|
CONTACT_BILLING_INFORMATION = 'Billing Information'
|
|
CONTACT_MILEAGE = 'Mileage'
|
|
CONTACT_DIRECTORY_SERVER = 'Directory Server'
|
|
CONTACT_ADDRESSES = 'Addresses'
|
|
CONTACT_CALENDARS = 'Calendars'
|
|
CONTACT_EMAILS = 'Emails'
|
|
CONTACT_EXTERNALIDS = 'External IDs'
|
|
CONTACT_EVENTS = 'Events'
|
|
CONTACT_HOBBIES = 'Hobbies'
|
|
CONTACT_IMS = 'IMs'
|
|
CONTACT_JOTS = 'Jots'
|
|
CONTACT_ORGANIZATIONS = 'Organizations'
|
|
CONTACT_PHONES = 'Phones'
|
|
CONTACT_RELATIONS = 'Relations'
|
|
CONTACT_USER_DEFINED_FIELDS = 'User Defined Fields'
|
|
CONTACT_WEBSITES = 'Websites'
|
|
#
|
|
class ContactsManager():
|
|
|
|
CONTACT_ARGUMENT_TO_PROPERTY_MAP = {
|
|
'json': CONTACT_JSON,
|
|
'name': CONTACT_NAME,
|
|
'prefix': CONTACT_NAME_PREFIX,
|
|
'givenname': CONTACT_GIVEN_NAME,
|
|
'additionalname': CONTACT_ADDITIONAL_NAME,
|
|
'familyname': CONTACT_FAMILY_NAME,
|
|
'firstname': CONTACT_GIVEN_NAME,
|
|
'middlename': CONTACT_ADDITIONAL_NAME,
|
|
'lastname': CONTACT_FAMILY_NAME,
|
|
'suffix': CONTACT_NAME_SUFFIX,
|
|
'nickname': CONTACT_NICKNAME,
|
|
'maidenname': CONTACT_MAIDENNAME,
|
|
'shortname': CONTACT_SHORTNAME,
|
|
'initials': CONTACT_INITIALS,
|
|
'birthday': CONTACT_BIRTHDAY,
|
|
'gender': CONTACT_GENDER,
|
|
'location': CONTACT_LOCATION,
|
|
'priority': CONTACT_PRIORITY,
|
|
'sensitivity': CONTACT_SENSITIVITY,
|
|
'subject': CONTACT_SUBJECT,
|
|
'language': CONTACT_LANGUAGE,
|
|
'note': CONTACT_NOTES,
|
|
'notes': CONTACT_NOTES,
|
|
'occupation': CONTACT_OCCUPATION,
|
|
'billinginfo': CONTACT_BILLING_INFORMATION,
|
|
'mileage': CONTACT_MILEAGE,
|
|
'directoryserver': CONTACT_DIRECTORY_SERVER,
|
|
'address': CONTACT_ADDRESSES,
|
|
'addresses': CONTACT_ADDRESSES,
|
|
'calendar': CONTACT_CALENDARS,
|
|
'calendars': CONTACT_CALENDARS,
|
|
'email': CONTACT_EMAILS,
|
|
'emails': CONTACT_EMAILS,
|
|
'externalid': CONTACT_EXTERNALIDS,
|
|
'externalids': CONTACT_EXTERNALIDS,
|
|
'event': CONTACT_EVENTS,
|
|
'events': CONTACT_EVENTS,
|
|
'hobby': CONTACT_HOBBIES,
|
|
'hobbies': CONTACT_HOBBIES,
|
|
'im': CONTACT_IMS,
|
|
'ims': CONTACT_IMS,
|
|
'jot': CONTACT_JOTS,
|
|
'jots': CONTACT_JOTS,
|
|
'organization': CONTACT_ORGANIZATIONS,
|
|
'organizations': CONTACT_ORGANIZATIONS,
|
|
'organisation': CONTACT_ORGANIZATIONS,
|
|
'organisations': CONTACT_ORGANIZATIONS,
|
|
'phone': CONTACT_PHONES,
|
|
'phones': CONTACT_PHONES,
|
|
'relation': CONTACT_RELATIONS,
|
|
'relations': CONTACT_RELATIONS,
|
|
'userdefinedfield': CONTACT_USER_DEFINED_FIELDS,
|
|
'userdefinedfields': CONTACT_USER_DEFINED_FIELDS,
|
|
'website': CONTACT_WEBSITES,
|
|
'websites': CONTACT_WEBSITES,
|
|
'updated': CONTACT_UPDATED,
|
|
}
|
|
|
|
GENDER_CHOICE_MAP = {'male': 'male', 'female': 'female'}
|
|
|
|
PRIORITY_CHOICE_MAP = {'low': 'low', 'normal': 'normal', 'high': 'high'}
|
|
|
|
SENSITIVITY_CHOICE_MAP = {
|
|
'confidential': 'confidential',
|
|
'normal': 'normal',
|
|
'personal': 'personal',
|
|
'private': 'private',
|
|
}
|
|
|
|
CONTACT_NAME_FIELDS = (
|
|
CONTACT_NAME_PREFIX,
|
|
CONTACT_GIVEN_NAME,
|
|
CONTACT_ADDITIONAL_NAME,
|
|
CONTACT_FAMILY_NAME,
|
|
CONTACT_NAME_SUFFIX,
|
|
)
|
|
|
|
ADDRESS_TYPE_ARGUMENT_TO_REL = {
|
|
'work': gdata.apps.contacts.REL_WORK,
|
|
'home': gdata.apps.contacts.REL_HOME,
|
|
'other': gdata.apps.contacts.REL_OTHER,
|
|
}
|
|
|
|
ADDRESS_REL_TO_TYPE_ARGUMENT = {
|
|
gdata.apps.contacts.REL_WORK: 'work',
|
|
gdata.apps.contacts.REL_HOME: 'home',
|
|
gdata.apps.contacts.REL_OTHER: 'other',
|
|
}
|
|
|
|
ADDRESS_ARGUMENT_TO_FIELD_MAP = {
|
|
'streetaddress': 'street',
|
|
'pobox': 'pobox',
|
|
'neighborhood': 'neighborhood',
|
|
'locality': 'city',
|
|
'region': 'region',
|
|
'postalcode': 'postcode',
|
|
'country': 'country',
|
|
'formatted': 'value', 'unstructured': 'value',
|
|
}
|
|
|
|
ADDRESS_FIELD_TO_ARGUMENT_MAP = {
|
|
'street': 'streetaddress',
|
|
'pobox': 'pobox',
|
|
'neighborhood': 'neighborhood',
|
|
'city': 'locality',
|
|
'region': 'region',
|
|
'postcode': 'postalcode',
|
|
'country': 'country',
|
|
}
|
|
|
|
ADDRESS_FIELD_PRINT_ORDER = [
|
|
'street',
|
|
'pobox',
|
|
'neighborhood',
|
|
'city',
|
|
'region',
|
|
'postcode',
|
|
'country',
|
|
]
|
|
|
|
CALENDAR_TYPE_ARGUMENT_TO_REL = {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'free-busy': 'free-busy',
|
|
}
|
|
|
|
CALENDAR_REL_TO_TYPE_ARGUMENT = {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'free-busy': 'free-busy',
|
|
}
|
|
|
|
EMAIL_TYPE_ARGUMENT_TO_REL = {
|
|
'work': gdata.apps.contacts.REL_WORK,
|
|
'home': gdata.apps.contacts.REL_HOME,
|
|
'other': gdata.apps.contacts.REL_OTHER,
|
|
}
|
|
|
|
EMAIL_REL_TO_TYPE_ARGUMENT = {
|
|
gdata.apps.contacts.REL_WORK: 'work',
|
|
gdata.apps.contacts.REL_HOME: 'home',
|
|
gdata.apps.contacts.REL_OTHER: 'other',
|
|
}
|
|
|
|
EVENT_TYPE_ARGUMENT_TO_REL = {
|
|
'anniversary': 'anniversary',
|
|
'other': 'other',
|
|
}
|
|
|
|
EVENT_REL_TO_TYPE_ARGUMENT = {
|
|
'anniversary': 'anniversary',
|
|
'other': 'other',
|
|
}
|
|
|
|
EXTERNALID_TYPE_ARGUMENT_TO_REL = {
|
|
'account': 'account',
|
|
'customer': 'customer',
|
|
'network': 'network',
|
|
'organization': 'organization',
|
|
'organisation': 'organization',
|
|
}
|
|
|
|
EXTERNALID_REL_TO_TYPE_ARGUMENT = {
|
|
'account': 'account',
|
|
'customer': 'customer',
|
|
'network': 'network',
|
|
'organization': 'organization',
|
|
'organisation': 'organization',
|
|
}
|
|
|
|
IM_TYPE_ARGUMENT_TO_REL = {
|
|
'work': gdata.apps.contacts.REL_WORK,
|
|
'home': gdata.apps.contacts.REL_HOME,
|
|
'other': gdata.apps.contacts.REL_OTHER,
|
|
}
|
|
|
|
IM_REL_TO_TYPE_ARGUMENT = {
|
|
gdata.apps.contacts.REL_WORK: 'work',
|
|
gdata.apps.contacts.REL_HOME: 'home',
|
|
gdata.apps.contacts.REL_OTHER: 'other',
|
|
}
|
|
|
|
IM_PROTOCOL_TO_REL_MAP = {
|
|
'aim': gdata.apps.contacts.IM_AIM,
|
|
'gtalk': gdata.apps.contacts.IM_GOOGLE_TALK,
|
|
'icq': gdata.apps.contacts.IM_ICQ,
|
|
'jabber': gdata.apps.contacts.IM_JABBER,
|
|
'msn': gdata.apps.contacts.IM_MSN,
|
|
'netmeeting': gdata.apps.contacts.IM_NETMEETING,
|
|
'qq': gdata.apps.contacts.IM_QQ,
|
|
'skype': gdata.apps.contacts.IM_SKYPE,
|
|
'xmpp': gdata.apps.contacts.IM_JABBER,
|
|
'yahoo': gdata.apps.contacts.IM_YAHOO,
|
|
}
|
|
|
|
IM_REL_TO_PROTOCOL_MAP = {
|
|
gdata.apps.contacts.IM_AIM: 'aim',
|
|
gdata.apps.contacts.IM_GOOGLE_TALK: 'gtalk',
|
|
gdata.apps.contacts.IM_ICQ: 'icq',
|
|
gdata.apps.contacts.IM_JABBER: 'jabber',
|
|
gdata.apps.contacts.IM_MSN: 'msn',
|
|
gdata.apps.contacts.IM_NETMEETING: 'netmeeting',
|
|
gdata.apps.contacts.IM_QQ: 'qq',
|
|
gdata.apps.contacts.IM_SKYPE: 'skype',
|
|
gdata.apps.contacts.IM_YAHOO: 'yahoo',
|
|
}
|
|
|
|
JOT_TYPE_ARGUMENT_TO_REL = {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'other': 'other',
|
|
'keywords': 'keywords',
|
|
'user': 'user',
|
|
}
|
|
|
|
JOT_REL_TO_TYPE_ARGUMENT = {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'other': 'other',
|
|
'keywords': 'keywords',
|
|
'user': 'user',
|
|
}
|
|
|
|
ORGANIZATION_TYPE_ARGUMENT_TO_REL = {
|
|
'work': gdata.apps.contacts.REL_WORK,
|
|
'other': gdata.apps.contacts.REL_OTHER,
|
|
}
|
|
|
|
ORGANIZATION_REL_TO_TYPE_ARGUMENT = {
|
|
gdata.apps.contacts.REL_WORK: 'work',
|
|
gdata.apps.contacts.REL_OTHER: 'other',
|
|
}
|
|
|
|
ORGANIZATION_ARGUMENT_TO_FIELD_MAP = {
|
|
'location': 'where',
|
|
'department': 'department',
|
|
'title': 'title',
|
|
'jobdescription': 'jobdescription',
|
|
'symbol': 'symbol',
|
|
}
|
|
|
|
ORGANIZATION_FIELD_TO_ARGUMENT_MAP = {
|
|
'where': 'location',
|
|
'department': 'department',
|
|
'title': 'title',
|
|
'jobdescription': 'jobdescription',
|
|
'symbol': 'symbol',
|
|
}
|
|
|
|
ORGANIZATION_FIELD_PRINT_ORDER = [
|
|
'where',
|
|
'department',
|
|
'title',
|
|
'jobdescription',
|
|
'symbol',
|
|
]
|
|
|
|
PHONE_TYPE_ARGUMENT_TO_REL = {
|
|
'work': gdata.apps.contacts.PHONE_WORK,
|
|
'home': gdata.apps.contacts.PHONE_HOME,
|
|
'other': gdata.apps.contacts.PHONE_OTHER,
|
|
'fax': gdata.apps.contacts.PHONE_FAX,
|
|
'home_fax': gdata.apps.contacts.PHONE_HOME_FAX,
|
|
'work_fax': gdata.apps.contacts.PHONE_WORK_FAX,
|
|
'other_fax': gdata.apps.contacts.PHONE_OTHER_FAX,
|
|
'main': gdata.apps.contacts.PHONE_MAIN,
|
|
'company_main': gdata.apps.contacts.PHONE_COMPANY_MAIN,
|
|
'assistant': gdata.apps.contacts.PHONE_ASSISTANT,
|
|
'mobile': gdata.apps.contacts.PHONE_MOBILE,
|
|
'work_mobile': gdata.apps.contacts.PHONE_WORK_MOBILE,
|
|
'pager': gdata.apps.contacts.PHONE_PAGER,
|
|
'work_pager': gdata.apps.contacts.PHONE_WORK_PAGER,
|
|
'car': gdata.apps.contacts.PHONE_CAR,
|
|
'radio': gdata.apps.contacts.PHONE_RADIO,
|
|
'callback': gdata.apps.contacts.PHONE_CALLBACK,
|
|
'isdn': gdata.apps.contacts.PHONE_ISDN,
|
|
'telex': gdata.apps.contacts.PHONE_TELEX,
|
|
'tty_tdd': gdata.apps.contacts.PHONE_TTY_TDD,
|
|
}
|
|
|
|
PHONE_REL_TO_TYPE_ARGUMENT = {
|
|
gdata.apps.contacts.PHONE_WORK: 'work',
|
|
gdata.apps.contacts.PHONE_HOME: 'home',
|
|
gdata.apps.contacts.PHONE_OTHER: 'other',
|
|
gdata.apps.contacts.PHONE_FAX: 'fax',
|
|
gdata.apps.contacts.PHONE_HOME_FAX: 'home_fax',
|
|
gdata.apps.contacts.PHONE_WORK_FAX: 'work_fax',
|
|
gdata.apps.contacts.PHONE_OTHER_FAX: 'other_fax',
|
|
gdata.apps.contacts.PHONE_MAIN: 'main',
|
|
gdata.apps.contacts.PHONE_COMPANY_MAIN: 'company_main',
|
|
gdata.apps.contacts.PHONE_ASSISTANT: 'assistant',
|
|
gdata.apps.contacts.PHONE_MOBILE: 'mobile',
|
|
gdata.apps.contacts.PHONE_WORK_MOBILE: 'work_mobile',
|
|
gdata.apps.contacts.PHONE_PAGER: 'pager',
|
|
gdata.apps.contacts.PHONE_WORK_PAGER: 'work_pager',
|
|
gdata.apps.contacts.PHONE_CAR: 'car',
|
|
gdata.apps.contacts.PHONE_RADIO: 'radio',
|
|
gdata.apps.contacts.PHONE_CALLBACK: 'callback',
|
|
gdata.apps.contacts.PHONE_ISDN: 'isdn',
|
|
gdata.apps.contacts.PHONE_TELEX: 'telex',
|
|
gdata.apps.contacts.PHONE_TTY_TDD: 'tty_tdd',
|
|
}
|
|
|
|
RELATION_TYPE_ARGUMENT_TO_REL = {
|
|
'spouse': 'spouse',
|
|
'child': 'child',
|
|
'mother': 'mother',
|
|
'father': 'father',
|
|
'parent': 'parent',
|
|
'brother': 'brother',
|
|
'sister': 'sister',
|
|
'friend': 'friend',
|
|
'relative': 'relative',
|
|
'manager': 'manager',
|
|
'assistant': 'assistant',
|
|
'referredby': 'referred-by',
|
|
'partner': 'partner',
|
|
'domesticpartner': 'domestic-partner',
|
|
}
|
|
|
|
RELATION_REL_TO_TYPE_ARGUMENT = {
|
|
'spouse' : 'spouse',
|
|
'child' : 'child',
|
|
'mother' : 'mother',
|
|
'father' : 'father',
|
|
'parent' : 'parent',
|
|
'brother' : 'brother',
|
|
'sister' : 'sister',
|
|
'friend' : 'friend',
|
|
'relative' : 'relative',
|
|
'manager' : 'manager',
|
|
'assistant' : 'assistant',
|
|
'referred-by' : 'referred_by',
|
|
'partner' : 'partner',
|
|
'domestic-partner' : 'domestic_partner',
|
|
}
|
|
|
|
WEBSITE_TYPE_ARGUMENT_TO_REL = {
|
|
'home-page': 'home-page',
|
|
'blog': 'blog',
|
|
'profile': 'profile',
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'other': 'other',
|
|
'ftp': 'ftp',
|
|
'reservations': 'reservations',
|
|
'app-install-page': 'app-install-page',
|
|
}
|
|
|
|
WEBSITE_REL_TO_TYPE_ARGUMENT = {
|
|
'home-page': 'home-page',
|
|
'blog': 'blog',
|
|
'profile': 'profile',
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'other': 'other',
|
|
'ftp': 'ftp',
|
|
'reservations': 'reservations',
|
|
'app-install-page': 'app-install-page',
|
|
}
|
|
|
|
CONTACT_NAME_PROPERTY_PRINT_ORDER = [
|
|
CONTACT_UPDATED,
|
|
CONTACT_NAME,
|
|
CONTACT_NAME_PREFIX,
|
|
CONTACT_GIVEN_NAME,
|
|
CONTACT_ADDITIONAL_NAME,
|
|
CONTACT_FAMILY_NAME,
|
|
CONTACT_NAME_SUFFIX,
|
|
CONTACT_NICKNAME,
|
|
CONTACT_MAIDENNAME,
|
|
CONTACT_SHORTNAME,
|
|
CONTACT_INITIALS,
|
|
CONTACT_BIRTHDAY,
|
|
CONTACT_GENDER,
|
|
CONTACT_LOCATION,
|
|
CONTACT_PRIORITY,
|
|
CONTACT_SENSITIVITY,
|
|
CONTACT_SUBJECT,
|
|
CONTACT_LANGUAGE,
|
|
CONTACT_NOTES,
|
|
CONTACT_OCCUPATION,
|
|
CONTACT_BILLING_INFORMATION,
|
|
CONTACT_MILEAGE,
|
|
CONTACT_DIRECTORY_SERVER,
|
|
]
|
|
|
|
CONTACT_ARRAY_PROPERTY_PRINT_ORDER = [
|
|
CONTACT_ADDRESSES,
|
|
CONTACT_EMAILS,
|
|
CONTACT_IMS,
|
|
CONTACT_PHONES,
|
|
CONTACT_CALENDARS,
|
|
CONTACT_ORGANIZATIONS,
|
|
CONTACT_EXTERNALIDS,
|
|
CONTACT_EVENTS,
|
|
CONTACT_HOBBIES,
|
|
CONTACT_JOTS,
|
|
CONTACT_RELATIONS,
|
|
CONTACT_WEBSITES,
|
|
CONTACT_USER_DEFINED_FIELDS,
|
|
]
|
|
|
|
CONTACT_ARRAY_PROPERTIES = {
|
|
CONTACT_ADDRESSES: {'relMap': ADDRESS_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'formatted', 'primary': True},
|
|
CONTACT_EMAILS: {'relMap': EMAIL_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'address', 'primary': True},
|
|
CONTACT_IMS: {'relMap': IM_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'address', 'primary': True},
|
|
CONTACT_PHONES: {'relMap': PHONE_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': True},
|
|
CONTACT_CALENDARS: {'relMap': CALENDAR_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'address', 'primary': True},
|
|
CONTACT_ORGANIZATIONS: {'relMap': ORGANIZATION_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'name', 'primary': True},
|
|
CONTACT_EXTERNALIDS: {'relMap': EXTERNALID_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': False},
|
|
CONTACT_EVENTS: {'relMap': EVENT_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'date', 'primary': False},
|
|
CONTACT_HOBBIES: {'relMap': None, 'infoTitle': 'value', 'primary': False},
|
|
CONTACT_JOTS: {'relMap': JOT_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': False},
|
|
CONTACT_RELATIONS: {'relMap': RELATION_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': False},
|
|
CONTACT_USER_DEFINED_FIELDS: {'relMap': None, 'infoTitle': 'value', 'primary': False},
|
|
CONTACT_WEBSITES: {'relMap': WEBSITE_REL_TO_TYPE_ARGUMENT, 'infoTitle': 'value', 'primary': True},
|
|
}
|
|
|
|
@staticmethod
|
|
def GetContactShortId(contactEntry):
|
|
full_id = contactEntry.id.text
|
|
return full_id[full_id.rfind('/')+1:]
|
|
|
|
@staticmethod
|
|
def GetContactFields(parameters=None):
|
|
|
|
fields = {}
|
|
|
|
def CheckClearFieldsList(fieldName):
|
|
if checkArgumentPresent(Cmd.CLEAR_NONE_ARGUMENT):
|
|
fields.pop(fieldName, None)
|
|
fields[fieldName] = []
|
|
return True
|
|
return False
|
|
|
|
def InitArrayItem(choices):
|
|
item = {}
|
|
rel = getChoice(choices, mapChoice=True, defaultChoice=None)
|
|
if rel:
|
|
item['rel'] = rel
|
|
item['label'] = None
|
|
else:
|
|
item['rel'] = None
|
|
item['label'] = getString(Cmd.OB_STRING)
|
|
return item
|
|
|
|
def PrimaryNotPrimary(pnp, entry):
|
|
if pnp == 'notprimary':
|
|
entry['primary'] = 'false'
|
|
return True
|
|
if pnp == 'primary':
|
|
entry['primary'] = 'true'
|
|
primary['location'] = Cmd.Location()
|
|
return True
|
|
return False
|
|
|
|
def GetPrimaryNotPrimaryChoice(entry):
|
|
if not getChoice({'primary': True, 'notprimary': False}, mapChoice=True):
|
|
entry['primary'] = 'false'
|
|
else:
|
|
entry['primary'] = 'true'
|
|
primary['location'] = Cmd.Location()
|
|
|
|
def AppendItemToFieldsList(fieldName, fieldValue, checkBlankField=None):
|
|
fields.setdefault(fieldName, [])
|
|
if checkBlankField is None or fieldValue[checkBlankField]:
|
|
if isinstance(fieldValue, dict) and fieldValue.get('primary', 'false') == 'true':
|
|
for citem in fields[fieldName]:
|
|
if citem.get('primary', 'false') == 'true':
|
|
Cmd.SetLocation(primary['location']-1)
|
|
usageErrorExit(Msg.MULTIPLE_ITEMS_MARKED_PRIMARY.format(fieldName))
|
|
fields[fieldName].append(fieldValue)
|
|
|
|
primary = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
if parameters is not None:
|
|
if _getCreateContactReturnOptions(parameters):
|
|
continue
|
|
Cmd.Backup()
|
|
fieldName = getChoice(ContactsManager.CONTACT_ARGUMENT_TO_PROPERTY_MAP, mapChoice=True)
|
|
if fieldName == CONTACT_BIRTHDAY:
|
|
fields[fieldName] = getYYYYMMDD(minLen=0)
|
|
elif fieldName == CONTACT_GENDER:
|
|
fields[fieldName] = getChoice(ContactsManager.GENDER_CHOICE_MAP, mapChoice=True)
|
|
elif fieldName == CONTACT_PRIORITY:
|
|
fields[fieldName] = getChoice(ContactsManager.PRIORITY_CHOICE_MAP, mapChoice=True)
|
|
elif fieldName == CONTACT_SENSITIVITY:
|
|
fields[fieldName] = getChoice(ContactsManager.SENSITIVITY_CHOICE_MAP, mapChoice=True)
|
|
elif fieldName == CONTACT_LANGUAGE:
|
|
fields[fieldName] = getLanguageCode(LANGUAGE_CODES_MAP)
|
|
elif fieldName == CONTACT_NOTES:
|
|
fields[fieldName] = getStringWithCRsNLsOrFile()[0]
|
|
elif fieldName == CONTACT_ADDRESSES:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.ADDRESS_TYPE_ARGUMENT_TO_REL)
|
|
entry['primary'] = 'false'
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument in ContactsManager.ADDRESS_ARGUMENT_TO_FIELD_MAP:
|
|
value = getString(Cmd.OB_STRING, minLen=0)
|
|
if value:
|
|
entry[ContactsManager.ADDRESS_ARGUMENT_TO_FIELD_MAP[argument]] = value.replace('\\n', '\n')
|
|
elif PrimaryNotPrimary(argument, entry):
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
AppendItemToFieldsList(fieldName, entry)
|
|
elif fieldName == CONTACT_CALENDARS:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.CALENDAR_TYPE_ARGUMENT_TO_REL)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
GetPrimaryNotPrimaryChoice(entry)
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_EMAILS:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.EMAIL_TYPE_ARGUMENT_TO_REL)
|
|
entry['value'] = getEmailAddress(noUid=True, minLen=0)
|
|
GetPrimaryNotPrimaryChoice(entry)
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_EVENTS:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.EVENT_TYPE_ARGUMENT_TO_REL)
|
|
entry['value'] = getYYYYMMDD(minLen=0)
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_EXTERNALIDS:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.EXTERNALID_TYPE_ARGUMENT_TO_REL)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_HOBBIES:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = {'value': getString(Cmd.OB_STRING, minLen=0)}
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_IMS:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.IM_TYPE_ARGUMENT_TO_REL)
|
|
entry['protocol'] = getChoice(ContactsManager.IM_PROTOCOL_TO_REL_MAP, mapChoice=True)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
GetPrimaryNotPrimaryChoice(entry)
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_JOTS:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = {'rel': getChoice(ContactsManager.JOT_TYPE_ARGUMENT_TO_REL, mapChoice=True)}
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_ORGANIZATIONS:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.ORGANIZATION_TYPE_ARGUMENT_TO_REL)
|
|
entry['primary'] = 'false'
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument in ContactsManager.ORGANIZATION_ARGUMENT_TO_FIELD_MAP:
|
|
value = getString(Cmd.OB_STRING, minLen=0)
|
|
if value:
|
|
entry[ContactsManager.ORGANIZATION_ARGUMENT_TO_FIELD_MAP[argument]] = value
|
|
elif PrimaryNotPrimary(argument, entry):
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_PHONES:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.PHONE_TYPE_ARGUMENT_TO_REL)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
GetPrimaryNotPrimaryChoice(entry)
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_RELATIONS:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.RELATION_TYPE_ARGUMENT_TO_REL)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_USER_DEFINED_FIELDS:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = {'rel': getString(Cmd.OB_STRING, minLen=0), 'value': getString(Cmd.OB_STRING, minLen=0)}
|
|
if not entry['rel'] or entry['rel'].lower() == 'none':
|
|
entry['rel'] = None
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
elif fieldName == CONTACT_WEBSITES:
|
|
if CheckClearFieldsList(fieldName):
|
|
continue
|
|
entry = InitArrayItem(ContactsManager.WEBSITE_TYPE_ARGUMENT_TO_REL)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
GetPrimaryNotPrimaryChoice(entry)
|
|
AppendItemToFieldsList(fieldName, entry, 'value')
|
|
else:
|
|
fields[fieldName] = getString(Cmd.OB_STRING, minLen=0)
|
|
return fields
|
|
|
|
@staticmethod
|
|
def FieldsToContact(fields):
|
|
def GetField(fieldName):
|
|
return fields.get(fieldName)
|
|
|
|
def SetClassAttribute(value, fieldClass, processNLs, attr):
|
|
if value:
|
|
if processNLs:
|
|
value = value.replace('\\n', '\n')
|
|
if attr == 'text':
|
|
return fieldClass(text=value)
|
|
if attr == 'code':
|
|
return fieldClass(code=value)
|
|
if attr == 'rel':
|
|
return fieldClass(rel=value)
|
|
if attr == 'value':
|
|
return fieldClass(value=value)
|
|
if attr == 'value_string':
|
|
return fieldClass(value_string=value)
|
|
if attr == 'when':
|
|
return fieldClass(when=value)
|
|
return None
|
|
|
|
def GetContactField(fieldName, fieldClass, processNLs=False, attr='text'):
|
|
return SetClassAttribute(fields.get(fieldName), fieldClass, processNLs, attr)
|
|
|
|
def GetListEntryField(entry, fieldName, fieldClass, processNLs=False, attr='text'):
|
|
return SetClassAttribute(entry.get(fieldName), fieldClass, processNLs, attr)
|
|
|
|
contactEntry = gdata.apps.contacts.ContactEntry()
|
|
value = GetField(CONTACT_NAME)
|
|
if not value:
|
|
value = ' '.join([fields[fieldName] for fieldName in ContactsManager.CONTACT_NAME_FIELDS if fieldName in fields])
|
|
contactEntry.name = gdata.apps.contacts.Name(full_name=gdata.apps.contacts.FullName(text=value))
|
|
contactEntry.name.name_prefix = GetContactField(CONTACT_NAME_PREFIX, gdata.apps.contacts.NamePrefix)
|
|
contactEntry.name.given_name = GetContactField(CONTACT_GIVEN_NAME, gdata.apps.contacts.GivenName)
|
|
contactEntry.name.additional_name = GetContactField(CONTACT_ADDITIONAL_NAME, gdata.apps.contacts.AdditionalName)
|
|
contactEntry.name.family_name = GetContactField(CONTACT_FAMILY_NAME, gdata.apps.contacts.FamilyName)
|
|
contactEntry.name.name_suffix = GetContactField(CONTACT_NAME_SUFFIX, gdata.apps.contacts.NameSuffix)
|
|
contactEntry.nickname = GetContactField(CONTACT_NICKNAME, gdata.apps.contacts.Nickname)
|
|
contactEntry.maidenName = GetContactField(CONTACT_MAIDENNAME, gdata.apps.contacts.MaidenName)
|
|
contactEntry.shortName = GetContactField(CONTACT_SHORTNAME, gdata.apps.contacts.ShortName)
|
|
contactEntry.initials = GetContactField(CONTACT_INITIALS, gdata.apps.contacts.Initials)
|
|
contactEntry.birthday = GetContactField(CONTACT_BIRTHDAY, gdata.apps.contacts.Birthday, attr='when')
|
|
contactEntry.gender = GetContactField(CONTACT_GENDER, gdata.apps.contacts.Gender, attr='value')
|
|
contactEntry.where = GetContactField(CONTACT_LOCATION, gdata.apps.contacts.Where, attr='value_string')
|
|
contactEntry.priority = GetContactField(CONTACT_PRIORITY, gdata.apps.contacts.Priority, attr='rel')
|
|
contactEntry.sensitivity = GetContactField(CONTACT_SENSITIVITY, gdata.apps.contacts.Sensitivity, attr='rel')
|
|
contactEntry.subject = GetContactField(CONTACT_SUBJECT, gdata.apps.contacts.Subject)
|
|
contactEntry.language = GetContactField(CONTACT_LANGUAGE, gdata.apps.contacts.Language, attr='code')
|
|
contactEntry.content = GetContactField(CONTACT_NOTES, gdata.apps.contacts.Content, processNLs=True)
|
|
contactEntry.occupation = GetContactField(CONTACT_OCCUPATION, gdata.apps.contacts.Occupation)
|
|
contactEntry.billingInformation = GetContactField(CONTACT_BILLING_INFORMATION, gdata.apps.contacts.BillingInformation, processNLs=True)
|
|
contactEntry.mileage = GetContactField(CONTACT_MILEAGE, gdata.apps.contacts.Mileage)
|
|
contactEntry.directoryServer = GetContactField(CONTACT_DIRECTORY_SERVER, gdata.apps.contacts.DirectoryServer)
|
|
value = GetField(CONTACT_ADDRESSES)
|
|
if value:
|
|
for address in value:
|
|
street = GetListEntryField(address, 'street', gdata.apps.contacts.Street)
|
|
pobox = GetListEntryField(address, 'pobox', gdata.apps.contacts.PoBox)
|
|
neighborhood = GetListEntryField(address, 'neighborhood', gdata.apps.contacts.Neighborhood)
|
|
city = GetListEntryField(address, 'city', gdata.apps.contacts.City)
|
|
region = GetListEntryField(address, 'region', gdata.apps.contacts.Region)
|
|
postcode = GetListEntryField(address, 'postcode', gdata.apps.contacts.Postcode)
|
|
country = GetListEntryField(address, 'country', gdata.apps.contacts.Country)
|
|
formatted_address = GetListEntryField(address, 'value', gdata.apps.contacts.FormattedAddress, processNLs=True)
|
|
contactEntry.structuredPostalAddress.append(gdata.apps.contacts.StructuredPostalAddress(street=street, pobox=pobox, neighborhood=neighborhood,
|
|
city=city, region=region,
|
|
postcode=postcode, country=country,
|
|
formatted_address=formatted_address,
|
|
rel=address['rel'], label=address['label'], primary=address['primary']))
|
|
value = GetField(CONTACT_CALENDARS)
|
|
if value:
|
|
for calendarLink in value:
|
|
contactEntry.calendarLink.append(gdata.apps.contacts.CalendarLink(href=calendarLink['value'], rel=calendarLink['rel'], label=calendarLink['label'], primary=calendarLink['primary']))
|
|
value = GetField(CONTACT_EMAILS)
|
|
if value:
|
|
for emailaddr in value:
|
|
contactEntry.email.append(gdata.apps.contacts.Email(address=emailaddr['value'], rel=emailaddr['rel'], label=emailaddr['label'], primary=emailaddr['primary']))
|
|
value = GetField(CONTACT_EXTERNALIDS)
|
|
if value:
|
|
for externalid in value:
|
|
contactEntry.externalId.append(gdata.apps.contacts.ExternalId(value=externalid['value'], rel=externalid['rel'], label=externalid['label']))
|
|
value = GetField(CONTACT_EVENTS)
|
|
if value:
|
|
for event in value:
|
|
contactEntry.event.append(gdata.apps.contacts.Event(rel=event['rel'], label=event['label'],
|
|
when=gdata.apps.contacts.When(startTime=event['value'])))
|
|
value = GetField(CONTACT_HOBBIES)
|
|
if value:
|
|
for hobby in value:
|
|
contactEntry.hobby.append(gdata.apps.contacts.Hobby(text=hobby['value']))
|
|
value = GetField(CONTACT_IMS)
|
|
if value:
|
|
for im in value:
|
|
contactEntry.im.append(gdata.apps.contacts.IM(address=im['value'], protocol=im['protocol'], rel=im['rel'], label=im['label'], primary=im['primary']))
|
|
value = GetField(CONTACT_JOTS)
|
|
if value:
|
|
for jot in value:
|
|
contactEntry.jot.append(gdata.apps.contacts.Jot(text=jot['value'], rel=jot['rel']))
|
|
value = GetField(CONTACT_ORGANIZATIONS)
|
|
if value:
|
|
for organization in value:
|
|
org_name = gdata.apps.contacts.OrgName(text=organization['value'])
|
|
department = GetListEntryField(organization, 'department', gdata.apps.contacts.OrgDepartment)
|
|
title = GetListEntryField(organization, 'title', gdata.apps.contacts.OrgTitle)
|
|
job_description = GetListEntryField(organization, 'jobdescription', gdata.apps.contacts.OrgJobDescription)
|
|
symbol = GetListEntryField(organization, 'symbol', gdata.apps.contacts.OrgSymbol)
|
|
where = GetListEntryField(organization, 'where', gdata.apps.contacts.Where, attr='value_string')
|
|
contactEntry.organization.append(gdata.apps.contacts.Organization(name=org_name, department=department,
|
|
title=title, job_description=job_description,
|
|
symbol=symbol, where=where,
|
|
rel=organization['rel'], label=organization['label'], primary=organization['primary']))
|
|
value = GetField(CONTACT_PHONES)
|
|
if value:
|
|
for phone in value:
|
|
contactEntry.phoneNumber.append(gdata.apps.contacts.PhoneNumber(text=phone['value'], rel=phone['rel'], label=phone['label'], primary=phone['primary']))
|
|
value = GetField(CONTACT_RELATIONS)
|
|
if value:
|
|
for relation in value:
|
|
contactEntry.relation.append(gdata.apps.contacts.Relation(text=relation['value'], rel=relation['rel'], label=relation['label']))
|
|
value = GetField(CONTACT_USER_DEFINED_FIELDS)
|
|
if value:
|
|
for userdefinedfield in value:
|
|
contactEntry.userDefinedField.append(gdata.apps.contacts.UserDefinedField(key=userdefinedfield['rel'], value=userdefinedfield['value']))
|
|
value = GetField(CONTACT_WEBSITES)
|
|
if value:
|
|
for website in value:
|
|
contactEntry.website.append(gdata.apps.contacts.Website(href=website['value'], rel=website['rel'], label=website['label'], primary=website['primary']))
|
|
return contactEntry
|
|
|
|
@staticmethod
|
|
def ContactToFields(contactEntry):
|
|
fields = {}
|
|
def GetContactField(fieldName, attrlist):
|
|
objAttr = contactEntry
|
|
for attr in attrlist:
|
|
objAttr = getattr(objAttr, attr)
|
|
if not objAttr:
|
|
return
|
|
fields[fieldName] = objAttr
|
|
|
|
def GetListEntryField(entry, attrlist):
|
|
objAttr = entry
|
|
for attr in attrlist:
|
|
objAttr = getattr(objAttr, attr)
|
|
if not objAttr:
|
|
return None
|
|
return objAttr
|
|
|
|
def AppendItemToFieldsList(fieldName, fieldValue):
|
|
fields.setdefault(fieldName, [])
|
|
fields[fieldName].append(fieldValue)
|
|
|
|
fields[CONTACT_ID] = ContactsManager.GetContactShortId(contactEntry)
|
|
GetContactField(CONTACT_UPDATED, ['updated', 'text'])
|
|
if not contactEntry.deleted:
|
|
GetContactField(CONTACT_NAME, ['title', 'text'])
|
|
else:
|
|
fields[CONTACT_NAME] = 'Deleted'
|
|
GetContactField(CONTACT_NAME_PREFIX, ['name', 'name_prefix', 'text'])
|
|
GetContactField(CONTACT_GIVEN_NAME, ['name', 'given_name', 'text'])
|
|
GetContactField(CONTACT_ADDITIONAL_NAME, ['name', 'additional_name', 'text'])
|
|
GetContactField(CONTACT_FAMILY_NAME, ['name', 'family_name', 'text'])
|
|
GetContactField(CONTACT_NAME_SUFFIX, ['name', 'name_suffix', 'text'])
|
|
GetContactField(CONTACT_NICKNAME, ['nickname', 'text'])
|
|
GetContactField(CONTACT_MAIDENNAME, ['maidenName', 'text'])
|
|
GetContactField(CONTACT_SHORTNAME, ['shortName', 'text'])
|
|
GetContactField(CONTACT_INITIALS, ['initials', 'text'])
|
|
GetContactField(CONTACT_BIRTHDAY, ['birthday', 'when'])
|
|
GetContactField(CONTACT_GENDER, ['gender', 'value'])
|
|
GetContactField(CONTACT_SUBJECT, ['subject', 'text'])
|
|
GetContactField(CONTACT_LANGUAGE, ['language', 'code'])
|
|
GetContactField(CONTACT_PRIORITY, ['priority', 'rel'])
|
|
GetContactField(CONTACT_SENSITIVITY, ['sensitivity', 'rel'])
|
|
GetContactField(CONTACT_NOTES, ['content', 'text'])
|
|
GetContactField(CONTACT_LOCATION, ['where', 'value_string'])
|
|
GetContactField(CONTACT_OCCUPATION, ['occupation', 'text'])
|
|
GetContactField(CONTACT_BILLING_INFORMATION, ['billingInformation', 'text'])
|
|
GetContactField(CONTACT_MILEAGE, ['mileage', 'text'])
|
|
GetContactField(CONTACT_DIRECTORY_SERVER, ['directoryServer', 'text'])
|
|
for address in contactEntry.structuredPostalAddress:
|
|
AppendItemToFieldsList(CONTACT_ADDRESSES,
|
|
{'rel': address.rel,
|
|
'label': address.label,
|
|
'value': GetListEntryField(address, ['formatted_address', 'text']),
|
|
'street': GetListEntryField(address, ['street', 'text']),
|
|
'pobox': GetListEntryField(address, ['pobox', 'text']),
|
|
'neighborhood': GetListEntryField(address, ['neighborhood', 'text']),
|
|
'city': GetListEntryField(address, ['city', 'text']),
|
|
'region': GetListEntryField(address, ['region', 'text']),
|
|
'postcode': GetListEntryField(address, ['postcode', 'text']),
|
|
'country': GetListEntryField(address, ['country', 'text']),
|
|
'primary': address.primary})
|
|
for calendarLink in contactEntry.calendarLink:
|
|
AppendItemToFieldsList(CONTACT_CALENDARS,
|
|
{'rel': calendarLink.rel,
|
|
'label': calendarLink.label,
|
|
'value': calendarLink.href,
|
|
'primary': calendarLink.primary})
|
|
for emailaddr in contactEntry.email:
|
|
AppendItemToFieldsList(CONTACT_EMAILS,
|
|
{'rel': emailaddr.rel,
|
|
'label': emailaddr.label,
|
|
'value': emailaddr.address,
|
|
'primary': emailaddr.primary})
|
|
for externalid in contactEntry.externalId:
|
|
AppendItemToFieldsList(CONTACT_EXTERNALIDS,
|
|
{'rel': externalid.rel,
|
|
'label': externalid.label,
|
|
'value': externalid.value})
|
|
for event in contactEntry.event:
|
|
AppendItemToFieldsList(CONTACT_EVENTS,
|
|
{'rel': event.rel,
|
|
'label': event.label,
|
|
'value': GetListEntryField(event, ['when', 'startTime'])})
|
|
for hobby in contactEntry.hobby:
|
|
AppendItemToFieldsList(CONTACT_HOBBIES,
|
|
{'value': hobby.text})
|
|
for im in contactEntry.im:
|
|
AppendItemToFieldsList(CONTACT_IMS,
|
|
{'rel': im.rel,
|
|
'label': im.label,
|
|
'value': im.address,
|
|
'protocol': im.protocol,
|
|
'primary': im.primary})
|
|
for jot in contactEntry.jot:
|
|
AppendItemToFieldsList(CONTACT_JOTS,
|
|
{'rel': jot.rel,
|
|
'value': jot.text})
|
|
for organization in contactEntry.organization:
|
|
AppendItemToFieldsList(CONTACT_ORGANIZATIONS,
|
|
{'rel': organization.rel,
|
|
'label': organization.label,
|
|
'value': GetListEntryField(organization, ['name', 'text']),
|
|
'department': GetListEntryField(organization, ['department', 'text']),
|
|
'title': GetListEntryField(organization, ['title', 'text']),
|
|
'symbol': GetListEntryField(organization, ['symbol', 'text']),
|
|
'jobdescription': GetListEntryField(organization, ['job_description', 'text']),
|
|
'where': GetListEntryField(organization, ['where', 'value_string']),
|
|
'primary': organization.primary})
|
|
for phone in contactEntry.phoneNumber:
|
|
AppendItemToFieldsList(CONTACT_PHONES,
|
|
{'rel': phone.rel,
|
|
'label': phone.label,
|
|
'value': phone.text,
|
|
'primary': phone.primary})
|
|
for relation in contactEntry.relation:
|
|
AppendItemToFieldsList(CONTACT_RELATIONS,
|
|
{'rel': relation.rel,
|
|
'label': relation.label,
|
|
'value': relation.text})
|
|
for userdefinedfield in contactEntry.userDefinedField:
|
|
AppendItemToFieldsList(CONTACT_USER_DEFINED_FIELDS,
|
|
{'rel': userdefinedfield.key,
|
|
'value': userdefinedfield.value})
|
|
for website in contactEntry.website:
|
|
AppendItemToFieldsList(CONTACT_WEBSITES,
|
|
{'rel': website.rel,
|
|
'label': website.label,
|
|
'value': website.href,
|
|
'primary': website.primary})
|
|
return fields
|
|
|
|
CONTACTS_PROJECTION_CHOICE_MAP = {'basic': 'thin', 'thin': 'thin', 'full': 'full'}
|
|
CONTACTS_ORDERBY_CHOICE_MAP = {'lastmodified': 'lastmodified'}
|
|
|
|
def normalizeContactId(contactId):
|
|
if contactId.startswith('id:'):
|
|
return contactId[3:]
|
|
return contactId
|
|
|
|
def _initContactQueryAttributes():
|
|
return {'query': None, 'projection': 'full', 'url_params': {'max-results': str(GC.Values[GC.CONTACT_MAX_RESULTS])},
|
|
'emailMatchPattern': None, 'emailMatchType': None}
|
|
|
|
def _getContactQueryAttributes(contactQuery, myarg, unknownAction, printShowCmd):
|
|
if myarg == 'query':
|
|
contactQuery['query'] = getString(Cmd.OB_QUERY)
|
|
elif myarg == 'emailmatchpattern':
|
|
contactQuery['emailMatchPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'emailmatchtype':
|
|
contactQuery['emailMatchType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
|
|
elif myarg == 'updatedmin':
|
|
contactQuery['url_params']['updated-min'] = getYYYYMMDD()
|
|
elif myarg == 'endquery':
|
|
return False
|
|
elif not printShowCmd:
|
|
if unknownAction < 0:
|
|
unknownArgumentExit()
|
|
if unknownAction > 0:
|
|
Cmd.Backup()
|
|
return False
|
|
elif myarg == 'orderby':
|
|
contactQuery['url_params']['orderby'], contactQuery['url_params']['sortorder'] = getOrderBySortOrder(CONTACTS_ORDERBY_CHOICE_MAP, 'ascending', False)
|
|
elif myarg in CONTACTS_PROJECTION_CHOICE_MAP:
|
|
contactQuery['projection'] = CONTACTS_PROJECTION_CHOICE_MAP[myarg]
|
|
elif myarg == 'showdeleted':
|
|
contactQuery['url_params']['showdeleted'] = 'true'
|
|
else:
|
|
if unknownAction < 0:
|
|
unknownArgumentExit()
|
|
if unknownAction > 0:
|
|
Cmd.Backup()
|
|
return False
|
|
return True
|
|
|
|
CONTACT_SELECT_ARGUMENTS = {'query', 'emailmatchpattern', 'emailmatchtype', 'updatedmin'}
|
|
|
|
def _getContactEntityList(unknownAction, printShowCmd):
|
|
contactQuery = _initContactQueryAttributes()
|
|
if Cmd.PeekArgumentPresent(CONTACT_SELECT_ARGUMENTS):
|
|
entityList = None
|
|
queriedContacts = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if not _getContactQueryAttributes(contactQuery, myarg, unknownAction, printShowCmd):
|
|
break
|
|
else:
|
|
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
|
|
queriedContacts = False
|
|
if unknownAction < 0:
|
|
checkForExtraneousArguments()
|
|
return (entityList, contactQuery, queriedContacts)
|
|
|
|
def queryContacts(contactsObject, contactQuery):
|
|
entityType = Ent.DOMAIN
|
|
user = GC.Values[GC.DOMAIN]
|
|
if contactQuery['query']:
|
|
uri = getContactsQuery(feed=contactsObject.GetContactFeedUri(contact_list=user, projection=contactQuery['projection']),
|
|
text_query=contactQuery['query']).ToUri()
|
|
else:
|
|
uri = contactsObject.GetContactFeedUri(contact_list=user, projection=contactQuery['projection'])
|
|
printGettingAllEntityItemsForWhom(Ent.CONTACT, user, query=contactQuery['query'])
|
|
try:
|
|
entityList = callGDataPages(contactsObject, 'GetContactsFeed',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwErrors=[GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
uri=uri, url_params=contactQuery['url_params'])
|
|
return entityList
|
|
except GDATA.badRequest as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, ''], str(e))
|
|
except GDATA.forbidden:
|
|
entityServiceNotApplicableWarning(entityType, user)
|
|
return None
|
|
|
|
def localContactSelects(contactsManager, contactQuery, fields):
|
|
if contactQuery['emailMatchPattern']:
|
|
emailMatchType = contactQuery['emailMatchType']
|
|
for item in fields.get(CONTACT_EMAILS, []):
|
|
if contactQuery['emailMatchPattern'].match(item['value']):
|
|
if (not emailMatchType or
|
|
emailMatchType == item.get('label') or
|
|
emailMatchType == contactsManager.CONTACT_ARRAY_PROPERTIES[CONTACT_EMAILS]['relMap'].get(item['rel'], 'custom')):
|
|
break
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def countLocalContactSelects(contactsManager, contacts, contactQuery):
|
|
if contacts is not None and contactQuery:
|
|
jcount = 0
|
|
for contact in contacts:
|
|
fields = contactsManager.ContactToFields(contact)
|
|
if localContactSelects(contactsManager, contactQuery, fields):
|
|
jcount += 1
|
|
else:
|
|
jcount = len(contacts) if contacts is not None else 0
|
|
return jcount
|
|
|
|
def clearEmailAddressMatches(contactsManager, contactClear, fields):
|
|
savedAddresses = []
|
|
updateRequired = False
|
|
emailMatchType = contactClear['emailClearType']
|
|
for item in fields.get(CONTACT_EMAILS, []):
|
|
if (contactClear['emailClearPattern'].match(item['value']) and
|
|
(not emailMatchType or
|
|
emailMatchType == item.get('label') or
|
|
emailMatchType == contactsManager.CONTACT_ARRAY_PROPERTIES[CONTACT_EMAILS]['relMap'].get(item['rel'], 'custom'))):
|
|
updateRequired = True
|
|
else:
|
|
savedAddresses.append(item)
|
|
if updateRequired:
|
|
fields[CONTACT_EMAILS] = savedAddresses
|
|
return updateRequired
|
|
|
|
def dedupEmailAddressMatches(contactsManager, emailMatchType, fields):
|
|
sai = -1
|
|
savedAddresses = []
|
|
matches = {}
|
|
updateRequired = False
|
|
for item in fields.get(CONTACT_EMAILS, []):
|
|
emailAddr = item['value']
|
|
emailType = item.get('label')
|
|
if emailType is None:
|
|
emailType = contactsManager.CONTACT_ARRAY_PROPERTIES[CONTACT_EMAILS]['relMap'].get(item['rel'], 'custom')
|
|
if (emailAddr in matches) and (not emailMatchType or emailType in matches[emailAddr]['types']):
|
|
if item['primary'] == 'true':
|
|
savedAddresses[matches[emailAddr]['sai']]['primary'] = 'true'
|
|
updateRequired = True
|
|
else:
|
|
savedAddresses.append(item)
|
|
sai += 1
|
|
matches.setdefault(emailAddr, {'types': set(), 'sai': sai})
|
|
matches[emailAddr]['types'].add(emailType)
|
|
if updateRequired:
|
|
fields[CONTACT_EMAILS] = savedAddresses
|
|
return updateRequired
|
|
|
|
def _createContact():
|
|
entityType = Ent.DOMAIN
|
|
contactsManager = ContactsManager()
|
|
parameters = {'csvPF': None, 'titles': ['Domain', CONTACT_ID], 'addCSVData': {}, 'returnIdOnly': False}
|
|
fields = contactsManager.GetContactFields(parameters)
|
|
csvPF = parameters['csvPF']
|
|
addCSVData = parameters['addCSVData']
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
returnIdOnly = parameters['returnIdOnly']
|
|
contactEntry = contactsManager.FieldsToContact(fields)
|
|
user, contactsObject = getContactsObject(True)
|
|
try:
|
|
contact = callGData(contactsObject, 'CreateContact',
|
|
throwErrors=[GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
new_contact=contactEntry, insert_uri=contactsObject.GetContactFeedUri(contact_list=user))
|
|
contactId = contactsManager.GetContactShortId(contact)
|
|
if returnIdOnly:
|
|
writeStdout(f'{contactId}\n')
|
|
elif not csvPF:
|
|
entityActionPerformed([entityType, user, Ent.CONTACT, contactId])
|
|
else:
|
|
row = {'Domain': user, CONTACT_ID: contactId}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
except GDATA.badRequest as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, ''], str(e))
|
|
except GDATA.forbidden:
|
|
entityServiceNotApplicableWarning(entityType, user)
|
|
except GDATA.serviceNotApplicable:
|
|
entityUnknownWarning(entityType, user)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Contacts')
|
|
|
|
# gam create contact <ContactAttribute>+
|
|
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*))| returnidonly]
|
|
def doCreateDomainContact():
|
|
_createContact()
|
|
|
|
def _clearUpdateContacts(updateContacts):
|
|
entityType = Ent.DOMAIN
|
|
contactsManager = ContactsManager()
|
|
entityList, contactQuery, queriedContacts = _getContactEntityList(1, False)
|
|
if updateContacts:
|
|
update_fields = contactsManager.GetContactFields()
|
|
else:
|
|
contactClear = {'emailClearPattern': contactQuery['emailMatchPattern'], 'emailClearType': contactQuery['emailMatchType']}
|
|
deleteClearedContactsWithNoEmails = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'emailclearpattern':
|
|
contactClear['emailClearPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'emailcleartype':
|
|
contactClear['emailClearType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
|
|
elif myarg == 'deleteclearedcontactswithnoemails':
|
|
deleteClearedContactsWithNoEmails = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not contactClear['emailClearPattern']:
|
|
missingArgumentExit('emailclearpattern')
|
|
user, contactsObject = getContactsObject(True)
|
|
if queriedContacts:
|
|
entityList = queryContacts(contactsObject, contactQuery)
|
|
if entityList is None:
|
|
return
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.CONTACT)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
try:
|
|
if not queriedContacts:
|
|
contactId = normalizeContactId(contact)
|
|
contact = callGData(contactsObject, 'GetContact',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN, GDATA.NOT_IMPLEMENTED],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId))
|
|
fields = contactsManager.ContactToFields(contact)
|
|
else:
|
|
contactId = contactsManager.GetContactShortId(contact)
|
|
fields = contactsManager.ContactToFields(contact)
|
|
if not localContactSelects(contactsManager, contactQuery, fields):
|
|
continue
|
|
if updateContacts:
|
|
for field, value in iter(update_fields.items()):
|
|
fields[field] = value
|
|
contactEntry = contactsManager.FieldsToContact(fields)
|
|
else:
|
|
if not clearEmailAddressMatches(contactsManager, contactClear, fields):
|
|
continue
|
|
if deleteClearedContactsWithNoEmails and not fields[CONTACT_EMAILS]:
|
|
Act.Set(Act.DELETE)
|
|
callGData(contactsObject, 'DeleteContact',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
|
|
edit_uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId), extra_headers={'If-Match': contact.etag})
|
|
entityActionPerformed([entityType, user, Ent.CONTACT, contactId], j, jcount)
|
|
continue
|
|
contactEntry = contactsManager.FieldsToContact(fields)
|
|
contactEntry.category = contact.category
|
|
contactEntry.link = contact.link
|
|
contactEntry.etag = contact.etag
|
|
contactEntry.id = contact.id
|
|
Act.Set(Act.UPDATE)
|
|
callGData(contactsObject, 'UpdateContact',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.PRECONDITION_FAILED, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
|
|
edit_uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId), updated_contact=contactEntry, extra_headers={'If-Match': contact.etag})
|
|
entityActionPerformed([entityType, user, Ent.CONTACT, contactId], j, jcount)
|
|
except (GDATA.notFound, GDATA.badRequest, GDATA.preconditionFailed) as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId], str(e), j, jcount)
|
|
except (GDATA.forbidden, GDATA.notImplemented):
|
|
entityServiceNotApplicableWarning(entityType, user)
|
|
break
|
|
except GDATA.serviceNotApplicable:
|
|
entityUnknownWarning(entityType, user)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam clear contacts <ContactEntity>|<ContactSelection>
|
|
# [clearmatchpattern <REMatchPattern>] [clearmatchtype work|home|other|<String>]
|
|
# [deleteclearedcontactswithnoemails]
|
|
def doClearDomainContacts():
|
|
_clearUpdateContacts(False)
|
|
|
|
# gam update contacts <ContactEntity>|<ContactSelection> <ContactAttribute>+
|
|
def doUpdateDomainContacts():
|
|
_clearUpdateContacts(True)
|
|
|
|
def _dedupContacts():
|
|
entityType = Ent.DOMAIN
|
|
contactsManager = ContactsManager()
|
|
contactQuery = _initContactQueryAttributes()
|
|
emailMatchType = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matchtype':
|
|
emailMatchType = getBoolean()
|
|
else:
|
|
_getContactQueryAttributes(contactQuery, myarg, -1, False)
|
|
user, contactsObject = getContactsObject(True)
|
|
contacts = queryContacts(contactsObject, contactQuery)
|
|
if contacts is None:
|
|
return
|
|
j = 0
|
|
jcount = len(contacts)
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.CONTACT)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return
|
|
Ind.Increment()
|
|
for contact in contacts:
|
|
j += 1
|
|
try:
|
|
fields = contactsManager.ContactToFields(contact)
|
|
if not localContactSelects(contactsManager, contactQuery, fields):
|
|
continue
|
|
if not dedupEmailAddressMatches(contactsManager, emailMatchType, fields):
|
|
continue
|
|
contactId = fields[CONTACT_ID]
|
|
contactEntry = contactsManager.FieldsToContact(fields)
|
|
contactEntry.category = contact.category
|
|
contactEntry.link = contact.link
|
|
contactEntry.etag = contact.etag
|
|
contactEntry.id = contact.id
|
|
Act.Set(Act.UPDATE)
|
|
callGData(contactsObject, 'UpdateContact',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.PRECONDITION_FAILED, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
|
|
edit_uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId), updated_contact=contactEntry, extra_headers={'If-Match': contact.etag})
|
|
entityActionPerformed([entityType, user, Ent.CONTACT, contactId], j, jcount)
|
|
except (GDATA.notFound, GDATA.badRequest, GDATA.preconditionFailed) as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId], str(e), j, jcount)
|
|
except (GDATA.forbidden, GDATA.notImplemented):
|
|
entityServiceNotApplicableWarning(entityType, user)
|
|
break
|
|
except GDATA.serviceNotApplicable:
|
|
entityUnknownWarning(entityType, user)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam dedup contacts <ContactEntity>|<ContactSelection> [matchType [<Boolean>]]
|
|
def doDedupDomainContacts():
|
|
_dedupContacts()
|
|
|
|
def _deleteContacts():
|
|
entityType = Ent.DOMAIN
|
|
contactsManager = ContactsManager()
|
|
entityList, contactQuery, queriedContacts = _getContactEntityList(-1, False)
|
|
user, contactsObject = getContactsObject(True)
|
|
if queriedContacts:
|
|
entityList = queryContacts(contactsObject, contactQuery)
|
|
if entityList is None:
|
|
return
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.CONTACT)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
try:
|
|
if not queriedContacts:
|
|
contactId = normalizeContactId(contact)
|
|
contact = callGData(contactsObject, 'GetContact',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN, GDATA.NOT_IMPLEMENTED],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId))
|
|
else:
|
|
contactId = contactsManager.GetContactShortId(contact)
|
|
fields = contactsManager.ContactToFields(contact)
|
|
if not localContactSelects(contactsManager, contactQuery, fields):
|
|
continue
|
|
callGData(contactsObject, 'DeleteContact',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN],
|
|
edit_uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId), extra_headers={'If-Match': contact.etag})
|
|
entityActionPerformed([entityType, user, Ent.CONTACT, contactId], j, jcount)
|
|
except (GDATA.notFound, GDATA.badRequest) as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId], str(e), j, jcount)
|
|
except (GDATA.forbidden, GDATA.notImplemented):
|
|
entityServiceNotApplicableWarning(entityType, user)
|
|
break
|
|
except GDATA.serviceNotApplicable:
|
|
entityUnknownWarning(entityType, user)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam delete contacts <ContactEntity>|<ContactSelection>
|
|
def doDeleteDomainContacts():
|
|
_deleteContacts()
|
|
|
|
CONTACT_TIME_OBJECTS = {CONTACT_UPDATED}
|
|
CONTACT_FIELDS_WITH_CRS_NLS = {CONTACT_NOTES, CONTACT_BILLING_INFORMATION}
|
|
|
|
def _showContact(contactsManager, fields, displayFieldsList, j, jcount, FJQC):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(fields, timeObjects=CONTACT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CONTACT, fields[CONTACT_ID]], j, jcount)
|
|
Ind.Increment()
|
|
for key in contactsManager.CONTACT_NAME_PROPERTY_PRINT_ORDER:
|
|
if displayFieldsList and key not in displayFieldsList:
|
|
continue
|
|
if key in fields:
|
|
if key in CONTACT_TIME_OBJECTS:
|
|
printKeyValueList([key, formatLocalTime(fields[key])])
|
|
elif key not in CONTACT_FIELDS_WITH_CRS_NLS:
|
|
printKeyValueList([key, fields[key]])
|
|
else:
|
|
printKeyValueWithCRsNLs(key, fields[key])
|
|
for key in contactsManager.CONTACT_ARRAY_PROPERTY_PRINT_ORDER:
|
|
if displayFieldsList and key not in displayFieldsList:
|
|
continue
|
|
if key in fields:
|
|
keymap = contactsManager.CONTACT_ARRAY_PROPERTIES[key]
|
|
printKeyValueList([key, None])
|
|
Ind.Increment()
|
|
for item in fields[key]:
|
|
fn = item.get('label')
|
|
if keymap['relMap']:
|
|
if not fn:
|
|
fn = keymap['relMap'].get(item['rel'], 'custom')
|
|
printKeyValueList(['type', fn])
|
|
Ind.Increment()
|
|
if keymap['primary']:
|
|
printKeyValueList(['rank', ['notprimary', 'primary'][item['primary'] == 'true']])
|
|
value = item['value']
|
|
if value is None:
|
|
value = ''
|
|
if key == CONTACT_IMS:
|
|
printKeyValueList(['protocol', contactsManager.IM_REL_TO_PROTOCOL_MAP.get(item['protocol'], item['protocol'])])
|
|
printKeyValueList([keymap['infoTitle'], value])
|
|
elif key == CONTACT_ADDRESSES:
|
|
printKeyValueWithCRsNLs(keymap['infoTitle'], value)
|
|
for org_key in contactsManager.ADDRESS_FIELD_PRINT_ORDER:
|
|
if item[org_key]:
|
|
printKeyValueList([contactsManager.ADDRESS_FIELD_TO_ARGUMENT_MAP[org_key], item[org_key]])
|
|
elif key == CONTACT_ORGANIZATIONS:
|
|
printKeyValueList([keymap['infoTitle'], value])
|
|
for org_key in contactsManager.ORGANIZATION_FIELD_PRINT_ORDER:
|
|
if item[org_key]:
|
|
printKeyValueList([contactsManager.ORGANIZATION_FIELD_TO_ARGUMENT_MAP[org_key], item[org_key]])
|
|
elif key == CONTACT_USER_DEFINED_FIELDS:
|
|
printKeyValueList([item.get('rel') or 'None', value])
|
|
else:
|
|
printKeyValueList([keymap['infoTitle'], value])
|
|
if keymap['relMap']:
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
def _getContactFieldsList(contactsManager, displayFieldsList):
|
|
for field in _getFieldsList():
|
|
if field in contactsManager.CONTACT_ARGUMENT_TO_PROPERTY_MAP:
|
|
displayFieldsList.append(contactsManager.CONTACT_ARGUMENT_TO_PROPERTY_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, contactsManager.CONTACT_ARGUMENT_TO_PROPERTY_MAP, True)
|
|
|
|
def _infoContacts(contactFeed):
|
|
entityType = Ent.DOMAIN
|
|
contactsManager = ContactsManager()
|
|
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
|
|
contactQuery = _initContactQueryAttributes()
|
|
FJQC = FormatJSONQuoteChar()
|
|
displayFieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in CONTACTS_PROJECTION_CHOICE_MAP:
|
|
contactQuery['projection'] = CONTACTS_PROJECTION_CHOICE_MAP[myarg]
|
|
elif myarg == 'fields':
|
|
_getContactFieldsList(contactsManager, displayFieldsList)
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
user, contactsObject = getContactsObject(contactFeed)
|
|
j = 0
|
|
jcount = len(entityList)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([entityType, user], jcount, Ent.CONTACT)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
try:
|
|
contactId = normalizeContactId(contact)
|
|
contact = callGData(contactsObject, 'GetContact',
|
|
bailOnInternalServerError=True,
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE,
|
|
GDATA.FORBIDDEN, GDATA.NOT_IMPLEMENTED, GDATA.INTERNAL_SERVER_ERROR],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
uri=contactsObject.GetContactFeedUri(contact_list=user, contactId=contactId, projection=contactQuery['projection']))
|
|
fields = contactsManager.ContactToFields(contact)
|
|
_showContact(contactsManager, fields, displayFieldsList, j, jcount, FJQC)
|
|
except (GDATA.notFound, GDATA.badRequest, GDATA.forbidden, GDATA.notImplemented, GDATA.internalServerError) as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId], str(e), j, jcount)
|
|
except GDATA.serviceNotApplicable:
|
|
entityUnknownWarning(entityType, user)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam info contacts <ContactEntity>
|
|
# [basic|full]
|
|
# [fields <ContactFieldNameList>] [formatjson]
|
|
def doInfoDomainContacts():
|
|
_infoContacts(True)
|
|
|
|
# gam info gal <ContactEntity>
|
|
# [basic|full]
|
|
# [fields <ContactFieldNameList>] [formatjson]
|
|
def doInfoGAL():
|
|
_infoContacts(False)
|
|
|
|
def _printShowContacts(contactFeed):
|
|
entityType = Ent.DOMAIN
|
|
entityTypeName = Ent.Singular(entityType)
|
|
contactsManager = ContactsManager()
|
|
csvPF = CSVPrintFile([entityTypeName, CONTACT_ID, CONTACT_NAME], 'sortall',
|
|
contactsManager.CONTACT_ARRAY_PROPERTY_PRINT_ORDER) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
CSVTitle = 'Contacts'
|
|
contactQuery = _initContactQueryAttributes()
|
|
countsOnly = False
|
|
displayFieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'fields':
|
|
_getContactFieldsList(contactsManager, displayFieldsList)
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
contactQuery['projection'] = CONTACTS_PROJECTION_CHOICE_MAP['basic']
|
|
if csvPF:
|
|
csvPF.SetTitles([entityTypeName, CSVTitle])
|
|
elif _getContactQueryAttributes(contactQuery, myarg, 0, True):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
user, contactsObject = getContactsObject(contactFeed)
|
|
contacts = queryContacts(contactsObject, contactQuery)
|
|
if countsOnly:
|
|
jcount = countLocalContactSelects(contactsManager, contacts, contactQuery)
|
|
if csvPF:
|
|
csvPF.WriteRowTitles({entityTypeName: user, CSVTitle: jcount})
|
|
else:
|
|
printEntityKVList([entityType, user], [CSVTitle, jcount])
|
|
elif contacts is not None:
|
|
jcount = len(contacts)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.CONTACT)
|
|
Ind.Increment()
|
|
j = 0
|
|
for contact in contacts:
|
|
j += 1
|
|
fields = contactsManager.ContactToFields(contact)
|
|
if not localContactSelects(contactsManager, contactQuery, fields):
|
|
continue
|
|
_showContact(contactsManager, fields, displayFieldsList, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
elif contacts:
|
|
for contact in contacts:
|
|
fields = contactsManager.ContactToFields(contact)
|
|
if not localContactSelects(contactsManager, contactQuery, fields):
|
|
continue
|
|
contactRow = {entityTypeName: user, CONTACT_ID: fields[CONTACT_ID]}
|
|
for key in contactsManager.CONTACT_NAME_PROPERTY_PRINT_ORDER:
|
|
if displayFieldsList and key not in displayFieldsList:
|
|
continue
|
|
if key in fields:
|
|
if key == CONTACT_UPDATED:
|
|
contactRow[key] = formatLocalTime(fields[key])
|
|
elif key not in (CONTACT_NOTES, CONTACT_BILLING_INFORMATION):
|
|
contactRow[key] = fields[key]
|
|
else:
|
|
contactRow[key] = escapeCRsNLs(fields[key])
|
|
for key in contactsManager.CONTACT_ARRAY_PROPERTY_PRINT_ORDER:
|
|
if displayFieldsList and key not in displayFieldsList:
|
|
continue
|
|
if key in fields:
|
|
keymap = contactsManager.CONTACT_ARRAY_PROPERTIES[key]
|
|
j = 0
|
|
contactRow[f'{key}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}count'] = len(fields[key])
|
|
for item in fields[key]:
|
|
j += 1
|
|
fn = f'{key}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}'
|
|
fnt = item.get('label')
|
|
if fnt:
|
|
contactRow[fn+'type'] = fnt
|
|
elif keymap['relMap']:
|
|
contactRow[fn+'type'] = keymap['relMap'].get(item['rel'], 'custom')
|
|
if keymap['primary']:
|
|
contactRow[fn+'rank'] = 'primary' if item['primary'] == 'true' else 'notprimary'
|
|
value = item['value']
|
|
if value is None:
|
|
value = ''
|
|
if key == CONTACT_IMS:
|
|
contactRow[fn+'protocol'] = contactsManager.IM_REL_TO_PROTOCOL_MAP.get(item['protocol'], item['protocol'])
|
|
contactRow[fn+keymap['infoTitle']] = value
|
|
elif key == CONTACT_ADDRESSES:
|
|
contactRow[fn+keymap['infoTitle']] = escapeCRsNLs(value)
|
|
for org_key in contactsManager.ADDRESS_FIELD_PRINT_ORDER:
|
|
if item[org_key]:
|
|
contactRow[fn+contactsManager.ADDRESS_FIELD_TO_ARGUMENT_MAP[org_key]] = escapeCRsNLs(item[org_key])
|
|
elif key == CONTACT_ORGANIZATIONS:
|
|
contactRow[fn+keymap['infoTitle']] = value
|
|
for org_key in contactsManager.ORGANIZATION_FIELD_PRINT_ORDER:
|
|
if item[org_key]:
|
|
contactRow[fn+contactsManager.ORGANIZATION_FIELD_TO_ARGUMENT_MAP[org_key]] = item[org_key]
|
|
elif key == CONTACT_USER_DEFINED_FIELDS:
|
|
contactRow[fn+'type'] = item.get('rel') or 'None'
|
|
contactRow[fn+keymap['infoTitle']] = value
|
|
else:
|
|
contactRow[fn+keymap['infoTitle']] = value
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(contactRow)
|
|
elif csvPF.CheckRowTitles(contactRow):
|
|
csvPF.WriteRowNoFilter({entityTypeName: user, CONTACT_ID: fields[CONTACT_ID],
|
|
CONTACT_NAME: fields.get(CONTACT_NAME, ''),
|
|
'JSON': json.dumps(cleanJSON(fields, timeObjects=CONTACT_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile(CSVTitle)
|
|
|
|
# gam print contacts [todrive <ToDriveAttribute>*] <ContactSelection>
|
|
# [basic|full|countsonly] [showdeleted] [orderby <ContactOrderByFieldName> [ascending|descending]]
|
|
# [fields <ContactFieldNameList>] [formatjson [quotechar <Character>]]
|
|
# gam show contacts <ContactSelection>
|
|
# [basic|full|countsonly] [showdeleted] [orderby <ContactOrderByFieldName> [ascending|descending]]
|
|
# [fields <ContactFieldNameList>] [formatjson]
|
|
def doPrintShowDomainContacts():
|
|
_printShowContacts(True)
|
|
|
|
# gam print gal [todrive <ToDriveAttribute>*] <ContactSelection>
|
|
# [basic|full] [orderby <ContactOrderByFieldName> [ascending|descending]]
|
|
# [fields <ContactFieldNameList>] [formatjson [quotechar <Character>]]
|
|
# gam show gal <ContactSelection>
|
|
# [basic|full|countsonly] [orderby <ContactOrderByFieldName> [ascending|descending]]
|
|
# [fields <ContactFieldNameList>] [formatjson]
|
|
def doPrintShowGAL():
|
|
_printShowContacts(False)
|
|
|
|
# Prople commands utilities
|
|
#
|
|
def normalizePeopleResourceName(resourceName):
|
|
if resourceName.startswith('people/'):
|
|
return resourceName
|
|
return f'people/{resourceName}'
|
|
|
|
def normalizeContactGroupResourceName(resourceName):
|
|
if resourceName.startswith('contactGroups/'):
|
|
return resourceName
|
|
return f'contactGroups/{resourceName}'
|
|
|
|
def normalizeOtherContactsResourceName(resourceName):
|
|
if resourceName.startswith('otherContacts/'):
|
|
return resourceName
|
|
return f'otherContacts/{resourceName}'
|
|
|
|
PEOPLE_JSON = 'JSON'
|
|
|
|
PEOPLE_ADDRESSES = 'addresses'
|
|
PEOPLE_BIOGRAPHIES = 'biographies'
|
|
PEOPLE_BIRTHDAYS = 'birthdays'
|
|
PEOPLE_CALENDAR_URLS = 'calendarUrls'
|
|
PEOPLE_CLIENT_DATA = 'clientData'
|
|
PEOPLE_COVER_PHOTOS = 'coverPhotos'
|
|
PEOPLE_EMAIL_ADDRESSES = 'emailAddresses'
|
|
PEOPLE_EVENTS = 'events'
|
|
PEOPLE_EXTERNAL_IDS = 'externalIds'
|
|
PEOPLE_FILE_ASES = 'fileAses'
|
|
PEOPLE_GENDERS = 'genders'
|
|
PEOPLE_IM_CLIENTS = 'imClients'
|
|
PEOPLE_INTERESTS = 'interests'
|
|
PEOPLE_LOCALES = 'locales'
|
|
PEOPLE_LOCATIONS = 'locations'
|
|
PEOPLE_MEMBERSHIPS = 'memberships'
|
|
PEOPLE_METADATA = 'metadata'
|
|
PEOPLE_MISC_KEYWORDS = 'miscKeywords'
|
|
PEOPLE_MISC_KEYWORDS_BILLING_INFORMATION = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_BILLING_INFORMATION'
|
|
PEOPLE_MISC_KEYWORDS_DIRECTORY_SERVER = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_DIRECTORY_SERVER'
|
|
PEOPLE_MISC_KEYWORDS_JOT = PEOPLE_MISC_KEYWORDS+'.jot'
|
|
PEOPLE_MISC_KEYWORDS_MILEAGE = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_MILEAGE'
|
|
PEOPLE_MISC_KEYWORDS_PRIORITY = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_PRIORITY'
|
|
PEOPLE_MISC_KEYWORDS_SENSITIVITY = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_SENSITIVITY'
|
|
PEOPLE_MISC_KEYWORDS_SUBJECT = PEOPLE_MISC_KEYWORDS+'.OUTLOOK_SUBJECT'
|
|
PEOPLE_NAMES = 'names'
|
|
PEOPLE_NAMES_FAMILY_NAME = PEOPLE_NAMES+'.familyName'
|
|
PEOPLE_NAMES_GIVEN_NAME = PEOPLE_NAMES+'.givenName'
|
|
PEOPLE_NAMES_HONORIFIC_PREFIX = PEOPLE_NAMES+'.honorificPrefix'
|
|
PEOPLE_NAMES_HONORIFIC_SUFFIX = PEOPLE_NAMES+'.honorificSuffix'
|
|
PEOPLE_NAMES_MIDDLE_NAME = PEOPLE_NAMES+'.middleName'
|
|
PEOPLE_NAMES_PHONETIC_FAMILY_NAME = PEOPLE_NAMES+'.phoneticFamilyName'
|
|
PEOPLE_NAMES_PHONETIC_GIVEN_NAME = PEOPLE_NAMES+'.phoneticGivenName'
|
|
PEOPLE_NAMES_PHONETIC_HONORIFIC_PREFIX = PEOPLE_NAMES+'.phoneticHonorificPrefix'
|
|
PEOPLE_NAMES_PHONETIC_HONORIFIC_SUFFIX = PEOPLE_NAMES+'.phoneticHonorificSuffix'
|
|
PEOPLE_NAMES_PHONETIC_MIDDLE_NAME = PEOPLE_NAMES+'.phoneticMiddleName'
|
|
PEOPLE_NAMES_UNSTRUCTURED_NAME = PEOPLE_NAMES+'.unstructuredName'
|
|
PEOPLE_NICKNAMES = 'nicknames'
|
|
PEOPLE_NICKNAMES_INITIALS = PEOPLE_NICKNAMES+'.INITIALS'
|
|
PEOPLE_NICKNAMES_MAIDENNAME = PEOPLE_NICKNAMES+'.MAIDEN_NAME'
|
|
PEOPLE_NICKNAMES_NICKNAME = PEOPLE_NICKNAMES+'.DEFAULT'
|
|
PEOPLE_NICKNAMES_SHORTNAME = PEOPLE_NICKNAMES+'.SHORT_NAME'
|
|
PEOPLE_OCCUPATIONS = 'occupations'
|
|
PEOPLE_ORGANIZATIONS = 'organizations'
|
|
PEOPLE_PHONE_NUMBERS = 'phoneNumbers'
|
|
PEOPLE_PHOTOS = 'photos'
|
|
PEOPLE_RELATIONS = 'relations'
|
|
PEOPLE_SIP_ADDRESSES = 'sipAddresses'
|
|
PEOPLE_SKILLS = 'skills'
|
|
PEOPLE_UPDATE_TIME = 'updateTime'
|
|
PEOPLE_URLS = 'urls'
|
|
PEOPLE_USER_DEFINED = 'userDefined'
|
|
|
|
PEOPLE_GROUPS = 'ContactGroups'
|
|
PEOPLE_GROUPS_LIST = 'ContactGroupsList'
|
|
PEOPLE_ADD_GROUPS = 'ContactAddGroups'
|
|
PEOPLE_ADD_GROUPS_LIST = 'ContactAddGroupsList'
|
|
PEOPLE_REMOVE_GROUPS = 'ContactRemoveGroups'
|
|
PEOPLE_REMOVE_GROUPS_LIST = 'ContactRemoveGroupsList'
|
|
|
|
PEOPLE_GROUP_NAME = 'name'
|
|
PEOPLE_GROUP_CLIENT_DATA = 'clientData'
|
|
#
|
|
class PeopleManager():
|
|
PEOPLE_ARGUMENT_TO_PROPERTY_MAP = {
|
|
'json': PEOPLE_JSON,
|
|
'additionalname': PEOPLE_NAMES_MIDDLE_NAME,
|
|
'address': PEOPLE_ADDRESSES,
|
|
'addresses': PEOPLE_ADDRESSES,
|
|
'billinginfo': PEOPLE_MISC_KEYWORDS_BILLING_INFORMATION,
|
|
'biography': PEOPLE_BIOGRAPHIES,
|
|
'biographies': PEOPLE_BIOGRAPHIES,
|
|
'birthday': PEOPLE_BIRTHDAYS,
|
|
'birthdays': PEOPLE_BIRTHDAYS,
|
|
'calendar': PEOPLE_CALENDAR_URLS,
|
|
'calendars': PEOPLE_CALENDAR_URLS,
|
|
'clientdata': PEOPLE_CLIENT_DATA,
|
|
# 'coverphoto': PEOPLE_COVER_PHOTOS,
|
|
# 'coverphotos': PEOPLE_COVER_PHOTOS,
|
|
'directoryserver': PEOPLE_MISC_KEYWORDS_DIRECTORY_SERVER,
|
|
'email': PEOPLE_EMAIL_ADDRESSES,
|
|
'emails': PEOPLE_EMAIL_ADDRESSES,
|
|
'emailadresses': PEOPLE_EMAIL_ADDRESSES,
|
|
'event': PEOPLE_EVENTS,
|
|
'events': PEOPLE_EVENTS,
|
|
'externalid': PEOPLE_EXTERNAL_IDS,
|
|
'externalids': PEOPLE_EXTERNAL_IDS,
|
|
'familyname': PEOPLE_NAMES_FAMILY_NAME,
|
|
'fileas': PEOPLE_FILE_ASES,
|
|
'firstname': PEOPLE_NAMES_GIVEN_NAME,
|
|
'gender': PEOPLE_GENDERS,
|
|
'genders': PEOPLE_GENDERS,
|
|
'givenname': PEOPLE_NAMES_GIVEN_NAME,
|
|
'hobby': PEOPLE_INTERESTS,
|
|
'hobbies': PEOPLE_INTERESTS,
|
|
'im': PEOPLE_IM_CLIENTS,
|
|
'ims': PEOPLE_IM_CLIENTS,
|
|
'imclients': 'imClients',
|
|
'initials': PEOPLE_NICKNAMES_INITIALS,
|
|
'interests': PEOPLE_INTERESTS,
|
|
'jot': PEOPLE_MISC_KEYWORDS_JOT,
|
|
'jots': PEOPLE_MISC_KEYWORDS_JOT,
|
|
'language': PEOPLE_LOCALES,
|
|
'lastname': PEOPLE_NAMES_FAMILY_NAME,
|
|
'locale': PEOPLE_LOCALES,
|
|
'location': PEOPLE_LOCATIONS,
|
|
'locations': PEOPLE_LOCATIONS,
|
|
'maidenname': PEOPLE_NICKNAMES_MAIDENNAME,
|
|
'middlename': PEOPLE_NAMES_MIDDLE_NAME,
|
|
'mileage': PEOPLE_MISC_KEYWORDS_MILEAGE,
|
|
'misckeywords': PEOPLE_MISC_KEYWORDS,
|
|
'name': PEOPLE_NAMES_UNSTRUCTURED_NAME,
|
|
'names': PEOPLE_NAMES_UNSTRUCTURED_NAME,
|
|
'nickname': PEOPLE_NICKNAMES_NICKNAME,
|
|
'nicknames': PEOPLE_NICKNAMES_NICKNAME,
|
|
'note': PEOPLE_BIOGRAPHIES,
|
|
'notes': PEOPLE_BIOGRAPHIES,
|
|
'occupation': PEOPLE_OCCUPATIONS,
|
|
'occupations': PEOPLE_OCCUPATIONS,
|
|
'organization': PEOPLE_ORGANIZATIONS,
|
|
'organizations': PEOPLE_ORGANIZATIONS,
|
|
'organisation': PEOPLE_ORGANIZATIONS,
|
|
'organisations': PEOPLE_ORGANIZATIONS,
|
|
'phone': PEOPLE_PHONE_NUMBERS,
|
|
'phones': PEOPLE_PHONE_NUMBERS,
|
|
'phonenumbers': PEOPLE_PHONE_NUMBERS,
|
|
# 'photo': PEOPLE_PHOTOS,
|
|
# 'photos': PEOPLE_PHOTOS,
|
|
'prefix': PEOPLE_NAMES_HONORIFIC_PREFIX,
|
|
'priority': PEOPLE_MISC_KEYWORDS_PRIORITY,
|
|
'relation': PEOPLE_RELATIONS,
|
|
'relations': PEOPLE_RELATIONS,
|
|
'sensitivity': PEOPLE_MISC_KEYWORDS_SENSITIVITY,
|
|
'shortname': PEOPLE_NICKNAMES_SHORTNAME,
|
|
'sipaddress': PEOPLE_SIP_ADDRESSES,
|
|
'sipaddresses': PEOPLE_SIP_ADDRESSES,
|
|
'skills': PEOPLE_SKILLS,
|
|
'subject': PEOPLE_MISC_KEYWORDS_SUBJECT,
|
|
'suffix': PEOPLE_NAMES_HONORIFIC_SUFFIX,
|
|
'url': PEOPLE_URLS,
|
|
'urls': PEOPLE_URLS,
|
|
'userdefined': PEOPLE_USER_DEFINED,
|
|
'userdefinedfield': PEOPLE_USER_DEFINED,
|
|
'userdefinedfields': PEOPLE_USER_DEFINED,
|
|
'website': PEOPLE_URLS,
|
|
'websites': PEOPLE_URLS,
|
|
'contactgroup': PEOPLE_GROUPS,
|
|
'contactgroups': PEOPLE_GROUPS,
|
|
'addcontactgroup': PEOPLE_ADD_GROUPS,
|
|
'addcontactgroups': PEOPLE_ADD_GROUPS,
|
|
'removecontactgroup': PEOPLE_REMOVE_GROUPS,
|
|
'removecontactgroups': PEOPLE_REMOVE_GROUPS,
|
|
}
|
|
|
|
ADDRESS_ARGUMENT_TO_FIELD_MAP = {
|
|
'formatted': 'formattedValue',
|
|
'unstructured': 'formattedValue',
|
|
'pobox': 'poBox',
|
|
'street': 'streetAddress',
|
|
'streetaddress': 'streetAddress',
|
|
'extended': 'extendedAddress',
|
|
'neighborhood': 'extendedAddress',
|
|
'city': 'city',
|
|
'locality': 'city',
|
|
'region': 'region',
|
|
'postalcode': 'postalCode',
|
|
'country': 'country',
|
|
'countrycode': 'countryCode',
|
|
}
|
|
|
|
JOT_TYPE_MAP = {
|
|
'work': 'WORK',
|
|
'home': 'HOME',
|
|
'other': 'OTHER',
|
|
'keyword': 'KEYWORD',
|
|
'keywords': 'KEYWORD',
|
|
'user': 'USER',
|
|
}
|
|
|
|
IM_PROTOCOLS = {
|
|
'aim': 'aim',
|
|
'googletalk': 'googleTalk',
|
|
'gtalk': 'googleTalk',
|
|
'icq': 'icq',
|
|
'jabber': 'jabber',
|
|
'msn': 'msn',
|
|
'netmeeting': 'netMeeting',
|
|
'qq': 'qq',
|
|
'skype': 'skype',
|
|
'xmpp': 'jabber',
|
|
'yahoo': 'yahoo',
|
|
}
|
|
|
|
ORGANIZATION_ARGUMENT_TO_FIELD_MAP = {
|
|
'startdate': 'startDate',
|
|
'enddate': 'endDate',
|
|
'current': 'current',
|
|
'phoneticname': 'phoneticName',
|
|
'title': 'title',
|
|
'department': 'department',
|
|
'jobdescription': 'jobDescription',
|
|
'symbol': 'symbol',
|
|
'domain': 'domain',
|
|
'location': 'location',
|
|
}
|
|
|
|
# Fields with a key and value
|
|
KEY_VALUE_FIELDS = {
|
|
PEOPLE_CLIENT_DATA,
|
|
PEOPLE_USER_DEFINED,
|
|
}
|
|
|
|
# Fields with a type and value
|
|
TYPE_VALUE_FIELDS = {
|
|
PEOPLE_EVENTS: {
|
|
'anniversary': 'anniversary',
|
|
'other': 'other',
|
|
},
|
|
PEOPLE_EXTERNAL_IDS: {
|
|
'account': 'account',
|
|
'customer': 'customer',
|
|
'loginid': 'loginId',
|
|
'network': 'network',
|
|
'organization': 'organization',
|
|
'organisation': 'organization',
|
|
},
|
|
PEOPLE_RELATIONS: {
|
|
'spouse' : 'spouse',
|
|
'child' : 'child',
|
|
'mother' : 'mother',
|
|
'father' : 'father',
|
|
'parent' : 'parent',
|
|
'brother' : 'brother',
|
|
'sister' : 'sister',
|
|
'friend' : 'friend',
|
|
'relative' : 'relative',
|
|
'domesticpartner' : 'domesticPartner',
|
|
'manager' : 'manager',
|
|
'assistant' : 'assistant',
|
|
'referredby' : 'referredBy',
|
|
'partner' : 'partner',
|
|
},
|
|
PEOPLE_SIP_ADDRESSES: {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'other': 'other',
|
|
'mobile': 'mobile',
|
|
},
|
|
}
|
|
|
|
# Fields with a type, value and primary|notprimary; some fields may have additional arguments
|
|
TYPE_VALUE_PNP_FIELDS = {
|
|
PEOPLE_ADDRESSES: {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'other': 'other',
|
|
},
|
|
PEOPLE_CALENDAR_URLS: {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'freebusy': 'freeBusy',
|
|
},
|
|
PEOPLE_EMAIL_ADDRESSES: {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'other': 'other',
|
|
},
|
|
PEOPLE_IM_CLIENTS: {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'other': 'other',
|
|
},
|
|
PEOPLE_ORGANIZATIONS: {
|
|
'school': 'school',
|
|
'work': 'work',
|
|
'other': 'other',
|
|
},
|
|
PEOPLE_PHONE_NUMBERS: {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'other': 'other',
|
|
'googlevoice': 'googleVoice',
|
|
'fax': 'homeFax',
|
|
'homefax': 'homeFax',
|
|
'workfax': 'workFax',
|
|
'otherfax': 'otherFax',
|
|
'main': 'main',
|
|
'company_main': 'companyMain',
|
|
'assistant': 'assistant',
|
|
'mobile': 'mobile',
|
|
'workmobile': 'workMobile',
|
|
'pager': 'pager',
|
|
'workpager': 'workPager',
|
|
'car': 'car',
|
|
'radio': 'radio',
|
|
'callback': 'callback',
|
|
'isdn': 'isdn',
|
|
'telex': 'telex',
|
|
'ttytdd': 'TTY_TDD',
|
|
},
|
|
PEOPLE_SIP_ADDRESSES: {
|
|
'work': 'work',
|
|
'home': 'home',
|
|
'mobile': 'mobile',
|
|
'other': 'other',
|
|
},
|
|
PEOPLE_URLS: {
|
|
'appinstallpage': 'appInstallPage',
|
|
'blog': 'blog',
|
|
'ftp': 'ftp',
|
|
'home': 'homePage',
|
|
'homepage': 'homePage',
|
|
'other': 'other',
|
|
'profile': 'profile',
|
|
'reservations': 'reservations',
|
|
'resume': 'resume',
|
|
'work': 'work',
|
|
}
|
|
}
|
|
|
|
# Fields that allow an empty type
|
|
EMPTY_TYPE_ALLOWED_FIELDS = {PEOPLE_ADDRESSES, PEOPLE_EMAIL_ADDRESSES, PEOPLE_PHONE_NUMBERS, PEOPLE_URLS}
|
|
|
|
# Fields with just a URL
|
|
# URL_FIELDS = {
|
|
# PEOPLE_COVER_PHOTOS,
|
|
# PEOPLE_PHOTOS,
|
|
# }
|
|
|
|
# Fields with a single value
|
|
SINGLE_VALUE_FIELDS = {
|
|
PEOPLE_FILE_ASES,
|
|
}
|
|
|
|
# Fields with multiple values
|
|
MULTI_VALUE_FIELDS = {
|
|
PEOPLE_INTERESTS,
|
|
PEOPLE_LOCALES,
|
|
PEOPLE_LOCATIONS,
|
|
PEOPLE_OCCUPATIONS,
|
|
PEOPLE_SKILLS,
|
|
}
|
|
|
|
@staticmethod
|
|
def GetPersonFields(entityType, allowAddRemove, parameters=None):
|
|
person = {}
|
|
contactGroupsLists = {
|
|
PEOPLE_GROUPS_LIST: [],
|
|
PEOPLE_ADD_GROUPS_LIST: [],
|
|
PEOPLE_REMOVE_GROUPS_LIST: []
|
|
}
|
|
locations = {'primary': None}
|
|
|
|
def CheckClearPersonField(fieldName):
|
|
if checkArgumentPresent(Cmd.CLEAR_NONE_ARGUMENT):
|
|
person.pop(fieldName, None)
|
|
person[fieldName] = []
|
|
return True
|
|
return False
|
|
|
|
def GetSingleFieldEntry(fieldName):
|
|
person.setdefault(fieldName, [])
|
|
if not person[fieldName]:
|
|
person[fieldName].append({})
|
|
return person[fieldName][0]
|
|
|
|
def InitArrayFieldEntry(choices, typeMinLen=1):
|
|
entry = {'metadata': {'primary': False}}
|
|
if choices is not None:
|
|
ftype = getChoice(choices, mapChoice=True, defaultChoice=None)
|
|
if ftype:
|
|
entry['type'] = ftype
|
|
else:
|
|
entry['type'] = getString(Cmd.OB_STRING, minLen=typeMinLen)
|
|
return entry
|
|
|
|
def GetMultiFieldEntry(fieldName):
|
|
person.setdefault(fieldName, [])
|
|
person[fieldName].append({})
|
|
return person[fieldName][-1]
|
|
|
|
def getDate(entry, fieldName):
|
|
event = getYYYYMMDD(minLen=0, returnDateTime=True)
|
|
if event:
|
|
entry[fieldName] = {'year': event.year, 'month': event.month, 'day': event.day}
|
|
|
|
def PrimaryNotPrimary(pnp, entry):
|
|
if pnp == 'notprimary':
|
|
entry['metadata']['primary'] = 'false'
|
|
return True
|
|
if pnp == 'primary':
|
|
entry['metadata']['primary'] = 'true'
|
|
locations['primary'] = Cmd.Location()
|
|
return True
|
|
return False
|
|
|
|
def GetPrimaryNotPrimaryChoice(entry):
|
|
pnp = getChoice({'primary': True, 'notprimary': False}, mapChoice=True)
|
|
entry['metadata']['primary'] = pnp
|
|
if pnp:
|
|
locations['primary'] = Cmd.Location()
|
|
|
|
def AppendArrayEntryToFields(fieldName, entry, checkBlankField=None):
|
|
person.setdefault(fieldName, [])
|
|
if checkBlankField is None or entry[checkBlankField]:
|
|
if entry.get('metadata', {}).get('primary', False):
|
|
for centry in person[fieldName][1:]:
|
|
if centry.get('metadata', {}).get('primary', False):
|
|
Cmd.SetLocation(locations['primary']-1)
|
|
usageErrorExit(Msg.MULTIPLE_ITEMS_MARKED_PRIMARY.format(fieldName))
|
|
person[fieldName].append(entry)
|
|
|
|
while Cmd.ArgumentsRemaining():
|
|
if parameters is not None:
|
|
if _getCreateContactReturnOptions(parameters):
|
|
continue
|
|
Cmd.Backup()
|
|
locations['fieldName'] = Cmd.Location()
|
|
fieldName = getChoice(PeopleManager.PEOPLE_ARGUMENT_TO_PROPERTY_MAP, mapChoice=True)
|
|
if '.' in fieldName:
|
|
fieldName, subFieldName = fieldName.split('.')
|
|
if fieldName == PEOPLE_ADDRESSES:
|
|
if CheckClearPersonField(fieldName):
|
|
continue
|
|
entry = InitArrayFieldEntry(PeopleManager.TYPE_VALUE_PNP_FIELDS[fieldName], typeMinLen=0)
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument in PeopleManager.ADDRESS_ARGUMENT_TO_FIELD_MAP:
|
|
subFieldName = PeopleManager.ADDRESS_ARGUMENT_TO_FIELD_MAP[argument]
|
|
value = getString(Cmd.OB_STRING, minLen=0)
|
|
if value: ### Delete?
|
|
entry[subFieldName] = value.replace('\\n', '\n')
|
|
elif PrimaryNotPrimary(argument, entry):
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
AppendArrayEntryToFields(fieldName, entry, None)
|
|
elif fieldName == PEOPLE_BIRTHDAYS:
|
|
entry = GetSingleFieldEntry(fieldName)
|
|
getDate(entry, 'date')
|
|
elif fieldName == PEOPLE_BIOGRAPHIES:
|
|
entry = GetSingleFieldEntry(fieldName)
|
|
text, _, html = getStringWithCRsNLsOrFile()
|
|
entry['value' ] = text
|
|
entry['contentType'] = ['TEXT_PLAIN', 'TEXT_HTML'][html]
|
|
elif fieldName == PEOPLE_GENDERS:
|
|
entry = GetSingleFieldEntry(fieldName)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif fieldName == PEOPLE_MISC_KEYWORDS:
|
|
entry = GetMultiFieldEntry(fieldName)
|
|
if subFieldName == 'jot':
|
|
subFieldName = getChoice(PeopleManager.JOT_TYPE_MAP, mapChoice=True)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
entry['type'] = subFieldName
|
|
elif fieldName == PEOPLE_NAMES:
|
|
entry = GetSingleFieldEntry(fieldName)
|
|
entry[subFieldName] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif fieldName == PEOPLE_NICKNAMES:
|
|
entry = GetMultiFieldEntry(fieldName)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
entry['type'] = subFieldName
|
|
elif fieldName == PEOPLE_ORGANIZATIONS:
|
|
entry = InitArrayFieldEntry(PeopleManager.TYPE_VALUE_PNP_FIELDS[fieldName])
|
|
entry['name'] = getString(Cmd.OB_STRING, minLen=0)
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument in PeopleManager.ORGANIZATION_ARGUMENT_TO_FIELD_MAP:
|
|
subFieldName = PeopleManager.ORGANIZATION_ARGUMENT_TO_FIELD_MAP[argument]
|
|
if subFieldName == 'current':
|
|
entry[subFieldName] = getBoolean()
|
|
elif subFieldName in {'startDate', 'endDate'}:
|
|
getDate(entry, subFieldName)
|
|
else:
|
|
value = getString(Cmd.OB_STRING, minLen=0)
|
|
if value: ### Delete?
|
|
entry[subFieldName] = value
|
|
elif PrimaryNotPrimary(argument, entry):
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
AppendArrayEntryToFields(fieldName, entry, None)
|
|
elif fieldName in PeopleManager.KEY_VALUE_FIELDS:
|
|
if CheckClearPersonField(fieldName):
|
|
continue
|
|
entry = InitArrayFieldEntry(None)
|
|
entry['key'] = getString(Cmd.OB_STRING, minLen=1)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
AppendArrayEntryToFields(fieldName, entry, 'value')
|
|
elif fieldName in PeopleManager.TYPE_VALUE_FIELDS:
|
|
if CheckClearPersonField(fieldName):
|
|
continue
|
|
entry = InitArrayFieldEntry(PeopleManager.TYPE_VALUE_FIELDS[fieldName])
|
|
if fieldName == PEOPLE_EVENTS:
|
|
checkBlankField = 'date'
|
|
getDate(entry, checkBlankField)
|
|
elif fieldName == PEOPLE_RELATIONS:
|
|
checkBlankField = 'type'
|
|
entry['person'] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
checkBlankField = 'value'
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
AppendArrayEntryToFields(fieldName, entry, checkBlankField)
|
|
elif fieldName in PeopleManager.TYPE_VALUE_PNP_FIELDS:
|
|
if CheckClearPersonField(fieldName):
|
|
continue
|
|
entry = InitArrayFieldEntry(PeopleManager.TYPE_VALUE_PNP_FIELDS[fieldName],
|
|
typeMinLen=0 if fieldName in PeopleManager.EMPTY_TYPE_ALLOWED_FIELDS else 1)
|
|
if fieldName == PEOPLE_IM_CLIENTS:
|
|
checkBlankField = None
|
|
entry['protocol'] = getChoice(PeopleManager.IM_PROTOCOLS, mapChoice=True)
|
|
entry['username'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif fieldName == PEOPLE_EMAIL_ADDRESSES:
|
|
checkBlankField = 'value'
|
|
entry[checkBlankField] = getString(Cmd.OB_STRING, minLen=0)
|
|
if checkArgumentPresent(['displayname']):
|
|
entry['displayName'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif fieldName == PEOPLE_CALENDAR_URLS:
|
|
checkBlankField = 'url'
|
|
entry[checkBlankField] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
checkBlankField = 'value'
|
|
entry[checkBlankField] = getString(Cmd.OB_STRING, minLen=0)
|
|
GetPrimaryNotPrimaryChoice(entry)
|
|
AppendArrayEntryToFields(fieldName, entry, checkBlankField)
|
|
elif fieldName in PeopleManager.SINGLE_VALUE_FIELDS:
|
|
entry = GetSingleFieldEntry(fieldName)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif fieldName in PeopleManager.MULTI_VALUE_FIELDS:
|
|
if CheckClearPersonField(fieldName):
|
|
continue
|
|
entry = InitArrayFieldEntry(None)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
AppendArrayEntryToFields(fieldName, entry, 'value')
|
|
# elif fieldName in PeopleManager.URL_FIELDS:
|
|
# if CheckClearPersonField(fieldName):
|
|
# continue
|
|
# entry = InitArrayFieldEntry(None)
|
|
# entry['url'] = getString(Cmd.OB_STRING, minLen=0)
|
|
# entry['default'] = False
|
|
# AppendArrayEntryToFields(fieldName, entry, 'url')
|
|
elif fieldName == PEOPLE_GROUPS:
|
|
if entityType != Ent.USER:
|
|
Cmd.Backup()
|
|
unknownArgumentExit()
|
|
contactGroupsLists[PEOPLE_GROUPS_LIST].append(getString(Cmd.OB_STRING))
|
|
elif fieldName == PEOPLE_ADD_GROUPS:
|
|
if not allowAddRemove:
|
|
unknownArgumentExit()
|
|
if entityType != Ent.USER:
|
|
Cmd.Backup()
|
|
unknownArgumentExit()
|
|
contactGroupsLists[PEOPLE_ADD_GROUPS_LIST].append(getString(Cmd.OB_STRING))
|
|
elif fieldName == PEOPLE_REMOVE_GROUPS:
|
|
if not allowAddRemove:
|
|
unknownArgumentExit()
|
|
if entityType != Ent.USER:
|
|
Cmd.Backup()
|
|
unknownArgumentExit()
|
|
contactGroupsLists[PEOPLE_REMOVE_GROUPS_LIST].append(getString(Cmd.OB_STRING))
|
|
elif fieldName == PEOPLE_JSON:
|
|
jsonData = getJSON(['resourceName', 'etag', 'metadata', PEOPLE_COVER_PHOTOS, PEOPLE_PHOTOS, PEOPLE_UPDATE_TIME])
|
|
for membership in jsonData.pop('memberships', []):
|
|
contactGroupName = membership.get('contactGroupMembership', {}).get('contactGroupName', '')
|
|
if contactGroupName:
|
|
contactGroupsLists[PEOPLE_GROUPS_LIST].append(contactGroupName)
|
|
newClientData = []
|
|
for clientData in jsonData.pop('clientData', []):
|
|
if clientData['key'] not in {'ContactId', 'CtsContactHash'}:
|
|
newClientData.append({'key': clientData['key'], 'value': clientData['value']})
|
|
if newClientData:
|
|
person.setdefault(PEOPLE_CLIENT_DATA, [])
|
|
person[PEOPLE_CLIENT_DATA].extend(newClientData)
|
|
person.update(jsonData)
|
|
return (person, set(person.keys()), contactGroupsLists)
|
|
|
|
PEOPLE_GROUP_ARGUMENT_TO_PROPERTY_MAP = {
|
|
'json': PEOPLE_JSON,
|
|
'name': PEOPLE_GROUP_NAME,
|
|
'clientdata': PEOPLE_GROUP_CLIENT_DATA,
|
|
}
|
|
|
|
@staticmethod
|
|
def AddContactGroupsToContact(contactEntry, contactGroupsList):
|
|
contactEntry[PEOPLE_MEMBERSHIPS] = []
|
|
for groupId in contactGroupsList:
|
|
if groupId != 'clear':
|
|
contactEntry[PEOPLE_MEMBERSHIPS].append({'contactGroupMembership': {'contactGroupResourceName': groupId}})
|
|
else:
|
|
contactEntry[PEOPLE_MEMBERSHIPS] = []
|
|
|
|
@staticmethod
|
|
def AddFilteredContactGroupsToContact(contactEntry, contactGroupsList, contactRemoveGroupsList):
|
|
contactEntry[PEOPLE_MEMBERSHIPS] = []
|
|
for groupId in contactGroupsList:
|
|
if groupId not in contactRemoveGroupsList:
|
|
contactEntry[PEOPLE_MEMBERSHIPS].append({'contactGroupMembership': {'contactGroupResourceName': groupId}})
|
|
|
|
@staticmethod
|
|
def AddAdditionalContactGroupsToContact(contactEntry, contactGroupsList):
|
|
for groupId in contactGroupsList:
|
|
contactEntry[PEOPLE_MEMBERSHIPS].append({'contactGroupMembership': {'contactGroupResourceName': groupId}})
|
|
|
|
@staticmethod
|
|
def GetContactGroupFields(parameters=None):
|
|
contactGroup = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
if parameters is not None:
|
|
if _getCreateContactReturnOptions(parameters):
|
|
continue
|
|
Cmd.Backup()
|
|
fieldName = getChoice(PeopleManager.PEOPLE_GROUP_ARGUMENT_TO_PROPERTY_MAP, mapChoice=True)
|
|
if fieldName == PEOPLE_GROUP_NAME:
|
|
contactGroup[PEOPLE_GROUP_NAME] = getString(Cmd.OB_STRING)
|
|
elif fieldName == PEOPLE_GROUP_CLIENT_DATA:
|
|
entry = {}
|
|
entry['key'] = getString(Cmd.OB_STRING, minLen=1)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
if entry['value']:
|
|
contactGroup.setdefault(fieldName, [])
|
|
contactGroup[fieldName].append(entry)
|
|
elif fieldName == PEOPLE_JSON:
|
|
jsonData = getJSON(['resourceName', 'etag', 'metadata', 'formattedName', 'memberResourceNames', 'memberCount'])
|
|
if jsonData.get('groupType', '') != 'SYSTEM_CONTACT_GROUP':
|
|
contactGroup[PEOPLE_GROUP_NAME] = jsonData['name']
|
|
return (contactGroup, ','.join(contactGroup.keys()))
|
|
|
|
PEOPLE_DIRECTORY_SOURCES_CHOICE_MAP = {
|
|
'contact': 'DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT',
|
|
'contacts': 'DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT',
|
|
'domaincontact': 'DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT',
|
|
'comaincontacts': 'DIRECTORY_SOURCE_TYPE_DOMAIN_CONTACT',
|
|
'profile': 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE',
|
|
'profiles': 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE'
|
|
}
|
|
|
|
PEOPLE_READ_SOURCES_CHOICE_MAP = {
|
|
'contact': 'READ_SOURCE_TYPE_CONTACT',
|
|
'contacts': 'READ_SOURCE_TYPE_CONTACT',
|
|
'domaincontact': 'READ_SOURCE_TYPE_DOMAIN_CONTACT',
|
|
'domaincontacts': 'READ_SOURCE_TYPE_DOMAIN_CONTACT',
|
|
'profile': 'READ_SOURCE_TYPE_PROFILE',
|
|
'profiles': 'READ_SOURCE_TYPE_PROFILE'
|
|
}
|
|
|
|
PEOPLE_DIRECTORY_MERGE_SOURCES_CHOICE_MAP = {
|
|
'contact': 'DIRECTORY_MERGE_SOURCE_TYPE_CONTACT',
|
|
'contacts': 'DIRECTORY_MERGE_SOURCE_TYPE_CONTACT',
|
|
}
|
|
|
|
def _initPeopleContactQueryAttributes(printShowCmd):
|
|
return {'query': None, 'updateTime': None,
|
|
'contactGroupSelect': None, 'contactGroupFilter': None, 'group': None, 'dropMemberships': False,
|
|
'mainContacts': True, 'otherContacts': printShowCmd, 'emailMatchPattern': None, 'emailMatchType': None}
|
|
|
|
def _getPeopleContactQueryAttributes(contactQuery, myarg, entityType, unknownAction, printShowCmd):
|
|
if myarg == 'query':
|
|
contactQuery['query'] = getString(Cmd.OB_QUERY)
|
|
elif myarg in {'contactgroup', 'selectcontactgroup'}:
|
|
if entityType == Ent.USER:
|
|
contactQuery['contactGroupSelect'] = getString(Cmd.OB_CONTACT_GROUP_ITEM)
|
|
contactQuery['contactGroupFilter'] = None
|
|
contactQuery['mainContacts'] = True
|
|
contactQuery['otherContacts'] = False
|
|
else:
|
|
unknownArgumentExit()
|
|
elif myarg == 'emailmatchpattern':
|
|
contactQuery['emailMatchPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'emailmatchtype':
|
|
contactQuery['emailMatchType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
|
|
elif myarg == 'updatedmin':
|
|
deprecatedArgument(myarg)
|
|
getYYYYMMDD()
|
|
elif myarg == 'endquery':
|
|
return False
|
|
elif not printShowCmd:
|
|
if unknownAction < 0:
|
|
unknownArgumentExit()
|
|
if unknownAction > 0:
|
|
Cmd.Backup()
|
|
return False
|
|
elif myarg == 'filtercontactgroup':
|
|
if entityType == Ent.USER:
|
|
contactQuery['contactGroupFilter'] = getString(Cmd.OB_CONTACT_GROUP_ITEM)
|
|
contactQuery['contactGroupSelect'] = None
|
|
contactQuery['mainContacts'] = True
|
|
contactQuery['otherContacts'] = False
|
|
else:
|
|
unknownArgumentExit()
|
|
elif myarg in {'maincontacts', 'selectmaincontacts'}:
|
|
if entityType == Ent.USER:
|
|
contactQuery['contactGroupSelect'] = None
|
|
contactQuery['contactGroupFilter'] = None
|
|
contactQuery['mainContacts'] = True
|
|
contactQuery['otherContacts'] = False
|
|
else:
|
|
unknownArgumentExit()
|
|
elif myarg in {'othercontacts', 'selectothercontacts'}:
|
|
if entityType == Ent.USER:
|
|
contactQuery['contactGroupSelect'] = None
|
|
contactQuery['contactGroupFilter'] = None
|
|
contactQuery['mainContacts'] = False
|
|
contactQuery['otherContacts'] = True
|
|
else:
|
|
unknownArgumentExit()
|
|
elif myarg == 'orderby':
|
|
getOrderBySortOrder(CONTACTS_ORDERBY_CHOICE_MAP, 'ascending', False)
|
|
deprecatedArgument(myarg)
|
|
elif myarg in CONTACTS_PROJECTION_CHOICE_MAP:
|
|
deprecatedArgument(myarg)
|
|
elif myarg == 'showdeleted':
|
|
deprecatedArgument(myarg)
|
|
else:
|
|
if unknownAction < 0:
|
|
unknownArgumentExit()
|
|
if unknownAction > 0:
|
|
Cmd.Backup()
|
|
return False
|
|
return True
|
|
|
|
PEOPLE_CONTACT_SELECT_ARGUMENTS = {
|
|
'query', 'contactgroup', 'selectcontactgroup',
|
|
'maincontacts', 'selectmaincontacts',
|
|
'othercontacts', 'selectothercontacts',
|
|
'emailmatchpattern', 'emailmatchtype',
|
|
}
|
|
PEOPLE_CONTACT_DEPRECATED_SELECT_ARGUMENTS = {
|
|
'orderby', 'basic', 'thin', 'full', 'showdeleted',
|
|
}
|
|
|
|
def _getPeopleContactEntityList(entityType, unknownAction, noEntityArguments=None):
|
|
contactQuery = _initPeopleContactQueryAttributes(False)
|
|
if noEntityArguments is not None and (not Cmd.ArgumentsRemaining() or Cmd.PeekArgumentPresent(noEntityArguments)):
|
|
# <PeopleResourceNameEntity>|<PeopleUserContactSelection> are optional in dedup|replacedomain contacts
|
|
entityList = None
|
|
queriedContacts = True
|
|
elif Cmd.PeekArgumentPresent(PEOPLE_CONTACT_SELECT_ARGUMENTS.union(PEOPLE_CONTACT_DEPRECATED_SELECT_ARGUMENTS)):
|
|
entityList = None
|
|
queriedContacts = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if not _getPeopleContactQueryAttributes(contactQuery, myarg, entityType, unknownAction, False):
|
|
break
|
|
else:
|
|
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
|
|
queriedContacts = False
|
|
if unknownAction < 0:
|
|
checkForExtraneousArguments()
|
|
return (entityList, entityList if isinstance(entityList, dict) else None, contactQuery, queriedContacts)
|
|
|
|
def _initPeopleOtherContactQueryAttributes():
|
|
return {'query': None,
|
|
'otherContacts': True, 'emailMatchPattern': None, 'emailMatchType': None}
|
|
|
|
def _getPeopleOtherContactQueryAttributes(contactQuery, myarg, unknownAction):
|
|
if myarg == 'query':
|
|
contactQuery['query'] = getString(Cmd.OB_QUERY)
|
|
elif myarg == 'emailmatchpattern':
|
|
contactQuery['emailMatchPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'emailmatchtype':
|
|
contactQuery['emailMatchType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
|
|
elif myarg == 'endquery':
|
|
return False
|
|
else:
|
|
if unknownAction < 0:
|
|
unknownArgumentExit()
|
|
if unknownAction > 0:
|
|
Cmd.Backup()
|
|
return False
|
|
return True
|
|
|
|
PEOPLE_OTHERCONTACT_SELECT_ARGUMENTS = {'query', 'emailmatchpattern', 'emailmatchtype'}
|
|
|
|
def _getPeopleOtherContactEntityList(unknownAction):
|
|
contactQuery = _initPeopleOtherContactQueryAttributes()
|
|
if Cmd.PeekArgumentPresent(PEOPLE_OTHERCONTACT_SELECT_ARGUMENTS):
|
|
entityList = None
|
|
queriedContacts = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if not _getPeopleOtherContactQueryAttributes(contactQuery, myarg, unknownAction):
|
|
break
|
|
else:
|
|
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
|
|
queriedContacts = False
|
|
if unknownAction < 0:
|
|
checkForExtraneousArguments()
|
|
return (entityList, entityList if isinstance(entityList, dict) else None, contactQuery, queriedContacts)
|
|
|
|
def _getPeopleOtherContacts(people, entityType, user, i=0, count=0):
|
|
try:
|
|
printGettingAllEntityItemsForWhom(Ent.OTHER_CONTACT, user, i, count)
|
|
results = callGAPIpages(people.otherContacts(), 'list', 'otherContacts',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageSize=1000,
|
|
readMask='emailAddresses', fields='nextPageToken,otherContacts(etag,resourceName,emailAddresses(value,type))')
|
|
otherContacts = {}
|
|
for contact in results:
|
|
resourceName = contact.pop('resourceName')
|
|
otherContacts[resourceName] = contact
|
|
return otherContacts
|
|
except GAPI.permissionDenied as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
entityUnknownWarning(entityType, user, i, count)
|
|
return None
|
|
|
|
def queryPeopleContacts(people, contactQuery, fields, sortOrder, entityType, user, i=0, count=0):
|
|
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['domaincontact' if entityType == Ent.DOMAIN else 'contact']]
|
|
printGettingAllEntityItemsForWhom(Ent.PEOPLE_CONTACT, user, i, count, query=contactQuery['query'])
|
|
pageMessage = getPageMessageForWhom()
|
|
try:
|
|
# Contact group not selected
|
|
if not contactQuery['contactGroupSelect']:
|
|
if not contactQuery['query']:
|
|
results = callGAPIpages(people.people().connections(), 'list', 'connections',
|
|
pageMessage=pageMessage,
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
|
|
resourceName='people/me', sources=sources, personFields=fields,
|
|
sortOrder=sortOrder, fields='nextPageToken,connections')
|
|
if contactQuery['contactGroupFilter']:
|
|
entityList = []
|
|
for person in results:
|
|
for membership in person.get(PEOPLE_MEMBERSHIPS, []):
|
|
if membership.get('contactGroupMembership', {}).get('contactGroupResourceName', '') == contactQuery['group']:
|
|
if contactQuery['dropMemberships']:
|
|
person.pop(PEOPLE_MEMBERSHIPS)
|
|
entityList.append(person)
|
|
break
|
|
else:
|
|
entityList = results
|
|
else:
|
|
results = callGAPI(people.people(), 'searchContacts',
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
sources=sources, readMask=fields, query=contactQuery['query'])
|
|
entityList = [person['person'] for person in results.get('results', [])]
|
|
totalItems = len(entityList)
|
|
# Contact group selected
|
|
else:
|
|
totalItems = callGAPI(people.contactGroups(), 'get',
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
resourceName=contactQuery['group'], groupFields='memberCount').get('memberCount', 0)
|
|
entityList = []
|
|
if totalItems > 0:
|
|
results = callGAPI(people.contactGroups(), 'get',
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
resourceName=contactQuery['group'], maxMembers=totalItems, groupFields='name')
|
|
for resourceName in results.get('memberResourceNames', []):
|
|
result = callGAPI(people.people(), 'get',
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=resourceName, sources=sources, personFields=fields)
|
|
|
|
entityList.append(result)
|
|
if pageMessage and (contactQuery['contactGroupSelect'] or contactQuery['contactGroupFilter'] or contactQuery['query']):
|
|
showMessage = pageMessage.replace(TOTAL_ITEMS_MARKER, str(totalItems))
|
|
writeGotMessage(showMessage.replace('{0}', str(Ent.Choose(Ent.PEOPLE_CONTACT, totalItems))))
|
|
return entityList
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
entityUnknownWarning(entityType, user, i, count)
|
|
return None
|
|
|
|
def queryPeopleOtherContacts(people, contactQuery, fields, entityType, user, i=0, count=0):
|
|
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['contact']]
|
|
printGettingAllEntityItemsForWhom(Ent.OTHER_CONTACT, user, i, count, query=contactQuery['query'])
|
|
pageMessage = getPageMessageForWhom()
|
|
try:
|
|
if not contactQuery['query']:
|
|
entityList = callGAPIpages(people.otherContacts(), 'list', 'otherContacts',
|
|
pageMessage=pageMessage,
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
|
|
readMask=fields, fields='nextPageToken,otherContacts', sources=sources)
|
|
else:
|
|
results = callGAPI(people.otherContacts(), 'search',
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
pageSize=30, readMask=fields, query=contactQuery['query'])
|
|
entityList = [person['person'] for person in results.get('results', [])]
|
|
totalItems = len(entityList)
|
|
if pageMessage:
|
|
showMessage = pageMessage.replace(TOTAL_ITEMS_MARKER, str(totalItems))
|
|
writeGotMessage(showMessage.replace('{0}', str(Ent.Choose(Ent.OTHER_CONTACT, totalItems))))
|
|
return entityList
|
|
except GAPI.permissionDenied as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
entityUnknownWarning(entityType, user, i, count)
|
|
return None
|
|
|
|
def getPeopleContactGroupsInfo(people, entityType, entityName, i, count):
|
|
contactGroupIDs = {}
|
|
contactGroupNames = {}
|
|
try:
|
|
groups = callGAPIpages(people.contactGroups(), 'list', 'contactGroups',
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
|
|
groupFields='name', fields='nextPageToken,contactGroups(resourceName,name,formattedName)')
|
|
if groups:
|
|
for group in groups:
|
|
contactGroupIDs[group['resourceName']] = group['formattedName']
|
|
contactGroupNames.setdefault(group['formattedName'], [])
|
|
contactGroupNames[group['formattedName']].append(group['resourceName'])
|
|
if group['formattedName'] != group['name']:
|
|
contactGroupNames.setdefault(group['name'], [])
|
|
contactGroupNames[group['name']].append(group['resourceName'])
|
|
except GAPI.permissionDenied as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except GAPI.forbidden:
|
|
userPeopleServiceNotEnabledWarning(entityName, i, count)
|
|
return (contactGroupIDs, False)
|
|
except GAPI.serviceNotAvailable:
|
|
entityUnknownWarning(entityType, entityName, i, count)
|
|
return (contactGroupIDs, False)
|
|
return (contactGroupIDs, contactGroupNames)
|
|
|
|
def validatePeopleContactGroup(people, contactGroupName,
|
|
contactGroupIDs, contactGroupNames, entityType, entityName, i, count):
|
|
if not contactGroupNames:
|
|
contactGroupIDs, contactGroupNames = getPeopleContactGroupsInfo(people, entityType, entityName, i, count)
|
|
if contactGroupNames is False:
|
|
return (None, contactGroupIDs, contactGroupNames)
|
|
if contactGroupName == 'clear':
|
|
return (contactGroupName, contactGroupIDs, contactGroupNames)
|
|
cg = UID_PATTERN.match(contactGroupName)
|
|
if cg:
|
|
contactGroupName = cg.group(1)
|
|
if contactGroupName in contactGroupIDs:
|
|
return (contactGroupName, contactGroupIDs, contactGroupNames)
|
|
normalizedContactGroupName = normalizeContactGroupResourceName(contactGroupName)
|
|
if normalizedContactGroupName in contactGroupIDs:
|
|
return (normalizedContactGroupName, contactGroupIDs, contactGroupNames)
|
|
else:
|
|
if contactGroupName in contactGroupIDs:
|
|
return (contactGroupName, contactGroupIDs, contactGroupNames)
|
|
if contactGroupName in contactGroupNames:
|
|
return (contactGroupNames[contactGroupName][0], contactGroupIDs, contactGroupNames)
|
|
normalizedContactGroupName = normalizeContactGroupResourceName(contactGroupName)
|
|
if normalizedContactGroupName != contactGroupName and normalizedContactGroupName in contactGroupIDs:
|
|
return (normalizedContactGroupName, contactGroupIDs, contactGroupNames)
|
|
return (None, contactGroupIDs, contactGroupNames)
|
|
|
|
def validatePeopleContactGroupsList(people, contactId,
|
|
contactGroupsList, entityType, entityName, i, count):
|
|
result = True
|
|
contactGroupIDs = contactGroupNames = None
|
|
validatedContactGroupsList = []
|
|
for contactGroup in contactGroupsList:
|
|
groupId, contactGroupIDs, contactGroupNames = validatePeopleContactGroup(people, contactGroup,
|
|
contactGroupIDs, contactGroupNames, entityType, entityName, i, count)
|
|
if groupId:
|
|
validatedContactGroupsList.append(groupId)
|
|
else:
|
|
if contactGroupNames:
|
|
entityActionNotPerformedWarning([entityType, entityName, Ent.CONTACT, contactId],
|
|
Ent.TypeNameMessage(Ent.CONTACT_GROUP, contactGroup, Msg.DOES_NOT_EXIST))
|
|
result = False
|
|
return (result, validatedContactGroupsList, contactGroupIDs)
|
|
|
|
# gam <UserTypeEntity> create contact <PeopleContactAttribute>+
|
|
# (contactgroup <ContactGroupItem>)*
|
|
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*))| returnidonly]
|
|
def createUserPeopleContact(users):
|
|
entityType = Ent.USER
|
|
peopleManager = PeopleManager()
|
|
peopleEntityType = Ent.CONTACT
|
|
sources = PEOPLE_READ_SOURCES_CHOICE_MAP['contact']
|
|
parameters = {'csvPF': None, 'titles': ['User', 'resourceName'], 'addCSVData': {}, 'returnIdOnly': False}
|
|
body, personFields, contactGroupsLists = peopleManager.GetPersonFields(entityType, False, parameters)
|
|
csvPF = parameters['csvPF']
|
|
addCSVData = parameters['addCSVData']
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
returnIdOnly = parameters['returnIdOnly']
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
if contactGroupsLists[PEOPLE_GROUPS_LIST]:
|
|
result, validatedContactGroupsList, _ = validatePeopleContactGroupsList(people, '',
|
|
contactGroupsLists[PEOPLE_GROUPS_LIST], entityType, user, i, count)
|
|
if not result:
|
|
continue
|
|
peopleManager.AddContactGroupsToContact(body, validatedContactGroupsList)
|
|
personFields.add(PEOPLE_MEMBERSHIPS)
|
|
else:
|
|
personFields.discard(PEOPLE_MEMBERSHIPS)
|
|
try:
|
|
result = callGAPI(people.people(), 'createContact',
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS+[GAPI.INVALID_ARGUMENT],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
personFields=','.join(personFields), body=body, sources=sources)
|
|
resourceName = result['resourceName']
|
|
if returnIdOnly:
|
|
writeStdout(f'{resourceName}\n')
|
|
elif not csvPF:
|
|
entityActionPerformed([entityType, user, peopleEntityType, resourceName], i, count)
|
|
else:
|
|
row = {'User': user, 'resourceName': resourceName}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
except GAPI.invalidArgument as e:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, None], str(e), i, count)
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('People Contacts')
|
|
|
|
def localPeopleContactSelects(contactQuery, contact):
|
|
if contactQuery['emailMatchPattern']:
|
|
emailMatchType = contactQuery['emailMatchType']
|
|
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
|
|
if contactQuery['emailMatchPattern'].match(item['value']):
|
|
if (not emailMatchType or emailMatchType == item.get('type', '')):
|
|
break
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def countLocalPeopleContactSelects(contactQuery, contacts):
|
|
if contacts is not None and contactQuery['emailMatchPattern']:
|
|
jcount = 0
|
|
for contact in contacts:
|
|
if localPeopleContactSelects(contactQuery, contact):
|
|
jcount += 1
|
|
else:
|
|
jcount = len(contacts) if contacts is not None else 0
|
|
return jcount
|
|
|
|
def clearPeopleEmailAddressMatches(contactClear, contact):
|
|
savedAddresses = []
|
|
updateRequired = False
|
|
emailMatchType = contactClear['emailClearType']
|
|
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
|
|
if (contactClear['emailClearPattern'].match(item['value']) and
|
|
(not emailMatchType or emailMatchType == item.get('type', ''))):
|
|
updateRequired = True
|
|
else:
|
|
savedAddresses.append(item)
|
|
if updateRequired:
|
|
contact[PEOPLE_EMAIL_ADDRESSES] = savedAddresses
|
|
return updateRequired
|
|
|
|
def _clearUpdatePeopleContacts(users, updateContacts):
|
|
action = Act.Get()
|
|
entityType = Ent.USER
|
|
peopleManager = PeopleManager()
|
|
peopleEntityType = Ent.PEOPLE_CONTACT
|
|
sources = PEOPLE_READ_SOURCES_CHOICE_MAP['contact']
|
|
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleContactEntityList(entityType, 1)
|
|
if updateContacts:
|
|
body, updatePersonFields, contactGroupsLists = peopleManager.GetPersonFields(entityType, True)
|
|
else:
|
|
contactClear = {'emailClearPattern': contactQuery['emailMatchPattern'], 'emailClearType': contactQuery['emailMatchType']}
|
|
deleteClearedContactsWithNoEmails = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'emailclearpattern':
|
|
contactClear['emailClearPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'emailcleartype':
|
|
contactClear['emailClearType'] = getString(Cmd.OB_CONTACT_EMAIL_TYPE)
|
|
elif myarg == 'deleteclearedcontactswithnoemails':
|
|
deleteClearedContactsWithNoEmails = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not contactClear['emailClearPattern']:
|
|
missingArgumentExit('emailclearpattern')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if resourceNameLists:
|
|
entityList = resourceNameLists[user]
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
if contactQuery['contactGroupSelect']:
|
|
groupId, _, contactGroupNames = validatePeopleContactGroup(people, contactQuery['contactGroupSelect'],
|
|
None, None, entityType, user, i, count)
|
|
if not groupId:
|
|
if contactGroupNames:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactQuery['contactGroupSelect']], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
contactQuery['group'] = groupId
|
|
if queriedContacts:
|
|
entityList = queryPeopleContacts(people, contactQuery, 'emailAddresses,memberships', None, entityType, user, i, count)
|
|
if entityList is None:
|
|
continue
|
|
Act.Set(action)
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
validatedContactGroupsLists = {
|
|
PEOPLE_GROUPS_LIST: [],
|
|
PEOPLE_ADD_GROUPS_LIST: [],
|
|
PEOPLE_REMOVE_GROUPS_LIST: []
|
|
}
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
try:
|
|
if not queriedContacts:
|
|
resourceName = contact
|
|
contact = callGAPI(people.people(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=contact, sources=sources, personFields='emailAddresses,memberships')
|
|
else:
|
|
if not localPeopleContactSelects(contactQuery, contact):
|
|
continue
|
|
resourceName = contact['resourceName']
|
|
if updateContacts:
|
|
body['etag'] = contact['etag']
|
|
existingContactGroupsList = []
|
|
for contactGroup in contact.get(PEOPLE_MEMBERSHIPS, []):
|
|
if 'contactGroupMembership' in contactGroup:
|
|
existingContactGroupsList.append(contactGroup['contactGroupMembership']['contactGroupResourceName'])
|
|
groupError = False
|
|
for field in [PEOPLE_GROUPS_LIST, PEOPLE_ADD_GROUPS_LIST, PEOPLE_REMOVE_GROUPS_LIST]:
|
|
if contactGroupsLists[field] and not validatedContactGroupsLists[field]:
|
|
status, validatedContactGroupsLists[field], _ = validatePeopleContactGroupsList(people, resourceName,
|
|
contactGroupsLists[field], entityType, user, i, count)
|
|
if not status:
|
|
groupError = True
|
|
if groupError:
|
|
break
|
|
if validatedContactGroupsLists[PEOPLE_GROUPS_LIST]:
|
|
peopleManager.AddContactGroupsToContact(body, validatedContactGroupsLists[PEOPLE_GROUPS_LIST])
|
|
updatePersonFields.add(PEOPLE_MEMBERSHIPS)
|
|
elif validatedContactGroupsLists[PEOPLE_ADD_GROUPS_LIST] or validatedContactGroupsLists[PEOPLE_REMOVE_GROUPS_LIST]:
|
|
body[PEOPLE_MEMBERSHIPS] = []
|
|
if contact.get(PEOPLE_MEMBERSHIPS):
|
|
peopleManager.AddFilteredContactGroupsToContact(body, existingContactGroupsList,
|
|
validatedContactGroupsLists[PEOPLE_REMOVE_GROUPS_LIST])
|
|
if validatedContactGroupsLists[PEOPLE_ADD_GROUPS_LIST]:
|
|
peopleManager.AddAdditionalContactGroupsToContact(body, validatedContactGroupsLists[PEOPLE_ADD_GROUPS_LIST])
|
|
updatePersonFields.add(PEOPLE_MEMBERSHIPS)
|
|
elif existingContactGroupsList:
|
|
updatePersonFields.discard(PEOPLE_MEMBERSHIPS)
|
|
else:
|
|
if not clearPeopleEmailAddressMatches(contactClear, contact):
|
|
continue
|
|
if deleteClearedContactsWithNoEmails and not contact[PEOPLE_EMAIL_ADDRESSES]:
|
|
Act.Set(Act.DELETE)
|
|
callGAPI(people.people(), 'deleteContact',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=resourceName)
|
|
entityActionPerformed([entityType, user, peopleEntityType, resourceName], j, jcount)
|
|
continue
|
|
body = contact
|
|
updatePersonFields = [PEOPLE_EMAIL_ADDRESSES]
|
|
person = callGAPI(people.people(), 'updateContact',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=resourceName,
|
|
updatePersonFields=','.join(updatePersonFields), body=body, sources=sources)
|
|
entityActionPerformed([entityType, user, peopleEntityType, person['resourceName']], j, jcount)
|
|
except GAPI.invalidArgument as e:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], str(e), j, jcount)
|
|
except (GAPI.notFound, GAPI.internalError):
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> clear contacts <PeopleResourceNameEntity>|<PeopleUserContactSelection>
|
|
# [emailclearpattern <REMatchPattern>] [emailcleartype work|home|other|<String>]
|
|
# [deleteclearedcontactswithnoemails]
|
|
def clearUserPeopleContacts(users):
|
|
_clearUpdatePeopleContacts(users, False)
|
|
|
|
# gam <UserTypeEntity> update contacts <PeopleResourceNameEntity>|(<PeopleUserContactSelection> endquery)
|
|
# <PeopleContactAttribute>+
|
|
# (contactgroup <ContactGroupItem>)*|((addcontactgroup <ContactGroupItem>)* (removecontactgroup <ContactGroupItem>)*)
|
|
# gam <UserTypeEntity> update contacts
|
|
def updateUserPeopleContacts(users):
|
|
_clearUpdatePeopleContacts(users, True)
|
|
|
|
def dedupPeopleEmailAddressMatches(emailMatchType, contact):
|
|
sai = -1
|
|
savedAddresses = []
|
|
matches = {}
|
|
updateRequired = False
|
|
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
|
|
emailAddr = item['value']
|
|
emailType = item.get('type', '')
|
|
if (emailAddr in matches) and (not emailMatchType or emailType in matches[emailAddr]['types']):
|
|
if item['metadata'].get('primary', False):
|
|
savedAddresses[matches[emailAddr]['sai']]['metadata']['primary'] = True
|
|
updateRequired = True
|
|
else:
|
|
savedAddresses.append(item)
|
|
sai += 1
|
|
matches.setdefault(emailAddr, {'types': set(), 'sai': sai})
|
|
matches[emailAddr]['types'].add(emailType)
|
|
if updateRequired:
|
|
contact[PEOPLE_EMAIL_ADDRESSES] = savedAddresses
|
|
return updateRequired
|
|
|
|
def replaceDomainPeopleEmailAddressMatches(contactQuery, contact, replaceDomains):
|
|
updateRequired = False
|
|
if contactQuery['emailMatchPattern']:
|
|
emailMatchType = contactQuery['emailMatchType']
|
|
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
|
|
emailAddr = item['value']
|
|
emailType = item.get('type', '')
|
|
if contactQuery['emailMatchPattern'].match(emailAddr):
|
|
userName, domain = splitEmailAddress(emailAddr)
|
|
domain = domain.lower()
|
|
if ((domain in replaceDomains) and
|
|
(not emailMatchType or emailType == emailMatchType)):
|
|
item['value'] = f'{userName}@{replaceDomains[domain]}'
|
|
updateRequired = True
|
|
else:
|
|
for item in contact.get(PEOPLE_EMAIL_ADDRESSES, []):
|
|
emailAddr = item['value']
|
|
emailType = item.get('type', '')
|
|
userName, domain = splitEmailAddress(emailAddr)
|
|
domain = domain.lower()
|
|
if domain in replaceDomains:
|
|
item['value'] = f'{userName}@{replaceDomains[domain]}'
|
|
updateRequired = True
|
|
return updateRequired
|
|
|
|
# gam <UserTypeEntity> dedup contacts
|
|
# [<PeopleResourceNameEntity>|<PeopleUserContactSelection>]
|
|
# [matchType [<Boolean>]]
|
|
# gam <UserTypeEntity> replacedomain contacts
|
|
# [<PeopleResourceNameEntity>|<PeopleUserContactSelection>]
|
|
# (domain <DomainName> <DomainName>)+
|
|
def dedupReplaceDomainUserPeopleContacts(users):
|
|
action = Act.Get()
|
|
entityType = Ent.USER
|
|
peopleEntityType = Ent.PEOPLE_CONTACT
|
|
sources = PEOPLE_READ_SOURCES_CHOICE_MAP['contact']
|
|
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleContactEntityList(entityType, 1, {'matchtype', 'domain'})
|
|
emailMatchType = False
|
|
replaceDomains = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if action == Act.DEDUP and myarg == 'matchtype':
|
|
emailMatchType = getBoolean()
|
|
elif action == Act.REPLACE_DOMAIN and myarg == 'domain':
|
|
domain = getString(Cmd.OB_DOMAIN_NAME).lower()
|
|
replaceDomains[domain] = getString(Cmd.OB_DOMAIN_NAME).lower()
|
|
else:
|
|
unknownArgumentExit()
|
|
if action == Act.REPLACE_DOMAIN and not replaceDomains:
|
|
missingArgumentExit('domain')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if resourceNameLists:
|
|
entityList = resourceNameLists[user]
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
if contactQuery['contactGroupSelect']:
|
|
groupId, _, contactGroupNames = validatePeopleContactGroup(people, contactQuery['contactGroupSelect'],
|
|
None, None, entityType, user, i, count)
|
|
if not groupId:
|
|
if contactGroupNames:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactQuery['contactGroupSelect']], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
contactQuery['group'] = groupId
|
|
if queriedContacts:
|
|
entityList = queryPeopleContacts(people, contactQuery, 'emailAddresses,memberships', None, entityType, user, i, count)
|
|
if entityList is None:
|
|
continue
|
|
Act.Set(action)
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Act.Set(Act.UPDATE)
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
try:
|
|
if not queriedContacts:
|
|
resourceName = contact
|
|
contact = callGAPI(people.people(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=contact, sources=sources, personFields='emailAddresses,memberships')
|
|
else:
|
|
if action == Act.DEDUP and not localPeopleContactSelects(contactQuery, contact):
|
|
continue
|
|
resourceName = contact['resourceName']
|
|
if action == Act.DEDUP:
|
|
if not dedupPeopleEmailAddressMatches(emailMatchType, contact):
|
|
continue
|
|
else:
|
|
if not replaceDomainPeopleEmailAddressMatches(contactQuery, contact, replaceDomains):
|
|
continue
|
|
Act.Set(Act.UPDATE)
|
|
callGAPI(people.people(), 'updateContact',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=resourceName,
|
|
updatePersonFields='emailAddresses', body=contact)
|
|
entityActionPerformed([entityType, user, peopleEntityType, resourceName], j, jcount)
|
|
except GAPI.invalidArgument as e:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], str(e), j, jcount)
|
|
except (GAPI.notFound, GAPI.internalError):
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delete contacts <PeopleResourceNameEntity>|<PeopleUserContactSelection>
|
|
def deleteUserPeopleContacts(users):
|
|
entityType = Ent.USER
|
|
peopleEntityType = Ent.PEOPLE_CONTACT
|
|
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleContactEntityList(entityType, -1)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if resourceNameLists:
|
|
entityList = resourceNameLists[user]
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
if contactQuery['contactGroupSelect']:
|
|
groupId, _, contactGroupNames = validatePeopleContactGroup(people, contactQuery['contactGroupSelect'],
|
|
None, None, entityType, user, i, count)
|
|
if not groupId:
|
|
if contactGroupNames:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactQuery['contactGroupSelect']], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
contactQuery['group'] = groupId
|
|
if queriedContacts:
|
|
entityList = queryPeopleContacts(people, contactQuery, 'emailAddresses', None, entityType, user, i, count)
|
|
if entityList is None:
|
|
continue
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
if isinstance(contact, dict):
|
|
if not localPeopleContactSelects(contactQuery, contact):
|
|
continue
|
|
resourceName = contact['resourceName']
|
|
else:
|
|
resourceName = normalizePeopleResourceName(contact)
|
|
try:
|
|
callGAPI(people.people(), 'deleteContact',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=resourceName)
|
|
entityActionPerformed([entityType, user, peopleEntityType, resourceName], j, jcount)
|
|
except (GAPI.notFound, GAPI.internalError):
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
Ind.Decrement()
|
|
|
|
def _initPersonMetadataParameters():
|
|
return {'strip': True, 'mapUpdateTime': False, 'sourceTypes': set()}
|
|
|
|
def _processPersonMetadata(person, parameters):
|
|
metadata = person.get(PEOPLE_METADATA, None)
|
|
if metadata is not None:
|
|
if parameters['mapUpdateTime']:
|
|
sources = person[PEOPLE_METADATA].get('sources', [])
|
|
if sources and sources[0].get(PEOPLE_UPDATE_TIME, None) is not None:
|
|
person[PEOPLE_UPDATE_TIME] = formatLocalTime(sources[0][PEOPLE_UPDATE_TIME])
|
|
if parameters['sourceTypes']:
|
|
stripKeys = []
|
|
for k, v in iter(person.items()):
|
|
if isinstance(v, list):
|
|
person[k] = []
|
|
for entry in v:
|
|
if isinstance(entry, dict):
|
|
if entry.get('metadata', {}).get('source', {}).get('type', None) in parameters['sourceTypes']:
|
|
person[k].append(entry)
|
|
else:
|
|
person[k].append(entry)
|
|
if not person[k]:
|
|
stripKeys.append(k)
|
|
for k in stripKeys:
|
|
person.pop(k, None)
|
|
if parameters['strip']:
|
|
person.pop(PEOPLE_METADATA, None)
|
|
for _, v in iter(person.items()):
|
|
if isinstance(v, list):
|
|
for entry in v:
|
|
if isinstance(entry, dict):
|
|
entry.pop(PEOPLE_METADATA, None)
|
|
|
|
def addContactGroupNamesToContacts(contacts, contactGroupIDs, showContactGroupNamesList):
|
|
for contact in contacts:
|
|
if showContactGroupNamesList:
|
|
contact[PEOPLE_GROUPS_LIST] = []
|
|
for membership in contact.get('memberships', []):
|
|
if 'contactGroupMembership' in membership:
|
|
membership['contactGroupMembership']['contactGroupName'] = contactGroupIDs.get(membership['contactGroupMembership']['contactGroupResourceName'], UNKNOWN)
|
|
if showContactGroupNamesList:
|
|
contact[PEOPLE_GROUPS_LIST].append(membership['contactGroupMembership']['contactGroupName'])
|
|
|
|
def _printPerson(entityTypeName, user, person, csvPF, FJQC, parameters):
|
|
_processPersonMetadata(person, parameters)
|
|
row = flattenJSON(person, flattened={entityTypeName: user})
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({entityTypeName: user, 'resourceName': person['resourceName'],
|
|
'JSON': json.dumps(cleanJSON(person),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
PEOPLE_CONTACT_OBJECT_KEYS = {
|
|
'addresses': 'type',
|
|
'calendarUrls': 'type',
|
|
'emailAddresses': 'type',
|
|
'events': 'type',
|
|
'externalIds': 'type',
|
|
'genders': 'value',
|
|
'imClients': 'type',
|
|
'locations': 'type',
|
|
'miscKeywords': 'type',
|
|
'nicknames': 'type',
|
|
'organizations': 'type',
|
|
'relations': 'type',
|
|
'urls': 'type',
|
|
'userDefined': 'key',
|
|
}
|
|
|
|
def _showPerson(userEntityType, user, entityType, person, i, count, FJQC, parameters):
|
|
_processPersonMetadata(person, parameters)
|
|
if not FJQC.formatJSON:
|
|
printEntity([userEntityType, user, entityType, person['resourceName']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, person, dictObjectsKey=PEOPLE_CONTACT_OBJECT_KEYS)
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(person), ensure_ascii=False, sort_keys=True))
|
|
|
|
def _printPersonEntityList(entityType, entityList, userEntityType, user, i, count, csvPF, FJQC, parameters, contactQuery):
|
|
if not csvPF:
|
|
jcount = len(entityList)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionModifierNumItems([userEntityType, user], Msg.MAXIMUM_OF, jcount, entityType, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for person in entityList:
|
|
j += 1
|
|
if not contactQuery or localPeopleContactSelects(contactQuery, person):
|
|
_showPerson(userEntityType, user, entityType, person, j, jcount, FJQC, parameters)
|
|
Ind.Decrement()
|
|
else:
|
|
entityTypeName = Ent.Singular(userEntityType)
|
|
for person in entityList:
|
|
if not contactQuery or localPeopleContactSelects(contactQuery, person):
|
|
_printPerson(entityTypeName, user, person, csvPF, FJQC, parameters)
|
|
|
|
PEOPLE_FIELDS_CHOICE_MAP = {
|
|
'additionalname': PEOPLE_NAMES,
|
|
'address': PEOPLE_ADDRESSES,
|
|
'addresses': PEOPLE_ADDRESSES,
|
|
'ageranges': 'ageRanges',
|
|
'billinginfo': PEOPLE_MISC_KEYWORDS,
|
|
'biography': PEOPLE_BIOGRAPHIES,
|
|
'biographies': PEOPLE_BIOGRAPHIES,
|
|
'birthday': PEOPLE_BIRTHDAYS,
|
|
'birthdays': PEOPLE_BIRTHDAYS,
|
|
'calendar': PEOPLE_CALENDAR_URLS,
|
|
'calendars': PEOPLE_CALENDAR_URLS,
|
|
'calendarurls': PEOPLE_CALENDAR_URLS,
|
|
'clientdata': PEOPLE_CLIENT_DATA,
|
|
'coverphotos': PEOPLE_COVER_PHOTOS,
|
|
'directoryserver': PEOPLE_MISC_KEYWORDS,
|
|
'email': PEOPLE_EMAIL_ADDRESSES,
|
|
'emails': PEOPLE_EMAIL_ADDRESSES,
|
|
'emailaddresses': PEOPLE_EMAIL_ADDRESSES,
|
|
'event': PEOPLE_EVENTS,
|
|
'events': PEOPLE_EVENTS,
|
|
'externalid': PEOPLE_EXTERNAL_IDS,
|
|
'externalids': PEOPLE_EXTERNAL_IDS,
|
|
'familyname': PEOPLE_NAMES,
|
|
'fileas': PEOPLE_FILE_ASES,
|
|
'firstname': PEOPLE_NAMES,
|
|
'gender': PEOPLE_GENDERS,
|
|
'genders': PEOPLE_GENDERS,
|
|
'givenname': PEOPLE_NAMES,
|
|
'hobby': PEOPLE_INTERESTS,
|
|
'hobbies': PEOPLE_INTERESTS,
|
|
'im': PEOPLE_IM_CLIENTS,
|
|
'ims': PEOPLE_IM_CLIENTS,
|
|
'imclients': PEOPLE_IM_CLIENTS,
|
|
'initials': PEOPLE_NICKNAMES,
|
|
'interests': PEOPLE_INTERESTS,
|
|
'jot': PEOPLE_MISC_KEYWORDS,
|
|
'jots': PEOPLE_MISC_KEYWORDS,
|
|
'language': PEOPLE_LOCALES,
|
|
'languages': PEOPLE_LOCALES,
|
|
'lastname': PEOPLE_NAMES,
|
|
'locales': PEOPLE_LOCALES,
|
|
'location': PEOPLE_LOCATIONS,
|
|
'locations': PEOPLE_LOCATIONS,
|
|
'maidenname': PEOPLE_NAMES,
|
|
'memberships': PEOPLE_MEMBERSHIPS,
|
|
'metadata': PEOPLE_METADATA,
|
|
'middlename': PEOPLE_NAMES,
|
|
'mileage': PEOPLE_MISC_KEYWORDS,
|
|
'misckeywords': PEOPLE_MISC_KEYWORDS,
|
|
'name': PEOPLE_NAMES,
|
|
'names': PEOPLE_NAMES,
|
|
'nickname': PEOPLE_NICKNAMES,
|
|
'nicknames': PEOPLE_NICKNAMES,
|
|
'note': PEOPLE_BIOGRAPHIES,
|
|
'notes': PEOPLE_BIOGRAPHIES,
|
|
'occupation': PEOPLE_OCCUPATIONS,
|
|
'occupations': PEOPLE_OCCUPATIONS,
|
|
'organization': PEOPLE_ORGANIZATIONS,
|
|
'organizations': PEOPLE_ORGANIZATIONS,
|
|
'organisation': PEOPLE_ORGANIZATIONS,
|
|
'organisations': PEOPLE_ORGANIZATIONS,
|
|
'phone': PEOPLE_PHONE_NUMBERS,
|
|
'phones': PEOPLE_PHONE_NUMBERS,
|
|
'phonenumbers': PEOPLE_PHONE_NUMBERS,
|
|
'photo': PEOPLE_PHOTOS,
|
|
'photos': PEOPLE_PHOTOS,
|
|
'prefix': PEOPLE_NAMES,
|
|
'priority': PEOPLE_MISC_KEYWORDS,
|
|
'relation': PEOPLE_RELATIONS,
|
|
'relations': PEOPLE_RELATIONS,
|
|
'sensitivity': PEOPLE_MISC_KEYWORDS,
|
|
'shortname': PEOPLE_NICKNAMES,
|
|
'sipaddress': PEOPLE_SIP_ADDRESSES,
|
|
'sipaddresses': PEOPLE_SIP_ADDRESSES,
|
|
'skills': PEOPLE_SKILLS,
|
|
'subject': PEOPLE_MISC_KEYWORDS,
|
|
'suffix': PEOPLE_NAMES,
|
|
'updated': PEOPLE_UPDATE_TIME,
|
|
'updatetime': PEOPLE_UPDATE_TIME,
|
|
'urls': PEOPLE_URLS,
|
|
'userdefined': PEOPLE_USER_DEFINED,
|
|
'userdefinedfield': PEOPLE_USER_DEFINED,
|
|
'userdefinedfields': PEOPLE_USER_DEFINED,
|
|
'website': PEOPLE_URLS,
|
|
'websites': PEOPLE_URLS,
|
|
}
|
|
|
|
PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP = {
|
|
'email': PEOPLE_EMAIL_ADDRESSES,
|
|
'emails': PEOPLE_EMAIL_ADDRESSES,
|
|
'emailaddresses': PEOPLE_EMAIL_ADDRESSES,
|
|
'metadata': PEOPLE_METADATA,
|
|
'names': PEOPLE_NAMES,
|
|
'phone': PEOPLE_PHONE_NUMBERS,
|
|
'phones': PEOPLE_PHONE_NUMBERS,
|
|
'phonenumbers': PEOPLE_PHONE_NUMBERS,
|
|
'photo': PEOPLE_PHOTOS,
|
|
'photos': PEOPLE_PHOTOS,
|
|
}
|
|
|
|
PEOPLE_CONTACTS_DEFAULT_FIELDS = ['names', 'emailaddresses', 'phonenumbers']
|
|
|
|
PEOPLE_ORDERBY_CHOICE_MAP = {
|
|
'firstname': 'FIRST_NAME_ASCENDING',
|
|
'lastname': 'LAST_NAME_ASCENDING',
|
|
'lastmodified': 'LAST_MODIFIED_',
|
|
}
|
|
|
|
def getPersonFieldsList(myarg, fieldsChoiceMap, fieldsList, initialField=None, fieldsArg='fields'):
|
|
if fieldsList is None:
|
|
fieldsList = []
|
|
return getFieldsList(myarg, fieldsChoiceMap, fieldsList, initialField, fieldsArg)
|
|
|
|
def _getPersonFields(fieldsChoiceMap, defaultFields, fieldsList, parameters):
|
|
if fieldsList is None:
|
|
fieldsList = []
|
|
for field in fieldsChoiceMap:
|
|
addFieldToFieldsList(field, fieldsChoiceMap, fieldsList)
|
|
elif not fieldsList:
|
|
for field in defaultFields:
|
|
addFieldToFieldsList(field, fieldsChoiceMap, fieldsList)
|
|
fieldsList = list(set(fieldsList))
|
|
if PEOPLE_UPDATE_TIME in fieldsList:
|
|
parameters['mapUpdateTime'] = True
|
|
fieldsList.remove(PEOPLE_UPDATE_TIME)
|
|
fieldsList.append(PEOPLE_METADATA)
|
|
return ','.join(fieldsList)
|
|
|
|
def _infoPeople(users, entityType, source):
|
|
if entityType == Ent.DOMAIN:
|
|
people = buildGAPIObject(API.PEOPLE)
|
|
peopleEntityType = Ent.DOMAIN_PROFILE if source == 'profile' else Ent.PEOPLE_CONTACT
|
|
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP[source]]
|
|
entityList = getEntityList(Cmd.OB_CONTACT_ENTITY)
|
|
resourceNameLists = entityList if isinstance(entityList, dict) else None
|
|
showContactGroups = False
|
|
FJQC = FormatJSONQuoteChar()
|
|
fieldsList = []
|
|
parameters = _initPersonMetadataParameters()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'allfields':
|
|
fieldsList = None
|
|
elif getPersonFieldsList(myarg, PEOPLE_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
elif myarg == 'showgroups':
|
|
showContactGroups = True
|
|
elif myarg == 'showmetadata':
|
|
parameters['strip'] = False
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if resourceNameLists:
|
|
entityList = resourceNameLists[user]
|
|
contactGroupIDs = contactGroupNames = None
|
|
if entityType != Ent.DOMAIN:
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
if showContactGroups:
|
|
contactGroupIDs, contactGroupNames = getPeopleContactGroupsInfo(people, entityType, user, i, count)
|
|
if contactGroupNames is False:
|
|
continue
|
|
j = 0
|
|
jcount = len(entityList)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([entityType, user], jcount, peopleEntityType, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
if isinstance(contact, dict):
|
|
resourceName = contact['resourceName']
|
|
else:
|
|
resourceName = normalizePeopleResourceName(contact)
|
|
try:
|
|
result = callGAPI(people.people(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR, GAPI.INVALID_ARGUMENT]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=resourceName, sources=sources, personFields=fields)
|
|
except (GAPI.notFound, GAPI.internalError):
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
|
|
continue
|
|
except GAPI.invalidArgument as e:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
if showContactGroups and contactGroupIDs:
|
|
addContactGroupNamesToContacts([result], contactGroupIDs, False)
|
|
_showPerson(entityType, user, peopleEntityType, result, j, jcount, FJQC, parameters)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> info contacts <PeopleResourceNameEntity>
|
|
# [showgroups]
|
|
# [allFields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [formatjson]
|
|
def infoUserPeopleContacts(users):
|
|
_infoPeople(users, Ent.USER, 'contact')
|
|
|
|
# gam <UserTypeEntity> print contacts [todrive <ToDriveAttribute>*] <PeoplePrintShowUserContactSelection>
|
|
# [showgroups|showgroupnameslist] [orderby firstname|lastname|(lastmodified ascending)|(lastnodified descending)
|
|
# [countsonly|allfields|fields <PeopleFieldNameList>] [showmetadata]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show contacts <PeoplePrintShowUserContactSelection>
|
|
# [showgroups] [orderby firstname|lastname|(lastmodified ascending)|(lastnodified descending)
|
|
# [countsonly|allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [formatjson]
|
|
def printShowUserPeopleContacts(users):
|
|
entityType = Ent.USER
|
|
entityTypeName = Ent.Singular(entityType)
|
|
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
CSVTitle = 'People Contacts'
|
|
fieldsList = []
|
|
parameters = _initPersonMetadataParameters()
|
|
sortOrder = None
|
|
countsOnly = showContactGroups = showContactGroupNamesList = False
|
|
contactQuery = _initPeopleContactQueryAttributes(True)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'showgroups':
|
|
showContactGroups = True
|
|
elif myarg == 'showgroupnameslist':
|
|
showContactGroups = showContactGroupNamesList = True
|
|
elif myarg == 'allfields':
|
|
fieldsList = None
|
|
elif getPersonFieldsList(myarg, PEOPLE_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
if csvPF:
|
|
csvPF.SetTitles([entityTypeName, CSVTitle])
|
|
elif myarg == 'showmetadata':
|
|
parameters['strip'] = False
|
|
elif myarg == 'orderby':
|
|
sortOrder = getChoice(PEOPLE_ORDERBY_CHOICE_MAP, mapChoice=True)
|
|
if sortOrder == 'LAST_MODIFIED_':
|
|
sortOrder += getChoice(SORTORDER_CHOICE_MAP, defaultChoice='DESCENDING', mapChoice=True)
|
|
elif _getPeopleContactQueryAttributes(contactQuery, myarg, entityType, 0, True):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if countsOnly:
|
|
fieldsList = ['emailAddresses']
|
|
if contactQuery['mainContacts']:
|
|
fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
|
|
if contactQuery['contactGroupFilter'] and 'memberships' not in fields:
|
|
fields += ',memberships'
|
|
contactQuery['dropMemberships'] = True
|
|
if contactQuery['otherContacts']:
|
|
if not fieldsList:
|
|
ofields = _getPersonFields(PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
|
|
else:
|
|
ofields = getFieldsFromFieldsList([PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP[field.lower()] for field in fieldsList if field.lower() in PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
if contactQuery['otherContacts']:
|
|
_, opeople = buildGAPIServiceObject(API.PEOPLE_OTHERCONTACTS, user, i, count)
|
|
if not opeople:
|
|
continue
|
|
contactGroupIDs = contactGroupNames = None
|
|
if showContactGroups:
|
|
contactGroupIDs, contactGroupNames = getPeopleContactGroupsInfo(people, entityType, user, i, count)
|
|
if contactGroupNames is False:
|
|
continue
|
|
contactGroupSelectFilter = contactQuery['contactGroupSelect'] or contactQuery['contactGroupFilter']
|
|
if contactGroupSelectFilter:
|
|
groupId, _, contactGroupNames =\
|
|
validatePeopleContactGroup(people, contactGroupSelectFilter,
|
|
contactGroupIDs, contactGroupNames, entityType, user, i, count)
|
|
if not groupId:
|
|
if contactGroupNames:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroupSelectFilter], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
contactQuery['group'] = groupId
|
|
if contactQuery['mainContacts']:
|
|
contacts = queryPeopleContacts(people, contactQuery, fields, sortOrder, entityType, user, i, count)
|
|
else:
|
|
contacts = []
|
|
if contactQuery['otherContacts']:
|
|
ocontacts = queryPeopleOtherContacts(opeople, contactQuery, ofields, entityType, user, i, count)
|
|
else:
|
|
ocontacts = []
|
|
if countsOnly:
|
|
jcount = countLocalPeopleContactSelects(contactQuery, contacts)+countLocalPeopleContactSelects(contactQuery, ocontacts)
|
|
if csvPF:
|
|
csvPF.WriteRowTitles({entityTypeName: user, CSVTitle: jcount})
|
|
else:
|
|
printEntityKVList([entityType, user], [CSVTitle, jcount], i, count)
|
|
elif contacts is not None or ocontacts is not None:
|
|
if not csvPF:
|
|
if contacts is not None and contactQuery['mainContacts']:
|
|
if showContactGroups and contactGroupIDs:
|
|
addContactGroupNamesToContacts(contacts, contactGroupIDs, False)
|
|
_printPersonEntityList(Ent.PEOPLE_CONTACT, contacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
|
|
if ocontacts is not None and contactQuery['otherContacts']:
|
|
_printPersonEntityList(Ent.OTHER_CONTACT, ocontacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
|
|
elif contacts or ocontacts:
|
|
if contacts:
|
|
if showContactGroups and contactGroupIDs:
|
|
addContactGroupNamesToContacts(contacts, contactGroupIDs, showContactGroupNamesList and FJQC.formatJSON)
|
|
_printPersonEntityList(Ent.PEOPLE_CONTACT, contacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
|
|
if ocontacts:
|
|
_printPersonEntityList(Ent.OTHER_CONTACT, ocontacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({Ent.Singular(entityType): user})
|
|
if csvPF:
|
|
csvPF.writeCSVfile(CSVTitle)
|
|
|
|
CONTACTGROUPS_MYCONTACTS_ID = 'contactGroups/myContacts'
|
|
CONTACTGROUPS_MYCONTACTS_NAME = 'My Contacts'
|
|
|
|
# gam <UserTypeEntity> copy othercontacts
|
|
# <OtherContactResourceNameEntity>|<OtherContactSelection>
|
|
def copyUserPeopleOtherContacts(users):
|
|
entityType = Ent.USER
|
|
peopleEntityType = Ent.OTHER_CONTACT
|
|
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['contact']]
|
|
copyMask = ['emailAddresses', 'names', 'phoneNumbers']
|
|
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleOtherContactEntityList(-1)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if resourceNameLists:
|
|
entityList = resourceNameLists[user]
|
|
user, people = buildGAPIServiceObject(API.PEOPLE_OTHERCONTACTS, user, i, count)
|
|
if not people:
|
|
continue
|
|
if queriedContacts:
|
|
entityList = queryPeopleOtherContacts(people, contactQuery, 'emailAddresses', entityType, user, i, count)
|
|
if entityList is None:
|
|
continue
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
if isinstance(contact, dict):
|
|
if not localPeopleContactSelects(contactQuery, contact):
|
|
continue
|
|
resourceName = contact['resourceName']
|
|
else:
|
|
resourceName = normalizeOtherContactsResourceName(contact)
|
|
try:
|
|
callGAPI(people.otherContacts(), 'copyOtherContactToMyContactsGroup',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
resourceName=resourceName, body={'copyMask': ','.join(copyMask), 'sources': sources})
|
|
entityModifierNewValueActionPerformed([entityType, user, peopleEntityType, resourceName], Act.MODIFIER_TO, CONTACTGROUPS_MYCONTACTS_NAME)
|
|
except (GAPI.notFound, GAPI.internalError):
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
|
|
continue
|
|
except GAPI.permissionDenied as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delete othercontacts
|
|
# <OtherContactResourceNameEntity>|<OtherContactSelection>
|
|
# gam <UserTypeEntity> move othercontacts
|
|
# <OtherContactResourceNameEntity>|<OtherContactSelection>
|
|
# gam <UserTypeEntity> update othercontacts
|
|
# <OtherResourceNameEntity>|<OtherContactSelection>
|
|
# <PeopleContactAttribute>*
|
|
# (contactgroup <ContactGroupItem>)*
|
|
def processUserPeopleOtherContacts(users):
|
|
action = Act.Get()
|
|
entityType = Ent.USER
|
|
peopleEntityType = Ent.OTHER_CONTACT
|
|
sources = PEOPLE_READ_SOURCES_CHOICE_MAP['contact']
|
|
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleOtherContactEntityList(1)
|
|
if action == Act.UPDATE:
|
|
Act.Set(Act.UPDATE_MOVE)
|
|
peopleManager = PeopleManager()
|
|
body, updatePersonFields, contactGroupsLists = peopleManager.GetPersonFields(entityType, False)
|
|
else:
|
|
body = {PEOPLE_MEMBERSHIPS: [{'contactGroupMembership': {'contactGroupResourceName': CONTACTGROUPS_MYCONTACTS_ID}}]}
|
|
updatePersonFields = [PEOPLE_MEMBERSHIPS]
|
|
checkForExtraneousArguments()
|
|
validatedContactGroupsList = [CONTACTGROUPS_MYCONTACTS_ID]
|
|
contactGroupIDs = {CONTACTGROUPS_MYCONTACTS_ID: CONTACTGROUPS_MYCONTACTS_NAME}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if resourceNameLists:
|
|
entityList = resourceNameLists[user]
|
|
user, people = buildGAPIServiceObject(API.PEOPLE_OTHERCONTACTS, user, i, count)
|
|
if not people:
|
|
continue
|
|
_, upeople = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not upeople:
|
|
continue
|
|
if queriedContacts:
|
|
entityList = queryPeopleOtherContacts(people, contactQuery, 'emailAddresses', entityType, user, i, count)
|
|
if entityList is None:
|
|
continue
|
|
else:
|
|
otherContacts = _getPeopleOtherContacts(people, entityType, user, i=0, count=0)
|
|
if otherContacts is None:
|
|
continue
|
|
if action == Act.UPDATE:
|
|
if contactGroupsLists[PEOPLE_GROUPS_LIST]:
|
|
result, validatedContactGroupsList, contactGroupIDs =\
|
|
validatePeopleContactGroupsList(people, '', contactGroupsLists[PEOPLE_GROUPS_LIST], entityType, user, i, count)
|
|
if not result:
|
|
continue
|
|
if CONTACTGROUPS_MYCONTACTS_ID not in validatedContactGroupsList:
|
|
validatedContactGroupsList.insert(0, CONTACTGROUPS_MYCONTACTS_ID)
|
|
updatePersonFields.add(PEOPLE_MEMBERSHIPS)
|
|
contactGroupNamesList = []
|
|
for resourceName in validatedContactGroupsList:
|
|
contactGroupNamesList.append(contactGroupIDs[resourceName])
|
|
contactGroupNames = ','.join(contactGroupNamesList)
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, peopleEntityType, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
if isinstance(contact, dict):
|
|
if not localPeopleContactSelects(contactQuery, contact):
|
|
continue
|
|
resourceName = contact['resourceName']
|
|
else:
|
|
resourceName = normalizeOtherContactsResourceName(contact)
|
|
contact = otherContacts.get(resourceName)
|
|
if contact is None:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
|
|
continue
|
|
peopleResourceName = resourceName.replace('otherContacts', 'people')
|
|
body['etag'] = contact['etag']
|
|
if action == Act.UPDATE and validatedContactGroupsList:
|
|
peopleManager.AddContactGroupsToContact(body, validatedContactGroupsList)
|
|
try:
|
|
callGAPI(upeople.people(), 'updateContact',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=peopleResourceName,
|
|
updatePersonFields=','.join(updatePersonFields), body=body, sources=sources)
|
|
if action != Act.DELETE:
|
|
entityModifierNewValueActionPerformed([entityType, user, peopleEntityType, resourceName],
|
|
Act.MODIFIER_TO, contactGroupNames, j, jcount)
|
|
else:
|
|
maxRetries = 5
|
|
for retry in range(1, maxRetries+1):
|
|
try:
|
|
callGAPI(upeople.people(), 'deleteContact',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=peopleResourceName)
|
|
entityActionPerformed([entityType, user, peopleEntityType, resourceName], j, jcount)
|
|
break
|
|
except (GAPI.notFound, GAPI.internalError):
|
|
if retry == maxRetries:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
|
|
break
|
|
time.sleep(retry*2)
|
|
except GAPI.invalidArgument as e:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.notFound, GAPI.internalError):
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
|
|
continue
|
|
except GAPI.permissionDenied as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print othercontacts [todrive <ToDriveAttribute>*] <OtherContactSelection>
|
|
# [countsonly|allfields|(fields <OtherContactFieldNameList>)] [showmetadata]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show othercontacts <OtherContactSelection>
|
|
# [countsonly|allfields|(fields <OtherContactFieldNameList>)] [showmetadata]
|
|
# [formatjson]
|
|
def printShowUserPeopleOtherContacts(users):
|
|
entityType = Ent.USER
|
|
entityTypeName = Ent.Singular(entityType)
|
|
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
CSVTitle = 'Other Contacts'
|
|
fieldsList = []
|
|
parameters = _initPersonMetadataParameters()
|
|
countsOnly = False
|
|
contactQuery = _initPeopleOtherContactQueryAttributes()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'allfields':
|
|
fieldsList = None
|
|
elif getPersonFieldsList(myarg, PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
if csvPF:
|
|
csvPF.SetTitles([entityTypeName, CSVTitle])
|
|
elif myarg == 'showmetadata':
|
|
parameters['strip'] = False
|
|
elif _getPeopleOtherContactQueryAttributes(contactQuery, myarg, 0):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if countsOnly:
|
|
fieldsList = ['emailAddresses']
|
|
fields = _getPersonFields(PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, people = buildGAPIServiceObject(API.PEOPLE_OTHERCONTACTS, user, i, count)
|
|
if not people:
|
|
continue
|
|
contacts = queryPeopleOtherContacts(people, contactQuery, fields, entityType, user, i, count)
|
|
if countsOnly:
|
|
jcount = countLocalPeopleContactSelects(contactQuery, contacts)
|
|
if csvPF:
|
|
csvPF.WriteRowTitles({entityTypeName: user, CSVTitle: jcount})
|
|
else:
|
|
printEntityKVList([entityType, user], [CSVTitle, jcount], i, count)
|
|
elif contacts is not None:
|
|
if not csvPF:
|
|
_printPersonEntityList(Ent.OTHER_CONTACT, contacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
|
|
elif contacts:
|
|
_printPersonEntityList(Ent.OTHER_CONTACT, contacts, entityType, user, i, count, csvPF, FJQC, parameters, contactQuery)
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({Ent.Singular(entityType): user})
|
|
if csvPF:
|
|
csvPF.writeCSVfile(CSVTitle)
|
|
|
|
def _printShowPeople(source):
|
|
people = buildGAPIObject(API.PEOPLE_DIRECTORY)
|
|
entityType = Ent.DOMAIN
|
|
entityTypeName = Ent.Singular(entityType)
|
|
sources = [PEOPLE_DIRECTORY_SOURCES_CHOICE_MAP[source]]
|
|
if sources[0] == 'DIRECTORY_SOURCE_TYPE_DOMAIN_PROFILE':
|
|
peopleEntityType = Ent.DOMAIN_PROFILE
|
|
CSVTitle = 'People Profiles'
|
|
else:
|
|
peopleEntityType = Ent.DOMAIN_PEOPLE_CONTACT
|
|
CSVTitle = 'People Contacts'
|
|
function = 'listDirectoryPeople'
|
|
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
parameters = _initPersonMetadataParameters()
|
|
mergeSources = []
|
|
fieldsList = []
|
|
parameters = _initPersonMetadataParameters()
|
|
countsOnly = False
|
|
kwargs = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif source is None and myarg in {'source', 'sources'}:
|
|
sources = [getChoice(PEOPLE_DIRECTORY_SOURCES_CHOICE_MAP, mapChoice=True)]
|
|
elif myarg in {'mergesource', 'mergesources'}:
|
|
mergeSources = [getChoice(PEOPLE_DIRECTORY_MERGE_SOURCES_CHOICE_MAP, mapChoice=True)]
|
|
elif myarg == 'showmetadata':
|
|
parameters['strip'] = False
|
|
elif myarg == 'allfields':
|
|
fieldsList = None
|
|
elif getPersonFieldsList(myarg, PEOPLE_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
fieldsList = [PEOPLE_METADATA]
|
|
if csvPF:
|
|
csvPF.SetTitles([entityTypeName, CSVTitle])
|
|
elif myarg == 'query':
|
|
kwargs['query'] = getString(Cmd.OB_QUERY)
|
|
function = 'searchDirectoryPeople'
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
|
|
printGettingAllEntityItemsForWhom(peopleEntityType, GC.Values[GC.DOMAIN], query=kwargs.get('query'))
|
|
try:
|
|
entityList = callGAPIpages(people.people(), function, 'people',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
|
|
sources=sources, mergeSources=mergeSources,
|
|
readMask=fields, fields='nextPageToken,people', **kwargs)
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
if not countsOnly:
|
|
_printPersonEntityList(peopleEntityType, entityList, Ent.DOMAIN, GC.Values[GC.DOMAIN], 0, 0, csvPF, FJQC, parameters, None)
|
|
else:
|
|
jcount = len(entityList)
|
|
if csvPF:
|
|
csvPF.WriteRowTitles({entityTypeName: GC.Values[GC.DOMAIN], CSVTitle: jcount})
|
|
else:
|
|
printEntityKVList([entityType, GC.Values[GC.DOMAIN]], [CSVTitle, jcount])
|
|
if csvPF:
|
|
csvPF.writeCSVfile(CSVTitle)
|
|
|
|
# gam info people|peopleprofile <PeopleResourceNameEntity>
|
|
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [formatjson]
|
|
def doInfoDomainPeopleProfile():
|
|
_infoPeople([GC.Values[GC.DOMAIN]], Ent.DOMAIN, 'profile')
|
|
|
|
# gam info peoplecontact <PeopleResourceNameEntity>
|
|
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [formatjson]
|
|
def doInfoDomainPeopleContacts():
|
|
_infoPeople([GC.Values[GC.DOMAIN]], Ent.DOMAIN, 'domaincontact')
|
|
|
|
# gam print people|peopleprofile [todrive <ToDriveAttribute>*]
|
|
# [query <String>]
|
|
# [mergesources <PeopleMergeSourceName>]
|
|
# [countsonly]
|
|
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show people|peopleprofile
|
|
# [query <String>]
|
|
# [mergesources <PeopleMergeSourceName>]
|
|
# [countsonly]
|
|
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [formatjson]
|
|
# gam print domaincontacts|peoplecontacts [todrive <ToDriveAttribute>*]
|
|
# [sources <PeopleSourceName>]
|
|
# [query <String>]
|
|
# [mergesources <PeopleMergeSourceName>]
|
|
# [countsonly]
|
|
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show domaincontacts|peoplecontacts
|
|
# [sources <PeopleSourceName>]
|
|
# [query <String>]
|
|
# [mergesources <PeopleMergeSourceName>]
|
|
# [countsonly]
|
|
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [formatjson]
|
|
def doPrintShowDomainPeopleProfiles():
|
|
_printShowPeople('profile')
|
|
|
|
def doPrintShowDomainPeopleContacts():
|
|
_printShowPeople('domaincontact')
|
|
|
|
PEOPLE_PROFILE_SOURCETYPE_CHOICE_MAP = {
|
|
'account': 'ACCOUNT',
|
|
'accounts': 'ACCOUNT',
|
|
'domain': 'DOMAIN_PROFILE',
|
|
'domains': 'DOMAIN_PROFILE',
|
|
'profile': 'PROFILE',
|
|
'profiles': 'PROFILE',
|
|
}
|
|
|
|
# gam <UserTypeEntity> print peopleprofile [todrive <ToDriveAttribute>*]
|
|
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [sources <PeopleProfileSourceNameList>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show peopleprofile
|
|
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [sources <PeopleProfileSourceNameList>]
|
|
# [formatjson]
|
|
def printShowUserPeopleProfiles(users):
|
|
entityType = Ent.USER
|
|
entityTypeName = Ent.Singular(entityType)
|
|
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['profile']]
|
|
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
parameters = _initPersonMetadataParameters()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'allfields':
|
|
fieldsList = None
|
|
elif getPersonFieldsList(myarg, PEOPLE_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
elif myarg in {'source', 'sources'}:
|
|
for field in _getFieldsList():
|
|
if field in PEOPLE_PROFILE_SOURCETYPE_CHOICE_MAP:
|
|
parameters['sourceTypes'].add(PEOPLE_PROFILE_SOURCETYPE_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, PEOPLE_PROFILE_SOURCETYPE_CHOICE_MAP, True)
|
|
elif myarg == 'showmetadata':
|
|
parameters['strip'] = False
|
|
elif myarg == 'peoplelookupuser':
|
|
deprecatedArgument(myarg)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
fields = _getPersonFields(PEOPLE_FIELDS_CHOICE_MAP, PEOPLE_CONTACTS_DEFAULT_FIELDS, fieldsList, parameters)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
# user, people = buildGAPIServiceObject(API.PEOPLE_DIRECTORY, user, i, count)
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.PEOPLE_PROFILE, user, i, count)
|
|
try:
|
|
result = callGAPI(people.people(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName='people/me', sources=sources, personFields=fields)
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.PEOPLE_PROFILE, user, i, count)
|
|
continue
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
if not csvPF:
|
|
_showPerson(entityType, user, Ent.PEOPLE_PROFILE, result, i, count, FJQC, parameters)
|
|
else:
|
|
_printPerson(entityTypeName, user, result, csvPF, FJQC, parameters)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('People Profiles')
|
|
|
|
def _processPeopleContactPhotos(users, function):
|
|
def _makeFilenameFromPattern(resourceName):
|
|
filename = filenamePattern[:]
|
|
if subForContactId:
|
|
filename = filename.replace('#contactid#', resourceName.split('/')[1])
|
|
if subForEmail:
|
|
for email in result.get(PEOPLE_EMAIL_ADDRESSES, []):
|
|
if email.get(PEOPLE_METADATA, {}).get('primary', False):
|
|
filename = filename.replace('#email#', email['value'])
|
|
break
|
|
else:
|
|
filename = filename.replace('#email#', resourceName.split('/')[1])
|
|
return filename
|
|
|
|
if users is not None:
|
|
entityType = Ent.USER
|
|
peopleEntityType = Ent.PEOPLE_CONTACT
|
|
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['contact']]
|
|
else:
|
|
users = [None]
|
|
people = buildGAPIObject(API.PEOPLE_DIRECTORY)
|
|
entityType = Ent.DOMAIN
|
|
peopleEntityType = Ent.PEOPLE_CONTACT
|
|
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['domaincontact']]
|
|
entityList, resourceNameLists, contactQuery, queriedContacts = _getPeopleContactEntityList(entityType, 1)
|
|
if function in {'updateContactPhoto', 'getContactPhoto'}:
|
|
targetFolder = os.getcwd()
|
|
filenamePattern = '#contactid#.jpg'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'drivedir':
|
|
targetFolder = GC.Values[GC.DRIVE_DIR]
|
|
elif myarg in {'sourcefolder', 'targetfolder'}:
|
|
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
if function == 'GetContactPhoto' and not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
elif myarg == 'filename':
|
|
filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN)
|
|
else:
|
|
unknownArgumentExit()
|
|
subForContactId = filenamePattern.find('#contactid#') != -1
|
|
subForEmail = filenamePattern.find('#email#') != -1
|
|
if not subForContactId and not subForEmail:
|
|
filename = filenamePattern
|
|
else: #elif function == 'deleteContactPhoto':
|
|
checkForExtraneousArguments()
|
|
subForContactId = subForEmail = False
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if resourceNameLists:
|
|
entityList = resourceNameLists[user]
|
|
if user is not None:
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
else:
|
|
user = Ent.Singular(entityType)
|
|
if contactQuery['contactGroupSelect']:
|
|
groupId, _, contactGroupNames = validatePeopleContactGroup(people, contactQuery['contactGroupSelect'],
|
|
None, None, entityType, user, i, count)
|
|
if not groupId:
|
|
if contactGroupNames:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactQuery['contactGroupSelect']], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
contactQuery['group'] = groupId
|
|
if queriedContacts:
|
|
entityList = queryPeopleContacts(people, contactQuery, 'emailAddresses,photos', None, entityType, user, i, count)
|
|
if entityList is None:
|
|
continue
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionModifierNumItems([entityType, user], Msg.MAXIMUM_OF, jcount, Ent.PHOTO, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
for contact in entityList:
|
|
j += 1
|
|
if isinstance(contact, dict):
|
|
if not localPeopleContactSelects(contactQuery, contact):
|
|
continue
|
|
resourceName = contact['resourceName']
|
|
else:
|
|
resourceName = normalizePeopleResourceName(contact)
|
|
try:
|
|
if subForEmail:
|
|
result = callGAPI(people.people(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=resourceName, sources=sources, personFields='emailAddresses')
|
|
if function == 'updateContactPhoto':
|
|
if subForContactId or subForEmail:
|
|
filename = _makeFilenameFromPattern(resourceName)
|
|
filename = os.path.join(targetFolder, filename)
|
|
with open(os.path.expanduser(filename), 'rb') as f:
|
|
image_data = f.read()
|
|
callGAPI(people.people(), function,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
resourceName=resourceName,
|
|
body={'photoBytes': base64.urlsafe_b64encode(image_data).decode(UTF8)})
|
|
entityActionPerformed([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount)
|
|
elif function == 'getContactPhoto':
|
|
if subForContactId or subForEmail:
|
|
filename = _makeFilenameFromPattern(resourceName)
|
|
filename = os.path.join(targetFolder, filename)
|
|
result = callGAPI(people.people(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=resourceName, personFields='photos')
|
|
url = None
|
|
for photo in result.get('photos', []):
|
|
if photo['metadata']['source']['type'] == 'CONTACT':
|
|
url = photo['url']
|
|
break
|
|
if not url:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, None], Msg.CONTACT_PHOTO_NOT_FOUND, j, jcount)
|
|
continue
|
|
try:
|
|
status, photo_data = getHttpObj().request(url, 'GET')
|
|
if status['status'] != '200':
|
|
entityActionFailedWarning([entityType, user, Ent.PHOTO, filename], Msg.NOT_ALLOWED, j, jcount)
|
|
continue
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], str(e), j, jcount)
|
|
continue
|
|
status, e = writeFileReturnError(filename, photo_data, mode='wb')
|
|
if status:
|
|
entityActionPerformed([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount)
|
|
else:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], str(e), j, jcount)
|
|
else: #elif function == 'deleteContactPhoto':
|
|
filename = ''
|
|
callGAPI(people.people(), function,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR, GAPI.PHOTO_NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
resourceName=resourceName)
|
|
entityActionPerformed([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount)
|
|
except (GAPI.notFound, GAPI.internalError):
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName], Msg.DOES_NOT_EXIST, j, jcount)
|
|
continue
|
|
except GAPI.photoNotFound:
|
|
entityDoesNotHaveItemWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount)
|
|
except (GAPI.invalidArgument, OSError, IOError) as e:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], str(e), j, jcount)
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> update contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
|
|
# [drivedir|(sourcefolder <FilePath>)] [filename <FileNamePattern>]
|
|
def updateUserPeopleContactPhoto(users):
|
|
_processPeopleContactPhotos(users, 'updateContactPhoto')
|
|
|
|
# gam <UserTypeEntity> get contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
|
|
# [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]
|
|
def getUserPeopleContactPhoto(users):
|
|
_processPeopleContactPhotos(users, 'getContactPhoto')
|
|
|
|
# gam <UserTypeEntity> delete contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
|
|
def deleteUserPeopleContactPhoto(users):
|
|
_processPeopleContactPhotos(users, 'deleteContactPhoto')
|
|
|
|
# gam update contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
|
|
# [drivedir|(sourcefolder <FilePath>)] [filename <FileNamePattern>]
|
|
def doUpdateDomainContactPhoto():
|
|
_processPeopleContactPhotos(None, 'updateContactPhoto')
|
|
|
|
# gam get contactphotos <PeopleResourceNameEntity>|<PeopleUserContactSelection>
|
|
# [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]
|
|
def doGetDomainContactPhoto():
|
|
_processPeopleContactPhotos(None, 'getContactPhoto')
|
|
|
|
# gam delete contactphoto <PeopleResourceNameEntity>|<PeopleUserContactSelection>
|
|
def doDeleteDomainContactPhoto():
|
|
_processPeopleContactPhotos(None, 'deleteContactPhoto')
|
|
|
|
# gam <UserTypeEntity> create contactgroup <ContactGroupAttribute>+
|
|
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*))| returnidonly]
|
|
def createUserPeopleContactGroup(users):
|
|
peopleManager = PeopleManager()
|
|
entityType = Ent.USER
|
|
parameters = {'csvPF': None, 'titles': ['User', 'resourceName'], 'addCSVData': {}, 'returnIdOnly': False}
|
|
body, _ = peopleManager.GetContactGroupFields(parameters)
|
|
if PEOPLE_GROUP_NAME not in body:
|
|
return
|
|
csvPF = parameters['csvPF']
|
|
addCSVData = parameters['addCSVData']
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
returnIdOnly = parameters['returnIdOnly']
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
_, contactGroupNames = getPeopleContactGroupsInfo(people, entityType, user, i, count)
|
|
if contactGroupNames is False:
|
|
continue
|
|
contactGroup = body[PEOPLE_GROUP_NAME]
|
|
if contactGroup in contactGroupNames:
|
|
entityActionFailedWarning([entityType, user], Ent.TypeNameMessage(Ent.CONTACT_GROUP, contactGroup, Msg.DUPLICATE), i, count)
|
|
continue
|
|
try:
|
|
result = callGAPI(people.contactGroups(), 'create',
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
body={'contactGroup': body}, fields='resourceName')
|
|
resourceName = result['resourceName']
|
|
if returnIdOnly:
|
|
writeStdout(f'{resourceName}\n')
|
|
elif not csvPF:
|
|
entityActionPerformed([entityType, user, Ent.CONTACT_GROUP, resourceName], i, count)
|
|
else:
|
|
row = {'User': user, 'resourceName': resourceName}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
except (GAPI.forbidden, GAPI.permissionDenied):
|
|
userPeopleServiceNotEnabledWarning(user, i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
entityUnknownWarning(entityType, user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('People Contact Groups')
|
|
|
|
# gam <UserTypeEntity> update contactgroups <ContactGroupItem> <ContactAttribute>+
|
|
def updateUserPeopleContactGroup(users):
|
|
peopleManager = PeopleManager()
|
|
entityType = Ent.USER
|
|
entityList = getStringReturnInList(Cmd.OB_CONTACT_GROUP_ITEM)
|
|
body, fields = peopleManager.GetContactGroupFields()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
contactGroupIDs = contactGroupNames = None
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionNumItems([entityType, user], jcount, Ent.CONTACT_GROUP, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
for contactGroup in entityList:
|
|
j += 1
|
|
try:
|
|
groupId, contactGroupIDs, contactGroupNames = validatePeopleContactGroup(people, contactGroup,
|
|
contactGroupIDs, contactGroupNames, entityType, user, i, count)
|
|
if not groupId:
|
|
if contactGroupNames:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], Msg.DOES_NOT_EXIST, j, jcount)
|
|
continue
|
|
break
|
|
result = callGAPI(people.contactGroups(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
resourceName=groupId)
|
|
body['etag'] = result['etag']
|
|
newContactGroup = body.get(PEOPLE_GROUP_NAME)
|
|
if newContactGroup:
|
|
if newContactGroup in contactGroupNames and groupId not in contactGroupNames[newContactGroup]:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup],
|
|
Ent.TypeNameMessage(Ent.CONTACT_GROUP, newContactGroup, Msg.DUPLICATE), i, count)
|
|
continue
|
|
callGAPI(people.contactGroups(), 'update',
|
|
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
resourceName=groupId, body={'contactGroup': body, 'updateGroupFields': fields})
|
|
entityActionPerformed([entityType, user, Ent.CONTACT_GROUP, contactGroup], j, jcount)
|
|
except (GAPI.notFound, GAPI.internalError) as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], str(e), j, jcount)
|
|
except (GAPI.forbidden, GAPI.permissionDenied):
|
|
userPeopleServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
except GAPI.serviceNotAvailable:
|
|
entityUnknownWarning(entityType, user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delete contactgroups <ContactGroupEntity>
|
|
def deleteUserPeopleContactGroups(users):
|
|
entityType = Ent.USER
|
|
entityList = getEntityList(Cmd.OB_CONTACT_GROUP_ENTITY, shlexSplit=True)
|
|
contactGroupIdLists = entityList if isinstance(entityList, dict) else None
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if contactGroupIdLists:
|
|
entityList = contactGroupIdLists[user]
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
contactGroupIDs = contactGroupNames = None
|
|
j = 0
|
|
jcount = len(entityList)
|
|
entityPerformActionNumItems([entityType, user], jcount, Ent.CONTACT_GROUP, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
for contactGroup in entityList:
|
|
j += 1
|
|
try:
|
|
groupId, contactGroupIDs, contactGroupNames = validatePeopleContactGroup(people, contactGroup,
|
|
contactGroupIDs, contactGroupNames, entityType, user, i, count)
|
|
if not groupId:
|
|
if contactGroupNames:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], Msg.DOES_NOT_EXIST, j, jcount)
|
|
continue
|
|
break
|
|
callGAPI(people.contactGroups(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
resourceName=groupId)
|
|
entityActionPerformed([entityType, user, Ent.CONTACT_GROUP, contactGroup], j, jcount)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], str(e), j, jcount)
|
|
except (GAPI.forbidden, GAPI.permissionDenied):
|
|
userPeopleServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
except GAPI.serviceNotAvailable:
|
|
entityUnknownWarning(entityType, user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
PEOPLE_GROUP_TIME_OBJECTS = {'updateTime'}
|
|
|
|
def _normalizeContactGroupMetadata(contactGroup):
|
|
normalizedContactGroup = contactGroup.copy()
|
|
for k, v in iter(normalizedContactGroup.pop('metadata', {}).items()):
|
|
normalizedContactGroup[k] = v
|
|
return normalizedContactGroup
|
|
|
|
def _showContactGroup(userEntityType, user, entityType, contactGroup, i, count, FJQC):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(contactGroup, timeObjects=PEOPLE_GROUP_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True))
|
|
return
|
|
normalizedContactGroup = _normalizeContactGroupMetadata(contactGroup)
|
|
printEntity([userEntityType, user, entityType, contactGroup['resourceName']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, normalizedContactGroup, timeObjects=PEOPLE_GROUP_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
def _printContactGroup(entityTypeName, user, contactGroup, csvPF, FJQC):
|
|
normalizedContactGroup = _normalizeContactGroupMetadata(contactGroup)
|
|
row = flattenJSON(normalizedContactGroup, flattened={entityTypeName: user}, timeObjects=PEOPLE_GROUP_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({entityTypeName: user, 'resourceName': contactGroup['resourceName'],
|
|
'JSON': json.dumps(cleanJSON(contactGroup, timeObjects=PEOPLE_GROUP_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP = {
|
|
'clientdata': 'clientData',
|
|
'grouptype': 'groupType',
|
|
'membercount': 'memberCount',
|
|
'metadata': 'metadata',
|
|
'name': 'name',
|
|
}
|
|
|
|
PEOPLE_CONTACTGROUPS_DEFAULT_FIELDS = ['name', 'metadata', 'grouptype', 'membercount']
|
|
|
|
# gam <UserTypeEntity> info contactgroups <PeopleContactGroupEntity>
|
|
# [allfields|(fields <PeoplaContactGroupFieldList>)] [showmetadata]
|
|
# [formatjson]
|
|
def infoUserPeopleContactGroups(users):
|
|
entityType = Ent.USER
|
|
entityList = getEntityList(Cmd.OB_CONTACT_GROUP_ENTITY, shlexSplit=True)
|
|
contactGroupIdLists = entityList if isinstance(entityList, dict) else None
|
|
FJQC = FormatJSONQuoteChar()
|
|
fieldsList = []
|
|
parameters = _initPersonMetadataParameters()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'allfields':
|
|
fieldsList = None
|
|
elif getPersonFieldsList(myarg, PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
elif myarg == 'showmetadata':
|
|
parameters['strip'] = False
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = _getPersonFields(PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP, PEOPLE_CONTACTGROUPS_DEFAULT_FIELDS, fieldsList, parameters)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if contactGroupIdLists:
|
|
entityList = contactGroupIdLists[user]
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
contactGroupIDs = contactGroupNames = None
|
|
j = 0
|
|
jcount = len(entityList)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([entityType, user], jcount, Ent.CONTACT_GROUP, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
for contactGroup in entityList:
|
|
j += 1
|
|
try:
|
|
groupId, contactGroupIDs, contactGroupNames = validatePeopleContactGroup(people, contactGroup,
|
|
contactGroupIDs, contactGroupNames, entityType, user, i, count)
|
|
if not groupId:
|
|
if contactGroupNames:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], Msg.DOES_NOT_EXIST, j, jcount)
|
|
continue
|
|
break
|
|
group = callGAPI(people.contactGroups(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
resourceName=groupId, groupFields=fields)
|
|
_showContactGroup(entityType, user, Ent.CONTACT_GROUP, group, j, jcount, FJQC)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT_GROUP, contactGroup], str(e), j, jcount)
|
|
except (GAPI.forbidden, GAPI.permissionDenied):
|
|
userPeopleServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
except GAPI.serviceNotAvailable:
|
|
entityUnknownWarning(entityType, user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print contactgroups [todrive <ToDriveAttribute>*]
|
|
# [allfields|(fields <PeoplaContactGroupFieldList>)] [showmetadata]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show contactgroups
|
|
# [allfields|(fields <PeoplaContactGroupFieldList>)] [showmetadata]
|
|
# [formatjson]
|
|
def printShowUserPeopleContactGroups(users):
|
|
entityType = Ent.USER
|
|
entityTypeName = Ent.Singular(entityType)
|
|
csvPF = CSVPrintFile([entityTypeName, 'resourceName'], 'sortall') if Act.csvFormat() else None
|
|
if csvPF:
|
|
csvPF.SetNoEscapeChar(True)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
parameters = _initPersonMetadataParameters()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'allfields':
|
|
fieldsList = None
|
|
elif getPersonFieldsList(myarg, PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
elif myarg == 'showmetadata':
|
|
parameters['strip'] = False
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
fields = _getPersonFields(PEOPLE_CONTACTGROUPS_FIELDS_CHOICE_MAP, PEOPLE_CONTACTGROUPS_DEFAULT_FIELDS, fieldsList, parameters)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
printGettingAllEntityItemsForWhom(Ent.PEOPLE_CONTACT_GROUP, user, i, count)
|
|
try:
|
|
entityList = callGAPIpages(people.contactGroups(), 'list', 'contactGroups',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
pageSize=GC.Values[GC.PEOPLE_MAX_RESULTS],
|
|
groupFields=fields, fields='nextPageToken,contactGroups')
|
|
except GAPI.permissionDenied as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
_printPersonEntityList(Ent.PEOPLE_CONTACT_GROUP, entityList, entityType, user, i, count, csvPF, FJQC, parameters, None)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('People Contact Groups')
|
|
|
|
# Delegate command utilities
|
|
def _validateUserGetDelegateList(cd, user, i, count, entity):
|
|
if entity['dict']:
|
|
entityList = entity['dict'][user]
|
|
else:
|
|
entityList = entity['list']
|
|
user = checkUserExists(cd, user, Ent.USER, i, count)
|
|
if not user:
|
|
return (user, None, 0)
|
|
jcount = len(entityList)
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, entity['item'], i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (user, entityList, jcount)
|
|
|
|
def _getDelegateName(cd, delegateEmail, delegateNames):
|
|
if delegateEmail in delegateNames:
|
|
return delegateNames[delegateEmail]
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=delegateEmail, fields='name(fullName)')
|
|
delegateName = result.get('name', {'fullName': delegateEmail}).get('fullName', delegateEmail)
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
delegateName = delegateEmail
|
|
delegateNames[delegateEmail] = delegateName
|
|
return delegateName
|
|
|
|
# gam <UserTypeEntity> create contactdelegate <UserEntity>
|
|
# gam <UserTypeEntity> delete contactdelegate <UserEntity>
|
|
def processContactDelegates(users):
|
|
condel = buildGAPIObject(API.CONTACTDELEGATION)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
function = 'delete' if Act.Get() == Act.DELETE else 'create'
|
|
delegateEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.DELEGATE)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, delegates, jcount = _validateUserGetDelegateList(cd, user, i, count, delegateEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for delegate in delegates:
|
|
j += 1
|
|
delegateEmail = checkUserExists(cd, delegate, Ent.CONTACT_DELEGATE, j, jcount)
|
|
if not delegateEmail:
|
|
continue
|
|
try:
|
|
if function == 'create':
|
|
callGAPI(condel.delegates(), function,
|
|
throwReasons=GAPI.CONTACT_DELEGATE_THROW_REASONS,
|
|
user=user, body={'email': delegateEmail})
|
|
else:
|
|
callGAPI(condel.delegates(), function,
|
|
throwReasons=GAPI.CONTACT_DELEGATE_THROW_REASONS,
|
|
user=user, delegate=delegateEmail)
|
|
entityActionPerformed([Ent.USER, user, Ent.CONTACT_DELEGATE, delegateEmail], j, jcount)
|
|
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CONTACT_DELEGATE, delegateEmail], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.badRequest):
|
|
userContactDelegateServiceNotEnabledWarning(user, i, count)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print contactdelegates [todrive <ToDriveAttribute>*] [shownames]
|
|
# gam <UserTypeEntity> show contactdelegates [shownames] [csv]
|
|
def printShowContactDelegates(users):
|
|
condel = buildGAPIObject(API.CONTACTDELEGATION)
|
|
titlesList = ['User', 'delegateAddress']
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvStyle = showNames = False
|
|
delegateNames = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif not csvPF and myarg == 'csv':
|
|
csvStyle = True
|
|
elif myarg == 'shownames':
|
|
titlesList = ['User', 'delegateName', 'delegateAddress']
|
|
showNames = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if csvPF:
|
|
csvPF.AddTitles(titlesList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = checkUserExists(cd, user, Ent.USER, i, count)
|
|
if not user:
|
|
continue
|
|
printGettingAllEntityItemsForWhom(Ent.CONTACT_DELEGATE, user, i, count)
|
|
try:
|
|
delegates = callGAPIpages(condel.delegates(), 'list', 'delegates',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.CONTACT_DELEGATE_THROW_REASONS,
|
|
user=user)
|
|
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CONTACT_DELEGATE, None], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.badRequest):
|
|
userContactDelegateServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(delegates)
|
|
if not csvPF:
|
|
if not csvStyle:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.CONTACT_DELEGATE, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for delegate in delegates:
|
|
j += 1
|
|
delegateEmail = delegate['email']
|
|
if showNames:
|
|
printEntity([Ent.CONTACT_DELEGATE, _getDelegateName(cd, delegateEmail, delegateNames)], j, jcount)
|
|
Ind.Increment()
|
|
printKeyValueList(['Delegate Email', delegateEmail])
|
|
Ind.Decrement()
|
|
else:
|
|
printEntity([Ent.DELEGATE, delegateEmail], j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
j = 0
|
|
for delegate in delegates:
|
|
j += 1
|
|
delegateEmail = delegate['email']
|
|
if showNames:
|
|
writeStdout(f'{user},{_getDelegateName(cd, delegateEmail, delegateNames)},{delegateEmail}\n')
|
|
else:
|
|
writeStdout(f'{user},{delegateEmail}\n')
|
|
else:
|
|
if delegates:
|
|
if showNames:
|
|
for delegate in delegates:
|
|
csvPF.WriteRow({'User': user, 'delegateName': _getDelegateName(cd, delegate['email'], delegateNames),
|
|
'delegateAddress': delegate['email']})
|
|
else:
|
|
for delegate in delegates:
|
|
csvPF.WriteRow({'User': user, 'delegateAddress': delegate['email']})
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'User': user})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Contact Delegates')
|
|
|
|
# CrOS commands utilities
|
|
def getCrOSDeviceEntity():
|
|
if checkArgumentPresent('crossn'):
|
|
return getItemsToModify(Cmd.ENTITY_CROS_SN, getString(Cmd.OB_SERIAL_NUMBER_LIST))
|
|
if checkArgumentPresent('query'):
|
|
return getItemsToModify(Cmd.ENTITY_CROS_QUERY, getString(Cmd.OB_QUERY))
|
|
deviceId = getString(Cmd.OB_CROS_DEVICE_ENTITY)
|
|
if deviceId[:6].lower() == 'query:':
|
|
query = deviceId[6:]
|
|
if query[:12].lower() == 'orgunitpath:':
|
|
return getItemsToModify(Cmd.ENTITY_CROS_OU, query[12:])
|
|
return getItemsToModify(Cmd.ENTITY_CROS_QUERY, query)
|
|
Cmd.Backup()
|
|
return getEntityList(Cmd.OB_CROS_ENTITY)
|
|
|
|
UPDATE_CROS_ARGUMENT_TO_PROPERTY_MAP = {
|
|
'annotatedassetid': 'annotatedAssetId',
|
|
'annotatedlocation': 'annotatedLocation',
|
|
'annotateduser': 'annotatedUser',
|
|
'asset': 'annotatedAssetId',
|
|
'assetid': 'annotatedAssetId',
|
|
'location': 'annotatedLocation',
|
|
'notes': 'notes',
|
|
'org': 'orgUnitPath',
|
|
'orgunitpath': 'orgUnitPath',
|
|
'ou': 'orgUnitPath',
|
|
'tag': 'annotatedAssetId',
|
|
'updatenotes': 'notes',
|
|
'user': 'annotatedUser',
|
|
}
|
|
|
|
CROS_ACTION_CHOICE_MAP = {
|
|
'deprovisiondifferentmodelreplace': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_DIFFERENT_MODEL_REPLACEMENT'),
|
|
'deprovisiondifferentmodelreplacement': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_DIFFERENT_MODEL_REPLACEMENT'),
|
|
'deprovisionretiringdevice': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_RETIRING_DEVICE'),
|
|
'deprovisionsamemodelreplace': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_SAME_MODEL_REPLACEMENT'),
|
|
'deprovisionsamemodelreplacement': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_SAME_MODEL_REPLACEMENT'),
|
|
'deprovisionupgradetransfer': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION', 'DEPROVISION_REASON_UPGRADE_TRANSFER'),
|
|
'disable': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DISABLE', None),
|
|
'reenable': ('CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_REENABLE', None),
|
|
# 'preprovisioneddisable': ('pre_provisioned_disable', None),
|
|
# 'preprovisionedreenable': ('pre_provisioned_reenable', None)
|
|
}
|
|
|
|
CROS_ACTION_NAME_MAP = {
|
|
'CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION': Act.DEPROVISION,
|
|
'CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DISABLE': Act.DISABLE,
|
|
'CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_REENABLE': Act.REENABLE,
|
|
# 'pre_provisioned_disable': Act.PRE_PROVISIONED_DISABLE,
|
|
# 'pre_provisioned_reenable': Act.PRE_PROVISIONED_REENABLE
|
|
}
|
|
|
|
# gam <CrOSTypeEntity> update <CrOSAttribute>+ [quickcrosmove [<Boolean>]] [nobatchupdate]
|
|
# gam <CrOSTypeEntity> update action <CrOSAction> [acknowledge_device_touch_requirement]
|
|
# [actionbatchsize <Integer>]
|
|
def updateCrOSDevices(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
noBatchUpdate = False
|
|
update_body = {}
|
|
action_body = {}
|
|
orgUnitPath = updateNotes = None
|
|
ackWipe = False
|
|
quickCrOSMove = GC.Values[GC.QUICK_CROS_MOVE]
|
|
actionBatchSize = 10
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in UPDATE_CROS_ARGUMENT_TO_PROPERTY_MAP:
|
|
up = UPDATE_CROS_ARGUMENT_TO_PROPERTY_MAP[myarg]
|
|
if up == 'orgUnitPath':
|
|
orgUnitPath = getOrgUnitItem()
|
|
elif up == 'notes':
|
|
update_body[up] = getStringWithCRsNLs()
|
|
updateNotes = update_body[up] if myarg == 'updatenotes' and update_body[up].find('#notes#') != -1 else None
|
|
else:
|
|
update_body[up] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'action':
|
|
actionLocation = Cmd.Location()
|
|
action_body['changeChromeOsDeviceStatusAction'], deprovisionReason = getChoice(CROS_ACTION_CHOICE_MAP, mapChoice=True)
|
|
if deprovisionReason:
|
|
action_body['deprovisionReason'] = deprovisionReason
|
|
Act.Set(CROS_ACTION_NAME_MAP[action_body['changeChromeOsDeviceStatusAction']])
|
|
elif myarg == 'acknowledgedevicetouchrequirement':
|
|
ackWipe = True
|
|
elif myarg == 'quickcrosmove':
|
|
quickCrOSMove = getBoolean()
|
|
elif myarg == 'nobatchupdate':
|
|
noBatchUpdate = getBoolean()
|
|
elif myarg == 'actionbatchsize':
|
|
actionBatchSize = getInteger(minVal=10, maxVal=250)
|
|
else:
|
|
unknownArgumentExit()
|
|
if action_body and update_body:
|
|
Cmd.SetLocation(actionLocation-1)
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('action', '<CrOSAttribute>'))
|
|
if orgUnitPath:
|
|
status, orgUnitPath, orgUnitId = checkOrgUnitPathExists(cd, orgUnitPath)
|
|
if not status:
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, ''], f'{Ent.Singular(Ent.ORGANIZATIONAL_UNIT)}: {orgUnitPath}, {Msg.DOES_NOT_EXIST}')
|
|
return
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
# Action
|
|
if action_body:
|
|
if action_body['changeChromeOsDeviceStatusAction'] == 'CHANGE_CHROME_OS_DEVICE_STATUS_ACTION_DEPROVISION' and not ackWipe:
|
|
stderrWarningMsg(Msg.REFUSING_TO_DEPROVISION_DEVICES.format(count))
|
|
systemErrorExit(ACTION_NOT_PERFORMED_RC, None)
|
|
while i < count:
|
|
bcount = min(count-i, actionBatchSize)
|
|
action_body['deviceIds'] = entityList[i:i+bcount]
|
|
try:
|
|
result = callGAPI(cd.customer().devices().chromeos(), 'batchChangeStatus',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.CONDITION_NOT_MET, GAPI.INVALID_INPUT,
|
|
GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], body=action_body)
|
|
for status in result['changeChromeOsDeviceStatusResults']:
|
|
i += 1
|
|
deviceId = status['deviceId']
|
|
if 'error' not in status:
|
|
entityActionPerformed([Ent.CROS_DEVICE, deviceId], i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], status['error']['message'], i, count)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.conditionNotMet, GAPI.invalidInput, GAPI.badRequest, GAPI.forbidden) as e:
|
|
entityActionFailedExit([Ent.CROS_DEVICE, None], str(e))
|
|
return
|
|
# Update
|
|
function = None
|
|
if update_body or noBatchUpdate:
|
|
if orgUnitPath and (not quickCrOSMove or noBatchUpdate):
|
|
update_body['orgUnitPath'] = orgUnitPath
|
|
if GC.Values[GC.UPDATE_CROS_OU_WITH_ID]:
|
|
update_body['orgUnitId'] = orgUnitId
|
|
orgUnitPath = None
|
|
function = 'update'
|
|
parmId = 'deviceId'
|
|
kwargs = {parmId: None, 'body': update_body, 'fields': ''}
|
|
if orgUnitPath:
|
|
Act.Set(Act.ADD)
|
|
_batchMoveCrOSesToOrgUnit(cd, orgUnitPath, orgUnitId, 0, 0, entityList, quickCrOSMove)
|
|
Act.Set(Act.UPDATE)
|
|
if function is None:
|
|
return
|
|
for deviceId in entityList:
|
|
i += 1
|
|
kwargs[parmId] = deviceId
|
|
try:
|
|
if updateNotes:
|
|
oldNotes = callGAPI(cd.chromeosdevices(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, fields='notes').get('notes', '')
|
|
update_body['notes'] = updateNotes.replace('#notes#', oldNotes)
|
|
callGAPI(cd.chromeosdevices(), function,
|
|
throwReasons=[GAPI.INVALID, GAPI.CONDITION_NOT_MET, GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], **kwargs)
|
|
entityActionPerformed([Ent.CROS_DEVICE, deviceId], i, count)
|
|
except (GAPI.invalid, GAPI.conditionNotMet, GAPI.invalidInput) as e:
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], str(e), i, count)
|
|
except GAPI.invalidOrgunit:
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], Msg.INVALID_ORGUNIT, i, count)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, deviceId, i, count)
|
|
|
|
# gam update cros|croses <CrOSEntity> <CrOSAttribute>+ [quickcrosmove [<Boolean>]] [nobatchupdate]
|
|
# gam update cros|croses <CrOSEntity> action <CrOSAction> [acknowledge_device_touch_requirement]
|
|
def doUpdateCrOSDevices():
|
|
updateCrOSDevices(getCrOSDeviceEntity())
|
|
|
|
CROS_COMMAND_CHOICE_MAP = {
|
|
'reboot': 'REBOOT',
|
|
'remotepowerwash': 'REMOTE_POWERWASH',
|
|
'setvolume': 'SET_VOLUME',
|
|
'takeascreenshot': 'TAKE_A_SCREENSHOT',
|
|
'wipeusers': 'WIPE_USERS'
|
|
}
|
|
|
|
CROS_DOIT_REQUIRED_COMMANDS = {'WIPE_USERS', 'REMOTE_POWERWASH'}
|
|
CROS_KIOSK_COMMANDS = {'REBOOT', 'SET_VOLUME', 'TAKE_A_SCREENSHOT'}
|
|
CROS_COMMAND_FINAL_STATES = {'EXPIRED', 'CANCELLED', 'EXECUTED_BY_CLIENT'}
|
|
CROS_COMMAND_TIME_OBJECTS = {'executeTime', 'issueTime', 'commandExpireTime'}
|
|
|
|
def displayCrOSCommandResult(cd, deviceId, commandId, checkResultRetries, i, count):
|
|
Ind.Increment()
|
|
try:
|
|
for _ in range(0, checkResultRetries):
|
|
time.sleep(2)
|
|
result = callGAPI(cd.customer().devices().chromeos().commands(), 'get',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, commandId=commandId)
|
|
showJSON(None, result, timeObjects=CROS_COMMAND_TIME_OBJECTS)
|
|
state = result.get('state')
|
|
if state in CROS_COMMAND_FINAL_STATES:
|
|
break
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId, Ent.COMMAND_ID, commandId], str(e), i, count)
|
|
Ind.Decrement()
|
|
|
|
# gam <CrOSTypeEntity> issuecommand command <CrOSCommand> [times_to_check_status <Integer>] [doit]
|
|
def issueCommandCrOSDevices(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
body = {}
|
|
checkResultRetries = 1
|
|
doit = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'command':
|
|
body['commandType'] = getChoice(CROS_COMMAND_CHOICE_MAP, mapChoice=True)
|
|
if body['commandType'] == 'SET_VOLUME':
|
|
body['payload'] = json.dumps({'volume': getInteger(minVal=0, maxVal=100)})
|
|
elif myarg == 'timestocheckstatus':
|
|
checkResultRetries = getInteger(minVal=0)
|
|
elif myarg == 'doit':
|
|
doit = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not body:
|
|
missingArgumentExit('command <CrOSCommand>')
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
if body['commandType'] in CROS_DOIT_REQUIRED_COMMANDS and not doit:
|
|
actionNotPerformedNumItemsWarning(count, Ent.CROS_DEVICE, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION)
|
|
return
|
|
for deviceId in entityList:
|
|
i += 1
|
|
try:
|
|
result = callGAPI(cd.customer().devices().chromeos(), 'issueCommand',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, body=body)
|
|
commandId = result.get('commandId')
|
|
entityActionPerformed([Ent.CROS_DEVICE, deviceId, Ent.ACTION, body['commandType'], Ent.COMMAND_ID, commandId], i, count)
|
|
displayCrOSCommandResult(cd, deviceId, commandId, checkResultRetries, i, count)
|
|
except GAPI.invalidArgument as e:
|
|
errMsg = str(e)
|
|
if body['commandType'] in CROS_KIOSK_COMMANDS:
|
|
errMsg += Msg.KIOSK_MODE_REQUIRED.format(body['commandType'])
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], errMsg, i, count)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId], str(e), i, count)
|
|
|
|
# gam issuecommand <CrOSEntity> command <CrOSCommand> [times_to_check_status <Integer>] [doit]
|
|
def doIssueCommandCrOSDevices():
|
|
issueCommandCrOSDevices(getCrOSDeviceEntity())
|
|
|
|
# gam <CrOSTypeEntity> getcommand commandid <CommandID> [times_to_check_status <Integer>]
|
|
def getCommandResultCrOSDevices(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
commandId = ''
|
|
checkResultRetries = 1
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'commandid':
|
|
commandId = getString(Cmd.OB_COMMAND_ID)
|
|
elif myarg == 'timestocheckstatus':
|
|
checkResultRetries = getInteger(minVal=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not commandId:
|
|
missingArgumentExit('commandid <CommandID>')
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for deviceId in entityList:
|
|
i += 1
|
|
printEntity([Ent.CROS_DEVICE, deviceId, Ent.COMMAND_ID, commandId], i, count)
|
|
displayCrOSCommandResult(cd, deviceId, commandId, checkResultRetries, i, count)
|
|
|
|
# gam getcommand <CrOSEntity> commandid <CommandID> [times_to_check_status <Integer>]
|
|
def doGetCommandResultCrOSDevices():
|
|
getCommandResultCrOSDevices(getCrOSDeviceEntity())
|
|
|
|
# From https://www.chromium.org/chromium-os/tpm_firmware_update
|
|
CROS_TPM_VULN_VERSIONS = ['41f', '420', '628', '8520']
|
|
CROS_TPM_FIXED_VERSIONS = ['422', '62b', '8521']
|
|
|
|
def checkTPMVulnerability(cros):
|
|
if 'tpmVersionInfo' in cros and 'firmwareVersion' in cros['tpmVersionInfo']:
|
|
if cros['tpmVersionInfo']['firmwareVersion'] in CROS_TPM_VULN_VERSIONS:
|
|
cros['tpmVersionInfo']['tpmVulnerability'] = 'VULNERABLE'
|
|
elif cros['tpmVersionInfo']['firmwareVersion'] in CROS_TPM_FIXED_VERSIONS:
|
|
cros['tpmVersionInfo']['tpmVulnerability'] = 'UPDATED'
|
|
else:
|
|
cros['tpmVersionInfo']['tpmVulnerability'] = 'NOT IMPACTED'
|
|
|
|
def _filterActiveTimeRanges(cros, selected, listLimit, startDate, endDate, activeTimeRangesOrder):
|
|
if not selected:
|
|
cros.pop('activeTimeRanges', None)
|
|
return []
|
|
filteredItems = []
|
|
activeTimeRanges = cros.get('activeTimeRanges', [])
|
|
if activeTimeRangesOrder == 'DESCENDING':
|
|
activeTimeRanges.reverse()
|
|
i = 0
|
|
for item in activeTimeRanges:
|
|
activityDate = datetime.datetime.strptime(item['date'], YYYYMMDD_FORMAT)
|
|
if ((startDate is None) or (activityDate >= startDate)) and ((endDate is None) or (activityDate <= endDate)):
|
|
item['duration'] = formatMilliSeconds(item['activeTime'])
|
|
item['minutes'] = item['activeTime']//60000
|
|
item['activeTime'] = str(item['activeTime'])
|
|
filteredItems.append(item)
|
|
i += 1
|
|
if listLimit and i == listLimit:
|
|
break
|
|
cros['activeTimeRanges'] = filteredItems
|
|
return cros['activeTimeRanges']
|
|
|
|
def _filterDeviceFiles(cros, selected, listLimit, startTime, endTime):
|
|
if not selected:
|
|
cros.pop('deviceFiles', None)
|
|
return []
|
|
filteredItems = []
|
|
i = 0
|
|
for item in cros.get('deviceFiles', []):
|
|
timeValue, _ = iso8601.parse_date(item['createTime'])
|
|
if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
|
|
item['createTime'] = formatLocalTime(item['createTime'])
|
|
filteredItems.append(item)
|
|
i += 1
|
|
if listLimit and i == listLimit:
|
|
break
|
|
cros['deviceFiles'] = filteredItems
|
|
return cros['deviceFiles']
|
|
|
|
def _filterCPUStatusReports(cros, selected, listLimit, startTime, endTime):
|
|
if not selected:
|
|
cros.pop('cpuStatusReports', None)
|
|
return []
|
|
filteredItems = []
|
|
i = 0
|
|
for item in cros.get('cpuStatusReports', []):
|
|
timeValue, _ = iso8601.parse_date(item['reportTime'])
|
|
if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
|
|
item['reportTime'] = formatLocalTime(item['reportTime'])
|
|
for tempInfo in item.get('cpuTemperatureInfo', []):
|
|
tempInfo['label'] = tempInfo['label'].strip()
|
|
if 'cpuUtilizationPercentageInfo' in item:
|
|
item['cpuUtilizationPercentageInfo'] = ','.join([str(x) for x in item['cpuUtilizationPercentageInfo']])
|
|
filteredItems.append(item)
|
|
i += 1
|
|
if listLimit and i == listLimit:
|
|
break
|
|
cros['cpuStatusReports'] = filteredItems
|
|
return cros['cpuStatusReports']
|
|
|
|
def _filterSystemRamFreeReports(cros, selected, listLimit, startTime, endTime):
|
|
if not selected:
|
|
cros.pop('systemRamFreeReports', None)
|
|
return []
|
|
filteredItems = []
|
|
i = 0
|
|
for item in cros.get('systemRamFreeReports', []):
|
|
timeValue, _ = iso8601.parse_date(item['reportTime'])
|
|
if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
|
|
item['reportTime'] = formatLocalTime(item['reportTime'])
|
|
item['systemRamFreeInfo'] = ','.join([str(x) for x in item['systemRamFreeInfo']])
|
|
filteredItems.append(item)
|
|
i += 1
|
|
if listLimit and i == listLimit:
|
|
break
|
|
cros['systemRamFreeReports'] = filteredItems
|
|
return cros['systemRamFreeReports']
|
|
|
|
def _filterRecentUsers(cros, selected, listLimit):
|
|
if not selected:
|
|
cros.pop('recentUsers', None)
|
|
return []
|
|
filteredItems = []
|
|
i = 0
|
|
for item in cros.get('recentUsers', []):
|
|
item['email'] = item.get('email', [UNKNOWN, 'UnmanagedUser'][item['type'] == 'USER_TYPE_UNMANAGED'])
|
|
filteredItems.append(item)
|
|
i += 1
|
|
if listLimit and i == listLimit:
|
|
break
|
|
cros['recentUsers'] = filteredItems
|
|
return cros['recentUsers']
|
|
|
|
def _filterScreenshotFiles(cros, selected, listLimit, startTime, endTime):
|
|
if not selected:
|
|
cros.pop('screenshotFiles', None)
|
|
return []
|
|
filteredItems = []
|
|
i = 0
|
|
for item in cros.get('screenshotFiles', []):
|
|
timeValue, _ = iso8601.parse_date(item['createTime'])
|
|
if ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime)):
|
|
item['createTime'] = formatLocalTime(item['createTime'])
|
|
filteredItems.append(item)
|
|
i += 1
|
|
if listLimit and i == listLimit:
|
|
break
|
|
cros['screenshotFiles'] = filteredItems
|
|
return cros['screenshotFiles']
|
|
|
|
def _filterBasicList(cros, field, selected, listLimit):
|
|
if not selected:
|
|
cros.pop(field, None)
|
|
return []
|
|
if listLimit:
|
|
filteredItems = []
|
|
i = 0
|
|
for item in cros.get(field, []):
|
|
filteredItems.append(item)
|
|
i += 1
|
|
if listLimit and i == listLimit:
|
|
break
|
|
cros[field] = filteredItems
|
|
return cros[field]
|
|
return cros.get(field, [])
|
|
|
|
def _computeDVRstorageFreePercentage(cros):
|
|
for diskVolumeReport in cros.get('diskVolumeReports', []):
|
|
volumeInfo = diskVolumeReport['volumeInfo']
|
|
for volume in volumeInfo:
|
|
if volume['storageTotal'] != '0':
|
|
volume['storageFreePercentage'] = str(int(int(volume['storageFree'])/int(volume['storageTotal'])*100))
|
|
else:
|
|
volume['storageFreePercentage'] = '0'
|
|
|
|
def _getFilterDateTime():
|
|
filterDate = getYYYYMMDD(returnDateTime=True)
|
|
return (filterDate, filterDate.replace(tzinfo=iso8601.UTC))
|
|
|
|
CROS_FIELDS_CHOICE_MAP = {
|
|
'activetimeranges': ['activeTimeRanges.activeTime', 'activeTimeRanges.date'],
|
|
'annotatedassetid': 'annotatedAssetId',
|
|
'annotatedlocation': 'annotatedLocation',
|
|
'annotateduser': 'annotatedUser',
|
|
'asset': 'annotatedAssetId',
|
|
'assetid': 'annotatedAssetId',
|
|
'autoupdateexpiration': 'autoUpdateExpiration',
|
|
'autoupdatethrough': 'autoUpdateThrough',
|
|
'backlightinfo': 'backlightInfo',
|
|
'bootmode': 'bootMode',
|
|
'cpuinfo': 'cpuInfo',
|
|
'cpustatusreports': 'cpuStatusReports',
|
|
'deprovisionreason': 'deprovisionReason',
|
|
'devicefiles': ['deviceFiles.type', 'deviceFiles.createTime'],
|
|
'deviceid': 'deviceId',
|
|
'devicelicensetype': 'deviceLicenseType',
|
|
'diskvolumereports': 'diskVolumeReports',
|
|
'dockmacaddress': 'dockMacAddress',
|
|
'ethernetmacaddress': 'ethernetMacAddress',
|
|
'ethernetmacaddress0': 'ethernetMacAddress0',
|
|
'extendedsupporteligible': 'extendedSupportEligible',
|
|
'extendedsupportstart': 'extendedSupportStart',
|
|
'extendedsupportenabled': 'extendedSupportEnabled',
|
|
'firmwareversion': 'firmwareVersion',
|
|
'firstenrollmenttime': 'firstEnrollmentTime',
|
|
'lastdeprovisiontimestamp': 'lastDeprovisionTimestamp',
|
|
'lastenrollmenttime': 'lastEnrollmentTime',
|
|
'lastknownnetwork': 'lastKnownNetwork',
|
|
'lastsync': 'lastSync',
|
|
'location': 'annotatedLocation',
|
|
'macaddress': 'macAddress',
|
|
'manufacturedate': 'manufactureDate',
|
|
'meid': 'meid',
|
|
'model': 'model',
|
|
'notes': 'notes',
|
|
'ordernumber': 'orderNumber',
|
|
'org': 'orgUnitPath',
|
|
'orgunitid': 'orgUnitId',
|
|
'orgunitpath': 'orgUnitPath',
|
|
'osupdatestatus': 'osUpdateStatus',
|
|
'osversion': 'osVersion',
|
|
'ou': 'orgUnitPath',
|
|
'platformversion': 'platformVersion',
|
|
'recentusers': ['recentUsers.email', 'recentUsers.type'],
|
|
'screenshotfiles': 'screenshotFiles',
|
|
'serialnumber': 'serialNumber',
|
|
'status': 'status',
|
|
'supportenddate': 'supportEndDate',
|
|
'systemramfreereports': 'systemRamFreeReports',
|
|
'systemramtotal': 'systemRamTotal',
|
|
'tag': 'annotatedAssetId',
|
|
'timeranges': ['activeTimeRanges.activeTime', 'activeTimeRanges.date'],
|
|
'times': ['activeTimeRanges.activeTime', 'activeTimeRanges.date'],
|
|
'tpmversioninfo': 'tpmVersionInfo',
|
|
'user': 'annotatedUser',
|
|
'users': ['recentUsers.email', 'recentUsers.type'],
|
|
'willautorenew': 'willAutoRenew',
|
|
}
|
|
CROS_BASIC_FIELDS_LIST = ['deviceId', 'annotatedAssetId', 'annotatedLocation', 'annotatedUser', 'lastSync', 'notes', 'serialNumber', 'status']
|
|
|
|
CROS_SCALAR_PROPERTY_PRINT_ORDER = [
|
|
'orgUnitId',
|
|
'orgUnitPath',
|
|
'annotatedAssetId',
|
|
'annotatedLocation',
|
|
'annotatedUser',
|
|
'lastSync',
|
|
'notes',
|
|
'serialNumber',
|
|
'status',
|
|
'deviceLicenseType',
|
|
'model',
|
|
'firmwareVersion',
|
|
'platformVersion',
|
|
'osVersion',
|
|
'bootMode',
|
|
'meid',
|
|
'dockMacAddress',
|
|
'ethernetMacAddress',
|
|
'ethernetMacAddress0',
|
|
'macAddress',
|
|
'systemRamTotal',
|
|
'firstEnrollmentTime',
|
|
'lastEnrollmentTime',
|
|
'deprovisionReason',
|
|
'lastDeprovisionTimestamp',
|
|
'orderNumber',
|
|
'manufactureDate',
|
|
'supportEndDate',
|
|
'autoUpdateExpiration',
|
|
'willAutoRenew',
|
|
]
|
|
|
|
CROS_LIST_FIELDS_CHOICE_MAP = {
|
|
'activetimeranges': 'activeTimeRanges',
|
|
'cpustatusreports': 'cpuStatusReports',
|
|
'devicefiles': 'deviceFiles',
|
|
'diskvolumereports': 'diskVolumeReports',
|
|
'files': 'deviceFiles',
|
|
'lastknownnetwork': 'lastKnownNetwork',
|
|
'recentusers': 'recentUsers',
|
|
'screenshotfiles': 'screenshotFiles',
|
|
'systemramfreereports': 'systemRamFreeReports',
|
|
'timeranges': 'activeTimeRanges',
|
|
'times': 'activeTimeRanges',
|
|
'users': 'recentUsers',
|
|
}
|
|
|
|
CROS_TIME_OBJECTS = {
|
|
'createTime',
|
|
'firstEnrollmentTime',
|
|
'lastDeprovisionTimestamp',
|
|
'lastEnrollmentTime',
|
|
'lastSync',
|
|
'reportTime',
|
|
'supportEndDate',
|
|
}
|
|
CROS_FIELDS_WITH_CRS_NLS = {'notes'}
|
|
CROS_START_ARGUMENTS = ['start', 'startdate', 'oldestdate']
|
|
CROS_END_ARGUMENTS = ['end', 'enddate']
|
|
|
|
# gam info cros <CrOSEntity>
|
|
# gam <CrOSTypeEntity> info
|
|
# [basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>]
|
|
# [nolists]
|
|
# [start <Date>] [end <Date>] [listlimit <Number>]
|
|
# [reverselists <CrOSListFieldNameList>]
|
|
# [timerangeorder ascending|descending] [showdvrsfp]
|
|
# [downloadfile latest|<Time>] [targetfolder <FilePath>]
|
|
# [formatjson]
|
|
def infoCrOSDevices(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
downloadfile = None
|
|
targetFolder = GC.Values[GC.DRIVE_DIR]
|
|
projection = None
|
|
fieldsList = []
|
|
reverseLists = []
|
|
noLists = showDVRstorageFreePercentage = False
|
|
FJQC = FormatJSONQuoteChar()
|
|
listLimit = 0
|
|
startDate = endDate = startTime = endTime = None
|
|
activeTimeRangesOrder = 'ASCENDING'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'nolists':
|
|
noLists = True
|
|
elif myarg == 'listlimit':
|
|
listLimit = getInteger(minVal=0)
|
|
elif myarg in CROS_START_ARGUMENTS:
|
|
startDate, startTime = _getFilterDateTime()
|
|
elif myarg in CROS_END_ARGUMENTS:
|
|
endDate, endTime = _getFilterDateTime()
|
|
elif myarg == 'timerangeorder':
|
|
activeTimeRangesOrder = getChoice(SORTORDER_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'allfields':
|
|
projection = 'FULL'
|
|
fieldsList = []
|
|
elif myarg in PROJECTION_CHOICE_MAP:
|
|
projection = PROJECTION_CHOICE_MAP[myarg]
|
|
if projection == 'FULL':
|
|
fieldsList = []
|
|
else:
|
|
fieldsList = CROS_BASIC_FIELDS_LIST[:]
|
|
elif myarg in CROS_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(myarg, CROS_FIELDS_CHOICE_MAP, fieldsList)
|
|
elif myarg == 'fields':
|
|
for field in _getFieldsList():
|
|
if field in CROS_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, CROS_FIELDS_CHOICE_MAP, fieldsList)
|
|
if field in CROS_LIST_FIELDS_CHOICE_MAP:
|
|
projection = 'FULL'
|
|
noLists = False
|
|
else:
|
|
invalidChoiceExit(field, CROS_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'reverselists':
|
|
for field in _getFieldsList():
|
|
if field in CROS_LIST_FIELDS_CHOICE_MAP:
|
|
reverseLists.append(CROS_LIST_FIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, CROS_LIST_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'downloadfile':
|
|
downloadfile = getString(Cmd.OB_STRING).lower()
|
|
if downloadfile != 'latest':
|
|
Cmd.Backup()
|
|
downloadfile = formatLocalTime(getTimeOrDeltaFromNow())
|
|
elif myarg == 'targetfolder':
|
|
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
elif myarg == 'showdvrsfp':
|
|
showDVRstorageFreePercentage = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if fieldsList:
|
|
fieldsList.append('deviceId')
|
|
if downloadfile:
|
|
fieldsList.append('deviceFiles.downloadUrl')
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for deviceId in entityList:
|
|
i += 1
|
|
try:
|
|
cros = callGAPI(cd.chromeosdevices(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, projection=projection, fields=fields)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, deviceId, i, count)
|
|
continue
|
|
checkTPMVulnerability(cros)
|
|
if 'autoUpdateExpiration' in cros:
|
|
cros['autoUpdateExpiration'] = formatLocalDatestamp(cros['autoUpdateExpiration'])
|
|
if showDVRstorageFreePercentage:
|
|
_computeDVRstorageFreePercentage(cros)
|
|
for field in reverseLists:
|
|
if field in cros:
|
|
cros[field].reverse()
|
|
if 'orgUnitId' in cros:
|
|
cros['orgUnitId'] = f"id:{cros['orgUnitId']}"
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(cros, timeObjects=CROS_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
continue
|
|
printEntity([Ent.CROS_DEVICE, deviceId], i, count)
|
|
Ind.Increment()
|
|
for up in CROS_SCALAR_PROPERTY_PRINT_ORDER:
|
|
if up in cros:
|
|
if up not in CROS_TIME_OBJECTS:
|
|
if up not in CROS_FIELDS_WITH_CRS_NLS:
|
|
printKeyValueList([up, cros[up]])
|
|
else:
|
|
printKeyValueWithCRsNLs(up, cros[up])
|
|
else:
|
|
printKeyValueList([up, formatLocalTime(cros[up])])
|
|
up = 'tpmVersionInfo'
|
|
if up in cros:
|
|
printKeyValueList([up, ''])
|
|
Ind.Increment()
|
|
for key, value in sorted(iter(cros[up].items())):
|
|
printKeyValueList([key, value])
|
|
Ind.Decrement()
|
|
if not noLists:
|
|
activeTimeRanges = _filterActiveTimeRanges(cros, True, listLimit, startDate, endDate, activeTimeRangesOrder)
|
|
if activeTimeRanges:
|
|
printKeyValueList(['activeTimeRanges'])
|
|
Ind.Increment()
|
|
for activeTimeRange in activeTimeRanges:
|
|
printKeyValueList(['date', activeTimeRange['date']])
|
|
Ind.Increment()
|
|
for key in ['activeTime', 'duration', 'minutes']:
|
|
printKeyValueList([key, activeTimeRange[key]])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
recentUsers = _filterRecentUsers(cros, True, listLimit)
|
|
if recentUsers:
|
|
printKeyValueList(['recentUsers'])
|
|
Ind.Increment()
|
|
for recentUser in recentUsers:
|
|
printKeyValueList(['type', recentUser['type']])
|
|
Ind.Increment()
|
|
printKeyValueList(['email', recentUser['email']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
deviceFiles = _filterDeviceFiles(cros, True, listLimit, startTime, endTime)
|
|
if deviceFiles:
|
|
printKeyValueList(['deviceFiles'])
|
|
Ind.Increment()
|
|
for deviceFile in deviceFiles:
|
|
printKeyValueList([deviceFile['type'], deviceFile['createTime']])
|
|
Ind.Decrement()
|
|
if downloadfile:
|
|
if downloadfile == 'latest':
|
|
deviceFile = deviceFiles[-1]
|
|
else:
|
|
for deviceFile in deviceFiles:
|
|
if deviceFile['createTime'] == downloadfile:
|
|
break
|
|
else:
|
|
deviceFile = None
|
|
if deviceFile:
|
|
downloadfilename = os.path.join(targetFolder, f'cros-logs-{deviceId}-{deviceFile["createTime"]}.zip')
|
|
_, content = cd._http.request(deviceFile['downloadUrl'])
|
|
writeFile(downloadfilename, content, mode='wb', continueOnError=True)
|
|
printKeyValueList(['Downloaded', downloadfilename])
|
|
else:
|
|
Act.Set(Act.DOWNLOAD)
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, deviceId, Ent.DEVICE_FILE, downloadfile],
|
|
Msg.DOES_NOT_EXIST, i, count)
|
|
Act.Set(Act.INFO)
|
|
elif downloadfile:
|
|
Act.Set(Act.DOWNLOAD)
|
|
entityActionNotPerformedWarning([Ent.CROS_DEVICE, deviceId, Ent.DEVICE_FILE, downloadfile],
|
|
Msg.NO_ENTITIES_FOUND.format(Ent.Plural(Ent.DEVICE_FILE)), i, count)
|
|
Act.Set(Act.INFO)
|
|
cpuStatusReports = _filterCPUStatusReports(cros, True, listLimit, startTime, endTime)
|
|
if cpuStatusReports:
|
|
printKeyValueList(['cpuStatusReports'])
|
|
Ind.Increment()
|
|
for cpuStatusReport in cpuStatusReports:
|
|
printKeyValueList(['reportTime', formatLocalTime(cpuStatusReport['reportTime'])])
|
|
Ind.Increment()
|
|
if 'cpuTemperatureInfo' in cpuStatusReport:
|
|
printKeyValueList(['cpuTemperatureInfo'])
|
|
Ind.Increment()
|
|
for tempInfo in cpuStatusReport.get('cpuTemperatureInfo', []):
|
|
printKeyValueList([tempInfo['label'], tempInfo['temperature']])
|
|
Ind.Decrement()
|
|
if 'cpuUtilizationPercentageInfo' in cpuStatusReport:
|
|
printKeyValueList(['cpuUtilizationPercentageInfo', cpuStatusReport['cpuUtilizationPercentageInfo']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
diskVolumeReports = _filterBasicList(cros, 'diskVolumeReports', True, listLimit)
|
|
if diskVolumeReports:
|
|
printKeyValueList(['diskVolumeReports'])
|
|
Ind.Increment()
|
|
printKeyValueList(['volumeInfo'])
|
|
for diskVolumeReport in diskVolumeReports:
|
|
volumeInfo = diskVolumeReport['volumeInfo']
|
|
Ind.Increment()
|
|
for volume in volumeInfo:
|
|
printKeyValueList(['volumeId', volume['volumeId']])
|
|
Ind.Increment()
|
|
printKeyValueList(['storageFree', volume['storageFree']])
|
|
printKeyValueList(['storageTotal', volume['storageTotal']])
|
|
if showDVRstorageFreePercentage:
|
|
printKeyValueList(['storageFreePercentage', volume['storageFreePercentage']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
lastKnownNetworks = _filterBasicList(cros, 'lastKnownNetwork', True, listLimit)
|
|
if lastKnownNetworks:
|
|
printKeyValueList(['lastKnownNetwork'])
|
|
Ind.Increment()
|
|
for lastKnownNetwork in lastKnownNetworks:
|
|
printKeyValueList(['ipAddress', lastKnownNetwork['ipAddress']])
|
|
Ind.Increment()
|
|
printKeyValueList(['wanIpAddress', lastKnownNetwork['wanIpAddress']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
screenshotFiles = _filterScreenshotFiles(cros, True, listLimit, startTime, endTime)
|
|
if screenshotFiles:
|
|
printKeyValueList(['screenshotFiles'])
|
|
Ind.Increment()
|
|
for screenshotFile in screenshotFiles:
|
|
printKeyValueList(['name', screenshotFile['name']])
|
|
Ind.Increment()
|
|
printKeyValueList(['type', screenshotFile['type']])
|
|
printKeyValueList(['downloadUrl', screenshotFile['downloadUrl']])
|
|
printKeyValueList(['createTime', screenshotFile['createTime']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
systemRamFreeReports = _filterSystemRamFreeReports(cros, True, listLimit, startTime, endTime)
|
|
if systemRamFreeReports:
|
|
printKeyValueList(['systemRamFreeReports'])
|
|
Ind.Increment()
|
|
for systemRamFreeReport in systemRamFreeReports:
|
|
printKeyValueList(['reportTime', systemRamFreeReport['reportTime']])
|
|
Ind.Increment()
|
|
printKeyValueList(['systemRamFreeInfo', systemRamFreeReport['systemRamFreeInfo']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
def doInfoCrOSDevices():
|
|
infoCrOSDevices(getCrOSDeviceEntity())
|
|
|
|
def getDeviceFilesEntity():
|
|
deviceFilesEntity = {'list': [], 'dict': None, 'count': None, 'time': None, 'range': None}
|
|
startEndTime = StartEndTime()
|
|
entitySelector = getEntitySelector()
|
|
if entitySelector:
|
|
entityList = getEntitySelection(entitySelector, False)
|
|
if isinstance(entityList, dict):
|
|
deviceFilesEntity['dict'] = entityList
|
|
else:
|
|
deviceFilesEntity['list'] = entityList
|
|
else:
|
|
myarg = getString(Cmd.OB_DEVICE_FILE_ENTITY, checkBlank=True)
|
|
mycmd = myarg.lower()
|
|
if mycmd in {'first', 'last', 'allexceptfirst', 'allexceptlast'}:
|
|
deviceFilesEntity['count'] = (mycmd, getInteger(minVal=1))
|
|
elif mycmd in {'before', 'after'}:
|
|
dateTime, _, _ = getTimeOrDeltaFromNow(True)
|
|
deviceFilesEntity['time'] = (mycmd, dateTime)
|
|
elif mycmd == 'range':
|
|
startEndTime.Get(mycmd)
|
|
deviceFilesEntity['range'] = (mycmd, startEndTime.startDateTime, startEndTime.endDateTime)
|
|
else:
|
|
for timeItem in myarg.split(','):
|
|
try:
|
|
timestamp, _ = iso8601.parse_date(timeItem)
|
|
deviceFilesEntity['list'].append(ISOformatTimeStamp(timestamp.astimezone(GC.Values[GC.TIMEZONE])))
|
|
except (iso8601.ParseError, OverflowError):
|
|
Cmd.Backup()
|
|
invalidArgumentExit(YYYYMMDDTHHMMSS_FORMAT_REQUIRED)
|
|
return deviceFilesEntity
|
|
|
|
def _selectDeviceFiles(deviceId, deviceFiles, deviceFilesEntity):
|
|
numDeviceFiles = len(deviceFiles)
|
|
if numDeviceFiles == 0:
|
|
return deviceFiles
|
|
for deviceFile in deviceFiles:
|
|
deviceFile['createTime'] = formatLocalTime(deviceFile['createTime'])
|
|
if deviceFilesEntity['count']:
|
|
countType = deviceFilesEntity['count'][0]
|
|
count = deviceFilesEntity['count'][1]
|
|
if countType == 'first':
|
|
return deviceFiles[:count]
|
|
if countType == 'last':
|
|
return deviceFiles[-count:]
|
|
if countType == 'allexceptfirst':
|
|
return deviceFiles[count:]
|
|
# if countType == 'allexceptlast':
|
|
return deviceFiles[:-count]
|
|
if deviceFilesEntity['time']:
|
|
dateTime = deviceFilesEntity['time'][1]
|
|
count = 0
|
|
if deviceFilesEntity['time'][0] == 'before':
|
|
for deviceFile in deviceFiles:
|
|
createTime, _ = iso8601.parse_date(deviceFile['createTime'])
|
|
if createTime >= dateTime:
|
|
break
|
|
count += 1
|
|
return deviceFiles[:count]
|
|
# if deviceFilesEntity['time'][0] == 'after':
|
|
for deviceFile in deviceFiles:
|
|
createTime, _ = iso8601.parse_date(deviceFile['createTime'])
|
|
if createTime >= dateTime:
|
|
break
|
|
count += 1
|
|
return deviceFiles[count:]
|
|
if deviceFilesEntity['range']:
|
|
dateTime = deviceFilesEntity['range'][1]
|
|
spos = 0
|
|
for deviceFile in deviceFiles:
|
|
createTime, _ = iso8601.parse_date(deviceFile['createTime'])
|
|
if createTime >= dateTime:
|
|
break
|
|
spos += 1
|
|
dateTime = deviceFilesEntity['range'][2]
|
|
epos = spos
|
|
for deviceFile in deviceFiles[spos:]:
|
|
createTime, _ = iso8601.parse_date(deviceFile['createTime'])
|
|
if createTime >= dateTime:
|
|
break
|
|
epos += 1
|
|
return deviceFiles[spos:epos]
|
|
# if deviceFilesEntity['range'] or deviceFilesEntity['dict']:
|
|
if deviceFilesEntity['dict']:
|
|
deviceFileCreateTimes = deviceFilesEntity['dict'][deviceId]
|
|
else:
|
|
deviceFileCreateTimes = deviceFilesEntity['list']
|
|
return [deviceFile for deviceFile in deviceFiles if deviceFile['createTime'] in deviceFileCreateTimes]
|
|
|
|
# gam <CrOSTypeEntity> get devicefile [select <DeviceFileEntity>] [targetfolder <FilePath>]
|
|
def getCrOSDeviceFiles(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
targetFolder = GC.Values[GC.DRIVE_DIR]
|
|
deviceFilesEntity = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'select':
|
|
deviceFilesEntity = getDeviceFilesEntity()
|
|
elif myarg == 'targetfolder':
|
|
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
else:
|
|
unknownArgumentExit()
|
|
fields = 'deviceFiles(type,createTime,downloadUrl)'
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for deviceId in entityList:
|
|
i += 1
|
|
try:
|
|
deviceFiles = callGAPIitems(cd.chromeosdevices(), 'get', 'deviceFiles',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], deviceId=deviceId, fields=fields)
|
|
if deviceFilesEntity:
|
|
deviceFiles = _selectDeviceFiles(deviceId, deviceFiles, deviceFilesEntity)
|
|
jcount = len(deviceFiles)
|
|
entityPerformActionNumItems([Ent.CROS_DEVICE, deviceId], jcount, Ent.DEVICE_FILE, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for deviceFile in deviceFiles:
|
|
j += 1
|
|
downloadfilename = os.path.join(targetFolder, f'cros-logs-{deviceId}-{deviceFile["createTime"]}.zip')
|
|
_, content = cd._http.request(deviceFile['downloadUrl'])
|
|
writeFile(downloadfilename, content, mode='wb', continueOnError=True)
|
|
entityActionPerformed([Ent.DEVICE_FILE, downloadfilename], j, jcount)
|
|
Ind.Decrement()
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, deviceId, i, count)
|
|
|
|
# gam get devicefile <CrOSEntity> [select <DeviceFileEntity>] [targetfolder <FilePath>]
|
|
def doGetCrOSDeviceFiles():
|
|
getCrOSDeviceFiles(getCrOSDeviceEntity())
|
|
|
|
def substituteQueryTimes(queries, queryTimes):
|
|
if queryTimes:
|
|
for i, query in enumerate(queries):
|
|
if query is not None:
|
|
for queryTimeName, queryTimeValue in iter(queryTimes.items()):
|
|
query = query.replace(f'#{queryTimeName}#', queryTimeValue)
|
|
queries[i] = query
|
|
|
|
# Get CrOS devices from gam.cfg print_cros_ous and print_cros_ous_and_children
|
|
def getCfgCrOSEntities():
|
|
if GC.Values[GC.PRINT_CROS_OUS]:
|
|
entityList = getItemsToModify(Cmd.ENTITY_CROS_OUS, GC.Values[GC.PRINT_CROS_OUS])
|
|
else:
|
|
entityList = []
|
|
if GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]:
|
|
entityList.extend(getItemsToModify(Cmd.ENTITY_CROS_OUS_AND_CHILDREN, GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]))
|
|
return entityList
|
|
|
|
CROS_ORDERBY_CHOICE_MAP = {
|
|
'lastsync': 'lastSync',
|
|
'location': 'annotatedLocation',
|
|
'notes': 'notes',
|
|
'serialnumber': 'serialNumber',
|
|
'status': 'status',
|
|
'supportenddate': 'supportEndDate',
|
|
'user': 'annotatedUser',
|
|
}
|
|
|
|
CROS_ENTITIES_MAP = {
|
|
'crosorg': Cmd.ENTITY_CROS_OU,
|
|
'crosorgandchildren': Cmd.ENTITY_CROS_OU_AND_CHILDREN,
|
|
'crosorgs': Cmd.ENTITY_CROS_OUS,
|
|
'crosorgsandchildren': Cmd.ENTITY_CROS_OUS_AND_CHILDREN,
|
|
'crosou': Cmd.ENTITY_CROS_OU,
|
|
'crosouandchildren': Cmd.ENTITY_CROS_OU_AND_CHILDREN,
|
|
'crosous': Cmd.ENTITY_CROS_OUS,
|
|
'crosousandchildren': Cmd.ENTITY_CROS_OUS_AND_CHILDREN
|
|
}
|
|
|
|
CROS_INDEXED_TITLES = ['activeTimeRanges', 'recentUsers', 'deviceFiles',
|
|
'cpuStatusReports', 'diskVolumeReports', 'lastKnownNetwork', 'screenshotFiles', 'systemRamFreeReports']
|
|
|
|
# gam print cros [todrive <ToDriveAttribute>*]
|
|
# [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
|
|
# [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
|
|
# (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
|
|
# [showitemcountonly]
|
|
# gam print cros [todrive <ToDriveAttribute>*] select <CrOSTypeEntity>
|
|
# gam <CrOSTypeEntity> print cros [todrive <ToDriveAttribute>*]
|
|
# [orderby <CrOSOrderByFieldName> [ascending|descending]]
|
|
# [basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>]
|
|
# [nolists|(<CrOSListFieldName>* [onerow])]
|
|
# [start <Date>] [end <Date>] [listlimit <Number>]
|
|
# [reverselists <CrOSListFieldNameList>]
|
|
# [timerangeorder ascending|descending] [showdvrsfp]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [sortheaders]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [showitemcountonly]
|
|
def doPrintCrOSDevices(entityList=None):
|
|
def _printCrOS(cros):
|
|
checkTPMVulnerability(cros)
|
|
if 'autoUpdateExpiration' in cros:
|
|
cros['autoUpdateExpiration'] = formatLocalDatestamp(cros['autoUpdateExpiration'])
|
|
if showDVRstorageFreePercentage:
|
|
_computeDVRstorageFreePercentage(cros)
|
|
for field in reverseLists:
|
|
if field in cros:
|
|
cros[field].reverse()
|
|
if 'orgUnitId' in cros:
|
|
cros['orgUnitId'] = f"id:{cros['orgUnitId']}"
|
|
if addCSVData:
|
|
cros.update(addCSVData)
|
|
if FJQC.formatJSON:
|
|
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(flattenJSON(cros, listLimit=listLimit, timeObjects=CROS_TIME_OBJECTS)):
|
|
csvPF.WriteRowNoFilter({'deviceId': cros['deviceId'],
|
|
'JSON': json.dumps(cleanJSON(cros, listLimit=listLimit, timeObjects=CROS_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
return
|
|
if 'notes' in cros:
|
|
cros['notes'] = escapeCRsNLs(cros['notes'])
|
|
for cpuStatusReport in cros.get('cpuStatusReports', []):
|
|
for tempInfo in cpuStatusReport.get('cpuTemperatureInfo', []):
|
|
tempInfo['label'] = tempInfo['label'].strip()
|
|
if not noLists and not selectedLists:
|
|
csvPF.WriteRowTitles(flattenJSON(cros, listLimit=listLimit, timeObjects=CROS_TIME_OBJECTS))
|
|
return
|
|
attrib = 'tpmVersionInfo'
|
|
if attrib in cros:
|
|
for key, value in sorted(iter(cros[attrib].items())):
|
|
attribKey = f'{attrib}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'
|
|
cros[attribKey] = value
|
|
cros.pop(attrib)
|
|
activeTimeRanges = _filterActiveTimeRanges(cros, selectedLists.get('activeTimeRanges', False), listLimit, startDate, endDate, activeTimeRangesOrder)
|
|
recentUsers = _filterRecentUsers(cros, selectedLists.get('recentUsers', False), listLimit)
|
|
deviceFiles = _filterDeviceFiles(cros, selectedLists.get('deviceFiles', False), listLimit, startTime, endTime)
|
|
cpuStatusReports = _filterCPUStatusReports(cros, selectedLists.get('cpuStatusReports', False), listLimit, startTime, endTime)
|
|
diskVolumeReports = _filterBasicList(cros, 'diskVolumeReports', selectedLists.get('diskVolumeReports', False), listLimit)
|
|
lastKnownNetworks = _filterBasicList(cros, 'lastKnownNetwork', selectedLists.get('lastKnownNetwork', False), listLimit)
|
|
screenshotFiles = _filterScreenshotFiles(cros, selectedLists.get('screenshotFiles', False), listLimit, startTime, endTime)
|
|
systemRamFreeReports = _filterSystemRamFreeReports(cros, selectedLists.get('systemRamFreeReports', False), listLimit, startTime, endTime)
|
|
if oneRow:
|
|
csvPF.WriteRowTitles(flattenJSON(cros, listLimit=listLimit, timeObjects=CROS_TIME_OBJECTS))
|
|
return
|
|
row = {}
|
|
for attrib in cros:
|
|
if attrib not in {'kind', 'etag', 'tpmVersionInfo', 'recentUsers', 'activeTimeRanges',
|
|
'deviceFiles', 'cpuStatusReports', 'diskVolumeReports', 'lastKnownNetwork', 'screenshotFiles', 'systemRamFreeReports'}:
|
|
if attrib not in CROS_TIME_OBJECTS:
|
|
row[attrib] = cros[attrib]
|
|
else:
|
|
row[attrib] = formatLocalTime(cros[attrib])
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
if noLists or (not activeTimeRanges and not recentUsers and not deviceFiles and
|
|
not cpuStatusReports and not diskVolumeReports and not lastKnownNetworks and not screenshotFiles and not systemRamFreeReports):
|
|
csvPF.WriteRowTitles(row)
|
|
return
|
|
lenATR = len(activeTimeRanges)
|
|
lenRU = len(recentUsers)
|
|
lenDF = len(deviceFiles)
|
|
lenCSR = len(cpuStatusReports)
|
|
lenDVR = len(diskVolumeReports)
|
|
lenLKN = len(lastKnownNetworks)
|
|
lenSSF = len(screenshotFiles)
|
|
lenSRFR = len(systemRamFreeReports)
|
|
new_row = row
|
|
for i in range(min(max(lenATR, lenRU, lenDF, lenCSR, lenDVR, lenLKN, lenSSF, lenSRFR), listLimit or max(lenATR, lenRU, lenDF, lenCSR, lenDVR, lenLKN, lenSSF, lenSRFR))):
|
|
new_row = row.copy()
|
|
if i < lenATR:
|
|
for key in ['date', 'activeTime', 'duration', 'minutes']:
|
|
new_row[f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = activeTimeRanges[i][key]
|
|
if i < lenRU:
|
|
for key in ['email', 'type']:
|
|
new_row[f'recentUsers{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = recentUsers[i][key]
|
|
if i < lenDF:
|
|
for key in ['type', 'createTime']:
|
|
new_row[f'deviceFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = deviceFiles[i][key]
|
|
if i < lenCSR:
|
|
new_row[f'cpuStatusReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}reportTime'] = cpuStatusReports[i]['reportTime']
|
|
for tempInfo in cpuStatusReports[i].get('cpuTemperatureInfo', []):
|
|
new_row[f'cpuStatusReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}cpuTemperatureInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{tempInfo["label"]}'] = tempInfo['temperature']
|
|
if 'cpuUtilizationPercentageInfo' in cpuStatusReports[i]:
|
|
new_row[f'cpuStatusReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}cpuUtilizationPercentageInfo'] = cpuStatusReports[i]['cpuUtilizationPercentageInfo']
|
|
if i < lenDVR:
|
|
j = 0
|
|
for volume in diskVolumeReports[i]['volumeInfo']:
|
|
new_row[f'diskVolumeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeId'] = volume['volumeId']
|
|
new_row[f'diskVolumeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}storageFree'] = volume['storageFree']
|
|
new_row[f'diskVolumeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}storageTotal'] = volume['storageTotal']
|
|
if showDVRstorageFreePercentage:
|
|
new_row[f'diskVolumeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}volumeInfo{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}storageFreePercentage'] = volume['storageFreePercentage']
|
|
j += 1
|
|
if i < lenLKN:
|
|
for key in ['ipAddress', 'wanIpAddress']:
|
|
if key in lastKnownNetworks[i]:
|
|
new_row[f'lastKnownNetwork{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = lastKnownNetworks[i][key]
|
|
if i < lenSSF:
|
|
for key in ['name', 'type', 'downloadUrl', 'createTime']:
|
|
if key in screenshotFiles[i]:
|
|
new_row[f'screenshotFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = screenshotFiles[i][key]
|
|
if i < lenSRFR:
|
|
for key in ['reportTime', 'systemRamFreeInfo']:
|
|
new_row[f'systemRamFreeReports{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = systemRamFreeReports[i][key]
|
|
csvPF.WriteRowTitles(new_row)
|
|
|
|
def _callbackPrintCrOS(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
_printCrOS(response)
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason in [GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN]:
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
errMsg = getHTTPError({}, http_status, reason, message)
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
fieldsList = ['deviceId']
|
|
reverseLists = []
|
|
csvPF = CSVPrintFile(fieldsList, indexedTitles=CROS_INDEXED_TITLES)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
projection = orderBy = sortOrder = None
|
|
ous = ['/']
|
|
includeChildOrgunits = True
|
|
queries = [None]
|
|
listLimit = 0
|
|
startDate = endDate = startTime = endTime = None
|
|
selectedLists = {}
|
|
queryTimes = {}
|
|
selectionAllowed = entityList is None
|
|
if selectionAllowed and (GC.Values[GC.PRINT_CROS_OUS] or GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]):
|
|
entityList = getCfgCrOSEntities()
|
|
selectionAllowed = False
|
|
allFields = noLists = oneRow = showDVRstorageFreePercentage = sortHeaders = False
|
|
activeTimeRangesOrder = 'ASCENDING'
|
|
showItemCountOnly = False
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif selectionAllowed and myarg == 'limittoou':
|
|
ous = [getOrgUnitItem()]
|
|
selectionAllowed = False
|
|
includeChildOrgunits = False
|
|
elif selectionAllowed and myarg in CROS_ENTITIES_MAP:
|
|
myarg = CROS_ENTITIES_MAP[myarg]
|
|
ous = convertEntityToList(getString(Cmd.OB_CROS_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN])
|
|
selectionAllowed = False
|
|
includeChildOrgunits = myarg in {Cmd.ENTITY_CROS_OU_AND_CHILDREN, Cmd.ENTITY_CROS_OUS_AND_CHILDREN}
|
|
elif (selectionAllowed or queries == [None]) and myarg in {'query', 'queries'}:
|
|
queries = getQueries(myarg)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
|
|
elif selectionAllowed and myarg == 'select':
|
|
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_CROS, crosAllowed=True, userAllowed=False)
|
|
selectionAllowed = False
|
|
elif myarg == 'orderby':
|
|
orderBy, sortOrder = getOrderBySortOrder(CROS_ORDERBY_CHOICE_MAP, 'DESCENDING', True)
|
|
elif myarg == 'onerow':
|
|
oneRow = True
|
|
elif myarg == 'nolists':
|
|
noLists = True
|
|
selectedLists = {}
|
|
elif myarg == 'listlimit':
|
|
listLimit = getInteger(minVal=0)
|
|
elif myarg in CROS_START_ARGUMENTS:
|
|
startDate, startTime = _getFilterDateTime()
|
|
elif myarg in CROS_END_ARGUMENTS:
|
|
endDate, endTime = _getFilterDateTime()
|
|
elif myarg == 'timerangeorder':
|
|
activeTimeRangesOrder = getChoice(SORTORDER_CHOICE_MAP, mapChoice=True)
|
|
elif myarg in PROJECTION_CHOICE_MAP:
|
|
projection = PROJECTION_CHOICE_MAP[myarg]
|
|
sortHeaders = True
|
|
if projection == 'FULL':
|
|
fieldsList = []
|
|
else:
|
|
fieldsList = CROS_BASIC_FIELDS_LIST[:]
|
|
elif myarg == 'allfields':
|
|
projection = 'FULL'
|
|
allFields = sortHeaders = True
|
|
fieldsList = []
|
|
elif myarg == 'sortheaders':
|
|
sortHeaders = getBoolean()
|
|
elif myarg in CROS_LIST_FIELDS_CHOICE_MAP:
|
|
selectedLists[CROS_LIST_FIELDS_CHOICE_MAP[myarg]] = True
|
|
elif myarg in CROS_FIELDS_CHOICE_MAP:
|
|
csvPF.AddField(myarg, CROS_FIELDS_CHOICE_MAP, fieldsList)
|
|
elif myarg == 'fields':
|
|
for field in _getFieldsList():
|
|
if field in CROS_FIELDS_CHOICE_MAP:
|
|
if field in CROS_LIST_FIELDS_CHOICE_MAP:
|
|
selectedLists[CROS_LIST_FIELDS_CHOICE_MAP[field]] = True
|
|
else:
|
|
csvPF.AddField(field, CROS_FIELDS_CHOICE_MAP, fieldsList)
|
|
else:
|
|
invalidChoiceExit(field, CROS_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'reverselists':
|
|
for field in _getFieldsList():
|
|
if field in CROS_LIST_FIELDS_CHOICE_MAP:
|
|
reverseLists.append(CROS_LIST_FIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, CROS_LIST_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'showdvrsfp':
|
|
showDVRstorageFreePercentage = True
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if selectedLists:
|
|
noLists = False
|
|
projection = 'FULL'
|
|
for selectList in selectedLists:
|
|
addFieldToFieldsList(selectList, CROS_FIELDS_CHOICE_MAP, fieldsList)
|
|
if fieldsList:
|
|
fieldsList.append('deviceId')
|
|
_, _, entityList = getEntityArgument(entityList)
|
|
if FJQC.formatJSON:
|
|
sortHeaders = False
|
|
csvPF.SetJSONTitles(['deviceId', 'JSON'])
|
|
substituteQueryTimes(queries, queryTimes)
|
|
if entityList is None:
|
|
sortRows = False
|
|
fields = getItemFieldsFromFieldsList('chromeosdevices', fieldsList)
|
|
for ou in ous:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT)) if not includeChildOrgunits else Msg.IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
|
|
for query in queries:
|
|
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, ou,
|
|
query=query, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
pageMessage = getPageMessageForWhom()
|
|
pageToken = None
|
|
totalItems = 0
|
|
tokenRetries = 0
|
|
while True:
|
|
try:
|
|
feed = callGAPI(cd.chromeosdevices(), 'list',
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageToken=pageToken,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], query=query, projection=projection,
|
|
orgUnitPath=ou, includeChildOrgunits=includeChildOrgunits,
|
|
orderBy=orderBy, sortOrder=sortOrder, fields=fields, maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
|
|
tokenRetries = 0
|
|
pageToken, totalItems = _processGAPIpagesResult(feed, 'chromeosdevices', None, totalItems, pageMessage, None, Ent.CROS_DEVICE)
|
|
if feed:
|
|
if not showItemCountOnly:
|
|
for cros in feed.get('chromeosdevices', []):
|
|
_printCrOS(cros)
|
|
del feed
|
|
if not pageToken:
|
|
_finalizeGAPIpagesResult(pageMessage)
|
|
printGotAccountEntities(totalItems)
|
|
break
|
|
except GAPI.invalidInput as e:
|
|
message = str(e)
|
|
# Invalid Input: xyz - Check for invalid pageToken!!
|
|
# 0123456789012345
|
|
if message[15:] == pageToken:
|
|
tokenRetries += 1
|
|
if tokenRetries <= 2:
|
|
writeStderr(f'{WARNING_PREFIX}{Msg.LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY}')
|
|
time.sleep(tokenRetries*5)
|
|
continue
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
|
|
return
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, None], invalidQuery(query) if query is not None else message)
|
|
return
|
|
except GAPI.invalidOrgunit as e:
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, None], str(e))
|
|
return
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{totalItems}\n')
|
|
return
|
|
else:
|
|
if showItemCountOnly:
|
|
writeStdout(f'{len(entityList)}\n')
|
|
return
|
|
sortRows = True
|
|
if allFields or len(set(fieldsList)) > 1:
|
|
jcount = len(entityList)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
svcargs = dict([('customerId', GC.Values[GC.CUSTOMER_ID]), ('deviceId', None), ('projection', projection), ('fields', fields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(cd.chromeosdevices(), 'get')
|
|
dbatch = cd.new_batch_http_request(callback=_callbackPrintCrOS)
|
|
bcount = 0
|
|
j = 0
|
|
for deviceId in entityList:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['deviceId'] = deviceId
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, deviceId))
|
|
bcount += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = cd.new_batch_http_request(callback=_callbackPrintCrOS)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
# The only field specified was deviceId, just list the CrOS devices
|
|
else:
|
|
for cros in entityList:
|
|
_printCrOS({'deviceId': cros})
|
|
if sortRows and orderBy:
|
|
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
|
|
if sortHeaders:
|
|
csvPF.SetSortTitles(['deviceId'])
|
|
csvPF.writeCSVfile('CrOS')
|
|
|
|
CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP = {
|
|
'activetimeranges': 'activeTimeRanges',
|
|
'devicefiles': 'deviceFiles',
|
|
'files': 'deviceFiles',
|
|
'recentusers': 'recentUsers',
|
|
'timeranges': 'activeTimeRanges',
|
|
'times': 'activeTimeRanges',
|
|
'users': 'recentUsers',
|
|
}
|
|
CROS_ACTIVITY_TIME_OBJECTS = {'createTime'}
|
|
|
|
# gam print crosactivity [todrive <ToDriveAttribute>*]
|
|
# [(query <QueryCrOS>)|(queries <QueryCrOSList>) [querytime<String> <Time>]
|
|
# [(limittoou|cros_ou <OrgUnitItem>)|(cros_ou_and_children <OrgUnitItem>)|
|
|
# (cros_ous <OrgUnitList>)|(cros_ous_and_children <OrgUnitList>)]]
|
|
# gam print crosactivity [todrive <ToDriveAttribute>*] select <CrOSTypeEntity>
|
|
# gam <CrOSTypeEntity> print crosactivity [todrive <ToDriveAttribute>*]
|
|
# [orderby <CrOSOrderByFieldName> [ascending|descending]]
|
|
# [recentusers] [timeranges] [both] [devicefiles] [all] [oneuserperrow]
|
|
# [start <Date>] [end <Date>] [listlimit <Number>]
|
|
# [reverselists <CrOSActivityListFieldNameList>]
|
|
# [timerangeorder ascending|descending]
|
|
# [delimiter <Character>]
|
|
# [formatjson [quotechar <Character>]]
|
|
def doPrintCrOSActivity(entityList=None):
|
|
def _printCrOS(cros):
|
|
row = {}
|
|
for field in reverseLists:
|
|
if field in cros:
|
|
cros[field].reverse()
|
|
if 'orgUnitId' in cros:
|
|
cros['orgUnitId'] = f"id:{cros['orgUnitId']}"
|
|
if FJQC.formatJSON:
|
|
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(flattenJSON(cros, listLimit=listLimit, timeObjects=CROS_ACTIVITY_TIME_OBJECTS)):
|
|
csvPF.WriteRowNoFilter({'deviceId': cros['deviceId'],
|
|
'JSON': json.dumps(cleanJSON(cros, timeObjects=CROS_ACTIVITY_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
return
|
|
for attrib in cros:
|
|
if attrib not in {'recentUsers', 'activeTimeRanges', 'deviceFiles'}:
|
|
row[attrib] = cros[attrib]
|
|
for activeTimeRange in _filterActiveTimeRanges(cros, selectedLists.get('activeTimeRanges', False), listLimit, startDate, endDate, activeTimeRangesOrder):
|
|
new_row = row.copy()
|
|
for key in ['date', 'duration', 'minutes']:
|
|
new_row[f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = activeTimeRange[key]
|
|
csvPF.WriteRow(new_row)
|
|
recentUsers = _filterRecentUsers(cros, selectedLists.get('recentUsers', False), listLimit)
|
|
if recentUsers:
|
|
if not oneUserPerRow:
|
|
new_row = row.copy()
|
|
new_row[f'recentUsers{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}email'] = delimiter.join([recentUser['email'] for recentUser in recentUsers])
|
|
csvPF.WriteRow(new_row)
|
|
else:
|
|
for recentUser in recentUsers:
|
|
new_row = row.copy()
|
|
new_row[f'recentUsers{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}email'] = recentUser['email']
|
|
csvPF.WriteRow(new_row)
|
|
for deviceFile in _filterDeviceFiles(cros, selectedLists.get('deviceFiles', False), listLimit, startTime, endTime):
|
|
new_row = row.copy()
|
|
for key in ['type', 'createTime']:
|
|
new_row[f'deviceFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{key}'] = deviceFile[key]
|
|
csvPF.WriteRow(new_row)
|
|
|
|
def _callbackPrintCrOS(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
_printCrOS(response)
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason in [GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN]:
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.CROS_DEVICE, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
errMsg = getHTTPError({}, http_status, reason, message)
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
fieldsList = ['deviceId', 'annotatedAssetId', 'annotatedLocation', 'serialNumber', 'orgUnitId', 'orgUnitPath']
|
|
reverseLists = []
|
|
csvPF = CSVPrintFile(fieldsList)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
projection = 'FULL'
|
|
orderBy = sortOrder = None
|
|
ous = [None]
|
|
queries = [None]
|
|
listLimit = 0
|
|
startDate = endDate = startTime = endTime = None
|
|
selectedLists = {}
|
|
queryTimes = {}
|
|
selectionAllowed = entityList is None
|
|
if selectionAllowed and (GC.Values[GC.PRINT_CROS_OUS] or GC.Values[GC.PRINT_CROS_OUS_AND_CHILDREN]):
|
|
entityList = getCfgCrOSEntities()
|
|
selectionAllowed = False
|
|
oneUserPerRow = False
|
|
directlyInOU = True
|
|
activeTimeRangesOrder = 'ASCENDING'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif selectionAllowed and myarg == 'limittoou':
|
|
ous = [getOrgUnitItem()]
|
|
selectionAllowed = False
|
|
directlyInOU = True
|
|
elif selectionAllowed and myarg in CROS_ENTITIES_MAP:
|
|
myarg = CROS_ENTITIES_MAP[myarg]
|
|
ous = convertEntityToList(getString(Cmd.OB_CROS_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OU_AND_CHILDREN])
|
|
selectionAllowed = False
|
|
directlyInOU = myarg in {Cmd.ENTITY_CROS_OU, Cmd.ENTITY_CROS_OUS}
|
|
elif (selectionAllowed or queries == [None]) and myarg in {'query', 'queries'}:
|
|
queries = getQueries(myarg)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
|
|
elif selectionAllowed and myarg == 'select':
|
|
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_CROS, crosAllowed=True, userAllowed=False)
|
|
selectionAllowed = False
|
|
elif myarg == 'oneuserperrow':
|
|
oneUserPerRow = True
|
|
elif myarg == 'listlimit':
|
|
listLimit = getInteger(minVal=0)
|
|
elif myarg in CROS_START_ARGUMENTS:
|
|
startDate, startTime = _getFilterDateTime()
|
|
elif myarg in CROS_END_ARGUMENTS:
|
|
endDate, endTime = _getFilterDateTime()
|
|
elif myarg == 'timerangeorder':
|
|
activeTimeRangesOrder = getChoice(SORTORDER_CHOICE_MAP, mapChoice=True)
|
|
elif myarg in CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP:
|
|
selectedLists[CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP[myarg]] = True
|
|
elif myarg == 'both':
|
|
selectedLists['activeTimeRanges'] = selectedLists['recentUsers'] = True
|
|
elif myarg == 'all':
|
|
selectedLists['activeTimeRanges'] = selectedLists['recentUsers'] = selectedLists['deviceFiles'] = True
|
|
elif myarg == 'reverselists':
|
|
for field in _getFieldsList():
|
|
if field in CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP:
|
|
reverseLists.append(CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, CROS_ACTIVITY_LIST_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'orderby':
|
|
orderBy, sortOrder = getOrderBySortOrder(CROS_ORDERBY_CHOICE_MAP, 'DESCENDING', True)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if not selectedLists:
|
|
selectedLists['activeTimeRanges'] = selectedLists['recentUsers'] = True
|
|
if selectedLists.get('recentUsers', False):
|
|
fieldsList.append('recentUsers')
|
|
csvPF.AddTitles(f'recentUsers{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}email')
|
|
if selectedLists.get('activeTimeRanges', False):
|
|
fieldsList.append('activeTimeRanges')
|
|
csvPF.AddTitles([f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}date',
|
|
f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}duration',
|
|
f'activeTimeRanges{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}minutes'])
|
|
if selectedLists.get('deviceFiles', False):
|
|
fieldsList.append('deviceFiles')
|
|
csvPF.AddTitles([f'deviceFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}type',
|
|
f'deviceFiles{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}createTime'])
|
|
_, _, entityList = getEntityArgument(entityList)
|
|
if FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(['deviceId', 'JSON'])
|
|
substituteQueryTimes(queries, queryTimes)
|
|
if entityList is None:
|
|
sortRows = False
|
|
fields = getItemFieldsFromFieldsList('chromeosdevices', fieldsList)
|
|
for ou in ous:
|
|
if ou is not None:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
ouList = [ou]
|
|
if not directlyInOU:
|
|
try:
|
|
orgs = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
|
|
type='all', fields='organizationUnits(orgUnitPath)')
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
return
|
|
ouList.extend([subou['orgUnitPath'] for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
|
|
for subou in ouList:
|
|
if subou is not None:
|
|
orgUnitPath = makeOrgUnitPathAbsolute(subou)
|
|
else:
|
|
orgUnitPath = subou
|
|
for query in queries:
|
|
if orgUnitPath is not None:
|
|
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
|
|
printGettingAllEntityItemsForWhom(Ent.CROS_DEVICE, orgUnitPath, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
printGettingAllAccountEntities(Ent.CROS_DEVICE, query)
|
|
pageMessage = getPageMessage()
|
|
pageToken = None
|
|
totalItems = 0
|
|
tokenRetries = 0
|
|
while True:
|
|
try:
|
|
feed = callGAPI(cd.chromeosdevices(), 'list',
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.INVALID_ORGUNIT,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageToken=pageToken,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], query=query, projection=projection, orgUnitPath=orgUnitPath,
|
|
orderBy=orderBy, sortOrder=sortOrder, fields=fields, maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
|
|
tokenRetries = 0
|
|
pageToken, totalItems = _processGAPIpagesResult(feed, 'chromeosdevices', None, totalItems, pageMessage, None, Ent.CROS_DEVICE)
|
|
if feed:
|
|
for cros in feed.get('chromeosdevices', []):
|
|
_printCrOS(cros)
|
|
del feed
|
|
if not pageToken:
|
|
_finalizeGAPIpagesResult(pageMessage)
|
|
printGotAccountEntities(totalItems)
|
|
break
|
|
except GAPI.invalidInput as e:
|
|
message = str(e)
|
|
# Invalid Input: xyz - Check for invalid pageToken!!
|
|
# 0123456789012345
|
|
if message[15:] == pageToken:
|
|
tokenRetries += 1
|
|
if tokenRetries <= 2:
|
|
writeStderr(f'{WARNING_PREFIX}{Msg.LIST_CHROMEOS_INVALID_INPUT_PAGE_TOKEN_RETRY}')
|
|
time.sleep(tokenRetries*5)
|
|
continue
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
|
|
return
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, None], invalidQuery(query) if query is not None else message)
|
|
return
|
|
except GAPI.invalidOrgunit as e:
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, None], str(e))
|
|
return
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
else:
|
|
sortRows = True
|
|
jcount = len(entityList)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
svcargs = dict([('customerId', GC.Values[GC.CUSTOMER_ID]), ('deviceId', None), ('projection', projection), ('fields', fields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(cd.chromeosdevices(), 'get')
|
|
dbatch = cd.new_batch_http_request(callback=_callbackPrintCrOS)
|
|
bcount = 0
|
|
j = 0
|
|
for deviceId in entityList:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['deviceId'] = deviceId
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, deviceId))
|
|
bcount += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = cd.new_batch_http_request(callback=_callbackPrintCrOS)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
if sortRows and orderBy:
|
|
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
|
|
csvPF.writeCSVfile('CrOS Activity')
|
|
|
|
# gam <CrOSTypeEntity> print [cros|croses|crosactivity]
|
|
def doPrintCrOSEntity(entityList):
|
|
if getChoice([Cmd.ARG_CROS, Cmd.ARG_CROSES, Cmd.ARG_CROSACTIVITY], defaultChoice=None) != Cmd.ARG_CROSACTIVITY:
|
|
if not Cmd.ArgumentsRemaining():
|
|
writeEntityNoHeaderCSVFile(Ent.CROS_DEVICE, entityList)
|
|
else:
|
|
doPrintCrOSDevices(entityList)
|
|
else:
|
|
doPrintCrOSActivity(entityList)
|
|
|
|
CROS_TELEMETRY_FIELDS_CHOICE_MAP = {
|
|
'audiostatusreport': 'audioStatusReport',
|
|
'batteryinfo': 'batteryInfo',
|
|
'batterystatusreport': 'batteryStatusReport',
|
|
'bootperformancereport': 'bootPerformanceReport',
|
|
'cpuinfo': 'cpuInfo',
|
|
'cpustatusreport': 'cpuStatusReport',
|
|
'customer': 'customer',
|
|
'deviceid': 'deviceId',
|
|
'graphicsinfo': 'graphicsInfo',
|
|
'graphicsstatusreport': 'graphicsStatusReport',
|
|
'memoryinfo': 'memoryInfo',
|
|
'memorystatusreport': 'memoryStatusReport',
|
|
'name': 'name',
|
|
'networkinfo': 'networkInfo',
|
|
'networkdiagnosticsreport': 'networkDiagnosticsReport',
|
|
'networkstatusreport': 'networkStatusReport',
|
|
'orgunitid': 'orgUnitId',
|
|
'osupdatestatus': 'osUpdateStatus',
|
|
'peripheralsreport': 'peripheralsReport',
|
|
'serialnumber': 'serialNumber',
|
|
'storageinfo': 'storageInfo',
|
|
'storagestatusreport': 'storageStatusReport',
|
|
'thunderboltinfo': 'thunderboltInfo',
|
|
}
|
|
CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP = {
|
|
'audiostatusreport': 'audioStatusReport',
|
|
'batteryinfo': 'batteryInfo',
|
|
'batterystatusreport': 'batteryStatusReport',
|
|
'bootperformancereport': 'bootPerformanceReport',
|
|
'cpuinfo': 'cpuInfo',
|
|
'cpustatusreport': 'cpuStatusReport',
|
|
'graphicsstatusreport': 'graphicsStatusReport',
|
|
'memorystatusreport': 'memoryStatusReport',
|
|
'networkdiagnosticsreport': 'networkDiagnosticsReport',
|
|
'networkstatusreport': 'networkStatusReport',
|
|
'osupdatestatus': 'osUpdateStatus',
|
|
'peripheralsreport': 'peripheralsReport',
|
|
'storagestatusreport': 'storageStatusReport',
|
|
'thunderboltinfo': 'thunderboltInfo',
|
|
}
|
|
|
|
CROS_TELEMETRY_SCALAR_FIELDS = ['deviceId', 'serialNumber', 'customer', 'name', 'orgUnitId', 'orgUnitPath']
|
|
CROS_TELEMETRY_SCALAR_FIELDS_SET = set(CROS_TELEMETRY_SCALAR_FIELDS)
|
|
CROS_TELEMETRY_LIST_FIELDS = list(CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP.values())
|
|
CROS_TELEMETRY_TIME_OBJECTS = {'reportTime', 'lastUpdateTime', 'lastUpdateCheckTime', 'lastRebootTime'}
|
|
|
|
# gam info crostelemetry <SerialNumber>
|
|
# <CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
|
|
# [start <Date>] [end <Date>] [listlimit <Number>]
|
|
# [reverselists <CrOSTelemetryListFieldNameList>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show crostelemetry
|
|
# [(ou|org|orgunit|ou_and_children <OrgUnitItem>)|(cros_sn <SerialNumber>)|(filter <String>)]
|
|
# <CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
|
|
# [start <Date>] [end <Date>] [listlimit <Number>]
|
|
# [reverselists <CrOSTelemetryListFieldNameList>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam print crostelemetry [todrive <ToDriveAttribute>*]
|
|
# [(ou|org|orgunit|ou_and_children <OrgUnitItem>)|(cros_sn <SerialNumber>)|(filter <String>)]
|
|
# <CrOSTelemetryFieldName>* [fields <CrOSTelemetryFieldNameList>]
|
|
# [reverselists <CrOSTelemetryListFieldNameList>]
|
|
# [start <Date>] [end <Date>] [listlimit <Number>]
|
|
# [formatjson [quotechar <Character>]]
|
|
def doInfoPrintShowCrOSTelemetry():
|
|
def _cleanDevice(device):
|
|
for field in reverseLists:
|
|
if field in device:
|
|
device[field].reverse()
|
|
if listLimit or startTime or endTime:
|
|
for field in CROS_TELEMETRY_LIST_FIELDS:
|
|
if field in device:
|
|
listItems = device.pop(field)
|
|
device[field] = []
|
|
i = 0
|
|
for item in listItems:
|
|
if 'reportTime' in item:
|
|
timeValue, _ = iso8601.parse_date(item['reportTime'])
|
|
else:
|
|
timeValue = None
|
|
if (timeValue is None) or (((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime))):
|
|
device[field].append(item)
|
|
i += 1
|
|
if listLimit and i == listLimit:
|
|
break
|
|
storageInfo = device.get('storageInfo', {})
|
|
if 'totalDiskBytes' in storageInfo and 'availableDiskBytes' in storageInfo:
|
|
disk_avail = int(storageInfo['availableDiskBytes'])
|
|
disk_size = int(storageInfo['totalDiskBytes'])
|
|
if diskPercentOnly:
|
|
device['storageInfo'] = {}
|
|
device['storageInfo']['percentDiskFree'] = int((disk_avail / disk_size) * 100)
|
|
device['storageInfo']['percentDiskUsed'] = 100 - device['storageInfo']['percentDiskFree']
|
|
for cpuStatusReport in device.get('cpuStatusReport', []):
|
|
for tempInfo in cpuStatusReport.pop('cpuTemperatureInfo', []):
|
|
if 'temperatureCelsius' in tempInfo:
|
|
cpuStatusReport[f"cpuTemperatureInfo.{tempInfo['label'].strip()}"] = tempInfo['temperatureCelsius']
|
|
if showOrgUnitPath:
|
|
device['orgUnitPath'] = convertOrgUnitIDtoPath(cd, device['orgUnitId'])
|
|
|
|
def _printDevice(device):
|
|
_cleanDevice(device)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(flattenJSON(device, timeObjects=CROS_TELEMETRY_TIME_OBJECTS))
|
|
else:
|
|
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(flattenJSON(device, timeObjects=CROS_TELEMETRY_TIME_OBJECTS)):
|
|
csvPF.WriteRowNoFilter({'deviceId': device['deviceId'],
|
|
'JSON': json.dumps(cleanJSON(device, timeObjects=CROS_TELEMETRY_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
def _showDevice(device, i=0, count=0):
|
|
_cleanDevice(device)
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(device), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CROS_DEVICE, device['deviceId']], i, count)
|
|
Ind.Increment()
|
|
for up in CROS_TELEMETRY_SCALAR_FIELDS:
|
|
if up in device:
|
|
printKeyValueList([up, device[up]])
|
|
showJSON(None, device, skipObjects=CROS_TELEMETRY_SCALAR_FIELDS_SET, timeObjects=CROS_TELEMETRY_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
|
|
cd = None
|
|
parent = _getCustomersCustomerIdWithC()
|
|
fieldsList = []
|
|
reverseLists = []
|
|
action = Act.Get()
|
|
if action == Act.INFO:
|
|
sn = getString(Cmd.OB_SERIAL_NUMBER)
|
|
pfilters = [(f'serialNumber={sn}', f'serialNumber={sn}')]
|
|
Act.Set(Act.SHOW)
|
|
else:
|
|
pfilters = []
|
|
csvPF = CSVPrintFile(['deviceId'], CROS_TELEMETRY_SCALAR_FIELDS, CROS_TELEMETRY_LIST_FIELDS) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
diskPercentOnly = showOrgUnitPath = False
|
|
listLimit = 0
|
|
startTime = endTime = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'ou', 'org', 'orgunit', 'limittoou', 'ouandchildren', 'crossn', 'filter'}:
|
|
if pfilters:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.ONLY_ONE_DEVICE_SELECTION_ALLOWED.format(pfilters[0][1]))
|
|
if myarg == 'crossn':
|
|
sn = getString(Cmd.OB_SERIAL_NUMBER)
|
|
pfilters = [(f'serialNumber={sn}', f'serialNumber={sn}')]
|
|
elif myarg == 'filter':
|
|
pf = getString(Cmd.OB_STRING)
|
|
pfilters = [(pf, pf)]
|
|
else:
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
orgUnitPath, orgUnitId = getOrgUnitId(cd)
|
|
pfilters = [(f'orgUnitId={orgUnitId[3:]}', f'orgUnitPath={orgUnitPath}')]
|
|
if myarg == 'ouandchildren':
|
|
try:
|
|
subous = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=orgUnitId,
|
|
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, orgUnitId)
|
|
return
|
|
pfilters.extend([(f'orgUnitId={subou["orgUnitId"][3:]}', f'orgUnitPath={subou["orgUnitPath"]}') for subou in subous.get('organizationUnits', [])])
|
|
elif myarg == 'listlimit':
|
|
listLimit = getInteger()
|
|
elif myarg in CROS_START_ARGUMENTS:
|
|
_, startTime = _getFilterDateTime()
|
|
elif myarg in CROS_END_ARGUMENTS:
|
|
_, endTime = _getFilterDateTime()
|
|
elif myarg in CROS_TELEMETRY_FIELDS_CHOICE_MAP:
|
|
fieldsList.append(CROS_TELEMETRY_FIELDS_CHOICE_MAP[myarg])
|
|
elif myarg == 'fields':
|
|
for field in _getFieldsList():
|
|
if field in CROS_TELEMETRY_FIELDS_CHOICE_MAP:
|
|
fieldsList.append(CROS_TELEMETRY_FIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, CROS_TELEMETRY_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'reverselists':
|
|
for field in _getFieldsList():
|
|
if field in CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP:
|
|
reverseLists.append(CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, CROS_TELEMETRY_LIST_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'showorgunitpath':
|
|
showOrgUnitPath = True
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
elif myarg == 'storagepercentonly':
|
|
diskPercentOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if fieldsList:
|
|
fieldsList.append('deviceId')
|
|
if showOrgUnitPath:
|
|
fieldsList.append('orgUnitId')
|
|
else:
|
|
fieldsList = list(CROS_TELEMETRY_FIELDS_CHOICE_MAP.values())
|
|
readMask = ','.join(set(fieldsList))
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(['deviceId', 'JSON'])
|
|
if not pfilters:
|
|
pfilters = [(None, 'All')]
|
|
for pfilter in pfilters:
|
|
printGettingAllAccountEntities(Ent.CROS_DEVICE, pfilter[1])
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
devices = callGAPIpages(cm.customers().telemetry().devices(), 'list', 'devices',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT, GAPI.INVALID_INPUT],
|
|
parent=parent, filter=pfilter[0],
|
|
readMask=readMask, pageSize=GC.Values[GC.DEVICE_MAX_RESULTS])
|
|
except (GAPI.invalidArgument, GAPI.invalidInput) as e:
|
|
message = str(e).replace('\n', ',')
|
|
entityActionFailedWarning([Ent.CROS_DEVICE, None], message)
|
|
return
|
|
except GAPI.permissionDenied as e:
|
|
accessErrorExitNonDirectory(API.CHROMEMANAGEMENT, str(e))
|
|
if csvPF:
|
|
for device in devices:
|
|
_printDevice(device)
|
|
else:
|
|
jcount = len(devices)
|
|
performActionNumItems(jcount, Ent.CROS_DEVICE)
|
|
j = 0
|
|
for device in devices:
|
|
j += 1
|
|
_showDevice(device, j, jcount)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('CrOS Devices Telemetry')
|
|
|
|
# gam delete browser <DeviceID>
|
|
def doDeleteBrowsers():
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
deviceId = getString(Cmd.OB_DEVICE_ID)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cbcm.chromebrowsers(), 'delete',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=customerId, deviceId=deviceId)
|
|
entityActionPerformed([Ent.CHROME_BROWSER, deviceId])
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_BROWSER, deviceId)
|
|
|
|
BROWSER_TIME_OBJECTS = {'firstRecordTime', 'lastActivityTime', 'lastPolicyFetchTime', 'lastRegistrationTime', 'lastStatusReportTime', 'safeBrowsingWarningsResetTime'}
|
|
|
|
def _showBrowser(browser, FJQC, i=0, count=0):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(browser), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CHROME_BROWSER, browser['deviceId']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, browser, timeObjects=BROWSER_TIME_OBJECTS, dictObjectsKey={'machinePolicies': 'name'})
|
|
Ind.Decrement()
|
|
|
|
BROWSER_FIELDS_CHOICE_MAP = {
|
|
'annotatedassetid': 'annotatedAssetId',
|
|
'annotatedlocation': 'annotatedLocation',
|
|
'annotatednotes': 'annotatedNotes',
|
|
'annotateduser': 'annotatedUser',
|
|
'asset': 'annotatedAssetId',
|
|
'assetid': 'annotatedAssetId',
|
|
'browsers': 'browsers',
|
|
'browserversions': 'browserVersions',
|
|
'deviceid': 'deviceId',
|
|
'deviceidentifiershistory': 'deviceIdentifiersHistory',
|
|
'extensioncount': 'extensionCount',
|
|
'lastactivitytime': 'lastActivityTime',
|
|
'lastdeviceuser': 'lastDeviceUser',
|
|
'lastdeviceusers': 'lastDeviceUsers',
|
|
'lastpolicyfetchtime': 'lastPolicyFetchTime',
|
|
'lastregistrationtime': 'lastRegistrationTime',
|
|
'laststatusreporttime': 'lastStatusReportTime',
|
|
'location': 'annotatedLocation',
|
|
'machineextensionpolicies': 'machineExtensionPolicies',
|
|
'machinename': 'machineName',
|
|
'machinepolicies': 'machinePolicies',
|
|
'notes': 'annotatedNotes',
|
|
'org': 'orgUnitPath',
|
|
'orgunit': 'orgUnitPath',
|
|
'orgunitpath': 'orgUnitPath',
|
|
'osarchitecture': 'osArchitecture',
|
|
'osplatform': 'osPlatform',
|
|
'osplatformversion': 'osPlatformVersion',
|
|
'osversion': 'osVersion',
|
|
'ou': 'orgUnitPath',
|
|
'policycount': 'policyCount',
|
|
'safebrowsingclickthroughcount': 'safeBrowsingClickThroughCount',
|
|
'serialnumber': 'serialNumber',
|
|
'user': 'annotatedUser',
|
|
'virtualdeviceid': 'virtualDeviceId',
|
|
}
|
|
BROWSER_ANNOTATED_FIELDS_LIST = ['annotatedAssetId', 'annotatedLocation', 'annotatedNotes', 'annotatedUser', 'deviceId']
|
|
BROWSER_FULL_ACCESS_FIELDS = {'browsers', 'lastDeviceUsers', 'lastStatusReportTime', 'machinePolicies'}
|
|
|
|
# gam info browser <DeviceID>
|
|
# (basic|full|annotated |
|
|
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]) |
|
|
# (rawfields <BrowserFieldNameList>))
|
|
# [formatjson]
|
|
def doInfoBrowsers():
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
deviceId = getString(Cmd.OB_DEVICE_ID)
|
|
projection = 'BASIC'
|
|
fieldsList = []
|
|
rawFields = None
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'annotated':
|
|
projection = 'BASIC'
|
|
fieldsList = BROWSER_ANNOTATED_FIELDS_LIST
|
|
elif myarg in PROJECTION_CHOICE_MAP:
|
|
projection = PROJECTION_CHOICE_MAP[myarg]
|
|
fieldsList = []
|
|
elif getFieldsList(myarg, BROWSER_FIELDS_CHOICE_MAP, fieldsList, initialField='deviceId'):
|
|
pass
|
|
elif myarg == 'rawfields':
|
|
projection = 'FULL'
|
|
rawFields = _getRawFields('deviceId')
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if projection == 'BASIC' and set(fieldsList).intersection(BROWSER_FULL_ACCESS_FIELDS):
|
|
projection = 'FULL'
|
|
fields = getFieldsFromFieldsList(fieldsList) if not rawFields else rawFields
|
|
try:
|
|
browser = callGAPI(cbcm.chromebrowsers(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN],
|
|
customer=customerId, deviceId=deviceId, projection=projection, fields=fields)
|
|
_showBrowser(browser, FJQC)
|
|
except GAPI.invalidArgument as e:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER, deviceId], str(e))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_BROWSER, deviceId)
|
|
|
|
# gam move browsers ou|org|orgunit <OrgUnitPath>
|
|
# ((ids <DeviceIDList>) |
|
|
# (queries <QueryBrowserList> [querytime<String> <Time>]) |
|
|
# (browserou <OrgUnitItem>) | (browserous <OrgUnitList>) |
|
|
# <FileSelector> | <CSVFileSelector>)
|
|
# [batchsize <Integer>]
|
|
def doMoveBrowsers():
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
deviceIds = []
|
|
batch_size = GC.Values[GC.BATCH_SIZE]
|
|
orgUnitPath = ''
|
|
queries = []
|
|
queryTimes = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'ou', 'org', 'orgunit'}:
|
|
orgUnitPath = getOrgUnitItem()
|
|
elif myarg == 'ids':
|
|
deviceIds.extend(convertEntityToList(getString(Cmd.OB_DEVICE_ID_LIST, minLen=0)))
|
|
elif myarg == 'file':
|
|
deviceIds.extend(getEntitiesFromFile(False))
|
|
elif myarg in {'csv', 'csvfile'}:
|
|
deviceIds.extend(getEntitiesFromCSVFile(False))
|
|
elif myarg in {'query', 'queries'}:
|
|
queries = getQueries(myarg)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
|
|
elif myarg == 'browserou':
|
|
deviceIds.extend(getItemsToModify(Cmd.ENTITY_BROWSER_OU, getOrgUnitItem(pathOnly=True, absolutePath=True)))
|
|
elif myarg == 'browserous':
|
|
deviceIds.extend(getItemsToModify(Cmd.ENTITY_BROWSER_OUS, getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True)))
|
|
elif myarg == 'batchsize':
|
|
batch_size = getInteger(minVal=1, maxVal=600)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not orgUnitPath:
|
|
missingArgumentExit('orgunit')
|
|
substituteQueryTimes(queries, queryTimes)
|
|
if queries:
|
|
deviceIds.extend(getItemsToModify(Cmd.ENTITY_BROWSER_QUERIES, queries))
|
|
body = {'org_unit_path': orgUnitPath}
|
|
bcount = 0
|
|
jcount = len(deviceIds)
|
|
j = 0
|
|
while bcount < jcount:
|
|
kcount = min(jcount-bcount, batch_size)
|
|
try:
|
|
body['resource_ids'] = deviceIds[bcount:bcount+kcount]
|
|
callGAPI(cbcm.chromebrowsers(), 'moveChromeBrowsersToOu',
|
|
mapNotFound=False,
|
|
throwReasons=[GAPI.INVALID_ORGUNIT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=customerId, body=body)
|
|
for deviceId in deviceIds:
|
|
j += 1
|
|
entityActionPerformed([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CHROME_BROWSER, deviceId], j, jcount)
|
|
bcount += kcount
|
|
except GAPI.invalidOrgunit:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], Msg.INVALID_ORGUNIT)
|
|
break
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CHROME_BROWSER, f'IDs: {deviceIds[bcount]} - {deviceIds[bcount+kcount-1]}'], str(e))
|
|
bcount += kcount
|
|
|
|
UPDATE_BROWSER_ARGUMENT_TO_PROPERTY_MAP = {
|
|
'annotatedassetid': 'annotatedAssetId',
|
|
'annotatedlocation': 'annotatedLocation',
|
|
'annotatednotes': 'annotatedNotes',
|
|
'annotateduser': 'annotatedUser',
|
|
'asset': 'annotatedAssetId',
|
|
'assetid': 'annotatedAssetId',
|
|
'location': 'annotatedLocation',
|
|
'notes': 'annotatedNotes',
|
|
'updatenotes': 'annotatedNotes',
|
|
'user': 'annotatedUser',
|
|
}
|
|
|
|
BROWSER_DEVICEID_ANNOTATED_FIELDS = 'deviceId,annotatedAssetId,annotatedLocation,annotatedNotes,annotatedUser'
|
|
|
|
# gam update browser <BrowserEntity> <BrowserAttibute>+ [updatenotes <String>]
|
|
def doUpdateBrowsers():
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_BROWSER, browserAllowed=True, crosAllowed=False, userAllowed=False)
|
|
body = {}
|
|
updateNotes = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in UPDATE_BROWSER_ARGUMENT_TO_PROPERTY_MAP:
|
|
up = UPDATE_BROWSER_ARGUMENT_TO_PROPERTY_MAP[myarg]
|
|
if up == 'annotatedNotes':
|
|
body[up] = getStringWithCRsNLs()
|
|
updateNotes = body[up] if myarg == 'updatenotes' and body[up].find('#notes#') != -1 else None
|
|
else:
|
|
body[up] = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
i = 0
|
|
count = len(entityList)
|
|
for deviceId in entityList:
|
|
i += 1
|
|
try:
|
|
browser = callGAPI(cbcm.chromebrowsers(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=customerId, deviceId=deviceId,
|
|
projection='BASIC', fields=BROWSER_DEVICEID_ANNOTATED_FIELDS)
|
|
if updateNotes:
|
|
body['annotatedNotes'] = updateNotes.replace('#notes#', browser['annotatedNotes'])
|
|
browser.update(body)
|
|
callGAPI(cbcm.chromebrowsers(), 'update',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=customerId, deviceId=deviceId,
|
|
body=browser, projection='BASIC', fields="deviceId")
|
|
entityActionPerformed([Ent.CHROME_BROWSER, deviceId], i, count)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_BROWSER, deviceId, i, count)
|
|
|
|
def _getChromeProfileName():
|
|
profileName = getString(Cmd.OB_CHROMEPROFILE_ID)
|
|
if not profileName.startswith('customers'):
|
|
customerId = _getCustomerId()
|
|
profileName = f'customers/{customerId}/profiles/{profileName}'
|
|
return profileName
|
|
|
|
# gam delete chromeprofile <ChromeProfileName>
|
|
def doDeleteChromeProfile():
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
|
|
profileName = _getChromeProfileName()
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cm.customers().profiles(), 'delete',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
|
|
name=profileName)
|
|
entityActionPerformed([Ent.CHROME_PROFILE, profileName])
|
|
except (GAPI.invalidArgument, GAPI.notFound, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit([Ent.CHROME_PROFILE, profileName], str(e))
|
|
|
|
CHROMEPROFILE_TIME_OBJECTS = {
|
|
'firstEnrollmentTime',
|
|
'lastActivityTime',
|
|
'lastPolicyFetchTime',
|
|
'lastPolicySyncTime',
|
|
'lastStatusReportTime',
|
|
}
|
|
|
|
def _showChromeProfile(profile, FJQC, i=0, count=0):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(profile, timeObjects=CHROMEPROFILE_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CHROME_PROFILE, profile['name']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, profile, timeObjects=CHROMEPROFILE_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
CHROMEPROFILE_FIELDS_CHOICE_MAP = {
|
|
'affiliationstate': 'affiliationState',
|
|
'annotatedlocation': 'annotatedLocation',
|
|
'annotateduser': 'annotatedUser',
|
|
'attestationcredential': 'attestationCredential',
|
|
'browserchannel': 'browserChannel',
|
|
'browserversion': 'browserVersion',
|
|
'deviceinfo': 'deviceInfo',
|
|
'displayname': 'displayName',
|
|
'extensioncount': 'extensionCount',
|
|
'firstenrollmenttime': 'firstEnrollmentTime',
|
|
'identityprovider':'identityProvider',
|
|
'lastactivitytime': 'lastActivityTime',
|
|
'lastpolicyfetchtime': 'lastPolicyFetchTime',
|
|
'lastpolicysynctime': 'lastPolicySyncTime',
|
|
'laststatusreporttime': 'lastStatusReportTime',
|
|
'name': 'name',
|
|
'osplatformtype': 'osPlatformType',
|
|
'osplatformversion':'osPlatformVersion',
|
|
'osversion': 'osVersion',
|
|
'policycount': 'policyCount',
|
|
'profileid': 'profileId',
|
|
'profilepermanentid': 'profilePermanentId',
|
|
'reportingdata': 'reportingData',
|
|
'useremail': 'userEmail',
|
|
'userid': 'userId',
|
|
}
|
|
|
|
# gam info chromeprofile <ChromeProfileName>
|
|
# <ChromeProfileFieldName>* [fields <ChromeProfileFieldNameList>]
|
|
# [formatjson]
|
|
def doInfoChromeProfile():
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
|
|
profileName = _getChromeProfileName()
|
|
fieldsList = []
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getFieldsList(myarg, CHROMEPROFILE_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
try:
|
|
profile = callGAPI(cm.customers().profiles(), 'get',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
|
|
name=profileName, fields=fields)
|
|
_showChromeProfile(profile, FJQC)
|
|
except (GAPI.invalidArgument, GAPI.notFound, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit([Ent.CHROME_PROFILE, profileName], str(e))
|
|
|
|
CHROMEPROFILE_ORDERBY_CHOICE_MAP = {
|
|
'affiliationstate': 'affiliationState',
|
|
'browserchannel': 'browserChannel',
|
|
'browserversion': 'browserVersion',
|
|
'displayname': 'displayName',
|
|
'extensioncount': 'extensionCount',
|
|
'firstenrollmenttime': 'firstEnrollmentTime',
|
|
'identityprovider': 'identityProvider',
|
|
'lastactivitytime': 'lastActivityTime',
|
|
'lastpolicysynctime': 'lastPolicySyncTime',
|
|
'laststatusreporttime': 'lastStatusReportTime',
|
|
'osplatformtype': 'osPlatformType',
|
|
'osversion': 'osVersion',
|
|
'policycount': 'policyCount',
|
|
'profileid': 'profileId',
|
|
'useremail': 'userEmail',
|
|
}
|
|
|
|
# gam show chromeprofiles
|
|
# [filtertime.* <Time>] [filter <String>]
|
|
# [orderby <ChromeProfileOrderByFieldName> [ascending|descending]]
|
|
# <ChromeProfileFieldName>* [fields <ChromeProfileFieldNameList>]
|
|
# [formatjson]
|
|
# gam print chromeprofiles [todrive <ToDriveAttribute>*]
|
|
# [filtertime.* <Time>] [filter <String>]
|
|
# [orderby <ChromeProfileOrderByFieldName> [ascending|descending]]
|
|
# <ChromeProfileFieldName>* [fields <ChromeProfileFieldNameList>]
|
|
# [[formatjson [quotechar <Character>]]
|
|
def doPrintShowChromeProfiles():
|
|
def _printProfile(profile):
|
|
row = flattenJSON(profile, timeObjects=CHROMEPROFILE_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'name': profile['name'], 'profileId': profile['profileId'],
|
|
'JSON': json.dumps(cleanJSON(profile, timeObjects=CHROMEPROFILE_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
|
|
csvPF = CSVPrintFile(['name', 'profileId']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
OBY = OrderBy(CHROMEPROFILE_ORDERBY_CHOICE_MAP)
|
|
sortHeaders = False
|
|
fieldsList = []
|
|
cbfilter = None
|
|
filterTimes = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getFieldsList(myarg, CHROMEPROFILE_FIELDS_CHOICE_MAP, fieldsList, initialField=['name', 'profileId']):
|
|
pass
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg.startswith('filtertime'):
|
|
filterTimes[myarg] = getTimeOrDeltaFromNow()
|
|
elif myarg in {'filter', 'filters'}:
|
|
cbfilter = getString(Cmd.OB_STRING)
|
|
elif myarg == 'sortheaders':
|
|
sortHeaders = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if filterTimes and filter is not None:
|
|
for filterTimeName, filterTimeValue in iter(filterTimes.items()):
|
|
cbfilter = cbfilter.replace(f'#{filterTimeName}#', filterTimeValue)
|
|
fields = getItemFieldsFromFieldsList('chromeBrowserProfiles', fieldsList)
|
|
customerId = _getCustomerId()
|
|
parent = f'customers/{customerId}'
|
|
printGettingAllAccountEntities(Ent.CHROME_PROFILE, cbfilter)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
feed = yieldGAPIpages(cm.customers().profiles(), 'list', 'chromeBrowserProfiles',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
parent=parent, pageSize=200,
|
|
filter=cbfilter, orderBy=OBY.orderBy, fields=fields)
|
|
for profiles in feed:
|
|
if not csvPF:
|
|
jcount = len(profiles)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, Ent.CHROME_PROFILE)
|
|
Ind.Increment()
|
|
j = 0
|
|
for profile in profiles:
|
|
j += 1
|
|
_showChromeProfile(profile, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for profile in profiles:
|
|
_printProfile(profile)
|
|
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit([Ent.CHROME_PROFILE, cbfilter], str(e))
|
|
if csvPF:
|
|
if sortHeaders:
|
|
csvPF.SetSortTitles(['name', 'profileId'])
|
|
csvPF.writeCSVfile('Chrome Profiles')
|
|
|
|
BROWSER_ORDERBY_CHOICE_MAP = {
|
|
'annotatedassetid': 'annotated_asset_id', 'asset': 'annotated_asset_id', 'assetid': 'annotated_asset_id',
|
|
'annotatedlocation': 'annotated_location', 'location': 'annotated_location',
|
|
'annotatednotes': 'notes', 'notes': 'notes',
|
|
'annotateduser': 'annotated_user', 'user': 'annotated_user',
|
|
'browserversionchannel': 'browser_version_channel',
|
|
'browserversionsortable': 'browser_version_sortable',
|
|
'deviceid': 'id', 'id': 'id',
|
|
'enrollmentdate': 'enrollment_date',
|
|
'extensioncount': 'extension_count',
|
|
'lastactivity': 'last_activity',
|
|
'lastsignedinuser': 'last_signed_in_user',
|
|
'lastsync': 'last_sync',
|
|
'machinename': 'machine_name',
|
|
'orgunit': 'org_unit', 'ou': 'org_unit', 'org': 'org_unit',
|
|
'osversion': 'os_version',
|
|
'osversionsortable': 'os_version_sortable',
|
|
'platformmajorversion': 'platform_major_version',
|
|
'policycount': 'policy_count',
|
|
# 'safebrowsingclickthrough': 'safe_browsing_clickthrough',
|
|
}
|
|
|
|
# gam show browsers
|
|
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
|
|
# [querytime<String> <Time>]
|
|
# [orderby <BrowserOrderByFieldName> [ascending|descending]]
|
|
# (basic|full|annotated |
|
|
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]) |
|
|
# (rawfields <BrowserFieldNameList>))
|
|
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]|(rawfields <BrowserFieldNameList>)
|
|
# [formatjson]
|
|
# gam print browsers [todrive <ToDriveAttribute>*]
|
|
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowser)|(queries <QueryBrowserList>))|(select <BrowserEntity>))
|
|
# [querytime<String> <Time>]
|
|
# [orderby <BrowserOrderByFieldName> [ascending|descending]]
|
|
# (basic|full|annotated |
|
|
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]) |
|
|
# (rawfields <BrowserFieldNameList>))
|
|
# (<BrowserFieldName>* [fields <BrowserFieldNameList>]|(rawfields <BrowserFieldNameList>)
|
|
# [sortheaders] [formatjson [quotechar <Character>]]
|
|
def doPrintShowBrowsers():
|
|
def _printBrowser(browser):
|
|
row = flattenJSON(browser, timeObjects=BROWSER_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'deviceId': browser['deviceId'],
|
|
'JSON': json.dumps(cleanJSON(browser, timeObjects=BROWSER_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
csvPF = CSVPrintFile(['deviceId']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
rawFields = None
|
|
projection = 'BASIC'
|
|
orderBy = 'id'
|
|
sortOrder = 'ASCENDING'
|
|
entityList = orgUnitPath = None
|
|
queries = [None]
|
|
queryTimes = {}
|
|
sortHeaders = sortRows = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'query', 'queries'}:
|
|
queries = getQueries(myarg)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
|
|
elif myarg in {'ou', 'org', 'orgunit', 'browserou'}:
|
|
orgUnitPath = getOrgUnitItem(pathOnly=True, absolutePath=True)
|
|
elif myarg == 'select':
|
|
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_BROWSER, browserAllowed=True, crosAllowed=False, userAllowed=False)
|
|
elif myarg == 'orderby':
|
|
orderBy, sortOrder = getOrderBySortOrder(BROWSER_ORDERBY_CHOICE_MAP, 'DESCENDING', True)
|
|
elif myarg == 'annotated':
|
|
projection = 'BASIC'
|
|
fieldsList = BROWSER_ANNOTATED_FIELDS_LIST
|
|
elif (myarg == 'projection') or myarg in PROJECTION_CHOICE_MAP:
|
|
if myarg == 'projection':
|
|
projection = getChoice(PROJECTION_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
projection = PROJECTION_CHOICE_MAP[myarg]
|
|
fieldsList = []
|
|
elif myarg == 'allfields':
|
|
projection = 'FULL'
|
|
sortHeaders = True
|
|
fieldsList = []
|
|
elif myarg == 'sortheaders':
|
|
sortHeaders = True
|
|
elif getFieldsList(myarg, BROWSER_FIELDS_CHOICE_MAP, fieldsList, initialField='deviceId'):
|
|
pass
|
|
elif myarg == 'rawfields':
|
|
projection = 'FULL'
|
|
rawFields = _getRawFields('deviceId')
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if projection == 'BASIC' and set(fieldsList).intersection(BROWSER_FULL_ACCESS_FIELDS):
|
|
projection = 'FULL'
|
|
if FJQC.formatJSON:
|
|
sortHeaders = False
|
|
substituteQueryTimes(queries, queryTimes)
|
|
if entityList is None:
|
|
fields = getItemFieldsFromFieldsList('browsers', fieldsList) if not rawFields else f'nextPageToken,browsers({rawFields})'
|
|
for query in queries:
|
|
printGettingAllAccountEntities(Ent.CHROME_BROWSER, query)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
feed = yieldGAPIpages(cbcm.chromebrowsers(), 'list', 'browsers',
|
|
pageMessage=pageMessage, messageAttribute='deviceId',
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customerId, orgUnitPath=orgUnitPath, query=query, projection=projection,
|
|
orderBy=orderBy, sortOrder=sortOrder, fields=fields)
|
|
for browsers in feed:
|
|
if not csvPF:
|
|
jcount = len(browsers)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, Ent.CHROME_BROWSER)
|
|
Ind.Increment()
|
|
j = 0
|
|
for browser in browsers:
|
|
j += 1
|
|
_showBrowser(browser, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for browser in browsers:
|
|
_printBrowser(browser)
|
|
except GAPI.invalidInput as e:
|
|
if query:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER, None], invalidQuery(query))
|
|
else:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER, None], str(e))
|
|
return
|
|
except (GAPI.invalidArgument, GAPI.invalidOrgunit, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER, None], str(e))
|
|
return
|
|
except (GAPI.badRequest, GAPI.resourceNotFound):
|
|
accessErrorExit(None)
|
|
else:
|
|
sortRows = True
|
|
jcount = len(entityList)
|
|
fields = getFieldsFromFieldsList(fieldsList) if not rawFields else rawFields
|
|
j = 0
|
|
for deviceId in entityList:
|
|
j += 1
|
|
try:
|
|
browser = callGAPI(cbcm.chromebrowsers(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN],
|
|
customer=customerId, deviceId=deviceId, projection=projection, fields=fields)
|
|
_printBrowser(browser)
|
|
except GAPI.invalidArgument as e:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER, deviceId], str(e))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_BROWSER, deviceId)
|
|
if csvPF:
|
|
if sortRows and orderBy:
|
|
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
|
|
if sortHeaders:
|
|
csvPF.SetSortTitles(['deviceId'])
|
|
csvPF.writeCSVfile('Browsers')
|
|
|
|
BROWSER_TOKEN_TIME_OBJECTS = {'createTime', 'expireTime', 'revokeTime'}
|
|
|
|
def _showBrowserToken(browser, FJQC, i=0, count=0):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(browser), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, browser['token']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, browser, timeObjects=BROWSER_TOKEN_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
# gam create browsertoken
|
|
# [ou|org|orgunit|browserou <OrgUnitPath>] [expire|expires <Time>]
|
|
# [formatjson]
|
|
def doCreateBrowserToken():
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
FJQC = FormatJSONQuoteChar()
|
|
body = {'token_type': 'CHROME_BROWSER'}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'ou', 'org', 'orgunit', 'browserou'}:
|
|
body['org_unit_path'] = getOrgUnitItem(pathOnly=True, absolutePath=True)
|
|
elif myarg in ['expire', 'expires']:
|
|
body['expire_time'] = getTimeOrDeltaFromNow()
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
try:
|
|
browser = callGAPI(cbcm.enrollmentTokens(), 'create',
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
|
|
customer=customerId, body=body)
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, browser['token']])
|
|
Ind.Increment()
|
|
_showBrowserToken(browser, FJQC, 0, 0)
|
|
Ind.Decrement()
|
|
except (GAPI.invalidInput, GAPI.invalidOrgunit) as e:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, None], str(e))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(None)
|
|
|
|
# gam revoke browsertoken <BrowserTokenPermanentID>
|
|
def doRevokeBrowserToken():
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
tokenPermanentId = getString(Cmd.OB_BROWSER_ENROLLEMNT_TOKEN_ID)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cbcm.enrollmentTokens(), 'revoke',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
|
|
customer=customerId, tokenPermanentId=tokenPermanentId)
|
|
entityActionPerformed([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, tokenPermanentId])
|
|
except (GAPI.invalid, GAPI.invalidInput, GAPI.badRequest, GAPI.resourceNotFound, GAPI.invalidOrgunit) as e:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, tokenPermanentId], str(e))
|
|
except GAPI.forbidden:
|
|
accessErrorExit(None)
|
|
|
|
BROWSER_TOKEN_FIELDS_CHOICE_MAP = {
|
|
'createtime': 'createTime',
|
|
'creatorid': 'creatorId',
|
|
'customerid': 'customerId',
|
|
'expiretime': 'expireTime',
|
|
'org': 'orgUnitPath',
|
|
'orgunit': 'orgUnitPath',
|
|
'orgunitpath': 'orgUnitPath',
|
|
'ou': 'orgUnitPath',
|
|
'revoketime': 'revokeTime',
|
|
'revokerid': 'revokerId',
|
|
'state': 'state',
|
|
'token': 'token',
|
|
'tokenpermanentid': 'tokenPermanentId',
|
|
}
|
|
|
|
# gam show browsertokens
|
|
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>)))
|
|
# [querytime<String> <Time>]
|
|
# [orderby <BrowserTokenFieldName> [ascending|descending]]
|
|
# [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
|
|
# [formatjson]
|
|
# gam print browsertokens [todrive <ToDriveAttribute>*]
|
|
# ([ou|org|orgunit|browserou <OrgUnitPath>] [(query <QueryBrowserToken)|(queries <QueryBrowserTokenList>)))
|
|
# [querytime<String> <Time>]
|
|
# [orderby <BrowserTokenFieldName> [ascending|descending]]
|
|
# [allfields] <BrowserTokenFieldName>* [fields <BrowserTokenFieldNameList>]
|
|
# [sortheaders] [formatjson [quotechar <Character>]]
|
|
def doPrintShowBrowserTokens():
|
|
def _printBrowserToken(browser):
|
|
row = flattenJSON(browser, timeObjects=BROWSER_TOKEN_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'token': browser['token'],
|
|
'JSON': json.dumps(cleanJSON(browser, timeObjects=BROWSER_TOKEN_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
csvPF = CSVPrintFile(['token']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
orderBy = 'token'
|
|
sortOrder = 'ASCENDING'
|
|
orgUnitPath = None
|
|
queries = [None]
|
|
queryTimes = {}
|
|
sortHeaders = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'query', 'queries'}:
|
|
queries = getQueries(myarg)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
|
|
elif myarg in {'ou', 'org', 'orgunit', 'browserou'}:
|
|
orgUnitPath = getOrgUnitItem(pathOnly=True, absolutePath=True)
|
|
elif myarg == 'orderby':
|
|
orderBy, sortOrder = getOrderBySortOrder(BROWSER_TOKEN_FIELDS_CHOICE_MAP, 'DESCENDING', True)
|
|
elif myarg == 'allfields':
|
|
sortHeaders = True
|
|
fieldsList = []
|
|
elif myarg == 'sortheaders':
|
|
sortHeaders = True
|
|
elif getFieldsList(myarg, BROWSER_TOKEN_FIELDS_CHOICE_MAP, fieldsList, initialField='token'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
fields = getItemFieldsFromFieldsList('chromeEnrollmentTokens', fieldsList)
|
|
if FJQC.formatJSON:
|
|
sortHeaders = False
|
|
substituteQueryTimes(queries, queryTimes)
|
|
for query in queries:
|
|
printGettingAllAccountEntities(Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, query)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
browsers = callGAPIpages(cbcm.enrollmentTokens(), 'list', 'chromeEnrollmentTokens',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.INVALID_ORGUNIT, GAPI.FORBIDDEN],
|
|
customer=customerId, orgUnitPath=orgUnitPath, query=query,
|
|
fields=fields)
|
|
if not csvPF:
|
|
jcount = len(browsers)
|
|
performActionNumItems(jcount, Ent.CHROME_BROWSER_ENROLLMENT_TOKEN)
|
|
Ind.Increment()
|
|
j = 0
|
|
for browser in browsers:
|
|
j += 1
|
|
_showBrowserToken(browser, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for browser in browsers:
|
|
_printBrowserToken(browser)
|
|
except GAPI.invalidInput as e:
|
|
if query:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, None], invalidQuery(query))
|
|
else:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, None], str(e))
|
|
except GAPI.invalidOrgunit as e:
|
|
entityActionFailedWarning([Ent.CHROME_BROWSER_ENROLLMENT_TOKEN, None], str(e))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(None)
|
|
if csvPF:
|
|
if orderBy:
|
|
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
|
|
if sortHeaders:
|
|
csvPF.SetSortTitles(['token'])
|
|
csvPF.writeCSVfile('Chrome Browser Enrollment Tokens')
|
|
|
|
def buildChatServiceObject(api=API.CHAT, user=None, i=0, count=0, entityTypeList=None, useAdminAccess=False):
|
|
if user is None:
|
|
_, chat = buildGAPIServiceObject(API.CHAT, user)
|
|
kvList = [Ent.CHAT_BOT, None]
|
|
else:
|
|
user, chat = buildGAPIServiceObject(api, user, i, count)
|
|
if not useAdminAccess:
|
|
kvList = [Ent.USER, user]
|
|
else:
|
|
kvList = [Ent.CHAT_ADMIN, f'{user}(asadmin)']
|
|
if entityTypeList is not None:
|
|
kvList.extend(entityTypeList)
|
|
return user, chat, kvList
|
|
|
|
def setupChatURL(chat):
|
|
return f'https://console.cloud.google.com/apis/api/chat.googleapis.com/hangouts-chat?project={chat._http.credentials.project_id}'
|
|
|
|
def exitIfChatNotConfigured(chat, kvList, errMsg, i, count):
|
|
if (('No bot associated with this project.' in errMsg) or
|
|
('Invalid project number.' in errMsg) or
|
|
('Google Chat app not found.' in errMsg)):
|
|
systemErrorExit(API_ACCESS_DENIED_RC, Msg.TO_SET_UP_GOOGLE_CHAT.format(setupChatURL(chat)))
|
|
entityActionFailedWarning(kvList, errMsg, i, count)
|
|
|
|
def _getChatAdminAccess(adminAPI, userAPI):
|
|
if checkArgumentPresent(ADMIN_ACCESS_OPTIONS) or GC.Values[GC.USE_CHAT_ADMIN_ACCESS]:
|
|
return (True, adminAPI, {'useAdminAccess': True})
|
|
return (False, userAPI, {})
|
|
|
|
def _chkChatAdminAccess(count):
|
|
if count != 1:
|
|
usageErrorExit(Msg.CHAT_ADMIN_ACCESS_LIMITED_TO_ONE_USER.format(count))
|
|
|
|
def _cleanChatSpace(space):
|
|
space.pop('type', None)
|
|
space.pop('threaded', None)
|
|
|
|
def _cleanChatMessage(message):
|
|
message.pop('cards', None)
|
|
|
|
CHAT_TIME_OBJECTS = {'createTime', 'deleteTime', 'eventTime', 'lastActiveTime', 'lastUpdateTime'}
|
|
|
|
def _showChatItem(citem, entityType, FJQC, i=0, count=0):
|
|
if entityType == Ent.CHAT_SPACE:
|
|
_cleanChatSpace(citem)
|
|
elif entityType == Ent.CHAT_MESSAGE:
|
|
_cleanChatMessage(citem)
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(citem, timeObjects=CHAT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([entityType, citem['name']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, citem, timeObjects=CHAT_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
def _printChatItem(user, citem, parent, entityType, csvPF, FJQC, addCSVData=None):
|
|
if entityType == Ent.CHAT_SPACE:
|
|
_cleanChatSpace(citem)
|
|
baserow = {'User': user} if user is not None else {}
|
|
else:
|
|
if user is not None:
|
|
baserow = {'User': user, 'space.name': parent['name'], 'space.displayName': parent['displayName']}
|
|
else:
|
|
baserow = {'space.name': parent['name'], 'space.displayName': parent['displayName']}
|
|
if entityType == Ent.CHAT_MEMBER:
|
|
if addCSVData:
|
|
baserow.update(addCSVData)
|
|
elif entityType == Ent.CHAT_MESSAGE:
|
|
_cleanChatMessage(citem)
|
|
row = flattenJSON(citem, flattened=baserow.copy(), timeObjects=CHAT_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = baserow.copy()
|
|
row.update({'name': citem['name'],
|
|
'JSON': json.dumps(cleanJSON(citem, timeObjects=CHAT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
# gam setup chat
|
|
def doSetupChat():
|
|
checkForExtraneousArguments()
|
|
_, chat , _ = buildChatServiceObject()
|
|
writeStdout(Msg.TO_SET_UP_GOOGLE_CHAT.format(setupChatURL(chat)))
|
|
|
|
def getSpaceName(myarg):
|
|
if myarg == 'space':
|
|
chatSpace = getString(Cmd.OB_CHAT_SPACE)
|
|
if chatSpace.startswith('spaces/'):
|
|
return chatSpace
|
|
if not chatSpace.startswith('space/'):
|
|
return 'spaces/'+chatSpace
|
|
_, chatSpace = chatSpace.split('/', 1)
|
|
else: # myarg.startswith('spaces/') or myarg.startswith('space/')
|
|
_, chatSpace = Cmd.Previous().split('/', 1)
|
|
return 'spaces/'+chatSpace
|
|
|
|
def getChatSpaceParameters(myarg, body, typeChoicesMap, updateMask):
|
|
if myarg == 'displayname':
|
|
body['displayName'] = getString(Cmd.OB_STRING, minLen=0, maxLen=128)
|
|
updateMask.add('displayName')
|
|
elif myarg == 'type':
|
|
body['spaceType'] = getChoice(typeChoicesMap, mapChoice=True)
|
|
updateMask.add('spaceType')
|
|
elif myarg == 'description':
|
|
body.setdefault('spaceDetails', {})
|
|
body['spaceDetails']['description'] = getString(Cmd.OB_STRING, minLen=0, maxLen=150)
|
|
updateMask.add('spaceDetails')
|
|
elif myarg in {'guidelines', 'rules'}:
|
|
body.setdefault('spaceDetails', {})
|
|
body['spaceDetails']['guidelines'] = getString(Cmd.OB_STRING, minLen=0, maxLen=5000)
|
|
updateMask.add('spaceDetails')
|
|
elif myarg == 'history':
|
|
body['spaceHistoryState'] = 'HISTORY_ON' if getBoolean() else 'HISTORY_OFF'
|
|
updateMask.add('spaceHistoryState')
|
|
elif myarg in {'audience', 'restricted'}:
|
|
body['accessSettings']= {'audience': None}
|
|
if myarg == 'audience':
|
|
body['accessSettings']['audience'] = getString(Cmd.OB_STRING, minLen=1, maxLen=100)
|
|
updateMask.add('accessSettings.audience')
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
CHAT_MEMBER_ROLE_MAP = {
|
|
'member': 'ROLE_MEMBER',
|
|
'manager': 'ROLE_MANAGER'
|
|
}
|
|
|
|
CHAT_MEMBER_TYPE_MAP = {
|
|
'bot': 'BOT',
|
|
'human': 'HUMAN'
|
|
}
|
|
|
|
CHAT_SPACE_TYPE_MAP = {
|
|
'space': 'SPACE',
|
|
'groupchat': 'GROUP_CHAT',
|
|
'directmessage': 'DIRECT_MESSAGE',
|
|
}
|
|
|
|
CHAT_SPACE_PREDEFINED_PERMS_MAP = {
|
|
'announcement': 'ANNOUNCEMENT_SPACE',
|
|
'collaboration': 'COLLABORATION_SPACE',
|
|
}
|
|
|
|
CHAT_SPACE_MIN_MAX_MEMBERS = {
|
|
'SPACE': {'min': 0, 'max': 20},
|
|
'GROUP_CHAT': {'min': 2, 'max': 20},
|
|
'DIRECT_MESSAGE': {'min': 1, 'max': 1},
|
|
}
|
|
# gam <UserTypeEntity> create chatspace
|
|
# [type <ChatSpaceType>] [announcement|collaboration]
|
|
# [restricted|(audience <String>)]
|
|
# [externalusersallowed <Boolean>]
|
|
# [members <UserTypeEntity>]
|
|
# [displayname <String>]
|
|
# [description <String>] [guidelines|rules <String>]
|
|
# [history <Boolean>]
|
|
# [<ChatContent>]
|
|
# [formatjson|returnidonly]
|
|
def createChatSpace(users):
|
|
FJQC = FormatJSONQuoteChar()
|
|
body = {'space': {'spaceType': CHAT_SPACE_TYPE_MAP['space'], 'displayName': ''}, 'requestId': str(uuid.uuid4())}
|
|
members = []
|
|
tbody = {}
|
|
returnIdOnly = False
|
|
updateMask = set()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getChatSpaceParameters(myarg, body['space'], CHAT_SPACE_TYPE_MAP, updateMask):
|
|
pass
|
|
elif myarg in CHAT_SPACE_PREDEFINED_PERMS_MAP:
|
|
body['space']['predefinedPermissionSettings'] = CHAT_SPACE_PREDEFINED_PERMS_MAP[myarg]
|
|
elif myarg == 'externalusersallowed':
|
|
body['space']['externalUserAllowed'] = getBoolean()
|
|
elif myarg == 'members':
|
|
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
elif myarg in SORF_TEXT_ARGUMENTS:
|
|
tbody['text'] = getStringOrFile(myarg, minLen=0, unescapeCRLF=True)[0]
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
spaceType = body['space']['spaceType']
|
|
jcount = len(members)
|
|
if (jcount < CHAT_SPACE_MIN_MAX_MEMBERS[spaceType]['min'] or
|
|
jcount > CHAT_SPACE_MIN_MAX_MEMBERS[spaceType]['max']):
|
|
systemErrorExit(USAGE_ERROR_RC,
|
|
Msg.INVALID_NUMBER_OF_CHAT_SPACE_MEMBERS.format(Ent.Singular(Ent.CHAT_SPACE),
|
|
spaceType, jcount,
|
|
CHAT_SPACE_MIN_MAX_MEMBERS[spaceType]['min'],
|
|
CHAT_SPACE_MIN_MAX_MEMBERS[spaceType]['max']))
|
|
mtype = CHAT_MEMBER_TYPE_MAP['human']
|
|
if members:
|
|
body['memberships'] = []
|
|
for member in members:
|
|
name = normalizeEmailAddressOrUID(member)
|
|
body['memberships'].append({'member': {'name': f'users/{name}', 'type': mtype}})
|
|
if spaceType == 'SPACE':
|
|
if not body['space']['displayName']:
|
|
missingArgumentExit('displayname')
|
|
elif spaceType == 'GROUP_CHAT':
|
|
body['space'].pop('displayName', None)
|
|
body['space'].pop('predefinedPermissionSettings', None)
|
|
else: # DIRECT_MESSAGE
|
|
body['space'].pop('displayName', None)
|
|
body['space'].pop('spaceDetails', None)
|
|
body['space'].pop('predefinedPermissionSettings', None)
|
|
body['space']['singleUserBotDm'] = False
|
|
if tbody:
|
|
trimChatMessageIfRequired(tbody)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(API.CHAT_SPACES, user, i, count,
|
|
[Ent.CHAT_SPACE, body['space'].get('displayName', spaceType)])
|
|
if not chat:
|
|
continue
|
|
try:
|
|
space = callGAPI(chat.spaces(), 'setup',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
body=body)
|
|
if not returnIdOnly:
|
|
kvList[-1] = space['name']
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, i, count)
|
|
Ind.Increment()
|
|
_showChatItem(space, Ent.CHAT_SPACE, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
writeStdout(f'{space["name"]}\n')
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
continue
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
if tbody:
|
|
parent = space['name']
|
|
_, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count,
|
|
[Ent.CHAT_SPACE, body['space'].get('displayName', parent)])
|
|
if not chat:
|
|
continue
|
|
try:
|
|
resp = callGAPI(chat.spaces().messages(), 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
parent=parent, requestId=str(uuid.uuid4()), body=tbody)
|
|
if not returnIdOnly:
|
|
kvList.extend([Ent.CHAT_MESSAGE, resp['name']])
|
|
entityActionPerformed(kvList, i, count)
|
|
else:
|
|
writeStdout(f'{resp["name"]}\n')
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
|
|
CHAT_UPDATE_SPACE_TYPE_MAP = {
|
|
'space': 'SPACE',
|
|
}
|
|
|
|
CHAT_SPACE_ROLE_PERMISSIONS_MAP = {
|
|
'managers': 'managersAllowed',
|
|
'members': 'membersAllowed',
|
|
}
|
|
|
|
CHAT_UPDATE_SPACE_PERMISSIONS_MAP = {
|
|
'managemembersandgroups': 'manageMembersAndGroups',
|
|
'modifyspacedetails': 'modifySpaceDetails',
|
|
'togglehistory': 'toggleHistory',
|
|
'useatmentionall': 'useAtMentionAll',
|
|
'manageapps': 'manageApps',
|
|
'managewebhooks': 'manageWebhooks',
|
|
'replymessages': 'replyMessages',
|
|
}
|
|
|
|
# gam <UserTypeEntity> update chatspace <ChatSpace>
|
|
# [restricted|(audience <String>)]|
|
|
# ([displayname <String>]
|
|
# [type space]
|
|
# [description <String>] [guidelines|rules <String>]
|
|
# [history <Boolean>])
|
|
# managemembersandgroups managers|members
|
|
# modifyspacedetails managers|members
|
|
# togglehistory managers|members
|
|
# useatmentionall managers|members
|
|
# manageapps managers|members
|
|
# managewebhooks managers|members
|
|
# replymessages managers|members
|
|
# [formatjson]
|
|
def updateChatSpace(users):
|
|
FJQC = FormatJSONQuoteChar()
|
|
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_SPACES_ADMIN, API.CHAT_SPACES)
|
|
name = None
|
|
body = {}
|
|
updateMask = set()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
|
|
name = getSpaceName(myarg)
|
|
elif getChatSpaceParameters(myarg, body, CHAT_UPDATE_SPACE_TYPE_MAP, updateMask):
|
|
pass
|
|
elif myarg in CHAT_UPDATE_SPACE_PERMISSIONS_MAP:
|
|
body.setdefault('permissionSettings', {})
|
|
permissionSetting = CHAT_UPDATE_SPACE_PERMISSIONS_MAP[myarg]
|
|
role = getChoice(CHAT_SPACE_ROLE_PERMISSIONS_MAP, mapChoice=True)
|
|
body['permissionSettings'][permissionSetting] = {'managersAllowed': True}
|
|
if role == 'membersAllowed':
|
|
body['permissionSettings'][permissionSetting].update({'membersAllowed': True})
|
|
updateMask.add(f'permissionSettings.{permissionSetting}')
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not name:
|
|
missingArgumentExit('space')
|
|
if 'accessSettings.audience' in updateMask:
|
|
tempMask = updateMask-{'accessSettings.audience'}
|
|
if tempMask:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('restricted/audience', 'displayname,type,description,guidelines,history'))
|
|
i, count, users = getEntityArgument(users)
|
|
if useAdminAccess:
|
|
_chkChatAdminAccess(count)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, name], useAdminAccess)
|
|
if not chat:
|
|
continue
|
|
try:
|
|
space = callGAPI(chat.spaces(), 'patch',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name, updateMask=','.join(updateMask), body=body, **kwargsUAA)
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, i, count)
|
|
Ind.Increment()
|
|
_showChatItem(space, Ent.CHAT_SPACE, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> delete chatspace <ChatSpace>
|
|
# gam <UserItem> delete chatspace asadmin <ChatSpace>
|
|
def deleteChatSpace(users):
|
|
name = None
|
|
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_SPACES_DELETE_ADMIN, API.CHAT_SPACES_DELETE)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
|
|
name = getSpaceName(myarg)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not name:
|
|
missingArgumentExit('space')
|
|
i, count, users = getEntityArgument(users)
|
|
if useAdminAccess:
|
|
_chkChatAdminAccess(count)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, name], useAdminAccess)
|
|
if not chat:
|
|
continue
|
|
try:
|
|
callGAPI(chat.spaces(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name, **kwargsUAA)
|
|
entityActionPerformed(kvList, i, count)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
|
|
CHAT_SPACES_FIELDS_CHOICE_MAP = {
|
|
"accesssettings": "accessSettings",
|
|
"admininstalled": "adminInstalled",
|
|
"createtime": "createTime",
|
|
"displayname": "displayName",
|
|
"externaluserallowed": "externalUserAllowed",
|
|
"importmode": "importMode",
|
|
"lastactivetime": "lastActiveTime",
|
|
"membershipcount": "membershipCount",
|
|
"name": "name",
|
|
"permissionsettings": "permissionSettings",
|
|
"singleuserbotdm": "singleUserBotDm",
|
|
"spacedetails": "spaceDetails",
|
|
"spacehistorystate": "spaceHistoryState",
|
|
"spacethreadingstate": "spaceThreadingState",
|
|
"spacetype": "spaceType",
|
|
"spaceuri": "spaceUri",
|
|
"threaded": "spaceThreadingState",
|
|
"type": "spaceType",
|
|
}
|
|
|
|
# gam [<UserTypeEntity>] info chatspace <ChatSpace>
|
|
# [fields <ChatSpaceFieldNameList>]
|
|
# [formatjson]
|
|
# gam <UserItem> info chatspace asadmin <ChatSpace>
|
|
# [fields <ChatSpaceFieldNameList>]
|
|
# [formatjson]
|
|
def infoChatSpace(users, name=None):
|
|
FJQC = FormatJSONQuoteChar()
|
|
if name is None:
|
|
function = 'get'
|
|
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_SPACES_ADMIN, API.CHAT_SPACES)
|
|
else:
|
|
function = 'findDirectMessage'
|
|
useAdminAccess = None
|
|
kwargsUAA = {}
|
|
api = API.CHAT_SPACES
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if function == 'get' and (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
|
|
name = getSpaceName(myarg)
|
|
elif getFieldsList(myarg, CHAT_SPACES_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if function == 'get' and not name:
|
|
missingArgumentExit('space')
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
if useAdminAccess:
|
|
_chkChatAdminAccess(count)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, name], useAdminAccess)
|
|
if not chat:
|
|
continue
|
|
try:
|
|
space = callGAPI(chat.spaces(), function,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name, fields=fields, **kwargsUAA)
|
|
if not FJQC.formatJSON:
|
|
entityPerformAction(kvList, i, count)
|
|
Ind.Increment()
|
|
_showChatItem(space, Ent.CHAT_SPACE, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
|
|
def doInfoChatSpace():
|
|
infoChatSpace([None])
|
|
|
|
# gam [<UserTypeEntity>] info chatspacedm <UserItem>
|
|
# [fields <ChatSpaceFieldNameList>]
|
|
# [formatjson]
|
|
def infoChatSpaceDM(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
name = convertEmailAddressToUID(getEmailAddress(returnUIDprefix='uid:'), cd, 'user')
|
|
infoChatSpace(users, f'users/{name}')
|
|
|
|
def _getChatPageMessage(entityType, user, i, count, pfilter, useAdminAccess=False):
|
|
if user is not None:
|
|
printGettingAllEntityItemsForWhom(entityType, user if not useAdminAccess else f'{user}(asadmin)', i, count, pfilter)
|
|
return getPageMessageForWhom()
|
|
printGettingAllAccountEntities(entityType, pfilter)
|
|
return getPageMessage()
|
|
|
|
def _getChatSpaceListParms(myarg, kwargs):
|
|
if myarg in {'type', 'types'}:
|
|
for ctype in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
|
|
if ctype in CHAT_SPACE_TYPE_MAP:
|
|
kwargs.setdefault('filter', '')
|
|
if kwargs['filter']:
|
|
kwargs['filter'] += ' OR '
|
|
kwargs['filter'] += f'spaceType = "{CHAT_SPACE_TYPE_MAP[ctype]}"'
|
|
else:
|
|
invalidChoiceExit(ctype, CHAT_SPACE_TYPE_MAP, True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _getChatSpaceSearchParms(myarg, queries, queryTimes, OBY):
|
|
if myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg == 'query':
|
|
queries[0] += ' AND '+getString(Cmd.OB_QUERY)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
CHAT_PAGE_SIZE = 1000
|
|
CHAT_SPACES_ADMIN_ORDERBY_CHOICE_MAP = {
|
|
'createtime': 'createTime',
|
|
'lastactivetime': 'lastActiveTime',
|
|
'membershipcount': 'membershipCount.joined_direct_human_user_count'
|
|
}
|
|
|
|
# gam [<UserTypeEntity>] show chatspaces
|
|
# [types <ChatSpaceTypeList>]
|
|
# [fields <ChatSpaceFieldNameList>]
|
|
# [formatjson]
|
|
# gam [<UserTypeEntity>] print chatspaces [todrive <ToDriveAttribute>*]
|
|
# [types <ChatSpaceTypeList>]
|
|
# [fields <ChatSpaceFieldNameList>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserItem> show chatspaces asadmin
|
|
# [query <String>] [querytime<String> <Time>]
|
|
# [orderby <ChatSpaceAdminOrderByFieldName> [ascending|descending]]
|
|
# [fields <ChatSpaceFieldNameList>]
|
|
# [formatjson]
|
|
# gam <UserItem> print chatspaces asadmin [todrive <ToDriveAttribute>*]
|
|
# [query <String>] [querytime<String> <Time>]
|
|
# [orderby <ChatSpaceAdminOrderByFieldName> [ascending|descending]]
|
|
# [fields <ChatSpaceFieldNameList>]
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowChatSpaces(users):
|
|
csvPF = CSVPrintFile(['User', 'name'] if not isinstance(users, list) else ['name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
OBY = OrderBy(CHAT_SPACES_ADMIN_ORDERBY_CHOICE_MAP)
|
|
useAdminAccess, api, kwargsCS = _getChatAdminAccess(API.CHAT_SPACES_ADMIN, API.CHAT_SPACES)
|
|
fieldsList = []
|
|
queries = []
|
|
queryTimes = {}
|
|
pfilter = ''
|
|
if useAdminAccess:
|
|
function = 'search'
|
|
queries = ['customer = "customers/my_customer" AND spaceType = "SPACE"']
|
|
else:
|
|
function = 'list'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getFieldsList(myarg, CHAT_SPACES_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
|
|
pass
|
|
elif not useAdminAccess and _getChatSpaceListParms(myarg, kwargsCS):
|
|
pass
|
|
elif useAdminAccess and _getChatSpaceSearchParms(myarg, queries, queryTimes, OBY):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
fields = getItemFieldsFromFieldsList('spaces', fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
if useAdminAccess:
|
|
_chkChatAdminAccess(count)
|
|
kwargsCS['orderBy'] = OBY.orderBy
|
|
substituteQueryTimes(queries, queryTimes)
|
|
pfilter = kwargsCS['query'] = queries[0]
|
|
kwargsCS['useAdminAccess'] = True
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(api, user, i, count, None, useAdminAccess)
|
|
if not chat:
|
|
continue
|
|
try:
|
|
spaces = callGAPIpages(chat.spaces(), function, 'spaces',
|
|
pageMessage=_getChatPageMessage(Ent.CHAT_SPACE, user, i, count, pfilter, useAdminAccess),
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
|
|
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
fields=fields, pageSize=CHAT_PAGE_SIZE, **kwargsCS)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.internalError, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
continue
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(spaces)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems(kvList, jcount, Ent.CHAT_SPACE, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for space in spaces:
|
|
j += 1
|
|
_showChatItem(space, Ent.CHAT_SPACE, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for space in spaces:
|
|
_printChatItem(user, space, None, Ent.CHAT_SPACE, csvPF, FJQC)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chat Spaces')
|
|
|
|
def doPrintShowChatSpaces():
|
|
printShowChatSpaces([None])
|
|
|
|
def _getChatMemberEmail(cd, member):
|
|
if 'member' in member:
|
|
if member['member']['type'] == 'HUMAN':
|
|
_, memberUid = member['member']['name'].split('/')
|
|
member['member']['email'], _ = convertUIDtoEmailAddressWithType(f'uid:{memberUid}', cd, None, emailTypes=['user'])
|
|
elif 'groupMember' in member:
|
|
_, memberUid = member['groupMember']['name'].split('/')
|
|
member['groupMember']['email'], _ = convertUIDtoEmailAddressWithType(f'uid:{memberUid}', cd, None, emailTypes=['group'])
|
|
|
|
def normalizeUserMember(user, userList):
|
|
userList.append(normalizeEmailAddressOrUID(user))
|
|
|
|
def getUserMemberID(cd, user, userList):
|
|
userList.append(convertEmailAddressToUID(user, cd, emailType='user'))
|
|
|
|
def getGroupMemberID(cd, group, groupList):
|
|
groupList.append(convertEmailAddressToUID(group, cd, emailType='group'))
|
|
|
|
# gam <UserTypeEntity> create chatmember <ChatSpace>
|
|
# [type human|bot] [role member|manager]
|
|
# (user <UserItem>)* (members <UserTypeEntity>)*
|
|
# (group <GroupItem>)* (groups <GroupEntity>)*
|
|
# [formatjson|returnidonly]
|
|
# gam <UserItem> create chatmember asadmin <ChatSpace>
|
|
# [type human|bot] [role member|manager]
|
|
# (user <UserItem>)* (members <UserTypeEntity>)*
|
|
# (group <GroupItem>)* (groups <GroupEntity>)*
|
|
# [formatjson|returnidonly]
|
|
def createChatMember(users):
|
|
def addMembers(members, field, entityType, i, count):
|
|
jcount = len(members)
|
|
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
|
|
if jcount == 0:
|
|
return
|
|
kvList.extend([entityType, ''])
|
|
Ind.Increment()
|
|
j = 0
|
|
for body in members:
|
|
j += 1
|
|
kvList[-1] = body[field]['name']
|
|
try:
|
|
member = callGAPI(chat.spaces().members(), 'create',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
|
|
GAPI.INTERNAL_ERROR, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
parent=parent, body=body, **kwargsUAA)
|
|
if role != 'ROLE_MEMBER' and entityType == Ent.CHAT_MANAGER_USER:
|
|
member = callGAPI(chat.spaces().members(), 'patch',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
|
|
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=member['name'], updateMask='role', body={'role': role}, **kwargsUAA)
|
|
if not returnIdOnly:
|
|
kvList[-1] = member['name']
|
|
_getChatMemberEmail(cd, member)
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, j, jcount)
|
|
Ind.Increment()
|
|
_showChatItem(member, Ent.CHAT_MEMBER, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
writeStdout(f'{member["name"]}\n')
|
|
except (GAPI.alreadyExists, GAPI.notFound, GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning(kvList, str(e))
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
FJQC = FormatJSONQuoteChar()
|
|
parent = None
|
|
role = CHAT_MEMBER_ROLE_MAP['member']
|
|
mtype = CHAT_MEMBER_TYPE_MAP['human']
|
|
userList = []
|
|
groupList = []
|
|
returnIdOnly = False
|
|
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
|
|
parent = getSpaceName(myarg)
|
|
elif myarg == 'user':
|
|
normalizeUserMember(getEmailAddress(returnUIDprefix='uid:'), userList)
|
|
elif myarg in {'member', 'members'}:
|
|
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
for user in members:
|
|
normalizeUserMember(user, userList)
|
|
elif myarg == 'group':
|
|
getGroupMemberID(cd, getEmailAddress(returnUIDprefix='uid:'), groupList)
|
|
elif myarg == 'groups':
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
getGroupMemberID(cd, group, groupList)
|
|
elif myarg == 'role':
|
|
role = getChoice(CHAT_MEMBER_ROLE_MAP, mapChoice=True)
|
|
elif myarg == 'type':
|
|
mtype = getChoice(CHAT_MEMBER_TYPE_MAP, mapChoice=True)
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not parent:
|
|
missingArgumentExit('space')
|
|
if not userList and not groupList:
|
|
missingArgumentExit('user|members|group|groups')
|
|
userEntityType = Ent.CHAT_MEMBER_USER if role == 'ROLE_MEMBER' else Ent.CHAT_MANAGER_USER
|
|
userMembers = []
|
|
for user in userList:
|
|
userMembers.append({'member': {'name': f'users/{user}', 'type': mtype}})
|
|
groupMembers = []
|
|
for group in groupList:
|
|
groupMembers.append({'groupMember': {'name': f'groups/{group}'}})
|
|
i, count, users = getEntityArgument(users)
|
|
if useAdminAccess:
|
|
_chkChatAdminAccess(count)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, parent], useAdminAccess)
|
|
if not chat:
|
|
continue
|
|
Ind.Increment()
|
|
if userMembers:
|
|
addMembers(userMembers, 'member', userEntityType, i, count)
|
|
if groupMembers:
|
|
addMembers(groupMembers, 'groupMember', Ent.CHAT_MEMBER_GROUP, i, count)
|
|
Ind.Decrement()
|
|
|
|
def _deleteChatMembers(chat, kvList, jcount, memberNames, i, count, kwargsUAA):
|
|
j = 0
|
|
for name in memberNames:
|
|
j += 1
|
|
kvList[-1] = name
|
|
try:
|
|
callGAPI(chat.spaces().members(), 'delete',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
name=name, **kwargsUAA)
|
|
entityActionPerformed(kvList, j, jcount)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
except (GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
|
|
# gam <UserTypeEntity> delete chatmember <ChatSpace>
|
|
# ((user <UserItem>)|(members <UserTypeEntity>)|
|
|
# (group <GroupItem>)|(groups <GroupEntity>))+
|
|
# gam <UserItem> delete chatmember asadmin <ChatSpace>
|
|
# ((user <UserItem>)|(members <UserTypeEntity>)|
|
|
# (group <GroupItem>)|(groups <GroupEntity>))+
|
|
# gam <UserTypeEntity> remove chatmember
|
|
# members <ChatMemberList>
|
|
# gam <UserItem> remove chatmember asadmin
|
|
# members <ChatMemberList>
|
|
# gam <UserTypeEntity> update chatmember <ChatSpace>
|
|
# role member|manager
|
|
# ((user <UserItem>)|(members <UserTypeEntity>))+
|
|
# gam <UserTypeEntity> modify chatmember
|
|
# role member|manager
|
|
# members <ChatMemberList>
|
|
# gam <UserItem> update chatmember asadmin<ChatSpace>
|
|
# role member|manager
|
|
# ((user <UserItem>)|(members <UserTypeEntity>))+
|
|
# gam <UserItem> modify chatmember asadmin
|
|
# role member|manager
|
|
# members <ChatMemberList>
|
|
def deleteUpdateChatMember(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
FJQC = FormatJSONQuoteChar()
|
|
action = Act.Get()
|
|
deleteMode = action in {Act.DELETE, Act.REMOVE}
|
|
parent = None
|
|
body = {}
|
|
memberNames = []
|
|
userList = []
|
|
groupList = []
|
|
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if action in {Act.UPDATE, Act.MODIFY} and myarg == 'role':
|
|
body['role'] = getChoice(CHAT_MEMBER_ROLE_MAP, mapChoice=True)
|
|
continue
|
|
if action in {Act.REMOVE, Act.MODIFY}:
|
|
if myarg in {'member', 'members'}:
|
|
memberNames.extend(getString(Cmd.OB_CHAT_MEMBER).replace(',', ' ').split())
|
|
else:
|
|
unknownArgumentExit()
|
|
else: # {Act.DELETE, Act.UPDATE}
|
|
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
|
|
parent = getSpaceName(myarg)
|
|
elif myarg == 'user':
|
|
normalizeUserMember(getEmailAddress(returnUIDprefix='uid:'), userList)
|
|
elif myarg in {'member', 'members'}:
|
|
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
for user in members:
|
|
normalizeUserMember(user, userList)
|
|
elif deleteMode and myarg == 'group':
|
|
getGroupMemberID(cd, getEmailAddress(returnUIDprefix='uid:'), groupList)
|
|
elif deleteMode and myarg == 'groups':
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
getGroupMemberID(cd, group, groupList)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not deleteMode and 'role' not in body:
|
|
missingArgumentExit('role')
|
|
if action in {Act.REMOVE, Act.MODIFY}:
|
|
if not memberNames:
|
|
missingArgumentExit('members')
|
|
else: # {Act.DELETE, Act.UPDATE}
|
|
if not parent:
|
|
missingArgumentExit('space')
|
|
if not userList and not groupList:
|
|
missingArgumentExit('user|members|group|groups')
|
|
for user in userList:
|
|
memberNames.append(f'{parent}/members/{user}')
|
|
for group in groupList:
|
|
memberNames.append(f'{parent}/members/group-{group}')
|
|
i, count, users = getEntityArgument(users)
|
|
if useAdminAccess:
|
|
_chkChatAdminAccess(count)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, parent] if parent is not None else None, useAdminAccess)
|
|
if not chat:
|
|
continue
|
|
jcount = len(memberNames)
|
|
entityPerformActionNumItems(kvList, jcount, Ent.CHAT_MEMBER, i, count)
|
|
kvList.extend([Ent.CHAT_MEMBER, ''])
|
|
Ind.Increment()
|
|
if deleteMode:
|
|
_deleteChatMembers(chat, kvList, jcount, memberNames, i, count, kwargsUAA)
|
|
else:
|
|
j = 0
|
|
for name in memberNames:
|
|
j += 1
|
|
kvList[-1] = name
|
|
try:
|
|
member = callGAPI(chat.spaces().members(), 'patch',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
|
|
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name, updateMask='role', body=body, **kwargsUAA)
|
|
_getChatMemberEmail(cd, member)
|
|
Ind.Increment()
|
|
_showChatItem(member, Ent.CHAT_MEMBER, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
except (GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
Ind.Decrement()
|
|
|
|
CHAT_SYNC_PREVIEW_TITLES = ['space', 'member', 'role', 'action', 'message']
|
|
|
|
# gam <UserTypeEntity> sync chatmembers [asadmin] <ChatSpace>
|
|
# [role member|manager] [type human|bot]
|
|
# [addonly|removeonly]
|
|
# [preview [actioncsv]]
|
|
# (users <UserTypeEntity>)* (groups <GroupEntity>)*
|
|
def syncChatMembers(users):
|
|
def _previewAction(members, entityType, jcount, action):
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in members:
|
|
j += 1
|
|
entityActionPerformed([Ent.CHAT_SPACE, parent, entityType, member, Ent.ROLE, role], j, jcount)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
for member in members:
|
|
csvPF.WriteRow({'space': parent, 'member': member, 'role': role, 'action': Act.PerformedName(action), 'message': Act.PREVIEW})
|
|
|
|
def addMembers(memberNames, members, entityType, i, count):
|
|
jcount = len(memberNames)
|
|
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
|
|
if jcount == 0:
|
|
return
|
|
if preview:
|
|
_previewAction(memberNames, entityType, jcount, Act.REMOVE)
|
|
return
|
|
kvList.extend([entityType, ''])
|
|
Ind.Increment()
|
|
j = 0
|
|
for memberName in memberNames:
|
|
j += 1
|
|
body = members[memberName]
|
|
kvList[-1] = memberName
|
|
try:
|
|
callGAPI(chat.spaces().members(), 'create',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
parent=parent, body=body, **kwargsUAA)
|
|
if role != 'ROLE_MEMBER' and entityType == Ent.CHAT_MANAGER_USER:
|
|
callGAPI(chat.spaces().members(), 'patch',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
name=memberName, updateMask='role', body={'role': role}, **kwargsUAA)
|
|
entityActionPerformed(kvList, j, jcount)
|
|
except (GAPI.alreadyExists, GAPI.notFound, GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
Ind.Decrement()
|
|
del kvList[-2:]
|
|
|
|
def deleteMembers(memberNames, entityType, i, count):
|
|
jcount = len(memberNames)
|
|
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
|
|
if jcount == 0:
|
|
return
|
|
if preview:
|
|
_previewAction(memberNames, entityType, jcount, Act.ADD)
|
|
return
|
|
kvList.extend([entityType, ''])
|
|
Ind.Increment()
|
|
_deleteChatMembers(chat, kvList, jcount, memberNames, i, count, kwargsUAA)
|
|
Ind.Decrement()
|
|
del kvList[-2:]
|
|
|
|
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
parent = None
|
|
role = CHAT_MEMBER_ROLE_MAP['member']
|
|
mtype = CHAT_MEMBER_TYPE_MAP['human']
|
|
syncOperation = 'addremove'
|
|
kwargs = {}
|
|
preview = False
|
|
csvPF = None
|
|
userList = []
|
|
usersSpecified = False
|
|
groupList = []
|
|
groupsSpecified = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
|
|
parent = getSpaceName(myarg)
|
|
elif myarg == 'role':
|
|
role = getChoice(CHAT_MEMBER_ROLE_MAP, mapChoice=True)
|
|
elif myarg == 'type':
|
|
mtype = getChoice(CHAT_MEMBER_TYPE_MAP, mapChoice=True)
|
|
elif myarg in {'addonly', 'removeonly'}:
|
|
syncOperation = myarg
|
|
elif myarg == 'preview':
|
|
preview = True
|
|
elif myarg == 'actioncsv':
|
|
csvPF = CSVPrintFile(CHAT_SYNC_PREVIEW_TITLES)
|
|
elif myarg == 'users':
|
|
for user in getEntityList(Cmd.OB_USER_ENTITY):
|
|
getUserMemberID(cd, user, userList)
|
|
usersSpecified = True
|
|
elif myarg in {'member', 'members'}:
|
|
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
for user in members:
|
|
getUserMemberID(cd, user, userList)
|
|
usersSpecified = True
|
|
elif myarg == 'groups':
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
getGroupMemberID(cd, group, groupList)
|
|
groupsSpecified = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not parent:
|
|
missingArgumentExit('space')
|
|
userEntityType = Ent.CHAT_MEMBER_USER if role == 'ROLE_MEMBER' else Ent.CHAT_MANAGER_USER
|
|
userMembers = {}
|
|
syncUsersSet = set()
|
|
for user in userList:
|
|
memberName = f'{parent}/members/{user}'
|
|
userMembers[memberName] = {'member': {'name': f'users/{user}', 'type': mtype}}
|
|
syncUsersSet.add(memberName)
|
|
groupMembers = {}
|
|
syncGroupsSet = set()
|
|
for group in groupList:
|
|
memberName = f'{parent}/members/group-{group}'
|
|
groupMembers[memberName] = {'groupMember': {'name': f'groups/{group}'}}
|
|
syncGroupsSet.add(memberName)
|
|
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parent}'
|
|
i, count, users = getEntityArgument(users)
|
|
if useAdminAccess:
|
|
kwargs['filter'] = 'member.type != "BOT"'
|
|
_chkChatAdminAccess(count)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, parent], useAdminAccess)
|
|
if not chat:
|
|
continue
|
|
currentUsersSet = set()
|
|
currentGroupsSet = set()
|
|
try:
|
|
members = callGAPIpages(chat.spaces().members(), 'list', 'memberships',
|
|
pageMessage=_getChatPageMessage(Ent.CHAT_MEMBER, user, i, count, qfilter),
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
parent=parent, showGroups=groupsSpecified, pageSize=CHAT_PAGE_SIZE, **kwargs, **kwargsUAA)
|
|
for member in members:
|
|
if 'member' in member:
|
|
if member['member']['type'] == mtype and member['role'] == role:
|
|
currentUsersSet.add(member['name'])
|
|
elif 'groupMember' in member:
|
|
currentGroupsSet.add(member['name'])
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
continue
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
if syncOperation != 'addonly':
|
|
Act.Set([Act.REMOVE, Act.REMOVE_PREVIEW][preview])
|
|
if usersSpecified:
|
|
deleteMembers(currentUsersSet-syncUsersSet, userEntityType, i, count)
|
|
if groupsSpecified:
|
|
deleteMembers(currentGroupsSet-syncGroupsSet, Ent.CHAT_MEMBER_GROUP, i, count)
|
|
if syncOperation != 'removeonly':
|
|
Act.Set([Act.ADD, Act.ADD_PREVIEW][preview])
|
|
if usersSpecified:
|
|
addMembers(syncUsersSet-currentUsersSet, userMembers, userEntityType, i, count)
|
|
if groupsSpecified:
|
|
addMembers(syncGroupsSet-currentGroupsSet, groupMembers, Ent.CHAT_MEMBER_GROUP, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chat Member Updates')
|
|
|
|
CHAT_MEMBERS_FIELDS_CHOICE_MAP = {
|
|
"createtime": "createTime",
|
|
"deletetime": "deleteTime",
|
|
"groupmember": "groupMember",
|
|
"member": "member",
|
|
"name": "name",
|
|
"role": "role",
|
|
"state": "state",
|
|
}
|
|
|
|
# gam [<UserTypeEntity>] info chatmember members <ChatMemberList>
|
|
# [fields <ChatMemberFieldNameList>]
|
|
# [formatjson]
|
|
# gam <UserItem> info chatmember asadmin members <ChatMemberList>
|
|
# [fields <ChatMemberFieldNameList>]
|
|
# [formatjson]
|
|
def infoChatMember(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
FJQC = FormatJSONQuoteChar()
|
|
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
|
|
fieldsList = []
|
|
memberNames = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'member', 'members'}:
|
|
memberNames.extend(getString(Cmd.OB_CHAT_MEMBER).replace(',', ' ').split())
|
|
elif getFieldsList(myarg, CHAT_MEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not memberNames:
|
|
missingArgumentExit('members')
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
if useAdminAccess:
|
|
_chkChatAdminAccess(count)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(api, user, i, count, None, useAdminAccess)
|
|
if not chat:
|
|
continue
|
|
jcount = len(memberNames)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems(kvList, jcount, Ent.CHAT_MEMBER, i, count)
|
|
kvList.extend([Ent.CHAT_MEMBER, ''])
|
|
j = 0
|
|
for name in memberNames:
|
|
j += 1
|
|
kvList[-1] = name
|
|
try:
|
|
member = callGAPI(chat.spaces().members(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
|
|
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name, fields=fields, **kwargsUAA)
|
|
_getChatMemberEmail(cd, member)
|
|
Ind.Increment()
|
|
_showChatItem(member, Ent.CHAT_MEMBER, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
|
|
def doInfoChatMember():
|
|
infoChatMember([None])
|
|
|
|
# gam [<UserTypeEntity>] show chatmembers
|
|
# <ChatSpace>* [types <ChatSpaceTypeList>]
|
|
# [showinvited [<Boolean>]] [showgroups [<Boolean>]] [filter <String>]
|
|
# [fields <ChatMemberFieldNameList>]
|
|
# [formatjson]
|
|
# gam [<UserTypeEntity>] print chatmembers [todrive <ToDriveAttribute>*]
|
|
# <ChatSpace>* [types <ChatSpaceTypeList>]
|
|
# [showinvited [<Boolean>]] [showgroups [<Boolean>]] [filter <String>]
|
|
# [fields <ChatMemberFieldNameList>]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserItem> show chatmembers asadmin
|
|
# <ChatSpace>* [query <String>] [querytime<String> <Time>]
|
|
# [orderby <ChatSpaceAdminOrderByFieldName> [ascending|descending]]
|
|
# [showinvited [<Boolean>]] [showgroups [<Boolean>]] [filter <String>]
|
|
# [fields <ChatMemberFieldNameList>]
|
|
# [formatjson]
|
|
# gam <UserItem> print chatmembers asadmin [todrive <ToDriveAttribute>*]
|
|
# <ChatSpace>* [query <String>] [querytime<String> <Time>]
|
|
# [orderby <ChatSpaceAdminOrderByFieldName> [ascending|descending]]
|
|
# [showinvited [<Boolean>]] [showgroups [<Boolean>]] [filter <String>]
|
|
# [fields <ChatMemberFieldNameList>]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowChatMembers(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['User', 'space.name', 'space.displayName', 'name'] if not isinstance(users, list) else ['space.name', 'space.displayName', 'name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
OBY = OrderBy(CHAT_SPACES_ADMIN_ORDERBY_CHOICE_MAP)
|
|
useAdminAccess, api, kwargsUAA = _getChatAdminAccess(API.CHAT_MEMBERSHIPS_ADMIN, API.CHAT_MEMBERSHIPS)
|
|
if useAdminAccess:
|
|
queries = ['customer = "customers/my_customer" AND spaceType = "SPACE"']
|
|
queryTimes = {}
|
|
fieldsList = []
|
|
pfilter = ''
|
|
kwargs = {}
|
|
kwargsCS = {}
|
|
parentList = []
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
|
|
parentList.append({'name': getSpaceName(myarg), 'displayName': ''})
|
|
elif getFieldsList(myarg, CHAT_MEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
|
|
pass
|
|
elif myarg == 'showinvited':
|
|
kwargs['showInvited'] = getBoolean()
|
|
elif myarg == 'showgroups':
|
|
kwargs['showGroups'] = getBoolean()
|
|
elif myarg =='filter':
|
|
pfilter = getString(Cmd.OB_STRING)
|
|
elif not useAdminAccess and _getChatSpaceListParms(myarg, kwargsCS):
|
|
pass
|
|
elif useAdminAccess and _getChatSpaceSearchParms(myarg, queries, queryTimes, OBY):
|
|
pass
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if useAdminAccess:
|
|
if not parentList:
|
|
kwargsCS['orderBy'] = OBY.orderBy
|
|
substituteQueryTimes(queries, queryTimes)
|
|
kwargsCS['query'] = queries[0]
|
|
kwargsCS['useAdminAccess'] = True
|
|
else:
|
|
if not parentList and not kwargsCS:
|
|
kwargsCS['filter'] = 'spaceType = "SPACE" OR spaceType = "GROUP_CHAT" OR spaceType = "DIRECT_MESSAGE"'
|
|
if pfilter:
|
|
if 'member.type' not in pfilter:
|
|
kwargs['filter'] = 'member.type != "BOT" AND '+pfilter
|
|
else:
|
|
kwargs['filter'] = pfilter
|
|
else:
|
|
kwargs['filter'] = 'member.type != "BOT"'
|
|
fields = getItemFieldsFromFieldsList('memberships', fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
if useAdminAccess:
|
|
_chkChatAdminAccess(count)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(api, user, i, count, [Ent.CHAT_SPACE, None], useAdminAccess)
|
|
if not chat:
|
|
continue
|
|
if useAdminAccess:
|
|
_, chatsp, _ = buildChatServiceObject(API.CHAT_SPACES_ADMIN, user, i, count, None, useAdminAccess)
|
|
else:
|
|
_, chatsp, _ = buildChatServiceObject(API.CHAT_SPACES, user, i, count, None, useAdminAccess)
|
|
if not chatsp:
|
|
continue
|
|
if kwargsCS:
|
|
if useAdminAccess:
|
|
try:
|
|
spaces = callGAPIpages(chatsp.spaces(), 'search', 'spaces',
|
|
pageMessage=_getChatPageMessage(Ent.CHAT_SPACE, user, i, count, queries[0], True),
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
|
|
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
fields="nextPageToken,spaces(name,displayName,spaceType,membershipCount)", pageSize=CHAT_PAGE_SIZE,
|
|
**kwargsCS)
|
|
for space in spaces:
|
|
if space['spaceType'] == 'SPACE' and 'membershipCount' in space:
|
|
parentList.append({'name': space['name'], 'displayName': space.get('displayName', 'None')})
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.internalError,
|
|
GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
continue
|
|
else:
|
|
try:
|
|
spaces = callGAPIpages(chatsp.spaces(), 'list', 'spaces',
|
|
pageMessage=_getChatPageMessage(Ent.CHAT_SPACE, user, i, count, kwargsCS['filter']),
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR,
|
|
GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
fields="nextPageToken,spaces(name,displayName,spaceType,membershipCount)", pageSize=CHAT_PAGE_SIZE,
|
|
**kwargsCS)
|
|
for space in spaces:
|
|
# if 'membershipCount' in space:
|
|
# parentList.append({'name': space['name'], 'displayName': space.get('displayName', 'None')})
|
|
parentList.append({'name': space['name'], 'displayName': space.get('displayName', 'None')})
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.internalError, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
continue
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(parentList)
|
|
j = 0
|
|
for parent in parentList:
|
|
j += 1
|
|
parentName = parent['name']
|
|
kvList[-1] = parentName
|
|
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parentName}'
|
|
if 'filter' in kwargs:
|
|
qfilter += f', {kwargs["filter"]}'
|
|
try:
|
|
if not parent['displayName']:
|
|
space = callGAPI(chatsp.spaces(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=parentName, fields='displayName', **kwargsUAA)
|
|
parent['displayName'] = space.get('displayName', 'None')
|
|
members = callGAPIpages(chat.spaces().members(), 'list', 'memberships',
|
|
pageMessage=_getChatPageMessage(Ent.CHAT_MEMBER, user, j, jcount, qfilter),
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
parent=parentName, fields=fields, pageSize=CHAT_PAGE_SIZE, **kwargs, **kwargsUAA)
|
|
for member in members:
|
|
_getChatMemberEmail(cd, member)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), j, jcount)
|
|
continue
|
|
if not csvPF:
|
|
kcount = len(members)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems(kvList, kcount, Ent.CHAT_MEMBER, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for member in members:
|
|
k += 1
|
|
member['space'] = {'name': parentName, 'displayName': parent['displayName']}
|
|
_showChatItem(member, Ent.CHAT_MEMBER, FJQC, k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for member in members:
|
|
_printChatItem(user, member, parent, Ent.CHAT_MEMBER, csvPF, FJQC, addCSVData)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chat Members')
|
|
|
|
def doPrintShowChatMembers():
|
|
printShowChatMembers([None])
|
|
|
|
def _getChatSenderEmail(cd, sender):
|
|
if sender['type'] == 'HUMAN':
|
|
_, senderUid = sender['name'].split('/')
|
|
sender['email'], _ = convertUIDtoEmailAddressWithType(f'uid:{senderUid}', cd, None, emailTypes=['user'])
|
|
|
|
def trimChatMessageIfRequired(body):
|
|
msgLen = len(body['text'])
|
|
if msgLen > 4096:
|
|
stderrWarningMsg(Msg.TRIMMED_MESSAGE_FROM_LENGTH_TO_MAXIMUM.format(msgLen, 4096))
|
|
body['text'] = body['text'][:4095]
|
|
|
|
CHAT_MESSAGE_REPLY_OPTION_MAP = {
|
|
'fail': 'REPLY_MESSAGE_OR_FAIL',
|
|
'fallbacktonew': 'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD',
|
|
}
|
|
|
|
# gam [<UserTypeEntityu>] create chatmessage <ChatSpace>
|
|
# <ChatContent>
|
|
# [messageId <ChatMessageID>]
|
|
# [(thread <ChatThread>)|(threadkey <String>) [replyoption fail|fallbacktonew]]
|
|
# [returnidonly]
|
|
def createChatMessage(users):
|
|
messageId = messageReplyOption = parent = None
|
|
body = {}
|
|
returnIdOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
|
|
parent = getSpaceName(myarg)
|
|
elif myarg == 'thread':
|
|
body.setdefault('thread', {})
|
|
body['thread']['name'] = getString(Cmd.OB_CHAT_THREAD)
|
|
Act.Set(Act.RESPOND)
|
|
elif myarg == 'threadkey':
|
|
body.setdefault('thread', {})
|
|
body['thread']['threadKey'] = getString(Cmd.OB_STRING)
|
|
Act.Set(Act.RESPOND)
|
|
elif myarg == 'replyoption':
|
|
messageReplyOption = getChoice(CHAT_MESSAGE_REPLY_OPTION_MAP, mapChoice=True)
|
|
elif myarg in SORF_TEXT_ARGUMENTS:
|
|
body['text'] = getStringOrFile(myarg, minLen=0, unescapeCRLF=True)[0]
|
|
elif myarg == 'messageid':
|
|
messageId = getString(Cmd.OB_CHAT_MESSAGE_ID, minLen=1, maxLen=63)
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not parent:
|
|
missingArgumentExit('space')
|
|
if 'text' not in body:
|
|
missingArgumentExit('text or textfile')
|
|
if 'thread' in body and messageReplyOption is None:
|
|
messageReplyOption = CHAT_MESSAGE_REPLY_OPTION_MAP['fail']
|
|
trimChatMessageIfRequired(body)
|
|
action = Act.Get()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_SPACE, parent])
|
|
if not chat:
|
|
continue
|
|
try:
|
|
resp = callGAPI(chat.spaces().messages(), 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
parent=parent, requestId=str(uuid.uuid4()),
|
|
messageReplyOption=messageReplyOption, messageId=messageId, body=body)
|
|
if not returnIdOnly:
|
|
kvList.extend([Ent.CHAT_MESSAGE, resp['name']])
|
|
if 'clientAssignedMessageId' in resp:
|
|
kvList.extend([Ent.CHAT_MESSAGE_ID, resp['clientAssignedMessageId']])
|
|
kvList.extend([Ent.CHAT_THREAD, resp['thread']['name']])
|
|
if (action == Act.RESPOND) and not resp.get('threadReply', False):
|
|
Act.Set(Act.CREATE)
|
|
entityActionPerformed(kvList, i, count)
|
|
Act.Set(action)
|
|
else:
|
|
writeStdout(f'{resp["name"]}\n')
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
|
|
def doCreateChatMessage():
|
|
createChatMessage([None])
|
|
|
|
# gam [<UserTypeMessage>] update chatmessage name <ChatMessage>
|
|
# <ChatContent>
|
|
def updateChatMessage(users):
|
|
name = None
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
name = getString(Cmd.OB_CHAT_MESSAGE)
|
|
elif myarg in SORF_TEXT_ARGUMENTS:
|
|
body['text'] = getStringOrFile(myarg, minLen=0, unescapeCRLF=True)[0]
|
|
else:
|
|
unknownArgumentExit()
|
|
if not name:
|
|
missingArgumentExit('name')
|
|
if 'text' not in body:
|
|
missingArgumentExit('text or textfile')
|
|
trimChatMessageIfRequired(body)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_MESSAGE, name])
|
|
if not chat:
|
|
continue
|
|
try:
|
|
resp = callGAPI(chat.spaces().messages(), 'patch',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name, updateMask='text', body=body)
|
|
kvList.extend([Ent.CHAT_THREAD, resp['thread']['name']])
|
|
entityActionPerformed(kvList, i, count)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
|
|
def doUpdateChatMessage():
|
|
updateChatMessage([None])
|
|
|
|
# gam [<UserTypeEntity>] delete chatmessage name <ChatMessage>
|
|
def deleteChatMessage(users):
|
|
name = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
name = getString(Cmd.OB_CHAT_MESSAGE)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not name:
|
|
missingArgumentExit('name')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_MESSAGE, name])
|
|
if not chat:
|
|
continue
|
|
try:
|
|
callGAPI(chat.spaces().messages(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name)
|
|
entityActionPerformed(kvList, i, count)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
|
|
def doDeleteChatMessage():
|
|
deleteChatMessage([None])
|
|
|
|
CHAT_MESSAGES_FIELDS_CHOICE_MAP = {
|
|
"accessorywidgets": "accessoryWidgets",
|
|
"actionresponse": "actionResponse",
|
|
"annotations": "annotations",
|
|
"argumenttext": "argumentText",
|
|
"attachedgifs": "attachedGifs",
|
|
"attachment": "attachment",
|
|
"cards": "cards",
|
|
"cardsv2": "cardsV2",
|
|
"clientassignedmessageid": "clientAssignedMessageId",
|
|
"createtime": "createTime",
|
|
"deletetime": "deleteTime",
|
|
"deletionmetadata": "deletionMetadata",
|
|
"emojireactionsummaries": "emojiReactionSummaries",
|
|
"fallbacktext": "fallbackText",
|
|
"formattedtext": "formattedText",
|
|
"lastupdatetime": "lastUpdateTime",
|
|
"matchedurl": "matchedUrl",
|
|
"name": "name",
|
|
"privatemessageviewer": "privateMessageViewer",
|
|
"quotedmessagemetadata": "quotedMessageMetadata",
|
|
"sender": "sender",
|
|
"slashcommand": "slashCommand",
|
|
"space": "space",
|
|
"text": "text",
|
|
"thread": "thread",
|
|
"threadreply": "threadReply",
|
|
}
|
|
|
|
# gam [<UserTypeEntity>] info chatmessage name <ChatMessage>
|
|
# [fields <ChatMessageFieldNameList>]
|
|
# [formatjson]
|
|
def infoChatMessage(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
FJQC = FormatJSONQuoteChar()
|
|
fieldsList = []
|
|
name = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
name = getString(Cmd.OB_CHAT_MESSAGE)
|
|
elif getFieldsList(myarg, CHAT_MESSAGES_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not name:
|
|
missingArgumentExit('name')
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_MESSAGE, name])
|
|
if not chat:
|
|
continue
|
|
try:
|
|
message = callGAPI(chat.spaces().messages(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name, fields=fields)
|
|
_getChatSenderEmail(cd, message['sender'])
|
|
if not FJQC.formatJSON:
|
|
entityPerformAction(kvList, i, count)
|
|
Ind.Increment()
|
|
_showChatItem(message, Ent.CHAT_MESSAGE, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
|
|
def doInfoChatMessage():
|
|
infoChatMessage([None])
|
|
|
|
# gam <UserTypeEntity> show chatmessages
|
|
# <ChatSpace>+
|
|
# [showdeleted [<Boolean>]] [filter <String>]
|
|
# [fields <ChatMessageFieldNameList>]
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> print chatmessages [todrive <ToDriveAttribute>*]
|
|
# <ChatSpace>+
|
|
# [showdeleted [<Boolean>]] [filter <String>]
|
|
# [fields <ChatMessageFieldNameList>]
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowChatMessages(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['User', 'space.name', 'space.displayName', 'name'] if not isinstance(users, list) else ['space.name', 'space.displayName', 'name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
pfilter = None
|
|
parentList = []
|
|
showDeleted = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
|
|
parentList.append({'name': getSpaceName(myarg), 'displayName': ''})
|
|
elif getFieldsList(myarg, CHAT_MESSAGES_FIELDS_CHOICE_MAP, fieldsList, initialField='name', onlyFieldsArg=True):
|
|
pass
|
|
elif myarg == 'showdeleted':
|
|
showDeleted = getBoolean()
|
|
elif myarg =='filter':
|
|
pfilter = getString(Cmd.OB_STRING)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if not parentList:
|
|
missingArgumentExit('space')
|
|
fields = getItemFieldsFromFieldsList('messages', fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(API.CHAT_MESSAGES, user, i, count, [Ent.CHAT_SPACE, None])
|
|
if not chat:
|
|
continue
|
|
_, chatspg, _ = buildChatServiceObject(API.CHAT_SPACES, user, i, count, None)
|
|
if not chatspg:
|
|
continue
|
|
jcount = len(parentList)
|
|
j = 0
|
|
for parent in parentList:
|
|
j += 1
|
|
parentName = parent['name']
|
|
kvList[-1] = parentName
|
|
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parentName}'
|
|
if pfilter:
|
|
qfilter += f', {pfilter}'
|
|
try:
|
|
if not parent['displayName']:
|
|
space = callGAPI(chatspg.spaces(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=parentName, fields='displayName')
|
|
parent['displayName'] = space.get('displayName', 'None')
|
|
messages = callGAPIpages(chat.spaces().messages(), 'list', 'messages',
|
|
pageMessage=_getChatPageMessage(Ent.CHAT_MESSAGE, user, i, count, qfilter),
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageSize=CHAT_PAGE_SIZE, parent=parentName, filter=pfilter, showDeleted=showDeleted,
|
|
fields=fields)
|
|
for message in messages:
|
|
if 'sender' in message:
|
|
_getChatSenderEmail(cd, message['sender'])
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
continue
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
if not csvPF:
|
|
kcount = len(messages)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems(kvList, kcount, Ent.CHAT_MESSAGE, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for message in messages:
|
|
k += 1
|
|
if 'space' in message:
|
|
message['space']['displayName'] = parent['displayName']
|
|
_showChatItem(message, Ent.CHAT_MESSAGE, FJQC, k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for message in messages:
|
|
_printChatItem(user, message, parent, Ent.CHAT_MESSAGE, csvPF, FJQC)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chat Messages')
|
|
|
|
# gam <UserTypeEntity> info chatevent name <ChatEvent>
|
|
# [formatjson]
|
|
def infoChatEvent(users):
|
|
FJQC = FormatJSONQuoteChar()
|
|
name = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
name = getString(Cmd.OB_CHAT_EVENT)
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not name:
|
|
missingArgumentExit('name')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(API.CHAT_EVENTS, user, i, count, [Ent.CHAT_EVENT, name])
|
|
if not chat:
|
|
continue
|
|
try:
|
|
event = callGAPI(chat.spaces().spaceEvents(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=name)
|
|
if not FJQC.formatJSON:
|
|
entityPerformAction(kvList, i, count)
|
|
Ind.Increment()
|
|
_showChatItem(event, Ent.CHAT_EVENT, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
|
|
def doInfoChatEvent():
|
|
infoChatEvent([None])
|
|
|
|
# gam <UserTypeEntity> show chatevents
|
|
# <ChatSpace>+
|
|
# filter <String>
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> print chatevents [todrive <ToDriveAttribute>*]
|
|
# <ChatSpace>+
|
|
# filter <String>
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowChatEvents(users):
|
|
csvPF = CSVPrintFile(['User', 'space.name', 'space.displayName', 'name'] if not isinstance(users, list) else ['space.name', 'space.displayName', 'name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
pfilter = None
|
|
parentList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/'):
|
|
parentList.append({'name': getSpaceName(myarg), 'displayName': ''})
|
|
elif myarg =='filter':
|
|
pfilter = getString(Cmd.OB_STRING)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if not parentList:
|
|
missingArgumentExit('space')
|
|
if not pfilter:
|
|
missingArgumentExit('filter')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, chat, kvList = buildChatServiceObject(API.CHAT_EVENTS, user, i, count, [Ent.CHAT_SPACE, None])
|
|
if not chat:
|
|
continue
|
|
_, chatspg, _ = buildChatServiceObject(API.CHAT_SPACES, user, i, count, None)
|
|
if not chatspg:
|
|
continue
|
|
jcount = len(parentList)
|
|
j = 0
|
|
for parent in parentList:
|
|
j += 1
|
|
parentName = parent['name']
|
|
kvList[-1] = parentName
|
|
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parentName}, {pfilter}'
|
|
try:
|
|
if not parent['displayName']:
|
|
space = callGAPI(chatspg.spaces(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
name=parentName, fields='displayName')
|
|
parent['displayName'] = space.get('displayName', 'None')
|
|
events = callGAPIpages(chat.spaces().spaceEvents(), 'list', 'spaceEvents',
|
|
pageMessage=_getChatPageMessage(Ent.CHAT_EVENT, user, i, count, qfilter),
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageSize=CHAT_PAGE_SIZE, parent=parentName, filter=pfilter)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
|
|
continue
|
|
except GAPI.failedPrecondition:
|
|
userChatServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
if not csvPF:
|
|
kcount = len(events)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems(kvList, kcount, Ent.CHAT_EVENT, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for event in events:
|
|
k += 1
|
|
event['space'] = {'name': parentName, 'displayName': parent['displayName']}
|
|
_showChatItem(event, Ent.CHAT_EVENT, FJQC, k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for event in events:
|
|
_printChatItem(user, event, parent, Ent.CHAT_EVENT, csvPF, FJQC)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chat Events')
|
|
|
|
def buildMeetServiceObject(api=API.MEET, user=None, i=0, count=0, entityTypeList=None):
|
|
if GC.Values[GC.MEET_V2_BETA]:
|
|
api = API.MEET_BETA
|
|
user, meet = buildGAPIServiceObject(api, user, i, count)
|
|
kvList = [Ent.USER, user]
|
|
if entityTypeList is not None:
|
|
kvList.extend(entityTypeList)
|
|
return user, meet, kvList
|
|
|
|
def _showMeetItem(mitem, FJQC=None, i=0, count=0):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(mitem), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.MEET_SPACE, mitem['name']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, mitem)
|
|
Ind.Decrement()
|
|
|
|
MEET_SPACE_OPTIONS_MAP = {
|
|
'accesstype': 'accessType',
|
|
'entrypointaccess': 'entryPointAccess',
|
|
'moderation': 'moderation',
|
|
'chatrestriction': 'chatRestriction',
|
|
'reactionrestriction': 'reactionRestriction',
|
|
'presentrestriction': 'presentRestriction',
|
|
'defaultjoinasviewer': 'defaultJoinAsViewerType',
|
|
'firstjoiner': 'firstJoinerType',
|
|
'autorecording': 'recordingConfig',
|
|
'autosmartnotes': 'smartNotesConfig',
|
|
'autotranscription': 'transcriptionConfig',
|
|
}
|
|
|
|
MEET_SPACE_ACCESSTYPE_CHOICES = {'open', 'trusted', 'restricted'}
|
|
MEET_SPACE_ENTRYPOINTACCESS_CHOICES_MAP = {
|
|
'all': 'ALL',
|
|
'creatorapponly': 'CREATOR_APP_ONLY'
|
|
}
|
|
|
|
MEET_SPACE_RESTRICTIONS_CHOICES_MAP = {
|
|
'hostsonly': 'HOSTS_ONLY',
|
|
'norestriction': 'NO_RESTRICTION'
|
|
}
|
|
|
|
#MEET_SPACE_FIRSTJOINERTYPE_CHOICES_MAP = {
|
|
# 'hostsonly': 'HOSTS_ONLY',
|
|
# 'anyone': 'ANYONE'
|
|
# }
|
|
|
|
MEET_SPACE_ARTIFACT_SUB_OPTIONS = {
|
|
'recordingConfig': 'autoRecordingGeneration',
|
|
'smartNotesConfig': 'autoSmartNotesGeneration',
|
|
'transcriptionConfig': 'autoTranscriptionGeneration'
|
|
}
|
|
|
|
# [accesstype open|trusted|restricted]
|
|
# [entrypointaccess all|creatorapponly]
|
|
# [moderation <Boolean>]
|
|
# [chatrestriction hostsonly|norestriction]
|
|
# [reactionrestriction hostsonly|norestriction]
|
|
# [presentrestriction hostsonly|norestriction]
|
|
# [defaultjoinasviewer <Boolean>]
|
|
# [firstjoiner hostsonly|anyone]
|
|
# [autorecording <Boolean>]
|
|
# [autosmartnotes <Boolean>]
|
|
# [autotranscription <Boolean>]
|
|
def _getMeetSpaceParameters(myarg, body):
|
|
option = MEET_SPACE_OPTIONS_MAP.get(myarg, None)
|
|
if option is None:
|
|
return False
|
|
if option == 'accessType':
|
|
body['config'][option] = getChoice(MEET_SPACE_ACCESSTYPE_CHOICES).upper()
|
|
elif option == 'entryPointAccess':
|
|
body['config'][option] = getChoice(MEET_SPACE_ENTRYPOINTACCESS_CHOICES_MAP, mapChoice=True)
|
|
elif option == 'moderation':
|
|
body['config'][option] = 'ON' if getBoolean() else 'OFF'
|
|
elif option in {'chatRestriction', 'reactionRestriction', 'presentRestriction'}:
|
|
body['config'].setdefault('moderationRestrictions', {})
|
|
body['config']['moderationRestrictions'][option] = getChoice(MEET_SPACE_RESTRICTIONS_CHOICES_MAP, mapChoice=True)
|
|
elif option == 'defaultJoinAsViewerType':
|
|
body['config'][option] = 'ON' if getBoolean() else 'OFF'
|
|
# elif option == 'firstJoinerType':
|
|
# body['config'][option] = getChoice(MEET_SPACE_FIRSTJOINERTYPE_CHOICES_MAP, mapChoice=True)
|
|
elif option in {'recordingConfig', 'transcriptionConfig', 'smartNotesConfig'}:
|
|
body['config'].setdefault('artifactConfig', {})
|
|
body['config']['artifactConfig'].setdefault(option, {})
|
|
body['config']['artifactConfig'][option][MEET_SPACE_ARTIFACT_SUB_OPTIONS[option]] = 'ON' if getBoolean() else 'OFF'
|
|
return True
|
|
|
|
# gam <UserTypeEntity> create meetspace
|
|
# <MeetSpaceOptions>*
|
|
# [formatjson|returnidonly]
|
|
def createMeetSpace(users):
|
|
FJQC = FormatJSONQuoteChar()
|
|
body = {'config': {'accessType': 'TRUSTED',
|
|
'entryPointAccess': 'ALL',
|
|
# 'moderation': 'OFF',
|
|
# 'moderationRestrictions': {},
|
|
# 'firstJoinerType': 'ANYONE',
|
|
}}
|
|
returnIdOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getMeetSpaceParameters(myarg, body):
|
|
pass
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_SPACE, None])
|
|
if not meet:
|
|
continue
|
|
try:
|
|
space = callGAPI(meet.spaces(), 'create',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
body=body)
|
|
if not returnIdOnly:
|
|
kvList[-1] = space['name']
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, i, count)
|
|
Ind.Increment()
|
|
_showMeetItem(space, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
writeStdout(f'{space["name"]}\n')
|
|
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit([Ent.MEET_SPACE, None], str(e))
|
|
|
|
# gam <UserTypeEntity> update meetspace <MeetSpaceName>
|
|
# <MeetSpaceOptions>*
|
|
# [formatjson]
|
|
def updateMeetSpace(users):
|
|
FJQC = FormatJSONQuoteChar()
|
|
name = None
|
|
body = {'config': {}}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
|
|
name = getSpaceName(myarg)
|
|
elif _getMeetSpaceParameters(myarg, body):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not name:
|
|
missingArgumentExit('space')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_SPACE, name])
|
|
if not meet:
|
|
continue
|
|
try:
|
|
space = callGAPI(meet.spaces(), 'patch',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name, updateMask='', body=body)
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, i, count)
|
|
Ind.Increment()
|
|
_showMeetItem(space, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit([Ent.MEET_SPACE, name], str(e))
|
|
|
|
# gam <UserTypeEntity> info meetspace <MeetSpaceName>
|
|
# [formatjson]
|
|
def infoMeetSpace(users):
|
|
FJQC = FormatJSONQuoteChar()
|
|
name = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
|
|
name = getSpaceName(myarg)
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not name:
|
|
missingArgumentExit('space')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_SPACE, name])
|
|
if not meet:
|
|
continue
|
|
try:
|
|
space = callGAPI(meet.spaces(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name)
|
|
if not FJQC.formatJSON:
|
|
entityPerformAction(kvList, i, count)
|
|
Ind.Increment()
|
|
_showMeetItem(space, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit(kvList, str(e))
|
|
|
|
# gam <UserTypeEntity> end meetconference <MeetSpaceName>
|
|
def endMeetConference(users):
|
|
name = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
|
|
name = getSpaceName(myarg)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not name:
|
|
missingArgumentExit('space')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_SPACE, name, Ent.MEET_CONFERENCE, None])
|
|
if not meet:
|
|
continue
|
|
try:
|
|
callGAPI(meet.spaces(), 'endActiveConference',
|
|
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name)
|
|
entityActionPerformed(kvList, i,count)
|
|
except (GAPI.failedPrecondition, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit([Ent.MEET_CONFERENCE, name], str(e))
|
|
|
|
def _getMeetPageMessage(entityType, user, i, count, pfilter):
|
|
printGettingAllEntityItemsForWhom(entityType, user, i, count, pfilter)
|
|
return getPageMessageForWhom()
|
|
|
|
MEET_CONFERENCE_TIME_OBJECTS = {'startTime', 'endTime', 'expireTime', 'earliestStartTime', 'latestEndTime'}
|
|
|
|
def _showMeetConfItem(citem, entityType, FJQC, i=0, count=0):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(citem, timeObjects=MEET_CONFERENCE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([entityType, citem['name']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, citem, timeObjects=MEET_CONFERENCE_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
def _printMeetConfItem(user, citem, csvPF, FJQC):
|
|
baserow = {'User': user}
|
|
row = flattenJSON(citem, flattened=baserow.copy(), timeObjects=MEET_CONFERENCE_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = baserow.copy()
|
|
row.update({'name': citem['name'],
|
|
'JSON': json.dumps(cleanJSON(citem, timeObjects=MEET_CONFERENCE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
# gam <UserItem> show meetconferences
|
|
# [space <MeetSpaceName>] [code <String>]
|
|
# [andquery|orquery <String>] [querytime<String> <Time>]
|
|
# [formatjson]
|
|
# gam <UserItem> print meetconferences [todrive <ToDriveAttribute>*]
|
|
# [space <MeetSpaceName>] [code <String>]
|
|
# [andquery|orquery <String>] [querytime<String> <Time>]
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowMeetConferences(users):
|
|
def _updateQuery(conjunction, clause):
|
|
if queries[0]:
|
|
queries[0] += f' {conjunction} '
|
|
queries[0] += clause
|
|
|
|
csvPF = CSVPrintFile(['User', 'space', 'name', 'startTime', 'endTime', 'expireTime']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
queries = ['']
|
|
queryTimes = {}
|
|
pfilter = ''
|
|
kwargs = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
|
|
_updateQuery('AND', f'space.name = "{getSpaceName(myarg)}"')
|
|
elif myarg == 'code':
|
|
_updateQuery('AND', f'space.meeting_code = "{getString(Cmd.OB_STRING)}"')
|
|
elif myarg == 'andquery':
|
|
_updateQuery('AND', getString(Cmd.OB_QUERY))
|
|
elif myarg == 'orquery':
|
|
_updateQuery('OR', getString(Cmd.OB_QUERY))
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
substituteQueryTimes(queries, queryTimes)
|
|
if queries[0]:
|
|
pfilter = kwargs['filter'] = queries[0]
|
|
else:
|
|
pfilter = None
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_CONFERENCE, None])
|
|
if not meet:
|
|
continue
|
|
try:
|
|
confRecs = callGAPIpages(meet.conferenceRecords(), 'list', 'conferenceRecords',
|
|
pageMessage=_getMeetPageMessage(Ent.MEET_CONFERENCE, user, i, count, pfilter),
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
pageSize=100, **kwargs)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit(kvList, str(e))
|
|
continue
|
|
jcount = len(confRecs)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems(kvList, jcount, Ent.MEET_CONFERENCE, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for confRec in confRecs:
|
|
j += 1
|
|
_showMeetConfItem(confRec, Ent.MEET_CONFERENCE, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for confRec in confRecs:
|
|
_printMeetConfItem(user, confRec, csvPF, FJQC)
|
|
if csvPF:
|
|
csvPF.writeCSVfile(Ent.Plural(Ent.MEET_CONFERENCE))
|
|
|
|
# gam <UserItem> show meetparticipants <MeetConferenceName>
|
|
# [query <String>] [querytime<String> <Time>]
|
|
# [formatjson]
|
|
# gam <UserItem> print meetparticipants <MeetConferenceName> [todrive <ToDriveAttribute>*]
|
|
# [query <String>] [querytime<String> <Time>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserItem> show meetrecordings <MeetConferenceName>
|
|
# [formatjson]
|
|
# gam <UserItem> print meetrecordings <MeetConferenceName> [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserItem> show meettranscripts <MeetConferenceName>
|
|
# [formatjson]
|
|
# gam <UserItem> print meettranscripts <MeetConferenceName> [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]]
|
|
def _printShowMeetItems(users, entityType):
|
|
def _updateQuery(conjunction, clause):
|
|
if queries[0]:
|
|
queries[0] += f' {conjunction} '
|
|
queries[0] += clause
|
|
|
|
csvPF = CSVPrintFile(['User', 'name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
queries = ['']
|
|
queryTimes = {}
|
|
pfilter = ''
|
|
kwargs = {}
|
|
parent = getString(Cmd.OB_MEET_CONFERENCE_NAME)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif entityType == Ent.MEET_PARTICIPANT and myarg == 'andquery':
|
|
_updateQuery('AND', getString(Cmd.OB_QUERY))
|
|
elif entityType == Ent.MEET_PARTICIPANT and myarg == 'orquery':
|
|
_updateQuery('OR', getString(Cmd.OB_QUERY))
|
|
elif entityType == Ent.MEET_PARTICIPANT and myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
substituteQueryTimes(queries, queryTimes)
|
|
if queries[0]:
|
|
pfilter = kwargs['filter'] = queries[0]
|
|
else:
|
|
pfilter = None
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, meet, kvList = buildMeetServiceObject(API.MEET, user, i, count, [Ent.MEET_CONFERENCE, parent])
|
|
if not meet:
|
|
continue
|
|
if entityType == Ent.MEET_PARTICIPANT:
|
|
service = meet.conferenceRecords().participants()
|
|
recType = 'participants'
|
|
pageSize = 250
|
|
elif entityType == Ent.MEET_RECORDING:
|
|
service = meet.conferenceRecords().recordings()
|
|
recType = 'recordings'
|
|
pageSize = 100
|
|
else:
|
|
service = meet.conferenceRecords().transcripts()
|
|
recType = 'transcripts'
|
|
pageSize = 100
|
|
try:
|
|
confRecs = callGAPIpages(service, 'list', recType,
|
|
pageMessage=_getMeetPageMessage(entityType, user, i, count, pfilter),
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
parent=parent, pageSize=pageSize, **kwargs)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedExit(kvList, str(e))
|
|
continue
|
|
jcount = len(confRecs)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for confRec in confRecs:
|
|
j += 1
|
|
_showMeetConfItem(confRec, entityType, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for confRec in confRecs:
|
|
_printMeetConfItem(user, confRec, csvPF, FJQC)
|
|
if csvPF:
|
|
csvPF.writeCSVfile(Ent.Plural(entityType))
|
|
|
|
def printShowMeetParticipants(users):
|
|
_printShowMeetItems(users, Ent.MEET_PARTICIPANT)
|
|
|
|
def printShowMeetRecordings(users):
|
|
_printShowMeetItems(users, Ent.MEET_RECORDING)
|
|
|
|
def printShowMeetTranscripts(users):
|
|
_printShowMeetItems(users, Ent.MEET_TRANSCRIPT)
|
|
|
|
def _getOrgunitsOrgUnitIdPath(cd, orgUnit):
|
|
if orgUnit.startswith('orgunits/'):
|
|
orgUnit = f'id:{orgUnit[9:]}'
|
|
orgUnitPath, orgUnitId = getOrgUnitId(cd, orgUnit)
|
|
return (orgUnitPath, f'orgunits/{orgUnitId[3:]}')
|
|
|
|
def _getChromePolicySchemaName():
|
|
name = getString(Cmd.OB_SCHEMA_NAME)
|
|
if not name.startswith('customers'):
|
|
name = f'customers/{GC.Values[GC.CUSTOMER_ID]}/policySchemas/{name}'
|
|
return name
|
|
|
|
def _getChromePolicySchema(cp, name, fields):
|
|
if not name.startswith('customers'):
|
|
name = f'customers/{GC.Values[GC.CUSTOMER_ID]}/policySchemas/{name}'
|
|
try:
|
|
return callGAPI(cp.customers().policySchemas(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
|
|
name=name, fields=fields)
|
|
except GAPI.notFound:
|
|
entityDoesNotExistExit(Ent.CHROME_POLICY_SCHEMA, name)
|
|
except (GAPI.badRequest, GAPI.forbidden):
|
|
accessErrorExit(None)
|
|
|
|
def commonprefix(m):
|
|
'''Given a list of strings m, return string which is prefix common to all'''
|
|
s1 = min(m)
|
|
loc = s1.find('ENUM_')
|
|
if loc > 0:
|
|
return s1[:loc+5]
|
|
s2 = max(m)
|
|
for i, c in enumerate(s1):
|
|
if c != s2[i]:
|
|
return s1[:i]
|
|
return s1
|
|
|
|
def simplifyChromeSchema(schema):
|
|
schema_name = schema['name'].split('/')[-1]
|
|
schema_dict = {'name': schema_name,
|
|
'description': schema.get('policyDescription', ''),
|
|
'settings': {}
|
|
}
|
|
fieldDescriptions = schema['fieldDescriptions']
|
|
for mtype in schema['definition']['messageType']:
|
|
for setting in mtype['field']:
|
|
setting_name = setting['name']
|
|
setting_dict = {'name': setting_name,
|
|
'constraints': None,
|
|
'descriptions': [],
|
|
'type': setting['type'],
|
|
}
|
|
if setting_dict['type'] == 'TYPE_STRING' and setting.get('label') == 'LABEL_REPEATED':
|
|
setting_dict['type'] = 'TYPE_LIST'
|
|
if setting_dict['type'] == 'TYPE_ENUM':
|
|
type_name = setting['typeName']
|
|
for an_enum in schema['definition']['enumType']:
|
|
if an_enum['name'] == type_name:
|
|
setting_dict['enums'] = [enum['name'] for enum in an_enum['value']]
|
|
setting_dict['enum_prefix'] = commonprefix(setting_dict['enums'])
|
|
prefix_len = len(setting_dict['enum_prefix'])
|
|
setting_dict['enums'] = [enum[prefix_len:] for enum in setting_dict['enums'] if not enum.endswith('UNSPECIFIED')]
|
|
setting_dict['descriptions'] = ['']*len(setting_dict['enums'])
|
|
for i, an in enumerate(setting_dict['enums']):
|
|
for fdesc in fieldDescriptions:
|
|
if fdesc.get('field') == setting_name:
|
|
for d in fdesc.get('knownValueDescriptions', []):
|
|
if d['value'][prefix_len:] == an:
|
|
setting_dict['descriptions'][i] = d.get('description', '')
|
|
break
|
|
break
|
|
break
|
|
elif setting_dict['type'] == 'TYPE_MESSAGE':
|
|
continue
|
|
else:
|
|
setting_dict['enums'] = None
|
|
for fdesc in schema['fieldDescriptions']:
|
|
if fdesc['field'] == setting_name:
|
|
if 'knownValueDescriptions' in fdesc:
|
|
setting_dict['descriptions'] = fdesc['knownValueDescriptions']
|
|
elif 'description' in fdesc:
|
|
setting_dict['descriptions'] = [fdesc['description']]
|
|
schema_dict['settings'][setting_name.lower()] = setting_dict
|
|
return(schema_name, schema_dict)
|
|
|
|
def _getPolicyOrgUnitTarget(cd, cp, myarg, groupEmail):
|
|
if groupEmail:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'group'))
|
|
targetName, targetResource = _getOrgunitsOrgUnitIdPath(cd, getString(Cmd.OB_ORGUNIT_PATH))
|
|
return (targetName, targetName, targetResource, Ent.ORGANIZATIONAL_UNIT, cp.customers().policies().orgunits())
|
|
|
|
def _getPolicyGroupTarget(cd, cp, myarg, orgUnit):
|
|
if orgUnit:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'ou|org|orgunit'))
|
|
targetName = getEmailAddress(returnUIDprefix='uid:')
|
|
targetResource = f"groups/{convertEmailAddressToUID(targetName, cd, emailType='group')}"
|
|
return (targetName, targetName, targetResource, Ent.GROUP, cp.customers().policies().groups())
|
|
|
|
def checkPolicyArgs(targetResource, printer_id, app_id):
|
|
if not targetResource:
|
|
missingArgumentExit('ou|org|orgunit|group')
|
|
if printer_id and app_id:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('printerid', 'appid'))
|
|
|
|
def setPolicyKVList(baseList, printer_id, app_id):
|
|
kvList = baseList[:]
|
|
if app_id:
|
|
kvList.extend([Ent.APP_ID, app_id])
|
|
elif printer_id:
|
|
kvList.extend([Ent.PRINTER_ID, printer_id])
|
|
return kvList
|
|
|
|
def updatePolicyRequests(body, targetResource, printer_id, app_id):
|
|
for request in body['requests']:
|
|
request.setdefault('policyTargetKey', {})
|
|
request['policyTargetKey']['targetResource'] = targetResource
|
|
if app_id or printer_id:
|
|
request['policyTargetKey'].setdefault('additionalTargetKeys', {})
|
|
if app_id:
|
|
request['policyTargetKey']['additionalTargetKeys']['app_id'] = app_id
|
|
elif printer_id:
|
|
request['policyTargetKey']['additionalTargetKeys']['printer_id'] = printer_id
|
|
|
|
# gam delete chromepolicy
|
|
# (<SchemaName> [<JSONData>])+
|
|
# ((ou|org|orgunit <OrgUnitItem>)|(group <GroupItem>))
|
|
# [(printerid <PrinterID>)|(appid <AppID>)]
|
|
def doDeleteChromePolicy():
|
|
cp = buildGAPIObject(API.CHROMEPOLICY)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
customer = _getCustomersCustomerIdWithC()
|
|
app_id = groupEmail = orgUnit = printer_id = targetResource = None
|
|
body = {'requests': []}
|
|
schemaNameList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'ou', 'org', 'orgunit'}:
|
|
orgUnit, targetName, targetResource, entityType, service = _getPolicyOrgUnitTarget(cd, cp, myarg, groupEmail)
|
|
function = 'batchInherit'
|
|
elif myarg == 'group':
|
|
groupEmail, targetName, targetResource, entityType, service = _getPolicyGroupTarget(cd, cp, myarg, orgUnit)
|
|
function = 'batchDelete'
|
|
elif myarg == 'printerid':
|
|
printer_id = getString(Cmd.OB_PRINTER_ID)
|
|
elif myarg == 'appid':
|
|
app_id = getString(Cmd.OB_APP_ID)
|
|
else:
|
|
schema = _getChromePolicySchema(cp, Cmd.Previous(), 'name')
|
|
schemaName = schema['name'].split('/')[-1]
|
|
schemaNameList.append(schemaName)
|
|
body['requests'].append({'policySchema': schemaName})
|
|
if checkArgumentPresent('json'):
|
|
jsonData = getJSON(['direct', 'name', 'orgUnitPath', 'parentOrgUnitPath', 'group'])
|
|
if 'additionalTargetKeys' in jsonData:
|
|
body['requests'][-1].setdefault('policyTargetKey', {'additionalTargetKeys': {}})
|
|
for atk in jsonData['additionalTargetKeys']:
|
|
body['requests'][-1]['policyTargetKey']['additionalTargetKeys'][atk['name']] = atk['value']
|
|
checkPolicyArgs(targetResource, printer_id, app_id)
|
|
count = len(body['requests'])
|
|
if count != 1:
|
|
entityPerformActionNumItems([entityType, targetName], count, Ent.CHROME_POLICY)
|
|
if count == 0:
|
|
return
|
|
kvList = setPolicyKVList([entityType, targetName, Ent.CHROME_POLICY, ','.join(schemaNameList)], printer_id, app_id)
|
|
updatePolicyRequests(body, targetResource, printer_id, app_id)
|
|
try:
|
|
callGAPI(service, function,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED,
|
|
GAPI.INVALID_ARGUMENT, GAPI.SERVICE_NOT_AVAILABLE, GAPI.QUOTA_EXCEEDED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customer, body=body)
|
|
entityActionPerformed(kvList)
|
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable, GAPI.quotaExceeded) as e:
|
|
entityActionFailedWarning(kvList, str(e))
|
|
|
|
CHROME_SCHEMA_TYPE_MESSAGE = {
|
|
'chrome.users.AutoUpdateCheckPeriodNew':
|
|
{'autoupdatecheckperiodminutesnew':
|
|
{'casedField': 'autoUpdateCheckPeriodMinutesNew',
|
|
'type': 'duration', 'minVal': 1, 'maxVal': 720, 'scale': 60}},
|
|
'chrome.users.AutoUpdateCheckPeriodNewV2':
|
|
{'autoupdatecheckperiodminutesnew':
|
|
{'casedField': 'autoUpdateCheckPeriodMinutesNew',
|
|
'type': 'duration', 'minVal': 1, 'maxVal': 720, 'scale': 60}},
|
|
'chrome.users.BrowserSwitcherDelayDuration':
|
|
{'browserswitcherdelayduration':
|
|
{'casedField': 'browserSwitcherDelayDuration',
|
|
'type': 'duration', 'minVal': 0, 'maxVal': 30, 'scale': 1}},
|
|
'chrome.users.CloudReportingUploadFrequencyV2':
|
|
{'cloudreportinguploadfrequency':
|
|
{'casedField': 'cloudReportingUploadFrequency',
|
|
'type': 'count', 'minVal': 3, 'maxVal': 24, 'scale': 1}},
|
|
'chrome.users.FetchKeepaliveDurationSecondsOnShutdown':
|
|
{'fetchkeepalivedurationsecondsonshutdown':
|
|
{'casedField': 'fetchKeepaliveDurationSecondsOnShutdown',
|
|
'type': 'duration', 'minVal': 0, 'maxVal': 5, 'scale': 1}},
|
|
'chrome.users.MaxInvalidationFetchDelay':
|
|
{'maxinvalidationfetchdelay':
|
|
{'casedField': 'maxInvalidationFetchDelay',
|
|
'type': 'duration', 'minVal': 1, 'maxVal': 30, 'scale': 1, 'default': 10}},
|
|
'chrome.users.PrintingMaxSheetsAllowed':
|
|
{'printingmaxsheetsallowednullable':
|
|
{'casedField': 'printingMaxSheetsAllowedNullable',
|
|
'type': 'value', 'minVal': 1, 'maxVal': None, 'scale': 1}},
|
|
'chrome.users.PrintJobHistoryExpirationPeriodNew':
|
|
{'printjobhistoryexpirationperioddaysnew':
|
|
{'casedField': 'printJobHistoryExpirationPeriodDaysNew',
|
|
'type': 'duration', 'minVal': -1, 'maxVal': None, 'scale': 86400}},
|
|
'chrome.users.RelaunchNotificationWithDurationV2':
|
|
{'relaunchnotificationperiodduration':
|
|
{'casedField': 'relaunchNotificationPeriodDuration',
|
|
'type': 'duration', 'minVal': -1, 'maxVal': None, 'scale': 3600}},
|
|
'chrome.users.SecurityTokenSessionSettings':
|
|
{'securitytokensessionnotificationseconds':
|
|
{'casedField': 'securityTokenSessionNotificationSeconds',
|
|
'type': 'duration', 'minVal': 0, 'maxVal': 9999, 'scale': 1}},
|
|
'chrome.users.SessionLength':
|
|
{'sessiondurationlimit':
|
|
{'casedField': 'sessionDurationLimit',
|
|
'type': 'duration', 'minVal': 1, 'maxVal': 1440, 'scale': 60}},
|
|
'chrome.users.UpdatesSuppressed':
|
|
{'updatessuppresseddurationmin':
|
|
{'casedField': 'updatesSuppressedDurationMin',
|
|
'type': 'count', 'minVal': 1, 'maxVal': 1440, 'scale': 1},
|
|
'updatessuppressedstarttime':
|
|
{'casedField': 'updatesSuppressedStartTime',
|
|
'type': 'timeOfDay'}},
|
|
'chrome.devices.managedguest.Avatar':
|
|
{'useravatarimage':
|
|
{'casedField': 'userAvatarImage',
|
|
'type': 'downloadUri'}},
|
|
'chrome.devices.managedguest.Wallpaper':
|
|
{'wallpaperimage':
|
|
{'casedField': 'wallpaperImage',
|
|
'type': 'downloadUri'}},
|
|
'chrome.devices.SignInWallpaperImage':
|
|
{'devicewallpaperimage':
|
|
{'casedField': 'deviceWallpaperImage',
|
|
'type': 'downloadUri'}},
|
|
'chrome.users.Avatar':
|
|
{'useravatarimage':
|
|
{'casedField': 'userAvatarImage',
|
|
'type': 'downloadUri'}},
|
|
'chrome.users.Wallpaper':
|
|
{'wallpaperimage':
|
|
{'casedField': 'wallpaperImage',
|
|
'type': 'downloadUri'}},
|
|
}
|
|
|
|
CHROME_TARGET_VERSION_CHANNEL_MINUS_PATTERN = re.compile(r'^([a-z]+)-(\d+)$')
|
|
CHROME_TARGET_VERSION_PATTERN = re.compile(r'^(\d{1,4}\.){1,4}$')
|
|
|
|
# gam update chromepolicy [convertcrnl]
|
|
# (<SchemaName> ((<Field> <Value>)+ | <JSONData>))+
|
|
# ((ou|orgunit <OrgUnitItem>)|(group <GroupItem>))
|
|
# [(printerid <PrinterID>)|(appid <AppID>)]
|
|
def doUpdateChromePolicy():
|
|
def getSpecialVtypeValue(vtype, value):
|
|
if vtype == 'duration':
|
|
return {vtype: f'{value}s'}
|
|
if vtype in {'value', 'downloadUri'}:
|
|
return {vtype: value}
|
|
if vtype == 'count':
|
|
return value
|
|
#if vtype == timeOfDay:
|
|
hours, minutes = value.split(':')
|
|
return {vtype: {'hours': hours, 'minutes': minutes}}
|
|
|
|
cp = buildGAPIObject(API.CHROMEPOLICY)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
cv = None
|
|
customer = _getCustomersCustomerIdWithC()
|
|
app_id = channelMap = groupEmail = orgUnit = printer_id = targetResource = None
|
|
body = {'requests': []}
|
|
schemaNameList = []
|
|
convertCRsNLs = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'ou', 'org', 'orgunit'}:
|
|
orgUnit, targetName, targetResource, entityType, service = _getPolicyOrgUnitTarget(cd, cp, myarg, groupEmail)
|
|
elif myarg == 'group':
|
|
groupEmail, targetName, targetResource, entityType, service = _getPolicyGroupTarget(cd, cp, myarg, orgUnit)
|
|
elif myarg == 'printerid':
|
|
printer_id = getString(Cmd.OB_PRINTER_ID)
|
|
elif myarg == 'appid':
|
|
app_id = getString(Cmd.OB_APP_ID)
|
|
elif myarg == 'convertcrnl':
|
|
convertCRsNLs = True
|
|
else:
|
|
schemaName, schema = simplifyChromeSchema(_getChromePolicySchema(cp, Cmd.Previous(), '*'))
|
|
body['requests'].append({'policyValue': {'policySchema': schemaName, 'value': {}},
|
|
'updateMask': ''})
|
|
schemaNameList.append(schemaName)
|
|
while Cmd.ArgumentsRemaining():
|
|
field = getArgumentEmptyAllowed()
|
|
# Allow an empty field/value pair which makes processing an input CSV file with schemas with different numbers of fields easy
|
|
if not field:
|
|
if Cmd.ArgumentsRemaining():
|
|
Cmd.Advance()
|
|
continue
|
|
if field in {'ou', 'org', 'orgunit', 'group', 'printerid', 'appid'} or '.' in field:
|
|
if field != 'appid' or not schemaName.startswith('chrome.devices.kiosk'):
|
|
Cmd.Backup()
|
|
break # field is actually a new policy name or orgunit
|
|
# JSON
|
|
if field == 'json':
|
|
jsonData = getJSON(['direct', 'name', 'orgUnitPath', 'parentOrgUnitPath', 'group'])
|
|
schemaNameAppId = schemaName
|
|
if 'additionalTargetKeys' in jsonData:
|
|
body['requests'][-1].setdefault('policyTargetKey', {'additionalTargetKeys': {}})
|
|
for atk in jsonData['additionalTargetKeys']:
|
|
body['requests'][-1]['policyTargetKey']['additionalTargetKeys'][atk['name']] = atk['value']
|
|
if atk['name'] == 'app_id':
|
|
schemaNameAppId += f"({atk['value']})"
|
|
schemaNameList[-1] = schemaNameAppId
|
|
for field in jsonData.get('fields', []):
|
|
casedField = field['name']
|
|
lowerField = casedField.lower()
|
|
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
|
|
tmschema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName, {}).get(lowerField)
|
|
if tmschema:
|
|
body['requests'][-1]['policyValue']['value'][casedField] = getSpecialVtypeValue(tmschema['type'], field['value'])
|
|
body['requests'][-1]['updateMask'] += f'{casedField},'
|
|
continue
|
|
vtype = schema['settings'].get(lowerField, {}).get('type')
|
|
value = field['value']
|
|
if vtype in ['TYPE_INT64', 'TYPE_INT32', 'TYPE_UINT64']:
|
|
value = int(value)
|
|
elif vtype == 'TYPE_BOOL':
|
|
pass
|
|
elif vtype == 'TYPE_ENUM':
|
|
prefix = schema['settings'][lowerField]['enum_prefix']
|
|
if not value.startswith(prefix):
|
|
value = f"{prefix}{value}"
|
|
elif vtype == 'TYPE_LIST':
|
|
value = value.split(',') if value else []
|
|
if myarg == 'chrome.users.chromebrowserupdates' and casedField == 'targetVersionPrefixSetting':
|
|
mg = CHROME_TARGET_VERSION_CHANNEL_MINUS_PATTERN.match(value)
|
|
if mg:
|
|
channel = mg.group(1).lower().replace('_', '')
|
|
if channelMap is None:
|
|
cv, channelMap = getPlatformChannelMap(cv, Ent.CHROME_CHANNEL)
|
|
if channel not in channelMap:
|
|
invalidChoiceExit(value, channelMap, True)
|
|
cv, status, milestone = getRelativeMilestone(cv, channelMap[channel], int(mg.group(2)))
|
|
if not status:
|
|
Cmd.Backup()
|
|
invalidArgumentExit(f'{milestone} for {casedField}: {value}')
|
|
value = f'{milestone}.'
|
|
elif value and not CHROME_TARGET_VERSION_PATTERN.match(value):
|
|
Cmd.Backup()
|
|
invalidArgumentExit(f'{Msg.CHROME_TARGET_VERSION_FORMAT} for {casedField}: {value}')
|
|
body['requests'][-1]['policyValue']['value'][casedField] = value
|
|
body['requests'][-1]['updateMask'] += f'{casedField},'
|
|
break
|
|
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
|
|
tmschema = CHROME_SCHEMA_TYPE_MESSAGE.get(schemaName, {}).get(field)
|
|
if tmschema:
|
|
casedField = tmschema['casedField']
|
|
vtype = tmschema['type']
|
|
if vtype == 'downloadUri':
|
|
value = getString(Cmd.OB_STRING)
|
|
elif vtype != 'timeOfDay':
|
|
if 'default' not in tmschema:
|
|
value = getInteger(minVal=tmschema['minVal'], maxVal=tmschema['maxVal'])*tmschema['scale']
|
|
else:
|
|
value = getIntegerEmptyAllowed(minVal=tmschema['minVal'], maxVal=tmschema['maxVal'], default=tmschema['default'])*tmschema['scale']
|
|
else:
|
|
value = getHHMM()
|
|
body['requests'][-1]['policyValue']['value'][casedField] = getSpecialVtypeValue(vtype, value)
|
|
body['requests'][-1]['updateMask'] += f'{casedField},'
|
|
continue
|
|
if field not in schema['settings']:
|
|
Cmd.Backup()
|
|
missingChoiceExit(schema['settings'])
|
|
casedField = schema['settings'][field]['name']
|
|
vtype = schema['settings'][field]['type']
|
|
value = getString(Cmd.OB_STRING, minLen=0 if vtype in {'TYPE_STRING', 'TYPE_LIST'} else 1)
|
|
if vtype in ['TYPE_INT64', 'TYPE_INT32', 'TYPE_UINT64']:
|
|
if not value.isnumeric():
|
|
Cmd.Backup()
|
|
invalidArgumentExit(integerLimits(None, None))
|
|
value = int(value)
|
|
elif vtype == 'TYPE_BOOL':
|
|
value = value.lower()
|
|
if value in TRUE_VALUES:
|
|
value = True
|
|
elif value in FALSE_VALUES:
|
|
value = False
|
|
else:
|
|
invalidChoiceExit(value, TRUE_FALSE, True)
|
|
elif vtype == 'TYPE_ENUM':
|
|
value = value.upper()
|
|
prefix = schema['settings'][field]['enum_prefix']
|
|
enum_values = schema['settings'][field]['enums']
|
|
if value in enum_values:
|
|
value = f'{prefix}{value}'
|
|
elif value.replace(prefix, '') in enum_values:
|
|
pass
|
|
else:
|
|
invalidChoiceExit(value, enum_values, True)
|
|
elif vtype == 'TYPE_LIST':
|
|
value = value.split(',') if value else []
|
|
elif vtype == 'TYPE_STRING' and convertCRsNLs:
|
|
value = unescapeCRsNLs(value)
|
|
if myarg == 'chrome.users.chromebrowserupdates' and casedField == 'targetVersionPrefixSetting':
|
|
mg = CHROME_TARGET_VERSION_CHANNEL_MINUS_PATTERN.match(value)
|
|
if mg:
|
|
channel = mg.group(1).lower().replace('_', '')
|
|
if channelMap is None:
|
|
cv, channelMap = getPlatformChannelMap(cv, Ent.CHROME_CHANNEL)
|
|
if channel not in channelMap:
|
|
invalidChoiceExit(value, channelMap, True)
|
|
cv, status, milestone = getRelativeMilestone(cv, channelMap[channel], int(mg.group(2)))
|
|
if not status:
|
|
Cmd.Backup()
|
|
invalidArgumentExit(f'{milestone} for {casedField}: {value}')
|
|
value = f'{milestone}.'
|
|
elif value and not CHROME_TARGET_VERSION_PATTERN.match(value):
|
|
Cmd.Backup()
|
|
invalidArgumentExit(Msg.CHROME_TARGET_VERSION_FORMAT)
|
|
body['requests'][-1]['policyValue']['value'][casedField] = value
|
|
body['requests'][-1]['updateMask'] += f'{casedField},'
|
|
checkPolicyArgs(targetResource, printer_id, app_id)
|
|
count = len(body['requests'])
|
|
if count > 0 and not body['requests'][-1]['updateMask']:
|
|
body['requests'].pop()
|
|
kvList = setPolicyKVList([entityType, targetName, Ent.CHROME_POLICY, ','.join(schemaNameList)], printer_id, app_id)
|
|
if count != 1:
|
|
entityPerformActionNumItems(kvList, count, Ent.CHROME_POLICY)
|
|
if count == 0:
|
|
return
|
|
updatePolicyRequests(body, targetResource, printer_id, app_id)
|
|
try:
|
|
callGAPI(service, 'batchModify',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT,
|
|
GAPI.SERVICE_NOT_AVAILABLE, GAPI.QUOTA_EXCEEDED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customer, body=body)
|
|
entityActionPerformed(kvList)
|
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable, GAPI.quotaExceeded) as e:
|
|
entityActionFailedWarning(kvList, str(e))
|
|
|
|
CHROME_POLICY_INDEXED_TITLES = ['fields']
|
|
|
|
CHROME_POLICY_SHOW_ALL = -1
|
|
CHROME_POLICY_SHOW_DIRECT = True
|
|
CHROME_POLICY_SHOW_INHERITED = False
|
|
CHROME_POLICY_SHOW_CHOICE_MAP = {
|
|
'all': CHROME_POLICY_SHOW_ALL,
|
|
'direct': CHROME_POLICY_SHOW_DIRECT,
|
|
'inherited': CHROME_POLICY_SHOW_INHERITED
|
|
}
|
|
|
|
# gam show chromepolicies
|
|
# ((ou|orgunit <OrgUnitItem>)|(group <GroupItem>))
|
|
# [(printerid <PrinterID>)|(appid <AppID>)]
|
|
# (filter <StringList>)* (namespace <NamespaceList>)*
|
|
# [show all|direct|inherited]
|
|
# [formatjson]
|
|
# gam print chromepolicies [todrive <ToDriveAttribute>*]
|
|
# ((ou|orgunit <OrgUnitItem>)|(group <GroupItem>))
|
|
# [(printerid <PrinterID>)|(appid <AppID>)]
|
|
# (filter <StringList>)* (namespace <NamespaceList>)*
|
|
# [show all|direct|inherited]
|
|
# [[formatjson [quotechar <Character>]]
|
|
def doPrintShowChromePolicies():
|
|
def normalizedPolicy(policy):
|
|
norm = {'name': policy['value']['policySchema']}
|
|
if app_id:
|
|
norm['appId'] = app_id
|
|
elif printer_id:
|
|
norm['printerId'] = printer_id
|
|
if entityType == Ent.ORGANIZATIONAL_UNIT:
|
|
orgUnitId = policy.get('targetKey', {}).get('targetResource')
|
|
norm['orgUnitPath'] = convertOrgUnitIDtoPath(cd, orgUnitId) if orgUnitId else UNKNOWN
|
|
parentOrgUnitId = policy.get('sourceKey', {}).get('targetResource')
|
|
norm['parentOrgUnitPath'] = convertOrgUnitIDtoPath(cd, parentOrgUnitId) if parentOrgUnitId else UNKNOWN
|
|
norm['direct'] = orgUnitId == parentOrgUnitId
|
|
else:
|
|
groupId = policy.get('targetKey', {}).get('targetResource')
|
|
if groupId is not None:
|
|
groupId = groupId.split('/')[1]
|
|
norm['group'] = getGroupEmailFromID(groupId, cd)
|
|
if norm['group'] is None:
|
|
norm['group'] = groupId
|
|
else:
|
|
norm['group'] = UNKNOWN
|
|
norm['additionalTargetKeys'] = []
|
|
for setting, value in sorted(policy.get('targetKey', {}).get('additionalTargetKeys', {}).items()):
|
|
norm['additionalTargetKeys'].append({'name': setting, 'value': value})
|
|
norm['fields'] = []
|
|
name = policy['value']['policySchema']
|
|
values = policy.get('value', {}).get('value', {})
|
|
if name in {'chrome.devices.managedguest.apps.ManagedConfiguration',
|
|
'chrome.devices.kiosk.apps.ManagedConfiguration',
|
|
'chrome.users.apps.ManagedConfiguration'} and 'managedConfiguration' in values:
|
|
values['managedConfiguration'] = json.dumps(values['managedConfiguration'], ensure_ascii=False).replace('\\n', '').replace('\\"', '"')[1:-1]
|
|
for setting, value in values.items():
|
|
# Handle TYPE_MESSAGE fields with durations, values, counts and timeOfDay as special cases
|
|
schema = CHROME_SCHEMA_TYPE_MESSAGE.get(name, {}).get(setting.lower())
|
|
if schema and setting == schema['casedField']:
|
|
vtype = schema['type']
|
|
if vtype in {'duration', 'value'}:
|
|
value = value.get(vtype, '')
|
|
if value:
|
|
if value.endswith('s'):
|
|
value = value[:-1]
|
|
value = int(value) // schema['scale']
|
|
elif vtype == 'count':
|
|
pass
|
|
elif vtype == 'downloadUri':
|
|
value = value.get(vtype, '')
|
|
else: #timeOfDay
|
|
hours = value.get(vtype, {}).get('hours', 0)
|
|
minutes = value.get(vtype, {}).get('minutes', 0)
|
|
value = f'{hours:02}:{minutes:02}'
|
|
elif isinstance(value, str) and value.find('_ENUM_') != -1:
|
|
value = value.split('_ENUM_')[-1]
|
|
elif isinstance(value, list):
|
|
if len(value) > 0:
|
|
if not isinstance(value[0], dict):
|
|
value = ','.join(value)
|
|
else:
|
|
value = json.dumps(value, ensure_ascii=False)
|
|
else:
|
|
value = ''
|
|
norm['fields'].append({'name': setting, 'value': value})
|
|
return norm
|
|
|
|
def _showPolicy(policy, j, jcount):
|
|
policy = normalizedPolicy(policy)
|
|
if (entityType == Ent.GROUP) or showPolicies in (CHROME_POLICY_SHOW_ALL, policy['direct']):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(policy), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printKeyValueListWithCount([policy['name']], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, policy, sortDictKeys=False)
|
|
Ind.Decrement()
|
|
|
|
def _printPolicy(policy):
|
|
policy = normalizedPolicy(policy)
|
|
if (entityType == Ent.GROUP) or showPolicies in (CHROME_POLICY_SHOW_ALL, policy['direct']):
|
|
row = flattenJSON(policy)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(row):
|
|
if entityType == Ent.ORGANIZATIONAL_UNIT:
|
|
csvPF.WriteRowNoFilter({'name': policy['name'],
|
|
'orgUnitPath': policy['orgUnitPath'],
|
|
'parentOrgUnitPath': policy['parentOrgUnitPath'],
|
|
'direct': policy['direct'],
|
|
'JSON': json.dumps(cleanJSON(policy),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
else:
|
|
csvPF.WriteRowNoFilter({'name': policy['name'],
|
|
'group': policy['group'],
|
|
'JSON': json.dumps(cleanJSON(policy),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
cp = buildGAPIObject(API.CHROMEPOLICY)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
customer = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile(['name'], indexedTitles=CHROME_POLICY_INDEXED_TITLES) if Act.csvFormat() else None
|
|
if csvPF:
|
|
csvPF.SetNoEscapeChar(True)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
app_id = groupEmail = orgUnit = printer_id = targetResource = None
|
|
showPolicies = CHROME_POLICY_SHOW_ALL
|
|
psFilters = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
orgUnit, targetName, targetResource, entityType, _ = _getPolicyOrgUnitTarget(cd, cp, myarg, groupEmail)
|
|
elif myarg == 'group':
|
|
groupEmail, targetName, targetResource, entityType, _ = _getPolicyGroupTarget(cd, cp, myarg, orgUnit)
|
|
elif myarg == 'printerid':
|
|
printer_id = getString(Cmd.OB_PRINTER_ID)
|
|
elif myarg == 'appid':
|
|
app_id = getString(Cmd.OB_APP_ID)
|
|
elif myarg == 'filter':
|
|
for psFilter in getString(Cmd.OB_STRING).replace(',', ' ').split():
|
|
psFilters.append(psFilter)
|
|
elif myarg == 'namespace':
|
|
for psFilter in getString(Cmd.OB_STRING).replace(',', ' ').split():
|
|
if psFilter.endswith('.*'):
|
|
psFilters.append(psFilter)
|
|
else:
|
|
psFilters.append(f'{psFilter}.*')
|
|
elif myarg == 'show':
|
|
showPolicies = getChoice(CHROME_POLICY_SHOW_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
checkPolicyArgs(targetResource, printer_id, app_id)
|
|
body = {'policyTargetKey': {'targetResource': targetResource}}
|
|
if app_id:
|
|
body['policyTargetKey']['additionalTargetKeys'] = {'app_id': app_id}
|
|
if not psFilters:
|
|
psFilters = ['chrome.users.apps.*',
|
|
'chrome.devices.kiosk.apps.*',
|
|
'chrome.devices.managedguest.apps.*',
|
|
]
|
|
elif printer_id:
|
|
body['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printer_id}
|
|
if not psFilters:
|
|
psFilters = ['chrome.printers.*']
|
|
elif not psFilters:
|
|
if entityType == Ent.ORGANIZATIONAL_UNIT:
|
|
psFilters = ['chrome.users.*',
|
|
'chrome.users.apps.*',
|
|
'chrome.users.appsconfig.*',
|
|
'chrome.devices.*',
|
|
'chrome.devices.kiosk.*',
|
|
'chrome.devices.kiosk.apps.*',
|
|
'chrome.devices.managedguest.*',
|
|
'chrome.devices.managedguest.apps.*',
|
|
'chrome.networks.cellular.*',
|
|
'chrome.networks.certificates.*',
|
|
'chrome.networks.ethernet.*',
|
|
'chrome.networks.globalsettings.*',
|
|
'chrome.networks.vpn.*',
|
|
'chrome.networks.wifi.*',
|
|
'chrome.printers.*',
|
|
'chrome.printservers.*',
|
|
]
|
|
else:
|
|
psFilters = ['chrome.users.*',
|
|
'chrome.users.apps.*',
|
|
'chrome.users.appsconfig.*',
|
|
'chrome.printers.*',
|
|
'chrome.printservers.*',
|
|
]
|
|
if csvPF:
|
|
if entityType == Ent.ORGANIZATIONAL_UNIT:
|
|
csvPF.AddTitles(['orgUnitPath', 'parentOrgUnitPath', 'direct'])
|
|
else:
|
|
csvPF.AddTitles(['group'])
|
|
csvPF.SetSortAllTitles()
|
|
if not FJQC.formatJSON:
|
|
if app_id:
|
|
csvPF.AddSortTitles(['appId'])
|
|
elif printer_id:
|
|
csvPF.AddSortTitles(['printerId'])
|
|
else:
|
|
csvPF.SetJSONTitles(csvPF.titlesList+['JSON'])
|
|
policies = []
|
|
for psFilter in psFilters:
|
|
body['policySchemaFilter'] = psFilter
|
|
body['pageToken'] = None
|
|
printGettingAllEntityItemsForWhom(Ent.CHROME_POLICY, targetName, query=body['policySchemaFilter'])
|
|
try:
|
|
policies.extend(callGAPIpages(cp.customers().policies(), 'resolve', 'resolvedPolicies',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT,
|
|
GAPI.SERVICE_NOT_AVAILABLE, GAPI.QUOTA_EXCEEDED],
|
|
customer=customer, body=body, pageArgsInBody=True))
|
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.quotaExceeded) as e:
|
|
entityActionFailedWarning([entityType, targetName], str(e))
|
|
continue
|
|
except (GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CHROME_POLICY, body['policySchemaFilter']], str(e))
|
|
continue
|
|
# sort policies first by app/printer id then by schema name
|
|
policies = sorted(policies,
|
|
key=lambda k: (list(k.get('targetKey', {}).get('additionalTargetKeys', {}).values()),
|
|
k.get('value', {}).get('policySchema', '')))
|
|
if not csvPF:
|
|
jcount = len(policies)
|
|
if not FJQC.formatJSON:
|
|
kvList = setPolicyKVList([entityType, targetName], printer_id, app_id)
|
|
entityPerformActionModifierNumItems(kvList, Msg.MAXIMUM_OF, jcount, Ent.CHROME_POLICY)
|
|
Ind.Increment()
|
|
j = 0
|
|
for policy in policies:
|
|
j += 1
|
|
_showPolicy(policy, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for policy in policies:
|
|
_printPolicy(policy)
|
|
if csvPF:
|
|
csvPF.writeCSVfile(f'Chrome Policies - {targetName}')
|
|
|
|
CHROME_IMAGE_SCHEMAS_MAP = {
|
|
'chrome.devices.managedguest.avatar': {'name': 'chrome.devices.managedguest.Avatar', 'field': 'userAvatarImage'},
|
|
'chrome.devices.managedguest.wallpaper': {'name': 'chrome.devices.managedguest.Wallpaper', 'field': 'wallpaperImage'},
|
|
'chrome.devices.signinwallpaperimage': {'name': 'chrome.devices.SignInWallpaperImage', 'field': 'deviceWallpaperImage'},
|
|
'chrome.users.avatar': {'name': 'chrome.users.Avatar', 'field': 'userAvatarImage'},
|
|
'chrome.users.wallpaper': {'name': 'chrome.users.Wallpaper', 'field': 'wallpaperImage'},
|
|
}
|
|
|
|
# gam create chromepolicyimage <ChromePolicyImageSchemaName> <FileName>
|
|
def doCreateChromePolicyImage():
|
|
cp = buildGAPIObject(API.CHROMEPOLICY)
|
|
parent = _getCustomersCustomerIdWithC()
|
|
schema = getChoice(CHROME_IMAGE_SCHEMAS_MAP, mapChoice=True)
|
|
parameters = {DFA_URL: None}
|
|
parameters[DFA_LOCALFILEPATH] = os.path.expanduser(getString(Cmd.OB_FILE_NAME))
|
|
try:
|
|
f = open(parameters[DFA_LOCALFILEPATH], 'rb')
|
|
f.close()
|
|
except IOError as e:
|
|
Cmd.Backup()
|
|
usageErrorExit(f'{parameters[DFA_LOCALFILEPATH]}: {str(e)}')
|
|
parameters[DFA_LOCALFILENAME] = os.path.basename(parameters[DFA_LOCALFILEPATH])
|
|
parameters[DFA_LOCALMIMETYPE] = mimetypes.guess_type(parameters[DFA_LOCALFILEPATH])[0]
|
|
if parameters[DFA_LOCALMIMETYPE] is None:
|
|
parameters[DFA_LOCALMIMETYPE] = 'image/jpeg'
|
|
checkForExtraneousArguments()
|
|
media_body = getMediaBody(parameters)
|
|
try:
|
|
result = callGAPI(cp.media(), 'upload',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN],
|
|
customer=parent, media_body=media_body,
|
|
body={'policyField': f"{schema['name']}.{schema['field']}"})
|
|
entityActionPerformed([Ent.CHROME_POLICY_IMAGE, f"{schema['name']} {schema['field']} {result['downloadUri']}"])
|
|
except (GAPI.invalidArgument, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CHROME_POLICY_IMAGE, f"{schema['name']}", Ent.FILE, f"{parameters[DFA_LOCALFILEPATH]}"], str(e))
|
|
|
|
def _showChromePolicySchema(schema, FJQC, i=0, count=0):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(schema), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CHROME_POLICY_SCHEMA, schema['name']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, schema)
|
|
Ind.Decrement()
|
|
|
|
CHROME_POLICY_SCHEMA_FIELDS_CHOICE_MAP = {
|
|
'accessrestrictions': 'accessRestrictions',
|
|
'additionaltargetkeynames': 'additionalTargetKeyNames',
|
|
'categorytitle': 'categoryTitle',
|
|
'definition': 'definition',
|
|
'fielddescriptions': 'fieldDescriptions',
|
|
'name': 'name',
|
|
'notices': 'notices',
|
|
'policyapilifecycle': 'policyApiLifecycle',
|
|
'policydescription': 'policyDescription',
|
|
'schemaname': 'schemaName',
|
|
'supporturi': 'supportUri',
|
|
'validtargetresources': 'validTargetResources',
|
|
}
|
|
|
|
# gam info chromeschema <SchemaName>
|
|
# <ChromePolicySchemaFieldName>* [fields <ChromePolicySchemaFieldNameList>]
|
|
# [formatjson]
|
|
def doInfoChromePolicySchemas():
|
|
cp = buildGAPIObject(API.CHROMEPOLICY)
|
|
FJQC = FormatJSONQuoteChar()
|
|
fieldsList = []
|
|
name = _getChromePolicySchemaName()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getFieldsList(myarg, CHROME_POLICY_SCHEMA_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
try:
|
|
schema = callGAPI(cp.customers().policySchemas(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
|
|
name=name, fields=fields)
|
|
_showChromePolicySchema(schema, FJQC, 0, 0)
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.CHROME_POLICY_SCHEMA, name)
|
|
except (GAPI.badRequest, GAPI.forbidden):
|
|
accessErrorExit(None)
|
|
|
|
# gam show chromeschemas
|
|
# [filter <String>]
|
|
# <ChromePolicySchemaFieldName>* [fields <ChromePolicySchemaFieldNameList>]
|
|
# [formatjson]
|
|
# gam print chromschemas [todrive <ToDriveAttribute>*]
|
|
# [filter <String>]
|
|
# <ChromePolicySchemaFieldName>* [fields <ChromePolicySchemaFieldNameList>]
|
|
# [[formatjson [quotechar <Character>]]
|
|
def doPrintShowChromeSchemas():
|
|
def _printChromePolicySchema(schema):
|
|
row = flattenJSON(schema)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(row):
|
|
row = {'name': schema['name']}
|
|
if 'policyDescription' in schema:
|
|
row['policyDescription'] = schema['policyDescription']
|
|
if 'policyApiLifecycle' in schema:
|
|
row['policyApiLifecycleStage'] = schema['policyApiLifecycle'].get('policyApiLifecycleStage', '')
|
|
row['JSON'] = json.dumps(cleanJSON(schema), ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
if checkArgumentPresent('std'):
|
|
doShowChromeSchemasStd()
|
|
return
|
|
cp = buildGAPIObject(API.CHROMEPOLICY)
|
|
parent = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile(['name', 'schemaName', 'policyDescription',
|
|
'policyApiLifecycle.policyApiLifecycleStage',
|
|
'policyApiLifecycle.description',
|
|
'policyApiLifecycle.endSupport.year',
|
|
'policyApiLifecycle.endSupport.month',
|
|
'policyApiLifecycle.endSupport.day',
|
|
'policyApiLifecycle.deprecatedInFavorOf',
|
|
'policyApiLifecycle.deprecatedInFavorOf.0'],
|
|
'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
pfilter = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'filter':
|
|
pfilter = getString(Cmd.OB_STRING)
|
|
elif getFieldsList(myarg, CHROME_POLICY_SCHEMA_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and FJQC.formatJSON:
|
|
jsonTitles = ['name']
|
|
if not fieldsList or 'policyDescription' in fieldsList:
|
|
jsonTitles.append('policyDescription')
|
|
if not fieldsList or 'policyApiLifecycle' in fieldsList:
|
|
jsonTitles.append('policyApiLifecycleStage')
|
|
jsonTitles.append('JSON')
|
|
csvPF.SetJSONTitles(jsonTitles)
|
|
fields = getItemFieldsFromFieldsList('policySchemas', fieldsList)
|
|
printGettingAllAccountEntities(Ent.CHROME_POLICY_SCHEMA, pfilter)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
schemas = callGAPIpages(cp.customers().policySchemas(), 'list', 'policySchemas',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
|
|
parent=parent, filter=pfilter, fields=fields)
|
|
if not csvPF:
|
|
jcount = len(schemas)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, Ent.CHROME_POLICY_SCHEMA)
|
|
Ind.Increment()
|
|
j = 0
|
|
for schema in schemas:
|
|
j += 1
|
|
_showChromePolicySchema(schema, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for schema in schemas:
|
|
_printChromePolicySchema(schema)
|
|
except GAPI.invalidInput as e:
|
|
entityActionFailedWarning([Ent.CHROME_POLICY, None], invalidQuery(pfilter) if pfilter else str(e))
|
|
except (GAPI.badRequest, GAPI.forbidden):
|
|
accessErrorExit(None)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chrome Policy Schemas')
|
|
|
|
# gam show chromeschemas std [filter <String>]
|
|
def doShowChromeSchemasStd():
|
|
cp = buildGAPIObject(API.CHROMEPOLICY)
|
|
sfilter = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'filter':
|
|
sfilter = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
parent = _getCustomersCustomerIdWithC()
|
|
result = callGAPIpages(cp.customers().policySchemas(), 'list', 'policySchemas',
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
parent=parent, filter=sfilter)
|
|
schemas = {}
|
|
for schema in result:
|
|
schema_name, schema_dict = simplifyChromeSchema(schema)
|
|
schemas[schema_name.lower()] = schema_dict
|
|
for _, value in sorted(iter(schemas.items())):
|
|
printKeyValueList([f'{value.get("name")}', f'{value.get("description")}'])
|
|
Ind.Increment()
|
|
for val in value['settings'].values():
|
|
vtype = val.get('type')
|
|
printKeyValueList([f'{val.get("name")}', f'{vtype}'])
|
|
Ind.Increment()
|
|
if vtype == 'TYPE_ENUM':
|
|
enums = val.get('enums', [])
|
|
descriptions = val.get('descriptions', [])
|
|
for i in range(len(val.get('enums', []))):
|
|
printKeyValueList([f'{enums[i]}', f'{descriptions[i]}'])
|
|
elif vtype == 'TYPE_BOOL':
|
|
pvs = val.get('descriptions')
|
|
for pvi in pvs:
|
|
if isinstance(pvi, dict):
|
|
pvalue = pvi.get('value')
|
|
pdescription = pvi.get('description')
|
|
printKeyValueList([f'{pvalue}', f'{pdescription}'])
|
|
elif isinstance(pvi, list):
|
|
printKeyValueList([f'{pvi[0]}'])
|
|
else:
|
|
description = val.get('descriptions')
|
|
if len(description) > 0:
|
|
printKeyValueList([f'{description[0]}'])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
printBlankLine()
|
|
|
|
# gam create chromenetwork
|
|
# <OrgUnitItem> <String> <JSONData>
|
|
def doCreateChromeNetwork():
|
|
cp = buildGAPIObject(API.CHROMEPOLICY)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
customer = _getCustomersCustomerIdWithC()
|
|
body = {}
|
|
orgUnitPath, body['targetResource'] = _getOrgunitsOrgUnitIdPath(cd, getString(Cmd.OB_ORGUNIT_PATH))
|
|
body['name'] = getString(Cmd.OB_STRING)
|
|
body.update(getJSON(['direct', 'name', 'orgUnitPath', 'parentOrgUnitPath', 'group']))
|
|
checkForExtraneousArguments()
|
|
kvList = [Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CHROME_NETWORK_NAME, body['name']]
|
|
try:
|
|
result = callGAPI(cp.customers().policies().networks(), 'defineNetwork',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT,
|
|
GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customer, body=body)
|
|
entityActionPerformed(kvList+[Ent.CHROME_NETWORK_ID, result['networkId']])
|
|
except (GAPI.alreadyExists, GAPI.permissionDenied, GAPI.invalidArgument,
|
|
GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvList, str(e))
|
|
|
|
# gam delete chromenetwork
|
|
# <OrgUnitItem> <NetworkID>
|
|
def doDeleteChromeNetwork():
|
|
cp = buildGAPIObject(API.CHROMEPOLICY)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
customer = _getCustomersCustomerIdWithC()
|
|
body = {}
|
|
orgUnitPath, body['targetResource'] = _getOrgunitsOrgUnitIdPath(cd, getString(Cmd.OB_ORGUNIT_PATH))
|
|
body['networkId'] = getString(Cmd.OB_NETWORK_ID)
|
|
checkForExtraneousArguments()
|
|
kvList = [Ent.ORGANIZATIONAL_UNIT, orgUnitPath, Ent.CHROME_NETWORK_ID, body['networkId']]
|
|
try:
|
|
callGAPI(cp.customers().policies().networks(), 'removeNetwork',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED,
|
|
GAPI.INVALID_ARGUMENT, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customer, body=body)
|
|
entityActionPerformed(kvList)
|
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvList, str(e))
|
|
|
|
# Device command utilities
|
|
def buildGAPICIDeviceServiceObject():
|
|
if not GC.Values[GC.ENABLE_DASA]:
|
|
_, ci = buildGAPIServiceObject(API.CLOUDIDENTITY_DEVICES, _getAdminEmail(), displayError=True)
|
|
else:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_DEVICES)
|
|
if not ci:
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
return ci
|
|
|
|
def getUpdateDeleteCIDeviceOptions(entityType, count, action, doit, actionChoices):
|
|
kwargs = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if not action and myarg == 'action':
|
|
action = getChoice(actionChoices)
|
|
elif action == 'wipe' and myarg == 'removeresetlock':
|
|
kwargs = {'body': {'removeResetLock': True}}
|
|
elif myarg == 'doit':
|
|
doit = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not action:
|
|
actionNotPerformedNumItemsWarning(count, entityType, Msg.NO_ACTION_SPECIFIED)
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
if not doit:
|
|
actionNotPerformedNumItemsWarning(count, entityType, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION)
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
return action, kwargs
|
|
|
|
def getCIDeviceEntity():
|
|
ci = buildGAPICIDeviceServiceObject()
|
|
customer = _getCustomersCustomerIdNoC()
|
|
if checkArgumentPresent('devicesn'):
|
|
query = f'serial:{getString(Cmd.OB_SERIAL_NUMBER)}'
|
|
elif checkArgumentPresent('query'):
|
|
query = getString(Cmd.OB_QUERY)
|
|
else:
|
|
name = getString(Cmd.OB_DEVICE_ENTITY)
|
|
if name[:6].lower() == 'query:':
|
|
query = name[6:]
|
|
else:
|
|
if name.lower() in {'id', 'name'}:
|
|
name = getString(Cmd.OB_DEVICE_ID)
|
|
if not name.startswith('devices/'):
|
|
name = f'devices/{name}'
|
|
return ([{'name': name}], ci, customer, True)
|
|
printGettingAllAccountEntities(Ent.DEVICE, query)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
devices = callGAPIpages(ci.devices(), 'list', 'devices',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageMessage=pageMessage,
|
|
customer=customer, filter=query,
|
|
fields='nextPageToken,devices(name)', pageSize=100)
|
|
return (devices, ci, customer, False)
|
|
except (GAPI.invalid, GAPI.invalidArgument):
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_QUERY)
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedWarning([Ent.DEVICE, None], str(e))
|
|
return ([], ci, customer, False)
|
|
|
|
DEVICE_USERNAME_PATTERN = re.compile(r'^(devices/.+)/(deviceUsers/.+)$')
|
|
DEVICE_USERNAME_FORMAT_REQUIRED = 'devices/<String>/deviceUsers/<String>'
|
|
def getCIDeviceUserEntity():
|
|
ci = buildGAPICIDeviceServiceObject()
|
|
customer = _getCustomersCustomerIdNoC()
|
|
if checkArgumentPresent('query'):
|
|
query = getString(Cmd.OB_QUERY)
|
|
else:
|
|
name = getString(Cmd.OB_DEVICE_USER_ENTITY)
|
|
if name[:6].lower() == 'query:':
|
|
query = name[6:]
|
|
else:
|
|
if name.lower() in {'id', 'name'}:
|
|
name = getString(Cmd.OB_DEVICE_USER_ID)
|
|
if DEVICE_USERNAME_PATTERN.match(name):
|
|
return ([{'name': name}], ci, customer, True)
|
|
Cmd.Backup()
|
|
invalidArgumentExit(DEVICE_USERNAME_FORMAT_REQUIRED)
|
|
printGettingAllAccountEntities(Ent.DEVICE_USER, query)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
deviceUsers = callGAPIpages(ci.devices().deviceUsers(), 'list', 'deviceUsers',
|
|
throwReasons=[GAPI.INVALID, GAPI.PERMISSION_DENIED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageMessage=pageMessage,
|
|
customer=customer, filter=query, parent='devices/-',
|
|
fields='nextPageToken,deviceUsers(name)', pageSize=20)
|
|
return (deviceUsers, ci, customer, False)
|
|
except GAPI.invalid:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_QUERY)
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedWarning([Ent.DEVICE_USER, None], str(e))
|
|
return ([], ci, customer, False)
|
|
|
|
def _makeDeviceId(name, device):
|
|
deviceId = f'{name}'
|
|
for field in ['deviceType', 'serialNumber', 'assetTag']:
|
|
if field in device:
|
|
deviceId += f', {field}: {device[field]}'
|
|
return deviceId
|
|
|
|
DEVICE_TYPE_MAP = {
|
|
'android': 'ANDROID',
|
|
'chromeos': 'CHROME_OS',
|
|
'googlesync': 'GOOGLE_SYNC',
|
|
'ios': 'IOS',
|
|
'linux': 'LINUX',
|
|
'macos': 'MAC_OS',
|
|
'windows': 'WINDOWS'
|
|
}
|
|
|
|
DEVICE_TIME_OBJECTS = {'createTime', 'firstSyncTime', 'lastSyncTime', 'lastUpdateTime', 'securityPatchTime'}
|
|
|
|
# gam create device serialnumber <String> devicetype <DeviceType> [assettag <String>]
|
|
def doCreateCIDevice():
|
|
ci = buildGAPICIDeviceServiceObject()
|
|
customer = _getCustomersCustomerIdNoC()
|
|
body = {'deviceType': '', 'serialNumber': ''}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'serialnumber':
|
|
body['serialNumber'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'devicetype':
|
|
body['deviceType'] = getChoice(DEVICE_TYPE_MAP, mapChoice=True)
|
|
elif myarg in {'assettag', 'assteid'}:
|
|
body['assetTag'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not body['serialNumber']:
|
|
missingArgumentExit('serialnumber')
|
|
if not body['deviceType']:
|
|
missingArgumentExit('devicetype')
|
|
try:
|
|
result = callGAPI(ci.devices(), 'create',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
|
|
customer=customer, body=body)
|
|
if 'response' in result:
|
|
entityActionPerformed([Ent.COMPANY_DEVICE, _makeDeviceId(f'{result["response"]["name"]}', body)])
|
|
else:
|
|
entityActionFailedWarning([Ent.COMPANY_DEVICE, _makeDeviceId('/devices/???', body)], result['error']['message'])
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.alreadyExists) as e:
|
|
entityActionFailedWarning([Ent.COMPANY_DEVICE, _makeDeviceId('/devices/???', body)], str(e))
|
|
|
|
DEVICE_ACTION_CHOICES = {'cancelwipe', 'wipe'}
|
|
|
|
def _performCIDeviceAction(action):
|
|
entityList, ci, customer, doit = getCIDeviceEntity()
|
|
count = len(entityList)
|
|
action, kwargs = getUpdateDeleteCIDeviceOptions(Ent.DEVICE, count, action, doit, DEVICE_ACTION_CHOICES)
|
|
if action == 'delete':
|
|
kwargs['customer'] = customer
|
|
else:
|
|
kwargs.setdefault('body', {})
|
|
kwargs['body']['customer'] = customer
|
|
i = 0
|
|
for device in entityList:
|
|
i += 1
|
|
name = device['name']
|
|
try:
|
|
result = callGAPI(ci.devices(), action,
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
name=name, **kwargs)
|
|
if result['done']:
|
|
if 'error' not in result:
|
|
entityActionPerformed([Ent.DEVICE, name], i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.DEVICE, name], result['error']['message'], i, count)
|
|
else:
|
|
entityActionPerformedMessage([Ent.DEVICE, name], Msg.ACTION_IN_PROGRESS.format(action), i, count)
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.DEVICE, f'{name}', i, count)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.DEVICE, f'{name}'], str(e), i, count)
|
|
|
|
# gam delete device <DeviceEntity> [doit]
|
|
def doDeleteCIDevice():
|
|
_performCIDeviceAction('delete')
|
|
|
|
# gam cancelwipe device <DeviceEntity> [doit]
|
|
def doCancelWipeCIDevice():
|
|
_performCIDeviceAction('cancelWipe')
|
|
|
|
# gam wipe device <DeviceEntity> [removeresetlock] [doit]
|
|
def doWipeCIDevice():
|
|
_performCIDeviceAction('wipe')
|
|
|
|
# gam update device <DeviceEntity> action <DeviceAction> [removeresetlock] [doit]
|
|
def doUpdateCIDevice():
|
|
_performCIDeviceAction(None)
|
|
|
|
DEVICE_MISSING_ACTION_MAP = {
|
|
'delete': 'delete',
|
|
'wipe': 'wipe',
|
|
'donothing': 'none',
|
|
'none': 'none',
|
|
}
|
|
|
|
# gam sync devices
|
|
# <CSVFileSelector>
|
|
# [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
|
|
# (devicetype_column <String>)|(static_devicetype <DeviceType>)
|
|
# (serialnumber_column <String>)
|
|
# [assettag_column <String>]
|
|
# [unassigned_missing_action delete|wipe|none|donothing]
|
|
# [assigned_missing_action delete|wipe|none|donothing]
|
|
# [preview]
|
|
def doSyncCIDevices():
|
|
ci = buildGAPICIDeviceServiceObject()
|
|
customer = _getCustomersCustomerIdNoC()
|
|
queryTimes = {}
|
|
queries = [None]
|
|
filename = None
|
|
serialNumberColumn = 'serialNumber'
|
|
deviceTypeColumn = 'deviceType'
|
|
assetTagColumn = None
|
|
staticDeviceType = None
|
|
fieldsList = ['serialNumber', 'deviceType', 'lastSyncTime', 'name']
|
|
unassignedMissingAction = 'delete'
|
|
assignedMissingAction = 'none'
|
|
preview = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in ['filter', 'filters', 'query', 'queries']:
|
|
queries = getQueries(myarg)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
|
|
elif myarg in {'csv', 'csvfile'}:
|
|
filename = getString(Cmd.OB_STRING)
|
|
elif myarg == 'serialnumbercolumn':
|
|
serialNumberColumn = getString(Cmd.OB_STRING)
|
|
elif myarg == 'devicetypecolumn':
|
|
deviceTypeColumn = getString(Cmd.OB_STRING)
|
|
elif myarg in {'assettagcolumn', 'assetidcolumn'}:
|
|
assetTagColumn = getString(Cmd.OB_STRING)
|
|
fieldsList.append('assetTag')
|
|
elif myarg == 'staticdevicetype':
|
|
staticDeviceType = getChoice(DEVICE_TYPE_MAP, mapChoice=True)
|
|
elif myarg == 'unassignedmissingaction':
|
|
unassignedMissingAction = getChoice(DEVICE_MISSING_ACTION_MAP, mapChoice=True)
|
|
elif myarg == 'assignedmissingaction':
|
|
assignedMissingAction = getChoice(DEVICE_MISSING_ACTION_MAP, mapChoice=True)
|
|
elif myarg == 'preview':
|
|
preview = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not filename:
|
|
missingArgumentExit('csvfile')
|
|
f, csvFile, fieldnames = openCSVFileReader(filename)
|
|
if serialNumberColumn not in fieldnames:
|
|
csvFieldErrorExit(serialNumberColumn, fieldnames)
|
|
if not staticDeviceType and deviceTypeColumn not in fieldnames:
|
|
csvFieldErrorExit(deviceTypeColumn, fieldnames)
|
|
if assetTagColumn and assetTagColumn not in fieldnames:
|
|
csvFieldErrorExit(assetTagColumn, fieldnames)
|
|
localDevices = {}
|
|
for row in csvFile:
|
|
# upper() is very important to comparison since Google
|
|
# always return uppercase serials
|
|
localDevice = {'serialNumber': row[serialNumberColumn].strip().upper()}
|
|
if staticDeviceType:
|
|
localDevice['deviceType'] = staticDeviceType
|
|
else:
|
|
dt = row[deviceTypeColumn].strip()
|
|
localDevice['deviceType'] = DEVICE_TYPE_MAP.get(dt.lower().replace('_', '').replace('-', ''), dt.upper())
|
|
sndt = f"{localDevice['serialNumber']}-{localDevice['deviceType']}"
|
|
if assetTagColumn:
|
|
localDevice['assetTag'] = row[assetTagColumn].strip()
|
|
sndt += f"-{localDevice['assetTag']}"
|
|
localDevices[sndt] = localDevice
|
|
closeFile(f)
|
|
fields = f'nextPageToken,devices({",".join(fieldsList)})'
|
|
remoteDevices = {}
|
|
remoteDeviceMap = {}
|
|
substituteQueryTimes(queries, queryTimes)
|
|
for query in queries:
|
|
printGettingAllAccountEntities(Ent.COMPANY_DEVICE, query)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
result = callGAPIpages(ci.devices(), 'list', 'devices',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageMessage=pageMessage,
|
|
customer=customer, filter=query, view='COMPANY_INVENTORY',
|
|
fields=fields, pageSize=100)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.COMPANY_DEVICE, None], str(e))
|
|
return
|
|
for remoteDevice in result:
|
|
sn = remoteDevice['serialNumber']
|
|
last_sync = remoteDevice.pop('lastSyncTime', NEVER_TIME_NOMS)
|
|
name = remoteDevice.pop('name')
|
|
sndt = f"{remoteDevice['serialNumber']}-{remoteDevice['deviceType']}"
|
|
if assetTagColumn:
|
|
if 'assetTag' not in remoteDevice:
|
|
remoteDevice['assetTag'] = ''
|
|
sndt += f"-{remoteDevice['assetTag']}"
|
|
remoteDevices[sndt] = remoteDevice
|
|
remoteDeviceMap[sndt] = {'name': name}
|
|
if last_sync == NEVER_TIME_NOMS:
|
|
remoteDeviceMap[sndt]['unassigned'] = True
|
|
devicesToAdd = []
|
|
for sndt, device in iter(localDevices.items()):
|
|
if sndt not in remoteDevices:
|
|
devicesToAdd.append(device)
|
|
missingDevices = []
|
|
for sndt, device in iter(remoteDevices.items()):
|
|
if sndt not in localDevices:
|
|
missingDevices.append(device)
|
|
Act.Set([Act.CREATE, Act.CREATE_PREVIEW][preview])
|
|
count = len(devicesToAdd)
|
|
performActionNumItems(count, Ent.COMPANY_DEVICE)
|
|
i = 0
|
|
for device in devicesToAdd:
|
|
i += 1
|
|
try:
|
|
if not preview:
|
|
result = callGAPI(ci.devices(), 'create',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
|
|
customer=customer, body=device)
|
|
if 'error' in result:
|
|
entityActionFailedWarning([Ent.COMPANY_DEVICE, _makeDeviceId('/devices/???', device)], result['error']['message'], i, count)
|
|
continue
|
|
deviceId = _makeDeviceId(f'{result["response"]["name"]}', device)
|
|
else:
|
|
deviceId = _makeDeviceId('/devices/???', device)
|
|
entityActionPerformed([Ent.COMPANY_DEVICE, deviceId], i, count)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.alreadyExists) as e:
|
|
entityActionFailedWarning([Ent.COMPANY_DEVICE, _makeDeviceId('/devices/???', device)], str(e), i, count)
|
|
Act.Set([Act.PROCESS, Act.PROCESS_PREVIEW][preview])
|
|
count = len(missingDevices)
|
|
performActionNumItems(count, Ent.COMPANY_DEVICE)
|
|
i = 0
|
|
for device in missingDevices:
|
|
i += 1
|
|
sn = device['serialNumber']
|
|
sndt = f"{sn}-{device['deviceType']}"
|
|
if assetTagColumn:
|
|
sndt += f"-{device['assetTag']}"
|
|
name = remoteDeviceMap[sndt]['name']
|
|
deviceId = _makeDeviceId(f'{name}', device)
|
|
unassigned = remoteDeviceMap[sndt].get('unassigned')
|
|
action = unassignedMissingAction if unassigned else assignedMissingAction
|
|
if action == 'none':
|
|
Act.Set([Act.NOACTION, Act.NOACTION_PREVIEW][preview])
|
|
entityActionPerformed([Ent.COMPANY_DEVICE, deviceId], i, count)
|
|
continue
|
|
if action == 'delete':
|
|
Act.Set([Act.DELETE, Act.DELETE_PREVIEW][preview])
|
|
kwargs = {'customer': customer}
|
|
else:
|
|
Act.Set([Act.WIPE, Act.WIPE_PREVIEW][preview])
|
|
kwargs = {'body': {'customer': customer}}
|
|
try:
|
|
if not preview:
|
|
result = callGAPI(ci.devices(), action,
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
name=name, **kwargs)
|
|
else:
|
|
result = {'done': True}
|
|
if result['done']:
|
|
if 'error' not in result:
|
|
entityActionPerformed([Ent.COMPANY_DEVICE, deviceId], i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.COMPANY_DEVICE, deviceId], result['error']['message'], i, count)
|
|
else:
|
|
entityActionPerformedMessage([Ent.COMPANY_DEVICE, deviceId], Msg.ACTION_IN_PROGRESS.format(action), i, count)
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.COMPANY_DEVICE, deviceId, i, count)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.COMPANY_DEVICE, deviceId], str(e), i, count)
|
|
|
|
DEVICE_FIELDS_CHOICE_MAP = {
|
|
'androidspecificattributes': 'androidSpecificAttributes',
|
|
'assettag': 'assetTag',
|
|
'basebandversion': 'basebandVersion',
|
|
'bootloaderversion': 'bootloaderVersion',
|
|
'brand': 'brand',
|
|
'buildnumber': 'buildNumber',
|
|
'compromisedstate': 'compromisedState',
|
|
'createtime': 'createTime',
|
|
'deviceid': 'deviceId',
|
|
'devicetype': 'deviceType',
|
|
'enableddeveloperoptions': 'enabledDeveloperOptions',
|
|
'enabledusbdebugging': 'enabledUsbDebugging',
|
|
'encryptionstate': 'encryptionState',
|
|
'endpointverificationspecificattributes': 'endpointVerificationSpecificAttributes',
|
|
'hostname': 'hostname',
|
|
'imei': 'imei',
|
|
'kernelversion': 'kernelVersion',
|
|
'lastsynctime': 'lastSyncTime',
|
|
'managementstate': 'managementState',
|
|
'manufacturer': 'manufacturer',
|
|
'meid': 'meid',
|
|
'model': 'model',
|
|
'name': 'name',
|
|
'networkoperator': 'networkOperator',
|
|
'osversion': 'osVersion',
|
|
'otheraccounts': 'otherAccounts',
|
|
'ownertype': 'ownerType',
|
|
'releaseversion': 'releaseVersion',
|
|
'securitypatchtime': 'securityPatchTime',
|
|
'serialnumber': 'serialNumber',
|
|
'unifieddeviceid': 'unifiedDeviceId',
|
|
'wifimacaddresses': 'wifiMacAddresses'
|
|
}
|
|
|
|
DEVICEUSER_FIELDS_CHOICE_MAP = {
|
|
'compromisedstate': 'compromisedState',
|
|
'createtime': 'createTime',
|
|
'firstsynctime': 'firstSyncTime',
|
|
'languagecode': 'languageCode',
|
|
'lastsynctime': 'lastSyncTime',
|
|
'managementstate': 'managementState',
|
|
'name': 'name',
|
|
'passwordstate': 'passwordState',
|
|
'useragent': 'userAgent',
|
|
'useremail': 'userEmail',
|
|
}
|
|
|
|
# gam info device <DeviceEntity>
|
|
# <DeviceFieldName>* [fields <DevieFieldNameList>] [userfields <DeviceUserFieldNameList>]
|
|
# [nodeviceusers]
|
|
# [formatjson]
|
|
def doInfoCIDevice():
|
|
entityList, ci, customer, _ = getCIDeviceEntity()
|
|
FJQC = FormatJSONQuoteChar()
|
|
fieldsList = []
|
|
userFieldsList = []
|
|
getDeviceUsers = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getFieldsList(myarg, DEVICE_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
|
|
pass
|
|
elif getFieldsList(myarg, DEVICEUSER_FIELDS_CHOICE_MAP, userFieldsList, initialField='name', fieldsArg='userfields'):
|
|
pass
|
|
elif myarg == 'nodeviceusers':
|
|
getDeviceUsers = False
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
userFields = getFieldsFromFieldsList(userFieldsList)
|
|
i = 0
|
|
count = len(entityList)
|
|
for device in entityList:
|
|
i += 1
|
|
name = device['name']
|
|
try:
|
|
device = callGAPI(ci.devices(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name, customer=customer, fields=fields)
|
|
if getDeviceUsers:
|
|
device_users = callGAPIpages(ci.devices().deviceUsers(), 'list', 'deviceUsers',
|
|
throwReasons=[GAPI.INVALID, GAPI.PERMISSION_DENIED],
|
|
parent=name, customer=customer, fields=userFields)
|
|
for device_user in device_users:
|
|
device_user['client_states'] = callGAPIpages(ci.devices().deviceUsers().clientStates(), 'list', 'clientStates',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
parent=device_user['name'], customer=customer)
|
|
else:
|
|
device_users = []
|
|
if FJQC.formatJSON:
|
|
if getDeviceUsers:
|
|
device['users'] = device_users
|
|
printLine(json.dumps(cleanJSON(device, timeObjects=DEVICE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
printEntity([Ent.DEVICE, device.pop('name')])
|
|
Ind.Increment()
|
|
showJSON(None, device, timeObjects=DEVICE_TIME_OBJECTS)
|
|
count = len(device_users)
|
|
i = 0
|
|
for device_user in device_users:
|
|
i += 1
|
|
printEntity([Ent.DEVICE_USER, device_user.pop('name')], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, device_user, timeObjects=DEVICE_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.DEVICE, f'{name}')
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.DEVICE, f'{name}'], str(e))
|
|
|
|
DEVICE_VIEW_CHOICE_MAP = {
|
|
'all': (None, Ent.DEVICE),
|
|
'company': ('COMPANY_INVENTORY', Ent.COMPANY_DEVICE),
|
|
'personal': ('USER_ASSIGNED_DEVICES', Ent.PERSONAL_DEVICE),
|
|
'nocompanydevices': ('USER_ASSIGNED_DEVICES', Ent.PERSONAL_DEVICE),
|
|
'nopersonaldevices': ('COMPANY_INVENTORY', Ent.COMPANY_DEVICE)
|
|
}
|
|
DEVICE_ORDERBY_CHOICE_MAP = {
|
|
'createtime': 'create_time',
|
|
'devicetype': 'device_type',
|
|
'lastsynctime': 'last_sync_time',
|
|
'model': 'model',
|
|
'osversion': 'os_version',
|
|
'serialnumber': 'serial_number'
|
|
}
|
|
|
|
# gam print devices [todrive <ToDriveAttribute>*]
|
|
# [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
|
|
# <DeviceFieldName>* [fields <DeviceFieldNameList>] [userfields <DeviceUserFieldNameList>]
|
|
# [orderby <DeviceOrderByFieldName> [ascending|descending]]
|
|
# [all|company|personal|nocompanydevices|nopersonaldevices]
|
|
# [nodeviceusers]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [showitemcountonly]
|
|
def doPrintCIDevices():
|
|
ci = buildGAPICIDeviceServiceObject()
|
|
customer = _getCustomersCustomerIdNoC()
|
|
parent = 'devices/-'
|
|
csvPF = CSVPrintFile(['name'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
OBY = OrderBy(DEVICE_ORDERBY_CHOICE_MAP)
|
|
fieldsList = []
|
|
userFieldsList = []
|
|
queryTimes = {}
|
|
queries = [None]
|
|
view, entityType = DEVICE_VIEW_CHOICE_MAP['all']
|
|
getDeviceUsers = True
|
|
showItemCountOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ['filter', 'filters', 'query', 'queries']:
|
|
queries = getQueries(myarg)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg in DEVICE_VIEW_CHOICE_MAP:
|
|
view, entityType = DEVICE_VIEW_CHOICE_MAP[myarg]
|
|
elif myarg == 'nodeviceusers':
|
|
getDeviceUsers = False
|
|
elif getFieldsList(myarg, DEVICE_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
|
|
pass
|
|
elif getFieldsList(myarg, DEVICEUSER_FIELDS_CHOICE_MAP, userFieldsList, initialField='name', fieldsArg='userfields'):
|
|
pass
|
|
elif myarg == 'sortheaders':
|
|
pass
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
fields = getItemFieldsFromFieldsList('devices', fieldsList)
|
|
userFields = getItemFieldsFromFieldsList('deviceUsers', userFieldsList)
|
|
substituteQueryTimes(queries, queryTimes)
|
|
itemCount = 0
|
|
for query in queries:
|
|
printGettingAllAccountEntities(entityType, query)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
devices = callGAPIpages(ci.devices(), 'list', 'devices',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customer, filter=query,
|
|
orderBy=OBY.orderBy, view=view, fields=fields, pageSize=100)
|
|
if showItemCountOnly:
|
|
itemCount += len(devices)
|
|
continue
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([entityType, None], str(e))
|
|
continue
|
|
if getDeviceUsers:
|
|
ci._http.credentials.refresh(transportCreateRequest())
|
|
deviceDict = {}
|
|
for device in devices:
|
|
deviceDict[device['name']] = device
|
|
printGettingAllAccountEntities(Ent.DEVICE_USER, query)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
deviceUsers = callGAPIpages(ci.devices().deviceUsers(), 'list', 'deviceUsers',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customer, filter=query, parent=parent,
|
|
orderBy=OBY.orderBy, fields=userFields, pageSize=20)
|
|
for deviceUser in deviceUsers:
|
|
mg = DEVICE_USERNAME_PATTERN.match(deviceUser['name'])
|
|
if mg:
|
|
deviceName = mg.group(1)
|
|
if deviceName in deviceDict:
|
|
deviceDict[deviceName].setdefault('users', [])
|
|
deviceDict[deviceName]['users'].append(deviceUser)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([entityType, None], str(e))
|
|
for device in devices:
|
|
row = flattenJSON(device, timeObjects=DEVICE_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'name': device['name'],
|
|
'JSON': json.dumps(cleanJSON(device, timeObjects=DEVICE_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
if showItemCountOnly:
|
|
writeStdout(f'{itemCount}\n')
|
|
return
|
|
csvPF.writeCSVfile('Devices')
|
|
|
|
DEVICE_USER_ACTION_CHOICES = {'approve', 'block', 'cancelwipe', 'wipe'}
|
|
|
|
def _performCIDeviceUserAction(action):
|
|
entityList, ci, customer, doit = getCIDeviceUserEntity()
|
|
count = len(entityList)
|
|
action, kwargs = getUpdateDeleteCIDeviceOptions(Ent.DEVICE_USER, count, action, doit, DEVICE_USER_ACTION_CHOICES)
|
|
if action == 'delete':
|
|
kwargs['customer'] = customer
|
|
else:
|
|
kwargs.setdefault('body', {})
|
|
kwargs['body']['customer'] = customer
|
|
i = 0
|
|
for deviceUser in entityList:
|
|
i += 1
|
|
name = deviceUser['name']
|
|
try:
|
|
result = callGAPI(ci.devices().deviceUsers(), action,
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
name=name, **kwargs)
|
|
if result['done']:
|
|
if 'error' not in result:
|
|
entityActionPerformed([Ent.DEVICE_USER, name], i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.DEVICE_USER, name], result['error']['message'], i, count)
|
|
else:
|
|
entityActionPerformedMessage([Ent.DEVICE_USER, name], Msg.ACTION_IN_PROGRESS.format(action), i, count)
|
|
Ind.Increment()
|
|
showJSON(None, result, timeObjects=DEVICE_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.DEVICE_USER, f'{name}', i, count)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.DEVICE_USER, f'{name}'], str(e), i, count)
|
|
|
|
# gam approve deviceuser <DeviceUserEntity> [doit]
|
|
def doApproveCIDeviceUser():
|
|
_performCIDeviceUserAction('approve')
|
|
|
|
# gam block deviceuser <DeviceUserEntity> [doit]
|
|
def doBlockCIDeviceUser():
|
|
_performCIDeviceUserAction('block')
|
|
|
|
# gam delete deviceuser <DeviceUserEntity> [doit]
|
|
def doDeleteCIDeviceUser():
|
|
_performCIDeviceUserAction('delete')
|
|
|
|
# gam cancelwipe deviceuser <DeviceUserEntity> [doit]
|
|
def doCancelWipeCIDeviceUser():
|
|
_performCIDeviceUserAction('cancelWipe')
|
|
|
|
# gam wipe deviceuser <DeviceUserEntity> [doit]
|
|
def doWipeCIDeviceUser():
|
|
_performCIDeviceUserAction('wipe')
|
|
|
|
# gam update deviceuser <DeviceUserEntity> action <DeviceUserAction> [doit]
|
|
def doUpdateCIDeviceUser():
|
|
_performCIDeviceUserAction(None)
|
|
|
|
# gam info deviceuser <DeviceUserEntity>
|
|
# <DeviceUserFieldName>* [fields <DevieUserFieldNameList>]
|
|
# [formatjson]
|
|
def doInfoCIDeviceUser():
|
|
entityList, ci, customer, _ = getCIDeviceUserEntity()
|
|
FJQC = FormatJSONQuoteChar()
|
|
userFieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getFieldsList(myarg, DEVICE_FIELDS_CHOICE_MAP, userFieldsList, initialField='name'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
userFields = getFieldsFromFieldsList(userFieldsList)
|
|
i = 0
|
|
count = len(entityList)
|
|
for deviceUser in entityList:
|
|
i += 1
|
|
name = deviceUser['name']
|
|
try:
|
|
deviceUser = callGAPI(ci.devices().deviceUsers(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name, customer=customer, fields=userFields)
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(deviceUser, timeObjects=DEVICE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
printEntity([Ent.DEVICE_USER, deviceUser.pop('name')], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, deviceUser, timeObjects=DEVICE_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.DEVICE_USER, f'{name}', i, count)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.DEVICE_USER, f'{name}'], str(e), i, count)
|
|
|
|
# gam print deviceusers [todrive <ToDriveAttribute>*]
|
|
# [select <DeviceID>]
|
|
# [(query <QueryDevice>)|(queries <QueryDeviceList>) (querytime<String> <Time>)*]
|
|
# <DeviceUserFieldName>* [fields <DevieUserFieldNameList>]
|
|
# [orderby <DeviceOrderByFieldName> [ascending|descending]]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [showitemcountonly]
|
|
def doPrintCIDeviceUsers():
|
|
ci = buildGAPICIDeviceServiceObject()
|
|
customer = _getCustomersCustomerIdNoC()
|
|
csvPF = CSVPrintFile(['name'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
OBY = OrderBy(DEVICE_ORDERBY_CHOICE_MAP)
|
|
userFieldsList = []
|
|
queryTimes = {}
|
|
queries = [None]
|
|
parent = 'devices/-'
|
|
showItemCountOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'select':
|
|
parent = getString(Cmd.OB_DEVICE_ID)
|
|
if not parent.startswith('devices/'):
|
|
parent = f'devices/{parent}'
|
|
elif myarg in ['filter', 'filters', 'query', 'queries']:
|
|
queries = getQueries(myarg)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif getFieldsList(myarg, DEVICEUSER_FIELDS_CHOICE_MAP, userFieldsList, initialField='name'):
|
|
pass
|
|
elif myarg == 'sortheaders':
|
|
pass
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
userFields = getItemFieldsFromFieldsList('deviceUsers', userFieldsList)
|
|
substituteQueryTimes(queries, queryTimes)
|
|
itemCount = 0
|
|
for query in queries:
|
|
printGettingAllAccountEntities(Ent.DEVICE_USER, query)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
deviceUsers = callGAPIpages(ci.devices().deviceUsers(), 'list', 'deviceUsers',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customer, filter=query,
|
|
orderBy=OBY.orderBy, parent=parent, fields=userFields, pageSize=20)
|
|
if showItemCountOnly:
|
|
itemCount += len(deviceUsers)
|
|
continue
|
|
for deviceUser in deviceUsers:
|
|
row = flattenJSON(deviceUser, timeObjects=DEVICE_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'name': deviceUser['name'],
|
|
'JSON': json.dumps(cleanJSON(deviceUser, timeObjects=DEVICE_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
except (GAPI.invalid, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.DEVICE_USER, None], str(e))
|
|
break
|
|
if showItemCountOnly:
|
|
writeStdout(f'{itemCount}\n')
|
|
return
|
|
csvPF.writeCSVfile('Device Users')
|
|
|
|
DEVICE_USER_COMPLIANCE_STATE_CHOICE_MAP = {
|
|
'compliant': 'COMPLIANT',
|
|
'noncompliant': 'NON_COMPLIANT'
|
|
}
|
|
DEVICE_USER_HEALTH_SCORE_CHOICE_MAP = {
|
|
'verypoor': 'VERY_POOR',
|
|
'poor': 'POOR',
|
|
'neutral': 'NEUTRAL',
|
|
'good': 'GOOD',
|
|
'verygood': 'VERY_GOOD'
|
|
}
|
|
DEVICE_USER_MANAGED_STATE_CHOICE_MAP = {
|
|
'clear': None,
|
|
'managed': 'MANAGED',
|
|
'unmanaged': 'UNMANAGED'
|
|
}
|
|
DEVICE_USER_CUSTOM_VALUE_TYPE_CHOICE_MAP = {
|
|
'bool': 'boolValue',
|
|
'boolean': 'boolValue',
|
|
'number': 'numberValue',
|
|
'string': 'stringValue'
|
|
}
|
|
|
|
# gam info deviceuserstate <DeviceUserEntity> [clientid <String>]
|
|
def doInfoCIDeviceUserState():
|
|
setTrueCustomerId()
|
|
entityList, ci, customer, _ = getCIDeviceUserEntity()
|
|
customerID = customer[10:]
|
|
client_id = f'{customerID}-gam'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'clientid':
|
|
client_id = f'{customerID}-{getString(Cmd.OB_STRING)}'
|
|
else:
|
|
unknownArgumentExit()
|
|
count = len(entityList)
|
|
i = 0
|
|
for deviceUser in entityList:
|
|
i += 1
|
|
deviceUser = deviceUser['name']
|
|
name = f'{deviceUser}/clientStates/{client_id}'
|
|
try:
|
|
result = callGAPI(ci.devices().deviceUsers().clientStates(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
name=name, customer=customer)
|
|
printEntity([Ent.DEVICE_USER_CLIENT_STATE, name], i, count)
|
|
Ind.Increment()
|
|
result.pop('name', None)
|
|
showJSON(None, result, timeObjects=DEVICE_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.DEVICE_USER, deviceUser, i, count)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.DEVICE_USER, deviceUser], str(e), i, count)
|
|
|
|
# gam update deviceuserstate <DeviceUserEntity> [clientid <String>]
|
|
# [customid <String>] [assettags clear|<AssetTagList>]
|
|
# [compliantstate|compliancestate compliant|noncompliant] [managedstate clear|managed|unmanaged]
|
|
# [healthscore verypoor|poor|neutral|good|verygood] [scorereason clear|<String>]
|
|
# (customvalue (bool|boolean <Boolean>)|(number <Integer>)(string <String>))*
|
|
def doUpdateCIDeviceUserState():
|
|
setTrueCustomerId()
|
|
entityList, ci, customer, _ = getCIDeviceUserEntity()
|
|
customerID = customer[10:]
|
|
client_id = f'{customerID}-gam'
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'clientid':
|
|
client_id = f'{customerID}-{getString(Cmd.OB_STRING)}'
|
|
elif myarg in ['assettag', 'assettags']:
|
|
body['assetTags'] = convertEntityToList(getString(Cmd.OB_STRING, minLen=0), shlexSplit=True)
|
|
if not body['assetTags'] or body['assetTags'] == ['clear']:
|
|
# TODO: This doesn't work to clear existing values; figure out why.
|
|
body['assetTags'] = [None]
|
|
elif myarg in ['compliantstate', 'compliancestate']:
|
|
body['complianceState'] = getChoice(DEVICE_USER_COMPLIANCE_STATE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'healthscore':
|
|
body['healthScore'] = getChoice(DEVICE_USER_HEALTH_SCORE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg in ['scorereason']:
|
|
body['scoreReason'] = getString(Cmd.OB_STRING)
|
|
if body['scoreReason'] == 'clear':
|
|
body['scoreReason'] = None
|
|
elif myarg == 'customid':
|
|
body['customId'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'customvalue':
|
|
valueType = getChoice(DEVICE_USER_CUSTOM_VALUE_TYPE_CHOICE_MAP, mapChoice=True)
|
|
key = getString(Cmd.OB_STRING)
|
|
if valueType == 'boolValue':
|
|
value = getBoolean()
|
|
elif valueType == 'numberValue':
|
|
value = getInteger()
|
|
else: # stringValue
|
|
value = getString(Cmd.OB_STRING)
|
|
body.setdefault('keyValuePairs', {})
|
|
body['keyValuePairs'][key] = {valueType: value}
|
|
elif myarg == 'managedstate':
|
|
body['managed'] = getChoice(DEVICE_USER_MANAGED_STATE_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
unknownArgumentExit()
|
|
count = len(entityList)
|
|
i = 0
|
|
for deviceUser in entityList:
|
|
i += 1
|
|
deviceUser = deviceUser['name']
|
|
name = f'{deviceUser}/clientStates/{client_id}'
|
|
try:
|
|
result = callGAPI(ci.devices().deviceUsers().clientStates(), 'patch',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
name=name, customer=customer, updateMask=','.join(body.keys()), body=body)
|
|
if result['done']:
|
|
if 'error' not in result:
|
|
entityActionPerformed([Ent.DEVICE_USER_CLIENT_STATE, name], i, count)
|
|
result = result['response']
|
|
result.pop('name')
|
|
else:
|
|
entityActionFailedWarning([Ent.DEVICE_USER_CLIENT_STATE, name], result['error']['message'], i, count)
|
|
else:
|
|
entityActionPerformedMessage([Ent.DEVICE_USER_CLIENT_STATE, name], Msg.ACTION_IN_PROGRESS.format('update Client State'), i, count)
|
|
Ind.Increment()
|
|
showJSON(None, result, timeObjects=DEVICE_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.DEVICE_USER, deviceUser)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.DEVICE_USER, deviceUser], str(e))
|
|
|
|
def isolatePrinterID(name):
|
|
''' converts long name into simple ID'''
|
|
return name.split('/')[-1]
|
|
|
|
def _getPrinterID():
|
|
cd = buildGAPIObject(API.PRINTERS)
|
|
customer = _getCustomersCustomerIdWithC()
|
|
printerId = getString(Cmd.OB_PRINTER_ID)
|
|
pattern = re.compile(rf'^{customer}/chrome/printers/(.+)$')
|
|
mg = pattern.match(printerId)
|
|
if mg:
|
|
return (printerId, mg.group(1), cd)
|
|
return (f'{customer}/chrome/printers/{printerId}', printerId, cd)
|
|
|
|
def _getPrinterEntity():
|
|
cd = buildGAPIObject(API.PRINTERS)
|
|
customer = _getCustomersCustomerIdWithC()
|
|
printerId = getString(Cmd.OB_PRINTER_ID)
|
|
entitySelector = printerId.lower()
|
|
if entitySelector == Cmd.ENTITY_SELECTOR_FILE:
|
|
printerIds = getEntitiesFromFile(False)
|
|
elif entitySelector == Cmd.ENTITY_SELECTOR_CSVFILE:
|
|
printerIds = getEntitiesFromCSVFile(False)
|
|
else:
|
|
printerIds = printerId.replace(',', ' ').split()
|
|
pattern = re.compile(rf'^{customer}/chrome/printers/(.+)$')
|
|
for i, printerId in enumerate(printerIds):
|
|
mg = pattern.match(printerId)
|
|
if mg:
|
|
printerIds[i] = mg.group(1)
|
|
return (customer, printerIds, cd)
|
|
|
|
CREATE_PRINTER_JSON_SKIP_FIELDS = ['id', 'name', 'createTime', 'orgUnitPath', 'auxiliaryMessages']
|
|
UPDATE_PRINTER_JSON_SKIP_FIELDS = ['id', 'name', 'createTime', 'orgUnitId', 'orgUnitPath', 'auxiliaryMessages']
|
|
|
|
def _getPrinterAttributes(cd, jsonDeleteFields):
|
|
'''get printer attributes for create/update commands'''
|
|
body = {}
|
|
showDetails = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'description':
|
|
body['description'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'displayname':
|
|
body['displayName'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'makeandmodel':
|
|
body['makeAndModel'] = getString(Cmd.OB_STRING)
|
|
elif myarg in {'ou', 'org', 'orgunit', 'orgunitid'}:
|
|
_, body['orgUnitId'] = getOrgUnitId(cd)
|
|
body['orgUnitId'] = body['orgUnitId'][3:]
|
|
elif myarg == 'uri':
|
|
body['uri'] = getString(Cmd.OB_STRING)
|
|
elif myarg in {'driverless', 'usedriverlessconfig'}:
|
|
body['useDriverlessConfig'] = getBoolean()
|
|
elif myarg == 'nodetails':
|
|
showDetails = False
|
|
elif myarg == 'json':
|
|
body.update(getJSON(jsonDeleteFields))
|
|
else:
|
|
unknownArgumentExit()
|
|
if body.get('makeAndModel'):
|
|
body.pop('useDriverlessConfig', None)
|
|
return (body, showDetails)
|
|
|
|
PRINTER_FIELDS_CHOICE_MAP = {
|
|
'auxiliarymessages': 'auxiliaryMessages',
|
|
'createtime': 'createTime',
|
|
'description': 'description',
|
|
'displayname': 'displayName',
|
|
'id': 'id',
|
|
'makeandmodel': 'makeAndModel',
|
|
'name': 'name',
|
|
'org': 'orgUnitId',
|
|
'orgunit': 'orgUnitId',
|
|
'orgunitid': 'orgUnitId',
|
|
'ou': 'orgUnitId',
|
|
'uri': 'uri',
|
|
'usedriverlessconfig': 'useDriverlessConfig',
|
|
}
|
|
PRINTER_TIME_OBJECTS = {'createTime'}
|
|
|
|
def _checkPrinterInheritance(cd, printer, orgUnitId, showInherited):
|
|
if 'orgUnitId' in printer:
|
|
if not showInherited:
|
|
if orgUnitId is not None and printer['orgUnitId'] != orgUnitId:
|
|
return False
|
|
elif orgUnitId is not None and printer['orgUnitId'] != orgUnitId:
|
|
printer['inherited'] = True
|
|
printer['parentOrgUnitId'] = printer['orgUnitId']
|
|
printer['parentOrgUnitPath'] = convertOrgUnitIDtoPath(cd, f'id:{printer["parentOrgUnitId"]}')
|
|
printer['orgUnitId'] = orgUnitId
|
|
else:
|
|
printer['inherited'] = False
|
|
printer['parentOrgUnitId'] = printer['parentOrgUnitPath'] = ''
|
|
printer['orgUnitPath'] = convertOrgUnitIDtoPath(cd, f'id:{printer["orgUnitId"]}')
|
|
return True
|
|
|
|
def _showPrinter(cd, printer, FJQC, orgUnitId=None, showInherited=False, i=0, count=0):
|
|
if not _checkPrinterInheritance(cd, printer, orgUnitId, showInherited):
|
|
return False
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(printer, timeObjects=PRINTER_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.PRINTER, printer['id']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, printer, timeObjects=PRINTER_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
# gam create printer <PrinterAttribute>+ [nodetails]
|
|
def doCreatePrinter():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
parent = _getCustomersCustomerIdWithC()
|
|
body, showDetails = _getPrinterAttributes(cd, CREATE_PRINTER_JSON_SKIP_FIELDS)
|
|
if not body.get('orgUnitId'):
|
|
missingArgumentExit('orgunit')
|
|
try:
|
|
printer = callGAPI(cd.customers().chrome().printers(), 'create',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
parent=parent, body=body)
|
|
entityActionPerformed([Ent.PRINTER, printer['id']])
|
|
if showDetails:
|
|
_showPrinter(cd, printer, None)
|
|
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.PRINTER, None], str(e))
|
|
|
|
# gam update printer <PrinterID> <PrinterAttribute>+ [nodetails]
|
|
def doUpdatePrinter():
|
|
name, printerId, cd = _getPrinterID()
|
|
body, showDetails = _getPrinterAttributes(cd, UPDATE_PRINTER_JSON_SKIP_FIELDS)
|
|
updateMask = ','.join(list(body.keys()))
|
|
# note clearMask seems unnecessary. Updating field to '' clears it.
|
|
try:
|
|
printer = callGAPI(cd.customers().chrome().printers(), 'patch',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name, updateMask=updateMask, body=body)
|
|
entityActionPerformed([Ent.PRINTER, printerId])
|
|
if showDetails:
|
|
_showPrinter(cd, printer, None)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.PRINTER, printerId], str(e))
|
|
|
|
# gam delete printer
|
|
# <PrinterIDList>|
|
|
# <FileSelector>|
|
|
# <CSVFileSelector>
|
|
def doDeletePrinter():
|
|
parent, printerIds, cd = _getPrinterEntity()
|
|
# max 50 per API call
|
|
batch_size = 50
|
|
for chunk in range(0, len(printerIds), batch_size):
|
|
body = {'printerIds': printerIds[chunk:chunk + batch_size]}
|
|
result = callGAPI(cd.customers().chrome().printers(), 'batchDeletePrinters',
|
|
parent=parent, body=body)
|
|
for printerId in result.get('printerIds', []):
|
|
entityActionPerformed([Ent.PRINTER, printerId])
|
|
for failure in result.get('failedPrinters', []):
|
|
if 'printerIds' in failure:
|
|
entityActionFailedWarning([Ent.PRINTER, failure['printerIds']], failure.get('errorMessage', 'Unknown printer'))
|
|
else:
|
|
entityActionFailedWarning([Ent.PRINTER, failure['printer']['id']], failure['errorMessage'])
|
|
|
|
# gam info printer <PrinterID>
|
|
# [fields <PrinterFieldNameList>] [formatjson]
|
|
def doInfoPrinter():
|
|
name, printerId, cd = _getPrinterID()
|
|
FJQC = FormatJSONQuoteChar()
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getFieldsList(myarg, PRINTER_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
try:
|
|
printer = callGAPI(cd.customers().chrome().printers(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.PERMISSION_DENIED],
|
|
name=name, fields=fields)
|
|
_showPrinter(cd, printer, FJQC)
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.PRINTER, f'{printerId}')
|
|
except (GAPI.invalid, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.DEVICE, f'{printerId}'], str(e))
|
|
|
|
ORGUNIT_ENTITIES_MAP = {
|
|
'org': Cmd.ENTITY_OU,
|
|
'organdchildren': Cmd.ENTITY_OU_AND_CHILDREN,
|
|
'orgs': Cmd.ENTITY_OUS,
|
|
'orgsandchildren': Cmd.ENTITY_OUS_AND_CHILDREN,
|
|
'ou': Cmd.ENTITY_OU,
|
|
'ouandchildren': Cmd.ENTITY_OU_AND_CHILDREN,
|
|
'ous': Cmd.ENTITY_OUS,
|
|
'ousandchildren': Cmd.ENTITY_OUS_AND_CHILDREN,
|
|
'orgunit': Cmd.ENTITY_OU,
|
|
'orgunitandchildren': Cmd.ENTITY_OU_AND_CHILDREN,
|
|
'orgunits': Cmd.ENTITY_OUS,
|
|
'orgunitsandchildren': Cmd.ENTITY_OUS_AND_CHILDREN,
|
|
}
|
|
|
|
# gam show printers
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [filter <String>] [showinherited [<Boolean>]
|
|
# [fields <PrinterFieldNameList>] [formatjson]
|
|
# gam print printers [todrive <ToDriveAttribute>*]
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [filter <String>] [showinherited [<Boolean>]
|
|
# [fields <PrinterFieldNameList>] [[formatjson [quotechar <Character>]]
|
|
def doPrintShowPrinters():
|
|
def _printPrinter(printer):
|
|
if not _checkPrinterInheritance(cd, printer, orgUnitId, showInherited):
|
|
return False
|
|
row = flattenJSON(printer, timeObjects=PRINTER_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'id': printer['id'],
|
|
'JSON': json.dumps(cleanJSON(printer, timeObjects=PRINTER_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
parent = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile(['id']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
ous = [None]
|
|
directlyInOU = True
|
|
pfilter = None
|
|
showInherited = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ORGUNIT_ENTITIES_MAP:
|
|
myarg = ORGUNIT_ENTITIES_MAP[myarg]
|
|
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
|
|
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
|
|
elif getFieldsList(myarg, PRINTER_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
|
pass
|
|
elif myarg == 'filter':
|
|
pfilter = getString(Cmd.OB_STRING)
|
|
elif myarg == 'showinherited':
|
|
showInherited = getBoolean()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if fieldsList and (not directlyInOU or showInherited):
|
|
fieldsList.append('orgUnitId')
|
|
fields = getItemFieldsFromFieldsList('printers', fieldsList)
|
|
for ou in ous:
|
|
if ou is not None:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
_, orgUnitId = getOrgUnitId(cd, ou)
|
|
ouList = [(ou, orgUnitId[3:])]
|
|
else:
|
|
ouList = [('/', None)]
|
|
if not directlyInOU:
|
|
try:
|
|
orgs = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
|
|
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
return
|
|
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
|
|
for subou in ouList:
|
|
orgUnitPath = subou[0]
|
|
orgUnitId = subou[1]
|
|
if orgUnitId is not None:
|
|
oneQualifier = Msg.DIRECTLY_IN_THE.format(Ent.Singular(Ent.ORGANIZATIONAL_UNIT))
|
|
printGettingAllEntityItemsForWhom(Ent.PRINTER, orgUnitPath, qualifier=oneQualifier, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
printGettingAllAccountEntities(Ent.PRINTER, pfilter)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
printers = callGAPIpages(cd.customers().chrome().printers(), 'list', 'printers',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID, GAPI.PERMISSION_DENIED],
|
|
parent=parent, orgUnitId=orgUnitId, filter=pfilter, fields=fields)
|
|
except (GAPI.invalid, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.PRINTER, None], str(e))
|
|
return
|
|
if not csvPF:
|
|
jcount = len(printers)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, Ent.PRINTER)
|
|
Ind.Increment()
|
|
j = 0
|
|
for printer in printers:
|
|
j += 1
|
|
_showPrinter(cd, printer, FJQC, orgUnitId, showInherited, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for printer in printers:
|
|
_printPrinter(printer)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Printers')
|
|
|
|
# gam print printermodels [todrive <ToDriveAttribute>*]
|
|
# [filter <String>]
|
|
# [[formatjson [quotechar <Character>]]
|
|
# gam show printermodels
|
|
# [filter <String>]
|
|
# [formatjson]
|
|
def doPrintShowPrinterModels():
|
|
def _showPrinterModel(model, FJQC, i, count):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(model), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.PRINTER_MODEL, model['manufacturer']], i, count)
|
|
Ind.Increment()
|
|
for field in ['displayName', 'makeAndModel']:
|
|
printKeyValueList([field, model[field]])
|
|
Ind.Decrement()
|
|
|
|
def _printPrinterModel(model):
|
|
row = flattenJSON(model)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'JSON': json.dumps(cleanJSON(model),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
parent = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
pfilter = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'filter':
|
|
pfilter = getString(Cmd.OB_STRING)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if not FJQC.formatJSON:
|
|
csvPF.SetTitles(['manufacturer', 'displayName', 'makeAndModel'])
|
|
csvPF.SetSortAllTitles()
|
|
printGettingAllAccountEntities(Ent.PRINTER_MODEL, pfilter)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
models = callGAPIpages(cd.customers().chrome().printers(), 'listPrinterModels', 'printerModels',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
pageMessage=pageMessage,
|
|
parent=parent, pageSize=10000, filter=pfilter)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.PRINTER_MODEL, None], str(e))
|
|
return
|
|
if not csvPF:
|
|
jcount = len(models)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, Ent.PRINTER_MODEL)
|
|
Ind.Increment()
|
|
j = 0
|
|
for model in models:
|
|
j += 1
|
|
_showPrinterModel(model, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for model in models:
|
|
_printPrinterModel(model)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Printer Models')
|
|
|
|
CHROME_APPS_TIME_OBJECTS = {'firstPublishTime', 'latestPublishTime'}
|
|
CHROME_APPS_TYPE_CHOICES = ['android', 'chrome', 'web']
|
|
|
|
# gam info chromeapp android|chrome|web <AppID> [formatjson]
|
|
def doInfoChromeApp():
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT_APPDETAILS)
|
|
app_type = getChoice(CHROME_APPS_TYPE_CHOICES)
|
|
app_id = getString(Cmd.OB_APP_ID)
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
FJQC.GetFormatJSON(myarg)
|
|
if app_type == 'chrome':
|
|
service = cm.customers().apps().chrome()
|
|
elif app_type == 'android':
|
|
service = cm.customers().apps().android()
|
|
else:
|
|
service = cm.customers().apps().web()
|
|
try:
|
|
appDetails = callGAPI(service, 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
name=f'customers/{GC.Values[GC.CUSTOMER_ID]}/apps/{app_type}/{app_id}')
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(appDetails), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CHROME_APP, app_id])
|
|
Ind.Increment()
|
|
showJSON(None, appDetails, timeObjects=CHROME_APPS_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(None, Ent.CHROME_APP, f'{app_type}/{app_id}')
|
|
|
|
def _getPrintChromeGetting(subou, pfilter, entityType):
|
|
orgUnitPath = subou[0]
|
|
orgUnitId = subou[1]
|
|
query = pfilter
|
|
if orgUnitId is not None:
|
|
if query:
|
|
query += ' AND '
|
|
else:
|
|
query = ''
|
|
query += f'orgUnitPath={orgUnitPath}'
|
|
printGettingAllAccountEntities(entityType, query)
|
|
return (orgUnitPath, orgUnitId)
|
|
|
|
CHROME_APPS_ORDERBY_CHOICE_MAP = {
|
|
'appname': 'app_name',
|
|
'apptype': 'appType',
|
|
'installtype': 'install_type',
|
|
'numberofpermissions': 'number_of_permissions',
|
|
'totalinstallcount': 'total_install_count',
|
|
}
|
|
CHROME_APPS_TITLES = [
|
|
'displayName',
|
|
'browserDeviceCount', 'osUserCount',
|
|
'appType', 'description',
|
|
'appInstallType', 'appSource',
|
|
'disabled', 'homepageUri', 'permissions'
|
|
]
|
|
|
|
# gam print chromeapps [todrive <ToDriveAttribute>*]
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [filter <String>]
|
|
# [orderby appname|apptype|installtype|numberofpermissions|totalinstallcount]
|
|
# [formatjson [quotechar <Character>]] [delimiter <Character>]
|
|
# gam show chromeapps
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [filter <String>]
|
|
# [orderby appname|apptype|installtype|numberofpermissions|totalinstallcount]
|
|
# [formatjson]
|
|
def doPrintShowChromeApps():
|
|
def _printApp(app):
|
|
if showOrgUnit:
|
|
app['orgUnitPath'] = orgUnitPath
|
|
row = flattenJSON(app, simpleLists=['permissions'], delimiter=delimiter)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'appId': app['appId'],
|
|
'JSON': json.dumps(cleanJSON(app), ensure_ascii=False, sort_keys=True)})
|
|
|
|
def _showApp(app, i=0, count=0):
|
|
if showOrgUnit:
|
|
app['orgUnitPath'] = orgUnitPath
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(app), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CHROME_APP, app['appId']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, app)
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
|
|
customerId = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile(['appId']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
ous = [None]
|
|
directlyInOU = True
|
|
showOrgUnit = False
|
|
orderBy = pfilter = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ORGUNIT_ENTITIES_MAP:
|
|
myarg = ORGUNIT_ENTITIES_MAP[myarg]
|
|
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
|
|
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
|
|
elif myarg == 'filter':
|
|
pfilter = getString(Cmd.OB_STRING)
|
|
elif myarg == 'orderby':
|
|
orderBy = getChoice(CHROME_APPS_ORDERBY_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if ous[0] is not None:
|
|
showOrgUnit = True
|
|
if csvPF and not FJQC.formatJSON:
|
|
csvPF.AddTitles(CHROME_APPS_TITLES)
|
|
if showOrgUnit:
|
|
csvPF.AddTitle('orgUnitPath')
|
|
for ou in ous:
|
|
if ou is not None:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
_, orgUnitId = getOrgUnitId(cd, ou)
|
|
ouList = [(ou, orgUnitId[3:])]
|
|
else:
|
|
ouList = [('/', None)]
|
|
if not directlyInOU:
|
|
try:
|
|
orgs = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
|
|
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
return
|
|
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
|
|
for subou in ouList:
|
|
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, pfilter, Ent.CHROME_APP)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
apps = callGAPIpages(cm.customers().reports(), 'countInstalledApps', 'installedApps',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageMessage=pageMessage,
|
|
customer=customerId, orgUnitId=orgUnitId, filter=pfilter, orderBy=orderBy)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CHROME_APP, None], str(e))
|
|
return
|
|
jcount = len(apps)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.CHROME_APP)
|
|
Ind.Increment()
|
|
j = 0
|
|
for app in apps:
|
|
j += 1
|
|
_showApp(app, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for app in apps:
|
|
_printApp(app)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chrome Installed Applications')
|
|
|
|
CHROME_APP_DEVICES_APPTYPE_CHOICE_MAP = {
|
|
'extension': 'EXTENSION',
|
|
'app': 'APP',
|
|
'theme': 'THEME',
|
|
'hostedapp': 'HOSTED_APP',
|
|
'androidapp': 'ANDROID_APP',
|
|
}
|
|
CHROME_APP_DEVICES_ORDERBY_CHOICE_MAP = {
|
|
'deviceid': 'deviceId',
|
|
'machine': 'machine',
|
|
}
|
|
CHROME_APP_DEVICES_TITLES = ['appType', 'deviceId', 'machine']
|
|
|
|
# gam print chromeappdevices [todrive <ToDriveAttribute>*]
|
|
# appid <AppID> apptype extension|app|theme|hostedapp|androidapp
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [start <Date>] [end <Date>]
|
|
# [orderby deviceid|machine]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show chromeappdevices
|
|
# appid <AppID> apptype extension|app|theme|hostedapp|androidapp
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [start <Date>] [end <Date>]
|
|
# [orderby deviceid|machine]
|
|
# [formatjson]
|
|
def doPrintShowChromeAppDevices():
|
|
def _printDevice(device):
|
|
device['appId'] = appId
|
|
device['appType'] = appType
|
|
if showOrgUnit:
|
|
device['orgUnitPath'] = orgUnitPath
|
|
row = flattenJSON(device)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRow(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'appId': device['appId'],
|
|
'JSON': json.dumps(cleanJSON(device), ensure_ascii=False, sort_keys=True)})
|
|
|
|
def _showDevice(device, i=0, count=0):
|
|
device['appId'] = appId
|
|
device['appType'] = appType
|
|
if showOrgUnit:
|
|
device['orgUnitPath'] = orgUnitPath
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(device), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CHROME_APP, device['appId']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, device)
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
|
|
customerId = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile(['appId']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
ous = [None]
|
|
directlyInOU = True
|
|
showOrgUnit = False
|
|
appId = appType = orderBy = None
|
|
startDate = endDate = pfilter = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ORGUNIT_ENTITIES_MAP:
|
|
myarg = ORGUNIT_ENTITIES_MAP[myarg]
|
|
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
|
|
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
|
|
elif myarg == 'appid':
|
|
appId = getString(Cmd.OB_APP_ID)
|
|
elif myarg == 'apptype':
|
|
appType = getChoice(CHROME_APP_DEVICES_APPTYPE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg in CROS_START_ARGUMENTS:
|
|
startDate, _ = _getFilterDateTime()
|
|
startDate = startDate.strftime(YYYYMMDD_FORMAT)
|
|
elif myarg in CROS_END_ARGUMENTS:
|
|
endDate, _ = _getFilterDateTime()
|
|
endDate = endDate.strftime(YYYYMMDD_FORMAT)
|
|
elif myarg == 'orderby':
|
|
orderBy = getChoice(CHROME_APP_DEVICES_ORDERBY_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if appId is None:
|
|
missingArgumentExit('appid')
|
|
if appType is None:
|
|
missingArgumentExit('apptype')
|
|
if endDate:
|
|
pfilter = f'last_active_date<={endDate}'
|
|
if startDate:
|
|
if pfilter:
|
|
pfilter += ' AND '
|
|
else:
|
|
pfilter = ''
|
|
pfilter += f'last_active_date>={startDate}'
|
|
if ous[0] is not None:
|
|
showOrgUnit = True
|
|
if csvPF and not FJQC.formatJSON:
|
|
csvPF.AddTitles(CHROME_APP_DEVICES_TITLES)
|
|
if showOrgUnit:
|
|
csvPF.AddTitle('orgUnitPath')
|
|
for ou in ous:
|
|
if ou is not None:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
_, orgUnitId = getOrgUnitId(cd, ou)
|
|
ouList = [(ou, orgUnitId[3:])]
|
|
else:
|
|
ouList = [('/', None)]
|
|
if not directlyInOU:
|
|
try:
|
|
orgs = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
|
|
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
return
|
|
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
|
|
for subou in ouList:
|
|
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, pfilter, Ent.CHROME_APP_DEVICE)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
devices = callGAPIpages(cm.customers().reports(), 'findInstalledAppDevices', 'devices',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageMessage=pageMessage,
|
|
appId=appId, appType=appType,
|
|
customer=customerId, orgUnitId=orgUnitId, filter=pfilter, orderBy=orderBy)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CHROME_APP_DEVICE, None], str(e))
|
|
return
|
|
jcount = len(devices)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.CHROME_APP_DEVICE)
|
|
Ind.Increment()
|
|
j = 0
|
|
for device in devices:
|
|
j += 1
|
|
_showDevice(device, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for device in devices:
|
|
_printDevice(device)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chrome Installed Application Devices')
|
|
|
|
|
|
CHROME_AUE_TITLES = ['aueMonth', 'aueYear', 'expired']
|
|
|
|
# gam print chromeaues [todrive <ToDriveAttribute>*]
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [minauedate <Date>] [maxauedate <Date>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show chromeaues
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [minauedate <Date>] [maxauedate <Date>]
|
|
# [formatjson]
|
|
def doPrintShowChromeAues():
|
|
def _printAue(aue):
|
|
if showOrgUnit:
|
|
aue['orgUnitPath'] = orgUnitPath
|
|
row = flattenJSON(aue)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRow(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'model': aue['model'], 'count': aue['count'],
|
|
'JSON': json.dumps(cleanJSON(aue), ensure_ascii=False, sort_keys=True)})
|
|
|
|
def _showAue(aue, i=0, count=0):
|
|
if showOrgUnit:
|
|
aue['orgUnitPath'] = orgUnitPath
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(aue), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CHROME_MODEL, aue['model']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, aue)
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
|
|
customerId = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile(['model', 'count']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
ous = [None]
|
|
directlyInOU = True
|
|
showOrgUnit = False
|
|
minAueDate = maxAueDate = pfilter = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ORGUNIT_ENTITIES_MAP:
|
|
myarg = ORGUNIT_ENTITIES_MAP[myarg]
|
|
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
|
|
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
|
|
elif myarg == 'minauedate':
|
|
minAueDate, _ = _getFilterDateTime()
|
|
minAueDate = minAueDate.strftime(YYYYMMDD_FORMAT)
|
|
elif myarg == 'maxauedate':
|
|
maxAueDate, _ = _getFilterDateTime()
|
|
maxAueDate = maxAueDate.strftime(YYYYMMDD_FORMAT)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if minAueDate:
|
|
pfilter = f'minAueDate={minAueDate}'
|
|
if maxAueDate:
|
|
if pfilter:
|
|
pfilter += ' AND '
|
|
else:
|
|
pfilter = ''
|
|
pfilter += f'maxAueDate>={maxAueDate}'
|
|
if ous[0] is not None:
|
|
showOrgUnit = True
|
|
if csvPF and not FJQC.formatJSON:
|
|
csvPF.AddTitles(CHROME_AUE_TITLES)
|
|
if showOrgUnit:
|
|
csvPF.AddTitle('orgUnitPath')
|
|
for ou in ous:
|
|
if ou is not None:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
_, orgUnitId = getOrgUnitId(cd, ou)
|
|
ouList = [(ou, orgUnitId[3:])]
|
|
else:
|
|
ouList = [('/', None)]
|
|
if not directlyInOU:
|
|
try:
|
|
orgs = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
|
|
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
return
|
|
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
|
|
for subou in ouList:
|
|
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, pfilter, Ent.CHROME_MODEL)
|
|
try:
|
|
aues = callGAPI(cm.customers().reports(), 'countChromeDevicesReachingAutoExpirationDate',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customerId, orgUnitId=orgUnitId, minAueDate=minAueDate, maxAueDate=maxAueDate).get('deviceAueCountReports', [])
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CHROME_MODEL, None], str(e))
|
|
return
|
|
jcount = len(aues)
|
|
if not csvPF:
|
|
Ind.Increment()
|
|
j = 0
|
|
for aue in sorted(aues, key=lambda k: k.get('model', UNKNOWN)):
|
|
j += 1
|
|
_showAue(aue, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for aue in sorted(aues, key=lambda k: k.get('model', UNKNOWN)):
|
|
_printAue(aue)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chrome AUEs')
|
|
|
|
CHROME_NEEDSATTN_TITLES = ['noRecentPolicySyncCount', 'noRecentUserActivityCount', 'pendingUpdate',
|
|
'osVersionNotCompliantCount', 'unsupportedPolicyCount']
|
|
|
|
# gam print chromeneedsattn [todrive <ToDriveAttribute>*]
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show chromeneedsattn
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [formatjson]
|
|
def doPrintShowChromeNeedsAttn():
|
|
def _printNeedsAttn(needsattn):
|
|
if showOrgUnit:
|
|
needsattn['orgUnitPath'] = orgUnitPath
|
|
row = flattenJSON(needsattn)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRow(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'orgUnitPath': orgUnitPath} if showOrgUnit else {}
|
|
row['JSON'] = json.dumps(cleanJSON(needsattn), ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
def _showNeedsAttn(needsattn, i=0, count=0):
|
|
if showOrgUnit:
|
|
needsattn['orgUnitPath'] = orgUnitPath
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(needsattn), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
if showOrgUnit:
|
|
printEntity([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, needsattn)
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
|
|
customerId = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile([]) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
ous = [None]
|
|
directlyInOU = True
|
|
showOrgUnit = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ORGUNIT_ENTITIES_MAP:
|
|
myarg = ORGUNIT_ENTITIES_MAP[myarg]
|
|
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
|
|
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if ous[0] is not None:
|
|
showOrgUnit = True
|
|
if csvPF:
|
|
if not FJQC.formatJSON:
|
|
csvPF.AddTitles(CHROME_NEEDSATTN_TITLES)
|
|
if showOrgUnit:
|
|
csvPF.AddTitle('orgUnitPath')
|
|
elif showOrgUnit:
|
|
csvPF.SetJSONTitles(['orgUnitPath', 'JSON'])
|
|
for ou in ous:
|
|
if ou is not None:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
_, orgUnitId = getOrgUnitId(cd, ou)
|
|
ouList = [(ou, orgUnitId[3:])]
|
|
else:
|
|
ouList = [('/', None)]
|
|
if not directlyInOU:
|
|
try:
|
|
orgs = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
|
|
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
return
|
|
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
|
|
count = len(ouList)
|
|
i = 0
|
|
for subou in ouList:
|
|
i += 1
|
|
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, None, Ent.CHROME_DEVICE)
|
|
try:
|
|
result = callGAPI(cm.customers().reports(), 'countChromeDevicesThatNeedAttention',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=customerId, orgUnitId=orgUnitId, readMask=','.join(CHROME_NEEDSATTN_TITLES))
|
|
for field in CHROME_NEEDSATTN_TITLES:
|
|
result.setdefault(field, 0)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.ORGANIZATIONAL_UNIT, orgUnitId], str(e))
|
|
return
|
|
if not csvPF:
|
|
_showNeedsAttn(result, i, count)
|
|
else:
|
|
_printNeedsAttn(result)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chrome Devices Needing Attention')
|
|
|
|
CHROME_VERSIONS_TITLES = ['channel', 'system', 'deviceOsVersion']
|
|
|
|
# gam print chromeversions [todrive <ToDriveAttribute>*]
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [start <Date>] [end <Date>]
|
|
# [recentfirst [<Boolean>]]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show chromeversions
|
|
# [(ou <OrgUnitItem>)|(ou_and_children <OrgUnitItem>)|
|
|
# (ous <OrgUnitList>)|(ous_and_children <OrgUnitList>)]
|
|
# [start <Date>] [end <Date>]
|
|
# [recentfirst [<Boolean>]]
|
|
# [formatjson]
|
|
def doPrintShowChromeVersions():
|
|
def _getVersionKey(v):
|
|
if 'version' not in v:
|
|
return (0, 0, 0, 0)
|
|
k = v['version'].split('.')
|
|
for i, x in enumerate(k):
|
|
k[i] = int(x)
|
|
return tuple(k)
|
|
|
|
def _printVersion(version):
|
|
if showOrgUnit:
|
|
version['orgUnitPath'] = orgUnitPath
|
|
if 'version' not in version:
|
|
version['version'] = UNKNOWN
|
|
row = flattenJSON(version)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRow(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'version': version['version'], 'count': version['count'],
|
|
'JSON': json.dumps(cleanJSON(version), ensure_ascii=False, sort_keys=True)})
|
|
|
|
def _showVersion(version, i=0, count=0):
|
|
if showOrgUnit:
|
|
version['orgUnitPath'] = orgUnitPath
|
|
if 'version' not in version:
|
|
version['version'] = UNKNOWN
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(version), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CHROME_VERSION, version['version']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, version)
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
cm = buildGAPIObject(API.CHROMEMANAGEMENT)
|
|
customerId = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile(['version', 'count']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
ous = [None]
|
|
directlyInOU = True
|
|
reverse = showOrgUnit = False
|
|
startDate = endDate = pfilter = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ORGUNIT_ENTITIES_MAP:
|
|
myarg = ORGUNIT_ENTITIES_MAP[myarg]
|
|
ous = convertEntityToList(getString(Cmd.OB_ENTITY, minLen=0), shlexSplit=True, nonListEntityType=myarg in [Cmd.ENTITY_OU, Cmd.ENTITY_OU_AND_CHILDREN])
|
|
directlyInOU = myarg in {Cmd.ENTITY_OU, Cmd.ENTITY_OUS}
|
|
elif myarg in CROS_START_ARGUMENTS:
|
|
startDate, _ = _getFilterDateTime()
|
|
startDate = startDate.strftime(YYYYMMDD_FORMAT)
|
|
elif myarg in CROS_END_ARGUMENTS:
|
|
endDate, _ = _getFilterDateTime()
|
|
endDate = endDate.strftime(YYYYMMDD_FORMAT)
|
|
elif myarg == 'recentfirst':
|
|
reverse = getBoolean()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if endDate:
|
|
pfilter = f'last_active_date<={endDate}'
|
|
if startDate:
|
|
if pfilter:
|
|
pfilter += ' AND '
|
|
else:
|
|
pfilter = ''
|
|
pfilter += f'last_active_date>={startDate}'
|
|
if ous[0] is not None:
|
|
showOrgUnit = True
|
|
if csvPF and not FJQC.formatJSON:
|
|
csvPF.AddTitles(CHROME_VERSIONS_TITLES)
|
|
if showOrgUnit:
|
|
csvPF.AddTitle('orgUnitPath')
|
|
for ou in ous:
|
|
if ou is not None:
|
|
ou = makeOrgUnitPathAbsolute(ou)
|
|
_, orgUnitId = getOrgUnitId(cd, ou)
|
|
ouList = [(ou, orgUnitId[3:])]
|
|
else:
|
|
ouList = [('/', None)]
|
|
if not directlyInOU:
|
|
try:
|
|
orgs = callGAPI(cd.orgunits(), 'list',
|
|
throwReasons=GAPI.ORGUNIT_GET_THROW_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID], orgUnitPath=makeOrgUnitPathRelative(ou),
|
|
type='all', fields='organizationUnits(orgUnitPath,orgUnitId)')
|
|
except (GAPI.invalidOrgunit, GAPI.orgunitNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalidCustomerId, GAPI.loginRequired):
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.ORGANIZATIONAL_UNIT, ou)
|
|
return
|
|
ouList.extend([(subou['orgUnitPath'], subou['orgUnitId'][3:]) for subou in sorted(orgs.get('organizationUnits', []), key=lambda k: k['orgUnitPath'])])
|
|
for subou in ouList:
|
|
orgUnitPath, orgUnitId = _getPrintChromeGetting(subou, pfilter, Ent.CHROME_VERSION)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
versions = callGAPIpages(cm.customers().reports(), 'countChromeVersions', 'browserVersions',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
pageMessage=pageMessage,
|
|
customer=customerId, orgUnitId=orgUnitId, filter=pfilter)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CHROME_VERSION, None], str(e))
|
|
return
|
|
jcount = len(versions)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.CHROME_VERSION)
|
|
Ind.Increment()
|
|
j = 0
|
|
for version in sorted(versions, key=_getVersionKey, reverse=reverse):
|
|
j += 1
|
|
_showVersion(version, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for version in sorted(versions, key=_getVersionKey, reverse=reverse):
|
|
_printVersion(version)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chrome Versions')
|
|
|
|
def getPlatformChannelMap(cv, entityType):
|
|
if cv is None:
|
|
cv = buildGAPIObjectNoAuthentication(API.CHROMEVERSIONHISTORY)
|
|
if entityType == Ent.CHROME_PLATFORM:
|
|
svc = cv.platforms()
|
|
parent = 'chrome'
|
|
field = 'platformType'
|
|
else: # elif entityType == Ent.CHROME_CHANNEL:
|
|
svc = cv.platforms().channels()
|
|
parent = 'chrome/platforms/all'
|
|
field = 'channelType'
|
|
try:
|
|
pcitems = callGAPIpages(svc, 'list', CHROME_VERSIONHISTORY_ITEMS[entityType],
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
parent=parent, fields=f'nextPageToken,{CHROME_VERSIONHISTORY_ITEMS[entityType]}')
|
|
pcMap = {'all': 'all'}
|
|
for pcitem in pcitems:
|
|
pcType = pcitem[field].lower()
|
|
pcMap[pcType.replace('_', '')] = pcType
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([entityType, None], str(e))
|
|
pcMap = {}
|
|
return (cv, pcMap)
|
|
|
|
def getRelativeMilestone(cv, channel, minus):
|
|
''' takes a channel and minus_versions like stable and -1. returns current given milestone number '''
|
|
if cv is None:
|
|
cv = buildGAPIObjectNoAuthentication(API.CHROMEVERSIONHISTORY)
|
|
try:
|
|
releases = callGAPIpages(cv.platforms().channels().versions().releases(), 'list', 'releases',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
parent=f'chrome/platforms/all/channels/{channel}/versions/all',
|
|
fields='nextPageToken,releases(version)')
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.CHROME_RELEASE, None], str(e))
|
|
return (cv, False, str(e))
|
|
milestones = []
|
|
# Note that milestones are usually sequential but some numbers
|
|
# may be skipped. For example, there was no Chrome 82 stable.
|
|
# Thus we need to do more than find the latest version and subtract.
|
|
for release in releases:
|
|
if 'version' in release:
|
|
milestone = int(release['version'].split('.')[0])
|
|
if milestone not in milestones:
|
|
milestones.append(int(milestone))
|
|
milestones.sort(reverse=True)
|
|
try:
|
|
return (cv, True, str(milestones[minus]))
|
|
except IndexError:
|
|
return (cv, False, f'{channel}-{0}:{channel}-{len(milestones)-1}')
|
|
|
|
CHROME_HISTORY_ENTITY_CHOICE_MAP = {
|
|
'platforms': Ent.CHROME_PLATFORM,
|
|
'channels': Ent.CHROME_CHANNEL,
|
|
'versions': Ent.CHROME_VERSION,
|
|
'releases': Ent.CHROME_RELEASE,
|
|
}
|
|
CHROME_VERSIONHISTORY_ORDERBY_CHOICE_MAP = {
|
|
Ent.CHROME_VERSION: {
|
|
'channel': 'channel',
|
|
'name': 'name',
|
|
'platform': 'platform',
|
|
'version': 'version'
|
|
},
|
|
Ent.CHROME_RELEASE: {
|
|
'channel': 'channel',
|
|
'endtime': 'endtime',
|
|
'fraction': 'fraction',
|
|
'name': 'name',
|
|
'platform': 'platform',
|
|
'starttime': 'starttime',
|
|
'version': 'version'
|
|
}
|
|
}
|
|
CHROME_VERSIONHISTORY_TITLES = {
|
|
Ent.CHROME_PLATFORM: ['platform'],
|
|
Ent.CHROME_CHANNEL: ['channel', 'platform'],
|
|
Ent.CHROME_VERSION: ['version', 'channel', 'platform',
|
|
'major_version', 'minor_version', 'build', 'patch'],
|
|
Ent.CHROME_RELEASE: ['version', 'channel', 'platform',
|
|
'major_version', 'minor_version', 'build', 'patch',
|
|
'fraction', 'fractionGroup', 'serving.startTime',
|
|
'serving.endTime', 'pinnable']
|
|
|
|
}
|
|
CHROME_VERSIONHISTORY_ITEMS = {
|
|
Ent.CHROME_PLATFORM: 'platforms',
|
|
Ent.CHROME_CHANNEL: 'channels',
|
|
Ent.CHROME_VERSION: 'versions',
|
|
Ent.CHROME_RELEASE: 'releases'
|
|
}
|
|
CHROME_VERSIONHISTORY_TIMEOBJECTS = {
|
|
Ent.CHROME_PLATFORM: None,
|
|
Ent.CHROME_CHANNEL: None,
|
|
Ent.CHROME_VERSION: None,
|
|
Ent.CHROME_RELEASE: ['startTime', 'endTime']
|
|
}
|
|
|
|
# gam print chromehistory platforms [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show chromehistory platforms
|
|
# [formatjson]
|
|
|
|
# gam print chromehistory channels [todrive <ToDriveAttribute>*]
|
|
# [platform <ChromePlatformType>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show chromehistory channels
|
|
# [platform <ChromePlatformType>]
|
|
# [formatjson]
|
|
|
|
# gam print chromehistory versions [todrive <ToDriveAttribute>*]
|
|
# [platform <ChromePlatformType>] [channel <ChromeChannelType>]
|
|
# (orderby <ChromeVersionsOrderByFieldName> [ascending|descending])*
|
|
# [filter <String>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show chromehistory versions
|
|
# [platform <ChromePlatformType>] [channel <ChromeChannelType>]
|
|
# (orderby <ChromeVersionsOrderByFieldName> [ascending|descending])*
|
|
# [filter <String>]
|
|
# [formatjson]
|
|
|
|
# gam print chromehistory releases [todrive <ToDriveAttribute>*]
|
|
# [platform <ChromePlatformType>] [channel <ChromeChannelType>] [version <String>]
|
|
# (orderby <ChromeReleasessOrderByFieldName> [ascending|descending])*
|
|
# [filter <String>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show chromehistory releases
|
|
# [platform <ChromePlatformType>] [channel <ChromeChannelType>] [version <String>]
|
|
# (orderby <ChromeReleasessOrderByFieldName> [ascending|descending])*
|
|
# [filter <String>]
|
|
# [formatjson]
|
|
|
|
def doPrintShowChromeHistory():
|
|
def addDetailFields(citem):
|
|
for key in list(citem):
|
|
if key.endswith('Type'):
|
|
citem[key[:-4]] = citem.pop(key)
|
|
if 'channel' in citem:
|
|
citem['channel'] = citem['channel'].lower()
|
|
else:
|
|
channel_match = re.search(r"\/channels\/([^/]*)", citem['name'])
|
|
if channel_match:
|
|
try:
|
|
citem['channel'] = channel_match.group(1)
|
|
except IndexError:
|
|
pass
|
|
if 'platform' in citem:
|
|
citem['platform'] = citem['platform'].lower()
|
|
else:
|
|
platform_match = re.search(r"\/platforms\/([^/]*)", citem['name'])
|
|
if platform_match:
|
|
try:
|
|
citem['platform'] = platform_match.group(1)
|
|
except IndexError:
|
|
pass
|
|
if citem.get('version', '').count('.') == 3:
|
|
citem['major_version'], citem['minor_version'], citem['build'], citem['patch'] = citem['version'].split('.')
|
|
citem.pop('name')
|
|
|
|
def _printItem(citem):
|
|
addDetailFields(citem)
|
|
row = flattenJSON(citem, timeObjects=CHROME_VERSIONHISTORY_TIMEOBJECTS[entityType])
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRow(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
keyField = CHROME_VERSIONHISTORY_TITLES[entityType][0]
|
|
csvPF.WriteRowNoFilter({keyField: citem[keyField],
|
|
'JSON': json.dumps(cleanJSON(citem), ensure_ascii=False, sort_keys=True)})
|
|
|
|
def _showItem(citem, i=0, count=0):
|
|
addDetailFields(citem)
|
|
keyField = CHROME_VERSIONHISTORY_TITLES[entityType][0]
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(citem), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([entityType, citem[keyField]], i, count)
|
|
Ind.Increment()
|
|
citem = flattenJSON(citem, timeObjects=CHROME_VERSIONHISTORY_TIMEOBJECTS[entityType])
|
|
for field in CHROME_VERSIONHISTORY_TITLES[entityType]:
|
|
if field in citem:
|
|
printKeyValueList([field, citem[field]])
|
|
Ind.Decrement()
|
|
|
|
cv = buildGAPIObjectNoAuthentication(API.CHROMEVERSIONHISTORY)
|
|
entityType = getChoice(CHROME_HISTORY_ENTITY_CHOICE_MAP, mapChoice=True)
|
|
csvPF = CSVPrintFile(CHROME_VERSIONHISTORY_TITLES[entityType][0:1]) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
platformMap = None
|
|
channelMap = None
|
|
cplatform = 'all'
|
|
channel = 'all'
|
|
version = 'all'
|
|
kwargs = {}
|
|
if entityType in {Ent.CHROME_VERSION, Ent.CHROME_RELEASE}:
|
|
OBY = OrderBy(CHROME_VERSIONHISTORY_ORDERBY_CHOICE_MAP[entityType])
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif entityType != Ent.CHROME_PLATFORM and myarg == 'platform':
|
|
if platformMap is None:
|
|
_, platformMap = getPlatformChannelMap(cv, Ent.CHROME_PLATFORM)
|
|
cplatform = getChoice(platformMap, mapChoice=True)
|
|
elif entityType in {Ent.CHROME_VERSION, Ent.CHROME_RELEASE} and myarg == 'channel':
|
|
if channelMap is None:
|
|
_, channelMap = getPlatformChannelMap(cv, Ent.CHROME_CHANNEL)
|
|
channel = getChoice(channelMap, mapChoice=True)
|
|
elif entityType == Ent.CHROME_RELEASE and myarg == 'version':
|
|
version = getString(Cmd.OB_CHROME_VERSION)
|
|
elif entityType in {Ent.CHROME_VERSION, Ent.CHROME_RELEASE} and myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif entityType in {Ent.CHROME_VERSION, Ent.CHROME_RELEASE} and myarg == 'filter':
|
|
kwargs['filter'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and not FJQC.formatJSON:
|
|
csvPF.AddTitles(CHROME_VERSIONHISTORY_TITLES[entityType][1:])
|
|
if entityType == Ent.CHROME_PLATFORM:
|
|
svc = cv.platforms()
|
|
parent = 'chrome'
|
|
elif entityType == Ent.CHROME_CHANNEL:
|
|
svc = cv.platforms().channels()
|
|
parent = f'chrome/platforms/{cplatform}'
|
|
elif entityType == Ent.CHROME_VERSION:
|
|
svc = cv.platforms().channels().versions()
|
|
parent = f'chrome/platforms/{cplatform}/channels/{channel}'
|
|
kwargs['orderBy'] = OBY.orderBy
|
|
else: #elif entityType == Ent.CHROME_RELEASE
|
|
svc = cv.platforms().channels().versions().releases()
|
|
parent = f'chrome/platforms/{cplatform}/channels/{channel}/versions/{version}'
|
|
kwargs['orderBy'] = OBY.orderBy
|
|
printGettingAllAccountEntities(entityType)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
citems = callGAPIpages(svc, 'list', CHROME_VERSIONHISTORY_ITEMS[entityType],
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
pageMessage=pageMessage,
|
|
parent=parent, fields=f'nextPageToken,{CHROME_VERSIONHISTORY_ITEMS[entityType]}',
|
|
**kwargs)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([entityType, None], str(e))
|
|
return
|
|
if not csvPF:
|
|
jcount = len(citems)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, entityType)
|
|
j = 0
|
|
for citem in citems:
|
|
j += 1
|
|
_showItem(citem, j, jcount)
|
|
else:
|
|
for citem in citems:
|
|
_printItem(citem)
|
|
if csvPF:
|
|
csvPF.writeCSVfile(Ent.Plural(entityType))
|
|
|
|
# gam print chromesnvalidity [todrive <ToDriveAttribute>*]
|
|
# cros_sn <SerialNumberEntity> [listlimit <Number>]
|
|
# [delimiter <Character>]
|
|
def doPrintChromeSnValidity():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['serialNumber', 'exactMatches', 'exactMatchDeviceIds', 'prefixMatches', 'prefixMatchDeviceIds'])
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
serialNumberList = []
|
|
listLimit = 0
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'crossn':
|
|
serialNumberList = getEntityList(Cmd.OB_STRING_LIST)
|
|
elif myarg == 'listlimit':
|
|
listLimit = getInteger(minVal=0)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
else:
|
|
unknownArgumentExit()
|
|
if not serialNumberList:
|
|
actionNotPerformedNumItemsWarning(0, Ent.CROS_SERIAL_NUMBER, Msg.NO_SERIAL_NUMBERS_SPECIFIED)
|
|
return
|
|
for serialNumber in serialNumberList:
|
|
query = f'id:{serialNumber}'
|
|
printGettingAllAccountEntities(Ent.CROS_DEVICE, query)
|
|
try:
|
|
devices = callGAPIpages(cd.chromeosdevices(), 'list', 'chromeosdevices',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customerId=GC.Values[GC.CUSTOMER_ID],
|
|
query=query, fields='nextPageToken,chromeosdevices(deviceId,serialNumber)',
|
|
orderBy='serialNumber', maxResults=GC.Values[GC.DEVICE_MAX_RESULTS])
|
|
except (GAPI.invalidInput, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
exactMatches = prefixMatches = 0
|
|
exactMatchDeviceIds = []
|
|
prefixMatchDeviceIds = []
|
|
serialNumberLower = serialNumber.lower()
|
|
for device in devices:
|
|
if device['serialNumber'].lower() == serialNumberLower:
|
|
exactMatches += 1
|
|
if not listLimit or exactMatches <= listLimit:
|
|
exactMatchDeviceIds.append(device['deviceId'])
|
|
else:
|
|
prefixMatches += 1
|
|
if not listLimit or prefixMatches <= listLimit:
|
|
prefixMatchDeviceIds.append(device['deviceId'])
|
|
row = {'serialNumber': serialNumber,
|
|
'exactMatches': exactMatches,
|
|
'exactMatchDeviceIds': delimiter.join(exactMatchDeviceIds),
|
|
'prefixMatches': prefixMatches,
|
|
'prefixMatchDeviceIds': delimiter.join(prefixMatchDeviceIds)}
|
|
csvPF.WriteRow(row)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Chrome Serial Number Validity')
|
|
|
|
# Mobile command utilities
|
|
MOBILE_ACTION_CHOICE_MAP = {
|
|
'accountwipe': 'admin_account_wipe',
|
|
'adminaccountwipe': 'admin_account_wipe',
|
|
'wipeaccount': 'admin_account_wipe',
|
|
'adminremotewipe': 'admin_remote_wipe',
|
|
'wipe': 'admin_remote_wipe',
|
|
'approve': 'approve',
|
|
'block': 'block',
|
|
'cancelremotewipethenactivate': 'cancel_remote_wipe_then_activate',
|
|
'cancelremotewipethenblock': 'cancel_remote_wipe_then_block',
|
|
}
|
|
|
|
def getMobileDeviceEntity():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
if checkArgumentPresent('query'):
|
|
query = getString(Cmd.OB_QUERY)
|
|
else:
|
|
resourceId = getString(Cmd.OB_MOBILE_DEVICE_ENTITY)
|
|
if resourceId[:6].lower() == 'query:':
|
|
query = resourceId[6:]
|
|
else:
|
|
Cmd.Backup()
|
|
query = None
|
|
if not query:
|
|
return ([{'resourceId': device, 'email': []} for device in getEntityList(Cmd.OB_MOBILE_ENTITY)], cd, True)
|
|
try:
|
|
printGettingAllAccountEntities(Ent.MOBILE_DEVICE, query)
|
|
devices = callGAPIpages(cd.mobiledevices(), 'list', 'mobiledevices',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], query=query,
|
|
fields='nextPageToken,mobiledevices(resourceId,email)')
|
|
except GAPI.invalidInput:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_QUERY)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
return ([{'resourceId': device['resourceId'], 'email': device.get('email', [])} for device in devices], cd, False)
|
|
|
|
def _getUpdateDeleteMobileOptions(myarg, options):
|
|
if myarg in {'matchusers', 'ifusers'}:
|
|
_, matchUsers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
options['matchUsers'] = {normalizeEmailAddressOrUID(user) for user in matchUsers}
|
|
elif myarg == 'doit':
|
|
options['doit'] = True
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
def _getMobileDeviceUser(mobileDevice, options):
|
|
if options['matchUsers']:
|
|
if mobileDevice['email']:
|
|
for deviceUser in mobileDevice['email']:
|
|
if deviceUser.lower() in options['matchUsers']:
|
|
return (deviceUser, True)
|
|
return (mobileDevice['email'][0], False)
|
|
return (UNKNOWN, False)
|
|
if mobileDevice['email']:
|
|
return (mobileDevice['email'][0], True)
|
|
return (UNKNOWN, True)
|
|
|
|
# gam update mobile|mobiles <MobileDeviceEntity> action <MobileAction>
|
|
# [doit] [matchusers <UserTypeEntity>]
|
|
def doUpdateMobileDevices():
|
|
entityList, cd, doit = getMobileDeviceEntity()
|
|
body = {}
|
|
options = {'doit': doit, 'matchUsers': set()}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'action':
|
|
body['action'] = getChoice(MOBILE_ACTION_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
_getUpdateDeleteMobileOptions(myarg, options)
|
|
if not body:
|
|
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, None], Msg.NO_ACTION_SPECIFIED)
|
|
return
|
|
i = 0
|
|
count = len(entityList)
|
|
for device in entityList:
|
|
i += 1
|
|
resourceId = device['resourceId']
|
|
deviceUser, status = _getMobileDeviceUser(device, options)
|
|
if not status:
|
|
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], Msg.USER_NOT_IN_MATCHUSERS, i, count)
|
|
elif not options['doit']:
|
|
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
|
|
else:
|
|
try:
|
|
callGAPI(cd.mobiledevices(), 'action',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.RESOURCE_ID_NOT_FOUND,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], resourceId=resourceId, body=body)
|
|
printEntityKVList([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser],
|
|
[Msg.ACTION_APPLIED, body['action']], i, count)
|
|
except GAPI.internalError:
|
|
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.resourceIdNotFound, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], str(e), i, count)
|
|
|
|
# gam delete mobile|mobiles <MobileDeviceEntity>
|
|
# [doit] [matchusers <UserTypeEntity>]
|
|
def doDeleteMobileDevices():
|
|
entityList, cd, doit = getMobileDeviceEntity()
|
|
options = {'doit': doit, 'matchUsers': set()}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
_getUpdateDeleteMobileOptions(myarg, options)
|
|
i = 0
|
|
count = len(entityList)
|
|
for device in entityList:
|
|
i += 1
|
|
resourceId = device['resourceId']
|
|
deviceUser, status = _getMobileDeviceUser(device, options)
|
|
if not status:
|
|
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], Msg.USER_NOT_IN_MATCHUSERS, i, count)
|
|
elif not options['doit']:
|
|
entityActionNotPerformedWarning([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
|
|
else:
|
|
try:
|
|
callGAPI(cd.mobiledevices(), 'delete',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.RESOURCE_ID_NOT_FOUND, GAPI.BAD_REQUEST,
|
|
GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], resourceId=resourceId)
|
|
entityActionPerformed([Ent.MOBILE_DEVICE, resourceId, Ent.USER, deviceUser], i, count)
|
|
except GAPI.internalError:
|
|
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.resourceIdNotFound, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], str(e), i, count)
|
|
|
|
MOBILE_FIELDS_CHOICE_MAP = {
|
|
'adbstatus': 'adbStatus',
|
|
'applications': 'applications',
|
|
'basebandversion': 'basebandVersion',
|
|
'bootloaderversion': 'bootloaderVersion',
|
|
'brand': 'brand',
|
|
'buildnumber': 'buildNumber',
|
|
'defaultlanguage': 'defaultLanguage',
|
|
'developeroptionsstatus': 'developerOptionsStatus',
|
|
'devicecompromisedstatus': 'deviceCompromisedStatus',
|
|
'deviceid': 'deviceId',
|
|
'devicepasswordstatus': 'devicePasswordStatus',
|
|
'email': 'email',
|
|
'encryptionstatus': 'encryptionStatus',
|
|
'firstsync': 'firstSync',
|
|
'hardware': 'hardware',
|
|
'hardwareid': 'hardwareId',
|
|
'imei': 'imei',
|
|
'kernelversion': 'kernelVersion',
|
|
'lastsync': 'lastSync',
|
|
'managedaccountisonownerprofile': 'managedAccountIsOnOwnerProfile',
|
|
'manufacturer': 'manufacturer',
|
|
'meid': 'meid',
|
|
'model': 'model',
|
|
'name': 'name',
|
|
'networkoperator': 'networkOperator',
|
|
'os': 'os',
|
|
'otheraccountsinfo': 'otherAccountsInfo',
|
|
'privilege': 'privilege',
|
|
'releaseversion': 'releaseVersion',
|
|
'resourceid': 'resourceId',
|
|
'securitypatchlevel': 'securityPatchLevel',
|
|
'serialnumber': 'serialNumber',
|
|
'status': 'status',
|
|
'supportsworkprofile': 'supportsWorkProfile',
|
|
'type': 'type',
|
|
'unknownsourcesstatus': 'unknownSourcesStatus',
|
|
'useragent': 'userAgent',
|
|
'wifimacaddress': 'wifiMacAddress',
|
|
}
|
|
|
|
MOBILE_TIME_OBJECTS = {'firstSync', 'lastSync'}
|
|
|
|
def _initMobileFieldsParameters():
|
|
return {'fieldsList': [], 'projection': None}
|
|
|
|
def _getMobileFieldsArguments(myarg, parameters):
|
|
if myarg == 'allfields':
|
|
parameters['projection'] = 'FULL'
|
|
parameters['fieldsList'] = []
|
|
elif myarg in PROJECTION_CHOICE_MAP:
|
|
parameters['projection'] = PROJECTION_CHOICE_MAP[myarg]
|
|
parameters['fieldsList'] = []
|
|
elif getFieldsList(myarg, MOBILE_FIELDS_CHOICE_MAP, parameters['fieldsList'], initialField='resourceId'):
|
|
pass
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
# gam info mobile|mobiles <MobileDeviceEntity>
|
|
# [basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>] [formatjson]
|
|
def doInfoMobileDevices():
|
|
entityList, cd, _ = getMobileDeviceEntity()
|
|
parameters = _initMobileFieldsParameters()
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getMobileFieldsArguments(myarg, parameters):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(parameters['fieldsList'])
|
|
i = 0
|
|
count = len(entityList)
|
|
for device in entityList:
|
|
i += 1
|
|
resourceId = device['resourceId']
|
|
try:
|
|
mobile = callGAPI(cd.mobiledevices(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.RESOURCE_ID_NOT_FOUND,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], resourceId=resourceId, projection=parameters['projection'], fields=fields)
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(mobile, timeObjects=MOBILE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
printEntity([Ent.MOBILE_DEVICE, resourceId], i, count)
|
|
Ind.Increment()
|
|
attrib = 'deviceId'
|
|
if attrib in mobile:
|
|
mobile[attrib] = mobile[attrib].encode('unicode-escape').decode(UTF8)
|
|
attrib = 'securityPatchLevel'
|
|
if attrib in mobile and int(mobile[attrib]):
|
|
mobile[attrib] = formatLocalTimestamp(mobile[attrib])
|
|
showJSON(None, mobile, timeObjects=MOBILE_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
except GAPI.internalError:
|
|
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.resourceIdNotFound, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.MOBILE_DEVICE, resourceId], str(e), i, count)
|
|
|
|
MOBILE_ORDERBY_CHOICE_MAP = {
|
|
'deviceid': 'deviceId',
|
|
'email': 'email',
|
|
'lastsync': 'lastSync',
|
|
'model': 'model',
|
|
'name': 'name',
|
|
'os': 'os',
|
|
'status': 'status',
|
|
'type': 'type',
|
|
}
|
|
|
|
# gam print mobile [todrive <ToDriveAttribute>*]
|
|
# [(query <QueryMobile>)|(queries <QueryMobileList>) [querytime<String> <Time>]]
|
|
# [orderby <MobileOrderByFieldName> [ascending|descending]]
|
|
# [basic|full|allfields] <MobileFieldName>* [fields <MobileFieldNameList>]
|
|
# [delimiter <Character>] [appslimit <Number>] [oneappperrow] [listlimit <Number>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [showitemcountonly]
|
|
def doPrintMobileDevices():
|
|
def _appDetails(app):
|
|
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>')
|
|
return '-'.join(appDetails)
|
|
|
|
def _printMobile(mobile):
|
|
if FJQC.formatJSON:
|
|
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(flattenJSON(mobile, listLimit=listLimit, timeObjects=MOBILE_TIME_OBJECTS)):
|
|
csvPF.WriteRowNoFilter({'resourceId': mobile['resourceId'],
|
|
'JSON': json.dumps(cleanJSON(mobile, listLimit=listLimit, timeObjects=MOBILE_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
return
|
|
row = {}
|
|
for attrib in mobile:
|
|
if attrib in DEFAULT_SKIP_OBJECTS:
|
|
pass
|
|
elif attrib in {'name', 'email', 'otherAccountsInfo'}:
|
|
if listLimit > 0:
|
|
row[attrib] = delimiter.join(mobile[attrib][0:listLimit])
|
|
elif listLimit == 0:
|
|
row[attrib] = delimiter.join(mobile[attrib])
|
|
elif attrib == 'deviceId':
|
|
row[attrib] = mobile[attrib].encode('unicode-escape').decode(UTF8)
|
|
elif attrib in MOBILE_TIME_OBJECTS:
|
|
row[attrib] = formatLocalTime(mobile[attrib])
|
|
elif attrib == 'securityPatchLevel' and int(mobile[attrib]):
|
|
row[attrib] = formatLocalTimestamp(mobile[attrib])
|
|
elif attrib != 'applications':
|
|
row[attrib] = mobile[attrib]
|
|
attrib = 'applications'
|
|
if not oneAppPerRow or attrib not in mobile or appsLimit < 0:
|
|
if attrib in mobile and appsLimit >= 0:
|
|
applications = []
|
|
j = 0
|
|
for app in mobile[attrib]:
|
|
j += 1
|
|
if appsLimit and (j > appsLimit):
|
|
break
|
|
applications.append(_appDetails(app))
|
|
row[attrib] = delimiter.join(applications)
|
|
csvPF.WriteRowTitles(row)
|
|
else:
|
|
j = 0
|
|
for app in mobile[attrib]:
|
|
j += 1
|
|
if appsLimit and (j > appsLimit):
|
|
break
|
|
appRow = row.copy()
|
|
appRow[attrib] = _appDetails(app)
|
|
csvPF.WriteRowTitles(appRow)
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
parameters = _initMobileFieldsParameters()
|
|
csvPF = CSVPrintFile('resourceId')
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
orderBy = sortOrder = None
|
|
oneAppPerRow = False
|
|
queryTimes = {}
|
|
queries = [None]
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
listLimit = 1
|
|
appsLimit = -1
|
|
showItemCountOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'query', 'queries'}:
|
|
queries = getQueries(myarg)
|
|
elif myarg.startswith('querytime'):
|
|
queryTimes[myarg] = getTimeOrDeltaFromNow()[0:19]
|
|
elif myarg == 'orderby':
|
|
orderBy, sortOrder = getOrderBySortOrder(MOBILE_ORDERBY_CHOICE_MAP)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg == 'listlimit':
|
|
listLimit = getInteger(minVal=-1)
|
|
elif myarg == 'appslimit':
|
|
appsLimit = getInteger(minVal=-1)
|
|
elif myarg == 'oneappperrow':
|
|
oneAppPerRow = True
|
|
elif _getMobileFieldsArguments(myarg, parameters):
|
|
pass
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if not FJQC.formatJSON:
|
|
csvPF.SetSortTitles(['resourceId', 'deviceId', 'serialNumber', 'name', 'email', 'status'])
|
|
if appsLimit >= 0:
|
|
parameters['projection'] = 'FULL'
|
|
fields = getItemFieldsFromFieldsList('mobiledevices', parameters['fieldsList'])
|
|
substituteQueryTimes(queries, queryTimes)
|
|
itemCount = 0
|
|
for query in queries:
|
|
printGettingAllAccountEntities(Ent.MOBILE_DEVICE, query)
|
|
pageMessage = getPageMessage()
|
|
totalItems = 0
|
|
try:
|
|
feed = yieldGAPIpages(cd.mobiledevices(), 'list', 'mobiledevices',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], query=query, projection=parameters['projection'],
|
|
orderBy=orderBy, sortOrder=sortOrder, fields=fields, maxResults=GC.Values[GC.MOBILE_MAX_RESULTS])
|
|
for mobiles in feed:
|
|
totalItems += len(mobiles)
|
|
if showItemCountOnly:
|
|
itemCount += len(mobiles)
|
|
continue
|
|
for mobile in mobiles:
|
|
_printMobile(mobile)
|
|
printGotAccountEntities(totalItems)
|
|
except GAPI.invalidInput:
|
|
entityActionFailedWarning([Ent.MOBILE_DEVICE, None], invalidQuery(query))
|
|
return
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{itemCount}\n')
|
|
return
|
|
csvPF.writeCSVfile('Mobile')
|
|
|
|
GROUP_DISCOVER_CHOICES = {
|
|
'allmemberscandiscover': 'ALL_MEMBERS_CAN_DISCOVER',
|
|
'allindomaincandiscover': 'ALL_IN_DOMAIN_CAN_DISCOVER',
|
|
'anyonecandiscover': 'ANYONE_CAN_DISCOVER',
|
|
}
|
|
GROUP_ASSIST_CONTENT_CHOICES = {
|
|
'allmembers': 'ALL_MEMBERS',
|
|
'ownersandmanagers': 'OWNERS_AND_MANAGERS',
|
|
'managersonly': 'MANAGERS_ONLY',
|
|
'ownersonly': 'OWNERS_ONLY',
|
|
'none': 'NONE',
|
|
}
|
|
GROUP_MODERATE_CONTENT_CHOICES = {
|
|
'allmembers': 'ALL_MEMBERS',
|
|
'ownersandmanagers': 'OWNERS_AND_MANAGERS',
|
|
'ownersonly': 'OWNERS_ONLY',
|
|
'none': 'NONE',
|
|
}
|
|
GROUP_MODERATE_MEMBERS_CHOICES = {
|
|
'allmembers': 'ALL_MEMBERS',
|
|
'ownersandmanagers': 'OWNERS_AND_MANAGERS',
|
|
'ownersonly': 'OWNERS_ONLY',
|
|
'none': 'NONE',
|
|
}
|
|
GROUP_DEPRECATED_ATTRIBUTES = {
|
|
'allowgooglecommunication': ['allowGoogleCommunication', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'favoriterepliesontop': ['favoriteRepliesOnTop', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'maxmessagebytes': ['maxMessageBytes', {GC.VAR_TYPE: GC.TYPE_INTEGER, GC.VAR_LIMITS: (ONE_KILO_BYTES, ONE_MEGA_BYTES)}],
|
|
'messagedisplayfont': ['messageDisplayFont', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'defaultfont': 'DEFAULT_FONT', 'fixedwidthfont': 'FIXED_WIDTH_FONT'}}],
|
|
'whocanaddreferences': ['whoCanAddReferences', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanmarkfavoritereplyonowntopic': ['whoCanMarkFavoriteReplyOnOwnTopic', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
}
|
|
GROUP_DISCOVER_ATTRIBUTES = {
|
|
'showingroupdirectory': ['showInGroupDirectory', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
}
|
|
GROUP_ASSIST_CONTENT_ATTRIBUTES = {
|
|
'whocanassigntopics': ['whoCanAssignTopics', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanenterfreeformtags': ['whoCanEnterFreeFormTags', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanhideabuse': ['whoCanHideAbuse', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanmaketopicssticky': ['whoCanMakeTopicsSticky', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanmarkduplicate': ['whoCanMarkDuplicate', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanmarkfavoritereplyonanytopic': ['whoCanMarkFavoriteReplyOnAnyTopic', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanmarknoresponseneeded': ['whoCanMarkNoResponseNeeded', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanmodifytagsandcategories': ['whoCanModifyTagsAndCategories', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocantaketopics': ['whoCanTakeTopics', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanunassigntopic': ['whoCanUnassignTopic', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanunmarkfavoritereplyonanytopic': ['whoCanUnmarkFavoriteReplyOnAnyTopic', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
}
|
|
GROUP_MODERATE_CONTENT_ATTRIBUTES = {
|
|
'whocanapprovemessages': ['whoCanApproveMessages', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
|
|
'whocandeleteanypost': ['whoCanDeleteAnyPost', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
|
|
'whocandeletetopics': ['whoCanDeleteTopics', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
|
|
'whocanlocktopics': ['whoCanLockTopics', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
|
|
'whocanmovetopicsin': ['whoCanMoveTopicsIn', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
|
|
'whocanmovetopicsout': ['whoCanMoveTopicsOut', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
|
|
'whocanpostannouncements': ['whoCanPostAnnouncements', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
|
|
}
|
|
GROUP_MODERATE_MEMBERS_ATTRIBUTES = {
|
|
'whocanadd': ['whoCanAdd', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'allmanagerscanadd': 'ALL_MANAGERS_CAN_ADD', 'allownerscanadd': 'ALL_OWNERS_CAN_ADD',
|
|
'allmemberscanadd': 'ALL_MEMBERS_CAN_ADD', 'nonecanadd': 'NONE_CAN_ADD'}}],
|
|
'whocanapprovemembers': ['whoCanApproveMembers', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'allownerscanapprove': 'ALL_OWNERS_CAN_APPROVE', 'allmanagerscanapprove': 'ALL_MANAGERS_CAN_APPROVE',
|
|
'allmemberscanapprove': 'ALL_MEMBERS_CAN_APPROVE', 'nonecanapprove': 'NONE_CAN_APPROVE'}}],
|
|
'whocanbanusers': ['whoCanBanUsers', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_MEMBERS_CHOICES}],
|
|
'whocaninvite': ['whoCanInvite', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'allmemberscaninvite': 'ALL_MEMBERS_CAN_INVITE', 'allmanagerscaninvite': 'ALL_MANAGERS_CAN_INVITE',
|
|
'allownerscaninvite': 'ALL_OWNERS_CAN_INVITE', 'nonecaninvite': 'NONE_CAN_INVITE'}}],
|
|
'whocanmodifymembers': ['whoCanModifyMembers', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_MEMBERS_CHOICES}],
|
|
}
|
|
GROUP_BASIC_ATTRIBUTES = {
|
|
'description': ['description', {GC.VAR_TYPE: GC.TYPE_STRING}],
|
|
'name': ['name', {GC.VAR_TYPE: GC.TYPE_STRING}],
|
|
'displayname': ['name', {GC.VAR_TYPE: GC.TYPE_STRING}],
|
|
}
|
|
GROUP_SETTINGS_ATTRIBUTES = {
|
|
'allowexternalmembers': ['allowExternalMembers', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'allowwebposting': ['allowWebPosting', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'archiveonly': ['archiveOnly', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'customfootertext': ['customFooterText', {GC.VAR_TYPE: GC.TYPE_STRING}],
|
|
'customreplyto': ['customReplyTo', {GC.VAR_TYPE: GC.TYPE_EMAIL_OPTIONAL}],
|
|
'customrolesenabledforsettingstobemerged': ['customRolesEnabledForSettingsToBeMerged', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'defaultmessagedenynotificationtext': ['defaultMessageDenyNotificationText', {GC.VAR_TYPE: GC.TYPE_STRING}],
|
|
'defaultsender': ['defaultSender', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'self': 'DEFAULT_SELF', 'defaultself': 'DEFAULT_SELF', 'group': 'GROUP'}}],
|
|
'enablecollaborativeinbox': ['enableCollaborativeInbox', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'includecustomfooter': ['includeCustomFooter', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'includeinglobaladdresslist': ['includeInGlobalAddressList', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'isarchived': ['isArchived', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'memberscanpostasthegroup': ['membersCanPostAsTheGroup', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'messagemoderationlevel': ['messageModerationLevel', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'moderateallmessages': 'MODERATE_ALL_MESSAGES', 'moderatenonmembers': 'MODERATE_NON_MEMBERS',
|
|
'moderatenewmembers': 'MODERATE_NEW_MEMBERS', 'moderatenone': 'MODERATE_NONE'}}],
|
|
'primarylanguage': ['primaryLanguage', {GC.VAR_TYPE: GC.TYPE_LANGUAGE}],
|
|
'replyto': ['replyTo', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'replytocustom': 'REPLY_TO_CUSTOM', 'replytosender': 'REPLY_TO_SENDER', 'replytolist': 'REPLY_TO_LIST',
|
|
'replytoowner': 'REPLY_TO_OWNER', 'replytoignore': 'REPLY_TO_IGNORE', 'replytomanagers': 'REPLY_TO_MANAGERS'}}],
|
|
'sendmessagedenynotification': ['sendMessageDenyNotification', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'spammoderationlevel': ['spamModerationLevel', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'allow': 'ALLOW', 'moderate': 'MODERATE', 'silentlymoderate': 'SILENTLY_MODERATE', 'reject': 'REJECT'}}],
|
|
'whocancontactowner': ['whoCanContactOwner', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'anyonecancontact': 'ANYONE_CAN_CONTACT', 'allindomaincancontact': 'ALL_IN_DOMAIN_CAN_CONTACT',
|
|
'allmemberscancontact': 'ALL_MEMBERS_CAN_CONTACT', 'allmanagerscancontact': 'ALL_MANAGERS_CAN_CONTACT',
|
|
'allownerscancontact': 'ALL_OWNERS_CAN_CONTACT'}}],
|
|
'whocanjoin': ['whoCanJoin', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'anyonecanjoin': 'ANYONE_CAN_JOIN', 'allindomaincanjoin': 'ALL_IN_DOMAIN_CAN_JOIN',
|
|
'invitedcanjoin': 'INVITED_CAN_JOIN', 'canrequesttojoin': 'CAN_REQUEST_TO_JOIN'}}],
|
|
'whocanleavegroup': ['whoCanLeaveGroup', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'allmanagerscanleave': 'ALL_MANAGERS_CAN_LEAVE', 'allownerscanleave': 'ALL_OWNERS_CAN_LEAVE',
|
|
'allmemberscanleave': 'ALL_MEMBERS_CAN_LEAVE', 'nonecanleave': 'NONE_CAN_LEAVE'}}],
|
|
'whocanpostmessage': ['whoCanPostMessage', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'nonecanpost': 'NONE_CAN_POST', 'allmanagerscanpost': 'ALL_MANAGERS_CAN_POST',
|
|
'allmemberscanpost': 'ALL_MEMBERS_CAN_POST', 'allownerscanpost': 'ALL_OWNERS_CAN_POST',
|
|
'allindomaincanpost': 'ALL_IN_DOMAIN_CAN_POST', 'anyonecanpost': 'ANYONE_CAN_POST'}}],
|
|
'whocanviewgroup': ['whoCanViewGroup', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'anyonecanview': 'ANYONE_CAN_VIEW', 'allindomaincanview': 'ALL_IN_DOMAIN_CAN_VIEW',
|
|
'allmemberscanview': 'ALL_MEMBERS_CAN_VIEW', 'allmanagerscanview': 'ALL_MANAGERS_CAN_VIEW',
|
|
'allownerscanview': 'ALL_OWNERS_CAN_VIEW'}}],
|
|
'whocanviewmembership': ['whoCanViewMembership', {GC.VAR_TYPE: GC.TYPE_CHOICE,
|
|
'choices': {'allindomaincanview': 'ALL_IN_DOMAIN_CAN_VIEW', 'allmemberscanview': 'ALL_MEMBERS_CAN_VIEW',
|
|
'allmanagerscanview': 'ALL_MANAGERS_CAN_VIEW', 'allownerscanview': 'ALL_OWNERS_CAN_VIEW'}}],
|
|
}
|
|
GROUP_ALIAS_ATTRIBUTES = {
|
|
'collaborative': ['enableCollaborativeInbox', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
'gal': ['includeInGlobalAddressList', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}],
|
|
}
|
|
GROUP_MERGED_ATTRIBUTES = {
|
|
'whocandiscovergroup': ['whoCanDiscoverGroup', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_DISCOVER_CHOICES}],
|
|
'whocanassistcontent': ['whoCanAssistContent', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_ASSIST_CONTENT_CHOICES}],
|
|
'whocanmoderatecontent': ['whoCanModerateContent', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_CONTENT_CHOICES}],
|
|
'whocanmoderatemembers': ['whoCanModerateMembers', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': GROUP_MODERATE_MEMBERS_CHOICES}],
|
|
}
|
|
GROUP_MERGED_ATTRIBUTES_PRINT_ORDER = ['whoCanDiscoverGroup', 'whoCanAssistContent', 'whoCanModerateContent', 'whoCanModerateMembers']
|
|
GROUP_MERGED_TO_COMPONENT_MAP = {
|
|
'whoCanDiscoverGroup': GROUP_DISCOVER_ATTRIBUTES,
|
|
'whoCanAssistContent': GROUP_ASSIST_CONTENT_ATTRIBUTES,
|
|
'whoCanModerateContent': GROUP_MODERATE_CONTENT_ATTRIBUTES,
|
|
'whoCanModerateMembers': GROUP_MODERATE_MEMBERS_ATTRIBUTES,
|
|
}
|
|
GROUP_ATTRIBUTES_SET = set(list(GROUP_BASIC_ATTRIBUTES)+list(GROUP_SETTINGS_ATTRIBUTES)+list(GROUP_ALIAS_ATTRIBUTES)+
|
|
list(GROUP_ASSIST_CONTENT_ATTRIBUTES)+list(GROUP_MODERATE_CONTENT_ATTRIBUTES)+list(GROUP_MODERATE_MEMBERS_ATTRIBUTES)+
|
|
list(GROUP_MERGED_ATTRIBUTES)+list(GROUP_DEPRECATED_ATTRIBUTES))
|
|
GROUP_FIELDS_WITH_CRS_NLS = {'customFooterText', 'defaultMessageDenyNotificationText', 'description'}
|
|
|
|
def getGroupAttrProperties(myarg):
|
|
attrProperties = GROUP_BASIC_ATTRIBUTES.get(myarg)
|
|
if attrProperties is not None:
|
|
return attrProperties
|
|
attrProperties = GROUP_SETTINGS_ATTRIBUTES.get(myarg)
|
|
if attrProperties is not None:
|
|
return attrProperties
|
|
attrProperties = GROUP_ALIAS_ATTRIBUTES.get(myarg)
|
|
if attrProperties is not None:
|
|
return attrProperties
|
|
attrProperties = GROUP_DISCOVER_ATTRIBUTES.get(myarg)
|
|
if attrProperties is not None:
|
|
return attrProperties
|
|
attrProperties = GROUP_ASSIST_CONTENT_ATTRIBUTES.get(myarg)
|
|
if attrProperties is not None:
|
|
return attrProperties
|
|
attrProperties = GROUP_MODERATE_CONTENT_ATTRIBUTES.get(myarg)
|
|
if attrProperties is not None:
|
|
return attrProperties
|
|
attrProperties = GROUP_MODERATE_MEMBERS_ATTRIBUTES.get(myarg)
|
|
if attrProperties is not None:
|
|
return attrProperties
|
|
attrProperties = GROUP_MERGED_ATTRIBUTES.get(myarg)
|
|
if attrProperties is not None:
|
|
return attrProperties
|
|
attrProperties = GROUP_DEPRECATED_ATTRIBUTES.get(myarg)
|
|
if attrProperties is not None:
|
|
return attrProperties
|
|
return None
|
|
|
|
def getGroupAttrValue(argument, gs_body):
|
|
if argument == 'copyfrom':
|
|
gs_body[argument] = getEmailAddress()
|
|
return
|
|
attrProperties = getGroupAttrProperties(argument)
|
|
if attrProperties is None:
|
|
unknownArgumentExit()
|
|
attrName = attrProperties[0]
|
|
attribute = attrProperties[1]
|
|
attrType = attribute[GC.VAR_TYPE]
|
|
if attrType == GC.TYPE_BOOLEAN:
|
|
gs_body[attrName] = str(getBoolean()).lower()
|
|
elif attrType == GC.TYPE_STRING:
|
|
if attrName in GROUP_FIELDS_WITH_CRS_NLS:
|
|
gs_body[attrName] = getStringWithCRsNLs()
|
|
else:
|
|
gs_body[attrName] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif attrType == GC.TYPE_CHOICE:
|
|
gs_body[attrName] = getChoice(attribute['choices'], mapChoice=True)
|
|
elif attrType in [GC.TYPE_EMAIL, GC.TYPE_EMAIL_OPTIONAL]:
|
|
gs_body[attrName] = getEmailAddress(noUid=True, optional=attrType == GC.TYPE_EMAIL_OPTIONAL)
|
|
if attrType == GC.TYPE_EMAIL_OPTIONAL and gs_body[attrName] is None:
|
|
gs_body[attrName] = ''
|
|
elif attrType == GC.TYPE_LANGUAGE:
|
|
gs_body[attrName] = getLanguageCode(LANGUAGE_CODES_MAP)
|
|
else: # GC.TYPE_INTEGER
|
|
minVal, maxVal = attribute[GC.VAR_LIMITS]
|
|
if attrName == 'maxMessageBytes':
|
|
gs_body[attrName] = getMaxMessageBytes(minVal, maxVal)
|
|
else:
|
|
gs_body[attrName] = getInteger(minVal, maxVal)
|
|
|
|
def GroupIsAbuseOrPostmaster(emailAddr):
|
|
return emailAddr.startswith('abuse@') or emailAddr.startswith('postmaster@')
|
|
|
|
def mapGroupEmailForSettings(emailAddr):
|
|
return emailAddr.replace('/', '%2F')
|
|
|
|
def getSettingsFromGroup(cd, group, gs, gs_body):
|
|
if gs_body:
|
|
copySettingsFromGroup = gs_body.pop('copyfrom', None)
|
|
if copySettingsFromGroup:
|
|
try:
|
|
if copySettingsFromGroup.find('@') == -1: # group settings API won't take uid so we make sure cd API is used so that we can grab real email.
|
|
copySettingsFromGroup = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=copySettingsFromGroup, fields='email')['email']
|
|
settings = callGAPI(gs.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
|
|
groupUniqueId=mapGroupEmailForSettings(copySettingsFromGroup), fields='*')
|
|
if settings is not None:
|
|
for field in ['email', 'name', 'description']:
|
|
settings.pop(field, None)
|
|
settings.update(gs_body)
|
|
return settings
|
|
entityItemValueListActionNotPerformedWarning([Ent.GROUP, group], [Ent.COPYFROM_GROUP, copySettingsFromGroup], Msg.API_ERROR_SETTINGS)
|
|
return None
|
|
except GAPI.notFound:
|
|
entityItemValueListActionNotPerformedWarning([Ent.GROUP, group], [Ent.COPYFROM_GROUP, copySettingsFromGroup], Msg.DOES_NOT_EXIST)
|
|
return None
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.backendError, GAPI.invalid, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
|
|
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
|
entityItemValueListActionNotPerformedWarning([Ent.GROUP, group], [Ent.COPYFROM_GROUP, copySettingsFromGroup], str(e))
|
|
return None
|
|
return gs_body
|
|
|
|
def checkReplyToCustom(group, settings, i=0, count=0):
|
|
if settings.get('replyTo') != 'REPLY_TO_CUSTOM' or settings.get('customReplyTo', ''):
|
|
return True
|
|
entityActionNotPerformedWarning([Ent.GROUP, group], Msg.REPLY_TO_CUSTOM_REQUIRES_EMAIL_ADDRESS, i, count)
|
|
return False
|
|
|
|
GROUP_CIGROUP_ENTITYTYPE_MAP = {False: Ent.GROUP, True: Ent.CLOUD_IDENTITY_GROUP}
|
|
GROUP_CIGROUP_FIELDS_MAP = {'name': 'displayName', 'displayname': 'displayName', 'description': 'description'}
|
|
GROUP_JSON_SKIP_FIELDS = ['email', 'adminCreated', 'directMembersCount', 'members', 'aliases', 'nonEditableAliases']
|
|
GROUP_ACCESS_TYPE_CHOICE_MAP = {
|
|
'public': {
|
|
'whoCanJoin': 'ALL_IN_DOMAIN_CAN_JOIN',
|
|
'whoCanPostMessage': 'ALL_IN_DOMAIN_CAN_POST',
|
|
'whoCanViewGroup': 'ALL_IN_DOMAIN_CAN_VIEW',
|
|
'whoCanViewMembership': 'ALL_IN_DOMAIN_CAN_VIEW',
|
|
},
|
|
'team': {
|
|
'whoCanJoin': 'CAN_REQUEST_TO_JOIN',
|
|
'whoCanPostMessage': 'ALL_IN_DOMAIN_CAN_POST',
|
|
'whoCanViewGroup': 'ALL_IN_DOMAIN_CAN_VIEW',
|
|
'whoCanViewMembership': 'ALL_IN_DOMAIN_CAN_VIEW',
|
|
},
|
|
'announcementonly': {
|
|
'whoCanJoin': 'ALL_IN_DOMAIN_CAN_JOIN',
|
|
'whoCanPostMessage': 'ALL_MANAGERS_CAN_POST',
|
|
'whoCanViewGroup': 'ALL_IN_DOMAIN_CAN_VIEW',
|
|
'whoCanViewMembership': 'ALL_MANAGERS_CAN_VIEW',
|
|
},
|
|
'restricted': {
|
|
'whoCanJoin': 'CAN_REQUEST_TO_JOIN',
|
|
'whoCanPostMessage': 'ALL_MEMBERS_CAN_POST',
|
|
'whoCanViewGroup': 'ALL_MEMBERS_CAN_VIEW',
|
|
'whoCanViewMembership': 'ALL_MEMBERS_CAN_VIEW',
|
|
}
|
|
}
|
|
|
|
# gam create group <EmailAddress> [copyfrom <GroupItem>] <GroupAttribute>*
|
|
# [verifynotinvitable]
|
|
def doCreateGroup(ciGroupsAPI=False):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
verifyNotInvitable = getBeforeUpdate = False
|
|
groupEmail = getEmailAddress(noUid=True)
|
|
entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI]
|
|
if not ciGroupsAPI:
|
|
ci = None
|
|
body = {'email': groupEmail}
|
|
else:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
initialGroupConfig = 'EMPTY'
|
|
setTrueCustomerId(cd)
|
|
parent = f'customers/{GC.Values[GC.CUSTOMER_ID]}'
|
|
body = {'groupKey': {'id': groupEmail},
|
|
'parent': parent,
|
|
'labels': {CIGROUP_DISCUSSION_FORUM_LABEL: ''},
|
|
}
|
|
gs_body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'getbeforeupdate':
|
|
getBeforeUpdate = True
|
|
elif myarg == 'json':
|
|
gs_body.update(getJSON(GROUP_JSON_SKIP_FIELDS))
|
|
elif myarg == 'accesstype':
|
|
gs_body.update(getChoice(GROUP_ACCESS_TYPE_CHOICE_MAP, mapChoice=True))
|
|
elif ciGroupsAPI and myarg in ['alias', 'aliases']:
|
|
body.setdefault('additionalGroupKeys', [])
|
|
for alias in convertEntityToList(getString(Cmd.OB_CIGROUP_ALIAS_LIST), shlexSplit=True):
|
|
body['additionalGroupKeys'].append({'id': alias})
|
|
elif ciGroupsAPI and myarg == 'dynamic':
|
|
body.setdefault('dynamicGroupMetadata', {'queries': []})
|
|
body['dynamicGroupMetadata']['queries'].append({'resourceType': 'USER',
|
|
'query': getString(Cmd.OB_QUERY)})
|
|
elif ciGroupsAPI and myarg == 'makeowner':
|
|
initialGroupConfig = 'WITH_INITIAL_OWNER'
|
|
elif ciGroupsAPI and myarg in {'security', 'makesecuritygroup'}:
|
|
body['labels'][CIGROUP_SECURITY_LABEL] = ''
|
|
elif myarg == 'verifynotinvitable':
|
|
verifyNotInvitable = True
|
|
else:
|
|
getGroupAttrValue(myarg, gs_body)
|
|
if verifyNotInvitable:
|
|
isInvitableUser, _ = _getIsInvitableUser(None, groupEmail)
|
|
if isInvitableUser:
|
|
entityActionNotPerformedWarning([Ent.GROUP, groupEmail], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT)
|
|
return
|
|
if ciGroupsAPI:
|
|
for k, v in iter(GROUP_CIGROUP_FIELDS_MAP.items()):
|
|
if k in gs_body:
|
|
body[v] = gs_body.pop(k)
|
|
body.setdefault('displayName', groupEmail)
|
|
if gs_body:
|
|
gs_body.setdefault('name', body.get('displayName', groupEmail))
|
|
gs = buildGAPIObject(API.GROUPSSETTINGS)
|
|
gs_body = getSettingsFromGroup(cd, groupEmail, gs, gs_body)
|
|
if not gs_body or not checkReplyToCustom(groupEmail, gs_body):
|
|
return
|
|
if not getBeforeUpdate:
|
|
settings = gs_body
|
|
try:
|
|
if not ciGroupsAPI:
|
|
callGAPI(cd.groups(), 'insert',
|
|
throwReasons=GAPI.GROUP_CREATE_THROW_REASONS,
|
|
body=body, fields='')
|
|
else:
|
|
callGAPI(ci.groups(), 'create',
|
|
throwReasons=GAPI.CIGROUP_CREATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
initialGroupConfig=initialGroupConfig, body=body, fields='')
|
|
if gs_body and not GroupIsAbuseOrPostmaster(groupEmail):
|
|
if getBeforeUpdate:
|
|
settings = callGAPI(gs.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
|
|
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND],
|
|
groupUniqueId=mapGroupEmailForSettings(groupEmail), fields='*')
|
|
settings.update(gs_body)
|
|
callGAPI(gs.groups(), 'update',
|
|
bailOnInvalidError='messageModerationLevel' in settings,
|
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS,
|
|
retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
|
|
groupUniqueId=mapGroupEmailForSettings(groupEmail), body=settings, fields='')
|
|
entityActionPerformed([entityType, groupEmail])
|
|
except (GAPI.alreadyExists, GAPI.duplicate):
|
|
duplicateAliasGroupUserWarning(cd, [entityType, groupEmail])
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([entityType, groupEmail], Msg.DOES_NOT_EXIST)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.invalidArgument, GAPI.failedPrecondition,
|
|
GAPI.badRequest, GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
|
entityActionFailedWarning([entityType, groupEmail], str(e))
|
|
except GAPI.required:
|
|
entityActionFailedWarning([entityType, groupEmail], Msg.INVALID_JSON_SETTING)
|
|
|
|
# [addonly|removeonly]
|
|
def getSyncOperation():
|
|
return getChoice(['addonly', 'removeonly'], defaultChoice='addremove')
|
|
|
|
UPDATE_GROUP_SUBCMDS = ['add', 'create', 'delete', 'remove', 'clear', 'sync', 'update']
|
|
GROUP_PREVIEW_TITLES = ['group', 'email', 'role', 'action', 'message']
|
|
|
|
# gam update groups <GroupEntity> [email <EmailAddress>]
|
|
# [copyfrom <GroupItem>] <GroupAttribute>*
|
|
# [security|makesecuritygroup]
|
|
# [admincreated <Boolean>]
|
|
# [verifynotinvitable]
|
|
# gam update groups <GroupEntity> create [<GroupRole>]
|
|
# [usersonly|groupsonly]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [delivery <DeliverySetting>]
|
|
# [preview] [actioncsv]
|
|
# <UserTypeEntity>
|
|
# gam update groups <GroupEntity> delete|remove [<GroupRole>]
|
|
# [usersonly|groupsonly]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [preview] [actioncsv]
|
|
# <UserTypeEntity>
|
|
# gam update groups <GroupEntity> sync [<GroupRole>|ignorerole]
|
|
# [usersonly|groupsonly] [addonly|removeonly]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [removedomainnostatusmembers]
|
|
# [delivery <DeliverySetting>] [preview] [actioncsv]
|
|
# (additionalmembers <EmailAddressEntity>)*
|
|
# <UserTypeEntity>
|
|
# gam update groups <GroupEntity> update [<GroupRole>]
|
|
# [usersonly|groupsonly]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [delivery <DeliverySetting>] [preview] [actioncsv]
|
|
# [createifnotfound]
|
|
# <UserTypeEntity>
|
|
# gam update groups <GroupEntity> clear [member] [manager] [owner]
|
|
# [usersonly|groupsonly]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [emailclearpattern|emailretainpattern <REMatchPattern>]
|
|
# [removedomainnostatusmembers]
|
|
# [preview] [actioncsv]
|
|
def doUpdateGroups():
|
|
|
|
def _getPreviewActionCSV():
|
|
preview = checkArgumentPresent('preview')
|
|
if checkArgumentPresent('actioncsv'):
|
|
csvPF = CSVPrintFile(GROUP_PREVIEW_TITLES)
|
|
else:
|
|
csvPF = None
|
|
return (preview, csvPF)
|
|
|
|
def _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count):
|
|
roleLower = role.lower()
|
|
if roleLower in GROUP_ROLES_MAP:
|
|
return (GROUP_ROLES_MAP[roleLower], groupMemberLists[origGroup][role])
|
|
entityActionNotPerformedWarning([entityType, group, Ent.ROLE, role], Msg.INVALID_ROLE.format(','.join(sorted(GROUP_ROLES_MAP))), i, count)
|
|
return (None, None)
|
|
|
|
def _getRoleGroupMemberType(defaultRole=Ent.ROLE_MEMBER, allowIgnoreRole=False):
|
|
if not allowIgnoreRole or not checkArgumentPresent(['ignorerole']):
|
|
role = getChoice(GROUP_ROLES_MAP, defaultChoice=defaultRole, mapChoice=True)
|
|
else:
|
|
role = Ent.ROLE_ALL
|
|
groupMemberType = getChoice({'usersonly': Ent.TYPE_USER, 'groupsonly': Ent.TYPE_GROUP}, defaultChoice='ALL', mapChoice=True)
|
|
return (role, groupMemberType)
|
|
|
|
def _getMemberEmailStatus(member):
|
|
if member['type'] == Ent.TYPE_CUSTOMER:
|
|
return (member['id'], member.get('status', 'UNKNOWN'))
|
|
email = member['email'].lower()
|
|
if not removeDomainNoStatusMembers or 'status' in member:
|
|
return (email, member.get('status', 'UNKNOWN'))
|
|
_, domain = splitEmailAddress(email)
|
|
if domain != GC.Values[GC.DOMAIN]:
|
|
return (email, 'UNKNOWN')
|
|
return (email, 'NONE')
|
|
|
|
def _executeBatch(dbatch, batchParms):
|
|
dbatch.execute()
|
|
if batchParms['wait'] > 0:
|
|
time.sleep(batchParms['wait'])
|
|
|
|
# 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 _previewAction(group, members, role, jcount, action):
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in members:
|
|
j += 1
|
|
entityActionPerformed([entityType, group, role, member], j, jcount)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
for member in members:
|
|
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.PerformedName(action), 'message': Act.PREVIEW})
|
|
|
|
def _showSuccess(group, member, role, delivery_settings, j, jcount, optMsg=None):
|
|
kvList = []
|
|
if role is not None and role != 'None':
|
|
kvList.append(f'{Ent.Singular(Ent.ROLE)}: {role}')
|
|
if delivery_settings != DELIVERY_SETTINGS_UNDEFINED:
|
|
kvList.append(f'{Ent.Singular(Ent.DELIVERY)}: {delivery_settings}')
|
|
if optMsg:
|
|
kvList.append(optMsg)
|
|
entityActionPerformedMessage([entityType, group, Ent.MEMBER, member], ', '.join(kvList), j, jcount)
|
|
if csvPF:
|
|
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.Performed(), 'message': Act.SUCCESS})
|
|
|
|
def _showFailure(group, member, role, errMsg, j, jcount):
|
|
entityActionFailedWarning([entityType, group, Ent.MEMBER, member, Ent.ROLE, role], errMsg, j, jcount)
|
|
if csvPF:
|
|
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.Failed(), 'message': errMsg})
|
|
|
|
def _addMember(group, i, count, role, delivery_settings, member, j, jcount):
|
|
body = {'role': role}
|
|
if member.find('@') != -1:
|
|
body['email'] = member
|
|
else:
|
|
body['id'] = member
|
|
if delivery_settings != DELIVERY_SETTINGS_UNDEFINED:
|
|
body['delivery_settings'] = delivery_settings
|
|
try:
|
|
callGAPI(cd.members(), 'insert',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.DUPLICATE, GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND,
|
|
GAPI.INVALID_MEMBER, GAPI.CYCLIC_MEMBERSHIPS_NOT_ALLOWED,
|
|
GAPI.CONDITION_NOT_MET, GAPI.CONFLICT],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=group, body=body, fields='')
|
|
_showSuccess(group, member, role, delivery_settings, j, jcount)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(entityType, group, i, count)
|
|
except (GAPI.duplicate, GAPI.memberNotFound, GAPI.resourceNotFound,
|
|
GAPI.invalidMember, GAPI.cyclicMembershipsNotAllowed, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
_showFailure(group, member, role, str(e), j, jcount)
|
|
except GAPI.conflict:
|
|
_showSuccess(group, member, role, delivery_settings, j, jcount, Msg.ACTION_MAY_BE_DELAYED)
|
|
|
|
def _handleDuplicateAdd(group, i, count, role, delivery_settings, member, j, jcount):
|
|
try:
|
|
result = callGAPI(cd.members(), 'get',
|
|
throwReasons=[GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
|
|
groupKey=group, memberKey=member, fields='role')
|
|
_showFailure(group, member, role, Msg.DUPLICATE_ALREADY_A_ROLE.format(Ent.Singular(result['role'])), j, jcount)
|
|
return
|
|
except (GAPI.memberNotFound, GAPI.resourceNotFound):
|
|
pass
|
|
printEntityKVList([entityType, group, Ent.MEMBER, member], [Msg.MEMBERSHIP_IS_PENDING_WILL_DELETE_ADD_TO_ACCEPT], j, jcount)
|
|
try:
|
|
callGAPI(cd.members(), 'delete',
|
|
throwReasons=[GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
|
|
groupKey=group, memberKey=member)
|
|
except (GAPI.memberNotFound, GAPI.resourceNotFound):
|
|
_showFailure(group, member, role, Msg.DUPLICATE, j, jcount)
|
|
return
|
|
_addMember(group, i, count, role, delivery_settings, member, j, jcount)
|
|
|
|
_ADD_MEMBER_REASON_TO_MESSAGE_MAP = {GAPI.DUPLICATE: Msg.DUPLICATE,
|
|
GAPI.CONDITION_NOT_MET: Msg.DUPLICATE,
|
|
GAPI.MEMBER_NOT_FOUND: Msg.DOES_NOT_EXIST,
|
|
GAPI.RESOURCE_NOT_FOUND: Msg.DOES_NOT_EXIST,
|
|
GAPI.INVALID_MEMBER: Msg.INVALID_MEMBER,
|
|
GAPI.CYCLIC_MEMBERSHIPS_NOT_ALLOWED: Msg.WOULD_MAKE_MEMBERSHIP_CYCLE}
|
|
|
|
def _callbackAddGroupMembers(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], ri[RI_OPTION], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason in GAPI.MEMBERS_THROW_REASONS and reason != GAPI.SERVICE_NOT_AVAILABLE:
|
|
entityUnknownWarning(entityType, ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]))
|
|
elif reason == GAPI.CONFLICT:
|
|
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], ri[RI_OPTION], int(ri[RI_J]), int(ri[RI_JCOUNT]), Msg.ACTION_MAY_BE_DELAYED)
|
|
elif reason in [GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET]:
|
|
_handleDuplicateAdd(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_OPTION], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
elif reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.MEMBERS_RETRY_REASONS:
|
|
errMsg = getHTTPError(_ADD_MEMBER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
|
|
_showFailure(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
if addBatchParms['adjust']:
|
|
addBatchParms['adjust'] = False
|
|
addBatchParms['wait'] += 0.25
|
|
writeStderr(f'{WARNING_PREFIX}{Msg.INTER_BATCH_WAIT_INCREASED.format(addBatchParms["wait"])}\n')
|
|
time.sleep(0.1)
|
|
_addMember(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_OPTION], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
def _batchAddGroupMembers(group, i, count, addMembers, role, delivery_settings):
|
|
Act.Set([Act.ADD, Act.ADD_PREVIEW][preview])
|
|
jcount = len(addMembers)
|
|
entityPerformActionNumItems([entityType, group], jcount, role, i, count)
|
|
if jcount == 0:
|
|
return
|
|
if preview:
|
|
_previewAction(group, addMembers, role, jcount, Act.ADD)
|
|
return
|
|
if addBatchParms['size'] == 1 or jcount <= addBatchParms['size']:
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in addMembers:
|
|
j += 1
|
|
_addMember(group, i, count, role, delivery_settings, member, j, jcount)
|
|
Ind.Decrement()
|
|
return
|
|
body = {'role': role}
|
|
if delivery_settings != DELIVERY_SETTINGS_UNDEFINED:
|
|
body['delivery_settings'] = delivery_settings
|
|
svcargs = dict([('groupKey', group), ('body', body), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(cd.members(), 'insert')
|
|
dbatch = cd.new_batch_http_request(callback=_callbackAddGroupMembers)
|
|
bcount = 0
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in addMembers:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
if member.find('@') != -1:
|
|
svcparms['body']['email'] = member
|
|
svcparms['body'].pop('id', None)
|
|
else:
|
|
svcparms['body']['id'] = member
|
|
svcparms['body'].pop('email', None)
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(group, i, count, j, jcount, member, role, delivery_settings))
|
|
bcount += 1
|
|
if bcount >= addBatchParms['size']:
|
|
addBatchParms['adjust'] = True
|
|
_executeBatch(dbatch, addBatchParms)
|
|
dbatch = cd.new_batch_http_request(callback=_callbackAddGroupMembers)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
Ind.Decrement()
|
|
|
|
def _removeMember(group, i, count, role, member, j, jcount):
|
|
try:
|
|
callGAPI(cd.members(), 'delete',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER,
|
|
GAPI.RESOURCE_NOT_FOUND,
|
|
GAPI.CONDITION_NOT_MET, GAPI.CONFLICT],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=group, memberKey=member)
|
|
_showSuccess(group, member, role, DELIVERY_SETTINGS_UNDEFINED, j, jcount)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(entityType, group, i, count)
|
|
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.resourceNotFound, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
_showFailure(group, member, role, str(e), j, jcount)
|
|
except GAPI.conflict:
|
|
_showSuccess(group, member, role, DELIVERY_SETTINGS_UNDEFINED, j, jcount, Msg.ACTION_MAY_BE_DELAYED)
|
|
|
|
_REMOVE_MEMBER_REASON_TO_MESSAGE_MAP = {GAPI.MEMBER_NOT_FOUND: f'{Msg.NOT_A} {Ent.Singular(Ent.MEMBER)}',
|
|
GAPI.CONDITION_NOT_MET: f'{Msg.NOT_A} {Ent.Singular(Ent.MEMBER)}',
|
|
GAPI.INVALID_MEMBER: Msg.DOES_NOT_EXIST}
|
|
|
|
def _callbackRemoveGroupMembers(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], DELIVERY_SETTINGS_UNDEFINED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason in GAPI.MEMBERS_THROW_REASONS and reason != GAPI.SERVICE_NOT_AVAILABLE:
|
|
entityUnknownWarning(entityType, ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]))
|
|
elif reason == GAPI.CONFLICT:
|
|
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], DELIVERY_SETTINGS_UNDEFINED, int(ri[RI_J]), int(ri[RI_JCOUNT]), Msg.ACTION_MAY_BE_DELAYED)
|
|
elif reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.MEMBERS_RETRY_REASONS+[GAPI.RESOURCE_NOT_FOUND]:
|
|
errMsg = getHTTPError(_REMOVE_MEMBER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
|
|
_showFailure(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
if remBatchParms['adjust']:
|
|
remBatchParms['adjust'] = False
|
|
remBatchParms['wait'] += 0.25
|
|
writeStderr(f'{WARNING_PREFIX}{Msg.INTER_BATCH_WAIT_INCREASED.format(remBatchParms["wait"])}\n')
|
|
time.sleep(0.1)
|
|
_removeMember(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
def _batchRemoveGroupMembers(group, i, count, removeMembers, role, qualifier=''):
|
|
Act.Set([Act.REMOVE, Act.REMOVE_PREVIEW][preview])
|
|
jcount = len(removeMembers)
|
|
if not qualifier:
|
|
entityPerformActionNumItems([entityType, group], jcount, role, i, count)
|
|
else:
|
|
entityPerformActionNumItemsModifier([entityType, group], jcount, role, qualifier, i, count)
|
|
if jcount == 0:
|
|
return
|
|
if preview:
|
|
_previewAction(group, removeMembers, role, jcount, Act.REMOVE)
|
|
return
|
|
if remBatchParms['size'] == 1 or jcount <= remBatchParms['size']:
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in removeMembers:
|
|
j += 1
|
|
_removeMember(group, i, count, role, member, j, jcount)
|
|
Ind.Decrement()
|
|
return
|
|
svcargs = dict([('groupKey', group), ('memberKey', None)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(cd.members(), 'delete')
|
|
dbatch = cd.new_batch_http_request(callback=_callbackRemoveGroupMembers)
|
|
bcount = 0
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in removeMembers:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['memberKey'] = member
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(group, i, count, j, jcount, svcparms['memberKey'], role))
|
|
bcount += 1
|
|
if bcount >= remBatchParms['size']:
|
|
remBatchParms['adjust'] = True
|
|
_executeBatch(dbatch, remBatchParms)
|
|
dbatch = cd.new_batch_http_request(callback=_callbackRemoveGroupMembers)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
Ind.Decrement()
|
|
|
|
_UPDATE_MEMBER_REASON_TO_MESSAGE_MAP = {GAPI.MEMBER_NOT_FOUND: f'{Msg.NOT_A} {Ent.Singular(Ent.MEMBER)}',
|
|
GAPI.INVALID_MEMBER: Msg.DOES_NOT_EXIST,
|
|
GAPI.RESOURCE_NOT_FOUND: Msg.DOES_NOT_EXIST}
|
|
|
|
def _getUpdateBody(role, delivery_settings):
|
|
body = {}
|
|
if role is not None:
|
|
body['role'] = role
|
|
else:
|
|
if delivery_settings == DELIVERY_SETTINGS_UNDEFINED:
|
|
# Backwards compatability; if neither role or delivery is specified, role = MEMBER
|
|
role = Ent.ROLE_MEMBER
|
|
body['role'] = role
|
|
if delivery_settings != DELIVERY_SETTINGS_UNDEFINED:
|
|
body['delivery_settings'] = delivery_settings
|
|
return (body, role)
|
|
|
|
def _updateMember(group, i, count, role, delivery_settings, member, j, jcount):
|
|
body, role = _getUpdateBody(role, delivery_settings)
|
|
try:
|
|
callGAPI(cd.members(), 'patch',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.RESOURCE_NOT_FOUND],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=group, memberKey=member, body=body, fields='')
|
|
_showSuccess(group, member, role, delivery_settings, j, jcount)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(entityType, group, i, count)
|
|
except GAPI.memberNotFound as e:
|
|
if createIfNotFound:
|
|
Act.Set(Act.ADD)
|
|
_addMember(group, i, count, role, delivery_settings, member, j, jcount)
|
|
Act.Set(Act.UPDATE)
|
|
else:
|
|
_showFailure(group, member, role, str(e), j, jcount)
|
|
except (GAPI.invalidMember, GAPI.resourceNotFound, GAPI.serviceNotAvailable) as e:
|
|
_showFailure(group, member, role, str(e), j, jcount)
|
|
|
|
def _callbackUpdateGroupMembers(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
_showSuccess(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], ri[RI_OPTION], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason == GAPI.MEMBER_NOT_FOUND and createIfNotFound:
|
|
Act.Set(Act.ADD)
|
|
_addMember(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_OPTION], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
Act.Set(Act.UPDATE)
|
|
elif reason in GAPI.MEMBERS_THROW_REASONS and reason != GAPI.SERVICE_NOT_AVAILABLE:
|
|
entityUnknownWarning(entityType, ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]))
|
|
elif reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.MEMBERS_RETRY_REASONS:
|
|
errMsg = getHTTPError(_UPDATE_MEMBER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
|
|
_showFailure(ri[RI_ENTITY], ri[RI_ITEM], ri[RI_ROLE], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
if addBatchParms['adjust']:
|
|
addBatchParms['adjust'] = False
|
|
addBatchParms['wait'] += 0.25
|
|
writeStderr(f'{WARNING_PREFIX}{Msg.INTER_BATCH_WAIT_INCREASED.format(addBatchParms["wait"])}\n')
|
|
time.sleep(0.1)
|
|
_updateMember(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]), ri[RI_ROLE], ri[RI_OPTION], ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
def _batchUpdateGroupMembers(group, i, count, updateMembers, role, delivery_settings):
|
|
Act.Set([Act.UPDATE, Act.UPDATE_PREVIEW][preview])
|
|
jcount = len(updateMembers)
|
|
entityPerformActionNumItems([entityType, group], jcount, Ent.MEMBER, i, count)
|
|
if jcount == 0:
|
|
return
|
|
if preview:
|
|
_previewAction(group, updateMembers, role or Ent.ROLE_USER, jcount, Act.UPDATE)
|
|
return
|
|
if updBatchParms['size'] == 1 or jcount <= updBatchParms['size']:
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in updateMembers:
|
|
j += 1
|
|
_updateMember(group, i, count, role, delivery_settings, member, j, jcount)
|
|
Ind.Decrement()
|
|
return
|
|
body, role = _getUpdateBody(role, delivery_settings)
|
|
svcargs = dict([('groupKey', group), ('memberKey', None), ('body', body), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(cd.members(), 'patch')
|
|
dbatch = cd.new_batch_http_request(callback=_callbackUpdateGroupMembers)
|
|
bcount = 0
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in updateMembers:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['memberKey'] = member
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(group, i, count, j, jcount, svcparms['memberKey'], role, delivery_settings))
|
|
bcount += 1
|
|
if bcount >= updBatchParms['size']:
|
|
updBatchParms['adjust'] = True
|
|
_executeBatch(dbatch, updBatchParms)
|
|
dbatch = cd.new_batch_http_request(callback=_callbackUpdateGroupMembers)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
entityType = Ent.GROUP
|
|
csvPF = None
|
|
getBeforeUpdate = preview = False
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
CL_subCommand = getChoice(UPDATE_GROUP_SUBCMDS, defaultChoice=None)
|
|
addBatchParms = {'size': GC.Values[GC.BATCH_SIZE], 'wait': GC.Values[GC.INTER_BATCH_WAIT], 'adjust': True}
|
|
remBatchParms = {'size': GC.Values[GC.BATCH_SIZE], 'wait': GC.Values[GC.INTER_BATCH_WAIT], 'adjust': True}
|
|
updBatchParms = {'size': GC.Values[GC.BATCH_SIZE], 'wait': GC.Values[GC.INTER_BATCH_WAIT], 'adjust': True}
|
|
if not CL_subCommand:
|
|
body = {}
|
|
gs_body = {}
|
|
ci_body = {}
|
|
verifyNotInvitable = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'email':
|
|
body['email'] = getEmailAddress(noUid=True)
|
|
elif myarg == 'admincreated':
|
|
body['adminCreated'] = getBoolean()
|
|
elif myarg == 'getbeforeupdate':
|
|
getBeforeUpdate = True
|
|
elif myarg in {'security', 'makesecuritygroup'}:
|
|
ci_body['labels'] = {CIGROUP_DISCUSSION_FORUM_LABEL: '',
|
|
CIGROUP_SECURITY_LABEL: ''}
|
|
elif myarg == 'json':
|
|
gs_body.update(getJSON(GROUP_JSON_SKIP_FIELDS))
|
|
elif myarg == 'accesstype':
|
|
gs_body.update(getChoice(GROUP_ACCESS_TYPE_CHOICE_MAP, mapChoice=True))
|
|
elif myarg == 'verifynotinvitable':
|
|
verifyNotInvitable = True
|
|
else:
|
|
getGroupAttrValue(myarg, gs_body)
|
|
if 'email' in body and verifyNotInvitable:
|
|
isInvitableUser, _ = _getIsInvitableUser(None, body['email'])
|
|
if isInvitableUser:
|
|
entityActionNotPerformedWarning([Ent.GROUP, body['email']], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT)
|
|
return
|
|
if ci_body:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
if 'email' in body:
|
|
ci_body['groupKey'] = {'id': body.pop('email')}
|
|
if gs_body:
|
|
gs = buildGAPIObject(API.GROUPSSETTINGS)
|
|
gs_body = getSettingsFromGroup(cd, ','.join(entityList), gs, gs_body)
|
|
if ci_body:
|
|
for k, v in iter(GROUP_CIGROUP_FIELDS_MAP.items()):
|
|
if k in gs_body:
|
|
ci_body[v] = gs_body.pop(k)
|
|
if gs_body:
|
|
if not getBeforeUpdate:
|
|
settings = gs_body
|
|
elif not ci_body:
|
|
return
|
|
elif not body and not ci_body:
|
|
return
|
|
Act.Set(Act.UPDATE)
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
ci, _, group = convertGroupCloudIDToEmail(ci, group, i, count)
|
|
if gs_body and not GroupIsAbuseOrPostmaster(group):
|
|
try:
|
|
if 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 = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=group, fields='email')['email']
|
|
if getBeforeUpdate:
|
|
settings = callGAPI(gs.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
|
|
groupUniqueId=mapGroupEmailForSettings(group), fields='*')
|
|
settings.update(gs_body)
|
|
if not checkReplyToCustom(group, settings, i, count):
|
|
continue
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([entityType, group], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.backendError, GAPI.invalid, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
|
|
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
|
entityActionFailedWarning([entityType, group], str(e), i, count)
|
|
continue
|
|
if body:
|
|
try:
|
|
group = callGAPI(cd.groups(), 'update',
|
|
throwReasons=GAPI.GROUP_UPDATE_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=group, body=body, fields='email')['email']
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.backendError, GAPI.badRequest, GAPI.invalid, GAPI.invalidInput,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([entityType, group], str(e), i, count)
|
|
continue
|
|
if gs_body and not GroupIsAbuseOrPostmaster(group):
|
|
try:
|
|
callGAPI(gs.groups(), 'update',
|
|
bailOnInvalidError='messageModerationLevel' in settings,
|
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
|
|
groupUniqueId=mapGroupEmailForSettings(group), body=settings, fields='')
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([entityType, group], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
|
|
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
|
entityActionFailedWarning([entityType, group], str(e), i, count)
|
|
continue
|
|
except GAPI.required:
|
|
entityActionFailedWarning([entityType, group], Msg.INVALID_JSON_SETTING, i, count)
|
|
continue
|
|
if ci_body:
|
|
_, name, _ = convertGroupEmailToCloudID(ci, group, i, count)
|
|
if not name:
|
|
continue
|
|
try:
|
|
callGAPI(ci.groups(), 'patch',
|
|
throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, body=ci_body, updateMask=','.join(list(ci_body.keys())))
|
|
except (GAPI.notFound, GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
|
|
continue
|
|
entityActionPerformed([entityType, group], i, count)
|
|
elif CL_subCommand in {'create', 'add'}:
|
|
baseRole, groupMemberType = _getRoleGroupMemberType()
|
|
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
|
|
delivery_settings = getDeliverySettings()
|
|
preview, csvPF = _getPreviewActionCSV()
|
|
_, addMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
|
|
groupMemberLists = addMembers if isinstance(addMembers, dict) else None
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
roleList = [baseRole]
|
|
if groupMemberLists:
|
|
if not subkeyRoleField:
|
|
addMembers = groupMemberLists[group]
|
|
else:
|
|
roleList = groupMemberLists[group]
|
|
origGroup = group
|
|
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
|
|
if not group:
|
|
continue
|
|
for role in roleList:
|
|
if groupMemberLists and subkeyRoleField:
|
|
role, addMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
|
|
if role is None:
|
|
continue
|
|
_batchAddGroupMembers(group, i, count,
|
|
[convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True) for member in addMembers],
|
|
role, delivery_settings)
|
|
elif CL_subCommand in {'delete', 'remove'}:
|
|
baseRole, groupMemberType = _getRoleGroupMemberType()
|
|
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
|
|
preview, csvPF = _getPreviewActionCSV()
|
|
_, removeMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
|
|
groupMemberLists = removeMembers if isinstance(removeMembers, dict) else None
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
roleList = [baseRole]
|
|
if groupMemberLists:
|
|
if not subkeyRoleField:
|
|
removeMembers = groupMemberLists[group]
|
|
else:
|
|
roleList = groupMemberLists[group]
|
|
origGroup = group
|
|
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
|
|
if not group:
|
|
continue
|
|
for role in roleList:
|
|
if groupMemberLists and subkeyRoleField:
|
|
role, removeMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
|
|
if role is None:
|
|
continue
|
|
_batchRemoveGroupMembers(group, i, count,
|
|
[convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True) for member in removeMembers],
|
|
role)
|
|
elif CL_subCommand == 'sync':
|
|
baseRole, groupMemberType = _getRoleGroupMemberType(allowIgnoreRole=True)
|
|
ignoreRole = baseRole == Ent.ROLE_ALL
|
|
syncOperation = getSyncOperation()
|
|
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
|
|
removeDomainNoStatusMembers = checkArgumentPresent('removedomainnostatusmembers')
|
|
delivery_settings = getDeliverySettings()
|
|
preview, csvPF = _getPreviewActionCSV()
|
|
additionalMembers = {}
|
|
while checkArgumentPresent('additionalmembers'):
|
|
additionalRole = getChoice(GROUP_ROLES_MAP, defaultChoice=baseRole, mapChoice=True)
|
|
additionalMembers.setdefault(additionalRole, [])
|
|
additionalMembers[additionalRole].extend(getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY))
|
|
_, syncMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
|
|
groupMemberLists = syncMembers if isinstance(syncMembers, dict) else None
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
syncMembersSets = {}
|
|
syncMembersMaps = {}
|
|
currentMembersSets = {}
|
|
currentMembersMaps = {}
|
|
domainNoStatusMembersSets = {}
|
|
if groupMemberLists is None:
|
|
syncMembersSets[baseRole] = set()
|
|
syncMembersMaps[baseRole] = {}
|
|
for member in syncMembers:
|
|
syncMembersSets[baseRole].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True), syncMembersMaps[baseRole]))
|
|
for member in additionalMembers.get(baseRole, []):
|
|
syncMembersSets[baseRole].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True), syncMembersMaps[baseRole]))
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
origGroup = group
|
|
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
|
|
if not group:
|
|
continue
|
|
if groupMemberLists is None:
|
|
roleList = [baseRole]
|
|
else:
|
|
if not subkeyRoleField:
|
|
roleList = [baseRole]
|
|
else:
|
|
roleList = groupMemberLists[origGroup]
|
|
for role in roleList:
|
|
role = role.upper()
|
|
syncMembersSets[role] = set()
|
|
syncMembersMaps[role] = {}
|
|
rolesSet = set()
|
|
for role in roleList:
|
|
origRole = role
|
|
role = role.upper()
|
|
if groupMemberLists is None:
|
|
rolesSet.add(role)
|
|
else:
|
|
if not subkeyRoleField:
|
|
rolesSet.add(role)
|
|
syncMembers = groupMemberLists[origGroup]
|
|
else:
|
|
role, syncMembers = _validateSubkeyRoleGetMembers(group, origRole, origGroup, groupMemberLists, i, count)
|
|
if role is None:
|
|
continue
|
|
rolesSet.add(role)
|
|
for member in syncMembers:
|
|
syncMembersSets[role].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True), syncMembersMaps[role]))
|
|
for member in additionalMembers.get(role, []):
|
|
syncMembersSets[role].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True), syncMembersMaps[role]))
|
|
if not rolesSet:
|
|
continue
|
|
memberRoles = ','.join(sorted(rolesSet))
|
|
printGettingAllEntityItemsForWhom(memberRoles, group, entityType=entityType)
|
|
try:
|
|
result = callGAPIpages(cd.members(), 'list', 'members',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=group,
|
|
roles=None if Ent.ROLE_MEMBER in rolesSet or ignoreRole else memberRoles,
|
|
fields='nextPageToken,members(email,id,type,status,role)',
|
|
maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(entityType, group, i, count)
|
|
continue
|
|
for role in rolesSet:
|
|
currentMembersSets[role] = set()
|
|
currentMembersMaps[role] = {}
|
|
domainNoStatusMembersSets[role] = set()
|
|
for member in result:
|
|
role = member.get('role', Ent.ROLE_MEMBER) if not ignoreRole else Ent.ROLE_ALL
|
|
email, memberStatus = _getMemberEmailStatus(member)
|
|
if groupMemberType in ('ALL', member['type']) and role in rolesSet:
|
|
if not removeDomainNoStatusMembers or memberStatus != 'NONE':
|
|
if _checkMemberStatusIsSuspendedIsArchived(memberStatus, isSuspended, isArchived):
|
|
currentMembersSets[role].add(_cleanConsumerAddress(email, currentMembersMaps[role]))
|
|
else:
|
|
domainNoStatusMembersSets[role].add(member['id'])
|
|
del result
|
|
if syncOperation != 'addonly':
|
|
for role in rolesSet:
|
|
if domainNoStatusMembersSets[role]:
|
|
_batchRemoveGroupMembers(group, i, count,
|
|
domainNoStatusMembersSets[role],
|
|
role)
|
|
_batchRemoveGroupMembers(group, i, count,
|
|
[currentMembersMaps[role].get(emailAddress, emailAddress) for emailAddress in currentMembersSets[role]-syncMembersSets[role]],
|
|
role)
|
|
if syncOperation != 'removeonly':
|
|
for role in [Ent.ROLE_OWNER, Ent.ROLE_MANAGER, Ent.ROLE_MEMBER, Ent.ROLE_ALL]:
|
|
if role in rolesSet:
|
|
_batchAddGroupMembers(group, i, count,
|
|
[syncMembersMaps[role].get(emailAddress, emailAddress) for emailAddress in syncMembersSets[role]-currentMembersSets[role]],
|
|
role if role != Ent.ROLE_ALL else Ent.ROLE_MEMBER, delivery_settings)
|
|
elif CL_subCommand == 'update':
|
|
baseRole, groupMemberType = _getRoleGroupMemberType(defaultRole=None)
|
|
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
|
|
delivery_settings = getDeliverySettings()
|
|
preview, csvPF = _getPreviewActionCSV()
|
|
createIfNotFound = checkArgumentPresent('createifnotfound')
|
|
_, updateMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
|
|
groupMemberLists = updateMembers if isinstance(updateMembers, dict) else None
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
roleList = [baseRole]
|
|
if groupMemberLists:
|
|
if not subkeyRoleField:
|
|
updateMembers = groupMemberLists[group]
|
|
else:
|
|
roleList = groupMemberLists[group]
|
|
origGroup = group
|
|
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
|
|
if not group:
|
|
continue
|
|
for role in roleList:
|
|
if groupMemberLists and subkeyRoleField:
|
|
role, updateMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
|
|
if role is None:
|
|
continue
|
|
_batchUpdateGroupMembers(group, i, count,
|
|
[convertUIDtoEmailAddress(member, cd=cd, emailTypes='any', checkForCustomerId=True) for member in updateMembers],
|
|
role, delivery_settings)
|
|
else: #clear
|
|
rolesSet = set()
|
|
groupMemberType = 'ALL'
|
|
isSuspended = isArchived = None
|
|
removeDomainNoStatusMembers = False
|
|
emailMatchPattern = None
|
|
clearMatch = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[myarg])
|
|
elif myarg == 'usersonly':
|
|
groupMemberType = Ent.TYPE_USER
|
|
elif myarg == 'groupsonly':
|
|
groupMemberType = Ent.TYPE_GROUP
|
|
elif myarg in SUSPENDED_ARGUMENTS:
|
|
isSuspended = _getIsSuspended(myarg)
|
|
elif myarg in ARCHIVED_ARGUMENTS:
|
|
isArchived = _getIsArchived(myarg)
|
|
elif myarg == 'removedomainnostatusmembers':
|
|
removeDomainNoStatusMembers = True
|
|
elif myarg in {'emailclearpattern', 'emailretainpattern'}:
|
|
emailMatchPattern = getREPattern(re.IGNORECASE)
|
|
clearMatch = myarg == 'emailclearpattern'
|
|
elif myarg == 'preview':
|
|
preview = True
|
|
elif myarg == 'actioncsv':
|
|
csvPF = CSVPrintFile(GROUP_PREVIEW_TITLES)
|
|
else:
|
|
unknownArgumentExit()
|
|
if isSuspended is not None and isArchived is not None:
|
|
if isSuspended == isArchived:
|
|
qualifier = '(Suspended) (Archived)' if isSuspended else '(Non-Suspended) (Non-Archived)'
|
|
else:
|
|
qualifier = '(Suspended)' if isSuspended else '(Archived)'
|
|
elif isSuspended is not None:
|
|
qualifier = '(Suspended)' if isSuspended else '(Non-Suspended)'
|
|
elif isArchived is not None:
|
|
qualifier = '(Archived)' if isArchived else '(Non-Archived)'
|
|
else:
|
|
qualifier = ''
|
|
Act.Set(Act.REMOVE)
|
|
if not rolesSet:
|
|
rolesSet.add(Ent.ROLE_MEMBER)
|
|
memberRoles = ','.join(sorted(rolesSet))
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
ci, _, group = checkGroupExists(cd, ci, False, group, i, count)
|
|
if not group:
|
|
continue
|
|
printGettingAllEntityItemsForWhom(memberRoles, group, entityType=entityType)
|
|
try:
|
|
result = callGAPIpages(cd.members(), 'list', 'members',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=group, roles=None if Ent.ROLE_MEMBER in rolesSet else memberRoles,
|
|
fields='nextPageToken,members(email,id,type,status,role)',
|
|
maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(entityType, group, i, count)
|
|
continue
|
|
removeMembers = {}
|
|
for role in rolesSet:
|
|
removeMembers[role] = set()
|
|
for member in result:
|
|
role = member.get('role', Ent.ROLE_MEMBER)
|
|
email, memberStatus = _getMemberEmailStatus(member)
|
|
if groupMemberType in ('ALL', member['type']) and role in rolesSet:
|
|
if not removeDomainNoStatusMembers:
|
|
if _checkMemberStatusIsSuspendedIsArchived(memberStatus, isSuspended, isArchived):
|
|
if emailMatchPattern is None:
|
|
removeMembers[role].add(email if memberStatus != 'UNKNOWN' else member['id'])
|
|
elif member['type'] == Ent.TYPE_CUSTOMER:
|
|
pass
|
|
elif emailMatchPattern.match(email):
|
|
if clearMatch:
|
|
removeMembers[role].add(email if memberStatus != 'UNKNOWN' else member['id'])
|
|
else:
|
|
if not clearMatch:
|
|
removeMembers[role].add(email if memberStatus != 'UNKNOWN' else member['id'])
|
|
elif memberStatus == 'NONE':
|
|
removeMembers[role].add(member['id'])
|
|
del result
|
|
for role in rolesSet:
|
|
_batchRemoveGroupMembers(group, i, count, removeMembers[role], role, qualifier)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Group Updates')
|
|
|
|
def verifyGroupPrimaryEmail(cd, group, i, count):
|
|
try:
|
|
result = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
|
|
groupKey=group, fields='id,email')
|
|
if (result['email'].lower() == group) or (result['id'] == group):
|
|
return True
|
|
entityActionNotPerformedWarning([Ent.GROUP, group], Msg.NOT_A_PRIMARY_EMAIL_ADDRESS, i, count)
|
|
return False
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
pass
|
|
entityUnknownWarning(Ent.GROUP, group, i, count)
|
|
return False
|
|
|
|
# gam delete groups <GroupEntity> [noactionifalias]
|
|
def doDeleteGroups(ciGroupsAPI=False):
|
|
if not ciGroupsAPI:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
else:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI]
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
if not ciGroupsAPI:
|
|
noActionIfAlias = checkArgumentPresent('noactionifalias')
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
try:
|
|
if not ciGroupsAPI:
|
|
ci, _, groupKey = convertGroupCloudIDToEmail(ci, group, i, count)
|
|
if noActionIfAlias and not verifyGroupPrimaryEmail(cd, groupKey, i, count):
|
|
continue
|
|
callGAPI(cd.groups(), 'delete',
|
|
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
groupKey=groupKey)
|
|
else:
|
|
_, groupKey, group = convertGroupEmailToCloudID(ci, group, i, count)
|
|
if not groupKey:
|
|
continue
|
|
callGAPI(ci.groups(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
name=groupKey)
|
|
entityActionPerformed([entityType, groupKey], i, count)
|
|
except (GAPI.notFound, GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.invalid):
|
|
entityUnknownWarning(entityType, groupKey, i, count)
|
|
|
|
def getGroupRoles(myarg, rolesSet):
|
|
if myarg in {'role', 'roles'}:
|
|
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
|
|
if role in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[role])
|
|
else:
|
|
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
|
|
elif myarg in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[myarg])
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
GROUP_MEMBER_TYPES_MAP = {
|
|
'customer': Ent.TYPE_CUSTOMER,
|
|
'group': Ent.TYPE_GROUP,
|
|
'user': Ent.TYPE_USER,
|
|
}
|
|
ALL_GROUP_MEMBER_TYPES = {Ent.TYPE_CUSTOMER, Ent.TYPE_GROUP, Ent.TYPE_USER}
|
|
|
|
def getGroupMemberTypes(myarg, typesSet):
|
|
if myarg in {'type', 'types'}:
|
|
for gtype in getString(Cmd.OB_GROUP_TYPE_LIST).lower().replace(',', ' ').split():
|
|
if gtype in GROUP_MEMBER_TYPES_MAP:
|
|
typesSet.add(GROUP_MEMBER_TYPES_MAP[gtype])
|
|
else:
|
|
invalidChoiceExit(gtype, GROUP_MEMBER_TYPES_MAP, True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
MEMBEROPTION_MEMBERNAMES = 0
|
|
MEMBEROPTION_NODUPLICATES = 1
|
|
MEMBEROPTION_RECURSIVE = 2
|
|
MEMBEROPTION_GETDELIVERYSETTINGS = 3
|
|
MEMBEROPTION_ISARCHIVED = 4
|
|
MEMBEROPTION_ISSUSPENDED = 5
|
|
MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP = 6
|
|
MEMBEROPTION_MATCHPATTERN = 7
|
|
MEMBEROPTION_DISPLAYMATCH = 8
|
|
|
|
def initMemberOptions():
|
|
return [False, False, False, False, None, None, False, None, True]
|
|
|
|
def getMemberMatchOptions(myarg, memberOptions):
|
|
if myarg in {'memberemaildisplaypattern', 'memberemailskippattern'}:
|
|
memberOptions[MEMBEROPTION_MATCHPATTERN] = getREPattern(re.IGNORECASE)
|
|
memberOptions[MEMBEROPTION_DISPLAYMATCH] = myarg == 'memberemaildisplaypattern'
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def checkMemberMatch(member, memberOptions):
|
|
if not memberOptions[MEMBEROPTION_MATCHPATTERN]:
|
|
return True
|
|
if member['type'] != Ent.TYPE_CUSTOMER:
|
|
if memberOptions[MEMBEROPTION_MATCHPATTERN].match(member['email']):
|
|
return memberOptions[MEMBEROPTION_DISPLAYMATCH]
|
|
return not memberOptions[MEMBEROPTION_DISPLAYMATCH]
|
|
if memberOptions[MEMBEROPTION_MATCHPATTERN].match(member['id']):
|
|
return memberOptions[MEMBEROPTION_DISPLAYMATCH]
|
|
return not memberOptions[MEMBEROPTION_DISPLAYMATCH]
|
|
|
|
def checkCIMemberMatch(member, memberOptions):
|
|
if not memberOptions[MEMBEROPTION_MATCHPATTERN]:
|
|
return True
|
|
if memberOptions[MEMBEROPTION_MATCHPATTERN].match(member.get('preferredMemberKey', {}).get('id', '')):
|
|
return memberOptions[MEMBEROPTION_DISPLAYMATCH]
|
|
return not memberOptions[MEMBEROPTION_DISPLAYMATCH]
|
|
|
|
def initIPSGMGroupMemberDisplayOptions():
|
|
return {Ent.ROLE_MEMBER: {'show': False},
|
|
Ent.ROLE_MANAGER: {'show': False},
|
|
Ent.ROLE_OWNER: {'show': False},
|
|
'categories': [],
|
|
'internal': False, 'external': False, 'showCategory': False,
|
|
'internalDomains': set([GC.Values[GC.DOMAIN]])}
|
|
|
|
def getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
def setMemberDisplayOptionsRole():
|
|
for role in rolesSet:
|
|
memberDisplayOptions[role]['show'] = True
|
|
|
|
if myarg in {'role', 'roles'}:
|
|
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
|
|
if role in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[role])
|
|
else:
|
|
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
|
|
setMemberDisplayOptionsRole()
|
|
elif myarg in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[myarg])
|
|
setMemberDisplayOptionsRole()
|
|
elif myarg == 'members':
|
|
role = Ent.ROLE_MEMBER
|
|
rolesSet.add(role)
|
|
memberDisplayOptions[role]['show'] = True
|
|
elif myarg == 'managers':
|
|
role = Ent.ROLE_MANAGER
|
|
rolesSet.add(role)
|
|
memberDisplayOptions[role]['show'] = True
|
|
elif myarg == 'owners':
|
|
role = Ent.ROLE_OWNER
|
|
rolesSet.add(role)
|
|
memberDisplayOptions[role]['show'] = True
|
|
elif myarg == 'internal':
|
|
memberDisplayOptions['internal'] = memberDisplayOptions['showCategory'] = True
|
|
elif myarg == 'external':
|
|
memberDisplayOptions['external'] = memberDisplayOptions['showCategory'] = True
|
|
elif myarg == 'internaldomains':
|
|
memberDisplayOptions['internalDomains'] = set(getString(Cmd.OB_DOMAIN_NAME_LIST).replace(',', ' ').lower().split())
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def initPGGroupMemberDisplayOptions():
|
|
return {Ent.ROLE_MEMBER: {'show': False, 'countOnly': False},
|
|
Ent.ROLE_MANAGER: {'show': False, 'countOnly': False},
|
|
Ent.ROLE_OWNER: {'show': False, 'countOnly': False},
|
|
'totalCount': False, 'categories': [],
|
|
'internal': False, 'external': False, 'showCategory': False,
|
|
'internalDomains': set([GC.Values[GC.DOMAIN]])}
|
|
|
|
def getPGGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
if myarg == 'memberscount':
|
|
role = Ent.ROLE_MEMBER
|
|
rolesSet.add(role)
|
|
memberDisplayOptions[role]['show'] = True
|
|
memberDisplayOptions[role]['countOnly'] = True
|
|
elif myarg == 'managerscount':
|
|
role = Ent.ROLE_MANAGER
|
|
rolesSet.add(role)
|
|
memberDisplayOptions[role]['show'] = True
|
|
memberDisplayOptions[role]['countOnly'] = True
|
|
elif myarg == 'ownerscount':
|
|
role = Ent.ROLE_OWNER
|
|
rolesSet.add(role)
|
|
memberDisplayOptions[role]['show'] = True
|
|
memberDisplayOptions[role]['countOnly'] = True
|
|
elif myarg == 'totalcount':
|
|
memberDisplayOptions['totalCount'] = True
|
|
elif myarg == 'countsonly':
|
|
for role in Ent.ROLE_LIST:
|
|
memberDisplayOptions[role]['countOnly'] = True
|
|
else:
|
|
return getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions)
|
|
return True
|
|
|
|
GROUP_FIELDS_CHOICE_MAP = {
|
|
'admincreated': 'adminCreated',
|
|
'aliases': ['aliases', 'nonEditableAliases'],
|
|
'description': 'description',
|
|
'directmemberscount': 'directMembersCount',
|
|
'email': 'email',
|
|
'id': 'id',
|
|
'name': 'name',
|
|
}
|
|
GROUP_INFO_PRINT_ORDER = ['id', 'name', 'description', 'directMembersCount', 'adminCreated']
|
|
INFO_GROUP_OPTIONS = {'nousers', 'groups'}
|
|
|
|
CIGROUP_FIELDS_CHOICE_MAP = {
|
|
'additionalgroupkeys': 'additionalGroupKeys',
|
|
'createtime': 'createTime',
|
|
'description': 'description',
|
|
'displayname': 'displayName',
|
|
'dynamicgroupmetadata': 'dynamicGroupMetadata',
|
|
'email': 'groupKey',
|
|
'groupkey': 'groupKey',
|
|
'id': 'name',
|
|
'labels': 'labels',
|
|
'name': 'displayName',
|
|
'parent': 'parent',
|
|
'updatetime': 'updateTime',
|
|
}
|
|
CIGROUP_FULL_FIELDS = {'additionalGroupKeys', 'createTime', 'dynamicGroupMetadata', 'parent', 'updateTime'}
|
|
CIGROUP_FIELDS_WITH_CRS_NLS = {'description'}
|
|
CIGROUP_INFO_ORDER = ['id', 'name', 'description', 'createTime', 'updateTime',
|
|
'groupKey', 'additionalGroupKeys', 'labels', 'parent', 'dynamicGroupMetadata',
|
|
'SecuritySettings']
|
|
CIGROUP_PRINT_ORDER = ['id', 'name', 'description', 'createTime', 'updateTime',
|
|
'groupKey', 'additionalGroupKeys', 'parent', 'dynamicGroupMetadata',
|
|
'memberRestrictionQuery', 'memberRestrictionEvaluation']
|
|
CIGROUP_TIME_OBJECTS = {'createTime', 'updateTime', 'statusTime'}
|
|
|
|
def mapCIGroupFieldNames(group):
|
|
if 'groupKey' in group:
|
|
group['email'] = group['groupKey'].pop('id')
|
|
if not group['groupKey']:
|
|
group.pop('groupKey')
|
|
if 'name' in group:
|
|
group['id'] = group.pop('name')
|
|
if 'displayName' in group:
|
|
group['name'] = group.pop('displayName')
|
|
|
|
def _showCIGroup(group, groupEmail, i=0, count=0):
|
|
printEntity([Ent.CLOUD_IDENTITY_GROUP, groupEmail], i, count)
|
|
mapCIGroupFieldNames(group)
|
|
Ind.Increment()
|
|
for key in CIGROUP_INFO_ORDER:
|
|
if key not in group:
|
|
continue
|
|
value = group[key]
|
|
if key == 'labels':
|
|
for k, v in iter(value.items()):
|
|
if v == '':
|
|
value[k] = True
|
|
if isinstance(value, (list, dict)):
|
|
showJSON(key, value, timeObjects=CIGROUP_TIME_OBJECTS)
|
|
elif key not in CIGROUP_FIELDS_WITH_CRS_NLS:
|
|
if key not in CIGROUP_TIME_OBJECTS:
|
|
printKeyValueList([key, value])
|
|
else:
|
|
printKeyValueList([key, formatLocalTime(value)])
|
|
else:
|
|
printKeyValueWithCRsNLs(key, value)
|
|
Ind.Decrement()
|
|
|
|
def infoGroups(entityList):
|
|
def initGroupFieldsLists():
|
|
if not groupFieldsLists['cd']:
|
|
groupFieldsLists['cd'] = ['email']
|
|
if not groupFieldsLists['ci']:
|
|
groupFieldsLists['ci'] = []
|
|
if not groupFieldsLists['gs']:
|
|
groupFieldsLists['gs'] = []
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
getAliases = getUsers = True
|
|
getCloudIdentity = getGroups = getSettings = False
|
|
showDeprecatedAttributes = True
|
|
FJQC = FormatJSONQuoteChar()
|
|
groups = []
|
|
members = []
|
|
groupFieldsLists = {'cd': None, 'ci': None, 'gs': None}
|
|
isSuspended = isArchived = None
|
|
rolesSet = set()
|
|
typesSet = set()
|
|
memberOptions = initMemberOptions()
|
|
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'quick':
|
|
getAliases = getUsers = False
|
|
elif myarg == 'nousers':
|
|
getUsers = False
|
|
elif myarg == 'nodeprecated':
|
|
showDeprecatedAttributes = not getBoolean()
|
|
elif myarg in SUSPENDED_ARGUMENTS:
|
|
isSuspended = _getIsSuspended(myarg)
|
|
elif myarg in ARCHIVED_ARGUMENTS:
|
|
isArchived = _getIsArchived(myarg)
|
|
elif myarg == 'noaliases':
|
|
getAliases = False
|
|
elif myarg == 'groups':
|
|
getGroups = True
|
|
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
getUsers = True
|
|
elif getGroupMemberTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif myarg == 'basic':
|
|
initGroupFieldsLists()
|
|
for field in GROUP_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
|
|
elif myarg in {'ciallfields', 'allcifields'}:
|
|
if not groupFieldsLists['ci']:
|
|
groupFieldsLists['ci'] = []
|
|
for field in CIGROUP_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
elif myarg in GROUP_FIELDS_CHOICE_MAP:
|
|
initGroupFieldsLists()
|
|
addFieldToFieldsList(myarg, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
|
|
elif myarg in GROUP_ATTRIBUTES_SET:
|
|
initGroupFieldsLists()
|
|
attrProperties = getGroupAttrProperties(myarg)
|
|
groupFieldsLists['gs'].extend([attrProperties[0]])
|
|
elif myarg == 'fields':
|
|
initGroupFieldsLists()
|
|
for field in _getFieldsList():
|
|
if field in GROUP_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
|
|
else:
|
|
attrProperties = getGroupAttrProperties(field)
|
|
if attrProperties is None:
|
|
invalidChoiceExit(field, list(GROUP_FIELDS_CHOICE_MAP)+list(GROUP_ATTRIBUTES_SET), True)
|
|
groupFieldsLists['gs'].extend([attrProperties[0]])
|
|
elif myarg == 'cifields':
|
|
initGroupFieldsLists()
|
|
for field in _getFieldsList():
|
|
if field in CIGROUP_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
else:
|
|
invalidChoiceExit(field, list(CIGROUP_FIELDS_CHOICE_MAP), True)
|
|
# Ignore info user arguments that may have come from whatis
|
|
elif myarg in INFO_USER_OPTIONS:
|
|
if myarg == 'schemas':
|
|
getString(Cmd.OB_SCHEMA_NAME_LIST)
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if isSuspended is not None and isArchived is not None:
|
|
if isSuspended == isArchived:
|
|
entityType = Ent.MEMBER_SUSPENDED_ARCHIVED if isSuspended else Ent.MEMBER_NOT_SUSPENDED_NOT_ARCHIVED
|
|
else:
|
|
entityType = Ent.MEMBER_SUSPENDED if isSuspended else Ent.MEMBER_ARCHIVED
|
|
elif isSuspended is not None:
|
|
entityType = Ent.MEMBER_SUSPENDED if isSuspended else Ent.MEMBER_NOT_SUSPENDED
|
|
elif isArchived is not None:
|
|
entityType = Ent.MEMBER_ARCHIVED if isArchived else Ent.MEMBER_NOT_ARCHIVED
|
|
else:
|
|
entityType = Ent.MEMBER
|
|
if not typesSet:
|
|
typesSet = ALL_GROUP_MEMBER_TYPES
|
|
cdfields = getFieldsFromFieldsList(groupFieldsLists['cd'])
|
|
memberRoles = ','.join(sorted(rolesSet)) if rolesSet else None
|
|
if groupFieldsLists['gs'] is None:
|
|
getSettings = True
|
|
gsfields = None
|
|
elif groupFieldsLists['gs']:
|
|
getSettings = True
|
|
gsfields = getFieldsFromFieldsList(groupFieldsLists['gs'])
|
|
else:
|
|
gsfields = None
|
|
if getSettings:
|
|
gs = buildGAPIObject(API.GROUPSSETTINGS)
|
|
if groupFieldsLists['ci']:
|
|
getCloudIdentity = True
|
|
cifields = getFieldsFromFieldsList(groupFieldsLists['ci'])
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
else:
|
|
cifields = None
|
|
checkCategory = memberDisplayOptions['showCategory']
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
ci, _, group = convertGroupCloudIDToEmail(ci, group, i, count)
|
|
try:
|
|
basic_info = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=group, fields=cdfields)
|
|
group = basic_info['email']
|
|
if getCloudIdentity:
|
|
_, name, _ = convertGroupEmailToCloudID(ci, group, i, count)
|
|
if not name:
|
|
continue
|
|
cigInfo = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, fields=cifields)
|
|
else:
|
|
cigInfo = {}
|
|
settings = {}
|
|
if getSettings and not GroupIsAbuseOrPostmaster(group):
|
|
# Use email address retrieved from cd since GS API doesn't support uid
|
|
settings = callGAPI(gs.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
|
|
groupUniqueId=mapGroupEmailForSettings(group), fields=gsfields)
|
|
if getGroups:
|
|
groups = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userKey=group, orderBy='email', fields='nextPageToken,groups(name,email)')
|
|
if getUsers:
|
|
validRoles, listRoles, listFields = _getRoleVerification(memberRoles, 'nextPageToken,members(email,id,role,status,type)')
|
|
result = callGAPIpages(cd.members(), 'list', 'members',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=group, roles=listRoles, fields=listFields, maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
members = []
|
|
for member in result:
|
|
if ((member['type'] in typesSet) and
|
|
_checkMemberRoleIsSuspendedIsArchived(member, validRoles, isSuspended, isArchived) and
|
|
checkMemberMatch(member, memberOptions) and
|
|
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
|
|
members.append(member)
|
|
if FJQC.formatJSON:
|
|
basic_info.update(settings)
|
|
if cigInfo:
|
|
basic_info['cloudIdentity'] = cigInfo
|
|
if getGroups:
|
|
basic_info['groups'] = groups
|
|
if getUsers:
|
|
basic_info['members'] = members
|
|
printLine(json.dumps(cleanJSON(basic_info), ensure_ascii=False, sort_keys=True))
|
|
continue
|
|
printEntity([Ent.GROUP, group], i, count)
|
|
Ind.Increment()
|
|
if cigInfo:
|
|
_showCIGroup(cigInfo, None)
|
|
printEntity([Ent.GROUP_SETTINGS, None])
|
|
Ind.Increment()
|
|
for key in GROUP_INFO_PRINT_ORDER:
|
|
if key not in basic_info:
|
|
continue
|
|
value = basic_info[key]
|
|
if isinstance(value, list):
|
|
printKeyValueList([key, None])
|
|
Ind.Increment()
|
|
for val in value:
|
|
printKeyValueList([val])
|
|
Ind.Decrement()
|
|
elif key not in GROUP_FIELDS_WITH_CRS_NLS:
|
|
printKeyValueList([key, value])
|
|
else:
|
|
printKeyValueWithCRsNLs(key, value)
|
|
if settings:
|
|
for key, attr in sorted(iter(GROUP_SETTINGS_ATTRIBUTES.items())):
|
|
key = attr[0]
|
|
if key in settings:
|
|
if key not in GROUP_FIELDS_WITH_CRS_NLS:
|
|
printKeyValueList([key, settings[key]])
|
|
else:
|
|
printKeyValueWithCRsNLs(key, settings[key])
|
|
for key in GROUP_MERGED_ATTRIBUTES_PRINT_ORDER:
|
|
if key in settings:
|
|
printKeyValueList([key, settings[key]])
|
|
Ind.Increment()
|
|
showTitle = False
|
|
else:
|
|
showTitle = True
|
|
if showDeprecatedAttributes:
|
|
for subkey, subattr in sorted(iter(GROUP_MERGED_TO_COMPONENT_MAP[key].items())):
|
|
subkey = subattr[0]
|
|
if subkey in settings:
|
|
if showTitle:
|
|
printKeyValueList([key, ''])
|
|
Ind.Increment()
|
|
showTitle = False
|
|
printKeyValueList([subkey, settings[subkey]])
|
|
if not showTitle:
|
|
Ind.Decrement()
|
|
if showDeprecatedAttributes:
|
|
showTitle = True
|
|
for subkey, attr in sorted(iter(GROUP_DEPRECATED_ATTRIBUTES.items())):
|
|
subkey = attr[0]
|
|
if subkey in settings:
|
|
if showTitle:
|
|
printKeyValueList(['Deprecated', ''])
|
|
Ind.Increment()
|
|
showTitle = False
|
|
if subkey != 'maxMessageBytes':
|
|
printKeyValueList([subkey, settings[subkey]])
|
|
else:
|
|
printKeyValueList([subkey, formatMaxMessageBytes(settings[subkey], ONE_KILO_BYTES, ONE_MEGA_BYTES)])
|
|
if not showTitle:
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
if getAliases:
|
|
for up in ['aliases', 'nonEditableAliases']:
|
|
aliases = basic_info.get(up, [])
|
|
if aliases:
|
|
printEntitiesCount([Ent.NONEDITABLE_ALIAS, Ent.EMAIL_ALIAS][up == 'aliases'], aliases)
|
|
Ind.Increment()
|
|
for alias in aliases:
|
|
printKeyValueList(['alias', alias])
|
|
Ind.Decrement()
|
|
if getGroups:
|
|
printEntitiesCount(Ent.GROUP, groups)
|
|
Ind.Increment()
|
|
for groupm in groups:
|
|
printKeyValueList([groupm['name'], groupm['email']])
|
|
Ind.Decrement()
|
|
if getUsers:
|
|
printEntitiesCount(entityType, members)
|
|
Ind.Increment()
|
|
for member in members:
|
|
memberDetails = [member.get('role', Ent.ROLE_MEMBER).lower(), f'{member.get("email", member["id"])} ({member["type"].lower()})']
|
|
if checkCategory:
|
|
memberDetails[1] += f' ({member["category"]})'
|
|
printKeyValueList(memberDetails)
|
|
Ind.Decrement()
|
|
printKeyValueList([Msg.TOTAL_ITEMS_IN_ENTITY.format(Ent.Plural(entityType), Ent.Singular(Ent.GROUP)), len(members)])
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([Ent.GROUP, group], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.groupNotFound, GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.backendError,
|
|
GAPI.invalid, GAPI.invalidMember, GAPI.invalidParameter, GAPI.invalidInput, GAPI.forbidden, GAPI.badRequest,
|
|
GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
|
entityActionFailedWarning([Ent.GROUP, group], str(e), i, count)
|
|
|
|
# gam info groups <GroupEntity>
|
|
# [nousers] [quick] [noaliases] [groups]
|
|
# [basic] <GroupFieldName>* [fields <GroupFieldNameList>] [nodeprecated]
|
|
# [ciallfields|(cifields <CIGroupFieldNameList>)]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners]
|
|
# [internal] [internaldomains <DomainList>] [external]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [types <GroupMemberTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
|
|
# [formatjson]
|
|
def doInfoGroups():
|
|
infoGroups(getEntityList(Cmd.OB_GROUP_ENTITY))
|
|
|
|
def groupFilters(kwargs, query):
|
|
queryTitle = ''
|
|
if kwargs.get('domain'):
|
|
queryTitle += f'domain={kwargs["domain"]}, '
|
|
if query is not None:
|
|
queryTitle += f'query="{query}", '
|
|
if queryTitle:
|
|
return query, queryTitle[:-2]
|
|
return query, queryTitle
|
|
|
|
def getGroupFilters(myarg, kwargsDict, showOwnedBy):
|
|
if getUserGroupDomainQueryFilters(myarg, kwargsDict):
|
|
pass
|
|
elif myarg in {'member', 'showownedby'}:
|
|
emailAddressOrUID = getEmailAddress()
|
|
if emailAddressOrUID != GC.Values[GC.CUSTOMER_ID].lower():
|
|
kwargsDict['queries'] = [f'memberKey={emailAddressOrUID}']
|
|
key = 'email' if emailAddressOrUID.find('@') != -1 else 'id'
|
|
else:
|
|
kwargsDict['queries'] = [f'memberKey={GC.Values[GC.CUSTOMER_ID]}']
|
|
key = 'id'
|
|
if myarg == 'showownedby':
|
|
showOwnedBy['key'] = key
|
|
showOwnedBy['value'] = emailAddressOrUID
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def checkGroupShowOwnedBy(showOwnedBy, members):
|
|
for member in members:
|
|
if (member.get('role', Ent.ROLE_MEMBER) == Ent.ROLE_OWNER) and (member.get(showOwnedBy['key'], '').lower() == showOwnedBy['value']):
|
|
return True
|
|
return False
|
|
|
|
def getGroupMatchPatterns(myarg, matchPatterns, ciGroupsAPI):
|
|
if myarg == 'emailmatchpattern':
|
|
matchPatterns['email'] = {'not': checkArgumentPresent('not'), 'pattern': getREPattern(re.IGNORECASE)}
|
|
elif myarg == 'namematchpattern':
|
|
matchPatterns['name' if not ciGroupsAPI else 'displayName'] = {'not': checkArgumentPresent('not'), 'pattern': getREPattern(re.IGNORECASE|re.UNICODE)}
|
|
elif myarg == 'descriptionmatchpattern':
|
|
matchPatterns['description'] = {'not': checkArgumentPresent('not'), 'pattern': getREPattern(re.IGNORECASE|re.UNICODE)}
|
|
elif not ciGroupsAPI and myarg == 'admincreatedmatch':
|
|
matchPatterns['adminCreated'] = getBoolean(None)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def updateFieldsForGroupMatchPatterns(matchPatterns, fieldsList, csvPF=None):
|
|
for field in ['name', 'description', 'adminCreated']:
|
|
if field in matchPatterns:
|
|
if csvPF is not None:
|
|
csvPF.AddField(field, GROUP_FIELDS_CHOICE_MAP, fieldsList)
|
|
else:
|
|
fieldsList.append(field)
|
|
|
|
def clearUnneededGroupMatchPatterns(matchPatterns):
|
|
for field in ['name', 'displayName', 'description', 'adminCreated']:
|
|
matchPatterns.pop(field, None)
|
|
|
|
def checkGroupMatchPatterns(groupEmail, group, matchPatterns):
|
|
for field, match in iter(matchPatterns.items()):
|
|
if field == 'email':
|
|
if not match['not']:
|
|
if not match['pattern'].match(groupEmail):
|
|
return False
|
|
else:
|
|
if match['pattern'].match(groupEmail):
|
|
return False
|
|
elif field == 'adminCreated':
|
|
if match != group[field]:
|
|
return False
|
|
else: # field in {'name', 'displayName', 'description'}:
|
|
if not match['not']:
|
|
if not match['pattern'].match(group[field]):
|
|
return False
|
|
else:
|
|
if match['pattern'].match(group[field]):
|
|
return False
|
|
return True
|
|
|
|
MEMBERS_TITLES = {
|
|
'combined': {
|
|
'total': ['TotalCount', ''],
|
|
Ent.ROLE_MEMBER: ['MembersCount', 'Members'],
|
|
Ent.ROLE_MANAGER: ['ManagersCount', 'Managers'],
|
|
Ent.ROLE_OWNER: ['OwnersCount', 'Owners']
|
|
},
|
|
'internal': {
|
|
'total': ['TotalInternalCount', ''],
|
|
Ent.ROLE_MEMBER: ['InternalMembersCount', 'InternalMembers'],
|
|
Ent.ROLE_MANAGER: ['InternalManagersCount', 'InternalManagers'],
|
|
Ent.ROLE_OWNER: ['InternalOwnersCount', 'InternalOwners']
|
|
},
|
|
'external': {
|
|
'total': ['TotalExternalCount', ''],
|
|
Ent.ROLE_MEMBER: ['ExternalMembersCount', 'ExternalMembers'],
|
|
Ent.ROLE_MANAGER: ['ExternalManagersCount', 'ExternalManagers'],
|
|
Ent.ROLE_OWNER: ['ExternalOwnersCount', 'ExternalOwners']
|
|
}
|
|
}
|
|
|
|
def setMemberDisplayTitles(memberDisplayOptions, csvPF):
|
|
if memberDisplayOptions['totalCount']:
|
|
csvPF.AddTitles(MEMBERS_TITLES['combined']['total'][0])
|
|
if not memberDisplayOptions['internal'] and not memberDisplayOptions['external']:
|
|
memberDisplayOptions['categories'].append('combined')
|
|
else:
|
|
if memberDisplayOptions['internal']:
|
|
memberDisplayOptions['categories'].append('internal')
|
|
if memberDisplayOptions['external']:
|
|
memberDisplayOptions['categories'].append('external')
|
|
for category in memberDisplayOptions['categories']:
|
|
if memberDisplayOptions['totalCount'] and category != 'combined':
|
|
csvPF.AddTitles(MEMBERS_TITLES[category]['total'][0])
|
|
for role in Ent.ROLE_LIST:
|
|
if memberDisplayOptions[role]['show']:
|
|
csvPF.AddTitles(MEMBERS_TITLES[category][role][0])
|
|
if not memberDisplayOptions[role]['countOnly']:
|
|
csvPF.AddTitles(MEMBERS_TITLES[category][role][1])
|
|
|
|
def setMemberDisplaySortTitles(memberDisplayOptions, sortTitles):
|
|
if memberDisplayOptions['totalCount']:
|
|
sortTitles.append(MEMBERS_TITLES['total'][0])
|
|
for category in memberDisplayOptions['categories']:
|
|
if memberDisplayOptions['totalCount'] and category != 'combined':
|
|
sortTitles.append(MEMBERS_TITLES[category]['total'][0])
|
|
for role in Ent.ROLE_LIST:
|
|
if memberDisplayOptions[role]['show']:
|
|
sortTitles.append(MEMBERS_TITLES[category][role][0])
|
|
if not memberDisplayOptions[role]['countOnly']:
|
|
sortTitles.append(MEMBERS_TITLES[category][role][1])
|
|
|
|
def addMemberInfoToRow(row, groupMembers, typesSet, memberOptions, memberDisplayOptions, delimiter,
|
|
isSuspended, isArchived, ciGroupsAPI):
|
|
membersInfo = {
|
|
'combined': {'totalTitle': 'TotalCount',
|
|
Ent.ROLE_MEMBER: {'titles': ['MembersCount', 'Members'],
|
|
'count': 0, 'email': []},
|
|
Ent.ROLE_MANAGER: {'titles': ['ManagersCount', 'Managers'],
|
|
'count': 0, 'email': []},
|
|
Ent.ROLE_OWNER: {'titles': ['OwnersCount', 'Owners'],
|
|
'count': 0, 'email': []}},
|
|
'internal': {'totalTitle': 'TotalInternalCount',
|
|
Ent.ROLE_MEMBER: {'titles': ['InternalMembersCount', 'InternalMembers'],
|
|
'count': 0, 'email': []},
|
|
Ent.ROLE_MANAGER: {'titles': ['InternalManagersCount', 'InternalManagers'],
|
|
'count': 0, 'email': []},
|
|
Ent.ROLE_OWNER: {'titles': ['InternalOwnersCount', 'InternalOwners'],
|
|
'count': 0, 'email': []}},
|
|
'external': {'totalTitle': 'TotalExternalCount',
|
|
Ent.ROLE_MEMBER: {'titles': ['ExternalMembersCount', 'ExternalMembers'],
|
|
'count': 0, 'email': []},
|
|
Ent.ROLE_MANAGER: {'titles': ['ExternalManagersCount', 'ExternalManagers'],
|
|
'count': 0, 'email': []},
|
|
Ent.ROLE_OWNER: {'titles': ['ExternalOwnersCount', 'ExternalOwners'],
|
|
'count': 0, 'email': []}}}
|
|
checkMatch = checkMemberMatch if not ciGroupsAPI else checkCIMemberMatch
|
|
for member in groupMembers:
|
|
if not ciGroupsAPI:
|
|
member_email = member.get('email', member.get('id', None))
|
|
else:
|
|
member_email = member.get('preferredMemberKey', {}).get('id', member['name'])
|
|
if not member_email:
|
|
writeStderr(f' Not sure what to do with: {member}\n')
|
|
continue
|
|
if not memberDisplayOptions['showCategory']:
|
|
category = 'combined'
|
|
else:
|
|
if member_email.find('@') > 0:
|
|
_, domain = member_email.lower().split('@', 1)
|
|
category = 'internal' if domain in memberDisplayOptions['internalDomains'] else 'external'
|
|
else:
|
|
category = 'internal'
|
|
if not memberDisplayOptions[category]:
|
|
continue
|
|
if ((member['type'] in typesSet) and
|
|
(ciGroupsAPI or _checkMemberIsSuspendedIsArchived(member, isSuspended, isArchived)) and
|
|
checkMatch(member, memberOptions)):
|
|
role = member.get('role', Ent.ROLE_MEMBER)
|
|
if role not in {Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER}:
|
|
role = Ent.ROLE_MEMBER
|
|
if memberDisplayOptions[role]['show']:
|
|
membersInfo[category][role]['count'] += 1
|
|
if not memberDisplayOptions[role]['countOnly']:
|
|
membersInfo[category][role]['email'].append(member_email)
|
|
totalCount = 0
|
|
for category in memberDisplayOptions['categories']:
|
|
categoryCount = 0
|
|
for role in Ent.ROLE_LIST:
|
|
if memberDisplayOptions[role]['show']:
|
|
categoryCount += membersInfo[category][role]['count']
|
|
row[membersInfo[category][role]['titles'][0]] = membersInfo[category][role]['count']
|
|
if not memberDisplayOptions[role]['countOnly']:
|
|
row[membersInfo[category][role]['titles'][1]] = delimiter.join(membersInfo[category][role]['email'])
|
|
if memberDisplayOptions['totalCount'] and category != 'combined':
|
|
row[membersInfo[category]['totalTitle']] = categoryCount
|
|
totalCount += categoryCount
|
|
if memberDisplayOptions['totalCount']:
|
|
row['TotalCount'] = totalCount
|
|
|
|
PRINT_GROUPS_JSON_TITLES = ['email', 'JSON']
|
|
|
|
# gam print groups [todrive <ToDriveAttribute>*]
|
|
# [([domain|domains <DomainNameEntity>] ([member|showownedby <EmailItem>]|[(query <QueryGroup>)|(queries <QueryUserList>)]))|
|
|
# (group|group_ns|group_susp <GroupItem>)|
|
|
# (select <GroupEntity>)]
|
|
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
|
|
# [descriptionmatchpattern [not] <REMatchPattern>] (matchsetting [not] <GroupAttribute>)*
|
|
# [admincreatedmatch <Boolean>]
|
|
# [maxresults <Number>]
|
|
# [allfields|([basic] [settings] <GroupFieldName>* [fields <GroupFieldNameList>])]
|
|
# [ciallfields|(cifields <CIGroupFieldNameList>)]
|
|
# [nodeprecated]
|
|
# [roles <GroupRoleList>]
|
|
# [members|memberscount] [managers|managerscount] [owners|ownerscount] [totalcount] [countsonly]
|
|
# [internal] [internaldomains <DomainList>] [external]
|
|
# [includederivedmembership]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [types <GroupMemberTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
|
|
# [convertcrnl] [delimiter <Character>] [sortheaders]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [showitemcountonly]
|
|
def doPrintGroups():
|
|
def _printGroupRow(groupEntity, groupSettings, groupMembers):
|
|
nonlocal itemCount
|
|
row = {}
|
|
if matchSettings:
|
|
if not isinstance(groupSettings, dict):
|
|
return
|
|
for key, match in iter(matchSettings.items()):
|
|
gvalue = groupSettings.get(key)
|
|
if match['notvalues'] and gvalue in match['notvalues']:
|
|
return
|
|
if match['values'] and gvalue not in match['values']:
|
|
return
|
|
if showOwnedBy and not checkGroupShowOwnedBy(showOwnedBy, groupMembers):
|
|
return
|
|
if showItemCountOnly:
|
|
itemCount += 1
|
|
return
|
|
if deprecatedAttributesSet and isinstance(groupSettings, dict):
|
|
deprecatedKeys = []
|
|
for key in groupSettings:
|
|
if key in deprecatedAttributesSet:
|
|
deprecatedKeys.append(key)
|
|
for key in deprecatedKeys:
|
|
groupSettings.pop(key)
|
|
if FJQC.formatJSON:
|
|
row['email'] = groupEntity['email']
|
|
row['JSON'] = json.dumps(cleanJSON(groupEntity), ensure_ascii=False, sort_keys=True)
|
|
if rolesSet and groupMembers is not None:
|
|
row['JSON-members'] = json.dumps(groupMembers, ensure_ascii=False, sort_keys=True)
|
|
if isinstance(groupSettings, dict):
|
|
row['JSON-settings'] = json.dumps(cleanJSON(groupSettings), ensure_ascii=False, sort_keys=True)
|
|
groupCloudIdentity = ciGroups.get(row['email'], {})
|
|
if groupCloudIdentity:
|
|
row['JSON-cloudIdentity'] = json.dumps(cleanJSON(groupCloudIdentity, timeObjects=CIGROUP_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
return
|
|
for field in groupFieldsLists['cd']:
|
|
if field in groupEntity:
|
|
if isinstance(groupEntity[field], list):
|
|
row[field] = delimiter.join(groupEntity[field])
|
|
elif convertCRNL and field in GROUP_FIELDS_WITH_CRS_NLS:
|
|
row[field] = escapeCRsNLs(groupEntity[field])
|
|
else:
|
|
row[field] = groupEntity[field]
|
|
if rolesSet and groupMembers is not None:
|
|
addMemberInfoToRow(row, groupMembers, typesSet, memberOptions, memberDisplayOptions, delimiter,
|
|
isSuspended, isArchived, False)
|
|
if isinstance(groupSettings, dict):
|
|
for key, value in iter(groupSettings.items()):
|
|
if key not in {'kind', 'etag', 'email', 'name', 'description'}:
|
|
if value is None:
|
|
value = ''
|
|
csvPF.AddTitles(key)
|
|
if convertCRNL and key in GROUP_FIELDS_WITH_CRS_NLS:
|
|
row[key] = escapeCRsNLs(value)
|
|
else:
|
|
row[key] = value
|
|
groupCloudEntity = ciGroups.get(row['email'], {})
|
|
if groupCloudEntity:
|
|
for k, v in iter(groupCloudEntity.pop('labels', {}).items()):
|
|
if v == '':
|
|
groupCloudEntity[f'labels{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = True
|
|
else:
|
|
groupCloudEntity[f'labels{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = v
|
|
for key, value in sorted(iter(flattenJSON({'cloudIdentity': groupCloudEntity}, flattened={}, timeObjects=CIGROUP_TIME_OBJECTS).items())):
|
|
csvPF.AddTitles(key)
|
|
row[key] = value
|
|
csvPF.WriteRow(row)
|
|
|
|
def _callbackProcessGroupBasic(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
i = int(ri[RI_I])
|
|
if exception is not None:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.GROUP_GET_RETRY_REASONS:
|
|
if reason in GAPI.GROUP_GET_THROW_REASONS:
|
|
entityUnknownWarning(Ent.GROUP, ri[RI_ENTITY], i, int(ri[RI_COUNT]))
|
|
else:
|
|
errMsg = getHTTPError({}, http_status, reason, message)
|
|
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP, None], errMsg, i, int(ri[RI_COUNT]))
|
|
return
|
|
waitOnFailure(1, 10, reason, message)
|
|
try:
|
|
response = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=ri[RI_ENTITY], fields=cdfields)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError) as e:
|
|
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP, None], str(e), i, int(ri[RI_COUNT]))
|
|
return
|
|
entityList.append(response)
|
|
|
|
def _callbackProcessGroupMembers(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
i = int(ri[RI_I])
|
|
totalItems = 0
|
|
items = 'members'
|
|
pageMessage = getPageMessageForWhom(forWhom=ri[RI_ENTITY], showFirstLastItems=True)
|
|
if exception is not None:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.MEMBERS_RETRY_REASONS:
|
|
errMsg = getHTTPError({}, http_status, reason, message)
|
|
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], ri[RI_ROLE], None], errMsg, i, int(ri[RI_COUNT]))
|
|
groupData[i]['required'] -= 1
|
|
return
|
|
waitOnFailure(1, 10, reason, message)
|
|
try:
|
|
response = callGAPI(cd.members(), 'list',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
includeDerivedMembership=memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP],
|
|
groupKey=ri[RI_ENTITY], roles=ri[RI_ROLE], fields='nextPageToken,members(email,id,role,type,status)',
|
|
maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], ri[RI_ROLE], None], str(e), i, int(ri[RI_COUNT]))
|
|
groupData[i]['required'] -= 1
|
|
return
|
|
while True:
|
|
pageToken, totalItems = _processGAPIpagesResult(response, items, groupData[i][items], totalItems, pageMessage, 'email', ri[RI_ROLE])
|
|
if not pageToken:
|
|
_finalizeGAPIpagesResult(pageMessage)
|
|
break
|
|
try:
|
|
response = callGAPI(cd.members(), 'list',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
pageToken=pageToken,
|
|
includeDerivedMembership=memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP],
|
|
groupKey=ri[RI_ENTITY], roles=ri[RI_ROLE], fields='nextPageToken,members(email,id,role,type,status)',
|
|
maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], ri[RI_ROLE], None], str(e), i, int(ri[RI_COUNT]))
|
|
break
|
|
groupData[i]['required'] -= 1
|
|
|
|
def _callbackProcessGroupSettings(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
i = int(ri[RI_I])
|
|
if exception is not None:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason not in GAPI.DEFAULT_RETRY_REASONS+GAPI.GROUP_SETTINGS_RETRY_REASONS:
|
|
errMsg = getHTTPError({}, http_status, reason, message)
|
|
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP_SETTINGS, None], errMsg, i, int(ri[RI_COUNT]))
|
|
groupData[i]['required'] -= 1
|
|
return
|
|
waitOnFailure(1, 10, reason, message)
|
|
try:
|
|
response = callGAPI(gs.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
|
|
groupUniqueId=mapGroupEmailForSettings(ri[RI_ENTITY]), fields=gsfields)
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP_SETTINGS, None], Msg.DOES_NOT_EXIST, i, int(ri[RI_COUNT]))
|
|
response = {}
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.backendError, GAPI.invalid, GAPI.invalidParameter, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
|
|
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
|
entityActionFailedWarning([Ent.GROUP, ri[RI_ENTITY], Ent.GROUP_SETTINGS, None], str(e), i, int(ri[RI_COUNT]))
|
|
response = {}
|
|
groupData[i]['settings'] = response
|
|
groupData[i]['required'] -= 1
|
|
|
|
def _writeCompleteRows():
|
|
complete = [k for k in groupData if groupData[k]['required'] == 0]
|
|
for k in complete:
|
|
_printGroupRow(groupData[k]['entity'], groupData[k]['settings'], groupData[k]['members'])
|
|
del groupData[k]
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
kwargsDict = initUserGroupDomainQueryFilters()
|
|
convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
getCloudIdentity = getSettings = showCIgroupKey = sortHeaders = False
|
|
memberDisplayOptions = initPGGroupMemberDisplayOptions()
|
|
maxResults = None
|
|
groupFieldsLists = {'cd': ['email'], 'ci': [], 'gs': []}
|
|
csvPF = CSVPrintFile(groupFieldsLists['cd'])
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
rolesSet = set()
|
|
typesSet = set()
|
|
memberOptions = initMemberOptions()
|
|
entitySelection = isSuspended = isArchived = None
|
|
showOwnedBy = {}
|
|
matchPatterns = {}
|
|
matchSettings = {}
|
|
deprecatedAttributesSet = set()
|
|
ciGroups = {}
|
|
showItemCountOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getGroupFilters(myarg, kwargsDict, showOwnedBy):
|
|
pass
|
|
elif getGroupMatchPatterns(myarg, matchPatterns, False):
|
|
pass
|
|
elif myarg in {'group', 'groupns', 'groupsusp'}:
|
|
entitySelection = [getString(Cmd.OB_EMAIL_ADDRESS)]
|
|
if myarg == 'groupns':
|
|
isSuspended = False
|
|
elif myarg == 'groupsusp':
|
|
isSuspended = True
|
|
elif myarg == 'select':
|
|
entitySelection = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
elif myarg in SUSPENDED_ARGUMENTS:
|
|
isSuspended = _getIsSuspended(myarg)
|
|
elif myarg in ARCHIVED_ARGUMENTS:
|
|
isArchived = _getIsArchived(myarg)
|
|
elif myarg == 'maxresults':
|
|
maxResults = getInteger(minVal=1, maxVal=200)
|
|
elif myarg == 'nodeprecated':
|
|
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_DISCOVER_ATTRIBUTES.values())])
|
|
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_ASSIST_CONTENT_ATTRIBUTES.values())])
|
|
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_MODERATE_CONTENT_ATTRIBUTES.values())])
|
|
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_MODERATE_MEMBERS_ATTRIBUTES.values())])
|
|
deprecatedAttributesSet.update([attr[0] for attr in iter(GROUP_DEPRECATED_ATTRIBUTES.values())])
|
|
elif myarg in {'convertcrnl', 'converttextnl', 'convertfooternl'}:
|
|
convertCRNL = True
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg == 'basic':
|
|
sortHeaders = True
|
|
for field in GROUP_FIELDS_CHOICE_MAP:
|
|
csvPF.AddField(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
|
|
elif myarg in {'ciallfields', 'allcifields'}:
|
|
sortHeaders = True
|
|
groupFieldsLists['ci'] = []
|
|
for field in CIGROUP_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
elif myarg == 'settings':
|
|
getSettings = sortHeaders = True
|
|
elif myarg == 'allfields':
|
|
getSettings = sortHeaders = True
|
|
groupFieldsLists['cd'] = []
|
|
groupFieldsLists['gs'] = []
|
|
for field in GROUP_FIELDS_CHOICE_MAP:
|
|
csvPF.AddField(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
|
|
elif myarg == 'sortheaders':
|
|
sortHeaders = getBoolean()
|
|
elif myarg in GROUP_FIELDS_CHOICE_MAP:
|
|
csvPF.AddField(myarg, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
|
|
elif myarg in GROUP_ATTRIBUTES_SET:
|
|
attrProperties = getGroupAttrProperties(myarg)
|
|
csvPF.AddField(myarg, {myarg: attrProperties[0]}, groupFieldsLists['gs'])
|
|
elif myarg == 'fields':
|
|
for field in _getFieldsList():
|
|
if field in GROUP_FIELDS_CHOICE_MAP:
|
|
csvPF.AddField(field, GROUP_FIELDS_CHOICE_MAP, groupFieldsLists['cd'])
|
|
else:
|
|
attrProperties = getGroupAttrProperties(field)
|
|
if attrProperties is None:
|
|
invalidChoiceExit(field, list(GROUP_FIELDS_CHOICE_MAP)+list(GROUP_ATTRIBUTES_SET), True)
|
|
csvPF.AddField(field, {field: attrProperties[0]}, groupFieldsLists['gs'])
|
|
elif myarg == 'cifields':
|
|
for field in _getFieldsList():
|
|
if field in CIGROUP_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
else:
|
|
invalidChoiceExit(field, list(CIGROUP_FIELDS_CHOICE_MAP), True)
|
|
elif myarg == 'matchsetting':
|
|
valueList = getChoice({'not': 'notvalues'}, mapChoice=True, defaultChoice='values')
|
|
matchBody = {}
|
|
getGroupAttrValue(getString(Cmd.OB_FIELD_NAME).lower(), matchBody)
|
|
for key, value in iter(matchBody.items()):
|
|
matchSettings.setdefault(key, {'notvalues': [], 'values': []})
|
|
matchSettings[key][valueList].append(value)
|
|
elif getPGGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
pass
|
|
elif getGroupMemberTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif myarg == 'includederivedmembership':
|
|
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = True
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if not typesSet:
|
|
typesSet = ALL_GROUP_MEMBER_TYPES
|
|
updateFieldsForGroupMatchPatterns(matchPatterns, groupFieldsLists['cd'], csvPF)
|
|
if groupFieldsLists['cd']:
|
|
cdfields = ','.join(set(groupFieldsLists['cd']))
|
|
cdfieldsnp = f'nextPageToken,groups({cdfields})'
|
|
else:
|
|
cdfields = cdfieldsnp = None
|
|
if matchSettings:
|
|
groupFieldsLists['gs'].extend(list(matchSettings))
|
|
if groupFieldsLists['gs']:
|
|
getSettings = True
|
|
gsfields = ','.join(set(groupFieldsLists['gs']))
|
|
else:
|
|
gsfields = None
|
|
if getSettings:
|
|
gs = buildGAPIObject(API.GROUPSSETTINGS)
|
|
if groupFieldsLists['ci']:
|
|
setTrueCustomerId(cd)
|
|
getCloudIdentity = True
|
|
showCIgroupKey = 'groupKey' in groupFieldsLists['ci']
|
|
if not showCIgroupKey:
|
|
groupFieldsLists['ci'].append('groupKey(id)')
|
|
cifields = ','.join(set(groupFieldsLists['ci']))
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
getRolesSet = rolesSet.copy()
|
|
if showOwnedBy:
|
|
getRolesSet.add(Ent.ROLE_OWNER)
|
|
getRoles = ','.join(sorted(getRolesSet))
|
|
showDetails = getRoles or getSettings or getCloudIdentity
|
|
if rolesSet:
|
|
setMemberDisplayTitles(memberDisplayOptions, csvPF)
|
|
if FJQC.formatJSON:
|
|
sortHeaders = False
|
|
csvPF.SetJSONTitles(PRINT_GROUPS_JSON_TITLES)
|
|
if rolesSet:
|
|
csvPF.AddJSONTitle('JSON-members')
|
|
if getSettings:
|
|
csvPF.AddJSONTitle('JSON-settings')
|
|
if getCloudIdentity:
|
|
csvPF.AddJSONTitle('JSON-cloudIdentity')
|
|
if entitySelection is None:
|
|
entityList = []
|
|
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
|
|
kwargs = kwargsQuery[0]
|
|
query = kwargsQuery[1]
|
|
query, pquery = groupFilters(kwargs, query)
|
|
printGettingAllAccountEntities(Ent.GROUP, pquery)
|
|
try:
|
|
entityList.extend(callGAPIpages(cd.groups(), 'list', 'groups',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', query=query, fields=cdfieldsnp, maxResults=maxResults, **kwargs))
|
|
except (GAPI.invalidMember, GAPI.invalidInput) as e:
|
|
if not invalidMember(query):
|
|
entityActionFailedExit([Ent.GROUP, None], str(e))
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
if kwargs.get('domain'):
|
|
badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain'])
|
|
else:
|
|
accessErrorExit(cd)
|
|
else:
|
|
svcargs = dict([('groupKey', None), ('fields', cdfields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
cdmethod = getattr(cd.groups(), 'get')
|
|
cdbatch = cd.new_batch_http_request(callback=_callbackProcessGroupBasic)
|
|
cdbcount = 0
|
|
entityList = []
|
|
i = 0
|
|
count = len(entitySelection)
|
|
for groupEntity in entitySelection:
|
|
i += 1
|
|
groupEmail = normalizeEmailAddressOrUID(groupEntity)
|
|
svcparms = svcargs.copy()
|
|
svcparms['groupKey'] = groupEmail
|
|
printGettingEntityItem(Ent.GROUP, groupEmail, i, count)
|
|
cdbatch.add(cdmethod(**svcparms), request_id=batchRequestID(groupEmail, i, count, 0, 0, None))
|
|
cdbcount += 1
|
|
if cdbcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(cdbatch)
|
|
cdbatch = cd.new_batch_http_request(callback=_callbackProcessGroupBasic)
|
|
cdbcount = 0
|
|
if cdbcount > 0:
|
|
cdbatch.execute()
|
|
required = 0
|
|
if getRoles:
|
|
required += 1
|
|
svcargs = dict([('groupKey', None), ('roles', getRoles), ('fields', 'nextPageToken,members(email,id,role,type,status)'),
|
|
('includeDerivedMembership', memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP]),
|
|
('maxResults', GC.Values[GC.MEMBER_MAX_RESULTS])]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
if getSettings:
|
|
required += 1
|
|
svcargsgs = dict([('groupUniqueId', None), ('fields', gsfields)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
cdmethod = getattr(cd.members(), 'list')
|
|
cdbatch = cd.new_batch_http_request(callback=_callbackProcessGroupMembers)
|
|
cdbcount = 0
|
|
if getSettings:
|
|
gsmethod = getattr(gs.groups(), 'get')
|
|
gsbatch = gs.new_batch_http_request(callback=_callbackProcessGroupSettings)
|
|
gsbcount = 0
|
|
groupData = {}
|
|
itemCount = 0
|
|
i = 0
|
|
count = len(entityList)
|
|
for groupEntity in entityList:
|
|
i += 1
|
|
groupEmail = groupEntity['email']
|
|
if not checkGroupMatchPatterns(groupEmail, groupEntity, matchPatterns):
|
|
continue
|
|
if not showDetails:
|
|
_printGroupRow(groupEntity, None, None)
|
|
continue
|
|
if getCloudIdentity:
|
|
_, name, groupEmail = convertGroupEmailToCloudID(ci, groupEmail, i, count)
|
|
printGettingEntityItemForWhom(Ent.CLOUD_IDENTITY_GROUP, groupEmail, i, count)
|
|
if name:
|
|
try:
|
|
ciGroup = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, fields=cifields)
|
|
key = ciGroup['groupKey']['id']
|
|
if not showCIgroupKey:
|
|
ciGroup.pop('groupKey')
|
|
ciGroups[key] = ciGroup
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.GROUP, groupEmail, Ent.CLOUD_IDENTITY_GROUP, None], str(e), i, count)
|
|
groupData[i] = {'entity': groupEntity, 'cloudIdentity': {}, 'settings': getSettings, 'members': [], 'required': required}
|
|
if getRoles:
|
|
printGettingEntityItemForWhom(getRoles, groupEmail, i, count)
|
|
svcparms = svcargs.copy()
|
|
svcparms['groupKey'] = groupEmail
|
|
cdbatch.add(cdmethod(**svcparms), request_id=batchRequestID(groupEmail, i, count, 0, 0, None, getRoles))
|
|
cdbcount += 1
|
|
if cdbcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(cdbatch)
|
|
cdbatch = cd.new_batch_http_request(callback=_callbackProcessGroupMembers)
|
|
cdbcount = 0
|
|
_writeCompleteRows()
|
|
if getSettings:
|
|
if not GroupIsAbuseOrPostmaster(groupEmail):
|
|
printGettingEntityItemForWhom(Ent.GROUP_SETTINGS, groupEmail, i, count)
|
|
svcparmsgs = svcargsgs.copy()
|
|
svcparmsgs['groupUniqueId'] = mapGroupEmailForSettings(groupEmail)
|
|
gsbatch.add(gsmethod(**svcparmsgs), request_id=batchRequestID(groupEmail, i, count, 0, 0, None))
|
|
gsbcount += 1
|
|
if gsbcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(gsbatch)
|
|
gsbatch = gs.new_batch_http_request(callback=_callbackProcessGroupSettings)
|
|
gsbcount = 0
|
|
_writeCompleteRows()
|
|
else:
|
|
groupData[i]['settings'] = False
|
|
groupData[i]['required'] -= 1
|
|
if cdbcount > 0:
|
|
cdbatch.execute()
|
|
if getSettings and gsbcount > 0:
|
|
gsbatch.execute()
|
|
_writeCompleteRows()
|
|
if showItemCountOnly:
|
|
writeStdout(f'{itemCount}\n')
|
|
return
|
|
if sortHeaders:
|
|
sortTitles = ['email']+GROUP_INFO_PRINT_ORDER+['aliases', 'nonEditableAliases']
|
|
if getSettings:
|
|
sortTitles += sorted([attr[0] for attr in iter(GROUP_SETTINGS_ATTRIBUTES.values())])
|
|
for key in GROUP_MERGED_ATTRIBUTES_PRINT_ORDER:
|
|
sortTitles.append(key)
|
|
if not deprecatedAttributesSet:
|
|
sortTitles += sorted([attr[0] for attr in iter(GROUP_MERGED_TO_COMPONENT_MAP[key].values())])
|
|
if not deprecatedAttributesSet:
|
|
sortTitles += sorted([attr[0] for attr in iter(GROUP_DEPRECATED_ATTRIBUTES.values())])
|
|
if rolesSet:
|
|
setMemberDisplaySortTitles(memberDisplayOptions, sortTitles)
|
|
csvPF.SetSortTitles(sortTitles)
|
|
csvPF.writeCSVfile('Groups')
|
|
|
|
def mapCIGroupMemberFieldNames(member):
|
|
member['email'] = member['preferredMemberKey'].pop('id')
|
|
if not member['preferredMemberKey']:
|
|
member.pop('preferredMemberKey')
|
|
if 'name' in member:
|
|
member['id'] = member.pop('name')
|
|
|
|
INFO_GROUPMEMBERS_FIELDS = ['role', 'type', 'status', 'delivery_settings']
|
|
|
|
# gam <UserTypeEntity> info member <GroupEntity>
|
|
def infoGroupMembers(entityList, ciGroupsAPI=False):
|
|
if not ciGroupsAPI:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
fieldsList = INFO_GROUPMEMBERS_FIELDS
|
|
fields = ','.join(fieldsList)
|
|
else:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
entityType = GROUP_CIGROUP_ENTITYTYPE_MAP[ciGroupsAPI]
|
|
groups = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
checkForExtraneousArguments()
|
|
groupsLists = groups if isinstance(groups, dict) else None
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for user in entityList:
|
|
i += 1
|
|
memberKey = normalizeEmailAddressOrUID(user)
|
|
if groupsLists:
|
|
groups = groupsLists[user]
|
|
jcount = len(groups)
|
|
entityPerformActionNumItems([Ent.MEMBER, memberKey], jcount, entityType, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for group in groups:
|
|
j += 1
|
|
if not ciGroupsAPI:
|
|
try:
|
|
ci, _, groupKey = convertGroupCloudIDToEmail(ci, group, i, count)
|
|
result = callGAPI(cd.members(), 'get',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND], retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=groupKey, memberKey=memberKey, fields=fields)
|
|
result.setdefault('role', Ent.ROLE_MEMBER)
|
|
printEntity([entityType, groupKey], j, jcount)
|
|
Ind.Increment()
|
|
for field in INFO_GROUPMEMBERS_FIELDS:
|
|
printKeyValueList([field, result[field]])
|
|
Ind.Decrement()
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([entityType, group], str(e), j, jcount)
|
|
except GAPI.memberNotFound:
|
|
entityActionFailedWarning([entityType, group, Ent.MEMBER, memberKey], Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.MEMBER)), j, jcount)
|
|
else:
|
|
_, groupKey, group = convertGroupEmailToCloudID(ci, group, j, jcount)
|
|
if not groupKey:
|
|
continue
|
|
try:
|
|
memberName = callGAPI(ci.groups().memberships(), 'lookup',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
|
|
parent=groupKey, memberKey_id=memberKey, fields='name').get('name')
|
|
result = callGAPI(ci.groups().memberships(), 'get',
|
|
name=memberName)
|
|
printEntity([entityType, group], j, jcount)
|
|
Ind.Increment()
|
|
getCIGroupMemberRoleFixType(result)
|
|
kvList = ['role', result['role']]
|
|
if 'expireTime' in result:
|
|
kvList.extend(['expireTime', formatLocalTime(result['expireTime'])])
|
|
printKeyValueList(kvList)
|
|
printKeyValueList(['type', result['type']])
|
|
for field in ['createTime', 'updateTime']:
|
|
printKeyValueList([field, formatLocalTime(result[field])])
|
|
if 'deliverySetting' in result:
|
|
printKeyValueList(['deliverySetting', result['deliverySetting']])
|
|
Ind.Decrement()
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([entityType, groupKey], str(e), j, jcount)
|
|
except (GAPI.notFound, GAPI.memberNotFound, GAPI.permissionDenied):
|
|
entityActionFailedWarning([entityType, groupKey, Ent.MEMBER, memberKey], Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.MEMBER)), j, jcount)
|
|
Ind.Decrement()
|
|
|
|
# gam info member <UserTypeEntity> <GroupEntity>
|
|
def doInfoGroupMembers():
|
|
infoGroupMembers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1], False)
|
|
|
|
def getGroupMembersEntityList(cd, entityList, matchPatterns, fieldsList, kwargsDict):
|
|
if entityList is None:
|
|
updateFieldsForGroupMatchPatterns(matchPatterns, fieldsList)
|
|
entityList = []
|
|
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
|
|
kwargs = kwargsQuery[0]
|
|
query = kwargsQuery[1]
|
|
query, pquery = groupFilters(kwargs, query)
|
|
printGettingAllAccountEntities(Ent.GROUP, pquery)
|
|
try:
|
|
entityList.extend(callGAPIpages(cd.groups(), 'list', 'groups',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='email',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', query=query, fields=f'nextPageToken,groups({",".join(set(fieldsList))})', **kwargs))
|
|
except (GAPI.invalidMember, GAPI.invalidInput) as e:
|
|
if not invalidMember(query):
|
|
entityActionFailedExit([Ent.GROUP, None], str(e))
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
if kwargs.get('domain'):
|
|
badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain'])
|
|
else:
|
|
accessErrorExit(cd)
|
|
else:
|
|
clearUnneededGroupMatchPatterns(matchPatterns)
|
|
return entityList
|
|
|
|
def getGroupMembers(cd, groupEmail, memberRoles, membersList, membersSet, i, count,
|
|
memberOptions, memberDisplayOptions, level, typesSet):
|
|
def _getMemberDeliverySettings(member):
|
|
if 'delivery_settings' not in member:
|
|
try:
|
|
member['delivery_settings'] = callGAPI(cd.members(), 'get',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=groupEmail, memberKey=member['id'], fields='delivery_settings')['delivery_settings']
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
|
|
pass
|
|
except GAPI.memberNotFound:
|
|
pass
|
|
else:
|
|
memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS] = False
|
|
|
|
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, groupEmail, i, count)
|
|
validRoles, listRoles, listFields = _getRoleVerification(memberRoles, 'nextPageToken,members(email,id,role,status,type,delivery_settings)')
|
|
try:
|
|
groupMembers = callGAPIpages(cd.members(), 'list', 'members',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
includeDerivedMembership=memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP],
|
|
groupKey=groupEmail, roles=listRoles, fields=listFields, maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(Ent.GROUP, groupEmail, i, count)
|
|
return
|
|
checkCategory = memberDisplayOptions['showCategory']
|
|
if not memberOptions[MEMBEROPTION_RECURSIVE]:
|
|
if memberOptions[MEMBEROPTION_NODUPLICATES]:
|
|
for member in groupMembers:
|
|
if (_checkMemberRoleIsSuspendedIsArchived(member, validRoles, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
|
|
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions)) and
|
|
member['id'] not in membersSet):
|
|
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
|
|
_getMemberDeliverySettings(member)
|
|
membersSet.add(member['id'])
|
|
if member['type'] in typesSet and checkMemberMatch(member, memberOptions):
|
|
membersList.append(member)
|
|
else:
|
|
for member in groupMembers:
|
|
if (_checkMemberRoleIsSuspendedIsArchived(member, validRoles, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
|
|
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
|
|
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
|
|
_getMemberDeliverySettings(member)
|
|
if member['type'] in typesSet and checkMemberMatch(member, memberOptions):
|
|
membersList.append(member)
|
|
elif memberOptions[MEMBEROPTION_NODUPLICATES]:
|
|
groupMemberList = []
|
|
for member in groupMembers:
|
|
if member['type'] != Ent.TYPE_GROUP:
|
|
if ((member['type'] in typesSet and
|
|
checkMemberMatch(member, memberOptions) and
|
|
_checkMemberRoleIsSuspendedIsArchived(member, validRoles, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
|
|
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions)) and
|
|
member['id'] not in membersSet)):
|
|
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
|
|
_getMemberDeliverySettings(member)
|
|
membersSet.add(member['id'])
|
|
member['level'] = level
|
|
member['subgroup'] = groupEmail
|
|
membersList.append(member)
|
|
else:
|
|
if member['id'] not in membersSet:
|
|
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
|
|
_getMemberDeliverySettings(member)
|
|
membersSet.add(member['id'])
|
|
if (member['type'] in typesSet and
|
|
checkMemberMatch(member, memberOptions) and
|
|
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
|
|
member['level'] = level
|
|
member['subgroup'] = groupEmail
|
|
membersList.append(member)
|
|
groupMemberList.append(member['email'])
|
|
for member in groupMemberList:
|
|
getGroupMembers(cd, member, memberRoles, membersList, membersSet, i, count,
|
|
memberOptions, memberDisplayOptions, level+1, typesSet)
|
|
else:
|
|
for member in groupMembers:
|
|
if member['type'] != Ent.TYPE_GROUP:
|
|
if ((member['type'] in typesSet) and
|
|
checkMemberMatch(member, memberOptions) and
|
|
_checkMemberRoleIsSuspendedIsArchived(member, validRoles,
|
|
memberOptions[MEMBEROPTION_ISSUSPENDED],
|
|
memberOptions[MEMBEROPTION_ISARCHIVED]) and
|
|
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
|
|
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
|
|
_getMemberDeliverySettings(member)
|
|
member['level'] = level
|
|
member['subgroup'] = groupEmail
|
|
membersList.append(member)
|
|
else:
|
|
if (member['type'] in typesSet and
|
|
checkMemberMatch(member, memberOptions) and
|
|
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
|
|
member['level'] = level
|
|
member['subgroup'] = groupEmail
|
|
membersList.append(member)
|
|
getGroupMembers(cd, member['email'], memberRoles, membersList, membersSet, i, count,
|
|
memberOptions, memberDisplayOptions, level+1, typesSet)
|
|
|
|
GROUPMEMBERS_FIELDS_CHOICE_MAP = {
|
|
'delivery': 'delivery_settings',
|
|
'deliverysettings': 'delivery_settings',
|
|
'email': 'email',
|
|
'group': 'group',
|
|
'groupemail': 'group',
|
|
'id': 'id',
|
|
'name': 'name',
|
|
'role': 'role',
|
|
'status': 'status',
|
|
'type': 'type',
|
|
'useremail': 'email',
|
|
}
|
|
|
|
GROUPMEMBERS_DEFAULT_FIELDS = ['group', 'type', 'role', 'id', 'status', 'email']
|
|
|
|
# gam print group-members [todrive <ToDriveAttribute>*]
|
|
# [([domain|domains <DomainNameEntity>] ([member|showownedby <EmailItem>]|[(query <QueryGroup>)|(queries <QueryUserList>)]))|
|
|
# (group|group_ns|group_susp <GroupItem>)|
|
|
# (select <GroupEntity>)]
|
|
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
|
|
# [descriptionmatchpattern [not] <REMatchPattern>]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners]
|
|
# [internal] [internaldomains <DomainList>] [external]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [types <GroupMemberTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
|
|
# [membernames] [showdeliverysettings]
|
|
# <MembersFieldName>* [fields <MembersFieldNameList>]
|
|
# [userfields <UserFieldNameList>]
|
|
# [allschemas|(schemas|custom|customschemas <SchemaNameList>)]
|
|
# [(recursive [noduplicates])|includederivedmembership] [nogroupemail]
|
|
# [peoplelookup|(peoplelookupuser <EmailAddress>)]
|
|
# [unknownname <String>] [cachememberinfo [Boolean]]
|
|
# [formatjson [quotechar <Character>]]
|
|
def doPrintGroupMembers():
|
|
def getNameFromPeople(memberId):
|
|
try:
|
|
info = callGAPI(people.people(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName=f'people/{memberId}', personFields='names')
|
|
if 'names' in info:
|
|
for sourceType in ['PROFILE', 'CONTACT']:
|
|
for name in info['names']:
|
|
if name['metadata']['source']['type'] == sourceType:
|
|
return name['displayName']
|
|
except (GAPI.notFound, GAPI.serviceNotAvailable, GAPI.forbidden, GAPI.permissionDenied, GAPI.failedPrecondition):
|
|
pass
|
|
return unknownName
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
people = None
|
|
memberOptions = initMemberOptions()
|
|
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
|
|
groupColumn = True
|
|
customerKey = GC.Values[GC.CUSTOMER_ID]
|
|
kwargsDict = initUserGroupDomainQueryFilters()
|
|
subTitle = f'{Msg.ALL} {Ent.Plural(Ent.GROUP)}'
|
|
fieldsList = []
|
|
csvPF = CSVPrintFile('group')
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
entityList = None
|
|
showOwnedBy = {}
|
|
cdfieldsList = ['email']
|
|
userFieldsList = []
|
|
schemaParms = _initSchemaParms('basic')
|
|
rolesSet = set()
|
|
typesSet = set()
|
|
matchPatterns = {}
|
|
showDeliverySettings = False
|
|
cacheMemberInfo = False
|
|
memberInfo = {}
|
|
memberNames = {}
|
|
unknownName = UNKNOWN
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getGroupFilters(myarg, kwargsDict, showOwnedBy):
|
|
pass
|
|
elif getGroupMatchPatterns(myarg, matchPatterns, False):
|
|
pass
|
|
elif myarg in {'group', 'groupns', 'groupsusp'}:
|
|
entityList = [getString(Cmd.OB_EMAIL_ADDRESS)]
|
|
subTitle = f'{Ent.Singular(Ent.GROUP)}={entityList[0]}'
|
|
if myarg == 'groupns':
|
|
memberOptions[MEMBEROPTION_ISSUSPENDED] = False
|
|
elif myarg == 'groupsusp':
|
|
memberOptions[MEMBEROPTION_ISSUSPENDED] = True
|
|
elif myarg == 'select':
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
subTitle = f'{Msg.SELECTED} {Ent.Plural(Ent.GROUP)}'
|
|
elif myarg in SUSPENDED_ARGUMENTS:
|
|
memberOptions[MEMBEROPTION_ISSUSPENDED] = _getIsSuspended(myarg)
|
|
elif myarg in ARCHIVED_ARGUMENTS:
|
|
memberOptions[MEMBEROPTION_ISARCHIVED] = _getIsArchived(myarg)
|
|
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
pass
|
|
elif getGroupMemberTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif csvPF.GetFieldsListTitles(myarg, GROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='email'):
|
|
pass
|
|
elif myarg == 'membernames':
|
|
memberOptions[MEMBEROPTION_MEMBERNAMES] = True
|
|
elif myarg == 'showdeliverysettings':
|
|
showDeliverySettings = True
|
|
elif myarg == 'userfields':
|
|
for field in _getFieldsList():
|
|
if field in USER_FIELDS_CHOICE_MAP:
|
|
csvPF.AddField(field, USER_FIELDS_CHOICE_MAP, userFieldsList)
|
|
else:
|
|
invalidChoiceExit(field, USER_FIELDS_CHOICE_MAP, True)
|
|
elif myarg in {'allschemas', 'custom', 'schemas', 'customschemas'}:
|
|
if myarg == 'allschemas':
|
|
schemaParms = _initSchemaParms('full')
|
|
else:
|
|
_getSchemaNameList(schemaParms)
|
|
userFieldsList.append('customSchemas')
|
|
elif myarg == 'noduplicates':
|
|
memberOptions[MEMBEROPTION_NODUPLICATES] = True
|
|
elif myarg == 'recursive':
|
|
memberOptions[MEMBEROPTION_RECURSIVE] = True
|
|
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = False
|
|
elif myarg == 'includederivedmembership':
|
|
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = True
|
|
memberOptions[MEMBEROPTION_RECURSIVE] = False
|
|
elif myarg == 'nogroupemail':
|
|
groupColumn = False
|
|
elif myarg == 'peoplelookup':
|
|
people = buildGAPIObject(API.PEOPLE)
|
|
elif myarg == 'peoplelookupuser':
|
|
_, people = buildGAPIServiceObject(API.PEOPLE, getEmailAddress())
|
|
if not people:
|
|
return
|
|
elif myarg == 'unknownname':
|
|
unknownName = getString(Cmd.OB_STRING)
|
|
elif myarg == 'cachememberinfo':
|
|
cacheMemberInfo = getBoolean()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if not typesSet:
|
|
typesSet = {Ent.TYPE_USER} if memberOptions[MEMBEROPTION_RECURSIVE] else ALL_GROUP_MEMBER_TYPES
|
|
entityList = getGroupMembersEntityList(cd, entityList, matchPatterns, cdfieldsList, kwargsDict)
|
|
if not fieldsList:
|
|
for field in GROUPMEMBERS_DEFAULT_FIELDS:
|
|
csvPF.AddField(field, {field: field}, fieldsList)
|
|
if showDeliverySettings:
|
|
field = 'delivery_settings'
|
|
csvPF.AddField(field, {field: field}, fieldsList)
|
|
elif 'name'in fieldsList:
|
|
memberOptions[MEMBEROPTION_MEMBERNAMES] = True
|
|
fieldsList.remove('name')
|
|
if 'group' in fieldsList:
|
|
fieldsList.remove('group')
|
|
if not groupColumn:
|
|
csvPF.RemoveTitles(['group'])
|
|
if memberDisplayOptions['showCategory']:
|
|
csvPF.AddTitles('category')
|
|
if userFieldsList:
|
|
if not memberOptions[MEMBEROPTION_MEMBERNAMES] and 'name.fullName' in userFieldsList:
|
|
memberOptions[MEMBEROPTION_MEMBERNAMES] = True
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
|
|
if 'name.fullName' not in userFieldsList:
|
|
userFieldsList.append('name.fullName')
|
|
csvPF.AddTitles('name')
|
|
csvPF.RemoveTitles([f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}fullName'])
|
|
if FJQC.formatJSON:
|
|
if groupColumn:
|
|
csvPF.SetJSONTitles(['group', 'JSON'])
|
|
else:
|
|
csvPF.SetJSONTitles(['JSON'])
|
|
memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS] = 'delivery_settings' in fieldsList
|
|
userFields = getFieldsFromFieldsList(userFieldsList)
|
|
if not rolesSet:
|
|
rolesSet = ALL_GROUP_ROLES
|
|
getRolesSet = rolesSet.copy()
|
|
if showOwnedBy:
|
|
getRolesSet.add(Ent.ROLE_OWNER)
|
|
getRoles = ','.join(sorted(getRolesSet))
|
|
level = 0
|
|
setCustomerMemberEmail = 'email' in fieldsList
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
if isinstance(group, dict):
|
|
groupEmail = group['email']
|
|
else:
|
|
groupEmail = convertUIDtoEmailAddress(group, cd, 'group', ciGroupsAPI=True)
|
|
ci, _, groupEmail = convertGroupCloudIDToEmail(ci, groupEmail, i, count)
|
|
if not checkGroupMatchPatterns(groupEmail, group, matchPatterns):
|
|
continue
|
|
membersList = []
|
|
membersSet = set()
|
|
getGroupMembers(cd, groupEmail, getRoles, membersList, membersSet, i, count,
|
|
memberOptions, memberDisplayOptions, level, typesSet)
|
|
if showOwnedBy and not checkGroupShowOwnedBy(showOwnedBy, membersList):
|
|
continue
|
|
for member in membersList:
|
|
if member['role'] not in rolesSet:
|
|
continue
|
|
memberId = member['id']
|
|
row = {}
|
|
if groupColumn:
|
|
row['group'] = groupEmail
|
|
if memberOptions[MEMBEROPTION_RECURSIVE]:
|
|
row['level'] = member['level']
|
|
row['subgroup'] = member['subgroup']
|
|
for title in fieldsList:
|
|
row[title] = member.get(title, '')
|
|
if setCustomerMemberEmail and (memberId == customerKey):
|
|
row['email'] = memberId
|
|
if memberDisplayOptions['showCategory']:
|
|
row['category'] = member['category']
|
|
memberType = member.get('type')
|
|
if userFieldsList:
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
|
|
row['name'] = unknownName
|
|
if memberType == Ent.TYPE_USER:
|
|
try:
|
|
if not cacheMemberInfo or memberId not in memberInfo:
|
|
mbinfo = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE, GAPI.FAILED_PRECONDITION],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userKey=memberId, projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
|
|
fields=userFields)
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
|
|
mname = mbinfo['name'].pop('fullName', unknownName)
|
|
row['name'] = mname
|
|
if not mbinfo['name']:
|
|
mbinfo.pop('name')
|
|
if cacheMemberInfo:
|
|
memberNames[memberId] = mname
|
|
if cacheMemberInfo:
|
|
memberInfo[memberId] = mbinfo
|
|
else:
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
|
|
row['name'] = memberNames[memberId]
|
|
mbinfo = memberInfo.get(memberId, {})
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(flattenJSON(mbinfo, flattened=row))
|
|
else:
|
|
row.update(mbinfo)
|
|
fjrow = {}
|
|
if groupColumn:
|
|
fjrow['group'] = groupEmail
|
|
fjrow['JSON'] = json.dumps(cleanJSON(row, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(fjrow)
|
|
continue
|
|
except GAPI.userNotFound:
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
|
|
if people:
|
|
if memberId not in memberNames:
|
|
memberNames[memberId] = getNameFromPeople(memberId)
|
|
else:
|
|
memberNames[memberId] = unknownName
|
|
row['name'] = memberNames[memberId]
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.serviceNotAvailable, GAPI.failedPrecondition):
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES] and cacheMemberInfo:
|
|
memberNames[memberId] = unknownName
|
|
elif memberType == Ent.TYPE_GROUP:
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
|
|
try:
|
|
if not cacheMemberInfo or memberId not in memberNames:
|
|
row['name'] = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE, GAPI.FAILED_PRECONDITION],
|
|
retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=memberId, fields='name')['name']
|
|
if cacheMemberInfo:
|
|
memberNames[memberId] = row['name']
|
|
else:
|
|
row['name'] = memberNames[memberId]
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.systemError, GAPI.serviceNotAvailable, GAPI.failedPrecondition):
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES] and cacheMemberInfo:
|
|
memberNames[memberId] = unknownName
|
|
elif memberType == Ent.TYPE_CUSTOMER:
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES]:
|
|
try:
|
|
if not cacheMemberInfo or memberId not in memberNames:
|
|
row['name'] = callGAPI(cd.customers(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerKey=memberId, fields='customerDomain')['customerDomain']
|
|
if cacheMemberInfo:
|
|
memberNames[memberId] = row['name']
|
|
else:
|
|
row['name'] = memberNames[memberId]
|
|
except (GAPI.badRequest, GAPI.invalidInput, GAPI.resourceNotFound, GAPI.forbidden):
|
|
if memberOptions[MEMBEROPTION_MEMBERNAMES] and cacheMemberInfo:
|
|
memberNames[memberId] = unknownName
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRow(row)
|
|
else:
|
|
fjrow = {}
|
|
if groupColumn:
|
|
fjrow['group'] = groupEmail
|
|
fjrow['JSON'] = json.dumps(cleanJSON(row, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(fjrow)
|
|
if not FJQC.formatJSON:
|
|
sortTitles = GROUPMEMBERS_DEFAULT_FIELDS
|
|
if memberDisplayOptions['showCategory']:
|
|
sortTitles.append('category')
|
|
csvPF.SetSortTitles(sortTitles)
|
|
csvPF.SortTitles()
|
|
csvPF.SetSortTitles([])
|
|
if memberOptions[MEMBEROPTION_RECURSIVE]:
|
|
csvPF.MoveTitlesToEnd(['level', 'subgroup'])
|
|
csvPF.writeCSVfile(f'Group Members ({subTitle})')
|
|
|
|
# gam show group-members
|
|
# [([domain|domains <DomainNameEntity>] ([member|showownedby <EmailItem>]|[(query <QueryGroup>)|(queries <QueryUserList>)]))|
|
|
# (group|group_ns|group_susp <GroupItem>)|
|
|
# (select <GroupEntity>)]
|
|
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
|
|
# [descriptionmatchpattern [not] <REMatchPattern>]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners] [depth <Number>]
|
|
# [internal] [internaldomains <DomainList>] [external]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [types <GroupMemberTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
|
|
# [includederivedmembership]
|
|
def doShowGroupMembers():
|
|
def _roleOrder(key):
|
|
return {Ent.ROLE_OWNER: 0, Ent.ROLE_MANAGER: 1, Ent.ROLE_MEMBER: 2}.get(key, 3)
|
|
|
|
def _typeOrder(key):
|
|
return {Ent.TYPE_CUSTOMER: 0, Ent.TYPE_USER: 1, Ent.TYPE_GROUP: 2, Ent.TYPE_EXTERNAL: 3}.get(key, 4)
|
|
|
|
def _statusOrder(key):
|
|
return {'ACTIVE': 0, 'SUSPENDED': 1, 'UNKNOWN': 2}.get(key, 3)
|
|
|
|
def _showGroup(groupEmail, depth):
|
|
try:
|
|
membersList = callGAPIpages(cd.members(), 'list', 'members',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
includeDerivedMembership=includeDerivedMembership,
|
|
groupKey=groupEmail, fields='nextPageToken,members(email,id,role,status,type)', maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
if showOwnedBy and not checkGroupShowOwnedBy(showOwnedBy, membersList):
|
|
return
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden, GAPI.serviceNotAvailable):
|
|
if depth == 0:
|
|
entityUnknownWarning(Ent.GROUP, groupEmail, i, count)
|
|
return
|
|
if depth == 0:
|
|
printEntity([Ent.GROUP, groupEmail], i, count)
|
|
if depth == 0 or Ent.TYPE_GROUP in typesSet:
|
|
Ind.Increment()
|
|
for member in sorted(membersList, key=lambda k: (_roleOrder(k.get('role', Ent.ROLE_MEMBER)), _typeOrder(k['type']), _statusOrder(k.get('status', '')))):
|
|
if (_checkMemberIsSuspendedIsArchived(member, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
|
|
(not checkCategory or _checkMemberCategory(member, memberDisplayOptions))):
|
|
if (member.get('role', Ent.ROLE_MEMBER) in rolesSet and
|
|
member['type'] in typesSet and
|
|
checkMemberMatch(member, memberOptions)):
|
|
memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["type"]}, {member.get("email", member["id"])}'
|
|
if checkCategory:
|
|
memberDetails += f', {member["category"]}'
|
|
memberDetails += f' , {member.get("status", "")}'
|
|
printKeyValueList([memberDetails])
|
|
if not includeDerivedMembership and (member['type'] == Ent.TYPE_GROUP) and (maxdepth == -1 or depth < maxdepth):
|
|
_showGroup(member['email'], depth+1)
|
|
if depth == 0 or Ent.TYPE_GROUP in typesSet:
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
kwargsDict = initUserGroupDomainQueryFilters()
|
|
entityList = None
|
|
showOwnedBy = {}
|
|
cdfieldsList = ['email']
|
|
rolesSet = set()
|
|
typesSet = set()
|
|
memberOptions = initMemberOptions()
|
|
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
|
|
matchPatterns = {}
|
|
maxdepth = -1
|
|
includeDerivedMembership = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getGroupFilters(myarg, kwargsDict, showOwnedBy):
|
|
pass
|
|
elif getGroupMatchPatterns(myarg, matchPatterns, False):
|
|
pass
|
|
elif myarg in {'group', 'groupns', 'groupsusp'}:
|
|
entityList = [getString(Cmd.OB_EMAIL_ADDRESS)]
|
|
if myarg == 'groupns':
|
|
memberOptions[MEMBEROPTION_ISSUSPENDED] = False
|
|
elif myarg == 'groupsusp':
|
|
memberOptions[MEMBEROPTION_ISSUSPENDED] = True
|
|
elif myarg == 'select':
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
elif myarg in SUSPENDED_ARGUMENTS:
|
|
memberOptions[MEMBEROPTION_ISSUSPENDED] = _getIsSuspended(myarg)
|
|
elif myarg in ARCHIVED_ARGUMENTS:
|
|
memberOptions[MEMBEROPTION_ISARCHIVED] = _getIsArchived(myarg)
|
|
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
pass
|
|
elif getGroupMemberTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif myarg == 'depth':
|
|
maxdepth = getInteger(minVal=-1)
|
|
elif myarg == 'includederivedmembership':
|
|
includeDerivedMembership = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not rolesSet:
|
|
rolesSet = ALL_GROUP_ROLES
|
|
if not typesSet:
|
|
typesSet = ALL_GROUP_MEMBER_TYPES
|
|
checkCategory = memberDisplayOptions['showCategory']
|
|
entityList = getGroupMembersEntityList(cd, entityList, matchPatterns, cdfieldsList, kwargsDict)
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
if isinstance(group, dict):
|
|
groupEmail = group['email']
|
|
else:
|
|
groupEmail = convertUIDtoEmailAddress(group, cd, 'group', ciGroupsAPI=True)
|
|
ci, _, groupEmail = convertGroupCloudIDToEmail(ci, groupEmail, i, count)
|
|
if checkGroupMatchPatterns(groupEmail, group, matchPatterns):
|
|
_showGroup(groupEmail, 0)
|
|
|
|
def getGroupParents(cd, groupParents, groupEmail, groupName, kwargs):
|
|
groupParents[groupEmail] = {'name': groupName, 'parents': []}
|
|
_setUserGroupArgs(groupEmail, kwargs)
|
|
try:
|
|
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', fields='nextPageToken,groups(email,name)', **kwargs)
|
|
for parentGroup in entityList:
|
|
groupParents[groupEmail]['parents'].append(parentGroup['email'])
|
|
if parentGroup['email'] not in groupParents:
|
|
getGroupParents(cd, groupParents, parentGroup['email'], parentGroup['name'], kwargs)
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
badRequestWarning(Ent.GROUP, Ent.MEMBER, groupEmail)
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
|
|
def showGroupParents(groupParents, groupEmail, role, i, count):
|
|
kvList = [groupEmail, f'{groupParents[groupEmail]["name"]}']
|
|
if role:
|
|
kvList.extend([Ent.Singular(Ent.ROLE), role])
|
|
printKeyValueListWithCount(kvList, i, count)
|
|
Ind.Increment()
|
|
for parentEmail in groupParents[groupEmail]['parents']:
|
|
showGroupParents(groupParents, parentEmail, None, 0, 0)
|
|
Ind.Decrement()
|
|
|
|
def addJsonGroupParents(groupParents, userGroup, groupEmail):
|
|
userGroup.setdefault('parents', [])
|
|
for parentEmail in groupParents[groupEmail]['parents']:
|
|
userGroup['parents'].append({'email': parentEmail, 'name': groupParents[parentEmail]['name'], 'parents': []})
|
|
addJsonGroupParents(groupParents, userGroup['parents'][-1], parentEmail)
|
|
|
|
def printGroupParents(groupParents, groupEmail, row, csvPF, delimiter, showParentsAsList):
|
|
if groupParents[groupEmail]['parents']:
|
|
for parentEmail in groupParents[groupEmail]['parents']:
|
|
row['parents'].append({'email': parentEmail, 'name': groupParents[parentEmail]['name']})
|
|
printGroupParents(groupParents, parentEmail, row, csvPF, delimiter, showParentsAsList)
|
|
del row['parents'][-1]
|
|
else:
|
|
if not showParentsAsList:
|
|
csvPF.WriteRowTitles(flattenJSON(row))
|
|
else:
|
|
crow = row.copy()
|
|
if 'Role' in row:
|
|
crow['Role'] = row['Role']
|
|
parents = crow.pop('parents')
|
|
crow['ParentsCount'] = len(parents)
|
|
crow['Parents'] = delimiter.join([parent['email'] for parent in parents])
|
|
crow['ParentsName'] = delimiter.join([parent['name'] for parent in parents])
|
|
csvPF.WriteRow(flattenJSON(crow))
|
|
|
|
# gam print grouptree <GroupEntity> [todrive <ToDriveAttribute>*]
|
|
# [showparentsaslist [<Boolean>]] [delimiter <Character>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show grouptree <GroupEntity>
|
|
# [formatjson]
|
|
def doPrintShowGroupTree():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
|
|
csvPF = CSVPrintFile(['Group', 'Name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
showParentsAsList = False
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif csvPF and myarg == 'showparentsaslist':
|
|
showParentsAsList = getBoolean()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and not FJQC.formatJSON:
|
|
if not showParentsAsList:
|
|
csvPF.SetIndexedTitles(['parents'])
|
|
else:
|
|
csvPF.AddTitles(['ParentsCount', 'Parents', 'ParentsName'])
|
|
groupParents = {}
|
|
i = 0
|
|
count = len(entityList)
|
|
if not csvPF and not FJQC.formatJSON:
|
|
performActionNumItems(count, Ent.GROUP_TREE)
|
|
for group in entityList:
|
|
i += 1
|
|
groupEmail = normalizeEmailAddressOrUID(group)
|
|
if groupEmail not in groupParents:
|
|
try:
|
|
groupName = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=groupEmail, fields='name')['name']
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.systemError) as e:
|
|
entityActionFailedWarning([Ent.GROUP, groupEmail], str(e), i, count)
|
|
continue
|
|
getGroupParents(cd, groupParents, groupEmail, groupName, kwargs)
|
|
if not FJQC.formatJSON:
|
|
if not csvPF:
|
|
showGroupParents(groupParents, groupEmail, None, i, count)
|
|
else:
|
|
row = {'Group': groupEmail, 'Name': groupParents[groupEmail]['name'], 'parents': []}
|
|
printGroupParents(groupParents, groupEmail, row, csvPF, delimiter, showParentsAsList)
|
|
else:
|
|
groupInfo = {'email': groupEmail, 'name': groupParents[groupEmail]['name'], 'parents': []}
|
|
addJsonGroupParents(groupParents, groupInfo, groupEmail)
|
|
if not csvPF:
|
|
printLine(json.dumps(cleanJSON(groupInfo), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
row = flattenJSON(groupInfo)
|
|
if csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'Group': groupEmail, 'Name': groupParents[groupEmail]['name'],
|
|
'JSON': json.dumps(cleanJSON(groupInfo),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Group Tree')
|
|
|
|
# gam create cigroup <EmailAddress>
|
|
# [copyfrom <GroupItem>] <GroupAttribute>
|
|
# [makeowner] [alias|aliases <CIGroupAliasList>]
|
|
# [security|makesecuritygroup]
|
|
# [dynamic <QueryDynamicGroup>]
|
|
def doCreateCIGroup():
|
|
doCreateGroup(ciGroupsAPI=True)
|
|
|
|
# gam update cigroups <GroupEntity> [email <EmailAddress>]
|
|
# [copyfrom <GroupItem>] <GroupAttribute>*
|
|
# [security|makesecuritygroup|dynamicsecurity|makedynamicsecuritygroup]
|
|
# [dynamic <QueryDynamicGroup>]
|
|
# [memberrestrictions <QueryMemberRestrictions>]
|
|
# [locked|unlocked]
|
|
# gam update cigroups <GroupEntity> add|create [<GroupRole>]
|
|
# [usersonly|groupsonly]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [expire|expires <Time>] [preview] [actioncsv]
|
|
# <UserTypeEntity>
|
|
# gam update cigroups <GroupEntity> delete|remove [<GroupRole>]
|
|
# [usersonly|groupsonly]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [preview] [actioncsv]
|
|
# <UserTypeEntity>
|
|
# gam update cigroups <GroupEntity> sync [<GroupRole>|ignorerole]
|
|
# [usersonly|groupsonly] [addonly|removeonly]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [expire|expires <Time>] [preview] [actioncsv]
|
|
# <UserTypeEntity>
|
|
# gam update cigroups <GroupEntity> update [<GroupRole>]
|
|
# [usersonly|groupsonly]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [expire|expires <Time>] [preview] [actioncsv]
|
|
# <UserTypeEntity>
|
|
# gam update cigroups <GroupEntity> clear [member] [manager] [owner]
|
|
# [usersonly|groupsonly]
|
|
# [emailclearpattern|emailretainpattern <REMatchPattern>]
|
|
# [preview] [actioncsv]
|
|
def doUpdateCIGroups():
|
|
|
|
def _getExpireTime(role):
|
|
if role == Ent.ROLE_MEMBER and checkArgumentPresent(['expire', 'expires']):
|
|
return getTimeOrDeltaFromNow()
|
|
return None
|
|
|
|
def _getPreviewActionCSV():
|
|
preview = checkArgumentPresent('preview')
|
|
if checkArgumentPresent('actioncsv'):
|
|
csvPF = CSVPrintFile(GROUP_PREVIEW_TITLES)
|
|
else:
|
|
csvPF = None
|
|
return (preview, csvPF)
|
|
|
|
def _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count):
|
|
roleLower = role.lower()
|
|
if roleLower in GROUP_ROLES_MAP:
|
|
return (GROUP_ROLES_MAP[roleLower], groupMemberLists[origGroup][role])
|
|
entityActionNotPerformedWarning([entityType, group, Ent.ROLE, role], Msg.INVALID_ROLE.format(','.join(sorted(GROUP_ROLES_MAP))), i, count)
|
|
return (None, None)
|
|
|
|
def _getRoleGroupMemberType(defaultRole=Ent.ROLE_MEMBER, allowIgnoreRole=False):
|
|
if not allowIgnoreRole or not checkArgumentPresent(['ignorerole']):
|
|
role = getChoice(GROUP_ROLES_MAP, defaultChoice=defaultRole, mapChoice=True)
|
|
else:
|
|
role = Ent.ROLE_ALL
|
|
groupMemberType = getChoice({'usersonly': Ent.TYPE_USER, 'groupsonly': Ent.TYPE_GROUP}, defaultChoice='ALL', mapChoice=True)
|
|
return (role, groupMemberType)
|
|
|
|
def _getMemberEmail(member):
|
|
if member['type'] == Ent.TYPE_CUSTOMER:
|
|
return member['id']
|
|
return member.get('preferredMemberKey', {}).get('id', '')
|
|
|
|
def checkDynamicGroup(ci, group, i, count):
|
|
try:
|
|
result = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=group, fields='labels')
|
|
if CIGROUP_DYNAMIC_LABEL in result.get('labels', {}):
|
|
entityActionNotPerformedWarning([entityType, group], Msg.DYNAMIC_GROUP_MEMBERSHIP_CANNOT_BE_MODIFIED, i, count)
|
|
return True
|
|
return False
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable):
|
|
return True
|
|
|
|
# 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 _previewAction(group, members, role, jcount, action):
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in members:
|
|
j += 1
|
|
entityActionPerformed([entityType, group, role, member], j, jcount)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
for member in members:
|
|
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.PerformedName(action), 'message': Act.PREVIEW})
|
|
|
|
def _showSuccess(group, member, role, expireTime, j, jcount, optMsg=None):
|
|
kvList = []
|
|
if role is not None and role != 'None':
|
|
kvList.append(f'{Ent.Singular(Ent.ROLE)}: {role}')
|
|
if expireTime:
|
|
kvList.extend(['expireTime', formatLocalTime(expireTime)])
|
|
if optMsg:
|
|
kvList.append(optMsg)
|
|
entityActionPerformedMessage([entityType, group, Ent.MEMBER, member], ', '.join(kvList), j, jcount)
|
|
if csvPF:
|
|
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.Performed(), 'message': Act.SUCCESS})
|
|
|
|
def _showFailure(group, member, role, errMsg, j, jcount):
|
|
entityActionFailedWarning([entityType, group, Ent.MEMBER, member], errMsg, j, jcount)
|
|
if csvPF:
|
|
csvPF.WriteRow({'group': group, 'email': member, 'role': role, 'action': Act.Failed(), 'message': errMsg})
|
|
|
|
def _batchAddGroupMembers(group, i, count, addMembers, role, expireTime):
|
|
Act.Set([Act.ADD, Act.ADD_PREVIEW][preview])
|
|
jcount = len(addMembers)
|
|
entityPerformActionNumItems([entityType, group], jcount, role, i, count)
|
|
if jcount == 0:
|
|
return
|
|
if preview:
|
|
_previewAction(group, addMembers, role, jcount, Act.ADD)
|
|
return
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in addMembers:
|
|
j += 1
|
|
body = {'preferredMemberKey': {'id': member}, 'roles': [{'name': Ent.ROLE_MEMBER}]}
|
|
if role != Ent.ROLE_MEMBER:
|
|
body['roles'].append({'name': role})
|
|
elif expireTime not in {None, NEVER_TIME}:
|
|
body['roles'][0]['expiryDetail'] = {'expireTime': expireTime}
|
|
try:
|
|
callGAPI(ci.groups().memberships(), 'create',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.DUPLICATE, GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND,
|
|
GAPI.INVALID_MEMBER, GAPI.CYCLIC_MEMBERSHIPS_NOT_ALLOWED,
|
|
GAPI.CONDITION_NOT_MET, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED,
|
|
GAPI.ALREADY_EXISTS, GAPI.CONFLICT],
|
|
parent=group, body=body, fields='')
|
|
_showSuccess(group, member, role, expireTime, j, jcount)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(entityType, group, i, count)
|
|
except (GAPI.duplicate, GAPI.memberNotFound, GAPI.resourceNotFound,
|
|
GAPI.invalidMember, GAPI.cyclicMembershipsNotAllowed, GAPI.conditionNotMet,
|
|
GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.alreadyExists) as e:
|
|
_showFailure(group, member, role, str(e), j, jcount)
|
|
except GAPI.conflict:
|
|
_showSuccess(group, member, role, expireTime, j, jcount, Msg.ACTION_MAY_BE_DELAYED)
|
|
Ind.Decrement()
|
|
|
|
def _batchRemoveGroupMembers(group, i, count, removeMembers, role):
|
|
Act.Set([Act.REMOVE, Act.REMOVE_PREVIEW][preview])
|
|
jcount = len(removeMembers)
|
|
entityPerformActionNumItems([entityType, group], jcount, role, i, count)
|
|
if jcount == 0:
|
|
return
|
|
if preview:
|
|
_previewAction(group, removeMembers, role, jcount, Act.REMOVE)
|
|
return
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in removeMembers:
|
|
j += 1
|
|
memberEmail = member['email']
|
|
try:
|
|
callGAPI(ci.groups().memberships(), 'delete',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
|
|
name=member['name'])
|
|
_showSuccess(group, memberEmail, role, None, j, jcount)
|
|
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
|
|
_showFailure(group, memberEmail, role, str(e), j, jcount)
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
cib = None
|
|
entityType = Ent.CLOUD_IDENTITY_GROUP
|
|
csvPF = None
|
|
getBeforeUpdate = preview = False
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
CL_subCommand = getChoice(UPDATE_GROUP_SUBCMDS, defaultChoice=None)
|
|
lockGroup = None
|
|
if not CL_subCommand:
|
|
gs_body = {}
|
|
ci_body = {}
|
|
se_body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'email':
|
|
ci_body['groupKey'] = {'id': getEmailAddress(noUid=True)}
|
|
elif myarg == 'getbeforeupdate':
|
|
getBeforeUpdate = True
|
|
elif myarg == 'dynamic':
|
|
ci_body.setdefault('dynamicGroupMetadata', {'queries': []})
|
|
ci_body['dynamicGroupMetadata']['queries'].append({'resourceType': 'USER',
|
|
'query': getString(Cmd.OB_QUERY)})
|
|
elif myarg in {'security', 'makesecuritygroup'}:
|
|
ci_body['labels'] = {CIGROUP_DISCUSSION_FORUM_LABEL: '',
|
|
CIGROUP_SECURITY_LABEL: ''}
|
|
elif myarg in {'dynamicsecurity', 'makedynamicsecuritygroup'}:
|
|
ci_body['labels'] = {CIGROUP_DISCUSSION_FORUM_LABEL: '',
|
|
CIGROUP_DYNAMIC_LABEL: '',
|
|
CIGROUP_SECURITY_LABEL: ''}
|
|
elif myarg in {'lockedsecurity', 'makelockedsecuritygroup'}:
|
|
ci_body['labels'] = {CIGROUP_DISCUSSION_FORUM_LABEL: '',
|
|
CIGROUP_LOCKED_LABEL: '',
|
|
CIGROUP_SECURITY_LABEL: ''}
|
|
elif myarg in ['memberrestriction', 'memberrestrictions']:
|
|
query = getString(Cmd.OB_QUERY, minLen=0)
|
|
member_types = {'USER': '1', 'SERVICE_ACCOUNT': '2', 'GROUP': '3',}
|
|
for key, val in iter(member_types.items()):
|
|
query = query.replace(key, val)
|
|
se_body['memberRestriction'] = {'query': query}
|
|
elif myarg == 'locked':
|
|
lockGroup = True
|
|
elif myarg == 'unlocked':
|
|
lockGroup = False
|
|
elif myarg == 'json':
|
|
gs_body.update(getJSON(GROUP_JSON_SKIP_FIELDS))
|
|
elif myarg == 'accesstype':
|
|
gs_body.update(getChoice(GROUP_ACCESS_TYPE_CHOICE_MAP, mapChoice=True))
|
|
else:
|
|
getGroupAttrValue(myarg, gs_body)
|
|
if gs_body:
|
|
gs = buildGAPIObject(API.GROUPSSETTINGS)
|
|
gs_body = getSettingsFromGroup(cd, ','.join(entityList), gs, gs_body)
|
|
for k, v in iter(GROUP_CIGROUP_FIELDS_MAP.items()):
|
|
if k in gs_body:
|
|
ci_body[v] = gs_body.pop(k)
|
|
if gs_body:
|
|
if not getBeforeUpdate:
|
|
settings = gs_body
|
|
elif not ci_body:
|
|
return
|
|
elif not ci_body and not se_body and lockGroup is None:
|
|
return
|
|
Act.Set(Act.UPDATE)
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
ci, _, group = convertGroupCloudIDToEmail(ci, group, i, count)
|
|
if gs_body and not GroupIsAbuseOrPostmaster(group):
|
|
try:
|
|
if 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 = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS, retryReasons=GAPI.GROUP_GET_RETRY_REASONS,
|
|
groupKey=group, fields='email')['email']
|
|
if getBeforeUpdate:
|
|
settings = callGAPI(gs.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
|
|
groupUniqueId=mapGroupEmailForSettings(group), fields='*')
|
|
settings.update(gs_body)
|
|
if not checkReplyToCustom(group, settings, i, count):
|
|
continue
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([entityType, group], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.backendError, GAPI.invalid, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
|
|
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
|
entityActionFailedWarning([entityType, group], str(e), i, count)
|
|
continue
|
|
if gs_body and not GroupIsAbuseOrPostmaster(group):
|
|
try:
|
|
callGAPI(gs.groups(), 'update',
|
|
bailOnInvalidError='messageModerationLevel' in settings,
|
|
throwReasons=GAPI.GROUP_SETTINGS_THROW_REASONS, retryReasons=GAPI.GROUP_SETTINGS_RETRY_REASONS,
|
|
groupUniqueId=mapGroupEmailForSettings(group), body=settings, fields='')
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([entityType, group], Msg.DOES_NOT_EXIST, i, count)
|
|
continue
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.backendError,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.invalidAttributeValue, GAPI.invalidInput, GAPI.badRequest, GAPI.permissionDenied,
|
|
GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable, GAPI.authError) as e:
|
|
entityActionFailedWarning([entityType, group], str(e), i, count)
|
|
continue
|
|
except GAPI.required:
|
|
entityActionFailedWarning([entityType, group], Msg.INVALID_JSON_SETTING, i, count)
|
|
continue
|
|
if ci_body or se_body or lockGroup is not None:
|
|
_, name, _ = convertGroupEmailToCloudID(ci, group, i, count)
|
|
if not name:
|
|
continue
|
|
cipl = ci
|
|
twoUpdates = False
|
|
if 'labels' in ci_body or lockGroup is not None:
|
|
try:
|
|
cigInfo = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS,
|
|
retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, fields='labels')
|
|
except (GAPI.notFound, GAPI.groupNotFound, GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.backendError,
|
|
GAPI.invalid, GAPI.invalidMember, GAPI.invalidParameter, GAPI.invalidInput, GAPI.forbidden, GAPI.badRequest,
|
|
GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
|
|
continue
|
|
# If a group currently isn't a security group or locked, and we want to add security and locked,
|
|
# we have to do two commands to meet a beta requirement
|
|
ci_body.setdefault('labels', {})
|
|
if ((CIGROUP_SECURITY_LABEL not in cigInfo['labels']) and
|
|
(CIGROUP_LOCKED_LABEL not in cigInfo['labels']) and
|
|
((CIGROUP_SECURITY_LABEL in ci_body['labels']) and
|
|
((CIGROUP_LOCKED_LABEL in ci_body['labels']) or lockGroup))):
|
|
twoUpdates = True
|
|
ci_body['labels'].update(cigInfo['labels'])
|
|
if lockGroup is not None:
|
|
if lockGroup:
|
|
if CIGROUP_LOCKED_LABEL not in ci_body['labels']:
|
|
ci_body['labels'][CIGROUP_LOCKED_LABEL] = ''
|
|
else:
|
|
if CIGROUP_LOCKED_LABEL in ci_body['labels']:
|
|
ci_body['labels'].pop(CIGROUP_LOCKED_LABEL)
|
|
if CIGROUP_LOCKED_LABEL in ci_body['labels']:
|
|
if cib is None:
|
|
cib = buildGAPIObject(API.CLOUDIDENTITY_GROUPS_BETA)
|
|
cipl = cib
|
|
if ci_body:
|
|
try:
|
|
if twoUpdates:
|
|
ci_body['labels'].pop(CIGROUP_LOCKED_LABEL)
|
|
callGAPI(cipl.groups(), 'patch',
|
|
throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS,
|
|
retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, body=ci_body, updateMask=','.join(list(ci_body.keys())))
|
|
ci_body['labels'][CIGROUP_LOCKED_LABEL] = ''
|
|
callGAPI(cipl.groups(), 'patch',
|
|
throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS,
|
|
retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, body=ci_body, updateMask=','.join(list(ci_body.keys())))
|
|
except (GAPI.notFound, GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
|
|
continue
|
|
if se_body:
|
|
# It seems like a bug that API requires /securitySettings appended to name.
|
|
# We'll see if Google servers change this at some point.
|
|
try:
|
|
callGAPI(ci.groups(), 'updateSecuritySettings',
|
|
throwReasons=GAPI.CIGROUP_UPDATE_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=f'{name}/securitySettings', updateMask='member_restriction.query', body=se_body)
|
|
except (GAPI.notFound, GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
|
|
continue
|
|
entityActionPerformed([entityType, group], i, count)
|
|
elif CL_subCommand in {'create', 'add'}:
|
|
baseRole, groupMemberType = _getRoleGroupMemberType()
|
|
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
|
|
expireTime = _getExpireTime(baseRole)
|
|
preview, csvPF = _getPreviewActionCSV()
|
|
_, addMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
|
|
groupMemberLists = addMembers if isinstance(addMembers, dict) else None
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
roleList = [baseRole]
|
|
if groupMemberLists:
|
|
if not subkeyRoleField:
|
|
addMembers = groupMemberLists[group]
|
|
else:
|
|
roleList = groupMemberLists[group]
|
|
origGroup = group
|
|
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
|
|
if not group or checkDynamicGroup(ci, parent, i, count):
|
|
continue
|
|
for role in roleList:
|
|
if groupMemberLists and subkeyRoleField:
|
|
role, addMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
|
|
if role is None:
|
|
continue
|
|
_batchAddGroupMembers(parent, i, count,
|
|
[convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True, ciGroupsAPI=True) for member in addMembers],
|
|
role, expireTime)
|
|
elif CL_subCommand in {'delete', 'remove'}:
|
|
baseRole, groupMemberType = _getRoleGroupMemberType()
|
|
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
|
|
preview, csvPF = _getPreviewActionCSV()
|
|
_, removeMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
|
|
groupMemberLists = removeMembers if isinstance(removeMembers, dict) else None
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
roleList = [baseRole]
|
|
if groupMemberLists:
|
|
if not subkeyRoleField:
|
|
removeMembers = groupMemberLists[group]
|
|
else:
|
|
roleList = groupMemberLists[group]
|
|
origGroup = group
|
|
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
|
|
if not group or checkDynamicGroup(ci, parent, i, count):
|
|
continue
|
|
for role in roleList:
|
|
if groupMemberLists and subkeyRoleField:
|
|
role, removeMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
|
|
if role is None:
|
|
continue
|
|
Act.Set([Act.DELETE, Act.DELETE_PREVIEW][preview])
|
|
jcount = len(removeMembers)
|
|
entityPerformActionNumItems([entityType, group], jcount, Ent.MEMBER, i, count)
|
|
if jcount == 0:
|
|
continue
|
|
if preview:
|
|
_previewAction(group, removeMembers, role or Ent.ROLE_USER, jcount, Act.DELETE)
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in removeMembers:
|
|
j += 1
|
|
memberEmail = convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True, ciGroupsAPI=True)
|
|
try:
|
|
memberName = callGAPI(ci.groups().memberships(), 'lookup',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS,
|
|
parent=parent, memberKey_id=memberEmail, fields='name').get('name')
|
|
callGAPI(ci.groups().memberships(), 'delete',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
|
|
name=memberName)
|
|
_showSuccess(group, memberEmail, role, None, j, jcount)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden,
|
|
GAPI.notFound, GAPI.memberNotFound, GAPI.permissionDenied, GAPI.invalidMember, GAPI.failedPrecondition) as e:
|
|
_showFailure(group, memberEmail, role, str(e), j, jcount)
|
|
Ind.Decrement()
|
|
elif CL_subCommand == 'sync':
|
|
baseRole, groupMemberType = _getRoleGroupMemberType(allowIgnoreRole=True)
|
|
ignoreRole = baseRole == Ent.ROLE_ALL
|
|
syncOperation = getSyncOperation()
|
|
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
|
|
expireTime = _getExpireTime(baseRole)
|
|
preview, csvPF = _getPreviewActionCSV()
|
|
_, syncMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
|
|
groupMemberLists = syncMembers if isinstance(syncMembers, dict) else None
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
syncMembersSets = {}
|
|
syncMembersMaps = {}
|
|
currentMembersSets = {}
|
|
currentMembersMaps = {}
|
|
if groupMemberLists is None:
|
|
syncMembersSets[baseRole] = set()
|
|
syncMembersMaps[baseRole] = {}
|
|
for member in syncMembers:
|
|
syncMembersSets[baseRole].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True, ciGroupsAPI=True), syncMembersMaps[baseRole]))
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
origGroup = group
|
|
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
|
|
if not group or checkDynamicGroup(ci, parent, i, count):
|
|
continue
|
|
if groupMemberLists is None:
|
|
roleList = [baseRole]
|
|
else:
|
|
if not subkeyRoleField:
|
|
roleList = [baseRole]
|
|
else:
|
|
roleList = groupMemberLists[origGroup]
|
|
for role in roleList:
|
|
role = role.upper()
|
|
syncMembersSets[role] = set()
|
|
syncMembersMaps[role] = {}
|
|
rolesSet = set()
|
|
for role in roleList:
|
|
origRole = role
|
|
role = role.upper()
|
|
if groupMemberLists is None:
|
|
rolesSet.add(role)
|
|
else:
|
|
if not subkeyRoleField:
|
|
rolesSet.add(role)
|
|
syncMembers = groupMemberLists[origGroup]
|
|
else:
|
|
role, syncMembers = _validateSubkeyRoleGetMembers(group, origRole, origGroup, groupMemberLists, i, count)
|
|
if role is None:
|
|
continue
|
|
rolesSet.add(role)
|
|
for member in syncMembers:
|
|
syncMembersSets[role].add(_cleanConsumerAddress(convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True), syncMembersMaps[role]))
|
|
if not rolesSet:
|
|
continue
|
|
memberRoles = ','.join(sorted(rolesSet))
|
|
printGettingAllEntityItemsForWhom(memberRoles, group, entityType=entityType)
|
|
try:
|
|
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
parent=parent, view='FULL',
|
|
fields='nextPageToken,memberships(name,preferredMemberKey(id),roles(name),type)',
|
|
pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, group)
|
|
continue
|
|
except (GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e))
|
|
continue
|
|
currentMembersNames = {}
|
|
for role in rolesSet:
|
|
currentMembersSets[role] = set()
|
|
currentMembersMaps[role] = {}
|
|
for member in result:
|
|
getCIGroupMemberRoleFixType(member)
|
|
role = member['role'] if not ignoreRole else Ent.ROLE_ALL
|
|
email = member.get('preferredMemberKey', {}).get('id', '')
|
|
if groupMemberType in ('ALL', member['type']) and role in rolesSet:
|
|
cleanAddress = _cleanConsumerAddress(email, currentMembersMaps[role])
|
|
currentMembersSets[role].add(cleanAddress)
|
|
currentMembersNames[cleanAddress] = member['name']
|
|
del result
|
|
if syncOperation != 'addonly':
|
|
for role in rolesSet:
|
|
_batchRemoveGroupMembers(parent, i, count,
|
|
[{'name': currentMembersNames[emailAddress],
|
|
'email': currentMembersMaps[role].get(emailAddress, emailAddress)} for emailAddress in currentMembersSets[role]-syncMembersSets[role]],
|
|
role)
|
|
if syncOperation != 'removeonly':
|
|
for role in [Ent.ROLE_OWNER, Ent.ROLE_MANAGER, Ent.ROLE_MEMBER, Ent.ROLE_ALL]:
|
|
if role in rolesSet:
|
|
_batchAddGroupMembers(parent, i, count,
|
|
[syncMembersMaps[role].get(emailAddress, emailAddress) for emailAddress in syncMembersSets[role]-currentMembersSets[role]],
|
|
role if role != Ent.ROLE_ALL else Ent.ROLE_MEMBER, expireTime)
|
|
elif CL_subCommand == 'update':
|
|
baseRole, groupMemberType = _getRoleGroupMemberType()
|
|
isSuspended, isArchived = _getOptionalIsSuspendedIsArchived()
|
|
expireTime = _getExpireTime(baseRole)
|
|
preview, csvPF = _getPreviewActionCSV()
|
|
_, updateMembers = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
isSuspended=isSuspended, isArchived=isArchived, groupMemberType=groupMemberType)
|
|
groupMemberLists = updateMembers if isinstance(updateMembers, dict) else None
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
roleList = [baseRole]
|
|
if groupMemberLists:
|
|
if not subkeyRoleField:
|
|
updateMembers = groupMemberLists[group]
|
|
else:
|
|
roleList = groupMemberLists[group]
|
|
origGroup = group
|
|
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
|
|
if not group or checkDynamicGroup(ci, parent, i, count):
|
|
continue
|
|
for role in roleList:
|
|
if groupMemberLists and subkeyRoleField:
|
|
role, updateMembers = _validateSubkeyRoleGetMembers(group, role, origGroup, groupMemberLists, i, count)
|
|
if role is None:
|
|
continue
|
|
Act.Set([Act.UPDATE, Act.UPDATE_PREVIEW][preview])
|
|
jcount = len(updateMembers)
|
|
entityPerformActionNumItems([entityType, group], jcount, Ent.MEMBER, i, count)
|
|
if jcount == 0:
|
|
continue
|
|
if preview:
|
|
_previewAction(group, updateMembers, role, jcount, Act.UPDATE)
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for member in updateMembers:
|
|
j += 1
|
|
memberEmail = convertUIDtoEmailAddress(member, cd=cd, emailTypes='any',
|
|
checkForCustomerId=True, ciGroupsAPI=True)
|
|
try:
|
|
memberName = callGAPI(ci.groups().memberships(), 'lookup',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS,
|
|
parent=parent, memberKey_id=memberEmail, fields='name').get('name')
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden,
|
|
GAPI.notFound, GAPI.memberNotFound, GAPI.permissionDenied, GAPI.invalidMember) as e:
|
|
_showFailure(group, memberEmail, role, str(e), j, jcount)
|
|
continue
|
|
preUpdateRoles = []
|
|
addRoles = []
|
|
removeRoles = []
|
|
postUpdateRoles = []
|
|
memberRoles = callGAPI(ci.groups().memberships(), 'get',
|
|
name=memberName, fields='name,preferredMemberKey,roles,type')
|
|
getCIGroupMemberRoleFixType(memberRoles)
|
|
current_roles = [crole['name'] for crole in memberRoles['roles']]
|
|
# When upgrading role, strip any expiryDetail from member before role changes
|
|
if role != Ent.ROLE_MEMBER:
|
|
if 'expireTime' in memberRoles:
|
|
preUpdateRoles.append({'fieldMask': 'expiryDetail.expireTime',
|
|
'membershipRole': {'name': Ent.ROLE_MEMBER, 'expiryDetail': {'expireTime': None}}})
|
|
# When downgrading role or simply updating member expireTime, update expiryDetail after role changes
|
|
elif expireTime:
|
|
postUpdateRoles.append({'fieldMask': 'expiryDetail.expireTime',
|
|
'membershipRole': {'name': role, 'expiryDetail': {'expireTime': expireTime if expireTime != NEVER_TIME else None}}})
|
|
for crole in current_roles:
|
|
if crole not in {Ent.ROLE_MEMBER, role}:
|
|
removeRoles.append(crole)
|
|
if role not in current_roles:
|
|
new_role = {'name': role}
|
|
if role == Ent.ROLE_MEMBER and expireTime not in {None, NEVER_TIME}:
|
|
new_role['expiryDetail'] = {'expireTime': expireTime}
|
|
postUpdateRoles = []
|
|
addRoles.append(new_role)
|
|
bodys = []
|
|
if preUpdateRoles:
|
|
bodys.append({'updateRolesParams': preUpdateRoles})
|
|
if addRoles:
|
|
bodys.append({'addRoles': addRoles})
|
|
if removeRoles:
|
|
bodys.append({'removeRoles': removeRoles})
|
|
if postUpdateRoles:
|
|
bodys.append({'updateRolesParams': postUpdateRoles})
|
|
errors = False
|
|
for body in bodys:
|
|
try:
|
|
callGAPI(ci.groups().memberships(), 'modifyMembershipRoles',
|
|
throwReasons=[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.FAILED_PRECONDITION,
|
|
GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=memberName, body=body)
|
|
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.failedPrecondition,
|
|
GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
_showFailure(group, memberEmail, role, str(e), j, jcount)
|
|
errors = True
|
|
break
|
|
if not errors:
|
|
_showSuccess(group, memberEmail, role, None, j, jcount)
|
|
Ind.Decrement()
|
|
else: #clear
|
|
rolesSet = set()
|
|
groupMemberType = 'ALL'
|
|
isSuspended = None
|
|
emailMatchPattern = None
|
|
clearMatch = True
|
|
qualifier = ''
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[myarg])
|
|
elif myarg == 'usersonly':
|
|
groupMemberType = Ent.TYPE_USER
|
|
elif myarg == 'groupsonly':
|
|
groupMemberType = Ent.TYPE_GROUP
|
|
elif myarg in {'emailclearpattern', 'emailretainpattern'}:
|
|
emailMatchPattern = getREPattern(re.IGNORECASE)
|
|
clearMatch = myarg == 'emailclearpattern'
|
|
elif myarg == 'preview':
|
|
preview = True
|
|
elif myarg == 'actioncsv':
|
|
csvPF = CSVPrintFile(GROUP_PREVIEW_TITLES)
|
|
else:
|
|
unknownArgumentExit()
|
|
Act.Set(Act.REMOVE)
|
|
if not rolesSet:
|
|
rolesSet.add(Ent.ROLE_MEMBER)
|
|
memberRoles = ','.join(sorted(rolesSet))
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
_, parent, group = checkGroupExists(cd, ci, True, group, i, count)
|
|
if not group or checkDynamicGroup(ci, parent, i, count):
|
|
continue
|
|
printGettingAllEntityItemsForWhom(memberRoles, group, qualifier=qualifier, entityType=entityType)
|
|
try:
|
|
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
parent=parent, view='FULL',
|
|
fields='nextPageToken,memberships(name,preferredMemberKey(id),roles(name),type)',
|
|
pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(entityType, group, i, count)
|
|
continue
|
|
except (GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([entityType, group], str(e), i, count)
|
|
continue
|
|
removeMembers = {}
|
|
for role in rolesSet:
|
|
removeMembers[role] = []
|
|
for member in result:
|
|
getCIGroupMemberRoleFixType(member)
|
|
memberName = member['name']
|
|
memberEmail = _getMemberEmail(member)
|
|
role = member['role']
|
|
if groupMemberType in ('ALL', member['type']) and role in rolesSet:
|
|
if emailMatchPattern is None:
|
|
removeMembers[role].append({'name': memberName, 'email': memberEmail})
|
|
elif member['type'] == Ent.TYPE_CUSTOMER:
|
|
pass
|
|
elif emailMatchPattern.match(email):
|
|
if clearMatch:
|
|
removeMembers[role].append({'name': memberName, 'email': memberEmail})
|
|
else:
|
|
if not clearMatch:
|
|
removeMembers[role].append({'name': memberName, 'email': memberEmail})
|
|
del result
|
|
for role in rolesSet:
|
|
_batchRemoveGroupMembers(group, i, count, removeMembers[role], role)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Cloud Identity Group Updates')
|
|
|
|
# gam delete cigroups <GroupEntity>
|
|
def doDeleteCIGroups():
|
|
doDeleteGroups(ciGroupsAPI=True)
|
|
|
|
CIGROUP_MEMBER_TYPES_MAP = {
|
|
'cbcmbrowser': Ent.TYPE_CBCM_BROWSER,
|
|
'chromeosdevice': Ent.TYPE_OTHER,
|
|
'customer': Ent.TYPE_CUSTOMER,
|
|
'group': Ent.TYPE_GROUP,
|
|
'other': Ent.TYPE_OTHER,
|
|
'serviceaccount': Ent.TYPE_SERVICE_ACCOUNT,
|
|
'user': Ent.TYPE_USER,
|
|
}
|
|
ALL_CIGROUP_MEMBER_TYPES = {
|
|
Ent.TYPE_CBCM_BROWSER, Ent.TYPE_CUSTOMER, Ent.TYPE_GROUP,
|
|
Ent.TYPE_OTHER, Ent.TYPE_SERVICE_ACCOUNT, Ent.TYPE_USER}
|
|
|
|
def getCIGroupMemberTypes(myarg, typesSet):
|
|
if myarg in {'type', 'types'}:
|
|
for gtype in getString(Cmd.OB_GROUP_TYPE_LIST).lower().replace('_', '').replace(',', ' ').split():
|
|
if gtype in CIGROUP_MEMBER_TYPES_MAP:
|
|
typesSet.add(CIGROUP_MEMBER_TYPES_MAP[gtype])
|
|
else:
|
|
invalidChoiceExit(gtype, CIGROUP_MEMBER_TYPES_MAP, True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
# gam info cigroups <GroupEntity>
|
|
# [nousers|membertree] [quick] [noaliases] [nojoindate] [showupdatedate]
|
|
# [nosecurity|nosecuritysettings]
|
|
# [allfields|<CIGroupFieldName>*|(fields <CIGroupFieldNameList>)]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners]
|
|
# [internal] [internaldomains <DomainList>] [external]
|
|
# [types <CIGroupMemberTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
|
|
# [formatjson]
|
|
def doInfoCIGroups():
|
|
def printCIGroupMemberTree(group_id, showRole):
|
|
if not group_id in cachedGroupMembers:
|
|
try:
|
|
cachedGroupMembers[group_id] = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS,
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
parent=group_id, view='FULL',
|
|
fields='nextPageToken,memberships(*)',
|
|
pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, group_id, i, count)
|
|
return
|
|
except (GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group_id], str(e), i, count)
|
|
return
|
|
for member in cachedGroupMembers[group_id]:
|
|
member_id = member.get('name', '')
|
|
member_id = member_id.split('/')[-1]
|
|
member_email = member.get('preferredMemberKey', {}).get('id')
|
|
member_type = member.get('type', 'USER').lower()
|
|
if showRole:
|
|
getCIGroupMemberRoleFixType(member)
|
|
printKeyValueList([member['role'].lower(), f'{member_email} ({member_type})'])
|
|
else:
|
|
writeStdout(f'{Ind.Spaces()}{member_email} ({member_type})\n')
|
|
if member_type == 'group':
|
|
Ind.Increment()
|
|
printCIGroupMemberTree(f'groups/{member_id}', False)
|
|
Ind.Decrement()
|
|
|
|
def initGroupFieldsLists():
|
|
if not groupFieldsLists['ci']:
|
|
groupFieldsLists['ci'] = ['groupKey']
|
|
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
getAliases = getSecuritySettings = getUsers = True
|
|
showJoinDate = True
|
|
showMemberTree = showUpdateDate = False
|
|
FJQC = FormatJSONQuoteChar()
|
|
members = []
|
|
groupFieldsLists = {'ci': None}
|
|
entityType = Ent.MEMBER
|
|
rolesSet = set()
|
|
typesSet = set()
|
|
memberOptions = initMemberOptions()
|
|
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
|
|
cachedGroupMembers = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'quick':
|
|
getAliases = getUsers = False
|
|
elif myarg == 'nousers':
|
|
getUsers = False
|
|
elif myarg == 'membertree':
|
|
showMemberTree = True
|
|
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
getUsers = True
|
|
elif getCIGroupMemberTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif myarg == 'noaliases':
|
|
getAliases = False
|
|
elif myarg in {'allfields', 'ciallfields', 'allcifields'}:
|
|
if not groupFieldsLists['ci']:
|
|
groupFieldsLists['ci'] = []
|
|
for field in CIGROUP_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
elif myarg in CIGROUP_FIELDS_CHOICE_MAP:
|
|
initGroupFieldsLists()
|
|
addFieldToFieldsList(myarg, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
elif myarg in {'fields', 'cifields'}:
|
|
initGroupFieldsLists()
|
|
for field in _getFieldsList():
|
|
if field in CIGROUP_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
else:
|
|
invalidChoiceExit(field, CIGROUP_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'nojoindate':
|
|
showJoinDate = False
|
|
elif myarg == 'showupdatedate':
|
|
showUpdateDate = True
|
|
elif myarg in ['nosecurity', 'nosecuritysettings']:
|
|
getSecuritySettings = False
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not typesSet:
|
|
typesSet = ALL_CIGROUP_MEMBER_TYPES
|
|
fields = getFieldsFromFieldsList(groupFieldsLists['ci'])
|
|
if not showJoinDate and not showUpdateDate:
|
|
view = 'BASIC'
|
|
pageSize = 1000
|
|
else:
|
|
view = 'FULL'
|
|
pageSize = 500
|
|
checkCategory = memberDisplayOptions['showCategory']
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
_, name, group = convertGroupEmailToCloudID(ci, group, i, count)
|
|
if not name:
|
|
continue
|
|
try:
|
|
cigInfo = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, fields=fields)
|
|
group = cigInfo['groupKey']['id']
|
|
if not getAliases:
|
|
cigInfo.pop('additionalGroupKeys', None)
|
|
if getUsers and not showMemberTree:
|
|
result = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
parent=name, view=view, fields='*', pageSize=pageSize)
|
|
members = []
|
|
for member in result:
|
|
getCIGroupMemberRoleFixType(member)
|
|
if (member['type'] in typesSet and
|
|
_checkMemberRole(member, rolesSet) and
|
|
checkCIMemberMatch(member, memberOptions) and
|
|
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
|
|
members.append(member)
|
|
if getSecuritySettings:
|
|
cigInfo['SecuritySettings'] = callGAPI(ci.groups(), 'getSecuritySettings',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=f'{name}/securitySettings', readMask='*')
|
|
if FJQC.formatJSON:
|
|
if getUsers and not showMemberTree:
|
|
cigInfo['members'] = members
|
|
printLine(json.dumps(cleanJSON(cigInfo, timeObjects=CIGROUP_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
continue
|
|
_showCIGroup(cigInfo, group, i, count)
|
|
if getUsers and not showMemberTree:
|
|
Ind.Increment()
|
|
printEntitiesCount(entityType, members)
|
|
Ind.Increment()
|
|
for member in members:
|
|
memberEmail = member.get('preferredMemberKey', {}).get('id', member['name'])
|
|
getCIGroupMemberRoleFixType(member)
|
|
memberDetails = [member['role'].lower(), f'{memberEmail} ({member["type"].lower()})']
|
|
if checkCategory:
|
|
memberDetails[1] += f' ({member["category"]})'
|
|
if showJoinDate:
|
|
memberDetails.extend(['joined', formatLocalTime(member['createTime']) if 'createTime' in member else UNKNOWN])
|
|
if showUpdateDate:
|
|
memberDetails.extend(['updated', formatLocalTime(member['updateTime']) if 'updateTime' in member else UNKNOWN])
|
|
if 'expireTime' in member:
|
|
memberDetails.extend(['expires', formatLocalTime(member['expireTime'])])
|
|
printKeyValueList(memberDetails)
|
|
Ind.Decrement()
|
|
printKeyValueList([Msg.TOTAL_ITEMS_IN_ENTITY.format(Ent.Plural(entityType), Ent.Singular(Ent.CLOUD_IDENTITY_GROUP)), len(members)])
|
|
Ind.Decrement()
|
|
elif showMemberTree:
|
|
Ind.Increment()
|
|
printEntity([Ent.MEMBERSHIP_TREE, ''])
|
|
Ind.Increment()
|
|
printCIGroupMemberTree(name, True)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.groupNotFound, GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.backendError,
|
|
GAPI.invalid, GAPI.invalidMember, GAPI.invalidParameter, GAPI.invalidInput, GAPI.forbidden, GAPI.badRequest,
|
|
GAPI.permissionDenied, GAPI.systemError, GAPI.serviceLimit, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, group], str(e), i, count)
|
|
|
|
def checkCIGroupShowOwnedBy(showOwnedBy, members):
|
|
for member in members:
|
|
if member['preferredMemberKey']['id'] == showOwnedBy:
|
|
if member['role'] == Ent.ROLE_OWNER:
|
|
return True
|
|
return False
|
|
|
|
def updateFieldsForCIGroupMatchPatterns(matchPatterns, fieldsList, csvPF=None):
|
|
for field in ['displayName', 'description']:
|
|
if field in matchPatterns:
|
|
if csvPF is not None:
|
|
csvPF.AddField(field, CIGROUP_FIELDS_CHOICE_MAP, fieldsList)
|
|
else:
|
|
fieldsList.append(field)
|
|
|
|
CIPOLICY_TIME_OBJECTS = {'createTime', 'updateTime'}
|
|
|
|
def _filterPolicies(ci, pageMessage, ifilter):
|
|
try:
|
|
policies = callGAPIpages(ci.policies(), 'list', 'policies',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
filter=ifilter,
|
|
fields='nextPageToken,policies(name,policyQuery(group,orgUnit,sortOrder),type,setting)',
|
|
pageSize=100)
|
|
# Google returns unordered results, sort them by setting type
|
|
return sorted(policies, key=lambda p: p.get('setting', {}).get('type', ''))
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.POLICY, ifilter], str(e))
|
|
return []
|
|
|
|
# Policies where GAM should offer additional guidance and information
|
|
CIPOLICY_ADDITIONAL_WARNINGS = {
|
|
'settings/drive_and_docs.external_sharing': {
|
|
'warningType': 'SUPERSEDED_POLICY',
|
|
'warningMessage': 'CAUTION: Drive Sharing settings are superseded by Drive Trust Rules if Trust Rules has been enabled for your domain. Drive Trust Rule settings are not available in the Policy API today so GAM is not able to check if Trust Rules is enabled and if the settings/drive_and_docs.external_sharing policies are actually in effect for your domain. If Drive Trust Rules is enabled for your domain then this settings/drive_and_docs.external_sharing policy does not accurately reflect your current Drive sharing settings.'
|
|
}
|
|
}
|
|
|
|
def _getPolicyAppNameFromId(httpObj, app):
|
|
app['applicationName'] = UNKNOWN
|
|
appId = app['applicationId']
|
|
url = f'https://workspace.google.com/marketplace/app/_/{appId}'
|
|
try:
|
|
resp, content = httpObj.request(url, 'GET')
|
|
except:
|
|
return
|
|
if resp.status != 200:
|
|
return
|
|
if isinstance(content, bytes):
|
|
content = content.decode()
|
|
pattern = f'https://workspace.google.com/marketplace/app/(.+?)/{appId}'
|
|
a = re.search(pattern, content)
|
|
if a:
|
|
app['applicationName'] = a.group(1)
|
|
|
|
def _cleanPolicy(policy, add_warnings, no_appnames,
|
|
groupEmailPattern, orgUnitPathPattern,
|
|
cd, groups_ci):
|
|
# convert any wordlists into spaced strings to reduce output complexity
|
|
if policy['setting']['type'] == 'settings/detector.word_list':
|
|
policy['setting']['value']['wordList'] = ' '.join(policy['setting']['value']['wordList']['words'])
|
|
# get application name for application id
|
|
if policy['setting']['type'] == 'settings/workspace_marketplace.apps_allowlist' and not no_appnames:
|
|
httpObj = getHttpObj(timeout=10)
|
|
for app in policy['setting']['value'].get('apps', []):
|
|
_getPolicyAppNameFromId(httpObj, app)
|
|
# add any warnings to applicable policies
|
|
if add_warnings and policy['setting']['type'] in CIPOLICY_ADDITIONAL_WARNINGS:
|
|
policy['warning'] = CIPOLICY_ADDITIONAL_WARNINGS[policy['setting']['type']]
|
|
if groupId := policy['policyQuery'].get('group'):
|
|
_, _, policy['policyQuery']['groupEmail'] = convertGroupCloudIDToEmail(groups_ci, groupId)
|
|
# all groups are in the root OU so the orgUnit attribute is useless
|
|
policy['policyQuery'].pop('orgUnit', None)
|
|
if groupEmailPattern is not None:
|
|
return groupEmailPattern.match(policy['policyQuery']['groupEmail'])
|
|
if orgUnitPathPattern is not None:
|
|
return False
|
|
elif orgId := policy['policyQuery'].get('orgUnit'):
|
|
policy['policyQuery']['orgUnitPath'] = convertOrgUnitIDtoPath(cd, orgId)
|
|
if orgUnitPathPattern is not None:
|
|
return orgUnitPathPattern.match(policy['policyQuery']['orgUnitPath'])
|
|
if groupEmailPattern is not None:
|
|
return False
|
|
return True
|
|
|
|
def _showPolicy(policy, FJQC, i=0, count=0):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(policy, timeObjects=CIPOLICY_TIME_OBJECTS),
|
|
ensure_ascii=False,
|
|
sort_keys=True))
|
|
return
|
|
printEntity([Ent.POLICY, policy['name']], i, count)
|
|
Ind.Increment()
|
|
policy.pop('name')
|
|
showJSON(None, policy, timeObjects=CIPOLICY_TIME_OBJECTS)
|
|
printBlankLine()
|
|
Ind.Decrement()
|
|
|
|
def _showPolicies(policies, FJQC, add_warnings, no_appnames,
|
|
groupEmailPattern, orgUnitPathPattern,
|
|
cd, groups_ci):
|
|
count = len(policies)
|
|
if groupEmailPattern is None and orgUnitPathPattern is None:
|
|
performActionNumItems(count, Ent.POLICY)
|
|
else:
|
|
performActionModifierNumItems(Msg.MAXIMUM_OF, count, Ent.POLICY)
|
|
Ind.Increment()
|
|
i = 0
|
|
for policy in policies:
|
|
i += 1
|
|
if _cleanPolicy(policy, add_warnings, no_appnames,
|
|
groupEmailPattern, orgUnitPathPattern,
|
|
cd, groups_ci):
|
|
_showPolicy(policy, FJQC, i, count)
|
|
Ind.Decrement()
|
|
|
|
def _checkPoliciesWithDASA():
|
|
if GC.Values[GC.ENABLE_DASA]:
|
|
systemErrorExit(USAGE_ERROR_RC,
|
|
Msg.COMMAND_NOT_COMPATIBLE_WITH_ENABLE_DASA.format(Act.ToPerform().lower(), Cmd.ARG_CIPOLICIES))
|
|
|
|
# gam info policies <CIPolicyNameEntity>
|
|
# [nowarnings] [noappnames]
|
|
# [formatjson]
|
|
def doInfoCIPolicies():
|
|
_checkPoliciesWithDASA()
|
|
groups_ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_POLICY)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
entityList = getEntityList(Cmd.OB_CIPOLICY_NAME_ENTITY)
|
|
FJQC = FormatJSONQuoteChar()
|
|
add_warnings = True
|
|
no_appnames = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'nowarnings':
|
|
add_warnings = False
|
|
elif myarg == 'noappnames':
|
|
no_appnames=True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i = 0
|
|
count = len(entityList)
|
|
for pname in entityList:
|
|
i += 1
|
|
if pname.startswith('policies/'):
|
|
try:
|
|
policies = [callGAPI(ci.policies(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
name=pname,
|
|
fields='name,policyQuery(group,orgUnit,sortOrder),type,setting')]
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.POLICY, pname], str(e), i, count)
|
|
continue
|
|
else:
|
|
if pname.startswith('settings/'):
|
|
pname = pname.split('/')[1]
|
|
ifilter = f"setting.type.matches('{pname}')"
|
|
printGettingAllAccountEntities(Ent.POLICY, ifilter)
|
|
policies = _filterPolicies(ci, getPageMessage(), ifilter)
|
|
_showPolicies(policies, FJQC, add_warnings, no_appnames,
|
|
None, None, cd, groups_ci)
|
|
|
|
# gam print policies [todrive <ToDriveAttribute>*]
|
|
# [filter <String>] [nowarnings] [noappnames]
|
|
# [group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show policies
|
|
# [filter <String>] [nowarnings] [noappnames]
|
|
# [group <REMatchPattern>] [ou|org|orgunit <REMatchPattern>]
|
|
# [formatjson]
|
|
def doPrintShowCIPolicies():
|
|
|
|
def _printPolicy(policy):
|
|
row = flattenJSON(policy, timeObjects=CIPOLICY_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'name': policy['name'],
|
|
'JSON': json.dumps(cleanJSON(policy, timeObjects=CIPOLICY_TIME_OBJECTS),
|
|
ensure_ascii=False,
|
|
sort_keys=True)})
|
|
|
|
_checkPoliciesWithDASA()
|
|
groups_ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_POLICY)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
ifilter = None
|
|
add_warnings = True
|
|
no_appnames = False
|
|
groupEmailPattern = orgUnitPathPattern = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'filter':
|
|
ifilter = getString(Cmd.OB_STRING)
|
|
elif myarg == 'nowarnings':
|
|
add_warnings = False
|
|
elif myarg == 'noappnames':
|
|
no_appnames = True
|
|
elif myarg == 'group':
|
|
groupEmailPattern = getREPattern(re.IGNORECASE)
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
orgUnitPathPattern = getREPattern(re.IGNORECASE)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
printGettingAllAccountEntities(Ent.POLICY, ifilter)
|
|
policies = _filterPolicies(ci, getPageMessage(), ifilter)
|
|
if not csvPF:
|
|
_showPolicies(policies, FJQC, add_warnings, no_appnames,
|
|
groupEmailPattern, orgUnitPathPattern,
|
|
cd, groups_ci)
|
|
else:
|
|
for policy in policies:
|
|
if _cleanPolicy(policy, add_warnings, no_appnames,
|
|
groupEmailPattern, orgUnitPathPattern,
|
|
cd, groups_ci):
|
|
_printPolicy(policy)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Policies')
|
|
|
|
PRINT_CIGROUPS_JSON_TITLES = ['email', 'JSON']
|
|
|
|
# gam print cigroups [todrive <ToDriveAttribute>*]
|
|
# [(cimember|ciowner <UserItem>)|(select <GroupEntity>)|(query <String>)]
|
|
# [showownedby <UserItem>]
|
|
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
|
|
# [descriptionmatchpattern [not] <REMatchPattern>]
|
|
# [basic|allfields|(<CIGroupFieldName>* [fields <CIGroupFieldNameList>])]
|
|
# [roles <GroupRoleList>] [memberrestrictions]
|
|
# [members|memberscount] [managers|managerscount] [owners|ownerscount] [totalcount] [countsonly]
|
|
# [internal] [internaldomains <DomainList>] [external]
|
|
# [types <CIGroupMemberTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
|
|
# [convertcrnl] [delimiter <Character>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [showitemcountonly]
|
|
def doPrintCIGroups():
|
|
def _printGroupRow(groupEntity, groupMembers):
|
|
nonlocal itemCount
|
|
for member in groupMembers:
|
|
getCIGroupMemberRoleFixType(member)
|
|
if showOwnedBy and not checkCIGroupShowOwnedBy(showOwnedBy, groupMembers):
|
|
return
|
|
if showItemCountOnly:
|
|
itemCount += 1
|
|
return
|
|
if not keepName:
|
|
groupEntity.pop('name', None)
|
|
row = {}
|
|
if FJQC.formatJSON:
|
|
row['email'] = groupEntity['groupKey']['id'].lower()
|
|
row['JSON'] = json.dumps(cleanJSON(groupEntity, timeObjects=CIGROUP_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)
|
|
if rolesSet and groupMembers is not None:
|
|
row['JSON-members'] = json.dumps(groupMembers, ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
return
|
|
mapCIGroupFieldNames(groupEntity)
|
|
for k, v in iter(groupEntity.pop('labels', {}).items()):
|
|
if v == '':
|
|
groupEntity[f'labels{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = True
|
|
else:
|
|
groupEntity[f'labels{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'] = v
|
|
for key, value in sorted(iter(flattenJSON(groupEntity, flattened={}, timeObjects=CIGROUP_TIME_OBJECTS).items())):
|
|
csvPF.AddTitles(key)
|
|
row[key] = value
|
|
if rolesSet and groupMembers is not None:
|
|
addMemberInfoToRow(row, groupMembers, typesSet, memberOptions, memberDisplayOptions, delimiter,
|
|
False, False, True)
|
|
csvPF.WriteRow(row)
|
|
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
setTrueCustomerId()
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
memberRestrictions = sortHeaders = False
|
|
memberDisplayOptions = initPGGroupMemberDisplayOptions()
|
|
pageSize = 500
|
|
parent = f'customers/{GC.Values[GC.CUSTOMER_ID]}'
|
|
groupFieldsLists = {'ci': ['groupKey']}
|
|
csvPF = CSVPrintFile(['email'])
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
rolesSet = set()
|
|
typesSet = set()
|
|
memberOptions = initMemberOptions()
|
|
entitySelection = groupMembers = memberQuery = query = showOwnedBy = None
|
|
matchPatterns = {}
|
|
showItemCountOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'showownedby':
|
|
showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user'])
|
|
elif myarg in {'cimember', 'enterprisemember', 'ciowner'}:
|
|
emailAddress = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user', 'group'])
|
|
memberQuery = f"member_key_id == '{emailAddress}' && CIGROUP_DISCUSSION_FORUM_LABEL in labels"
|
|
entitySelection = None
|
|
if myarg == 'ciowner':
|
|
showOwnedBy = emailAddress
|
|
elif myarg == 'query':
|
|
query = getString(Cmd.OB_QUERY)
|
|
entitySelection = None
|
|
elif getGroupMatchPatterns(myarg, matchPatterns, True):
|
|
pass
|
|
elif myarg == 'select':
|
|
entitySelection = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
query = None
|
|
elif myarg == 'maxresults':
|
|
pageSize = getInteger(minVal=1, maxVal=500)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg in {'allfields', 'ciallfields', 'allcifields'}:
|
|
sortHeaders = True
|
|
groupFieldsLists = {'ci': []}
|
|
for field in CIGROUP_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
elif myarg == 'basic':
|
|
sortHeaders = True
|
|
groupFieldsLists = {'ci': ['*']}
|
|
elif myarg == 'sortheaders':
|
|
sortHeaders = getBoolean()
|
|
elif myarg in CIGROUP_FIELDS_CHOICE_MAP:
|
|
csvPF.AddField(myarg, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
elif myarg in {'fields', 'cifields'}:
|
|
for field in _getFieldsList():
|
|
if field in CIGROUP_FIELDS_CHOICE_MAP:
|
|
csvPF.AddField(field, CIGROUP_FIELDS_CHOICE_MAP, groupFieldsLists['ci'])
|
|
else:
|
|
invalidChoiceExit(field, list(CIGROUP_FIELDS_CHOICE_MAP), True)
|
|
elif getPGGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
pass
|
|
elif getCIGroupMemberTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif myarg == 'memberrestrictions':
|
|
memberRestrictions = True
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if not typesSet:
|
|
typesSet = ALL_CIGROUP_MEMBER_TYPES
|
|
csvPF.MapTitles('name', 'id')
|
|
csvPF.MapTitles('displayName', 'name')
|
|
csvPF.RemoveTitles('labels')
|
|
updateFieldsForCIGroupMatchPatterns(matchPatterns, groupFieldsLists['ci'], csvPF)
|
|
if groupFieldsLists['ci'] and groupFieldsLists['ci'][0] != '*':
|
|
keepName = 'name' in groupFieldsLists['ci']
|
|
groupFieldsLists['ci'].append('name')
|
|
fields = ','.join(set(groupFieldsLists['ci']))
|
|
else:
|
|
keepName = True
|
|
fields = '*'
|
|
fieldsnp = f'nextPageToken,groups({fields})'
|
|
getRolesSet = rolesSet.copy()
|
|
if showOwnedBy:
|
|
getRolesSet.add(Ent.ROLE_OWNER)
|
|
getRoles = ','.join(sorted(getRolesSet))
|
|
if rolesSet:
|
|
setMemberDisplayTitles(memberDisplayOptions, csvPF)
|
|
if FJQC.formatJSON:
|
|
sortHeaders = False
|
|
csvPF.SetJSONTitles(PRINT_CIGROUPS_JSON_TITLES)
|
|
if rolesSet:
|
|
csvPF.AddJSONTitle('JSON-members')
|
|
if memberQuery:
|
|
printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, memberQuery)
|
|
try:
|
|
result = callGAPIpages(ci.groups().memberships(), 'searchTransitiveGroups', 'memberships',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'],
|
|
throwReasons=GAPI.CIGROUP_LIST_USERKEY_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
parent='groups/-', query=memberQuery,
|
|
fields='nextPageToken,memberships(group,groupKey(id),relationType)', pageSize=pageSize)
|
|
entitySelection = [{'email': entity['groupKey']['id'], 'name': entity['group']} for entity in result if entity['relationType'] == 'DIRECT']
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, None], str(e))
|
|
return
|
|
getFullFieldsList = []
|
|
if entitySelection is None:
|
|
if groupFieldsLists['ci']:
|
|
for field in groupFieldsLists['ci']:
|
|
if field in CIGROUP_FULL_FIELDS:
|
|
getFullFieldsList.append(field)
|
|
else:
|
|
getFullFieldsList = list(CIGROUP_FULL_FIELDS)
|
|
getFullFields = ','.join(getFullFieldsList)#
|
|
cipl = ci
|
|
if query:
|
|
method = 'search'
|
|
if 'parent' not in query:
|
|
query += f" && parent == '{parent}'"
|
|
kwargs = {'query': query}
|
|
if CIGROUP_LOCKED_LABEL in query:
|
|
cipl = buildGAPIObject(API.CLOUDIDENTITY_GROUPS_BETA)
|
|
else:
|
|
method = 'list'
|
|
kwargs = {'parent': parent}
|
|
printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, query)
|
|
try:
|
|
entityList = callGAPIpages(cipl.groups(), method, 'groups',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'],
|
|
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
view='FULL', fields=fieldsnp, pageSize=pageSize, **kwargs)
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, None], str(e))
|
|
return
|
|
else:
|
|
getFullFields = ''
|
|
entityList = []
|
|
i = 0
|
|
count = len(entitySelection)
|
|
for group in entitySelection:
|
|
i += 1
|
|
if isinstance(group, dict):
|
|
name = group['name']
|
|
groupEmail = group['email']
|
|
else:
|
|
_, name, groupEmail = convertGroupEmailToCloudID(ci, group, i, count)
|
|
printGettingEntityItemForWhom(Ent.CLOUD_IDENTITY_GROUP, groupEmail, i, count)
|
|
if name:
|
|
try:
|
|
ciGroup = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, fields=fields)
|
|
entityList.append(ciGroup)
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
|
|
itemCount = 0
|
|
i = 0
|
|
count = len(entityList)
|
|
for groupEntity in entityList:
|
|
i += 1
|
|
groupEmail = groupEntity['groupKey']['id'].lower()
|
|
if not checkGroupMatchPatterns(groupEmail, groupEntity, matchPatterns):
|
|
continue
|
|
if getFullFields:
|
|
try:
|
|
fullInfo = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=groupEntity['name'], fields=getFullFields)
|
|
groupEntity.update(fullInfo)
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
|
|
groupMembers = {}
|
|
if getRoles:
|
|
printGettingEntityItemForWhom(getRoles, groupEmail, i, count)
|
|
try:
|
|
groupMembers = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
pageMessage=getPageMessage(), messageAttribute=['preferredMemberKey', 'id'],
|
|
parent=groupEntity['name'], view='FULL', fields='*', pageSize=pageSize)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupEmail, i, count)
|
|
except (GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
|
|
if memberRestrictions:
|
|
printGettingEntityItemForWhom(Ent.MEMBER_RESTRICTION, groupEmail, i, count)
|
|
try:
|
|
secInfo = callGAPI(ci.groups(), 'getSecuritySettings',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=f"{groupEntity['name']}/securitySettings", readMask='*')
|
|
if 'memberRestriction' in secInfo:
|
|
groupEntity['memberRestrictionQuery'] = secInfo['memberRestriction'].get('query', '')
|
|
groupEntity['memberRestrictionEvaluation'] = secInfo['memberRestriction'].get('evaluation', {}).get('state', '')
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
|
|
_printGroupRow(groupEntity, groupMembers)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{itemCount}\n')
|
|
return
|
|
if sortHeaders:
|
|
sortTitles = ['email']+CIGROUP_PRINT_ORDER
|
|
if rolesSet:
|
|
setMemberDisplaySortTitles(memberDisplayOptions, sortTitles)
|
|
csvPF.SetSortTitles(sortTitles)
|
|
csvPF.SortRows('email', False)
|
|
csvPF.writeCSVfile('Cloud Identity Groups')
|
|
|
|
# gam <UserTypeEntity> info cimember <GroupEntity>
|
|
def infoCIGroupMembers(entityList):
|
|
infoGroupMembers(entityList, True)
|
|
|
|
# gam info cimember <UserTypeEntity> <GroupEntity>
|
|
def doInfoCIGroupMembers():
|
|
infoGroupMembers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1], True)
|
|
|
|
def getCIGroupMembersEntityList(ci, entityList, query, subTitle, matchPatterns, fieldsList, csvPF):
|
|
if query:
|
|
printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, query)
|
|
parent = 'groups/-'
|
|
try:
|
|
result = callGAPIpages(ci.groups().memberships(), 'searchTransitiveGroups', 'memberships',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'],
|
|
throwReasons=GAPI.CIGROUP_LIST_USERKEY_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
parent=parent, query=query,
|
|
fields='nextPageToken,memberships(groupKey(id),relationType)', pageSize=500)
|
|
entityList = [entity['groupKey']['id'] for entity in result if entity['relationType'] == 'DIRECT']
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedExit([Ent.CLOUD_IDENTITY_GROUP, parent], str(e))
|
|
elif entityList is None:
|
|
updateFieldsForCIGroupMatchPatterns(matchPatterns, fieldsList, csvPF)
|
|
printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, subTitle)
|
|
parent = f'customers/{GC.Values[GC.CUSTOMER_ID]}'
|
|
try:
|
|
entityList = callGAPIpages(ci.groups(), 'list', 'groups',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute=['groupKey', 'id'],
|
|
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
parent=parent, view='FULL',
|
|
fields=f'nextPageToken,groups({",".join(set(fieldsList))})', pageSize=500)
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedExit([Ent.CLOUD_IDENTITY_GROUP, parent], str(e))
|
|
else:
|
|
clearUnneededGroupMatchPatterns(matchPatterns)
|
|
return entityList
|
|
|
|
def getCIGroupTransitiveMembers(ci, groupName, membersList, i, count):
|
|
try:
|
|
groupMembers = callGAPIpages(ci.groups().memberships(), 'searchTransitiveMemberships', 'memberships',
|
|
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
parent=groupName,
|
|
fields='nextPageToken,memberships(*)', pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, GAPI.systemError, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupName, i, count)
|
|
return False
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedExit([Ent.CLOUD_IDENTITY_GROUP, groupName], str(e))
|
|
return False
|
|
for member in groupMembers:
|
|
membersList.append(getCIGroupTransitiveMemberRoleFixType(groupName, member))
|
|
return True
|
|
|
|
def getCIGroupMembers(ci, groupName, memberRoles, membersList, membersSet, i, count,
|
|
memberOptions, memberDisplayOptions, level, typesSet, groupEmail, kwargs):
|
|
nameToPrint = groupEmail if groupEmail else groupName
|
|
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, nameToPrint, i, count)
|
|
validRoles = _getCIRoleVerification(memberRoles)
|
|
if memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP]:
|
|
groupMembers = []
|
|
if not getCIGroupTransitiveMembers(ci, groupName, groupMembers, i, count):
|
|
return
|
|
for member in groupMembers:
|
|
if _checkMemberRole(member, validRoles):
|
|
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
|
|
membersList.append(member)
|
|
return
|
|
try:
|
|
groupMembers = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
parent=groupName, **kwargs)
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, GAPI.systemError,
|
|
GAPI.permissionDenied, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, nameToPrint, i, count)
|
|
return
|
|
checkCategory = memberDisplayOptions['showCategory']
|
|
if not memberOptions[MEMBEROPTION_RECURSIVE]:
|
|
if memberOptions[MEMBEROPTION_NODUPLICATES]:
|
|
for member in groupMembers:
|
|
getCIGroupMemberRoleFixType(member)
|
|
memberName = member.get('preferredMemberKey', {}).get('id', '')
|
|
if (_checkMemberRole(member, validRoles) and
|
|
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions)) and
|
|
memberName not in membersSet):
|
|
membersSet.add(memberName)
|
|
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
|
|
membersList.append(member)
|
|
else:
|
|
for member in groupMembers:
|
|
getCIGroupMemberRoleFixType(member)
|
|
if (_checkMemberRole(member, validRoles) and
|
|
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
|
|
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
|
|
membersList.append(member)
|
|
elif memberOptions[MEMBEROPTION_NODUPLICATES]:
|
|
groupMemberList = []
|
|
for member in groupMembers:
|
|
getCIGroupMemberRoleFixType(member)
|
|
memberName = member.get('preferredMemberKey', {}).get('id', '')
|
|
if member['type'] != Ent.TYPE_GROUP:
|
|
if (member['type'] in typesSet and
|
|
checkCIMemberMatch(member, memberOptions) and
|
|
_checkMemberRole(member, validRoles) and
|
|
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions)) and
|
|
memberName not in membersSet):
|
|
membersSet.add(memberName)
|
|
member['level'] = level
|
|
member['subgroup'] = nameToPrint
|
|
membersList.append(member)
|
|
else:
|
|
if memberName not in membersSet:
|
|
membersSet.add(memberName)
|
|
if (member['type'] in typesSet and
|
|
checkCIMemberMatch(member, memberOptions) and
|
|
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
|
|
member['level'] = level
|
|
member['subgroup'] = nameToPrint
|
|
membersList.append(member)
|
|
_, gname = member['name'].rsplit('/', 1)
|
|
groupMemberList.append((f'groups/{gname}', memberName))
|
|
for member in groupMemberList:
|
|
getCIGroupMembers(ci, member[0], memberRoles, membersList, membersSet, i, count,
|
|
memberOptions, memberDisplayOptions, level+1, typesSet, member[1], kwargs)
|
|
else:
|
|
for member in groupMembers:
|
|
getCIGroupMemberRoleFixType(member)
|
|
memberName = member.get('preferredMemberKey', {}).get('id', '')
|
|
if member['type'] != Ent.TYPE_GROUP:
|
|
if (member['type'] in typesSet and
|
|
checkCIMemberMatch(member, memberOptions) and
|
|
_checkMemberRole(member, validRoles) and
|
|
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
|
|
member['level'] = level
|
|
member['subgroup'] = nameToPrint
|
|
membersList.append(member)
|
|
else:
|
|
if (member['type'] in typesSet and
|
|
checkCIMemberMatch(member, memberOptions) and
|
|
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
|
|
member['level'] = level
|
|
member['subgroup'] = nameToPrint
|
|
membersList.append(member)
|
|
_, gname = member['name'].rsplit('/', 1)
|
|
getCIGroupMembers(ci, f'groups/{gname}', memberRoles, membersList, membersSet, i, count,
|
|
memberOptions, memberDisplayOptions, level+1, typesSet, memberName, kwargs)
|
|
|
|
CIGROUPMEMBERS_FIELDS_CHOICE_MAP = {
|
|
'createtime': 'createTime',
|
|
'delivery': 'deliverySetting',
|
|
'deliverysettings': 'deliverySetting',
|
|
'email': 'preferredMemberKey',
|
|
'expiretime': 'expireTime',
|
|
'id': 'name',
|
|
'memberkey': 'preferredMemberKey',
|
|
'name': 'name',
|
|
'preferredmemberkey': 'preferredMemberKey',
|
|
'role': 'roles',
|
|
'roles': 'roles',
|
|
'type': 'type',
|
|
'updatetime': 'updateTime',
|
|
'useremail': 'preferredMemberKey',
|
|
}
|
|
CIGROUPMEMBERS_DEFAULT_FIELDS = [
|
|
'type', 'roles', 'name', 'preferredmemberkey', 'createtime', 'updatetime', 'expiretime']
|
|
CIGROUPMEMBERS_SORT_FIELDS = [
|
|
'type', 'role', 'id', 'email',
|
|
'name', 'preferredMemberKey.id', 'preferredMemberKey.namespace',
|
|
'createTime', 'updateTime', 'expireTime'
|
|
]
|
|
CIGROUPMEMBERS_TIME_OBJECTS = {'createTime', 'updateTime', 'expireTime'}
|
|
|
|
def _getCIListGroupMembersArgs(listView):
|
|
if listView == 'full':
|
|
return {'view': 'FULL', 'pageSize': GC.Values[GC.MEMBER_MAX_RESULTS_CI_FULL],
|
|
'fields': 'nextPageToken,memberships(*)'}
|
|
if listView == 'basic':
|
|
return {'view': 'FULL', 'pageSize': GC.Values[GC.MEMBER_MAX_RESULTS_CI_FULL],
|
|
'fields': 'nextPageToken,memberships(name,preferredMemberKey,roles,type)'}
|
|
return {'view': 'BASIC', 'pageSize': GC.Values[GC.MEMBER_MAX_RESULTS_CI_BASIC],
|
|
'fields': 'nextPageToken,memberships(*)'}
|
|
|
|
# gam print cigroup-members [todrive <ToDriveAttribute>*]
|
|
# [(cimember|ciowner <UserItem>)|(cigroup <GroupItem>)|(select <GroupEntity>)]
|
|
# [showownedby <UserItem>]
|
|
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
|
|
# [descriptionmatchpattern [not] <REMatchPattern>]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners]
|
|
# [types <CIGroupMemberTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
|
|
# <CIGroupMembersFieldName>* [fields <CIGroupMembersFieldNameList>]
|
|
# [minimal|basic|full]
|
|
# [(recursive [noduplicates])|includederivedmembership] [nogroupeemail]
|
|
# [formatjson [quotechar <Character>]]
|
|
def doPrintCIGroupMembers():
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
setTrueCustomerId()
|
|
memberOptions = initMemberOptions()
|
|
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
|
|
groupColumn = True
|
|
subTitle = f'{Msg.ALL} {Ent.Plural(Ent.CLOUD_IDENTITY_GROUP)}'
|
|
fieldsList = []
|
|
groupFieldsLists = {'ci': ['groupKey', 'name']}
|
|
csvPF = CSVPrintFile(['group'])
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
entityList = query = showOwnedBy = None
|
|
rolesSet = set()
|
|
typesSet = set()
|
|
matchPatterns = {}
|
|
listView = 'full'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'showownedby':
|
|
showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user'])
|
|
elif myarg in {'cimember', 'enterprisemember', 'ciowner'}:
|
|
emailAddress = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user', 'group'])
|
|
query = f"member_key_id == '{emailAddress}' && CIGROUP_DISCUSSION_FORUM_LABEL in labels"
|
|
entityList = None
|
|
if myarg == 'ciowner':
|
|
showOwnedBy = emailAddress
|
|
elif myarg in {'cigroup', 'group'}:
|
|
entityList = [getString(Cmd.OB_EMAIL_ADDRESS)]
|
|
subTitle = f'{Ent.Singular(Ent.CLOUD_IDENTITY_GROUP)}={entityList[0]}'
|
|
query = None
|
|
elif getGroupMatchPatterns(myarg, matchPatterns, True):
|
|
pass
|
|
elif myarg == 'select':
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
subTitle = f'{Msg.SELECTED} {Ent.Plural(Ent.CLOUD_IDENTITY_GROUP)}'
|
|
query = None
|
|
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
pass
|
|
elif getCIGroupMemberTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif getFieldsList(myarg, CIGROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList, initialField='preferredMemberKey'):
|
|
pass
|
|
elif myarg == 'noduplicates':
|
|
memberOptions[MEMBEROPTION_NODUPLICATES] = True
|
|
elif myarg == 'recursive':
|
|
memberOptions[MEMBEROPTION_RECURSIVE] = True
|
|
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = False
|
|
elif myarg == 'includederivedmembership':
|
|
memberOptions[MEMBEROPTION_INCLUDEDERIVEDMEMBERSHIP] = True
|
|
memberOptions[MEMBEROPTION_RECURSIVE] = False
|
|
elif myarg == 'nogroupemail':
|
|
groupColumn = False
|
|
elif myarg in {'minimal', 'basic', 'full'}:
|
|
listView = myarg
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if listView == 'minimal' and memberOptions[MEMBEROPTION_RECURSIVE]:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('minimal', 'recursive'))
|
|
if not typesSet:
|
|
typesSet = {Ent.TYPE_USER} if memberOptions[MEMBEROPTION_RECURSIVE] else ALL_CIGROUP_MEMBER_TYPES
|
|
fields = ','.join(set(groupFieldsLists['ci']))
|
|
entityList = getCIGroupMembersEntityList(ci, entityList, query, subTitle, matchPatterns, groupFieldsLists['ci'], csvPF)
|
|
if not fieldsList:
|
|
for field in CIGROUPMEMBERS_DEFAULT_FIELDS:
|
|
addFieldToFieldsList(field, CIGROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList)
|
|
if not groupColumn:
|
|
csvPF.RemoveTitles(['group'])
|
|
if memberDisplayOptions['showCategory']:
|
|
csvPF.AddTitles('category')
|
|
if FJQC.formatJSON:
|
|
if groupColumn:
|
|
csvPF.SetJSONTitles(['group', 'JSON'])
|
|
else:
|
|
csvPF.SetJSONTitles(['JSON'])
|
|
displayFieldsList = fieldsList[:]
|
|
if 'roles' in displayFieldsList:
|
|
displayFieldsList.remove('roles')
|
|
displayFieldsList.append('role')
|
|
if not rolesSet:
|
|
rolesSet = ALL_GROUP_ROLES
|
|
getRolesSet = rolesSet.copy()
|
|
if showOwnedBy:
|
|
getRolesSet.add(Ent.ROLE_OWNER)
|
|
getRoles = ','.join(sorted(getRolesSet))
|
|
kwargs = _getCIListGroupMembersArgs(listView)
|
|
level = 0
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
if isinstance(group, dict):
|
|
groupEntity = group
|
|
else:
|
|
_, name, groupEmail = convertGroupEmailToCloudID(ci, group, i, count)
|
|
if not name:
|
|
continue
|
|
try:
|
|
groupEntity = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, fields=fields)
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError,
|
|
GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
|
|
continue
|
|
groupEmail = groupEntity['groupKey']['id'].lower()
|
|
if not checkGroupMatchPatterns(groupEmail, groupEntity, matchPatterns):
|
|
continue
|
|
membersList = []
|
|
membersSet = set()
|
|
getCIGroupMembers(ci, groupEntity['name'], getRoles, membersList, membersSet, i, count,
|
|
memberOptions, memberDisplayOptions, level, typesSet, groupEmail, kwargs)
|
|
if showOwnedBy and not checkCIGroupShowOwnedBy(showOwnedBy, membersList):
|
|
continue
|
|
for member in membersList:
|
|
if member['role'] not in rolesSet:
|
|
continue
|
|
row = {}
|
|
if groupColumn:
|
|
row['group'] = groupEmail
|
|
dmember = {}
|
|
for field in displayFieldsList:
|
|
if field in member:
|
|
dmember[field] = member[field]
|
|
if memberOptions[MEMBEROPTION_RECURSIVE]:
|
|
row['level'] = member['level']
|
|
row['subgroup'] = member['subgroup']
|
|
if memberDisplayOptions['showCategory']:
|
|
row['category'] = member['category']
|
|
if listView == 'minimal':
|
|
dmember.pop('type', None)
|
|
mapCIGroupMemberFieldNames(dmember)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(flattenJSON(dmember, flattened=row, timeObjects=CIGROUPMEMBERS_TIME_OBJECTS))
|
|
else:
|
|
row.update(dmember)
|
|
fjrow = {}
|
|
if groupColumn:
|
|
fjrow['group'] = groupEmail
|
|
fjrow['JSON'] = json.dumps(cleanJSON(row, timeObjects=CIGROUPMEMBERS_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(fjrow)
|
|
if not FJQC.formatJSON:
|
|
sortTitles = ['group'] if groupColumn else []
|
|
sortTitles.extend(CIGROUPMEMBERS_SORT_FIELDS)
|
|
if memberDisplayOptions['showCategory']:
|
|
emailIndex = sortTitles.index('email')
|
|
sortTitles.insert(emailIndex+1, 'category')
|
|
csvPF.SetSortTitles(sortTitles)
|
|
csvPF.SortTitles()
|
|
csvPF.SetSortTitles([])
|
|
if memberOptions[MEMBEROPTION_RECURSIVE]:
|
|
csvPF.MoveTitlesToEnd(['level', 'subgroup'])
|
|
csvPF.writeCSVfile(f'Cloud Identity Group Members ({subTitle})')
|
|
|
|
# gam show cigroup-members
|
|
# [(cimember|ciowner <UserItem>)|(cigroup <GroupItem>)|(select <GroupEntity>)]
|
|
# [showownedby <UserItem>]
|
|
# [emailmatchpattern [not] <REMatchPattern>] [namematchpattern [not] <REMatchPattern>]
|
|
# [descriptionmatchpattern [not] <REMatchPattern>]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners]
|
|
# [internal] [internaldomains <DomainList>] [external]
|
|
# [types <CIGroupMemberTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <REMatchPattern>]
|
|
# [minimal|basic|full]
|
|
# [(depth <Number>) | includederivedmembership]
|
|
def doShowCIGroupMembers():
|
|
def _roleOrder(key):
|
|
return {Ent.ROLE_OWNER: 0, Ent.ROLE_MANAGER: 1, Ent.ROLE_MEMBER: 2}.get(key, 3)
|
|
|
|
def _typeOrder(key):
|
|
return {Ent.TYPE_CUSTOMER: 0, Ent.TYPE_USER: 1, Ent.TYPE_GROUP: 2,
|
|
Ent.TYPE_CBCM_BROWSER: 3, Ent.TYPE_OTHER: 4, Ent.TYPE_EXTERNAL: 5}.get(key, 6)
|
|
|
|
def _showGroup(groupName, groupEmail, depth):
|
|
if includeDerivedMembership:
|
|
membersList = []
|
|
if not getCIGroupTransitiveMembers(ci, groupName, membersList, i, count):
|
|
return
|
|
else:
|
|
try:
|
|
membersList = callGAPIpages(ci.groups().memberships(), 'list', 'memberships',
|
|
throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
parent=groupName, **kwargs)
|
|
for member in membersList:
|
|
getCIGroupMemberRoleFixType(member)
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, GAPI.systemError,
|
|
GAPI.permissionDenied, GAPI.serviceNotAvailable):
|
|
if depth == 0:
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupEmail, i, count)
|
|
return
|
|
if showOwnedBy and not checkCIGroupShowOwnedBy(showOwnedBy, membersList):
|
|
return
|
|
if depth == 0:
|
|
printEntity([Ent.CLOUD_IDENTITY_GROUP, groupEmail], i, count)
|
|
if depth == 0 or Ent.TYPE_GROUP in typesSet:
|
|
Ind.Increment()
|
|
for member in sorted(membersList, key=lambda k: (_roleOrder(k.get('role', Ent.ROLE_MEMBER)), _typeOrder(k['type']))):
|
|
if (_checkMemberIsSuspendedIsArchived(member, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) and
|
|
(not checkCategory or _checkCIMemberCategory(member, memberDisplayOptions))):
|
|
if (_checkMemberRole(member, rolesSet) and
|
|
member['type'] in typesSet and
|
|
checkCIMemberMatch(member, memberOptions)):
|
|
if listView != 'minimal':
|
|
memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["type"]}, {member["preferredMemberKey"]["id"]}'
|
|
else:
|
|
memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["preferredMemberKey"]["id"]}'
|
|
if checkCategory:
|
|
memberDetails += f', {member["category"]}'
|
|
for field in ['createTime', 'updateTime', 'expireTime']:
|
|
if field in member:
|
|
memberDetails += f', {formatLocalTime(member[field])}'
|
|
printKeyValueList([memberDetails])
|
|
if not includeDerivedMembership and (member['type'] == Ent.TYPE_GROUP) and (maxdepth == -1 or depth < maxdepth):
|
|
_, gname = member['name'].rsplit('/', 1)
|
|
_showGroup(f'groups/{gname}', member['preferredMemberKey']['id'], depth+1)
|
|
if depth == 0 or Ent.TYPE_GROUP in typesSet:
|
|
Ind.Decrement()
|
|
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS)
|
|
setTrueCustomerId()
|
|
subTitle = f'{Msg.ALL} {Ent.Plural(Ent.CLOUD_IDENTITY_GROUP)}'
|
|
groupFieldsLists = {'ci': ['groupKey', 'name']}
|
|
entityList = query = showOwnedBy = None
|
|
rolesSet = set()
|
|
typesSet = set()
|
|
memberOptions = initMemberOptions()
|
|
memberDisplayOptions = initIPSGMGroupMemberDisplayOptions()
|
|
matchPatterns = {}
|
|
maxdepth = -1
|
|
includeDerivedMembership = False
|
|
listView = 'full'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'showownedby':
|
|
showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user'])
|
|
elif myarg in {'cimember', 'enterprisemember', 'ciowner'}:
|
|
emailAddress = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user', 'group'])
|
|
query = f"member_key_id == '{emailAddress}' && CIGROUP_DISCUSSION_FORUM_LABEL in labels"
|
|
entityList = None
|
|
if myarg == 'ciowner':
|
|
showOwnedBy = emailAddress
|
|
elif myarg in {'cigroup', 'group'}:
|
|
entityList = [getString(Cmd.OB_EMAIL_ADDRESS)]
|
|
subTitle = f'{Ent.Singular(Ent.CLOUD_IDENTITY_GROUP)}={entityList[0]}'
|
|
query = None
|
|
elif getGroupMatchPatterns(myarg, matchPatterns, False):
|
|
pass
|
|
elif myarg == 'select':
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
subTitle = f'{Msg.SELECTED} {Ent.Plural(Ent.CLOUD_IDENTITY_GROUP)}'
|
|
query = None
|
|
elif getIPSGMGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
pass
|
|
elif getCIGroupMemberTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif myarg == 'depth':
|
|
maxdepth = getInteger(minVal=-1)
|
|
elif myarg == 'includederivedmembership':
|
|
includeDerivedMembership = True
|
|
elif myarg in {'minimal', 'basic', 'full'}:
|
|
listView = myarg
|
|
else:
|
|
unknownArgumentExit()
|
|
if not rolesSet:
|
|
rolesSet = ALL_GROUP_ROLES
|
|
if not typesSet:
|
|
typesSet = ALL_CIGROUP_MEMBER_TYPES
|
|
checkCategory = memberDisplayOptions['showCategory']
|
|
fields = ','.join(set(groupFieldsLists['ci']))
|
|
entityList = getCIGroupMembersEntityList(ci, entityList, query, subTitle, matchPatterns, groupFieldsLists['ci'], None)
|
|
kwargs = _getCIListGroupMembersArgs(listView)
|
|
i = 0
|
|
count = len(entityList)
|
|
for group in entityList:
|
|
i += 1
|
|
if isinstance(group, dict):
|
|
groupEntity = group
|
|
else:
|
|
_, name, groupEmail = convertGroupEmailToCloudID(ci, group, i, count)
|
|
if not name:
|
|
continue
|
|
try:
|
|
groupEntity = callGAPI(ci.groups(), 'get',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS,
|
|
name=name, fields=fields)
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.systemError,
|
|
GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CLOUD_IDENTITY_GROUP, groupEmail], str(e), i, count)
|
|
continue
|
|
groupEmail = groupEntity['groupKey']['id'].lower()
|
|
if checkGroupMatchPatterns(groupEmail, groupEntity, matchPatterns):
|
|
_showGroup(groupEntity['name'], groupEmail, 0)
|
|
|
|
# gam print licenses [todrive <ToDriveAttribute>*]
|
|
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)|allskus|gsuite]
|
|
# [maxresults <Integer>]
|
|
# [countsonly]
|
|
def doPrintLicenses(returnFields=None, skus=None, countsOnly=False, returnCounts=False):
|
|
lic = buildGAPIObject(API.LICENSING)
|
|
setTrueCustomerId()
|
|
customerId = _getCustomerId()
|
|
csvPF = CSVPrintFile()
|
|
products = []
|
|
feed = []
|
|
licenseCounts = []
|
|
maxResults = GC.Values[GC.LICENSE_MAX_RESULTS]
|
|
if not returnFields:
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if not returnCounts and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'products', 'product'}:
|
|
products = getGoogleProductList()
|
|
skus = []
|
|
elif myarg in {'skus', 'sku'}:
|
|
skus = getGoogleSKUList()
|
|
products = []
|
|
elif myarg == 'allskus':
|
|
skus = SKU.getAllSKUs()
|
|
products = []
|
|
elif myarg == 'gsuite':
|
|
skus = SKU.getGSuiteSKUs()
|
|
products = []
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
elif myarg == 'maxresults':
|
|
maxResults = getInteger(minVal=10, maxVal=1000)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not skus and not products and GM.Globals[GM.LICENSE_SKUS]:
|
|
skus = GM.Globals[GM.LICENSE_SKUS]
|
|
if not countsOnly:
|
|
fields = getItemFieldsFromFieldsList('items', ['productId', 'skuId', 'userId'])
|
|
csvPF.SetTitles(['userId', 'productId', 'productDisplay', 'skuId', 'skuDisplay'])
|
|
else:
|
|
fields = getItemFieldsFromFieldsList('items', ['userId'])
|
|
if not returnCounts:
|
|
if skus:
|
|
csvPF.SetTitles(['productId', 'productDisplay', 'skuId', 'skuDisplay', 'licenses'])
|
|
else:
|
|
csvPF.SetTitles(['productId', 'productDisplay', 'licenses'])
|
|
else:
|
|
fields = getItemFieldsFromFieldsList('items', returnFields)
|
|
if skus:
|
|
for sku in skus:
|
|
Ent.SetGetting(Ent.LICENSE)
|
|
productId = sku[0]
|
|
skuId = sku[1]
|
|
productDisplay = SKU.formatProductIdDisplayName(productId)
|
|
skuIdDisplay = SKU.formatSKUIdDisplayName(skuId)
|
|
try:
|
|
feed += callGAPIpages(lic.licenseAssignments(), 'listForProductAndSku', 'items',
|
|
pageMessage=getPageMessageForWhom(forWhom=skuIdDisplay),
|
|
throwReasons=[GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
customerId=customerId, productId=productId, skuId=skuId,
|
|
maxResults=maxResults, fields=fields)
|
|
if countsOnly:
|
|
licenseCounts.append([Ent.PRODUCT, productId, Ent.SKU, [skuId, skuIdDisplay][returnCounts], Ent.LICENSE, len(feed)])
|
|
feed = []
|
|
except (GAPI.invalid, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionNotPerformedWarning([Ent.PRODUCT, productDisplay, Ent.SKU, skuIdDisplay], str(e))
|
|
else:
|
|
suppressErrorMsg = False
|
|
if not products:
|
|
suppressErrorMsg = True
|
|
products = SKU.getSortedProductList()
|
|
for productId in products:
|
|
Ent.SetGetting(Ent.LICENSE)
|
|
productDisplay = SKU.formatProductIdDisplayName(productId)
|
|
try:
|
|
feed += callGAPIpages(lic.licenseAssignments(), 'listForProduct', 'items',
|
|
pageMessage=getPageMessageForWhom(forWhom=productDisplay),
|
|
throwReasons=[GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
customerId=customerId, productId=productId,
|
|
maxResults=maxResults, fields=fields)
|
|
if countsOnly:
|
|
licenseCounts.append([Ent.PRODUCT, [productId, productDisplay][returnCounts], Ent.LICENSE, len(feed)])
|
|
feed = []
|
|
except (GAPI.invalid, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
if not suppressErrorMsg:
|
|
entityActionNotPerformedWarning([Ent.PRODUCT, productDisplay], str(e))
|
|
if countsOnly:
|
|
if returnCounts:
|
|
return licenseCounts
|
|
if skus:
|
|
for u_license in licenseCounts:
|
|
csvPF.WriteRow({'productId': u_license[1], 'productDisplay': SKU.productIdToDisplayName(u_license[1]),
|
|
'skuId': u_license[3], 'skuDisplay': SKU.skuIdToDisplayName(u_license[3]), 'licenses': u_license[5]})
|
|
else:
|
|
for u_license in licenseCounts:
|
|
csvPF.WriteRow({'productId': u_license[1], 'productDisplay': SKU.productIdToDisplayName(u_license[1]), 'licenses': u_license[3]})
|
|
csvPF.writeCSVfile('Licenses')
|
|
return
|
|
if returnFields:
|
|
if returnFields == ['userId']:
|
|
userIds = []
|
|
for u_license in feed:
|
|
userId = u_license.get('userId', '').lower()
|
|
if userId:
|
|
userIds.append(userId)
|
|
return userIds
|
|
userSkuIds = {}
|
|
for u_license in feed:
|
|
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 feed:
|
|
userId = u_license.get('userId', '').lower()
|
|
productId = u_license.get('productId', '')
|
|
skuId = u_license.get('skuId', '')
|
|
csvPF.WriteRow({'userId': userId,
|
|
'productId': productId, 'productDisplay': SKU.productIdToDisplayName(productId),
|
|
'skuId': skuId, 'skuDisplay': SKU.skuIdToDisplayName(skuId)})
|
|
csvPF.writeCSVfile('Licenses')
|
|
|
|
# gam show licenses
|
|
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)|allskus|gsuite]
|
|
# [maxresults <Integer>]
|
|
def doShowLicenses():
|
|
licenseCounts = doPrintLicenses(countsOnly=True, returnCounts=True)
|
|
for u_license in licenseCounts:
|
|
printEntityKVList(u_license[:-2], [Ent.Plural(u_license[-2]), u_license[-1]])
|
|
|
|
# gam delete alert <AlertID>
|
|
# gam undelete alert <AlertID>
|
|
def doDeleteOrUndeleteAlert():
|
|
alertId = getString(Cmd.OB_ALERT_ID)
|
|
if Act.Get() == Act.DELETE:
|
|
action = 'delete'
|
|
kwargs = {}
|
|
else:
|
|
action = 'undelete'
|
|
kwargs = {'body': {}}
|
|
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
|
|
if not ac:
|
|
return
|
|
try:
|
|
callGAPI(ac.alerts(), action,
|
|
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
alertId=alertId, **kwargs)
|
|
entityActionPerformed([Ent.ALERT, alertId])
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.ALERT_ID, alertId], str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.permissionDenied):
|
|
userAlertsServiceNotEnabledWarning(user)
|
|
|
|
ALERT_TIME_OBJECTS = {'createTime', 'startTime', 'endTime'}
|
|
|
|
def _showAlert(alert, FJQC, i=0, count=0):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(alert, timeObjects=ALERT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.ALERT_ID, alert['alertId']], i, count)
|
|
Ind.Increment()
|
|
for field in ['createTime', 'startTime', 'endTime']:
|
|
if field in alert:
|
|
printKeyValueList([field, formatLocalTime(alert[field])])
|
|
for field in ['customerId', 'type', 'source', 'deleted', 'securityInvestigationToolLink']:
|
|
if field in alert:
|
|
printKeyValueList([field, alert[field]])
|
|
if 'data' in alert:
|
|
showJSON('data', alert['data'])
|
|
Ind.Decrement()
|
|
|
|
# gam info alert <AlertID> [formatjson]
|
|
def doInfoAlert():
|
|
alertId = getString(Cmd.OB_ALERT_ID)
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
FJQC.GetFormatJSON(myarg)
|
|
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
|
|
if not ac:
|
|
return
|
|
try:
|
|
alert = callGAPI(ac.alerts(), 'get',
|
|
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
alertId=alertId)
|
|
_showAlert(alert, FJQC)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.ALERT_ID, alertId], str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.permissionDenied):
|
|
userAlertsServiceNotEnabledWarning(user)
|
|
|
|
ALERT_ORDERBY_CHOICE_MAP = {
|
|
'createdate': 'create_time',
|
|
'createtime': 'create_time',
|
|
}
|
|
|
|
# gam show alerts [filter <String>] [orderby createtime [ascending|descending]]
|
|
# [formatjson]
|
|
# gam print alerts [todrive <ToDriveAttribute>*] [filter <String>] [orderby createtime [ascending|descending]]
|
|
# [formatjson [quotechar <Character>]]
|
|
def doPrintShowAlerts():
|
|
csvPF = CSVPrintFile(['alertId', 'createTime']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
kwargs = {}
|
|
OBY = OrderBy(ALERT_ORDERBY_CHOICE_MAP)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'filter':
|
|
kwargs['filter'] = getString(Cmd.OB_STRING).replace("'", '"')
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and not FJQC.formatJSON:
|
|
csvPF.SetSortTitles(['alertId', 'createTime', 'startTime', 'endTime', 'customerId', 'type', 'source', 'deleted'])
|
|
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
|
|
if not ac:
|
|
return
|
|
try:
|
|
alerts = callGAPIpages(ac.alerts(), 'list', 'alerts',
|
|
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT],
|
|
orderBy=OBY.orderBy, **kwargs)
|
|
except (GAPI.badRequest, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.ALERT, None], str(e))
|
|
return
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.permissionDenied):
|
|
userAlertsServiceNotEnabledWarning(user)
|
|
return
|
|
if not csvPF:
|
|
jcount = len(alerts)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, Ent.ALERT)
|
|
Ind.Increment()
|
|
j = 0
|
|
for alert in alerts:
|
|
j += 1
|
|
_showAlert(alert, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for alert in alerts:
|
|
row = flattenJSON(alert, timeObjects=ALERT_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'alertId': alert['alertId'],
|
|
'createTime': formatLocalTime(alert['createTime']),
|
|
'JSON': json.dumps(cleanJSON(alert, timeObjects=ALERT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Alerts')
|
|
|
|
ALERT_TYPE_MAP = {
|
|
'notuseful': 'NOT_USEFUL',
|
|
'somewhatuseful': 'SOMEWHAT_USEFUL',
|
|
'veryuseful': 'VERY_USEFUL',
|
|
}
|
|
|
|
# gam create alertfeedback <AlertID> not_useful|somewhat_useful|very_useful
|
|
def doCreateAlertFeedback():
|
|
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
|
|
if not ac:
|
|
return
|
|
alertId = getString(Cmd.OB_ALERT_ID)
|
|
body = {'type': getChoice(ALERT_TYPE_MAP, mapChoice=True)}
|
|
try:
|
|
result = callGAPI(ac.alerts().feedback(), 'create',
|
|
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
alertId=alertId, body=body)
|
|
entityActionPerformed([Ent.ALERT, alertId, Ent.ALERT_FEEDBACK_ID, result['feedbackId']])
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.ALERT_ID, alertId], str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
userAlertsServiceNotEnabledWarning(user)
|
|
|
|
def _showAlertFeedback(feedback, FJQC, i=0, count=0):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(feedback, timeObjects=ALERT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.ALERT_FEEDBACK_ID, feedback['feedbackId']], i, count)
|
|
Ind.Increment()
|
|
for field in ['createTime']:
|
|
if field in feedback:
|
|
printKeyValueList([field, formatLocalTime(feedback[field])])
|
|
for field in ['alertId', 'customerId', 'type', 'email']:
|
|
if field in feedback:
|
|
printKeyValueList([field, feedback[field]])
|
|
Ind.Decrement()
|
|
|
|
ALERT_FEEDBACK_ORDERBY_CHOICE_MAP = {
|
|
'createdate': 'createTime',
|
|
'createtime': 'createTime',
|
|
}
|
|
|
|
# gam show alertfeedback [alert <AlertID>] [filter <String>] [orderby createtime [ascending|descending]]
|
|
# [formatjson]
|
|
# gam print alertfeedback [todrive <ToDriveAttribute>*] [alert <AlertID>] [filter <String>] [orderby createtime [ascending|descending]]
|
|
# [formatjson [quotechar <Character>]]
|
|
def doPrintShowAlertFeedback():
|
|
csvPF = CSVPrintFile(['feedbackId', 'createTime']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
kwargs = {}
|
|
alertId = '-'
|
|
OBY = OrderBy(ALERT_FEEDBACK_ORDERBY_CHOICE_MAP)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'alertid':
|
|
alertId = getString(Cmd.OB_ALERT_ID)
|
|
elif myarg == 'filter':
|
|
kwargs['filter'] = getString(Cmd.OB_STRING).replace("'", '"')
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and not FJQC.formatJSON:
|
|
csvPF.SetSortTitles(['feedbackId', 'createTime', 'alertId', 'customerId', 'type', 'email'])
|
|
user, ac = buildGAPIServiceObject(API.ALERTCENTER, _getAdminEmail())
|
|
if not ac:
|
|
return
|
|
try:
|
|
feedbacks = callGAPIpages(ac.alerts().feedback(), 'list', 'feedback',
|
|
throwReasons=GAPI.ALERT_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.BAD_REQUEST],
|
|
alertId=alertId, **kwargs)
|
|
except (GAPI.notFound, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.ALERT_ID, alertId], str(e))
|
|
return
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.permissionDenied):
|
|
userAlertsServiceNotEnabledWarning(user)
|
|
return
|
|
for sk in OBY.items:
|
|
if sk.endswith(' desc'):
|
|
field, _ = sk.split(' ')
|
|
reverse = True
|
|
else:
|
|
field = sk
|
|
reverse = False
|
|
feedbacks = sorted(feedbacks, key=lambda k: k[field], reverse=reverse) #pylint: disable=cell-var-from-loop
|
|
if not csvPF:
|
|
jcount = len(feedbacks)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, Ent.ALERT_FEEDBACK)
|
|
Ind.Increment()
|
|
j = 0
|
|
for feedback in feedbacks:
|
|
j += 1
|
|
_showAlertFeedback(feedback, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for feedback in feedbacks:
|
|
row = flattenJSON(feedback, timeObjects=ALERT_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'feedbackId': feedback['feedbackId'],
|
|
'createTime': formatLocalTime(feedback['createTime']),
|
|
'JSON': json.dumps(cleanJSON(feedback, timeObjects=ALERT_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Alert Feedbacks')
|
|
|
|
def ACLRuleDict(rule):
|
|
if rule['scope']['type'] != 'default':
|
|
return {'Scope': f'{rule["scope"]["type"]}:{rule["scope"]["value"]}', 'Role': rule['role']}
|
|
return {'Scope': f'{rule["scope"]["type"]}', 'Role': rule['role']}
|
|
|
|
def ACLRuleKeyValueList(rule):
|
|
if rule['scope']['type'] != 'default':
|
|
return ['Scope', f'{rule["scope"]["type"]}:{rule["scope"]["value"]}', 'Role', rule['role']]
|
|
return ['Scope', f'{rule["scope"]["type"]}', 'Role', rule['role']]
|
|
|
|
def formatACLRule(rule):
|
|
return formatKeyValueList('(', ACLRuleKeyValueList(rule), ')')
|
|
|
|
def formatACLScopeRole(scope, role):
|
|
if role:
|
|
return formatKeyValueList('(', ['Scope', scope, 'Role', role], ')')
|
|
return formatKeyValueList('(', ['Scope', scope], ')')
|
|
|
|
def normalizeRuleId(ruleId):
|
|
ruleIdParts = ruleId.split(':', 1)
|
|
if (len(ruleIdParts) == 1) or not ruleIdParts[1]:
|
|
if ruleIdParts[0] == 'default':
|
|
return ruleId
|
|
if ruleIdParts[0] == 'domain':
|
|
return f'domain:{GC.Values[GC.DOMAIN]}'
|
|
return f'user:{normalizeEmailAddressOrUID(ruleIdParts[0], noUid=True)}'
|
|
if ruleIdParts[0] in {'user', 'group'}:
|
|
return f'{ruleIdParts[0]}:{normalizeEmailAddressOrUID(ruleIdParts[1], noUid=True)}'
|
|
return ruleId
|
|
|
|
def makeRoleRuleIdBody(role, ruleId):
|
|
ruleIdParts = ruleId.split(':', 1)
|
|
if len(ruleIdParts) == 1:
|
|
if ruleIdParts[0] == 'default':
|
|
return {'role': role, 'scope': {'type': ruleIdParts[0]}}
|
|
if ruleIdParts[0] == 'domain':
|
|
return {'role': role, 'scope': {'type': ruleIdParts[0], 'value': GC.Values[GC.DOMAIN]}}
|
|
return {'role': role, 'scope': {'type': 'user', 'value': ruleIdParts[0]}}
|
|
return {'role': role, 'scope': {'type': ruleIdParts[0], 'value': ruleIdParts[1]}}
|
|
|
|
BUILDING_ADDRESS_FIELD_MAP = {
|
|
'address': 'addressLines',
|
|
'addresslines': 'addressLines',
|
|
'administrativearea': 'administrativeArea',
|
|
'city': 'locality',
|
|
'country': 'regionCode',
|
|
'language': 'languageCode',
|
|
'languagecode': 'languageCode',
|
|
'locality': 'locality',
|
|
'postalcode': 'postalCode',
|
|
'regioncode': 'regionCode',
|
|
'state': 'administrativeArea',
|
|
'sublocality': 'sublocality',
|
|
'zipcode': 'postalCode',
|
|
}
|
|
|
|
def _getBuildingAttributes(body):
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'id':
|
|
body['buildingId'] = getString(Cmd.OB_BUILDING_ID, maxLen=100)
|
|
elif myarg == 'name':
|
|
body['buildingName'] = getString(Cmd.OB_STRING, maxLen=100)
|
|
elif myarg in {'lat', 'latitude'}:
|
|
body.setdefault('coordinates', {})
|
|
body['coordinates']['latitude'] = getFloat(minVal=-180.0, maxVal=180.0)
|
|
elif myarg in {'long', 'lng', 'longitude'}:
|
|
body.setdefault('coordinates', {})
|
|
body['coordinates']['longitude'] = getFloat(minVal=-180.0, maxVal=180.0)
|
|
elif myarg == 'description':
|
|
body['description'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'floors':
|
|
body['floorNames'] = getString(Cmd.OB_STRING).split(',')
|
|
elif myarg in BUILDING_ADDRESS_FIELD_MAP:
|
|
myarg = BUILDING_ADDRESS_FIELD_MAP[myarg]
|
|
body.setdefault('address', {})
|
|
if myarg == 'addressLines':
|
|
body['address'][myarg] = getStringWithCRsNLs().split('\n')
|
|
elif myarg == 'languageCode':
|
|
body['address'][myarg] = getLanguageCode(LANGUAGE_CODES_MAP)
|
|
else:
|
|
body['address'][myarg] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
return body
|
|
|
|
# gam create building <Name> <BuildingAttribute>*
|
|
def doCreateBuilding():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
body = _getBuildingAttributes({'buildingId': str(uuid.uuid4()),
|
|
'buildingName': getString(Cmd.OB_NAME, maxLen=100),
|
|
'floorNames': ['1']})
|
|
try:
|
|
callGAPI(cd.resources().buildings(), 'insert',
|
|
throwReasons=[GAPI.DUPLICATE, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], body=body)
|
|
entityActionPerformed([Ent.BUILDING_ID, body['buildingId'], Ent.BUILDING, body['buildingName']])
|
|
except GAPI.duplicate:
|
|
entityDuplicateWarning([Ent.BUILDING_ID, body['buildingId'], Ent.BUILDING, body['buildingName']])
|
|
except GAPI.invalidInput as e:
|
|
entityActionFailedWarning([Ent.BUILDING_ID, body['buildingId'], Ent.BUILDING, body['buildingName']], str(e))
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
def _makeBuildingIdNameMap(cd=None):
|
|
GM.Globals[GM.MAKE_BUILDING_ID_NAME_MAP] = False
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
try:
|
|
buildings = callGAPIpages(cd.resources().buildings(), 'list', 'buildings',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID],
|
|
fields='nextPageToken,buildings(buildingId,buildingName)')
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
for building in buildings:
|
|
GM.Globals[GM.MAP_BUILDING_ID_TO_NAME][building['buildingId']] = building['buildingName']
|
|
GM.Globals[GM.MAP_BUILDING_NAME_TO_ID][building['buildingName']] = building['buildingId']
|
|
|
|
def _getBuildingByNameOrId(cd, minLen=1, allowNV=False):
|
|
which_building = getString(Cmd.OB_BUILDING_ID, minLen=minLen)
|
|
if not which_building or (minLen == 0 and which_building in {'id:', 'uid:'}):
|
|
return ''
|
|
cg = UID_PATTERN.match(which_building)
|
|
if cg:
|
|
return cg.group(1)
|
|
if allowNV and which_building.startswith('nv:'):
|
|
return which_building[3:]
|
|
if GM.Globals[GM.MAKE_BUILDING_ID_NAME_MAP]:
|
|
_makeBuildingIdNameMap(cd)
|
|
# Exact name match, return ID
|
|
if which_building in GM.Globals[GM.MAP_BUILDING_NAME_TO_ID]:
|
|
return GM.Globals[GM.MAP_BUILDING_NAME_TO_ID][which_building]
|
|
# No exact name match, check for case insensitive name matches
|
|
which_building_lower = which_building.lower()
|
|
ci_matches = []
|
|
for buildingName, buildingId in iter(GM.Globals[GM.MAP_BUILDING_NAME_TO_ID].items()):
|
|
if buildingName.lower() == which_building_lower:
|
|
ci_matches.append({'buildingName': buildingName, 'buildingId': buildingId})
|
|
# One match, return ID
|
|
if len(ci_matches) == 1:
|
|
return ci_matches[0]['buildingId']
|
|
# No or multiple name matches, try ID
|
|
# Exact ID match, return ID
|
|
if which_building in GM.Globals[GM.MAP_BUILDING_ID_TO_NAME]:
|
|
return which_building
|
|
# No exact ID match, check for case insensitive id match
|
|
for buildingId in GM.Globals[GM.MAP_BUILDING_ID_TO_NAME]:
|
|
# Match, return ID
|
|
if buildingId.lower() == which_building_lower:
|
|
return buildingId
|
|
# Multiple name matches
|
|
if len(ci_matches) > 1:
|
|
printErrorMessage(1, Msg.MULTIPLE_BUILDINGS_SAME_NAME.format(len(ci_matches), Ent.Plural(Ent.BUILDING)))
|
|
Ind.Increment()
|
|
for building in ci_matches:
|
|
printEntity([Ent.BUILDING, building['buildingName'], Ent.BUILDING_ID, building['buildingId']])
|
|
Ind.Decrement()
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.PLEASE_SPECIFY_BUILDING_EXACT_CASE_NAME_OR_ID)
|
|
# No matches
|
|
entityDoesNotExistExit(Ent.BUILDING, which_building)
|
|
|
|
def _getBuildingNameById(cd, buildingId):
|
|
if GM.Globals[GM.MAKE_BUILDING_ID_NAME_MAP]:
|
|
_makeBuildingIdNameMap(cd)
|
|
return GM.Globals[GM.MAP_BUILDING_ID_TO_NAME].get(buildingId, buildingId)
|
|
|
|
# gam update building <BuildIngID> <BuildingAttribute>*
|
|
def doUpdateBuilding():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
buildingId = _getBuildingByNameOrId(cd)
|
|
body = _getBuildingAttributes({})
|
|
try:
|
|
callGAPI(cd.resources().buildings(), 'patch',
|
|
throwReasons=[GAPI.DUPLICATE, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], buildingId=buildingId, body=body)
|
|
entityActionPerformed([Ent.BUILDING_ID, buildingId])
|
|
except GAPI.duplicate:
|
|
entityDuplicateWarning([Ent.BUILDING, body['buildingName']])
|
|
except GAPI.resourceNotFound:
|
|
entityUnknownWarning(Ent.BUILDING_ID, buildingId)
|
|
except GAPI.invalidInput as e:
|
|
entityActionFailedWarning([Ent.BUILDING_ID, buildingId], str(e))
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
# gam delete building <BuildIngID>
|
|
def doDeleteBuilding():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
buildingId = _getBuildingByNameOrId(cd)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cd.resources().buildings(), 'delete',
|
|
throwReasons=[GAPI.RESOURCE_NOT_FOUND, GAPI.CONDITION_NOT_MET,
|
|
GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], buildingId=buildingId)
|
|
entityActionPerformed([Ent.BUILDING_ID, buildingId])
|
|
except GAPI.resourceNotFound:
|
|
entityUnknownWarning(Ent.BUILDING_ID, buildingId)
|
|
except GAPI.conditionNotMet as e:
|
|
entityActionFailedWarning([Ent.BUILDING_ID, buildingId], str(e))
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
BUILDING_ADDRESS_PRINT_ORDER = ['addressLines', 'sublocality', 'locality', 'administrativeArea', 'postalCode', 'regionCode', 'languageCode']
|
|
|
|
def _showBuilding(building, delimiter=',', i=0, count=0, FJQC=None):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(building), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
if 'buildingName' in building:
|
|
printEntity([Ent.BUILDING, building['buildingName']], i, count)
|
|
Ind.Increment()
|
|
printKeyValueList(['buildingId', f'id:{building["buildingId"]}'])
|
|
else:
|
|
printEntity([Ent.BUILDING_ID, f'id:{building["buildingId"]}'], i, count)
|
|
Ind.Increment()
|
|
if 'description' in building:
|
|
printKeyValueList(['description', building['description']])
|
|
if 'floorNames' in building:
|
|
printKeyValueList(['floorNames', delimiter.join(building['floorNames'])])
|
|
if 'coordinates' in building:
|
|
printKeyValueList(['coordinates', None])
|
|
Ind.Increment()
|
|
printKeyValueList(['latitude', f'{building["coordinates"].get("latitude", 0):4.7f}'])
|
|
printKeyValueList(['longitude', f'{building["coordinates"].get("longitude", 0):4.7f}'])
|
|
Ind.Decrement()
|
|
if 'address' in building:
|
|
printKeyValueList(['address', None])
|
|
Ind.Increment()
|
|
for field in BUILDING_ADDRESS_PRINT_ORDER:
|
|
if field in building['address']:
|
|
if field != 'addressLines':
|
|
printKeyValueList([field, building['address'][field]])
|
|
else:
|
|
printKeyValueList([field, None])
|
|
Ind.Increment()
|
|
for line in building['address'][field]:
|
|
printKeyValueList([line])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam info building <BuildingID>
|
|
# [formatjson]
|
|
def doInfoBuilding():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
buildingId = _getBuildingByNameOrId(cd)
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
FJQC.GetFormatJSON(myarg)
|
|
try:
|
|
building = callGAPI(cd.resources().buildings(), 'get',
|
|
throwReasons=[GAPI.RESOURCE_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], buildingId=buildingId)
|
|
_showBuilding(building, FJQC=FJQC)
|
|
except GAPI.resourceNotFound:
|
|
entityUnknownWarning(Ent.BUILDING_ID, buildingId)
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
BUILDINGS_FIELDS_CHOICE_MAP = {
|
|
'address': 'address',
|
|
'buildingid': 'buildingId',
|
|
'buildingname': 'buildingName',
|
|
'coordinates': 'coordinates',
|
|
'description': 'description',
|
|
'floors': 'floorNames',
|
|
'floornames': 'floorNames',
|
|
'id': 'buildingId',
|
|
'name': 'buildingName',
|
|
}
|
|
BUILDINGS_SORT_TITLES = ['buildingId', 'buildingName', 'description', 'floorNames']
|
|
|
|
# gam print buildings [todrive <ToDriveAttribute>*]
|
|
# [allfields|<BuildingFildName>*|(fields <BuildingFieldNameList>)]
|
|
# [delimiter <Character>] [formatjson [quotechar <Character>]]
|
|
# gam show buildings
|
|
# [allfields|<BuildingFildName>*|(fields <BuildingFieldNameList>)]
|
|
# [formatjson]
|
|
def doPrintShowBuildings():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['buildingId'], BUILDINGS_SORT_TITLES) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER] if csvPF else ','
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg == 'allfields':
|
|
fieldsList = []
|
|
elif getFieldsList(myarg, BUILDINGS_FIELDS_CHOICE_MAP, fieldsList, initialField='buildingId'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
fields = getItemFieldsFromFieldsList('buildings', fieldsList)
|
|
try:
|
|
buildings = callGAPIpages(cd.resources().buildings(), 'list', 'buildings',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
if not csvPF:
|
|
jcount = len(buildings)
|
|
performActionNumItems(jcount, Ent.BUILDING)
|
|
Ind.Increment()
|
|
j = 0
|
|
for building in buildings:
|
|
j += 1
|
|
_showBuilding(building, delimiter, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
for building in buildings:
|
|
if 'buildingId' in building:
|
|
building['buildingId'] = f'id:{building["buildingId"]}'
|
|
if 'floorNames' in building:
|
|
building['floorNames'] = delimiter.join(building['floorNames'])
|
|
if 'address' in building and 'addressLines' in building['address']:
|
|
building['address']['addressLines'] = '\n'.join(building['address']['addressLines'])
|
|
if 'coordinates' in building:
|
|
building['coordinates']['latitude'] = f'{building["coordinates"].get("latitude", 0):4.7f}'
|
|
building['coordinates']['longitude'] = f'{building["coordinates"].get("longitude", 0):4.7f}'
|
|
row = flattenJSON(building)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
else:
|
|
if (not csvPF.rowFilter and not csvPF.rowDropFilter) or csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'buildingId': building['buildingId'],
|
|
'JSON': json.dumps(cleanJSON(building),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Buildings')
|
|
|
|
def _getFeatureAttributes(body):
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
body['name'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
return body
|
|
|
|
# gam create feature name <Name>
|
|
def doCreateFeature():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
body = _getFeatureAttributes({})
|
|
try:
|
|
callGAPI(cd.resources().features(), 'insert',
|
|
throwReasons=[GAPI.DUPLICATE, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], body=body)
|
|
entityActionPerformed([Ent.BUILDING, body['name']])
|
|
except GAPI.duplicate:
|
|
entityDuplicateWarning([Ent.FEATURE, body['name']])
|
|
except GAPI.invalidInput as e:
|
|
entityActionFailedWarning([Ent.FEATURE, body['name']], str(e))
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
#gam update feature <Name> name <Name>
|
|
def doUpdateFeature():
|
|
# update does not work for name and name is only field to be updated
|
|
# if additional writable fields are added to feature in the future
|
|
# we'll add support for update as well as rename
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
oldName = getString(Cmd.OB_STRING)
|
|
getChoice(['name'])
|
|
body = {'newName': getString(Cmd.OB_STRING)}
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cd.resources().features(), 'rename',
|
|
throwReasons=[GAPI.DUPLICATE, GAPI.RESOURCE_NOT_FOUND, GAPI.INVALID_INPUT, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], oldName=oldName, body=body)
|
|
entityActionPerformed([Ent.FEATURE, oldName])
|
|
except GAPI.duplicate:
|
|
entityDuplicateWarning([Ent.FEATURE, body['newName']])
|
|
except GAPI.resourceNotFound:
|
|
entityUnknownWarning(Ent.FEATURE, oldName)
|
|
except GAPI.invalidInput as e:
|
|
entityActionFailedWarning([Ent.FEATURE, oldName], str(e))
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
# gam delete feature <Name>
|
|
def doDeleteFeature():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
featureKey = getString(Cmd.OB_NAME)
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(cd.resources().features(), 'delete',
|
|
throwReasons=[GAPI.RESOURCE_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], featureKey=featureKey)
|
|
entityActionPerformed([Ent.FEATURE, featureKey])
|
|
except GAPI.resourceNotFound:
|
|
entityUnknownWarning(Ent.FEATURE, featureKey)
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
FEATURE_FIELDS_CHOICE_MAP = {
|
|
'name': 'name',
|
|
}
|
|
|
|
# gam print features [todrive <ToDriveAttribute>*]
|
|
# gam show features
|
|
def doPrintShowFeatures():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile('name') if Act.csvFormat() else None
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'allfields':
|
|
fieldsList = []
|
|
elif getFieldsList(myarg, FEATURE_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
fields = getItemFieldsFromFieldsList('features', fieldsList)
|
|
try:
|
|
features = callGAPIpages(cd.resources().features(), 'list', 'features',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
|
|
except (GAPI.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
if not csvPF:
|
|
jcount = len(features)
|
|
performActionNumItems(jcount, Ent.FEATURE)
|
|
Ind.Increment()
|
|
j = 0
|
|
for feature in features:
|
|
j += 1
|
|
printEntity([Ent.FEATURE, feature['name']], j, jcount)
|
|
else:
|
|
for feature in features:
|
|
csvPF.WriteRowTitles(flattenJSON(feature))
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Features')
|
|
|
|
RESOURCE_CATEGORY_MAP = {
|
|
'conference': 'CONFERENCE_ROOM',
|
|
'conferenceroom': 'CONFERENCE_ROOM',
|
|
'room': 'CONFERENCE_ROOM',
|
|
'other': 'OTHER',
|
|
'categoryunknown': 'CATEGORY_UNKNOWN',
|
|
'unknown': 'CATEGORY_UNKNOWN',
|
|
}
|
|
|
|
def _getResourceCalendarAttributes(cd, body, updateMode):
|
|
featureChanges = {'add': set(), 'remove': set()}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'name', 'resourcename'}:
|
|
body['resourceName'] = getString(Cmd.OB_STRING)
|
|
elif myarg in {'description', 'resourcedescription'}:
|
|
body['resourceDescription'] = getStringWithCRsNLs()
|
|
elif myarg in {'type', 'resourcetype'}:
|
|
body['resourceType'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'building', 'buildingid'}:
|
|
body['buildingId'] = _getBuildingByNameOrId(cd, minLen=0)
|
|
elif myarg == 'capacity':
|
|
body['capacity'] = getInteger(minVal=0)
|
|
elif myarg in {'feature', 'features', 'featureinstances'}:
|
|
body.setdefault('featureInstances', [])
|
|
for feature in shlexSplitList(getString(Cmd.OB_STRING, minLen=0)):
|
|
body['featureInstances'].append({'feature': {'name': feature}})
|
|
elif myarg in {'addfeature', 'addfeatures', 'addfeatureinstances'}:
|
|
if not updateMode:
|
|
body.setdefault('featureInstances', [])
|
|
for feature in shlexSplitList(getString(Cmd.OB_STRING, minLen=0)):
|
|
body['featureInstances'].append({'feature': {'name': feature}})
|
|
else:
|
|
for feature in shlexSplitList(getString(Cmd.OB_STRING, minLen=0)):
|
|
featureChanges['add'].add(feature)
|
|
elif updateMode and myarg in {'removefeature', 'removefeatures', 'removefeatureinstances'}:
|
|
for feature in shlexSplitList(getString(Cmd.OB_STRING, minLen=0)):
|
|
featureChanges['remove'].add(feature)
|
|
elif myarg in {'floor', 'floorname'}:
|
|
body['floorName'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'floorsection':
|
|
body['floorSection'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'category', 'resourcecategory'}:
|
|
body['resourceCategory'] = getChoice(RESOURCE_CATEGORY_MAP, mapChoice=True)
|
|
elif myarg in {'userdescription', 'uservisibledescription'}:
|
|
body['userVisibleDescription'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
if ('featureInstances' in body and not body['featureInstances'] and
|
|
not featureChanges['add'] and not featureChanges['remove']):
|
|
body['featureInstances'] = [{}]
|
|
if not updateMode:
|
|
return body
|
|
return body, featureChanges
|
|
|
|
# gam create resource <ResourceID> <Name> <ResourceAttribute>*
|
|
def doCreateResourceCalendar():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
body = _getResourceCalendarAttributes(cd, {'resourceId': getString(Cmd.OB_RESOURCE_ID), 'resourceName': getString(Cmd.OB_NAME)}, False)
|
|
try:
|
|
callGAPI(cd.resources().calendars(), 'insert',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.SERVICE_NOT_AVAILABLE,
|
|
GAPI.REQUIRED, GAPI.DUPLICATE, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=GC.Values[GC.CUSTOMER_ID], body=body, fields='')
|
|
entityActionPerformed([Ent.RESOURCE_CALENDAR, body['resourceId']])
|
|
except (GAPI.invalid, GAPI.invalidInput, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, body['resourceId']], str(e))
|
|
except GAPI.required as e:
|
|
errMsg = str(e)
|
|
if '[resourceCapacity, resourceFloorName]' in errMsg:
|
|
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, body['resourceId']], Msg.RESOURCE_CAPACITY_FLOOR_REQUIRED)
|
|
elif'[resourceFloorName]' in errMsg:
|
|
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, body['resourceId']], Msg.RESOURCE_FLOOR_REQUIRED)
|
|
else:
|
|
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, body['resourceId']], errMsg)
|
|
except GAPI.duplicate:
|
|
entityDuplicateWarning([Ent.RESOURCE_CALENDAR, body['resourceId']])
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
def _doUpdateResourceCalendars(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
body, featureChanges = _getResourceCalendarAttributes(cd, {}, True)
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
try:
|
|
if featureChanges['add'] or featureChanges['remove']:
|
|
features = callGAPI(cd.resources().calendars(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId, fields='featureInstances(feature(name))')
|
|
bodyFeatures = body.pop('featureInstances', [])
|
|
body['featureInstances'] = []
|
|
featureSet = set()
|
|
for feature in bodyFeatures:
|
|
featureName = feature['feature']['name']
|
|
if featureName not in featureChanges['remove'] and featureName not in featureSet:
|
|
body['featureInstances'].append({'feature': {'name': featureName}})
|
|
featureSet.add(featureName)
|
|
for feature in features.get('featureInstances', []):
|
|
featureName = feature['feature']['name']
|
|
if featureName not in featureChanges['remove'] and featureName not in featureSet:
|
|
body['featureInstances'].append({'feature': {'name': featureName}})
|
|
featureSet.add(featureName)
|
|
for featureName in featureChanges['add']:
|
|
if featureName not in featureChanges['remove'] and featureName not in featureSet:
|
|
body['featureInstances'].append({'feature': {'name': featureName}})
|
|
featureSet.add(featureName)
|
|
if not body['featureInstances']:
|
|
body['featureInstances'] = [{}]
|
|
callGAPI(cd.resources().calendars(), 'patch',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.SERVICE_NOT_AVAILABLE, GAPI.REQUIRED,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId, body=body, fields='')
|
|
entityActionPerformed([Ent.RESOURCE_CALENDAR, resourceId], i, count)
|
|
except (GAPI.invalid, GAPI.invalidInput, GAPI.serviceNotAvailable, GAPI.required) as e:
|
|
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, resourceId], str(e), i, count)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
|
|
|
|
# gam update resources <ResourceEntity> <ResourceAttribute>*
|
|
def doUpdateResourceCalendars():
|
|
_doUpdateResourceCalendars(getEntityList(Cmd.OB_RESOURCE_ENTITY))
|
|
|
|
# gam update resource <ResourceID> <ResourceAttribute>*
|
|
def doUpdateResourceCalendar():
|
|
_doUpdateResourceCalendars(getStringReturnInList(Cmd.OB_RESOURCE_ID))
|
|
|
|
def _doDeleteResourceCalendars(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
try:
|
|
callGAPI(cd.resources().calendars(), 'delete',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId)
|
|
entityActionPerformed([Ent.RESOURCE_CALENDAR, resourceId], i, count)
|
|
except GAPI.serviceNotAvailable as e:
|
|
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, resourceId], str(e), i, count)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
|
|
|
|
# gam delete resources <ResourceEntity>
|
|
def doDeleteResourceCalendars():
|
|
_doDeleteResourceCalendars(getEntityList(Cmd.OB_RESOURCE_ENTITY))
|
|
|
|
# gam delete resource <ResourceID>
|
|
def doDeleteResourceCalendar():
|
|
_doDeleteResourceCalendars(getStringReturnInList(Cmd.OB_RESOURCE_ID))
|
|
|
|
def _getResourceACLsCalSettings(cal, resource, getCalSettings, getCalPermissions, i, count):
|
|
calId = resource['resourceEmail']
|
|
try:
|
|
if getCalPermissions:
|
|
acls = callGAPIpages(cal.acl(), 'list', 'items',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.AUTH_ERROR],
|
|
calendarId=calId, fields='nextPageToken,items(id,role,scope)')
|
|
else:
|
|
acls = {}
|
|
if getCalSettings:
|
|
settings = callGAPI(cal.calendars(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
calendarId=calId)
|
|
settings.pop('etag', None)
|
|
settings.pop('kind', None)
|
|
resource.update({'calendar': settings})
|
|
return (True, acls)
|
|
except (GAPI.forbidden, GAPI.serviceNotAvailable, GAPI.authError, GAPI.notACalendarUser) as e:
|
|
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, calId], str(e), i, count)
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.RESOURCE_CALENDAR, calId, i, count)
|
|
return (False, None)
|
|
|
|
RESOURCE_DFLT_FIELDS = ['resourceId', 'resourceName', 'resourceEmail', 'resourceDescription', 'resourceType']
|
|
RESOURCE_ADDTL_FIELDS = [
|
|
'buildingId', # buildingId must be first element
|
|
'capacity',
|
|
'featureInstances',
|
|
'floorName',
|
|
'floorSection',
|
|
'generatedResourceName',
|
|
'resourceCategory',
|
|
'userVisibleDescription',
|
|
]
|
|
RESOURCE_ALL_FIELDS = RESOURCE_DFLT_FIELDS+RESOURCE_ADDTL_FIELDS
|
|
RESOURCE_FIELDS_WITH_CRS_NLS = {'resourceDescription'}
|
|
|
|
def _showResource(cd, resource, i, count, FJQC, acls=None, noSelfOwner=False):
|
|
|
|
def _showResourceField(title, resource, field):
|
|
if field in resource:
|
|
if field not in RESOURCE_FIELDS_WITH_CRS_NLS:
|
|
printKeyValueList([title, resource[field]])
|
|
else:
|
|
printKeyValueWithCRsNLs(title, resource[field])
|
|
|
|
if 'buildingId' in resource:
|
|
resource['buildingName'] = _getBuildingNameById(cd, resource['buildingId'])
|
|
resource['buildingId'] = f'id:{resource["buildingId"]}'
|
|
if FJQC.formatJSON:
|
|
if acls:
|
|
resource['acls'] = [{'id': rule['id'], 'role': rule['role']} for rule in acls]
|
|
printLine(json.dumps(cleanJSON(resource), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.RESOURCE_ID, resource['resourceId']], i, count)
|
|
Ind.Increment()
|
|
_showResourceField('Name', resource, 'resourceName')
|
|
_showResourceField('Email', resource, 'resourceEmail')
|
|
_showResourceField('Type', resource, 'resourceType')
|
|
_showResourceField('Description', resource, 'resourceDescription')
|
|
if 'featureInstances' in resource:
|
|
resource['featureInstances'] = ', '.join([a_feature['feature']['name'] for a_feature in resource.pop('featureInstances')])
|
|
if 'buildingId' in resource:
|
|
_showResourceField('buildingId', resource, 'buildingId')
|
|
_showResourceField('buildingName', resource, 'buildingName')
|
|
for field in RESOURCE_ADDTL_FIELDS[1:]:
|
|
_showResourceField(field, resource, field)
|
|
calendar = resource.get('calendar')
|
|
if calendar:
|
|
_showCalendarSettings(calendar, 0, 0)
|
|
if acls:
|
|
j = 0
|
|
jcount = len(acls)
|
|
printEntitiesCount(Ent.CALENDAR_ACL, acls)
|
|
Ind.Increment()
|
|
for rule in acls:
|
|
j += 1
|
|
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == resource['resourceEmail']:
|
|
continue
|
|
printKeyValueListWithCount(ACLRuleKeyValueList(rule), j, jcount)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
def _doInfoResourceCalendars(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
getCalSettings = getCalPermissions = noSelfOwner = False
|
|
FJQC = FormatJSONQuoteChar()
|
|
acls = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
|
|
getCalPermissions = True
|
|
elif myarg == 'noselfowner':
|
|
noSelfOwner = True
|
|
elif myarg == Cmd.ARG_CALENDAR:
|
|
getCalSettings = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if getCalSettings or getCalPermissions:
|
|
cal = buildGAPIObject(API.CALENDAR)
|
|
fields = ','.join(RESOURCE_ALL_FIELDS)
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
try:
|
|
resource = callGAPI(cd.resources().calendars(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId, fields=fields)
|
|
if getCalSettings or getCalPermissions:
|
|
status, acls = _getResourceACLsCalSettings(cal, resource, getCalSettings, getCalPermissions, i, count)
|
|
if not status:
|
|
continue
|
|
_showResource(cd, resource, i, count, FJQC, acls, noSelfOwner)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
|
|
|
|
# gam info resources <ResourceEntity>
|
|
# [acls] [noselfowner] [calendar] [formatjson]
|
|
def doInfoResourceCalendars():
|
|
_doInfoResourceCalendars(getEntityList(Cmd.OB_RESOURCE_ENTITY))
|
|
|
|
# gam info resource <ResourceID>
|
|
# [acls] [noselfowner] [calendar] [formatjson]
|
|
def doInfoResourceCalendar():
|
|
_doInfoResourceCalendars(getStringReturnInList(Cmd.OB_RESOURCE_ID))
|
|
|
|
RESOURCE_FIELDS_CHOICE_MAP = {
|
|
'description': 'resourceDescription',
|
|
'building': 'buildingId',
|
|
'buildingid': 'buildingId',
|
|
'capacity': 'capacity',
|
|
'category': 'resourceCategory',
|
|
'email': 'resourceEmail',
|
|
'feature': 'featureInstances',
|
|
'features': 'featureInstances',
|
|
'featureinstances': 'featureInstances',
|
|
'floor': 'floorName',
|
|
'floorname': 'floorName',
|
|
'floorsection': 'floorSection',
|
|
'generatedresourcename': 'generatedResourceName',
|
|
'id': 'resourceId',
|
|
'name': 'resourceName',
|
|
'resourcecategory': 'resourceCategory',
|
|
'resourcedescription': 'resourceDescription',
|
|
'resourceemail': 'resourceEmail',
|
|
'resourceid': 'resourceId',
|
|
'resourcename': 'resourceName',
|
|
'resourcetype': 'resourceType',
|
|
'type': 'resourceType',
|
|
'userdescription': 'userVisibleDescription',
|
|
'uservisibledescription': 'userVisibleDescription',
|
|
}
|
|
|
|
# gam show resources
|
|
# [allfields|<ResourceFieldName>*|(fields <ResourceFieldNameList>)]
|
|
# [query <String>]
|
|
# [acls] [noselfowner] [calendar] [convertcrnl] [formatjson]
|
|
# gam print resources [todrive <ToDriveAttribute>*]
|
|
# [allfields|<ResourceFieldName>*|(fields <ResourceFieldNameList>)]
|
|
# [query <String>]
|
|
# [acls] [noselfowner] [calendar] [convertcrnl] [formatjson [quotechar <Character>]]
|
|
# [showitemcountonly]
|
|
def doPrintShowResourceCalendars():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]
|
|
getCalSettings = getCalPermissions = noSelfOwner = False
|
|
acls = query = None
|
|
fieldsList = []
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
showItemCountOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'query':
|
|
query = getString(Cmd.OB_QUERY)
|
|
elif myarg == 'allfields':
|
|
fieldsList = RESOURCE_ALL_FIELDS[:]
|
|
elif myarg in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
|
|
getCalPermissions = True
|
|
elif myarg == 'noselfowner':
|
|
noSelfOwner = True
|
|
elif myarg == Cmd.ARG_CALENDAR:
|
|
getCalSettings = True
|
|
elif myarg in RESOURCE_FIELDS_CHOICE_MAP:
|
|
if not fieldsList:
|
|
fieldsList = ['resourceId']
|
|
fieldsList.append(RESOURCE_FIELDS_CHOICE_MAP[myarg])
|
|
elif myarg == 'fields':
|
|
if not fieldsList:
|
|
fieldsList = ['resourceId']
|
|
for field in _getFieldsList():
|
|
if field in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
|
|
getCalPermissions = True
|
|
elif field == Cmd.ARG_CALENDAR:
|
|
getCalSettings = True
|
|
elif field in RESOURCE_FIELDS_CHOICE_MAP:
|
|
fieldsList.append(RESOURCE_FIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, RESOURCE_FIELDS_CHOICE_MAP, True)
|
|
elif myarg in {'convertcrnl', 'converttextnl'}:
|
|
convertCRNL = True
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if not fieldsList:
|
|
fieldsList = RESOURCE_DFLT_FIELDS[:]
|
|
if getCalSettings or getCalPermissions:
|
|
cal = buildGAPIObject(API.CALENDAR)
|
|
fields = getItemFieldsFromFieldsList('items', fieldsList+['resourceEmail'])
|
|
else:
|
|
fields = getItemFieldsFromFieldsList('items', fieldsList)
|
|
if 'buildingId' in fieldsList:
|
|
fieldsList.append('buildingName')
|
|
if csvPF:
|
|
if not FJQC.formatJSON:
|
|
csvPF.AddTitles(fieldsList)
|
|
csvPF.SetSortTitles(RESOURCE_DFLT_FIELDS)
|
|
else:
|
|
if 'resourceName' in fieldsList:
|
|
sortTitles = ['resourceId', 'resourceName', 'JSON']
|
|
else:
|
|
sortTitles = ['resourceId', 'JSON']
|
|
csvPF.AddJSONTitles(sortTitles)
|
|
printGettingAllAccountEntities(Ent.RESOURCE_CALENDAR)
|
|
try:
|
|
resources = callGAPIpages(cd.resources().calendars(), 'list', 'items',
|
|
pageMessage=getPageMessage(showFirstLastItems=True), messageAttribute='resourceName',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT],
|
|
query=query, customer=GC.Values[GC.CUSTOMER_ID], fields=fields)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
except GAPI.invalidInput as e:
|
|
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, ''], str(e))
|
|
return
|
|
count = len(resources)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{count}\n')
|
|
return
|
|
i = 0
|
|
for resource in resources:
|
|
i += 1
|
|
if getCalSettings or getCalPermissions:
|
|
status, acls = _getResourceACLsCalSettings(cal, resource, getCalSettings, getCalPermissions, i, count)
|
|
if not status:
|
|
continue
|
|
if not csvPF:
|
|
_showResource(cd, resource, i, count, FJQC, acls, noSelfOwner)
|
|
else:
|
|
if 'buildingId' in resource:
|
|
resource['buildingName'] = _getBuildingNameById(cd, resource['buildingId'])
|
|
resource['buildingId'] = f'id:{resource["buildingId"]}'
|
|
if not FJQC.formatJSON:
|
|
if 'featureInstances' in resource:
|
|
resource['featureInstances'] = ', '.join([a_feature['feature']['name'] for a_feature in resource.pop('featureInstances')])
|
|
row = {}
|
|
for field in fieldsList:
|
|
if convertCRNL and field in RESOURCE_FIELDS_WITH_CRS_NLS:
|
|
row[field] = escapeCRsNLs(resource.get(field, ''))
|
|
else:
|
|
row[field] = resource.get(field, '')
|
|
if getCalSettings and 'calendar' in resource:
|
|
flattenJSON(resource['calendar'], flattened=row)
|
|
if getCalPermissions:
|
|
for rule in acls:
|
|
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == resource['resourceEmail']:
|
|
continue
|
|
csvPF.WriteRowTitles(flattenJSON(rule, flattened=row.copy()))
|
|
else:
|
|
csvPF.WriteRowTitles(row)
|
|
else:
|
|
if getCalPermissions:
|
|
resource['acls'] = []
|
|
for rule in acls:
|
|
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == resource['resourceEmail']:
|
|
continue
|
|
resource['acls'].append({'id': rule['id'], 'role': rule['role']})
|
|
row = {'resourceId': resource['resourceId'], 'JSON': json.dumps(cleanJSON(resource), ensure_ascii=False, sort_keys=True)}
|
|
if 'resourceName' in resource:
|
|
row['resourceName'] = resource['resourceName']
|
|
csvPF.WriteRow(row)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Resources')
|
|
|
|
# Calendar commands utilities
|
|
def normalizeCalendarId(calId, user):
|
|
if not user or calId.lower() != 'primary':
|
|
return convertUIDtoEmailAddress(calId, emailTypes=['user', 'resource'])
|
|
return user
|
|
|
|
def checkCalendarExists(cal, calId, i, count, showMessage=False):
|
|
if not cal:
|
|
cal = buildGAPIObject(API.CALENDAR)
|
|
try:
|
|
return callGAPI(cal.calendars(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
calendarId=calId, fields='id')['id']
|
|
except GAPI.notFound as e:
|
|
if showMessage:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e))
|
|
return None
|
|
except GAPI.notACalendarUser:
|
|
if showMessage:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
return None
|
|
|
|
def validateCalendar(calId, i=0, count=0, noClientAccess=False):
|
|
cal = None
|
|
if not calId.endswith('.calendar.google.com'):
|
|
calId, cal = buildGAPIServiceObject(API.CALENDAR, calId, i, count, displayError=noClientAccess)
|
|
if not cal:
|
|
if noClientAccess:
|
|
return (calId, None)
|
|
cal = buildGAPIObject(API.CALENDAR)
|
|
try:
|
|
callGAPI(cal.calendars(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
calendarId=calId, fields='')
|
|
return (calId, cal)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
return (calId, None)
|
|
|
|
def getNormalizedCalIdCal(cal, calId, user, i=0, count=0):
|
|
if not cal:
|
|
return validateCalendar(calId, i, count)
|
|
return (normalizeCalendarId(calId, user), cal)
|
|
|
|
CALENDAR_ACL_ROLES_MAP = {
|
|
'editor': 'writer',
|
|
'freebusy': 'freeBusyReader',
|
|
'freebusyreader': 'freeBusyReader',
|
|
'owner': 'owner',
|
|
'read': 'reader',
|
|
'reader': 'reader',
|
|
'writer': 'writer',
|
|
'none': 'none',
|
|
}
|
|
|
|
ACL_SCOPE_CHOICES = ['default', 'user', 'group', 'domain'] # default must be first element
|
|
|
|
def getACLScope():
|
|
scopeType, scopeValue = getChoiceAndValue(Cmd.OB_ACL_SCOPE, ACL_SCOPE_CHOICES[1:], ':')
|
|
if scopeType:
|
|
if scopeType != 'domain':
|
|
scopeValue = normalizeEmailAddressOrUID(scopeValue, noUid=True)
|
|
else:
|
|
scopeValue = scopeValue.lower()
|
|
return (scopeType, scopeValue)
|
|
scopeType = getChoice(ACL_SCOPE_CHOICES, defaultChoice='user')
|
|
if scopeType == 'domain':
|
|
entity = getString(Cmd.OB_DOMAIN_NAME, optional=True)
|
|
if entity:
|
|
scopeValue = entity.lower()
|
|
else:
|
|
scopeValue = GC.Values[GC.DOMAIN]
|
|
elif scopeType != 'default':
|
|
scopeValue = getEmailAddress(noUid=True)
|
|
else:
|
|
scopeValue = None
|
|
return (scopeType, scopeValue)
|
|
|
|
def getCalendarACLScope():
|
|
scopeType, scopeValue = getACLScope()
|
|
if scopeType != 'default':
|
|
return {'list': [f'{scopeType}:{scopeValue}'], 'dict': None}
|
|
return {'list': [scopeType], 'dict': None}
|
|
|
|
def getCalendarSiteACLScopeEntity():
|
|
ACLScopeEntity = {'list': getEntityList(Cmd.OB_ACL_SCOPE_ENTITY), 'dict': None}
|
|
if isinstance(ACLScopeEntity['list'], dict):
|
|
ACLScopeEntity['dict'] = ACLScopeEntity['list']
|
|
return ACLScopeEntity
|
|
|
|
def getCalendarACLSendNotifications():
|
|
return getBoolean() if checkArgumentPresent('sendnotifications') else True
|
|
|
|
def getCalendarCreateUpdateACLsOptions(getScopeEntity):
|
|
role = getChoice(CALENDAR_ACL_ROLES_MAP, mapChoice=True)
|
|
ACLScopeEntity = getCalendarSiteACLScopeEntity() if getScopeEntity else getCalendarACLScope()
|
|
sendNotifications = getCalendarACLSendNotifications()
|
|
checkForExtraneousArguments()
|
|
return (role, ACLScopeEntity, sendNotifications)
|
|
|
|
def getCalendarDeleteACLsOptions(getScopeEntity):
|
|
rolesMap = CALENDAR_ACL_ROLES_MAP.copy()
|
|
rolesMap['id'] = 'id'
|
|
role = getChoice(rolesMap, defaultChoice=None, mapChoice=True)
|
|
ACLScopeEntity = getCalendarSiteACLScopeEntity() if getScopeEntity else getCalendarACLScope()
|
|
checkForExtraneousArguments()
|
|
return (role, ACLScopeEntity)
|
|
|
|
def _normalizeCalIdGetRuleIds(origUser, user, origCal, calId, j, jcount, ACLScopeEntity, showAction=True):
|
|
if ACLScopeEntity['dict']:
|
|
if origUser:
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
ruleIds = ACLScopeEntity['dict'][calId]
|
|
else:
|
|
ruleIds = ACLScopeEntity['dict'][origUser][calId]
|
|
else:
|
|
ruleIds = ACLScopeEntity['dict'][calId]
|
|
else:
|
|
ruleIds = ACLScopeEntity['list']
|
|
calId, cal = getNormalizedCalIdCal(origCal, calId, user, j, jcount)
|
|
if not cal:
|
|
return (calId, cal, None, 0)
|
|
kcount = len(ruleIds)
|
|
if kcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if showAction:
|
|
entityPerformActionNumItems([Ent.CALENDAR, calId], kcount, Ent.CALENDAR_ACL, j, jcount)
|
|
return (calId, cal, ruleIds, kcount)
|
|
|
|
def _processCalendarACLs(cal, function, entityType, calId, j, jcount, k, kcount, role, ruleId, sendNotifications):
|
|
result = True
|
|
if function == 'insert':
|
|
kwargs = {'body': makeRoleRuleIdBody(role, ruleId), 'fields': '', 'sendNotifications': sendNotifications}
|
|
elif function == 'patch':
|
|
kwargs = {'ruleId': ruleId, 'body': {'role': role}, 'fields': '', 'sendNotifications': sendNotifications}
|
|
else: # elif function == 'delete':
|
|
kwargs = {'ruleId': ruleId}
|
|
try:
|
|
callGAPI(cal.acl(), function,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_PARAMETER, GAPI.INVALID_SCOPE_VALUE,
|
|
GAPI.ILLEGAL_ACCESS_ROLE_FOR_DEFAULT, GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWNER_ACL,
|
|
GAPI.FORBIDDEN, GAPI.AUTH_ERROR, GAPI.CONDITION_NOT_MET],
|
|
calendarId=calId, **kwargs)
|
|
entityActionPerformed([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, role)], k, kcount)
|
|
except GAPI.notFound as e:
|
|
if not checkCalendarExists(cal, calId, j, jcount):
|
|
entityUnknownWarning(entityType, calId, j, jcount)
|
|
result = False
|
|
else:
|
|
entityActionFailedWarning([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, role)], str(e), k, kcount)
|
|
except (GAPI.invalid, GAPI.invalidParameter, GAPI.invalidScopeValue,
|
|
GAPI.illegalAccessRoleForDefault, GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnerAcl,
|
|
GAPI.forbidden, GAPI.authError, GAPI.conditionNotMet) as e:
|
|
entityActionFailedWarning([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, role)], str(e), k, kcount)
|
|
return result
|
|
|
|
def _createCalendarACLs(cal, entityType, calId, j, jcount, role, ruleIds, kcount, sendNotifications):
|
|
Ind.Increment()
|
|
k = 0
|
|
for ruleId in ruleIds:
|
|
k += 1
|
|
ruleId = normalizeRuleId(ruleId)
|
|
if not _processCalendarACLs(cal, 'insert', entityType, calId, j, jcount, k, kcount, role, ruleId, sendNotifications):
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def _doCalendarsCreateACLs(origUser, user, origCal, calIds, count, role, ACLScopeEntity, sendNotifications):
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal, ruleIds, jcount = _normalizeCalIdGetRuleIds(origUser, user, origCal, calId, i, count, ACLScopeEntity)
|
|
if jcount == 0:
|
|
continue
|
|
_createCalendarACLs(cal, Ent.CALENDAR, calId, i, count, role, ruleIds, jcount, sendNotifications)
|
|
|
|
# gam calendar <CalendarEntity> create <CalendarACLRole> <CalendarACLScope> [sendnotifications <Boolean>]
|
|
def doCalendarsCreateACL(calIds):
|
|
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(False)
|
|
_doCalendarsCreateACLs(None, None, None, calIds, len(calIds), role, ACLScopeEntity, sendNotifications)
|
|
|
|
# gam calendars <CalendarEntity> create acls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
|
|
def doCalendarsCreateACLs(calIds):
|
|
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
|
|
_doCalendarsCreateACLs(None, None, None, calIds, len(calIds), role, ACLScopeEntity, sendNotifications)
|
|
|
|
def _updateDeleteCalendarACLs(cal, function, entityType, calId, j, jcount, role, ruleIds, kcount, sendNotifications):
|
|
Ind.Increment()
|
|
k = 0
|
|
for ruleId in ruleIds:
|
|
k += 1
|
|
ruleId = normalizeRuleId(ruleId)
|
|
if not _processCalendarACLs(cal, function, entityType, calId, j, jcount, k, kcount, role, ruleId, sendNotifications):
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def _doUpdateDeleteCalendarACLs(origUser, user, origCal, function, calIds, count, ACLScopeEntity, role, sendNotifications):
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal, ruleIds, jcount = _normalizeCalIdGetRuleIds(origUser, user, origCal, calId, i, count, ACLScopeEntity)
|
|
if jcount == 0:
|
|
continue
|
|
_updateDeleteCalendarACLs(cal, function, Ent.CALENDAR, calId, i, count, role, ruleIds, jcount, sendNotifications)
|
|
|
|
# gam calendar <CalendarEntity> update <CalendarACLRole> <CalendarACLScope> [sendnotifications <Boolean>]
|
|
def doCalendarsUpdateACL(calIds):
|
|
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(False)
|
|
_doUpdateDeleteCalendarACLs(None, None, None, 'patch', calIds, len(calIds), ACLScopeEntity, role, sendNotifications)
|
|
|
|
# gam calendars <CalendarEntity> update acls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
|
|
def doCalendarsUpdateACLs(calIds):
|
|
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
|
|
_doUpdateDeleteCalendarACLs(None, None, None, 'patch', calIds, len(calIds), ACLScopeEntity, role, sendNotifications)
|
|
|
|
# gam calendar <CalendarEntity> delete [<CalendarACLRole>] <CalendarACLScope>
|
|
def doCalendarsDeleteACL(calIds):
|
|
role, ACLScopeEntity = getCalendarDeleteACLsOptions(False)
|
|
_doUpdateDeleteCalendarACLs(None, None, None, 'delete', calIds, len(calIds), ACLScopeEntity, role, False)
|
|
|
|
# gam calendars <CalendarEntity> delete acls <CalendarACLScopeEntity>
|
|
def doCalendarsDeleteACLs(calIds):
|
|
role, ACLScopeEntity = getCalendarDeleteACLsOptions(True)
|
|
_doUpdateDeleteCalendarACLs(None, None, None, 'delete', calIds, len(calIds), ACLScopeEntity, role, False)
|
|
|
|
def _showCalendarACL(user, entityType, calId, acl, k, kcount, FJQC):
|
|
if FJQC.formatJSON:
|
|
if entityType == Ent.CALENDAR:
|
|
if user:
|
|
printLine(json.dumps(cleanJSON({'primaryEmail': user, 'calendarId': calId, 'acl': acl}),
|
|
ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
printLine(json.dumps(cleanJSON({'calendarId': calId, 'acl': acl}),
|
|
ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
printLine(json.dumps(cleanJSON({'resourceId': user, 'resourceEmail': calId, 'acl': acl}),
|
|
ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
printKeyValueListWithCount(ACLRuleKeyValueList(acl), k, kcount)
|
|
|
|
def _infoCalendarACLs(cal, user, entityType, calId, j, jcount, ruleIds, kcount, FJQC):
|
|
Ind.Increment()
|
|
k = 0
|
|
for ruleId in ruleIds:
|
|
k += 1
|
|
ruleId = normalizeRuleId(ruleId)
|
|
try:
|
|
result = callGAPI(cal.acl(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_SCOPE_VALUE, GAPI.FORBIDDEN, GAPI.AUTH_ERROR],
|
|
calendarId=calId, ruleId=ruleId, fields='id,role,scope')
|
|
_showCalendarACL(user, entityType, calId, result, k, kcount, FJQC)
|
|
except (GAPI.notFound, GAPI.invalid) as e:
|
|
if not checkCalendarExists(cal, calId, j, jcount):
|
|
entityUnknownWarning(entityType, calId, j, jcount)
|
|
break
|
|
entityActionFailedWarning([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, None)], str(e), k, kcount)
|
|
except (GAPI.invalidScopeValue, GAPI.forbidden, GAPI.authError) as e:
|
|
entityActionFailedWarning([entityType, calId, Ent.CALENDAR_ACL, formatACLScopeRole(ruleId, None)], str(e), k, kcount)
|
|
Ind.Decrement()
|
|
|
|
def _doInfoCalendarACLs(origUser, user, origCal, calIds, count, ACLScopeEntity, FJQC):
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal, ruleIds, jcount = _normalizeCalIdGetRuleIds(origUser, user, origCal, calId, i, count, ACLScopeEntity, showAction=not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
_infoCalendarACLs(cal, user, Ent.CALENDAR, calId, i, count, ruleIds, jcount, FJQC)
|
|
|
|
def _getCalendarInfoACLOptions():
|
|
return FormatJSONQuoteChar(formatJSONOnly=True)
|
|
|
|
# gam calendars <CalendarEntity> info acl|acls <CalendarACLScopeEntity>
|
|
# [formatjson]
|
|
def doCalendarsInfoACLs(calIds):
|
|
ACLScopeEntity = getCalendarSiteACLScopeEntity()
|
|
FJQC = _getCalendarInfoACLOptions()
|
|
_doInfoCalendarACLs(None, None, None, calIds, len(calIds), ACLScopeEntity, FJQC)
|
|
|
|
def _printShowCalendarACLs(cal, user, entityType, calId, i, count, csvPF, FJQC, noSelfOwner, addCSVData):
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.CALENDAR_ACL, calId, i, count)
|
|
try:
|
|
acls = callGAPIpages(cal.acl(), 'list', 'items',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.AUTH_ERROR],
|
|
calendarId=calId, fields='nextPageToken,items(id,role,scope)')
|
|
except (GAPI.forbidden, GAPI.authError) as e:
|
|
entityActionFailedWarning([entityType, calId], str(e), i, count)
|
|
return
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(entityType, calId, i, count)
|
|
return
|
|
jcount = len(acls)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
if not noSelfOwner:
|
|
entityPerformActionNumItems([entityType, calId], jcount, Ent.CALENDAR_ACL, i, count)
|
|
else:
|
|
entityPerformActionModifierNumItems([entityType, calId], Msg.MAXIMUM_OF, jcount, Ent.CALENDAR_ACL, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for rule in acls:
|
|
j += 1
|
|
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == calId:
|
|
continue
|
|
_showCalendarACL(user, entityType, calId, rule, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
if entityType == Ent.CALENDAR:
|
|
if acls:
|
|
for rule in acls:
|
|
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == calId:
|
|
continue
|
|
row = {'calendarId': calId}
|
|
if user:
|
|
row['primaryEmail'] = user
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
flattenJSON(rule, flattened=row)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'calendarId': calId, 'JSON': json.dumps(cleanJSON(rule),
|
|
ensure_ascii=False, sort_keys=False)}
|
|
if user:
|
|
row['primaryEmail'] = user
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRowNoFilter(row)
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT] and user:
|
|
csvPF.WriteRowNoFilter({'calendarId': calId, 'primaryEmail': user})
|
|
else: # Ent.RESOURCE_CALENDAR
|
|
for rule in acls:
|
|
if noSelfOwner and rule['role'] == 'owner' and rule['scope']['value'] == calId:
|
|
continue
|
|
row = {'resourceId': user, 'resourceEmail': calId}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
flattenJSON(rule, flattened=row)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'resourceId': user, 'resourceEmail': calId, 'JSON': json.dumps(cleanJSON(rule),
|
|
ensure_ascii=False, sort_keys=False)}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
def _getCalendarPrintShowACLOptions(titles):
|
|
csvPF = CSVPrintFile(titles, 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
noSelfOwner = False
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'noselfowner':
|
|
noSelfOwner = True
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF:
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles(sorted(addCSVData.keys()))
|
|
csvPF.MoveJSONTitlesToEnd(['JSON'])
|
|
csvPF.SetSortAllTitles()
|
|
return (csvPF, FJQC, noSelfOwner, addCSVData)
|
|
|
|
# gam calendars <CalendarEntity> print acls [todrive <ToDriveAttribute>*]
|
|
# [noselfowner] (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam calendars <CalendarEntity> show acls
|
|
# [noselfowner]
|
|
# [formatjson]
|
|
# gam calendar <CalendarEntity> printacl [todrive <ToDriveAttribute>*]
|
|
# [noselfowner] (addcsvdata <FieldName> <String>)*
|
|
# [formatjson]
|
|
# gam calendar <CalendarEntity> showacl
|
|
# [noselfowner]
|
|
# [formatjson]
|
|
def doCalendarsPrintShowACLs(calIds):
|
|
csvPF, FJQC, noSelfOwner, addCSVData = _getCalendarPrintShowACLOptions(['calendarId'])
|
|
count = len(calIds)
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal = validateCalendar(calId, i, count)
|
|
if not cal:
|
|
continue
|
|
_printShowCalendarACLs(cal, None, Ent.CALENDAR, calId, i, count, csvPF, FJQC, noSelfOwner, addCSVData)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Calendar ACLs')
|
|
|
|
EVENT_TYPE_BIRTHDAY = 'birthday'
|
|
EVENT_TYPE_DEFAULT = 'default'
|
|
EVENT_TYPE_FOCUSTIME = 'focusTime'
|
|
EVENT_TYPE_FROMGMAIL = 'fromGmail'
|
|
EVENT_TYPE_OUTOFOFFICE = 'outOfOffice'
|
|
EVENT_TYPE_WORKINGLOCATION = 'workingLocation'
|
|
|
|
EVENT_TYPES_CHOICE_MAP = {
|
|
'birthday': EVENT_TYPE_BIRTHDAY,
|
|
'default': EVENT_TYPE_DEFAULT,
|
|
'focustime': EVENT_TYPE_FOCUSTIME,
|
|
'fromgmail': EVENT_TYPE_FROMGMAIL,
|
|
'outofoffice': EVENT_TYPE_OUTOFOFFICE,
|
|
'workinglocation': EVENT_TYPE_WORKINGLOCATION,
|
|
}
|
|
|
|
EVENT_TYPE_PROPERTIES_NAME_MAP = {
|
|
EVENT_TYPE_DEFAULT: None,
|
|
EVENT_TYPE_FOCUSTIME: f'{EVENT_TYPE_FOCUSTIME}Properties',
|
|
EVENT_TYPE_FROMGMAIL: None,
|
|
EVENT_TYPE_OUTOFOFFICE: f'{EVENT_TYPE_OUTOFOFFICE}Properties',
|
|
EVENT_TYPE_WORKINGLOCATION: f'{EVENT_TYPE_WORKINGLOCATION}Properties',
|
|
}
|
|
|
|
EVENT_TYPE_ENTITY_MAP = {
|
|
EVENT_TYPE_BIRTHDAY: Ent.EVENT_BIRTHDAY,
|
|
EVENT_TYPE_DEFAULT: None,
|
|
EVENT_TYPE_FOCUSTIME: Ent.EVENT_FOCUSTIME,
|
|
EVENT_TYPE_FROMGMAIL: None,
|
|
EVENT_TYPE_OUTOFOFFICE: Ent.EVENT_OUTOFOFFICE,
|
|
EVENT_TYPE_WORKINGLOCATION: Ent.EVENT_WORKINGLOCATION,
|
|
}
|
|
|
|
def _getEventTypes():
|
|
typesList = []
|
|
for field in _getFieldsList():
|
|
if field in EVENT_TYPES_CHOICE_MAP:
|
|
addFieldToFieldsList(field, EVENT_TYPES_CHOICE_MAP, typesList)
|
|
else:
|
|
invalidChoiceExit(field, EVENT_TYPES_CHOICE_MAP, True)
|
|
# return ','.join(typesList)
|
|
return typesList
|
|
|
|
LIST_EVENTS_DISPLAY_PROPERTIES = {
|
|
'alwaysincludeemail': ('alwaysIncludeEmail', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
|
|
'icaluid': ('iCalUID', {GC.VAR_TYPE: GC.TYPE_STRING}),
|
|
'maxattendees': ('maxAttendees', {GC.VAR_TYPE: GC.TYPE_INTEGER, GC.VAR_LIMITS: (1, None)}),
|
|
'orderby': ('orderBy', {GC.VAR_TYPE: GC.TYPE_CHOICE, 'choices': {'starttime': 'startTime', 'updated': 'updated'}}),
|
|
'timezone': ('timeZone', {GC.VAR_TYPE: GC.TYPE_STRING}),
|
|
}
|
|
|
|
LIST_EVENTS_SELECT_PROPERTIES = {
|
|
'after': ('timeMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
|
|
'before': ('timeMax', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
|
|
'endtime': ('timeMax', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
|
|
'includedeleted': ('showDeleted', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
|
|
'includehidden': ('showHiddenInvitations', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
|
|
'privateextendedproperty': ('privateExtendedProperty', {GC.VAR_TYPE: GC.TYPE_STRING}),
|
|
'sharedextendedproperty': ('sharedExtendedProperty', {GC.VAR_TYPE: GC.TYPE_STRING}),
|
|
'showdeletedevents': ('showDeleted', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
|
|
'showhiddeninvitations': ('showHiddenInvitations', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
|
|
'singleevents': ('singleEvents', {GC.VAR_TYPE: GC.TYPE_BOOLEAN}),
|
|
'starttime': ('timeMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
|
|
'timemax': ('timeMax', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
|
|
'timemin': ('timeMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
|
|
'updated': ('updatedMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
|
|
'updatedmin': ('updatedMin', {GC.VAR_TYPE: GC.TYPE_DATETIME}),
|
|
'eventtype': ('eventTypes', {GC.VAR_TYPE: GC.TYPE_CHOICE_LIST}),
|
|
'eventtypes': ('eventTypes', {GC.VAR_TYPE: GC.TYPE_CHOICE_LIST}),
|
|
}
|
|
|
|
LIST_EVENTS_MATCH_FIELDS = {
|
|
'attendees': ['attendees', 'email'],
|
|
'attendeesonlydomainlist': ['attendees', 'onlydomainlist'],
|
|
'attendeesdomainlist': ['attendees', 'domainlist'],
|
|
'attendeesnotdomainlist': ['attendees', 'notdomainlist'],
|
|
'attendeespattern': ['attendees', 'match'],
|
|
'attendeesstatus': ['attendees', 'status'],
|
|
'description': ['description'],
|
|
'hangoutlink': ['hangoutLink'],
|
|
'location': ['location'],
|
|
'summary': ['summary'],
|
|
'creatorname': ['creator', 'displayName'],
|
|
'creatoremail': ['creator', 'email'],
|
|
'organizername': ['organizer', 'displayName'],
|
|
'organizeremail': ['organizer', 'email'],
|
|
'organizerself': ['organizer', 'self'],
|
|
'organisername': ['organizer', 'displayName'],
|
|
'organiseremail': ['organizer', 'email'],
|
|
'organiserself': ['organizer', 'self'],
|
|
'status': ['status'],
|
|
'transparency': ['transparency'],
|
|
'visibility': ['visibility'],
|
|
}
|
|
|
|
def _getCalendarListEventsProperty(myarg, attributes, kwargs):
|
|
attrName, attribute = attributes.get(myarg, (None, None))
|
|
if not attrName:
|
|
return False
|
|
attrType = attribute[GC.VAR_TYPE]
|
|
if attrType == GC.TYPE_BOOLEAN:
|
|
kwargs[attrName] = True
|
|
elif attrType == GC.TYPE_STRING:
|
|
kwargs[attrName] = getString(Cmd.OB_STRING)
|
|
elif attrType == GC.TYPE_CHOICE:
|
|
kwargs[attrName] = getChoice(attribute['choices'], mapChoice=True)
|
|
elif attrType == GC.TYPE_DATETIME:
|
|
kwargs[attrName] = getTimeOrDeltaFromNow()
|
|
elif attrType == GC.TYPE_INTEGER:
|
|
minVal, maxVal = attribute[GC.VAR_LIMITS]
|
|
kwargs[attrName] = getInteger(minVal=minVal, maxVal=maxVal)
|
|
else: # elif attrType == GC.TYPE_CHOICE_LIST:
|
|
if attrName == 'eventTypes':
|
|
kwargs[attrName] = _getEventTypes()
|
|
return True
|
|
|
|
def _getCalendarListEventsDisplayProperty(myarg, calendarEventEntity):
|
|
return _getCalendarListEventsProperty(myarg, LIST_EVENTS_DISPLAY_PROPERTIES, calendarEventEntity['kwargs'])
|
|
|
|
def initCalendarEventEntity():
|
|
return {'list': [], 'queries': [], 'kwargs': {}, 'dict': None,
|
|
'matches': [], 'maxinstances': -1, 'countsOnly': False, 'showDayOfWeek': False}
|
|
|
|
def getCalendarEventEntity():
|
|
calendarEventEntity = initCalendarEventEntity()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'event', 'events'}:
|
|
entitySelector = getEntitySelector()
|
|
if entitySelector:
|
|
entityList = getEntitySelection(entitySelector, False)
|
|
if isinstance(entityList, dict):
|
|
calendarEventEntity['dict'] = entityList
|
|
else:
|
|
calendarEventEntity['list'] = entityList
|
|
else:
|
|
calendarEventEntity['list'].extend(convertEntityToList(getString(Cmd.OB_EVENT_ID)))
|
|
elif myarg in {'id', 'eventid'}:
|
|
calendarEventEntity['list'].append(getString(Cmd.OB_EVENT_ID))
|
|
elif myarg in {'q', 'query', 'eventquery'}:
|
|
calendarEventEntity['queries'].append(getString(Cmd.OB_QUERY))
|
|
elif myarg == 'matchfield':
|
|
matchField = getChoice(LIST_EVENTS_MATCH_FIELDS, mapChoice=True)
|
|
if matchField[0] == 'organizer' and matchField[1] == 'self':
|
|
calendarEventEntity['matches'].append((matchField, getBoolean()))
|
|
elif matchField[0] != 'attendees' or matchField[1] == 'match':
|
|
calendarEventEntity['matches'].append((matchField, getREPattern(re.IGNORECASE)))
|
|
elif matchField[0] == 'attendees' and matchField[1] in {'onlydomainlist', 'domainlist', 'notdomainlist'}:
|
|
calendarEventEntity['matches'].append((matchField, set(getString(Cmd.OB_DOMAIN_NAME_LIST).replace(',', ' ').split())))
|
|
elif matchField[1] == 'email':
|
|
calendarEventEntity['matches'].append((matchField, getNormalizedEmailAddressEntity()))
|
|
else: #status
|
|
calendarEventEntity['matches'].append((matchField,
|
|
getChoice(CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP, defaultChoice=False, mapChoice=True),
|
|
getChoice(CALENDAR_ATTENDEE_STATUS_CHOICE_MAP, defaultChoice='needsAction', mapChoice=True),
|
|
getNormalizedEmailAddressEntity()))
|
|
elif myarg == 'maxinstances':
|
|
calendarEventEntity['maxinstances'] = getInteger(minVal=-1)
|
|
elif _getCalendarListEventsProperty(myarg, LIST_EVENTS_SELECT_PROPERTIES, calendarEventEntity['kwargs']):
|
|
pass
|
|
else:
|
|
Cmd.Backup()
|
|
break
|
|
return calendarEventEntity
|
|
|
|
CALENDAR_EVENT_SENDUPDATES_CHOICE_MAP = {'all': 'all', 'externalonly': 'externalOnly', 'none': 'none'}
|
|
|
|
def _getCalendarSendUpdates(myarg, parameters):
|
|
if myarg == 'sendnotifications':
|
|
parameters['sendUpdates'] = 'all' if getBoolean() else 'none'
|
|
elif myarg == 'notifyattendees':
|
|
parameters['sendUpdates'] = 'all'
|
|
elif myarg == 'sendupdates':
|
|
parameters['sendUpdates'] = getChoice(CALENDAR_EVENT_SENDUPDATES_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
CALENDAR_MIN_COLOR_INDEX = 1
|
|
CALENDAR_MAX_COLOR_INDEX = 24
|
|
|
|
CALENDAR_EVENT_MIN_COLOR_INDEX = 1
|
|
CALENDAR_EVENT_MAX_COLOR_INDEX = 11
|
|
|
|
CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP = {
|
|
'optional': True,
|
|
'required': False,
|
|
'true': True,
|
|
'false': False,
|
|
'': None,
|
|
}
|
|
CALENDAR_ATTENDEE_STATUS_CHOICE_MAP = {
|
|
'accepted': 'accepted',
|
|
'declined': 'declined',
|
|
'needsaction': 'needsAction',
|
|
'tentative': 'tentative',
|
|
'': None,
|
|
}
|
|
CALENDAR_EVENT_STATUS_CHOICES = ['confirmed', 'tentative', 'cancelled']
|
|
CALENDAR_EVENT_TRANSPARENCY_CHOICES = ['opaque', 'transparent']
|
|
CALENDAR_EVENT_VISIBILITY_CHOICES = ['default', 'public', 'private', 'confedential']
|
|
|
|
EVENT_JSON_CLEAR_FIELDS = ['created', 'creator', 'endTimeUpspecified', 'hangoutLink', 'htmlLink', 'eventType',
|
|
'privateCopy', 'locked', 'recurringEventId', 'updated']
|
|
EVENT_JSON_INSERT_CLEAR_FIELDS = ['iCalUID', 'id', 'organizer']
|
|
EVENT_JSON_UPDATE_CLEAR_FIELDS = ['iCalUID', 'id', 'organizer']
|
|
EVENT_JSON_SUBFIELD_CLEAR_FIELDS = {
|
|
'attendees': ['id', 'organizer', 'self'],
|
|
'attachments': ['fileId', 'iconLink', 'mimeType', 'title'],
|
|
'organizer': ['id', 'self'],
|
|
}
|
|
EVENT_JSONATTENDEES_SUBFIELD_CLEAR_FIELDS = {
|
|
'attendees': ['id', 'organizer', 'self'],
|
|
}
|
|
|
|
def _getCalendarEventAttribute(myarg, body, parameters, function):
|
|
def clearJSONfields(body, clearFields):
|
|
for field in clearFields:
|
|
body.pop(field, None)
|
|
|
|
def clearJSONsubfields(body, clearFields):
|
|
for field, subfields in iter(clearFields.items()):
|
|
if field in body:
|
|
if isinstance(body[field], list):
|
|
for item in body[field]:
|
|
for subfield in subfields:
|
|
item.pop(subfield, None)
|
|
else:
|
|
for subfield in subfields:
|
|
body.pop(subfield, None)
|
|
|
|
cd = None
|
|
if function == 'insert' and myarg in {'id', 'eventid'}:
|
|
body['id'] = getEventID()
|
|
elif function == 'import' and myarg == 'icaluid':
|
|
body['iCalUID'] = getString(Cmd.OB_ICALUID)
|
|
elif myarg == 'description':
|
|
body['description'] = getStringWithCRsNLs()
|
|
elif function == 'update' and myarg == 'replacedescription':
|
|
parameters['replaceDescription'].append(getREPatternSubstitution(re.IGNORECASE))
|
|
elif myarg == 'location':
|
|
body['location'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'source':
|
|
body['source'] = {'title': getString(Cmd.OB_STRING), 'url': getString(Cmd.OB_URL)}
|
|
elif myarg == 'summary':
|
|
body['summary'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'start', 'starttime'}:
|
|
body['start'] = getEventTime()
|
|
elif myarg in {'originalstart', 'originalstarttime'}:
|
|
body['originalStart'] = getEventTime()
|
|
elif myarg in {'end', 'endtime'}:
|
|
body['end'] = getEventTime()
|
|
elif myarg == 'allday':
|
|
body['start'] = body['end'] = {'date': getYYYYMMDD()}
|
|
elif myarg == 'range':
|
|
body['start'] = {'date': getYYYYMMDD()}
|
|
body['end'] = {'date': getYYYYMMDD()}
|
|
elif myarg == 'timerange':
|
|
body['start'] = {'dateTime': getTimeOrDeltaFromNow()}
|
|
body['end'] = {'dateTime': getTimeOrDeltaFromNow()}
|
|
elif myarg == 'birthday':
|
|
body['eventType'] = EVENT_TYPE_BIRTHDAY
|
|
body['visibility'] = 'private'
|
|
body['transparency'] = 'transparent'
|
|
bday = getYYYYMMDD(returnDateTime=True)
|
|
body['start'] = body['end'] = {'date': bday.strftime(YYYYMMDD_FORMAT)}
|
|
if bday.month != 2 or bday.day != 29:
|
|
body['recurrence'] = ['RRULE:FREQ=YEARLY']
|
|
else:
|
|
body['recurrence'] = ['RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=-1']
|
|
elif myarg == 'attachment':
|
|
body.setdefault('attachments', [])
|
|
body['attachments'].append({'title': getString(Cmd.OB_STRING), 'fileUrl': getString(Cmd.OB_URL)})
|
|
elif function == 'update' and myarg == 'clearattachments':
|
|
body['attachments'] = []
|
|
elif myarg in {'hangoutsmeet', 'googlemeet'}:
|
|
body['conferenceData'] = {'createRequest': {'conferenceSolutionKey': {'type': 'hangoutsMeet'}, 'requestId': f'{str(uuid.uuid4())}'}}
|
|
elif function == 'update' and myarg in {'clearhangoutsmeet', 'cleargooglemeet'}:
|
|
body['conferenceData'] = None
|
|
elif myarg == 'recurrence':
|
|
body.setdefault('recurrence', [])
|
|
body['recurrence'].append(getString(Cmd.OB_RECURRENCE))
|
|
elif myarg == 'timezone':
|
|
parameters['timeZone'] = getString(Cmd.OB_STRING)
|
|
elif function == 'update' and myarg == 'replacemode':
|
|
parameters['replaceMode'] = True
|
|
elif function == 'update' and myarg == 'clearattendees':
|
|
parameters['clearAttendees'] = True
|
|
elif function == 'update' and myarg == 'removeattendee':
|
|
parameters['removeAttendees'].add(getEmailAddress(noUid=True))
|
|
elif function == 'update' and myarg == 'selectremoveattendees':
|
|
_, attendeeList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
for attendee in attendeeList:
|
|
parameters['removeAttendees'].add(normalizeEmailAddressOrUID(attendee, noUid=True))
|
|
elif myarg == 'attendee':
|
|
parameters['attendees'].append({'email': getEmailAddress(noUid=True)})
|
|
elif myarg == 'optionalattendee':
|
|
parameters['attendees'].append({'email': getEmailAddress(noUid=True), 'optional': True})
|
|
elif myarg in {'attendeestatus', 'selectattendees'}:
|
|
optional = getChoice(CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP, defaultChoice=None, mapChoice=True)
|
|
responseStatus = getChoice(CALENDAR_ATTENDEE_STATUS_CHOICE_MAP, defaultChoice=None, mapChoice=True)
|
|
if myarg == 'attendeestatus':
|
|
attendeeList = [getEmailAddress(noUid=True)]
|
|
else:
|
|
_, attendeeList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
for attendee in attendeeList:
|
|
addAttendee = {'email': normalizeEmailAddressOrUID(attendee, noUid=True)}
|
|
if optional is not None:
|
|
addAttendee['optional'] = optional
|
|
if responseStatus is not None:
|
|
addAttendee['responseStatus'] = responseStatus
|
|
parameters['attendees'].append(addAttendee)
|
|
elif function == 'update' and myarg == 'clearresources':
|
|
parameters['clearResources'] = True
|
|
elif myarg == 'resource':
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
parameters['attendees'].append({'email': _validateResourceId(cd, getString(Cmd.OB_RESOURCE_ID), 0, 0, True),
|
|
'responseStatus': 'accepted', 'resource': True})
|
|
elif myarg == 'removeresource':
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
parameters['removeAttendees'].add(_validateResourceId(cd, getString(Cmd.OB_RESOURCE_ID), 0, 0, True))
|
|
elif myarg == 'json':
|
|
jsonData = getJSON(EVENT_JSON_CLEAR_FIELDS)
|
|
if function == 'insert':
|
|
body.update(jsonData)
|
|
clearJSONfields(body, EVENT_JSON_INSERT_CLEAR_FIELDS)
|
|
elif function == 'import':
|
|
if 'id' in jsonData:
|
|
jsonData['iCalUID'] = jsonData.pop('id')
|
|
body.update(jsonData)
|
|
elif function == 'update':
|
|
if 'event' in jsonData and 'attendees' in jsonData['event']:
|
|
parameters['attendees'].extend(jsonData['event'].pop('attendees'))
|
|
clearJSONsubfields(parameters, EVENT_JSONATTENDEES_SUBFIELD_CLEAR_FIELDS)
|
|
body.update(jsonData)
|
|
clearJSONfields(body, EVENT_JSON_UPDATE_CLEAR_FIELDS)
|
|
clearJSONsubfields(body, EVENT_JSON_SUBFIELD_CLEAR_FIELDS)
|
|
if ('conferenceData' in body and body['conferenceData'] and
|
|
'createRequest' in body['conferenceData'] and
|
|
'status' in body['conferenceData']['createRequest']):
|
|
body['conferenceData']['createRequest']['status'].pop('statusCode', None)
|
|
elif myarg == 'jsonattendees':
|
|
jsonData = getJSON([])
|
|
if 'event' in jsonData and 'attendees' in jsonData['event']:
|
|
parameters['attendees'].extend(jsonData['event']['attendees'])
|
|
elif 'attendees' in jsonData:
|
|
parameters['attendees'].extend(jsonData['attendees'])
|
|
clearJSONsubfields(parameters, EVENT_JSONATTENDEES_SUBFIELD_CLEAR_FIELDS)
|
|
elif function != 'import' and _getCalendarSendUpdates(myarg, parameters):
|
|
pass
|
|
elif myarg == 'anyonecanaddself':
|
|
body['anyoneCanAddSelf'] = getBoolean()
|
|
elif myarg == 'guestscaninviteothers':
|
|
body['guestsCanInviteOthers'] = getBoolean()
|
|
elif myarg == 'guestscantinviteothers':
|
|
body['guestsCanInviteOthers'] = False
|
|
elif myarg == 'guestscanmodify':
|
|
body['guestsCanModify'] = getBoolean()
|
|
elif myarg == 'guestscanseeotherguests':
|
|
body['guestsCanSeeOtherGuests'] = getBoolean()
|
|
elif myarg == 'guestscantseeotherguests':
|
|
body['guestsCanSeeOtherGuests'] = False
|
|
elif myarg == 'status':
|
|
body['status'] = getChoice(CALENDAR_EVENT_STATUS_CHOICES)
|
|
elif myarg == 'tentative':
|
|
body['status'] = 'tentative'
|
|
elif myarg == 'transparency':
|
|
body['transparency'] = getChoice(CALENDAR_EVENT_TRANSPARENCY_CHOICES)
|
|
elif myarg == 'available':
|
|
body['transparency'] = 'transparent'
|
|
elif myarg == 'visibility':
|
|
body['visibility'] = getChoice(CALENDAR_EVENT_VISIBILITY_CHOICES)
|
|
elif myarg in {'color', 'colour'}:
|
|
body['colorId'] = getChoice(CALENDAR_EVENT_COLOR_MAP, mapChoice=True)
|
|
elif myarg in {'colorindex', 'colorid', 'colourindex', 'colourid'}:
|
|
body['colorId'] = getInteger(CALENDAR_EVENT_MIN_COLOR_INDEX, CALENDAR_EVENT_MAX_COLOR_INDEX)
|
|
elif myarg == 'noreminders':
|
|
body['reminders'] = {'overrides': [], 'useDefault': False}
|
|
elif myarg == 'reminder':
|
|
body.setdefault('reminders', {'overrides': [], 'useDefault': False})
|
|
body['reminders']['overrides'].append(getCalendarReminder())
|
|
body['reminders']['useDefault'] = False
|
|
elif myarg == 'sequence':
|
|
body['sequence'] = getInteger(minVal=0)
|
|
elif myarg == 'privateproperty':
|
|
body.setdefault('extendedProperties', {})
|
|
body['extendedProperties'].setdefault('private', {})
|
|
key = getString(Cmd.OB_PROPERTY_KEY)
|
|
body['extendedProperties']['private'][key] = getString(Cmd.OB_PROPERTY_VALUE, minLen=0)
|
|
elif myarg == 'sharedproperty':
|
|
body.setdefault('extendedProperties', {})
|
|
body['extendedProperties'].setdefault('shared', {})
|
|
key = getString(Cmd.OB_PROPERTY_KEY)
|
|
body['extendedProperties']['shared'][key] = getString(Cmd.OB_PROPERTY_VALUE, minLen=0)
|
|
elif function == 'update' and myarg == 'clearprivateproperty':
|
|
body.setdefault('extendedProperties', {})
|
|
body['extendedProperties'].setdefault('private', {})
|
|
body['extendedProperties']['private'][getString(Cmd.OB_PROPERTY_KEY)] = None
|
|
elif function == 'update' and myarg == 'clearsharedproperty':
|
|
body.setdefault('extendedProperties', {})
|
|
body['extendedProperties'].setdefault('shared', {})
|
|
body['extendedProperties']['shared'][getString(Cmd.OB_PROPERTY_KEY)] = None
|
|
elif function == 'import' and myarg in {'organizername', 'organisername'}:
|
|
body.setdefault('organizer', {})
|
|
body['organizer']['displayName'] = getString(Cmd.OB_NAME)
|
|
elif function == 'import' and myarg in {'organizeremail', 'organiseremail'}:
|
|
body.setdefault('organizer', {})
|
|
body['organizer']['email'] = getEmailAddress(noUid=True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _getEventMatchFields(calendarEventEntity, fieldsList):
|
|
for match in calendarEventEntity['matches']:
|
|
if match[0][0] != 'attendees':
|
|
fieldsList.append(match[0][0])
|
|
else:
|
|
fieldsList.append('attendees/email')
|
|
if match[0][1] == 'status':
|
|
fieldsList.extend(['attendees/optional', 'attendees/responseStatus'])
|
|
|
|
def _eventMatches(event, match):
|
|
if match[0][0] != 'attendees':
|
|
eventAttr = event
|
|
for attr in match[0]:
|
|
eventAttr = eventAttr.get(attr, '')
|
|
if not eventAttr:
|
|
break
|
|
if match[0][0] == 'organizer' and match[0][1] == 'self':
|
|
return bool(eventAttr) == match[1]
|
|
if match[0][0] != 'hangoutLink':
|
|
return match[1].search(eventAttr) is not None
|
|
# vkj-przn-nvg or vkjprznnvg
|
|
return match[1].search(eventAttr) is not None or match[1].search(eventAttr.replace('-', '')) is not None
|
|
attendees = [attendee['email'] for attendee in event.get('attendees', []) if 'email' in attendee]
|
|
if not attendees:
|
|
return False
|
|
if match[0][1] == 'email':
|
|
for attendee in match[1]:
|
|
if attendee not in attendees:
|
|
return False
|
|
return True
|
|
if match[0][1] == 'match':
|
|
for attendee in attendees:
|
|
if match[1].search(attendee) is not None:
|
|
return True
|
|
return False
|
|
if match[0][1] == 'onlydomainlist':
|
|
for attendee in attendees:
|
|
_, domain = attendee.lower().split('@', 1)
|
|
if domain not in match[1]:
|
|
return False
|
|
return True
|
|
if match[0][1] == 'domainlist':
|
|
for attendee in attendees:
|
|
_, domain = attendee.lower().split('@', 1)
|
|
if domain in match[1]:
|
|
return True
|
|
return False
|
|
if match[0][1] == 'notdomainlist':
|
|
for attendee in attendees:
|
|
_, domain = attendee.lower().split('@', 1)
|
|
if domain not in match[1]:
|
|
return True
|
|
return False
|
|
# status
|
|
for matchEmail in match[3]:
|
|
for attendee in event['attendees']:
|
|
if 'email' in attendee and matchEmail == attendee['email']:
|
|
if attendee.get('optional', False) != match[1] or attendee.get('responseStatus') != match[2]:
|
|
return False
|
|
break
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _validateCalendarGetEventIDs(origUser, user, origCal, calId, j, jcount, calendarEventEntity, doIt=True, showAction=True):
|
|
if calendarEventEntity['dict']:
|
|
if origUser:
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
calEventIds = calendarEventEntity['dict'][calId]
|
|
else:
|
|
calEventIds = calendarEventEntity['dict'][origUser][calId]
|
|
else:
|
|
calEventIds = calendarEventEntity['dict'][calId]
|
|
else:
|
|
calEventIds = calendarEventEntity['list']
|
|
calId, cal = getNormalizedCalIdCal(origCal, calId, user, j, jcount)
|
|
if not cal:
|
|
return (calId, cal, None, 0)
|
|
if not calEventIds:
|
|
fieldsList = ['id']
|
|
_getEventMatchFields(calendarEventEntity, fieldsList)
|
|
fields = ','.join(fieldsList)
|
|
try:
|
|
eventIdsSet = set()
|
|
calEventIds = []
|
|
if len(calendarEventEntity['queries']) <= 1:
|
|
if len(calendarEventEntity['queries']) == 1:
|
|
calendarEventEntity['kwargs']['q'] = calendarEventEntity['queries'][0]
|
|
events = callGAPIpages(cal.events(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
calendarId=calId, fields=f'nextPageToken,items({fields})',
|
|
maxResults=GC.Values[GC.EVENT_MAX_RESULTS], **calendarEventEntity['kwargs'])
|
|
for event in events:
|
|
for match in calendarEventEntity['matches']:
|
|
if not _eventMatches(event, match):
|
|
break
|
|
else:
|
|
calEventIds.append(event['id'])
|
|
else:
|
|
for query in calendarEventEntity['queries']:
|
|
calendarEventEntity['kwargs']['q'] = query
|
|
events = callGAPIpages(cal.events(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
calendarId=calId, fields=f'nextPageToken,items({fields})',
|
|
maxResults=GC.Values[GC.EVENT_MAX_RESULTS], **calendarEventEntity['kwargs'])
|
|
for event in events:
|
|
for match in calendarEventEntity['matches']:
|
|
if not _eventMatches(event, match):
|
|
break
|
|
else:
|
|
eventId = event['id']
|
|
if eventId not in eventIdsSet:
|
|
calEventIds.append(eventId)
|
|
eventIdsSet.add(eventId)
|
|
kcount = len(calEventIds)
|
|
if kcount == 0:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.CALENDAR, calId], Ent.EVENT, kcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(Ent.EVENT)), j, jcount)
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (calId, cal, None, 0)
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
return (calId, cal, None, 0)
|
|
except (GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
return (calId, cal, None, 0)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, j, jcount)
|
|
return (calId, cal, None, 0)
|
|
else:
|
|
kcount = len(calEventIds)
|
|
if kcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not doIt:
|
|
if showAction:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.CALENDAR, calId], Ent.EVENT, kcount, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, j, jcount)
|
|
return (calId, cal, None, 0)
|
|
if showAction:
|
|
entityPerformActionNumItems([Ent.CALENDAR, calId], kcount, Ent.EVENT, j, jcount)
|
|
return (calId, cal, calEventIds, kcount)
|
|
|
|
def _validateCalendarGetEvents(origUser, user, origCal, calId, j, jcount, calendarEventEntity,
|
|
fieldsList, showAction):
|
|
if calendarEventEntity['dict']:
|
|
if origUser:
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
calEventIds = calendarEventEntity['dict'][calId]
|
|
else:
|
|
calEventIds = calendarEventEntity['dict'][origUser][calId]
|
|
else:
|
|
calEventIds = calendarEventEntity['dict'][calId]
|
|
else:
|
|
calEventIds = calendarEventEntity['list']
|
|
calId, cal = getNormalizedCalIdCal(origCal, calId, user, j, jcount)
|
|
if not cal:
|
|
return (calId, cal, [], 0)
|
|
eventIdsSet = set()
|
|
eventsList = []
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
ifields = getItemFieldsFromFieldsList('items', fieldsList)
|
|
try:
|
|
if not calEventIds:
|
|
if len(calendarEventEntity['queries']) <= 1:
|
|
if len(calendarEventEntity['queries']) == 1:
|
|
calendarEventEntity['kwargs']['q'] = calendarEventEntity['queries'][0]
|
|
events = callGAPIpages(cal.events(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
calendarId=calId, fields=ifields,
|
|
maxResults=GC.Values[GC.EVENT_MAX_RESULTS], **calendarEventEntity['kwargs'])
|
|
for event in events:
|
|
for match in calendarEventEntity['matches']:
|
|
if not _eventMatches(event, match):
|
|
break
|
|
else:
|
|
eventsList.append(event)
|
|
else:
|
|
for query in calendarEventEntity['queries']:
|
|
calendarEventEntity['kwargs']['q'] = query
|
|
events = callGAPIpages(cal.events(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
calendarId=calId, fields=ifields,
|
|
maxResults=GC.Values[GC.EVENT_MAX_RESULTS], **calendarEventEntity['kwargs'])
|
|
for event in events:
|
|
for match in calendarEventEntity['matches']:
|
|
if not _eventMatches(event, match):
|
|
break
|
|
else:
|
|
eventId = event['id']
|
|
if eventId not in eventIdsSet:
|
|
eventsList.append(event)
|
|
eventIdsSet.add(eventId)
|
|
else:
|
|
k = 0
|
|
for eventId in calEventIds:
|
|
k += 1
|
|
if eventId not in eventIdsSet:
|
|
eventsList.append(callGAPI(cal.events(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN],
|
|
calendarId=calId, eventId=eventId, fields=fields))
|
|
eventIdsSet.add(eventId)
|
|
kcount = len(eventsList)
|
|
if showAction:
|
|
entityPerformActionNumItems([Ent.CALENDAR, calId], kcount, Ent.EVENT, j, jcount)
|
|
if kcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (calId, cal, eventsList, kcount)
|
|
except (GAPI.notFound, GAPI.deleted) as e:
|
|
if not checkCalendarExists(cal, calId, j, jcount):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
else:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except (GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, j, jcount)
|
|
return (calId, cal, [], 0)
|
|
|
|
def _getCalendarCreateImportUpdateEventOptions(function, entityType):
|
|
body = {}
|
|
parameters = {'clearAttendees': False, 'replaceMode': False, 'clearResources': False,
|
|
'attendees': [], 'removeAttendees': set(),
|
|
'replaceDescription': [], 'sendUpdates': 'none',
|
|
'csvPF': None, 'FJQC': FormatJSONQuoteChar(None), 'showDayOfWeek': False}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'csv':
|
|
parameters['csvPF'] = CSVPrintFile(['primaryEmail', 'calendarId', 'id'] if entityType == Ent.USER else ['calendarId', 'id'], 'sortall', indexedTitles=EVENT_INDEXED_TITLES)
|
|
parameters['FJQC'].SetCsvPF(parameters['csvPF'])
|
|
elif parameters['csvPF'] and myarg == 'todrive':
|
|
parameters['csvPF'].GetTodriveParameters()
|
|
elif myarg == 'showdayofweek':
|
|
parameters['showDayOfWeek'] = True
|
|
elif _getCalendarEventAttribute(myarg, body, parameters, function):
|
|
pass
|
|
else:
|
|
parameters['FJQC'].GetFormatJSONQuoteChar(myarg, True)
|
|
return (body, parameters)
|
|
|
|
def _setEventRecurrenceTimeZone(cal, calId, body, parameters, i, count):
|
|
if ('recurrence' in body) and (('start' in body) or ('end' in body)):
|
|
timeZone = parameters.get('timeZone')
|
|
if not timeZone:
|
|
try:
|
|
timeZone = callGAPI(cal.calendars(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
calendarId=calId, fields='timeZone')['timeZone']
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
return False
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
return False
|
|
if 'start' in body:
|
|
body['start']['timeZone'] = timeZone
|
|
if 'end' in body:
|
|
body['end']['timeZone'] = timeZone
|
|
return True
|
|
|
|
def _getEventDaysOfWeek(event):
|
|
for attr in ['start', 'end']:
|
|
if attr in event:
|
|
if 'date' in event[attr]:
|
|
try:
|
|
dateTime = datetime.datetime.strptime(event[attr]['date'], YYYYMMDD_FORMAT)
|
|
event[attr]['dayOfWeek'] = calendarlib.day_abbr[dateTime.weekday()]
|
|
except ValueError:
|
|
pass
|
|
elif 'dateTime' in event[attr]:
|
|
try:
|
|
dateTime, _ = iso8601.parse_date(event[attr]['dateTime'])
|
|
event[attr]['dayOfWeek'] = calendarlib.day_abbr[dateTime.weekday()]
|
|
except (iso8601.ParseError, OverflowError):
|
|
pass
|
|
|
|
def _createCalendarEvents(user, origCal, function, calIds, count, body, parameters):
|
|
if parameters['attendees']:
|
|
body['attendees'] = parameters.pop('attendees')
|
|
fields = 'id' if parameters['csvPF'] is None else '*'
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal = getNormalizedCalIdCal(origCal, calId, user, i, count)
|
|
if not cal:
|
|
continue
|
|
if not _setEventRecurrenceTimeZone(cal, calId, body, parameters, i, count):
|
|
continue
|
|
event = {'id': body.get('id', UNKNOWN)}
|
|
if function == 'import' and body.get('status', '') == 'cancelled':
|
|
entityActionNotPerformedWarning([Ent.CALENDAR, calId, Ent.EVENT, body.get('iCalUID', event['id'])], Msg.EVENT_IS_CANCELED, count)
|
|
continue
|
|
try:
|
|
if function == 'insert':
|
|
event = callGAPI(cal.events(), 'insert',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.INVALID, GAPI.REQUIRED, GAPI.TIME_RANGE_EMPTY, GAPI.EVENT_DURATION_EXCEEDS_LIMIT,
|
|
GAPI.REQUIRED_ACCESS_LEVEL, GAPI.DUPLICATE, GAPI.FORBIDDEN,
|
|
GAPI.MALFORMED_WORKING_LOCATION_EVENT, GAPI.BAD_REQUEST],
|
|
calendarId=calId, conferenceDataVersion=1, sendUpdates=parameters['sendUpdates'], supportsAttachments=True, body=body, fields=fields)
|
|
else:
|
|
event = callGAPI(cal.events(), 'import_',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.INVALID, GAPI.REQUIRED, GAPI.TIME_RANGE_EMPTY, GAPI.EVENT_DURATION_EXCEEDS_LIMIT,
|
|
GAPI.REQUIRED_ACCESS_LEVEL, GAPI.DUPLICATE, GAPI.FORBIDDEN,
|
|
GAPI.MALFORMED_WORKING_LOCATION_EVENT, GAPI.BAD_REQUEST,
|
|
GAPI.PARTICIPANT_IS_NEITHER_ORGANIZER_NOR_ATTENDEE],
|
|
calendarId=calId, conferenceDataVersion=1, supportsAttachments=True, body=body, fields=fields)
|
|
if parameters['csvPF'] is None:
|
|
entityActionPerformed([Ent.CALENDAR, calId, Ent.EVENT, event['id']], i, count)
|
|
else:
|
|
if parameters['showDayOfWeek']:
|
|
_getEventDaysOfWeek(event)
|
|
_printCalendarEvent(user, calId, event, parameters['csvPF'], parameters['FJQC'])
|
|
except (GAPI.invalid, GAPI.required, GAPI.timeRangeEmpty, GAPI.eventDurationExceedsLimit,
|
|
GAPI.requiredAccessLevel, GAPI.participantIsNeitherOrganizerNorAttendee,
|
|
GAPI.malformedWorkingLocationEvent, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, event['id']], str(e), i, count)
|
|
except GAPI.duplicate as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, event['id']], str(e), i, count)
|
|
except GAPI.forbidden as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
break
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
break
|
|
if parameters['csvPF']:
|
|
parameters['csvPF'].writeCSVfile('Calendar Created Events')
|
|
|
|
# gam calendars <CalendarEntity> create event [id <String>] <EventAddAttribute>+
|
|
# [showdayofweek]
|
|
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
|
|
# gam calendar <UserItem> addevent [id <String>] <EventAddAttribute>+
|
|
# [showdayofweek]
|
|
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
|
|
def doCalendarsCreateEvent(calIds):
|
|
body, parameters = _getCalendarCreateImportUpdateEventOptions('insert', Ent.CALENDAR)
|
|
_createCalendarEvents(None, None, 'insert', calIds, len(calIds), body, parameters)
|
|
|
|
# gam calendars <CalendarEntity> import event icaluid <iCalUID> <EventImportAttribute>+
|
|
# [showdayofweek]
|
|
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
|
|
def doCalendarsImportEvent(calIds):
|
|
body, parameters = _getCalendarCreateImportUpdateEventOptions('import', Ent.CALENDAR)
|
|
_createCalendarEvents(None, None, 'import', calIds, len(calIds), body, parameters)
|
|
|
|
def _updateCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, body, parameters):
|
|
updateFieldList = []
|
|
if parameters['replaceDescription']:
|
|
updateFieldList.append('description')
|
|
if not parameters['replaceMode'] and (parameters['attendees'] or parameters['removeAttendees'] or parameters['clearResources']):
|
|
updateFieldList.append('attendees')
|
|
updateFields = ','.join(updateFieldList)
|
|
if 'attendees' not in updateFieldList:
|
|
if parameters['attendees']:
|
|
body['attendees'] = parameters.pop('attendees')
|
|
elif parameters['clearAttendees']:
|
|
body['attendees'] = []
|
|
pfields = '' if parameters['csvPF'] is None else '*'
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal, calEventIds, jcount = _validateCalendarGetEventIDs(origUser, user, origCal, calId, i, count, calendarEventEntity)
|
|
if jcount == 0:
|
|
continue
|
|
if not _setEventRecurrenceTimeZone(cal, calId, body, parameters, i, count):
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for eventId in calEventIds:
|
|
j += 1
|
|
try:
|
|
if updateFieldList:
|
|
event = callGAPI(cal.events(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN],
|
|
calendarId=calId, eventId=eventId, fields=updateFields)
|
|
if 'description' in updateFieldList and 'description' in event:
|
|
body['description'] = event['description']
|
|
for replacement in parameters['replaceDescription']:
|
|
body['description'] = re.sub(replacement[0], replacement[1], body['description'])
|
|
if 'attendees' in updateFieldList:
|
|
if not parameters['clearAttendees']:
|
|
if 'attendees' in event:
|
|
body['attendees'] = event['attendees']
|
|
for addAttendee in parameters['attendees']:
|
|
for attendee in body['attendees']:
|
|
if attendee['email'].lower() == addAttendee['email']:
|
|
attendee.update(addAttendee)
|
|
break
|
|
else:
|
|
body['attendees'].append(addAttendee)
|
|
elif parameters['attendees']:
|
|
body['attendees'] = parameters['attendees']
|
|
else:
|
|
body['attendees'] = []
|
|
elif parameters['attendees']:
|
|
body['attendees'] = parameters['attendees']
|
|
else:
|
|
body['attendees'] = []
|
|
if parameters['removeAttendees']:
|
|
body['attendees'] = [attendee for attendee in body['attendees'] if attendee['email'].lower() not in parameters['removeAttendees']]
|
|
if parameters['clearResources']:
|
|
body['attendees'] = [attendee for attendee in body['attendees'] if not attendee['email'].lower().endswith('@resource.calendar.google.com')]
|
|
event = callGAPI(cal.events(), 'patch',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN,
|
|
GAPI.INVALID, GAPI.REQUIRED, GAPI.TIME_RANGE_EMPTY, GAPI.EVENT_DURATION_EXCEEDS_LIMIT,
|
|
GAPI.REQUIRED_ACCESS_LEVEL, GAPI.CANNOT_CHANGE_ORGANIZER_OF_INSTANCE,
|
|
GAPI.MALFORMED_WORKING_LOCATION_EVENT],
|
|
calendarId=calId, eventId=eventId, conferenceDataVersion=1, sendUpdates=parameters['sendUpdates'], supportsAttachments=True,
|
|
body=body, fields=pfields)
|
|
if parameters['csvPF'] is None:
|
|
entityActionPerformed([Ent.CALENDAR, calId, Ent.EVENT, eventId], j, jcount)
|
|
else:
|
|
if parameters['showDayOfWeek']:
|
|
_getEventDaysOfWeek(event)
|
|
_printCalendarEvent(user, calId, event, parameters['csvPF'], parameters['FJQC'])
|
|
except (GAPI.notFound, GAPI.deleted) as e:
|
|
if not checkCalendarExists(cal, calId, j, jcount):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
break
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except (GAPI.forbidden, GAPI.invalid, GAPI.required, GAPI.timeRangeEmpty, GAPI.eventDurationExceedsLimit,
|
|
GAPI.requiredAccessLevel, GAPI.cannotChangeOrganizerOfInstance, GAPI.malformedWorkingLocationEvent) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
if parameters['csvPF']:
|
|
parameters['csvPF'].writeCSVfile('Calendar Updated Events')
|
|
|
|
# gam calendars <CalendarEntity> update events [<EventEntity>] [replacemode] <EventUpdateAttribute>+ [<EventNotificationAttribute>]
|
|
# [showdayofweek]
|
|
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
|
|
def doCalendarsUpdateEvents(calIds):
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
body, parameters = _getCalendarCreateImportUpdateEventOptions('update', Ent.CALENDAR)
|
|
_updateCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, body, parameters)
|
|
|
|
# gam calendar <CalendarEntity> updateevent <EventID> [replacemode] <EventUpdateAttribute>+ [<EventNotificationAttribute>]
|
|
# [showdayofweek]
|
|
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
|
|
def doCalendarsUpdateEventsOld(calIds):
|
|
calendarEventEntity = initCalendarEventEntity()
|
|
calendarEventEntity['list'].append(getString(Cmd.OB_EVENT_ID))
|
|
body, parameters = _getCalendarCreateImportUpdateEventOptions('update', Ent.CALENDAR)
|
|
_updateCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, body, parameters)
|
|
|
|
def _getCalendarDeleteEventOptions(calendarEventEntity=None):
|
|
parameters = {'sendUpdates': 'none', 'doIt': False}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getCalendarSendUpdates(myarg, parameters):
|
|
pass
|
|
elif calendarEventEntity and myarg in {'id', 'eventid'}:
|
|
calendarEventEntity['list'].append(getString(Cmd.OB_EVENT_ID))
|
|
elif myarg == 'doit':
|
|
parameters['doIt'] = True
|
|
else:
|
|
unknownArgumentExit()
|
|
return parameters
|
|
|
|
def _deleteCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, parameters):
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal, calEventIds, jcount = _validateCalendarGetEventIDs(origUser, user, origCal, calId, i, count, calendarEventEntity, doIt=parameters['doIt'])
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for eventId in calEventIds:
|
|
j += 1
|
|
try:
|
|
callGAPI(cal.events(), 'delete',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN,
|
|
GAPI.INVALID, GAPI.REQUIRED, GAPI.REQUIRED_ACCESS_LEVEL],
|
|
calendarId=calId, eventId=eventId, sendUpdates=parameters['sendUpdates'])
|
|
entityActionPerformed([Ent.CALENDAR, calId, Ent.EVENT, eventId], j, jcount)
|
|
except (GAPI.notFound, GAPI.deleted) as e:
|
|
if not checkCalendarExists(cal, calId, i, count):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, i, count)
|
|
break
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except (GAPI.forbidden, GAPI.invalid, GAPI.required, GAPI.requiredAccessLevel) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam calendars <CalendarEntity> delete event <EventEntity> [doit] [<EventNotificationAttribute>]
|
|
def doCalendarsDeleteEvents(calIds):
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
parameters = _getCalendarDeleteEventOptions()
|
|
_deleteCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, parameters)
|
|
|
|
# gam calendar <CalendarEntity> deleteevent (id|eventid <EventID>)+ [doit] [<EventNotificationAttribute>]
|
|
def doCalendarsDeleteEventsOld(calIds):
|
|
calendarEventEntity = initCalendarEventEntity()
|
|
parameters = _getCalendarDeleteEventOptions(calendarEventEntity)
|
|
_deleteCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, parameters)
|
|
|
|
def _getCalendarMoveEventsOptions(calendarEventEntity=None):
|
|
parameters = {'sendUpdates': 'none'}
|
|
newCalId = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getCalendarSendUpdates(myarg, parameters):
|
|
pass
|
|
elif calendarEventEntity and myarg in {'id', 'eventid'}:
|
|
calendarEventEntity['list'].append(getString(Cmd.OB_EVENT_ID))
|
|
elif calendarEventEntity and myarg == 'destination':
|
|
newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM))
|
|
else:
|
|
unknownArgumentExit()
|
|
return (parameters, newCalId)
|
|
|
|
def _moveCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, newCalId, parameters):
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal, calEventIds, jcount = _validateCalendarGetEventIDs(origUser, user, origCal, calId, i, count, calendarEventEntity)
|
|
if jcount == 0:
|
|
continue
|
|
kvList = [Ent.USER, user] if user else []
|
|
kvList.extend([Ent.CALENDAR, calId])
|
|
Ind.Increment()
|
|
j = 0
|
|
for eventId in calEventIds:
|
|
j += 1
|
|
kvListEvent = kvList+[Ent.EVENT, eventId]
|
|
kvListEventNewCal = kvListEvent+[Ent.CALENDAR, newCalId]
|
|
try:
|
|
callGAPI(cal.events(), 'move',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.REQUIRED_ACCESS_LEVEL,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST,
|
|
GAPI.CANNOT_CHANGE_ORGANIZER, GAPI.CANNOT_CHANGE_ORGANIZER_OF_INSTANCE],
|
|
calendarId=calId, eventId=eventId, destination=newCalId, sendUpdates=parameters['sendUpdates'], fields='')
|
|
entityModifierNewValueActionPerformed(kvListEvent, Act.MODIFIER_TO, f'{Ent.Singular(Ent.CALENDAR)}: {newCalId}', j, jcount)
|
|
except GAPI.notFound as e:
|
|
if not checkCalendarExists(cal, calId, i, count):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, i, count)
|
|
break
|
|
entityActionFailedWarning(kvListEventNewCal, Ent.TypeNameMessage(Ent.EVENT, eventId, str(e)), j, jcount)
|
|
except GAPI.requiredAccessLevel:
|
|
# Correct "You need to have reader access to this calendar." to "Writer access required to both calendars."
|
|
entityActionFailedWarning(kvListEventNewCal, Msg.WRITER_ACCESS_REQUIRED_TO_BOTH_CALENDARS, j, jcount)
|
|
except (GAPI.forbidden, GAPI.invalid, GAPI.badRequest,
|
|
GAPI.cannotChangeOrganizer, GAPI.cannotChangeOrganizerOfInstance) as e:
|
|
entityActionFailedWarning(kvListEventNewCal, str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam calendars <CalendarEntity> move events <EventEntity> to|destination <CalendarItem> [<EventNotificationAttribute>]
|
|
def doCalendarsMoveEvents(calIds):
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
checkArgumentPresent(['to', 'destination'])
|
|
newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM))
|
|
parameters, _ = _getCalendarMoveEventsOptions()
|
|
if not checkCalendarExists(None, newCalId, 0, 0, True):
|
|
return
|
|
_moveCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, newCalId, parameters)
|
|
|
|
# gam calendars <CalendarEntity> moveevent (id|eventid <EventID>)+ destination <CalendarItem> [<EventNotificationAttribute>]
|
|
def doCalendarsMoveEventsOld(calIds):
|
|
calendarEventEntity = initCalendarEventEntity()
|
|
parameters, newCalId = _getCalendarMoveEventsOptions(calendarEventEntity)
|
|
if not checkCalendarExists(None, newCalId, 0, 0, True):
|
|
return
|
|
_moveCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, newCalId, parameters)
|
|
|
|
def _purgeCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, parameters, emptyTrash):
|
|
body = {'summary': f'GamPurgeCalendar-{random.randint(1, 99999):05}'}
|
|
if user:
|
|
entityValueList = [Ent.USER, user, Ent.CALENDAR, body['summary']]
|
|
else:
|
|
entityValueList = [Ent.CALENDAR, body['summary']]
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal = getNormalizedCalIdCal(origCal, calId, user, i, count)
|
|
if not cal:
|
|
continue
|
|
try:
|
|
purgeCalId = callGAPI(cal.calendars(), 'insert',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.FORBIDDEN],
|
|
body=body, fields='id')['id']
|
|
Act.Set(Act.CREATE)
|
|
entityActionPerformed(entityValueList)
|
|
Ind.Increment()
|
|
if not emptyTrash:
|
|
Act.Set(Act.DELETE)
|
|
_deleteCalendarEvents(origUser, user, cal, [calId], count, calendarEventEntity, parameters)
|
|
Act.Set(Act.MOVE)
|
|
calendarEventEntity['kwargs']['showDeleted'] = True
|
|
_moveCalendarEvents(origUser, user, cal, [calId], count, calendarEventEntity, purgeCalId, parameters)
|
|
calendarEventEntity['kwargs'].pop('showDeleted')
|
|
Ind.Decrement()
|
|
callGAPI(cal.calendars(), 'delete',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
calendarId=purgeCalId)
|
|
Act.Set(Act.REMOVE)
|
|
entityActionPerformed(entityValueList)
|
|
except (GAPI.notFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, body['summary']], str(e))
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
|
|
# gam calendars <CalendarEntity> purge event <EventEntity> [doit] [<EventNotificationAttribute>]
|
|
def doCalendarsPurgeEvents(calIds):
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
parameters = _getCalendarDeleteEventOptions()
|
|
_purgeCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, parameters, False)
|
|
|
|
def _wipeCalendarEvents(user, origCal, calIds, count):
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal = getNormalizedCalIdCal(origCal, calId, user, i, count)
|
|
if not cal:
|
|
continue
|
|
try:
|
|
callGAPI(cal.calendars(), 'clear',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID,
|
|
GAPI.REQUIRED_ACCESS_LEVEL, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
calendarId=calId)
|
|
entityActionPerformed([Ent.CALENDAR, calId], i, count)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.requiredAccessLevel, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
|
|
# gam calendars <CalendarEntity> wipe events
|
|
# gam calendar <CalendarEntity> wipe
|
|
def doCalendarsWipeEvents(calIds):
|
|
checkArgumentPresent([Cmd.ARG_EVENT, Cmd.ARG_EVENTS])
|
|
checkForExtraneousArguments()
|
|
_wipeCalendarEvents(None, None, calIds, len(calIds))
|
|
|
|
def _emptyCalendarTrash(user, origCal, calIds, count):
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal = getNormalizedCalIdCal(origCal, calId, user, i, count)
|
|
if not cal:
|
|
continue
|
|
Act.Set(Act.PURGE)
|
|
calendarEventEntity = initCalendarEventEntity()
|
|
try:
|
|
events = callGAPIpages(cal.events(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
calendarId=calId, showDeleted=True, fields='nextPageToken,items(id,status,organizer(self),recurringEventId)',
|
|
maxResults=GC.Values[GC.EVENT_MAX_RESULTS])
|
|
except (GAPI.notFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
continue
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
continue
|
|
for event in events:
|
|
if event['status'] == 'cancelled' and event.get('organizer', {}).get('self', user is None) and not event.get('recurringEventId', ''):
|
|
calendarEventEntity['list'].append(event['id'])
|
|
jcount = len(calendarEventEntity['list'])
|
|
if not user:
|
|
entityPerformActionNumItems([Ent.CALENDAR, calId], jcount, Ent.TRASHED_EVENT, i, count)
|
|
Ind.Increment()
|
|
if jcount > 0:
|
|
_purgeCalendarEvents(user, user, cal, [calId], 1, calendarEventEntity, {'sendUpdates': 'none', 'doIt': True}, True)
|
|
if not user:
|
|
Ind.Decrement()
|
|
|
|
# gam calendars <CalendarEntity> empty calendartrash
|
|
def doCalendarsEmptyTrash(calIds):
|
|
checkForExtraneousArguments()
|
|
Act.Set(Act.PURGE)
|
|
_emptyCalendarTrash(None, None, calIds, len(calIds))
|
|
|
|
EVENT_SHOW_ORDER = ['id', 'summary', 'status', 'description', 'location',
|
|
'start', 'end', 'endTimeUnspecified',
|
|
'creator', 'organizer', 'created', 'updated', 'iCalUID']
|
|
EVENT_PRINT_ORDER = ['id', 'summary', 'status', 'description', 'location',
|
|
'created', 'updated', 'iCalUID']
|
|
|
|
EVENT_TIME_OBJECTS = {'created', 'updated'}
|
|
|
|
def _showCalendarEvent(primaryEmail, calId, eventEntityType, event, k, kcount, FJQC):
|
|
if FJQC.formatJSON:
|
|
if primaryEmail:
|
|
printLine(json.dumps(cleanJSON({'primaryEmail': primaryEmail, 'calendarId': calId, 'event': event},
|
|
timeObjects=EVENT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
printLine(json.dumps(cleanJSON({'calendarId': calId, 'event': event},
|
|
timeObjects=EVENT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([eventEntityType, event['id']], k, kcount)
|
|
skipObjects = {'id'}
|
|
Ind.Increment()
|
|
for field in EVENT_SHOW_ORDER:
|
|
if field in event:
|
|
if field != 'description':
|
|
showJSON(field, event[field], skipObjects, EVENT_TIME_OBJECTS)
|
|
else:
|
|
printKeyValueWithCRsNLs(field, event[field])
|
|
skipObjects.add(field)
|
|
showJSON(None, event, skipObjects)
|
|
Ind.Decrement()
|
|
|
|
def _printCalendarEvent(user, calId, event, csvPF, FJQC):
|
|
row = {'calendarId': calId, 'id': event['id']}
|
|
if user:
|
|
row['primaryEmail'] = user
|
|
flattenJSON(event, flattened=row, timeObjects=EVENT_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'calendarId': calId, 'id': event['id'],
|
|
'JSON': json.dumps(cleanJSON(event, timeObjects=EVENT_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=False)}
|
|
if user:
|
|
row['primaryEmail'] = user
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
def _printShowCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity,
|
|
csvPF, FJQC, fieldsList):
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.EVENT, calId, i, count)
|
|
calId, _, events, jcount = _validateCalendarGetEvents(origUser, user, origCal, calId, i, count, calendarEventEntity,
|
|
fieldsList, not csvPF and not FJQC.formatJSON and not calendarEventEntity['countsOnly'])
|
|
if not csvPF:
|
|
if not calendarEventEntity['countsOnly']:
|
|
Ind.Increment()
|
|
j = 0
|
|
for event in events:
|
|
j += 1
|
|
if calendarEventEntity['showDayOfWeek']:
|
|
_getEventDaysOfWeek(event)
|
|
_showCalendarEvent(user, calId, Ent.EVENT, event, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
printKeyValueList([Ent.Singular(Ent.CALENDAR), calId, Ent.Choose(Ent.EVENT, jcount), jcount])
|
|
else:
|
|
if not calendarEventEntity['countsOnly']:
|
|
if events:
|
|
for event in events:
|
|
if calendarEventEntity['showDayOfWeek']:
|
|
_getEventDaysOfWeek(event)
|
|
_printCalendarEvent(user, calId, event, csvPF, FJQC)
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT] and user:
|
|
csvPF.WriteRowNoFilter({'calendarId': calId, 'primaryEmail': user, 'id': ''})
|
|
else:
|
|
row = {'calendarId': calId}
|
|
if user:
|
|
row['primaryEmail'] = user
|
|
row['events'] = jcount
|
|
csvPF.WriteRow(row)
|
|
|
|
EVENT_FIELDS_CHOICE_MAP = {
|
|
'anyonecanaddself': 'anyoneCanAddSelf',
|
|
'attachments': 'attachments',
|
|
'attendees': 'attendees',
|
|
'attendeesomitted': 'attendeesOmitted',
|
|
'colorid': 'colorId',
|
|
'conferencedata': 'conferenceData',
|
|
'created': 'created',
|
|
'creator': 'creator',
|
|
'description': 'description',
|
|
'end': 'end',
|
|
'endtime': 'end',
|
|
'endtimeunspecified': 'endTimeUnspecified',
|
|
'extendedproperties': 'extendedProperties',
|
|
'focustimeproperties': 'focusTimeProperties',
|
|
'gadget': 'gadget',
|
|
'guestscaninviteothers': 'guestsCanInviteOthers',
|
|
'guestscanmodify': 'guestsCanModify',
|
|
'guestscanseeotherguests': 'guestsCanSeeOtherGuests',
|
|
'hangoutlink': 'hangoutLink',
|
|
'htmllink': 'htmlLink',
|
|
'eventtype': 'eventType',
|
|
'icaluid': 'iCalUID',
|
|
'id': 'id',
|
|
'location': 'location',
|
|
'locked': 'locked',
|
|
'organizer': 'organizer',
|
|
'organiser': 'organizer',
|
|
'originalstart': 'originalStartTime',
|
|
'originalstarttime': 'originalStartTime',
|
|
'outofofficeproperties': 'outOfOfficeProperties',
|
|
'privatecopy': 'privateCopy',
|
|
'recurrence': 'recurrence',
|
|
'recurringeventid': 'recurringEventId',
|
|
'reminders': 'reminders',
|
|
'sequence': 'sequence',
|
|
'source': 'source',
|
|
'start': 'start',
|
|
'starttime': 'start',
|
|
'status': 'status',
|
|
'summary': 'summary',
|
|
'transparency': 'transparency',
|
|
'updated': 'updated',
|
|
'visibility': 'visibility',
|
|
'workinglocationproperties': 'workingLocationProperties',
|
|
}
|
|
|
|
EVENT_ATTACHMENTS_SUBFIELDS_CHOICE_MAP = {
|
|
'fileid': 'fileId',
|
|
'fileurl': 'fileUrl',
|
|
'iconlink': 'iconLink',
|
|
'mimetype': 'mimeType',
|
|
'title': 'title',
|
|
}
|
|
|
|
EVENT_ATTENDEES_SUBFIELDS_CHOICE_MAP = {
|
|
'additionalguests': 'additionalGuests',
|
|
'comment': 'comment',
|
|
'displayname': 'displayName',
|
|
'email': 'email',
|
|
'id': 'id',
|
|
'optional': 'optional',
|
|
'organizer': 'organizer',
|
|
'organiser': 'organizer',
|
|
'resource': 'resource',
|
|
'responsestatus': 'responseStatus',
|
|
'self': 'self',
|
|
}
|
|
|
|
EVENT_CONFERENCEDATA_SUBFIELDS_CHOICE_MAP = {
|
|
'conferenceid': 'conferenceId',
|
|
'conferencesolution': 'conferenceSolution',
|
|
'createrequest': 'createRequest',
|
|
'entrypoints': 'entryPoints',
|
|
'notes': 'notes',
|
|
'signature': 'signature',
|
|
}
|
|
|
|
EVENT_CREATOR_SUBFIELDS_CHOICE_MAP = {
|
|
'displayname': 'displayName',
|
|
'email': 'email',
|
|
'id': 'id',
|
|
'self': 'self',
|
|
}
|
|
|
|
EVENT_FOCUSTIME_SUBFIELDS_CHOICE_MAP = {
|
|
'autodeclinemode': 'autoDeclineMode',
|
|
'chatstatus': 'chatStatus',
|
|
'declinemessage': 'declineMessage',
|
|
}
|
|
|
|
EVENT_ORGANIZER_SUBFIELDS_CHOICE_MAP = {
|
|
'displayname': 'displayName',
|
|
'email': 'email',
|
|
'id': 'id',
|
|
'self': 'self',
|
|
}
|
|
|
|
EVENT_OUTOFOFFICE_SUBFIELDS_CHOICE_MAP = {
|
|
'autodeclinemode': 'autoDeclineMode',
|
|
'declinemessage': 'declineMessage',
|
|
}
|
|
|
|
EVENT_WORKINGLOCATION_SUBFIELDS_CHOICE_MAP = {
|
|
'homeoffice': 'homeOffice',
|
|
'customlocation': 'customLocation',
|
|
'officelocation': 'officeLocation',
|
|
}
|
|
|
|
EVENT_SUBFIELDS_CHOICE_MAP = {
|
|
'attachments': EVENT_ATTACHMENTS_SUBFIELDS_CHOICE_MAP,
|
|
'attendees': EVENT_ATTENDEES_SUBFIELDS_CHOICE_MAP,
|
|
'conferencedata': EVENT_CONFERENCEDATA_SUBFIELDS_CHOICE_MAP,
|
|
'creator': EVENT_CREATOR_SUBFIELDS_CHOICE_MAP,
|
|
'focustimeproperties': EVENT_FOCUSTIME_SUBFIELDS_CHOICE_MAP,
|
|
'organizer': EVENT_ORGANIZER_SUBFIELDS_CHOICE_MAP,
|
|
'organiser': EVENT_ORGANIZER_SUBFIELDS_CHOICE_MAP,
|
|
'outofofficeproperties': EVENT_OUTOFOFFICE_SUBFIELDS_CHOICE_MAP,
|
|
'workinglocationproperties': EVENT_WORKINGLOCATION_SUBFIELDS_CHOICE_MAP,
|
|
}
|
|
|
|
def _getEventFields(fieldsList):
|
|
if not fieldsList:
|
|
fieldsList.append('id')
|
|
for field in _getFieldsList():
|
|
if field.find('.') == -1:
|
|
if field in EVENT_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, EVENT_FIELDS_CHOICE_MAP, fieldsList)
|
|
else:
|
|
invalidChoiceExit(field, EVENT_FIELDS_CHOICE_MAP, True)
|
|
else:
|
|
field, subField = field.split('.', 1)
|
|
if field in EVENT_SUBFIELDS_CHOICE_MAP:
|
|
if subField in EVENT_SUBFIELDS_CHOICE_MAP[field]:
|
|
fieldsList.append(f'{EVENT_FIELDS_CHOICE_MAP[field]}.{EVENT_SUBFIELDS_CHOICE_MAP[field][subField]}')
|
|
else:
|
|
invalidChoiceExit(subField, list(EVENT_SUBFIELDS_CHOICE_MAP[field]), True)
|
|
else:
|
|
invalidChoiceExit(field, list(EVENT_SUBFIELDS_CHOICE_MAP), True)
|
|
|
|
def _addEventEntitySelectFields(calendarEventEntity, fieldsList):
|
|
if fieldsList:
|
|
_getEventMatchFields(calendarEventEntity, fieldsList)
|
|
if calendarEventEntity['maxinstances'] != -1:
|
|
fieldsList.append('recurrence')
|
|
|
|
def _getCalendarInfoEventOptions(calendarEventEntity):
|
|
FJQC = FormatJSONQuoteChar()
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'fields':
|
|
_getEventFields(fieldsList)
|
|
elif myarg == 'showdayofweek':
|
|
calendarEventEntity['showDayOfWeek'] = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
_addEventEntitySelectFields(calendarEventEntity, fieldsList)
|
|
return (FJQC, fieldsList)
|
|
|
|
def _infoCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEntity, FJQC, fieldsList):
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
ifields = getItemFieldsFromFieldsList('items', fieldsList)
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal, calEventIds, jcount = _validateCalendarGetEventIDs(origUser, user, origCal, calId, i, count, calendarEventEntity, showAction=not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for eventId in calEventIds:
|
|
j += 1
|
|
try:
|
|
event = callGAPI(cal.events(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN],
|
|
calendarId=calId, eventId=eventId, fields=fields)
|
|
if calendarEventEntity['maxinstances'] == -1 or 'recurrence' not in event:
|
|
if calendarEventEntity['showDayOfWeek']:
|
|
_getEventDaysOfWeek(event)
|
|
_showCalendarEvent(user, calId, Ent.EVENT, event, j, jcount, FJQC)
|
|
else:
|
|
instances = callGAPIpages(cal.events(), 'instances', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN],
|
|
calendarId=calId, eventId=eventId, fields=ifields,
|
|
maxItems=calendarEventEntity['maxinstances'], maxResults=GC.Values[GC.EVENT_MAX_RESULTS])
|
|
lcount = len(instances)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.EVENT, event['id']], lcount, Ent.INSTANCE, j, jcount)
|
|
Ind.Increment()
|
|
l = 0
|
|
for instance in instances:
|
|
l += 1
|
|
if calendarEventEntity['showDayOfWeek']:
|
|
_getEventDaysOfWeek(instance)
|
|
_showCalendarEvent(user, calId, Ent.INSTANCE, instance, l, lcount, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.deleted) as e:
|
|
if not checkCalendarExists(cal, calId, i, count):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, i, count)
|
|
break
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except (GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
break
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam calendars <CalendarEntity> info events <EventEntity> [maxinstances <Number>]
|
|
# [fields <EventFieldNameList>] [showdayofweek]
|
|
# [formatjson]
|
|
def doCalendarsInfoEvents(calIds):
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
FJQC, fieldsList = _getCalendarInfoEventOptions(calendarEventEntity)
|
|
_infoCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity, FJQC, fieldsList)
|
|
|
|
EVENT_INDEXED_TITLES = ['attendees', 'attachments', 'recurrence']
|
|
|
|
def _getCalendarPrintShowEventOptions(calendarEventEntity, entityType):
|
|
csvPF = CSVPrintFile(['primaryEmail', 'calendarId', 'id'] if entityType == Ent.USER else ['calendarId', 'id'], 'sortall', indexedTitles=EVENT_INDEXED_TITLES) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getCalendarListEventsDisplayProperty(myarg, calendarEventEntity):
|
|
pass
|
|
elif myarg == 'fields':
|
|
_getEventFields(fieldsList)
|
|
elif myarg == 'countsonly':
|
|
calendarEventEntity['countsOnly'] = True
|
|
elif myarg == 'showdayofweek':
|
|
calendarEventEntity['showDayOfWeek'] = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if calendarEventEntity['countsOnly']:
|
|
fieldsList = ['id']
|
|
if csvPF:
|
|
if calendarEventEntity['countsOnly']:
|
|
csvPF.AddTitles(['events'])
|
|
elif not FJQC.formatJSON and not fieldsList:
|
|
csvPF.AddSortTitles(EVENT_PRINT_ORDER)
|
|
_addEventEntitySelectFields(calendarEventEntity, fieldsList)
|
|
return (csvPF, FJQC, fieldsList)
|
|
|
|
# gam calendars <CalendarEntity> print events <EventEntity> <EventDisplayProperties>*
|
|
# [fields <EventFieldNameList>] [showdayofweek]
|
|
# [countsonly] [formatjson [quotechar <Character>]] [todrive <ToDriveAttribute>*]
|
|
# gam calendars <CalendarEntity> show events <EventEntity> <EventDisplayProperties>*
|
|
# [fields <EventFieldNameList>] [showdayofweek]
|
|
# [countsonly] [formatjson]
|
|
def doCalendarsPrintShowEvents(calIds):
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
csvPF, FJQC, fieldsList = _getCalendarPrintShowEventOptions(calendarEventEntity, Ent.CALENDAR)
|
|
_printShowCalendarEvents(None, None, None, calIds, len(calIds), calendarEventEntity,
|
|
csvPF, FJQC, fieldsList)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Calendar Events')
|
|
|
|
# <CalendarSettings> ::==
|
|
# [description <String>] [location <String>] [summary <String>] [timezone <TimeZone>]
|
|
def _getCalendarSetting(myarg, body):
|
|
if myarg == 'description':
|
|
body['description'] = getStringWithCRsNLs()
|
|
elif myarg == 'location':
|
|
body['location'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'summary':
|
|
body['summary'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'timezone':
|
|
body['timeZone'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def getCalendarSettings(summaryRequired=False):
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getCalendarSetting(myarg, body):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
if summaryRequired and not body.get('summary', None):
|
|
missingArgumentExit('summary <String>')
|
|
return body
|
|
|
|
# gam calendars <CalendarEntity> modify <CalendarSettings>
|
|
def doCalendarsModifySettings(calIds):
|
|
body = getCalendarSettings(summaryRequired=False)
|
|
count = len(calIds)
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal = validateCalendar(calId, i, count)
|
|
if not cal:
|
|
continue
|
|
try:
|
|
callGAPI(cal.calendars(), 'patch',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
calendarId=calId, body=body)
|
|
entityActionPerformed([Ent.CALENDAR, calId], i, count)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
|
|
def _showCalendarSettings(calendar, j, jcount):
|
|
printEntity([Ent.CALENDAR, calendar['id']], j, jcount)
|
|
Ind.Increment()
|
|
if 'summaryOverride' in calendar or 'summary' in calendar:
|
|
printKeyValueList(['Summary', calendar.get('summaryOverride', calendar.get('summary', ''))])
|
|
if 'description' in calendar:
|
|
printKeyValueWithCRsNLs('Description', calendar.get('description', ''))
|
|
if 'location' in calendar:
|
|
printKeyValueList(['Location', calendar.get('location', '')])
|
|
if 'timeZone' in calendar:
|
|
printKeyValueList(['Timezone', calendar.get('timeZone', '')])
|
|
if 'conferenceProperties' in calendar:
|
|
printKeyValueList(['ConferenceProperties', None])
|
|
Ind.Increment()
|
|
printKeyValueList(['AllowedConferenceSolutionTypes', ','.join(calendar.get('conferenceProperties', {}).get('allowedConferenceSolutionTypes', []))])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
CALENDAR_SETTINGS_FIELDS_CHOICE_MAP = {
|
|
'conferenceproperties': 'conferenceProperties',
|
|
'description': 'description',
|
|
'id': 'id',
|
|
'location': 'location',
|
|
'summary': 'summary',
|
|
'timezone': 'timeZone',
|
|
}
|
|
|
|
# gam calendars <CalendarEntity> print settings [todrive <ToDriveAttribute>*]
|
|
# [fields <CalendarSettingsFieldList>]
|
|
# [formatjson] [quotechar <Character>}
|
|
# gam calendars <CalendarEntity> show settings
|
|
# [fields <CalendarSettingsFieldList>]
|
|
# [formatjson]
|
|
def doCalendarsPrintShowSettings(calIds):
|
|
csvPF = CSVPrintFile(['calendarId'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getFieldsList(myarg, CALENDAR_SETTINGS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
count = len(calIds)
|
|
i = 0
|
|
for calId in calIds:
|
|
i += 1
|
|
calId, cal = validateCalendar(calId, i, count)
|
|
if not cal:
|
|
continue
|
|
try:
|
|
calendar = callGAPI(cal.calendars(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
calendarId=calId, fields=fields)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
_showCalendarSettings(calendar, i, count)
|
|
else:
|
|
printLine(json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
row = flattenJSON(calendar)
|
|
if not FJQC.formatJSON:
|
|
row['calendarId'] = row.pop('id')
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'calendarId': calId, 'JSON': json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True)})
|
|
except (GAPI.notFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(calId, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Calendar Settings')
|
|
|
|
def _validateResourceId(cd, resourceId, i, count, exitOnNotFound):
|
|
try:
|
|
return callGAPI(cd.resources().calendars(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId, fields='resourceEmail')['resourceEmail']
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
if exitOnNotFound:
|
|
entityDoesNotExistExit(Ent.RESOURCE_CALENDAR, resourceId, i, count)
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
|
|
return None
|
|
|
|
def _normalizeResourceIdGetRuleIds(cd, resourceId, i, count, ACLScopeEntity, showAction=True):
|
|
calId = _validateResourceId(cd, resourceId, i, count, False)
|
|
if not calId:
|
|
return (None, None, 0)
|
|
if ACLScopeEntity['dict']:
|
|
ruleIds = ACLScopeEntity['dict'][resourceId]
|
|
else:
|
|
ruleIds = ACLScopeEntity['list']
|
|
jcount = len(ruleIds)
|
|
if showAction:
|
|
entityPerformActionNumItems([Ent.RESOURCE_CALENDAR, resourceId], jcount, Ent.CALENDAR_ACL, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (calId, ruleIds, jcount)
|
|
|
|
# gam resource <ResourceID> create calendaracls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
|
|
# gam resources <ResourceEntity> create calendaracls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
|
|
def doResourceCreateCalendarACLs(entityList):
|
|
cal = buildGAPIObject(API.CALENDAR)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
calId, ruleIds, jcount = _normalizeResourceIdGetRuleIds(cd, resourceId, i, count, ACLScopeEntity)
|
|
if jcount == 0:
|
|
continue
|
|
_createCalendarACLs(cal, Ent.RESOURCE_CALENDAR, calId, i, count, role, ruleIds, jcount, sendNotifications)
|
|
|
|
def _resourceUpdateDeleteCalendarACLs(entityList, function, ACLScopeEntity, role, sendNotifications):
|
|
cal = buildGAPIObject(API.CALENDAR)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
calId, ruleIds, jcount = _normalizeResourceIdGetRuleIds(cd, resourceId, i, count, ACLScopeEntity)
|
|
if jcount == 0:
|
|
continue
|
|
_updateDeleteCalendarACLs(cal, function, Ent.RESOURCE_CALENDAR, calId, i, count, role, ruleIds, jcount, sendNotifications)
|
|
|
|
# gam resource <ResourceID> update calendaracls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
|
|
# gam resources <ResourceEntity> update calendaracls <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
|
|
def doResourceUpdateCalendarACLs(entityList):
|
|
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
|
|
_resourceUpdateDeleteCalendarACLs(entityList, 'patch', ACLScopeEntity, role, sendNotifications)
|
|
|
|
# gam resource <ResourceID> delete calendaracls [<CalendarACLRole>] <CalendarACLScopeEntity>
|
|
# gam resources <ResourceEntity> delete calendaracls [<CalendarACLRole>] <CalendarACLScopeEntity>
|
|
def doResourceDeleteCalendarACLs(entityList):
|
|
role, ACLScopeEntity = getCalendarDeleteACLsOptions(True)
|
|
_resourceUpdateDeleteCalendarACLs(entityList, 'delete', ACLScopeEntity, role, False)
|
|
|
|
# gam resource <ResourceID> info calendaracls <CalendarACLScopeEntity>
|
|
# [formatjson]
|
|
# gam resources <ResourceEntity> info calendaracls <CalendarACLScopeEntity>
|
|
# [formatjson]
|
|
def doResourceInfoCalendarACLs(entityList):
|
|
cal = buildGAPIObject(API.CALENDAR)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ACLScopeEntity = getCalendarSiteACLScopeEntity()
|
|
FJQC = _getCalendarInfoACLOptions()
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
calId, ruleIds, jcount = _normalizeResourceIdGetRuleIds(cd, resourceId, i, count, ACLScopeEntity, showAction=not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
_infoCalendarACLs(cal, resourceId, Ent.RESOURCE_CALENDAR, calId, i, count, ruleIds, jcount, FJQC)
|
|
|
|
# gam resource <ResourceID> print calendaracls [todrive <ToDriveAttribute>*]
|
|
# [noselfowner] (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam resources <ResourceEntity> print calendaracls [todrive <ToDriveAttribute>*]
|
|
# [noselfowner] (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam resource <ResourceID> show calendaracls
|
|
# [noselfowner]
|
|
# [formatjson]
|
|
# gam resources <ResourceEntity> show calendaracls
|
|
# [noselfowner]
|
|
# [formatjson]
|
|
def doResourcePrintShowCalendarACLs(entityList):
|
|
cal = buildGAPIObject(API.CALENDAR)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF, FJQC, noSelfOwner, addCSVData = _getCalendarPrintShowACLOptions(['resourceId', 'resourceEmail'])
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
calId = _validateResourceId(cd, resourceId, i, count, False)
|
|
if not calId:
|
|
continue
|
|
_printShowCalendarACLs(cal, resourceId, Ent.RESOURCE_CALENDAR, calId, i, count, csvPF, FJQC, noSelfOwner, addCSVData)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Resource Calendar ACLs')
|
|
|
|
def _showSchema(schema, i=0, count=0):
|
|
printEntity([Ent.USER_SCHEMA, schema['schemaName']], i, count)
|
|
Ind.Increment()
|
|
for a_key in schema:
|
|
if a_key not in {'kind', 'etag', 'schemaName', 'fields'}:
|
|
printKeyValueList([a_key, schema[a_key]])
|
|
for field in schema['fields']:
|
|
printKeyValueList(['Field', field['fieldName']])
|
|
Ind.Increment()
|
|
for a_key in field:
|
|
if a_key not in {'kind', 'etag', 'fieldName'}:
|
|
if a_key != 'numericIndexingSpec':
|
|
printKeyValueList([a_key, field[a_key]])
|
|
else:
|
|
printKeyValueList([a_key, ''])
|
|
Ind.Increment()
|
|
for s_key in field[a_key]:
|
|
printKeyValueList([s_key, field[a_key][s_key]])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
SCHEMA_FIELDTYPE_CHOICE_MAP = {
|
|
'bool': 'BOOL',
|
|
'boolean': 'BOOL',
|
|
'date': 'DATE',
|
|
'double': 'DOUBLE',
|
|
'email': 'EMAIL',
|
|
'int64': 'INT64',
|
|
'phone': 'PHONE',
|
|
'string': 'STRING',
|
|
}
|
|
|
|
# gam create schema|schemas <SchemaName> [displayname <String>] <SchemaFieldDefinition>+
|
|
# gam update schema|schemas <SchemaEntity> [displayname <String>] <SchemaFieldDefinition>+
|
|
def doCreateUpdateUserSchemas():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
updateCmd = Act.Get() == Act.UPDATE
|
|
if not updateCmd:
|
|
entityList = getStringReturnInList(Cmd.OB_SCHEMA_NAME)
|
|
else:
|
|
entityList = getEntityList(Cmd.OB_SCHEMA_ENTITY)
|
|
addBody = {'schemaName': '', 'fields': []}
|
|
deleteFields = []
|
|
schemaDisplayName = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'field':
|
|
fieldName = getString(Cmd.OB_FIELD_NAME)
|
|
a_field = {'fieldName': fieldName.replace(' ', '_'), 'displayName': fieldName, 'fieldType': 'STRING'}
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument == 'displayname':
|
|
a_field['displayName'] = getString(Cmd.OB_FIELD_NAME)
|
|
elif argument == 'type':
|
|
a_field['fieldType'] = getChoice(SCHEMA_FIELDTYPE_CHOICE_MAP, mapChoice=True)
|
|
elif argument in {'multivalued', 'multivalue'}:
|
|
a_field['multiValued'] = True
|
|
elif argument == 'indexed':
|
|
a_field['indexed'] = True
|
|
elif argument == 'restricted':
|
|
a_field['readAccessType'] = 'ADMINS_AND_SELF'
|
|
elif argument == 'range':
|
|
a_field['numericIndexingSpec'] = {'minValue': getInteger(), 'maxValue': getInteger()}
|
|
elif argument == 'endfield':
|
|
break
|
|
elif argument == 'field':
|
|
Cmd.Backup()
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
addBody['fields'].append(a_field)
|
|
elif updateCmd and myarg == 'deletefield':
|
|
deleteFields.append(getString(Cmd.OB_FIELD_NAME).replace(' ', '_'))
|
|
elif myarg == 'displayname':
|
|
schemaDisplayName = getString(Cmd.OB_SCHEMA_NAME)
|
|
else:
|
|
unknownArgumentExit()
|
|
i = 0
|
|
count = len(entityList)
|
|
if not updateCmd:
|
|
if not addBody['fields']:
|
|
missingArgumentExit('SchemaFieldDefinition')
|
|
elif schemaDisplayName and count > 1:
|
|
usageErrorExit(Msg.DISPLAYNAME_NOT_ALLOWED_WHEN_UPDATING_MULTIPLE_SCHEMAS)
|
|
for schemaName in entityList:
|
|
i += 1
|
|
try:
|
|
if updateCmd:
|
|
oldBody = callGAPI(cd.schemas(), 'get',
|
|
throwReasons=[GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], schemaKey=schemaName, fields='schemaName,displayName,fields')
|
|
for field in oldBody['fields']:
|
|
field.pop('etag', None)
|
|
field.pop('kind', None)
|
|
field.pop('fieldId', None)
|
|
badDelete = False
|
|
for delField in deleteFields:
|
|
fieldNameLower = delField.lower()
|
|
for n, field in enumerate(oldBody['fields']):
|
|
if field['fieldName'].lower() == fieldNameLower:
|
|
del oldBody['fields'][n]
|
|
break
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER_SCHEMA, schemaName, Ent.FIELD, delField], Msg.DOES_NOT_EXIST)
|
|
badDelete = True
|
|
if badDelete:
|
|
continue
|
|
for addField in addBody['fields']:
|
|
fieldNameLower = addField['fieldName'].lower()
|
|
for n, field in enumerate(oldBody['fields']):
|
|
if field['fieldName'].lower() == fieldNameLower:
|
|
del oldBody['fields'][n]
|
|
break
|
|
oldBody['fields'].extend(addBody['fields'])
|
|
if not oldBody['fields']:
|
|
entityActionNotPerformedWarning([Ent.USER_SCHEMA, schemaName],
|
|
Msg.SCHEMA_WOULD_HAVE_NO_FIELDS.format(Ent.Singular(Ent.USER_SCHEMA), Ent.Plural(Ent.FIELD)))
|
|
continue
|
|
if schemaDisplayName:
|
|
oldBody['displayName'] = schemaDisplayName
|
|
result = callGAPI(cd.schemas(), 'update',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.FIELD_IN_USE],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], body=oldBody, schemaKey=schemaName)
|
|
entityActionPerformed([Ent.USER_SCHEMA, result['schemaName']], i, count)
|
|
else:
|
|
addBody['schemaName'] = schemaName.replace(' ', '_')
|
|
addBody['displayName'] = schemaDisplayName if schemaDisplayName else schemaName
|
|
result = callGAPI(cd.schemas(), 'insert',
|
|
throwReasons=[GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], body=addBody, fields='schemaName')
|
|
entityActionPerformed([Ent.USER_SCHEMA, result['schemaName']], i, count)
|
|
except GAPI.duplicate:
|
|
entityDuplicateWarning([Ent.USER_SCHEMA, schemaName], i, count)
|
|
except (GAPI.conditionNotMet, GAPI.fieldInUse, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER_SCHEMA, schemaName], str(e), i, count)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.USER_SCHEMA, schemaName, i, count)
|
|
|
|
# gam delete schema|schemas <SchemaEntity>
|
|
def doDeleteUserSchemas():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
entityList = getEntityList(Cmd.OB_SCHEMA_ENTITY)
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for schemaKey in entityList:
|
|
i += 1
|
|
try:
|
|
callGAPI(cd.schemas(), 'delete',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN, GAPI.FIELD_IN_USE],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], schemaKey=schemaKey)
|
|
entityActionPerformed([Ent.USER_SCHEMA, schemaKey], i, count)
|
|
except GAPI.fieldInUse as e:
|
|
entityActionFailedWarning([Ent.USER_SCHEMA, schemaKey], str(e), i, count)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.USER_SCHEMA, schemaKey, i, count)
|
|
|
|
# gam info schema|schemas <SchemaEntity>
|
|
def doInfoUserSchemas():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
entityList = getEntityList(Cmd.OB_SCHEMA_ENTITY)
|
|
checkForExtraneousArguments()
|
|
i = 0
|
|
count = len(entityList)
|
|
for schemaKey in entityList:
|
|
i += 1
|
|
try:
|
|
schema = callGAPI(cd.schemas(), 'get',
|
|
throwReasons=[GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], schemaKey=schemaKey)
|
|
_showSchema(schema, i, count)
|
|
except (GAPI.invalid, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.USER_SCHEMA, schemaKey, i, count)
|
|
|
|
SCHEMAS_SORT_TITLES = ['schemaId', 'schemaName', 'displayName']
|
|
SCHEMAS_INDEXED_TITLES = ['fields']
|
|
|
|
# gam print schema|schemas [todrive <ToDriveAttribute>*]
|
|
# gam show schema|schemas
|
|
def doPrintShowUserSchemas():
|
|
csvPF = CSVPrintFile(SCHEMAS_SORT_TITLES, 'sortall', SCHEMAS_INDEXED_TITLES) if Act.csvFormat() else None
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
getTodriveOnly(csvPF)
|
|
try:
|
|
result = callGAPI(cd.schemas(), 'list',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID])
|
|
jcount = len(result.get('schemas', [])) if (result) else 0
|
|
if not csvPF:
|
|
performActionNumItems(jcount, Ent.USER_SCHEMA)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
else:
|
|
if not csvPF:
|
|
Ind.Increment()
|
|
j = 0
|
|
for schema in result['schemas']:
|
|
j += 1
|
|
_showSchema(schema, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for schema in result['schemas']:
|
|
csvPF.WriteRowTitles(flattenJSON(schema))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('User Schemas')
|
|
|
|
def _copyStorageObjects(objects, target_bucket, target_prefix):
|
|
"""Copies objects to target_bucket.
|
|
|
|
Args:
|
|
objects: list of object dicts
|
|
[
|
|
{
|
|
bucket: source bucket,
|
|
name: source object name,
|
|
(optional) md5Hash: source file hash value
|
|
},
|
|
...
|
|
]
|
|
target_bucket: target bucket id
|
|
target_prefix: prefix name to prepend to target object
|
|
|
|
"""
|
|
|
|
def process_rewrite(request_id, response, exception):
|
|
fileIndex = int(request_id)
|
|
if exception:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
# Poor man's backoff/retry
|
|
if http_status == 429 or http_status > 499:
|
|
writeStderr(f'Temporary error: {http_status} - {message}, Backing off: 10 seconds\n')
|
|
flushStderr()
|
|
time.sleep(10)
|
|
next_batch.add(s.objects().rewrite(**files_to_copy[fileIndex]['method']), request_id=request_id)
|
|
return
|
|
if reason in GAPI.REASON_EXCEPTION_MAP:
|
|
raise GAPI.REASON_EXCEPTION_MAP[reason](message)
|
|
raise exception(message)
|
|
source_displayname = files_to_copy[fileIndex]['source_displayname']
|
|
target_displayname = files_to_copy[fileIndex]['target_displayname']
|
|
if response.get('done'):
|
|
source_md5 = files_to_copy[fileIndex]['md5Hash']
|
|
target_md5 = response['resource']['md5Hash']
|
|
if source_md5 != target_md5:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, f'Target file {target_displayname} checksum {target_md5} does not match source {source_md5}. This should not happen')
|
|
entityActionPerformedMessage([Ent.CLOUD_STORAGE_FILE, None], f'{1:>7.2%} Complete', fileIndex+1, totalFiles)
|
|
Ind.Increment()
|
|
writeStdout(formatKeyValueList(Ind.Spaces(), ['Source', source_displayname], '\n'))
|
|
writeStdout(formatKeyValueList(Ind.Spaces(), ['Target', target_displayname], '\n'))
|
|
Ind.Decrement()
|
|
else:
|
|
total_bytes = float(response.get('objectSize'))
|
|
done_bytes = float(response.get('totalBytesRewritten'))
|
|
entityActionPerformedMessage([Ent.CLOUD_STORAGE_FILE, None], f'{(done_bytes / total_bytes):>7.2%} Complete', fileIndex+1, totalFiles)
|
|
Ind.Increment()
|
|
writeStdout(formatKeyValueList(Ind.Spaces(), ['Source', source_displayname], '\n'))
|
|
writeStdout(formatKeyValueList(Ind.Spaces(), ['Target', target_displayname], '\n'))
|
|
Ind.Decrement()
|
|
files_to_copy[fileIndex]['method']['rewriteToken'] = response.get('rewriteToken')
|
|
next_batch.add(s.objects().rewrite(**files_to_copy[fileIndex]['method']), request_id=request_id)
|
|
|
|
action = Act.Get()
|
|
Act.Set(Act.COPY)
|
|
s = buildGAPIObject(API.STORAGEWRITE)
|
|
sbatch = s.new_batch_http_request(callback=process_rewrite)
|
|
files_to_copy = []
|
|
for object_ in objects:
|
|
files_to_copy.append(
|
|
{
|
|
'md5Hash': object_['md5Hash'],
|
|
'source_displayname': f'{object_["bucket"]}:{object_["name"]}',
|
|
'target_displayname': f'{target_bucket}:{target_prefix}{object_["name"]}',
|
|
'method': {
|
|
'destinationBucket': target_bucket,
|
|
'destinationObject': f'{target_prefix}{object_["name"]}',
|
|
'sourceBucket': object_['bucket'],
|
|
'sourceObject': object_['name'],
|
|
'maxBytesRewrittenPerCall': 1048576, # uncomment to easily test multiple rewrite API calls per object
|
|
},
|
|
})
|
|
totalFiles = len(files_to_copy)
|
|
i = 0
|
|
try:
|
|
for file in files_to_copy:
|
|
while len(sbatch._order) == 100:
|
|
next_batch = s.new_batch_http_request(callback=process_rewrite)
|
|
sbatch.execute()
|
|
sbatch = next_batch
|
|
sbatch.add(s.objects().rewrite(**file['method']), request_id=str(i))
|
|
i += 1
|
|
while len(sbatch._order) > 0:
|
|
next_batch = s.new_batch_http_request(callback=process_rewrite)
|
|
sbatch.execute()
|
|
sbatch = next_batch
|
|
except GAPI.notFound:
|
|
Act.Set(action)
|
|
entityDoesNotExistExit(Ent.CLOUD_STORAGE_BUCKET, target_bucket)
|
|
except Exception:
|
|
ClientAPIAccessDeniedExit()
|
|
Act.Set(action)
|
|
|
|
def _getCloudStorageObject(s, bucket, s_object, localFilename, expectedMd5=None, zipToStdout=False, j=0, jcount=0):
|
|
if not zipToStdout:
|
|
localFilename = cleanFilepath(localFilename)
|
|
entityValueList = [Ent.DRIVE_FILE, localFilename]
|
|
if os.path.exists(localFilename):
|
|
printEntityMessage(entityValueList, Msg.EXISTS)
|
|
if not expectedMd5:
|
|
return # nothing to verify, just assume we're good.
|
|
if md5MatchesFile(localFilename, expectedMd5):
|
|
return
|
|
printEntityMessage(entityValueList, Msg.DOWNLOADING_AGAIN_AND_OVER_WRITING)
|
|
entityPerformAction(entityValueList)
|
|
file_path = os.path.dirname(localFilename)
|
|
if not os.path.exists(file_path):
|
|
os.makedirs(file_path)
|
|
f = openFile(localFilename, 'wb')
|
|
else:
|
|
f = openFile('-', 'wb')
|
|
try:
|
|
request = s.objects().get_media(bucket=bucket, object=s_object)
|
|
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
|
|
done = False
|
|
while not done:
|
|
status, done = downloader.next_chunk()
|
|
if not zipToStdout and status.progress() < 1.0:
|
|
entityActionPerformedMessage([Ent.CLOUD_STORAGE_FILE, s_object], f'{status.progress():>7.2%}', j, jcount)
|
|
if not zipToStdout:
|
|
entityModifierNewValueActionPerformed([Ent.CLOUD_STORAGE_FILE, s_object], Act.MODIFIER_TO, localFilename, j, jcount)
|
|
closeFile(f, True)
|
|
if expectedMd5 and not md5MatchesFile(localFilename, expectedMd5):
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(localFilename, Msg.CORRUPT_FILE))
|
|
except googleapiclient.http.HttpError as e:
|
|
mg = HTTP_ERROR_PATTERN.match(str(e))
|
|
entityModifierNewValueActionFailedWarning([Ent.CLOUD_STORAGE_FILE, s_object], Act.MODIFIER_TO, localFilename, mg.group(1) if mg else str(e), j, jcount)
|
|
|
|
TAKEOUT_EXPORT_PATTERN = re.compile(r'(takeout-export-[a-f,0-9,-]*)')
|
|
|
|
# gam copy storagebucket sourcebucket <StorageBucketName> targetbucket <StorageBucketName>
|
|
# [sourceprefix <String>] [targetprefix <String>]
|
|
def doCopyCloudStorageBucket():
|
|
s = buildGAPIObject(API.STORAGEREAD)
|
|
source_bucket = None
|
|
target_bucket = None
|
|
source_prefix = None
|
|
target_prefix = ''
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'sourcebucket':
|
|
source_bucket = getString(Cmd.OB_URL)
|
|
elif myarg == 'targetbucket':
|
|
target_bucket = getString(Cmd.OB_URL)
|
|
elif myarg == 'sourceprefix':
|
|
source_prefix = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'targetprefix':
|
|
target_prefix = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not target_bucket:
|
|
missingArgumentExit('targetbucket')
|
|
if not source_bucket:
|
|
missingArgumentExit('sourcebucket')
|
|
printGettingAllAccountEntities(Ent.FILE)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
objects = callGAPIpages(s.objects(), 'list', 'items',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
bucket=source_bucket, prefix=source_prefix,
|
|
fields='nextPageToken,items(name,bucket,md5Hash)')
|
|
except GAPI.notFound:
|
|
entityDoesNotExistExit(Ent.CLOUD_STORAGE_BUCKET, source_bucket)
|
|
except GAPI.forbidden as e:
|
|
entityActionFailedExit([Ent.CLOUD_STORAGE_BUCKET, source_bucket], str(e))
|
|
_copyStorageObjects(objects, target_bucket, target_prefix)
|
|
|
|
# gam download storagebucket <TakeoutBucketName>
|
|
# [targetfolder <FilePath>]
|
|
def doDownloadCloudStorageBucket():
|
|
bucket_url = getString(Cmd.OB_STRING)
|
|
targetFolder = GC.Values[GC.DRIVE_DIR]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'targetfolder':
|
|
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
else:
|
|
unknownArgumentExit()
|
|
bucket_match = re.search(TAKEOUT_EXPORT_PATTERN, bucket_url)
|
|
if not bucket_match:
|
|
systemErrorExit(ACTION_NOT_PERFORMED_RC, f'Could not find a takeout-export-* bucket in {bucket_url}')
|
|
bucket = bucket_match.group(1)
|
|
s = buildGAPIObject(API.STORAGEREAD)
|
|
printGettingAllAccountEntities(Ent.FILE)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
objects = callGAPIpages(s.objects(), 'list', 'items',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
bucket=bucket, projection='noAcl', fields='nextPageToken,items(name,md5Hash)')
|
|
except GAPI.notFound:
|
|
entityDoesNotExistExit(Ent.CLOUD_STORAGE_BUCKET, bucket)
|
|
except GAPI.forbidden as e:
|
|
entityActionFailedExit([Ent.CLOUD_STORAGE_BUCKET, bucket], str(e))
|
|
count = len(objects)
|
|
i = 0
|
|
for s_object in objects:
|
|
i += 1
|
|
printGettingEntityItem(Ent.FILE, s_object['name'], i, count)
|
|
expectedMd5 = base64.b64decode(s_object['md5Hash']).hex()
|
|
_getCloudStorageObject(s, bucket, s_object['name'], os.path.join(targetFolder, s_object['name']),
|
|
expectedMd5=expectedMd5)
|
|
|
|
# gam download storagefile <StorageBucketObjectName>
|
|
# [targetfolder <FilePath>] [overwrite [<Boolean>]] [nogcspath [Boolean>]]
|
|
def doDownloadCloudStorageFile():
|
|
bucket, s_object, bucketObject = getBucketObjectName()
|
|
targetFolder = GC.Values[GC.DRIVE_DIR]
|
|
overwrite = nogcspath = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'targetfolder':
|
|
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
elif myarg == 'overwrite':
|
|
overwrite = getBoolean()
|
|
elif myarg == 'nogcspath':
|
|
nogcspath = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
s_obpaths = s_object.rsplit('/', 1)
|
|
s_obfile = s_obpaths[-1]
|
|
if len(s_obpaths) > 1 and not nogcspath:
|
|
targetFolder = os.path.join(targetFolder, s_obpaths[0])
|
|
filename, _ = uniqueFilename(targetFolder, s_obfile, overwrite)
|
|
filepath = os.path.dirname(filename)
|
|
if not os.path.exists(filepath):
|
|
os.makedirs(filepath)
|
|
s = buildGAPIObject(API.STORAGEREAD)
|
|
printGettingEntityItem(Ent.FILE, s_object)
|
|
f = openFile(filename, 'wb')
|
|
try:
|
|
request = s.objects().get_media(bucket=bucket, object=s_object)
|
|
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
|
|
done = False
|
|
while not done:
|
|
_, done = downloader.next_chunk()
|
|
entityModifierNewValueActionPerformed([Ent.CLOUD_STORAGE_FILE, s_object], Act.MODIFIER_TO, filename)
|
|
closeFile(f, True)
|
|
except googleapiclient.http.HttpError as e:
|
|
mg = HTTP_ERROR_PATTERN.match(str(e))
|
|
entityActionFailedWarning([Ent.CLOUD_STORAGE_FILE, bucketObject], mg.group(1) if mg else str(e))
|
|
|
|
def formatVaultNameId(vaultName, vaultId):
|
|
return f'{vaultName}({vaultId})'
|
|
|
|
def convertExportNameToID(v, nameOrId, matterId, matterNameId):
|
|
cg = UID_PATTERN.match(nameOrId)
|
|
if cg:
|
|
try:
|
|
export = callGAPI(v.matters().exports(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, exportId=cg.group(1))
|
|
return (export['id'], export['name'], formatVaultNameId(export['id'], export['name']))
|
|
except (GAPI.notFound, GAPI.badRequest):
|
|
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, nameOrId])
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
nameOrIdlower = nameOrId.lower()
|
|
try:
|
|
exports = callGAPIpages(v.matters().exports(), 'list', 'exports',
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, fields='exports(id,name),nextPageToken')
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
for export in exports:
|
|
if export['name'].lower() == nameOrIdlower:
|
|
return (export['id'], export['name'], formatVaultNameId(export['id'], export['name']))
|
|
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, nameOrId])
|
|
|
|
def convertHoldNameToID(v, nameOrId, matterId, matterNameId):
|
|
cg = UID_PATTERN.match(nameOrId)
|
|
if cg:
|
|
try:
|
|
hold = callGAPI(v.matters().holds(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, holdId=cg.group(1))
|
|
return (hold['holdId'], hold['name'], formatVaultNameId(hold['holdId'], hold['name']))
|
|
except (GAPI.notFound, GAPI.badRequest):
|
|
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, nameOrId])
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
nameOrIdlower = nameOrId.lower()
|
|
try:
|
|
holds = callGAPIpages(v.matters().holds(), 'list', 'holds',
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, fields='holds(holdId,name),nextPageToken')
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
for hold in holds:
|
|
if hold['name'].lower() == nameOrIdlower:
|
|
return (hold['holdId'], hold['name'], formatVaultNameId(hold['holdId'], hold['name']))
|
|
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, nameOrId])
|
|
|
|
def convertMatterNameToID(v, nameOrId, state=None):
|
|
cg = UID_PATTERN.match(nameOrId)
|
|
if cg:
|
|
try:
|
|
matter = callGAPI(v.matters(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=cg.group(1), view='BASIC', fields='matterId,name,state')
|
|
return (matter['matterId'], matter['name'], formatVaultNameId(matter['name'], matter['matterId']), matter['state'])
|
|
except (GAPI.notFound, GAPI.forbidden):
|
|
entityDoesNotExistExit(Ent.VAULT_MATTER, nameOrId)
|
|
try:
|
|
matters = callGAPIpages(v.matters(), 'list', 'matters',
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
view='BASIC', state=state, fields='matters(matterId,name,state),nextPageToken')
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
nameOrIdlower = nameOrId.lower()
|
|
ids = []
|
|
states = []
|
|
for matter in matters:
|
|
if matter['name'].lower() == nameOrIdlower:
|
|
nameOrId = matter['name']
|
|
ids.append(matter['matterId'])
|
|
states.append(matter['state'])
|
|
if len(ids) == 1:
|
|
return (ids[0], nameOrId, formatVaultNameId(nameOrId, ids[0]), states[0])
|
|
if not ids:
|
|
entityDoesNotExistExit(Ent.VAULT_MATTER, nameOrId)
|
|
else:
|
|
entityIsNotUniqueExit(Ent.VAULT_MATTER, nameOrId, Ent.VAULT_MATTER_ID, ids)
|
|
|
|
def convertQueryNameToID(v, nameOrId, matterId, matterNameId):
|
|
cg = UID_PATTERN.match(nameOrId)
|
|
if cg:
|
|
try:
|
|
query = callGAPI(v.matters().savedQueries(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, savedQueryId=cg.group(1))
|
|
return (query['savedQueryId'], query['displayName'], formatVaultNameId(query['savedQueryId'], query['displayName']))
|
|
except (GAPI.notFound, GAPI.badRequest):
|
|
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, nameOrId])
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
nameOrIdlower = nameOrId.lower()
|
|
try:
|
|
queries = callGAPIpages(v.matters().savedQueries(), 'list', 'savedQueries',
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, fields='savedQueries(savedQueryId,displayName),nextPageToken')
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
ClientAPIAccessDeniedExit(str(e))
|
|
for query in queries:
|
|
if query['displayName'].lower() == nameOrIdlower:
|
|
return (query['savedQueryId'], query['displayName'], formatVaultNameId(query['savedQueryId'], query['displayName']))
|
|
entityDoesNotHaveItemExit([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, nameOrId])
|
|
|
|
def getMatterItem(v, state=None):
|
|
matterId, _, matterNameId, _ = convertMatterNameToID(v, getString(Cmd.OB_MATTER_ITEM), state=state)
|
|
return (matterId, matterNameId)
|
|
|
|
def warnMatterNotOpen(v, matter, matterNameId, j, jcount):
|
|
if v is not None:
|
|
try:
|
|
matter['state'] = callGAPI(v.matters(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matter['matterId'], view='BASIC', fields='state')['state']
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.invalidArgument):
|
|
matter['state'] = 'Unknown'
|
|
else:
|
|
setSysExitRC(DATA_NOT_AVALIABLE_RC)
|
|
message = formatKeyValueList('',
|
|
Ent.FormatEntityValueList([Ent.VAULT_MATTER, matterNameId])+
|
|
[Msg.MATTER_NOT_OPEN.format(matter['state'])],
|
|
currentCount(j, jcount))
|
|
writeStderr(f'\n{Ind.Spaces()}{WARNING_PREFIX}{message}\n')
|
|
|
|
def _cleanVaultExport(export, cd):
|
|
query = export.get('query')
|
|
if query:
|
|
if cd is not None:
|
|
if 'orgUnitInfo' in query:
|
|
query['orgUnitInfo']['orgUnitPath'] = convertOrgUnitIDtoPath(cd, query['orgUnitInfo']['orgUnitId'])
|
|
|
|
VAULT_EXPORT_TIME_OBJECTS = {'versionDate', 'createTime', 'startTime', 'endTime'}
|
|
|
|
def _showVaultExport(matterNameId, export, cd, FJQC, k=0, kcount=0):
|
|
_cleanVaultExport(export, cd)
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(export, timeObjects=VAULT_EXPORT_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
|
|
return
|
|
if matterNameId is not None:
|
|
printEntity([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, formatVaultNameId(export['name'], export['id'])], k, kcount)
|
|
Ind.Increment()
|
|
showJSON(None, export, timeObjects=VAULT_EXPORT_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
VAULT_SEARCH_METHODS_MAP = {
|
|
'account': 'ACCOUNT',
|
|
'accounts': 'ACCOUNT',
|
|
'entireorg': 'ENTIRE_ORG',
|
|
'everyone': 'ENTIRE_ORG',
|
|
'org': 'ORG_UNIT',
|
|
'orgunit': 'ORG_UNIT',
|
|
'ou': 'ORG_UNIT',
|
|
'room': 'ROOM',
|
|
'rooms': 'ROOM',
|
|
'shareddrive': 'SHARED_DRIVE',
|
|
'shareddrives': 'SHARED_DRIVE',
|
|
'sitesurl': 'SITES_URL',
|
|
'teamdrive': 'SHARED_DRIVE',
|
|
'teamdrives': 'SHARED_DRIVE',
|
|
}
|
|
VAULT_CORPUS_ARGUMENT_MAP = {
|
|
'calendar': 'CALENDAR',
|
|
'drive': 'DRIVE',
|
|
'gemini': 'GEMINI',
|
|
'groups': 'GROUPS',
|
|
'hangoutschat': 'HANGOUTS_CHAT',
|
|
'mail': 'MAIL',
|
|
'voice': 'VOICE',
|
|
}
|
|
VAULT_COUNTS_CORPUS_ARGUMENT_MAP = {
|
|
'mail': 'MAIL',
|
|
'groups': 'GROUPS',
|
|
}
|
|
VAULT_RESPONSE_STATUS_MAP = {
|
|
'accepted': 'ATTENDEE_RESPONSE_ACCEPTED',
|
|
'declined': 'ATTENDEE_RESPONSE_DECLINED',
|
|
'needsaction': 'ATTENDEE_RESPONSE_NEEDS_ACTION',
|
|
'tentative': 'ATTENDEE_RESPONSE_TENTATIVE',
|
|
}
|
|
VAULT_VOICE_COVERED_DATA_MAP = {
|
|
'calllogs': 'CALL_LOGS',
|
|
'textmessages': 'TEXT_MESSAGES',
|
|
'voicemails': 'VOICEMAILS',
|
|
}
|
|
VAULT_EXPORT_DATASCOPE_MAP = {
|
|
'alldata': 'ALL_DATA',
|
|
'helddata': 'HELD_DATA',
|
|
'unprocesseddata': 'UNPROCESSED_DATA',
|
|
}
|
|
VAULT_EXPORT_FORMAT_MAP = {
|
|
'ics': 'ICS',
|
|
'mbox': 'MBOX',
|
|
'pst': 'PST',
|
|
'xml': 'XML',
|
|
}
|
|
VAULT_CORPUS_EXPORT_FORMATS = {
|
|
'CALENDAR': ['ICS', 'PST'],
|
|
'DRIVE': [],
|
|
'GEMINI': ['XML'],
|
|
'GROUPS': ['MBOX', 'PST'],
|
|
'HANGOUTS_CHAT': ['MBOX', 'PST'],
|
|
'MAIL': ['MBOX', 'PST'],
|
|
'VOICE' : ['MBOX', 'PST'],
|
|
}
|
|
VAULT_CSE_OPTION_MAP = {
|
|
'any': 'CLIENT_SIDE_ENCRYPTED_OPTION_ANY',
|
|
'encrypted': 'CLIENT_SIDE_ENCRYPTED_OPTION_ENCRYPTED',
|
|
'unencrypted': 'CLIENT_SIDE_ENCRYPTED_OPTION_UNENCRYPTED',
|
|
}
|
|
VAULT_EXPORT_REGION_MAP = {
|
|
'any': 'ANY',
|
|
'europe': 'EUROPE',
|
|
'us': 'US',
|
|
}
|
|
VAULT_CORPUS_OPTIONS_MAP = {
|
|
'CALENDAR': 'calendarOptions',
|
|
'DRIVE': 'driveOptions',
|
|
'GEMINI': 'geminiOptions',
|
|
'GROUPS': 'groupsOptions',
|
|
'HANGOUTS_CHAT': 'hangoutsChatOptions',
|
|
'MAIL': 'mailOptions',
|
|
'VOICE': 'voiceOptions',
|
|
}
|
|
VAULT_CORPUS_QUERY_MAP = {
|
|
'CALENDAR': None,
|
|
'DRIVE': 'driveQuery',
|
|
'GEMINI': None,
|
|
'GROUPS': 'groupsQuery',
|
|
'MAIL': 'mailQuery',
|
|
'HANGOUTS_CHAT': 'hangoutsChatQuery',
|
|
'VOICE': 'voiceQuery',
|
|
}
|
|
VAULT_QUERY_ARGS = [
|
|
'corpus', 'scope', 'terms', 'start', 'starttime', 'end', 'endtime', 'timezone',
|
|
# calendar
|
|
'locationquery', 'peoplequery', 'minuswords', 'responsestatuses', 'caldendarversiondate',
|
|
# drive
|
|
'driveclientsideencryption', 'driveversiondate', 'includeshareddrives', 'includeteamdrives',
|
|
# hangoutsChat
|
|
'includerooms',
|
|
# mail
|
|
'mailclientsideencryption', 'excludedrafts',
|
|
# voice
|
|
'covereddata',
|
|
] + list(VAULT_SEARCH_METHODS_MAP.keys())
|
|
|
|
def _buildVaultQuery(myarg, query, corpusArgumentMap):
|
|
if not query:
|
|
query['dataScope'] = 'ALL_DATA'
|
|
if myarg == 'corpus':
|
|
query['corpus'] = getChoice(corpusArgumentMap, mapChoice=True)
|
|
elif myarg in VAULT_SEARCH_METHODS_MAP:
|
|
if query.get('searchMethod'):
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.MULTIPLE_SEARCH_METHODS_SPECIFIED.format(formatChoiceList(VAULT_SEARCH_METHODS_MAP)))
|
|
searchMethod = VAULT_SEARCH_METHODS_MAP[myarg]
|
|
query['searchMethod'] = searchMethod
|
|
if searchMethod == 'ACCOUNT':
|
|
query['accountInfo'] = {'emails': getNormalizedEmailAddressEntity()}
|
|
elif searchMethod == 'ORG_UNIT':
|
|
query['orgUnitInfo'] = {'orgUnitId': getOrgUnitId()[1]}
|
|
elif searchMethod == 'SHARED_DRIVE':
|
|
query['sharedDriveInfo'] = {'sharedDriveIds': getString(Cmd.OB_SHAREDDRIVE_ID_LIST).replace(',', ' ').split()}
|
|
elif searchMethod == 'ROOM':
|
|
query['hangoutsChatInfo'] = {'roomId': getString(Cmd.OB_ROOM_LIST).replace(',', ' ').split()}
|
|
elif searchMethod == 'SITES_URL':
|
|
query['sitesUrlInfo'] = {'urls': getString(Cmd.OB_URL_LIST).replace(',', ' ').split()}
|
|
elif myarg == 'scope':
|
|
query['dataScope'] = getChoice(VAULT_EXPORT_DATASCOPE_MAP, mapChoice=True)
|
|
elif myarg == 'terms':
|
|
query['terms'] = getString(Cmd.OB_STRING)
|
|
elif myarg in {'start', 'starttime'}:
|
|
query['startTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg in {'end', 'endtime'}:
|
|
query['endTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg == 'timezone':
|
|
query['timeZone'] = getString(Cmd.OB_STRING)
|
|
# calendar
|
|
elif myarg == 'locationquery':
|
|
query.setdefault('calendarOptions', {})['locationQuery'] = shlexSplitList(getString(Cmd.OB_STRING_LIST))
|
|
elif myarg == 'peoplequery':
|
|
query.setdefault('calendarOptions', {})['peopleQuery'] = shlexSplitList(getString(Cmd.OB_STRING_LIST))
|
|
elif myarg == 'minuswords':
|
|
query.setdefault('calendarOptions', {})['minusWords'] = shlexSplitList(getString(Cmd.OB_STRING_LIST))
|
|
elif myarg == 'responsestatuses':
|
|
query.setdefault('calendarOptions', {})['responseStatuses'] = []
|
|
for response in getString(Cmd.OB_FIELD_NAME_LIST).lower().replace('_', '').replace(',', ' ').split():
|
|
if response in VAULT_RESPONSE_STATUS_MAP:
|
|
query['calendarOptions']['responseStatuses'].append(VAULT_RESPONSE_STATUS_MAP[response])
|
|
else:
|
|
invalidChoiceExit(response, VAULT_RESPONSE_STATUS_MAP, True)
|
|
elif myarg == 'calendarversiondate':
|
|
query.setdefault('calendarOptions', {})['versionDate'] = getTimeOrDeltaFromNow()
|
|
# drive
|
|
elif myarg == 'driveversiondate':
|
|
query.setdefault('driveOptions', {})['versionDate'] = getTimeOrDeltaFromNow()
|
|
elif myarg in {'includeshareddrives', 'includeteamdrives'}:
|
|
query.setdefault('driveOptions', {})['includeSharedDrives'] = getBoolean()
|
|
elif myarg == 'driveclientsideencryption':
|
|
query.setdefault('driveOptions', {})['clientSideEncryptedOption'] = getChoice(VAULT_CSE_OPTION_MAP, mapChoice=True)
|
|
# hangoutsChat
|
|
elif myarg == 'includerooms':
|
|
query['hangoutsChatOptions'] = {'includeRooms': getBoolean()}
|
|
# mail
|
|
elif myarg == 'excludedrafts':
|
|
query['mailOptions'] = {'excludeDrafts': getBoolean()}
|
|
elif myarg == 'mailclientsideencryption':
|
|
query.setdefault('mailOptions', {})['clientSideEncryptedOption'] = getChoice(VAULT_CSE_OPTION_MAP, mapChoice=True)
|
|
# voice
|
|
elif myarg == 'covereddata':
|
|
query['voiceOptions'] = {'coveredData': getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True)}
|
|
|
|
def _validateVaultQuery(body, corpusArgumentMap):
|
|
if 'corpus' not in body['query']:
|
|
missingArgumentExit(f'corpus {formatChoiceList(corpusArgumentMap)}')
|
|
if 'searchMethod' not in body['query']:
|
|
missingArgumentExit(formatChoiceList(VAULT_SEARCH_METHODS_MAP))
|
|
if 'exportOptions' in body:
|
|
for corpus, options in iter(VAULT_CORPUS_OPTIONS_MAP.items()):
|
|
if body['query']['corpus'] != corpus:
|
|
body['exportOptions'].pop(options, None)
|
|
|
|
# gam create vaultexport|export matter <MatterItem> [name <String>] corpus calendar|drive|gemini|groups|hangouts_chat|mail|voice
|
|
# (accounts <EmailAddressEntity>) | (orgunit|org|ou <OrgUnitPath>) | everyone
|
|
# (shareddrives|teamdrives <TeamDriveIDList>) | (rooms <RoomList>) | (sitesurl <URLList>)
|
|
# [scope <all_data|held_data|unprocessed_data>]
|
|
# [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>]
|
|
# [locationquery <StringList>] [peoplequery <StringList>] [minuswords <StringList>]
|
|
# [responsestatuses <AttendeeStatus>(,<AttendeeStatus>)*] [calendarversiondate <Date>|<Time>]
|
|
# [includeshareddrives <Boolean>] [driveversiondate <Date>|<Time>] [includeaccessinfo <Boolean>]
|
|
# [driveclientsideencryption any|encrypted|unencrypted]
|
|
# [includerooms <Boolean>]
|
|
# [excludedrafts <Boolean>] [mailclientsideencryption any|encrypted|unencrypted]
|
|
# [showconfidentialmodecontent <Boolean>] [usenewexport <Boolean>] [exportlinkeddrivefiles <Boolean>]
|
|
# [covereddata calllogs|textmessages|voicemails]
|
|
# [format ics|mbox|pst|xml]
|
|
# [region any|europe|us] [showdetails|returnidonly]
|
|
def doCreateVaultExport():
|
|
v = buildGAPIObject(API.VAULT)
|
|
matterId = None
|
|
body = {'query': {'dataScope': 'ALL_DATA'}, 'exportOptions': {}}
|
|
exportFormat = None
|
|
showConfidentialModeContent = None
|
|
exportLinkedDriveFiles = None
|
|
returnIdOnly = showDetails = False
|
|
useNewExport = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v, state='OPEN')
|
|
body['matterId'] = matterId
|
|
elif myarg == 'name':
|
|
body['name'] = getString(Cmd.OB_STRING)
|
|
elif myarg in VAULT_QUERY_ARGS:
|
|
_buildVaultQuery(myarg, body['query'], VAULT_CORPUS_ARGUMENT_MAP)
|
|
elif myarg == 'usenewexport':
|
|
useNewExport = getBoolean()
|
|
elif myarg == 'format':
|
|
exportFormat = getChoice(VAULT_EXPORT_FORMAT_MAP, mapChoice=True)
|
|
elif myarg == 'showconfidentialmodecontent':
|
|
showConfidentialModeContent = getBoolean()
|
|
elif myarg == 'exportlinkeddrivefiles':
|
|
exportLinkedDriveFiles = getBoolean()
|
|
elif myarg == 'region':
|
|
body['exportOptions']['region'] = getChoice(VAULT_EXPORT_REGION_MAP, mapChoice=True)
|
|
elif myarg == 'includeaccessinfo':
|
|
body['exportOptions'].setdefault('driveOptions', {})['includeAccessInfo'] = getBoolean()
|
|
elif myarg == 'covereddata':
|
|
body['exportOptions'].setdefault('voiceOptions', {})['coveredData'] = getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True)
|
|
elif myarg == 'showdetails':
|
|
showDetails = True
|
|
returnIdOnly = False
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
showDetails = False
|
|
else:
|
|
unknownArgumentExit()
|
|
if not matterId:
|
|
missingArgumentExit('matter')
|
|
_validateVaultQuery(body, VAULT_CORPUS_ARGUMENT_MAP)
|
|
if exportFormat is not None:
|
|
if not exportFormat in VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']]:
|
|
invalidChoiceExit(exportFormat, VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']], False)
|
|
elif body['query']['corpus'] != 'DRIVE':
|
|
exportFormat = VAULT_CORPUS_EXPORT_FORMATS[body['query']['corpus']][0]
|
|
if 'name' not in body:
|
|
body['name'] = f'GAM {body["query"]["corpus"]} Export - {ISOformatTimeStamp(todaysTime())}'
|
|
optionsField = VAULT_CORPUS_OPTIONS_MAP[body['query']['corpus']]
|
|
if body['query']['corpus'] != 'DRIVE':
|
|
body['exportOptions'][optionsField] = {'exportFormat': exportFormat}
|
|
if body['query']['corpus'] == 'MAIL':
|
|
if showConfidentialModeContent is not None:
|
|
body['exportOptions'][optionsField]['showConfidentialModeContent'] = showConfidentialModeContent
|
|
if useNewExport is not None:
|
|
body['exportOptions'][optionsField]['useNewExport'] = useNewExport
|
|
if exportLinkedDriveFiles is not None:
|
|
body['exportOptions'][optionsField]['exportLinkedDriveFiles'] = exportLinkedDriveFiles
|
|
try:
|
|
export = callGAPI(v.matters().exports(), 'create',
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.BAD_REQUEST, GAPI.BACKEND_ERROR, GAPI.INVALID_ARGUMENT,
|
|
GAPI.INVALID, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.QUOTA_EXCEEDED],
|
|
matterId=matterId, body=body)
|
|
if not returnIdOnly:
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, formatVaultNameId(export['name'], export['id'])])
|
|
if showDetails:
|
|
_showVaultExport(None, export, None, None)
|
|
else:
|
|
writeStdout(f'{export["id"]}\n')
|
|
except (GAPI.alreadyExists, GAPI.badRequest, GAPI.backendError, GAPI.invalidArgument,
|
|
GAPI.invalid, GAPI.failedPrecondition, GAPI.forbidden, GAPI.quotaExceeded) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, body.get('name')], str(e))
|
|
|
|
# gam delete vaultexport|export <ExportItem> matter <MatterItem>
|
|
# gam delete vaultexport|export <MatterItem> <ExportItem>
|
|
def doDeleteVaultExport():
|
|
v = buildGAPIObject(API.VAULT)
|
|
if not Cmd.ArgumentIsAhead('matter'):
|
|
matterId, matterNameId = getMatterItem(v)
|
|
exportId, exportName, exportNameId = convertExportNameToID(v, getString(Cmd.OB_EXPORT_ITEM), matterId, matterNameId)
|
|
else:
|
|
exportName = getString(Cmd.OB_EXPORT_ITEM)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v)
|
|
exportId, exportName, exportNameId = convertExportNameToID(v, exportName, matterId, matterNameId)
|
|
else:
|
|
unknownArgumentExit()
|
|
try:
|
|
callGAPI(v.matters().exports(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, exportId=exportId)
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId])
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], str(e))
|
|
|
|
VAULT_EXPORT_FIELDS_CHOICE_MAP = {
|
|
'cloudstoragesink': 'cloudStorageSink',
|
|
'createtime': 'createTime',
|
|
'exportoptions': 'exportOptions',
|
|
'id': 'id',
|
|
'matterid': 'matterId',
|
|
'name': 'name',
|
|
'query': 'query',
|
|
'requester': 'requester',
|
|
'requester.displayname': 'requester.displayName',
|
|
'requester.email': 'requester.email',
|
|
'stats': 'stats',
|
|
'stats.exportedartifactcount': 'stats.exportedArtifactCount',
|
|
'stats.sizeinbytes': 'stats.sizeInBytes',
|
|
'stats.totalartifactcount': 'stats.totalArtifactCount',
|
|
'status': 'status',
|
|
}
|
|
|
|
# gam info vaultexport|export <ExportItem> matter <MatterItem>
|
|
# [fields <VaultExportFieldNameList>] [shownames]
|
|
# [formatjson]
|
|
# gam info vaultexport|export <MatterItem> <ExportItem>
|
|
# [fields <VaultExportFieldNameList>] [shownames]
|
|
# [formatjson]
|
|
def doInfoVaultExport():
|
|
v = buildGAPIObject(API.VAULT)
|
|
if not Cmd.ArgumentIsAhead('matter'):
|
|
matterId, matterNameId = getMatterItem(v)
|
|
exportId, exportName, exportNameId = convertExportNameToID(v, getString(Cmd.OB_EXPORT_ITEM), matterId, matterNameId)
|
|
else:
|
|
exportName = getString(Cmd.OB_EXPORT_ITEM)
|
|
cd = None
|
|
fieldsList = []
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v)
|
|
exportId, exportName, exportNameId = convertExportNameToID(v, exportName, matterId, matterNameId)
|
|
elif myarg == 'shownames':
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
elif getFieldsList(myarg, VAULT_EXPORT_FIELDS_CHOICE_MAP, fieldsList, initialField=['id', 'name']):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
try:
|
|
export = callGAPI(v.matters().exports(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, exportId=exportId, fields=fields)
|
|
_showVaultExport(matterNameId, export, cd, FJQC)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], str(e))
|
|
|
|
VAULT_EXPORT_STATUS_MAP = {'completed': 'COMPLETED', 'failed': 'FAILED', 'inprogress': 'IN_PROGRESS'}
|
|
PRINT_VAULT_EXPORTS_TITLES = ['matterId', 'matterName', 'id', 'name']
|
|
|
|
# gam print vaultexports|exports [todrive <ToDriveAttribute>*]
|
|
# [matters <MatterItemList>] [exportstatus <ExportStatusList>]
|
|
# [fields <VaultExportFieldNameList>] [shownames]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [oneitemperrow]
|
|
# gam show vaultexports|exports
|
|
# [matters <MatterItemList>] [exportstatus <ExportStatusList>]
|
|
# [fields <VaultExportFieldNameList>] [shownames]
|
|
# [formatjson]
|
|
def doPrintShowVaultExports():
|
|
def _printVaultExport(export):
|
|
row = flattenJSON(export, flattened={'matterId': matterId, 'matterName': matterName}, timeObjects=VAULT_EXPORT_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'matterId': matterId, 'matterName': matterName,
|
|
'id': export['id'], 'name': export['name'],
|
|
'JSON': json.dumps(cleanJSON(export, timeObjects=VAULT_EXPORT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
|
|
|
|
v = buildGAPIObject(API.VAULT)
|
|
csvPF = CSVPrintFile(PRINT_VAULT_EXPORTS_TITLES, 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar()
|
|
matters = []
|
|
exportStatusList = []
|
|
cd = None
|
|
fieldsList = []
|
|
oneItemPerRow = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'matter', 'matters'}:
|
|
matters = shlexSplitList(getString(Cmd.OB_MATTER_ITEM_LIST))
|
|
elif myarg == 'exportstatus':
|
|
for state in getString(Cmd.OB_STATE_NAME_LIST).lower().replace('_', '').replace(',', ' ').split():
|
|
if state in VAULT_EXPORT_STATUS_MAP:
|
|
exportStatusList.append(VAULT_EXPORT_STATUS_MAP[state])
|
|
else:
|
|
invalidChoiceExit(state, list(VAULT_EXPORT_STATUS_MAP), True)
|
|
elif myarg == 'shownames':
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
elif getFieldsList(myarg, VAULT_EXPORT_FIELDS_CHOICE_MAP, fieldsList, initialField=['id', 'name']):
|
|
pass
|
|
elif csvPF and myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
fields = getItemFieldsFromFieldsList('exports', fieldsList)
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(PRINT_VAULT_EXPORTS_TITLES+['JSON'])
|
|
exportStatuses = set(exportStatusList)
|
|
exportQualifier = f' ({",".join(exportStatusList)})' if exportStatusList else ''
|
|
if not matters:
|
|
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=' (OPEN)')
|
|
try:
|
|
results = callGAPIpages(v.matters(), 'list', 'matters',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_EXPORT, None], str(e))
|
|
return
|
|
else:
|
|
results = []
|
|
for matter in matters:
|
|
matterId, matterName, _, state = convertMatterNameToID(v, matter)
|
|
results.append({'matterId': matterId, 'name': matterName, 'state': state})
|
|
jcount = len(results)
|
|
if not csvPF:
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
j = 0
|
|
for matter in results:
|
|
j += 1
|
|
matterId = matter['matterId']
|
|
matterName = matter['name']
|
|
matterNameId = formatVaultNameId(matterName, matterId)
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(Ent.VAULT_EXPORT, f'{Ent.Singular(Ent.VAULT_MATTER)}: {matterNameId}',
|
|
j, jcount, qualifier=exportQualifier)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
if matter['state'] == 'OPEN':
|
|
try:
|
|
exports = callGAPIpages(v.matters().exports(), 'list', 'exports',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, fields=fields)
|
|
except GAPI.failedPrecondition:
|
|
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
|
|
continue
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_EXPORT, None], str(e))
|
|
break
|
|
else:
|
|
warnMatterNotOpen(None, matter, matterNameId, j, jcount)
|
|
continue
|
|
kcount = len(exports)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], kcount, Ent.VAULT_EXPORT, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for export in exports:
|
|
k += 1
|
|
if not exportStatuses or export['status'] in exportStatuses:
|
|
_showVaultExport(matterNameId, export, cd, FJQC, k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for export in exports:
|
|
if not exportStatuses or export['status'] in exportStatuses:
|
|
_cleanVaultExport(export, cd)
|
|
if not oneItemPerRow or not export.get('cloudStorageSink', {}).get('files'):
|
|
_printVaultExport(export)
|
|
else:
|
|
for file in export['cloudStorageSink'].pop('files'):
|
|
export['cloudStorageSink']['files'] = file
|
|
_printVaultExport(export)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Vault Exports')
|
|
|
|
def md5MatchesFile(filename, expected_md5, j=0, jcount=0):
|
|
action = Act.Get()
|
|
Act.Set(Act.VERIFY)
|
|
try:
|
|
f = openFile(filename, 'rb')
|
|
hash_md5 = hashlib.md5()
|
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
hash_md5.update(chunk)
|
|
closeFile(f)
|
|
actual_hash = hash_md5.hexdigest()
|
|
if actual_hash == expected_md5:
|
|
entityActionPerformed([Ent.FILE, filename, Ent.MD5HASH, expected_md5], j, jcount)
|
|
Act.Set(action)
|
|
return True
|
|
entityActionFailedWarning([Ent.FILE, filename, Ent.MD5HASH, expected_md5], Msg.DOES_NOT_MATCH.format(actual_hash), j, jcount)
|
|
Act.Set(action)
|
|
return False
|
|
except IOError as e:
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(filename, e))
|
|
|
|
# gam copy vaultexport|export <ExportItem> matter <MatterItem>
|
|
# [targetbucket <String>] [targetprefix <String>]
|
|
# [bucketmatchpattern <REMatchPattern>] [objectmatchpattern <REMatchPattern>]
|
|
# [copyattempts <Integer>] [retryinterval <Integer>]
|
|
# gam copy vaultexport|export <MatterItem> <ExportItem>
|
|
# [targetbucket <String>] [targetprefix <String>]
|
|
# [bucketmatchpattern <REMatchPattern>] [objectmatchpattern <REMatchPattern>]
|
|
# [copyattempts <Integer>] [retryinterval <Integer>]
|
|
def doCopyVaultExport():
|
|
v = buildGAPIObject(API.VAULT)
|
|
if not Cmd.ArgumentIsAhead('matter'):
|
|
matterId, matterNameId = getMatterItem(v)
|
|
exportId, exportName, exportNameId = convertExportNameToID(v, getString(Cmd.OB_EXPORT_ITEM), matterId, matterNameId)
|
|
else:
|
|
exportName = getString(Cmd.OB_EXPORT_ITEM)
|
|
target_bucket = None
|
|
target_prefix = ''
|
|
bucketMatchPattern = objectMatchPattern = None
|
|
copyAttempts = 1
|
|
retryInterval = 30
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v)
|
|
exportId, exportName, exportNameId = convertExportNameToID(v, exportName, matterId, matterNameId)
|
|
elif myarg == 'targetbucket':
|
|
target_bucket = getString(Cmd.OB_URL)
|
|
elif myarg == 'targetprefix':
|
|
target_prefix = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'bucketmatchpattern':
|
|
bucketMatchPattern = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'objectmatchpattern':
|
|
objectMatchPattern = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'copyattempts':
|
|
copyAttempts = getInteger(minVal=1)
|
|
elif myarg == 'retryinterval':
|
|
retryInterval = getInteger(minVal=10)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not target_bucket:
|
|
missingArgumentExit('targetbucket')
|
|
attempts = 0
|
|
while True:
|
|
try:
|
|
export = callGAPI(v.matters().exports(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, exportId=exportId)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], str(e))
|
|
return
|
|
if export['status'] == 'COMPLETED':
|
|
break
|
|
attempts += 1
|
|
entityActionNotPerformedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], Msg.EXPORT_NOT_COMPLETE.format(export['status']), attempts, copyAttempts)
|
|
if attempts >= copyAttempts:
|
|
return
|
|
time.sleep(retryInterval)
|
|
objects = []
|
|
for s_file in export['cloudStorageSink']['files']:
|
|
if ((bucketMatchPattern and not bucketMatchPattern.match(s_file['bucketName'])) or
|
|
(objectMatchPattern and not objectMatchPattern.match(s_file['objectName']))):
|
|
continue
|
|
# Convert to md5Hash format Storage API uses because OF COURSE they differ
|
|
md5Hash = base64.b64encode(bytes.fromhex(s_file['md5Hash'])).decode()
|
|
objects.append({'bucket': s_file['bucketName'], 'name': s_file['objectName'], 'md5Hash': md5Hash})
|
|
_copyStorageObjects(objects, target_bucket, target_prefix)
|
|
|
|
ZIP_EXTENSION_PATTERN = re.compile(r'^.*\.zip$', re.IGNORECASE)
|
|
|
|
# gam download vaultexport|export <ExportItem> matter <MatterItem>
|
|
# [targetfolder <FilePath>] [targetname <FileName>] [noverify] [noextract] [ziptostdout]
|
|
# [bucketmatchpattern <REMatchPattern>] [objectmatchpattern <REMatchPattern>]
|
|
# [downloadattempts <Integer>] [retryinterval <Integer>]
|
|
# gam download vaultexport|export <MatterItem> <ExportItem>
|
|
# [targetfolder <FilePath>] [targetname <FileName>] [noverify] [noextract] [ziptostdout]
|
|
# [bucketmatchpattern <REMatchPattern>] [objectmatchpattern <REMatchPattern>]
|
|
# [downloadattempts <Integer>] [retryinterval <Integer>]
|
|
def doDownloadVaultExport():
|
|
def extract_nested_zip(zippedFile):
|
|
""" Extract a zip file including any nested zip files
|
|
Delete the zip file(s) after extraction
|
|
"""
|
|
Act.Set(Act.UNZIP)
|
|
performAction(Ent.FILE, zippedFile)
|
|
Ind.Increment()
|
|
with zipfile.ZipFile(zippedFile, 'r') as zfile:
|
|
inner_files = zfile.infolist()
|
|
for inner_file in inner_files:
|
|
Act.Set(Act.EXTRACT)
|
|
performAction(Ent.FILE, inner_file.filename)
|
|
innerFilePath = zfile.extract(inner_file, targetFolder)
|
|
if ZIP_EXTENSION_PATTERN.match(inner_file.filename):
|
|
extract_nested_zip(innerFilePath)
|
|
Ind.Decrement()
|
|
try:
|
|
os.remove(zippedFile)
|
|
except OSError as e:
|
|
stderrWarningMsg(e)
|
|
|
|
v = buildGAPIObject(API.VAULT)
|
|
s = buildGAPIObject(API.STORAGEREAD)
|
|
verifyFiles = extractFiles = True
|
|
targetFolder = GC.Values[GC.DRIVE_DIR]
|
|
targetName = None
|
|
if not Cmd.ArgumentIsAhead('matter'):
|
|
matterId, matterNameId = getMatterItem(v)
|
|
exportId, exportName, exportNameId = convertExportNameToID(v, getString(Cmd.OB_EXPORT_ITEM), matterId, matterNameId)
|
|
else:
|
|
exportName = getString(Cmd.OB_EXPORT_ITEM)
|
|
zipToStdout = False
|
|
bucketMatchPattern = objectMatchPattern = None
|
|
downloadAttempts = 1
|
|
retryInterval = 30
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v)
|
|
exportId, exportName, exportNameId = convertExportNameToID(v, exportName, matterId, matterNameId)
|
|
elif myarg == 'targetname':
|
|
targetName = getString(Cmd.OB_FILE_NAME)
|
|
elif myarg == 'targetfolder':
|
|
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
elif myarg == 'noverify':
|
|
verifyFiles = False
|
|
elif myarg == 'noextract':
|
|
extractFiles = False
|
|
elif myarg == 'ziptostdout':
|
|
zipToStdout = True
|
|
verifyFiles = extractFiles = False
|
|
elif myarg == 'bucketmatchpattern':
|
|
bucketMatchPattern = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'objectmatchpattern':
|
|
objectMatchPattern = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'downloadattempts':
|
|
downloadAttempts = getInteger(minVal=1)
|
|
elif myarg == 'retryinterval':
|
|
retryInterval = getInteger(minVal=10)
|
|
else:
|
|
unknownArgumentExit()
|
|
attempts = 0
|
|
while True:
|
|
try:
|
|
export = callGAPI(v.matters().exports(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, exportId=exportId)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], str(e))
|
|
return
|
|
if export['status'] == 'COMPLETED':
|
|
break
|
|
attempts += 1
|
|
entityActionNotPerformedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], Msg.EXPORT_NOT_COMPLETE.format(export['status']), attempts, downloadAttempts)
|
|
if attempts >= downloadAttempts:
|
|
return
|
|
time.sleep(retryInterval)
|
|
jcount = len(export['cloudStorageSink']['files'])
|
|
if not zipToStdout:
|
|
if not bucketMatchPattern and not objectMatchPattern:
|
|
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], jcount, Ent.CLOUD_STORAGE_FILE)
|
|
else:
|
|
entityPerformActionModifierNumItems([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId], Msg.MAXIMUM_OF, jcount, Ent.CLOUD_STORAGE_FILE)
|
|
Ind.Increment()
|
|
j = 0
|
|
extCounts = {}
|
|
for s_file in export['cloudStorageSink']['files']:
|
|
j += 1
|
|
bucket = s_file['bucketName']
|
|
s_object = s_file['objectName']
|
|
if ((bucketMatchPattern and not bucketMatchPattern.match(bucket)) or
|
|
(objectMatchPattern and not objectMatchPattern.match(s_object))):
|
|
continue
|
|
filename = os.path.join(targetFolder, cleanFilename(s_object))
|
|
if zipToStdout and not ZIP_EXTENSION_PATTERN.match(filename):
|
|
continue
|
|
if targetName:
|
|
_, s_objectFilename = s_object.rsplit('/', 1)
|
|
if s_objectFilename.find('.') != -1:
|
|
s_objectFilename, s_objectExtension = s_objectFilename.rsplit('.', 1)
|
|
else:
|
|
s_objectExtension = ''
|
|
if targetName.find('#') == -1:
|
|
extCounts.setdefault(s_objectExtension, 0)
|
|
extCounts[s_objectExtension] += 1
|
|
if s_objectExtension:
|
|
filename = f"{targetName}-{extCounts[s_objectExtension]}.{s_objectExtension}"
|
|
else:
|
|
filename = f"{targetName}-{extCounts[s_objectExtension]}"
|
|
else:
|
|
filename = targetName.replace('#objectname#', s_object).replace('#filename#', s_objectFilename).replace('#extension#', s_objectExtension)
|
|
filename = os.path.join(targetFolder, cleanFilename(filename))
|
|
Act.Set(Act.DOWNLOAD)
|
|
if not zipToStdout:
|
|
performAction(Ent.CLOUD_STORAGE_FILE, s_object, j, jcount)
|
|
Ind.Increment()
|
|
_getCloudStorageObject(s, bucket, s_object, filename,
|
|
expectedMd5=s_file['md5Hash'] if verifyFiles else None,
|
|
zipToStdout=zipToStdout, j=j, jcount=jcount)
|
|
if extractFiles and ZIP_EXTENSION_PATTERN.match(filename):
|
|
Act.Set(Act.EXTRACT)
|
|
extract_nested_zip(filename)
|
|
Act.Set(Act.DOWNLOAD)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
def _cleanVaultHold(hold, cd):
|
|
if cd is not None:
|
|
if 'accounts' in hold:
|
|
accountType = 'group' if hold['corpus'] == 'GROUPS' else 'user'
|
|
for i in range(0, len(hold['accounts'])):
|
|
hold['accounts'][i]['email'] = convertUIDtoEmailAddress(f'uid:{hold["accounts"][i]["accountId"]}', cd, accountType)
|
|
if 'orgUnit' in hold:
|
|
hold['orgUnit']['orgUnitPath'] = convertOrgUnitIDtoPath(cd, hold['orgUnit']['orgUnitId'])
|
|
query = hold.get('query')
|
|
if query:
|
|
if 'driveQuery' in hold['query']:
|
|
hold['query']['driveQuery'].pop('includeTeamDriveFiles', None)
|
|
|
|
VAULT_HOLD_TIME_OBJECTS = {'holdTime', 'updateTime', 'startTime', 'endTime'}
|
|
|
|
def _showVaultHold(matterNameId, hold, cd, FJQC, k=0, kcount=0):
|
|
_cleanVaultHold(hold, cd)
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(hold, timeObjects=VAULT_HOLD_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
|
|
return
|
|
if matterNameId is not None:
|
|
printEntity([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, formatVaultNameId(hold['name'], hold['holdId'])], k, kcount)
|
|
Ind.Increment()
|
|
showJSON(None, hold, timeObjects=VAULT_HOLD_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
def _getHoldQueryParameters(myarg, queryParameters):
|
|
if myarg == 'query':
|
|
queryParameters['queryLocation'] = Cmd.Location()
|
|
queryParameters['query'] = getString(Cmd.OB_QUERY)
|
|
elif myarg == 'terms':
|
|
queryParameters['terms'] = getString(Cmd.OB_STRING)
|
|
elif myarg in {'start', 'starttime'}:
|
|
queryParameters['startTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg in {'end', 'endtime'}:
|
|
queryParameters['endTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg == 'includerooms':
|
|
queryParameters['includeRooms'] = getBoolean()
|
|
elif myarg in {'includeshareddrives', 'includeteamdrives'}:
|
|
queryParameters['includeSharedDriveFiles'] = getBoolean()
|
|
elif myarg == 'covereddata':
|
|
queryParameters['coveredData'] = getChoice(VAULT_VOICE_COVERED_DATA_MAP, mapChoice=True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _setHoldQuery(body, queryParameters):
|
|
queryType = VAULT_CORPUS_QUERY_MAP[body['corpus']]
|
|
if queryType is None:
|
|
return
|
|
body['query'] = {queryType: {}}
|
|
if body['corpus'] == 'DRIVE':
|
|
if queryParameters.get('query'):
|
|
try:
|
|
body['query'][queryType] = json.loads(queryParameters['query'])
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
Cmd.SetLocation(queryParameters['queryLocation'])
|
|
usageErrorExit(f'{str(e)}: {queryParameters["query"]}')
|
|
elif queryParameters.get('includeSharedDriveFiles'):
|
|
body['query'][queryType]['includeSharedDriveFiles'] = queryParameters['includeSharedDriveFiles']
|
|
elif body['corpus'] in {'GROUPS', 'MAIL'}:
|
|
if queryParameters.get('query'):
|
|
body['query'][queryType]['terms'] = queryParameters['query']
|
|
elif queryParameters.get('terms'):
|
|
body['query'][queryType]['terms'] = queryParameters['terms']
|
|
if queryParameters.get('startTime'):
|
|
body['query'][queryType]['startTime'] = queryParameters['startTime']
|
|
if queryParameters.get('endTime'):
|
|
body['query'][queryType]['endTime'] = queryParameters['endTime']
|
|
elif body['corpus'] == 'HANGOUTS_CHAT':
|
|
if queryParameters.get('includeRooms'):
|
|
body['query'][queryType]['includeRooms'] = queryParameters['includeRooms']
|
|
elif body['corpus'] == 'VOICE':
|
|
if queryParameters.get('coveredData'):
|
|
body['query'][queryType]['coveredData'] = queryParameters['coveredData']
|
|
|
|
# gam create vaulthold|hold matter <MatterItem> [name <String>] corpus calendar|drive|mail|groups|hangouts_chat|voice
|
|
# [(accounts|groups|users <EmailItemList>) | (orgunit|org|ou <OrgUnit>)]
|
|
# [query <QueryVaultCorpus>]
|
|
# [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
|
# [includerooms <Boolean>]
|
|
# [covereddata calllogs|textmessages|voicemails]
|
|
# [includeshareddrives|includeteamdrives <Boolean>]
|
|
# [showdetails|returnidonly]
|
|
def doCreateVaultHold():
|
|
v = buildGAPIObject(API.VAULT)
|
|
body = {}
|
|
matterId = None
|
|
accounts = []
|
|
queryParameters = {}
|
|
returnIdOnly = showDetails = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v)
|
|
elif myarg == 'name':
|
|
body['name'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'corpus':
|
|
body['corpus'] = getChoice(VAULT_CORPUS_ARGUMENT_MAP, mapChoice=True)
|
|
elif myarg in {'accounts', 'users', 'groups'}:
|
|
accountsLocation = Cmd.Location()
|
|
accounts = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
body['orgUnit'] = {'orgUnitId': getOrgUnitId()[1]}
|
|
elif _getHoldQueryParameters(myarg, queryParameters):
|
|
pass
|
|
elif myarg == 'showdetails':
|
|
showDetails = True
|
|
returnIdOnly = False
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
showDetails = False
|
|
else:
|
|
unknownArgumentExit()
|
|
if matterId is None:
|
|
missingArgumentExit('matter')
|
|
if not body.get('corpus'):
|
|
missingArgumentExit(f'corpus {"|".join(VAULT_CORPUS_ARGUMENT_MAP)}')
|
|
if 'name' not in body:
|
|
body['name'] = f'GAM {body["corpus"]} Hold - {ISOformatTimeStamp(todaysTime())}'
|
|
_setHoldQuery(body, queryParameters)
|
|
if accounts:
|
|
body['accounts'] = []
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
accountType = 'group' if body['corpus'] == 'GROUPS' else 'user'
|
|
for account in accounts:
|
|
body['accounts'].append({'accountId': convertEmailAddressToUID(account, cd, accountType, accountsLocation)})
|
|
try:
|
|
hold = callGAPI(v.matters().holds(), 'create',
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.BAD_REQUEST, GAPI.BACKEND_ERROR, GAPI.FAILED_PRECONDITION,
|
|
GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, body=body)
|
|
if not returnIdOnly:
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, formatVaultNameId(hold['name'], hold['holdId'])])
|
|
if showDetails:
|
|
_showVaultHold(None, hold, None, None)
|
|
else:
|
|
writeStdout(f'{hold["holdId"]}\n')
|
|
except (GAPI.alreadyExists, GAPI.badRequest, GAPI.backendError, GAPI.failedPrecondition,
|
|
GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, body.get('name')], str(e))
|
|
|
|
# gam update vaulthold|hold <HoldItem> matter <MatterItem>
|
|
# [([addaccounts|addgroups|addusers <EmailItemList>] [removeaccounts|removegroups|removeusers <EmailItemList>]) | (orgunit|org|ou <OrgUnit>)]
|
|
# [query <QueryVaultCorpus>]
|
|
# [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
|
# [includerooms <Boolean>]
|
|
# [covereddata calllogs|textmessages|voicemails]
|
|
# [includeshareddrives|includeteamdrives <Boolean>]
|
|
# [showdetails]
|
|
def doUpdateVaultHold():
|
|
v = buildGAPIObject(API.VAULT)
|
|
holdName = getString(Cmd.OB_HOLD_ITEM)
|
|
body = {}
|
|
cd = matterId = None
|
|
addAccounts = []
|
|
addAccountIds = []
|
|
removeAccounts = []
|
|
removeAccountIds = []
|
|
queryParameters = {}
|
|
showDetails = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v)
|
|
holdId, holdName, holdNameId = convertHoldNameToID(v, holdName, matterId, matterNameId)
|
|
elif myarg in {'addusers', 'addaccounts', 'addgroups'}:
|
|
addAccountsLocation = Cmd.Location()
|
|
addAccounts = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
|
|
elif myarg in {'removeusers', 'removeaccounts', 'removegroups'}:
|
|
removeAccountsLocation = Cmd.Location()
|
|
removeAccounts = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
body['orgUnit'] = {'orgUnitId': getOrgUnitId()[1]}
|
|
elif _getHoldQueryParameters(myarg, queryParameters):
|
|
pass
|
|
elif myarg == 'showdetails':
|
|
showDetails = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if matterId is None:
|
|
missingArgumentExit('matter')
|
|
try:
|
|
old_body = callGAPI(v.matters().holds(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, holdId=holdId, fields='name,corpus,query,orgUnit')
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], str(e))
|
|
return
|
|
accountType = 'group' if old_body['corpus'] == 'GROUPS' else 'user'
|
|
if addAccounts:
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
for account in addAccounts:
|
|
addAccountIds.append({'email': account, 'id': convertEmailAddressToUID(account, cd, accountType, addAccountsLocation)})
|
|
if removeAccounts:
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
for account in removeAccounts:
|
|
removeAccountIds.append({'email': account, 'id': convertEmailAddressToUID(account, cd, accountType, removeAccountsLocation)})
|
|
if queryParameters or body.get('orgUnit'):
|
|
body['corpus'] = old_body['corpus']
|
|
if 'orgUnit' in old_body and 'orgUnit' not in body:
|
|
# bah, API requires this to be sent on update even when it's not changing
|
|
body['orgUnit'] = old_body['orgUnit']
|
|
if queryParameters:
|
|
_setHoldQuery(body, queryParameters)
|
|
else:
|
|
body['query'] = old_body['query']
|
|
if body:
|
|
try:
|
|
hold = callGAPI(v.matters().holds(), 'update',
|
|
throwReas=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, holdId=holdId, body=body)
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId])
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], str(e))
|
|
return
|
|
jcount = len(addAccountIds)
|
|
if jcount > 0:
|
|
Act.Set(Act.ADD)
|
|
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], jcount, Ent.ACCOUNT)
|
|
Ind.Increment()
|
|
j = 0
|
|
for account in addAccountIds:
|
|
j += 1
|
|
try:
|
|
callGAPI(v.matters().holds().accounts(), 'create',
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.BACKEND_ERROR, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, holdId=holdId, body={'accountId': account['id']})
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId, Ent.ACCOUNT, account['email']], j, jcount)
|
|
except (GAPI.alreadyExists, GAPI.backendError) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId, Ent.ACCOUNT, account['email']], str(e), j, jcount)
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, None], str(e))
|
|
return
|
|
Ind.Decrement()
|
|
jcount = len(removeAccountIds)
|
|
if jcount > 0:
|
|
Act.Set(Act.REMOVE)
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], jcount, Ent.ACCOUNT)
|
|
Ind.Increment()
|
|
j = 0
|
|
for account in removeAccountIds:
|
|
j += 1
|
|
try:
|
|
callGAPI(v.matters().holds().accounts(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BACKEND_ERROR, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, holdId=holdId, accountId=account['id'])
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId, Ent.ACCOUNT, account['email']], j, jcount)
|
|
except (GAPI.alreadyExists, GAPI.backendError) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId, Ent.ACCOUNT, account['email']], str(e), j, jcount)
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, None], str(e))
|
|
return
|
|
Ind.Decrement()
|
|
if showDetails:
|
|
_showVaultHold(None, hold, cd, None)
|
|
|
|
# gam delete vaulthold|hold <HoldItem> matter <MatterItem>
|
|
# gam delete vaulthold|hold <MatterItem> <HoldItem>
|
|
def doDeleteVaultHold():
|
|
v = buildGAPIObject(API.VAULT)
|
|
if not Cmd.ArgumentIsAhead('matter'):
|
|
matterId, matterNameId = getMatterItem(v)
|
|
holdId, holdName, holdNameId = convertHoldNameToID(v, getString(Cmd.OB_HOLD_ITEM), matterId, matterNameId)
|
|
else:
|
|
holdName = getString(Cmd.OB_HOLD_ITEM)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v)
|
|
holdId, holdName, holdNameId = convertHoldNameToID(v, holdName, matterId, matterNameId)
|
|
else:
|
|
unknownArgumentExit()
|
|
try:
|
|
callGAPI(v.matters().holds(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, holdId=holdId)
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId])
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], str(e))
|
|
|
|
VAULT_HOLD_FIELDS_CHOICE_MAP = {
|
|
'accounts': 'accounts',
|
|
'accounts.acountid': 'accounts.accountId',
|
|
'accounts.email': 'accounts.email',
|
|
'accounts.firstname': 'accounts.firstName',
|
|
'accounts.holdtime': 'accounts.holdTime',
|
|
'accounts.lastname': 'accounts.lastName',
|
|
'corpus': 'corpus',
|
|
'holdid': 'holdId',
|
|
'name': 'name',
|
|
'orgunit': 'orgUnit',
|
|
'orgunit.holdtime': 'orgUnit.holdTime',
|
|
'orgunit.ordunitid': 'orgUnit.orgUnitId',
|
|
'query': 'query',
|
|
'updatetime': 'updateTime',
|
|
}
|
|
|
|
# gam info vaulthold|hold <HoldItem> matter <MatterItem>
|
|
# [fields <VaultHoldFieldNameList>] [shownames]
|
|
# [formatjson]
|
|
# gam info vaulthold|hold <MatterItem> <HoldItem>
|
|
# [fields <VaultHoldFieldNameList>] [shownames]
|
|
# [formatjson]
|
|
def doInfoVaultHold():
|
|
v = buildGAPIObject(API.VAULT)
|
|
if not Cmd.ArgumentIsAhead('matter'):
|
|
matterId, matterNameId = getMatterItem(v)
|
|
holdId, holdName, holdNameId = convertHoldNameToID(v, getString(Cmd.OB_HOLD_ITEM), matterId, matterNameId)
|
|
else:
|
|
holdName = getString(Cmd.OB_HOLD_ITEM)
|
|
cd = None
|
|
fieldsList = []
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v)
|
|
holdId, holdName, holdNameId = convertHoldNameToID(v, holdName, matterId, matterNameId)
|
|
elif myarg == 'shownames':
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
elif getFieldsList(myarg, VAULT_HOLD_FIELDS_CHOICE_MAP, fieldsList, initialField=['holdId', 'name']):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
try:
|
|
hold = callGAPI(v.matters().holds(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, holdId=holdId, fields=fields)
|
|
_showVaultHold(matterNameId, hold, cd, FJQC)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId], str(e))
|
|
|
|
PRINT_VAULT_HOLDS_TITLES = ['matterId', 'matterName', 'holdId', 'name', 'updateTime']
|
|
|
|
# gam print vaultholds|holds [todrive <ToDriveAttribute>*] [matters <MatterItemList>]
|
|
# [fields <VaultHoldFieldNameList>] [shownames]
|
|
# [formatjson [quotechar <Character>]]
|
|
# [oneitemperrow]
|
|
# gam show vaultholds|holds [matters <MatterItemList>]
|
|
# [fields <VaultHoldFieldNameList>] [shownames]
|
|
# [formatjson]
|
|
def doPrintShowVaultHolds():
|
|
def _printVaultHold(hold):
|
|
row = flattenJSON(hold, flattened={'matterId': matterId, 'matterName': matterName}, timeObjects=VAULT_HOLD_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'matterId': matterId, 'matterName': matterName,
|
|
'holdId': hold['holdId'], 'name': hold['name'],
|
|
'JSON': json.dumps(cleanJSON(hold, timeObjects=VAULT_HOLD_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
|
|
|
|
v = buildGAPIObject(API.VAULT)
|
|
csvPF = CSVPrintFile(PRINT_VAULT_HOLDS_TITLES, 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar()
|
|
matters = []
|
|
cd = None
|
|
fieldsList = []
|
|
oneItemPerRow = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'matter', 'matters'}:
|
|
matters = shlexSplitList(getString(Cmd.OB_MATTER_ITEM_LIST))
|
|
elif myarg == 'shownames':
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
elif getFieldsList(myarg, VAULT_HOLD_FIELDS_CHOICE_MAP, fieldsList, initialField=['holdId', 'name']):
|
|
pass
|
|
elif csvPF and myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
fields = getItemFieldsFromFieldsList('holds', fieldsList)
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(PRINT_VAULT_HOLDS_TITLES+['JSON'])
|
|
if not matters:
|
|
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=' (OPEN)')
|
|
try:
|
|
results = callGAPIpages(v.matters(), 'list', 'matters',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_HOLD, None], str(e))
|
|
return
|
|
else:
|
|
results = []
|
|
for matter in matters:
|
|
matterId, matterName, _, state = convertMatterNameToID(v, matter)
|
|
results.append({'matterId': matterId, 'name': matterName, 'state': state})
|
|
jcount = len(results)
|
|
if not csvPF:
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
j = 0
|
|
for matter in results:
|
|
j += 1
|
|
matterId = matter['matterId']
|
|
matterName = matter['name']
|
|
matterNameId = formatVaultNameId(matterName, matterId)
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(Ent.VAULT_HOLD, f'{Ent.Singular(Ent.VAULT_MATTER)}: {matterNameId}', j, jcount)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
if matter['state'] == 'OPEN':
|
|
try:
|
|
holds = callGAPIpages(v.matters().holds(), 'list', 'holds',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, fields=fields)
|
|
except GAPI.failedPrecondition:
|
|
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
|
|
continue
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_HOLD, None], str(e))
|
|
break
|
|
else:
|
|
warnMatterNotOpen(None, matter, matterNameId, j, jcount)
|
|
continue
|
|
kcount = len(holds)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], kcount, Ent.VAULT_HOLD, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for hold in holds:
|
|
k += 1
|
|
_showVaultHold(matterNameId, hold, cd, FJQC, k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for hold in holds:
|
|
_cleanVaultHold(hold, cd)
|
|
if not oneItemPerRow or not hold.get('accounts', []):
|
|
_printVaultHold(hold)
|
|
else:
|
|
for account in hold.pop('accounts'):
|
|
hold['account'] = account
|
|
_printVaultHold(hold)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Vault Holds')
|
|
|
|
PRINT_USER_VAULT_HOLDS_TITLES = ['User', 'matterId', 'matterName', 'holdId', 'name', 'orgUnitId', 'orgUnitPath']
|
|
|
|
# gam <UserTypeEntity> print vaultholds|holds [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show vaultholds|holds
|
|
def printShowUserVaultHolds(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
v = buildGAPIObject(API.VAULT)
|
|
csvPF = CSVPrintFile(PRINT_USER_VAULT_HOLDS_TITLES, 'sortall') if Act.csvFormat() else None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
unknownArgumentExit()
|
|
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=' (OPEN)')
|
|
try:
|
|
matters = callGAPIpages(v.matters(), 'list', 'matters',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_HOLD, None], str(e))
|
|
return
|
|
jcount = len(matters)
|
|
j = 0
|
|
for matter in matters:
|
|
j += 1
|
|
matterId = matter['matterId']
|
|
matterName = matter['name']
|
|
matterNameId = formatVaultNameId(matterName, matterId)
|
|
printGettingAllEntityItemsForWhom(Ent.VAULT_HOLD, f'{Ent.Singular(Ent.VAULT_MATTER)}: {matterNameId}', j, jcount)
|
|
try:
|
|
matter['holds'] = callGAPIpages(v.matters().holds(), 'list', 'holds',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, fields='holds(holdId,name,accounts(accountId,email),orgUnit(orgUnitId)),nextPageToken')
|
|
except GAPI.failedPrecondition:
|
|
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_HOLD, None], str(e), j, jcount)
|
|
totalHolds = 0
|
|
_, _, entityList = getEntityArgument(entityList)
|
|
for user in entityList:
|
|
user = normalizeEmailAddressOrUID(user)
|
|
orgUnits = getAllParentOrgUnitsForUser(cd, user)
|
|
for matter in matters:
|
|
matterId = matter['matterId']
|
|
matterName = matter['name']
|
|
matterNameId = formatVaultNameId(matterName, matterId)
|
|
for hold in matter.get('holds', []):
|
|
if 'orgUnit' in hold:
|
|
orgUnitId = hold['orgUnit'].get('orgUnitId')
|
|
if orgUnitId in orgUnits:
|
|
if not csvPF:
|
|
printEntityMessage([Ent.USER, user,
|
|
Ent.ORGANIZATIONAL_UNIT, orgUnits[orgUnitId],
|
|
Ent.VAULT_MATTER, formatVaultNameId(matter['name'], matter['matterId']),
|
|
Ent.VAULT_HOLD, formatVaultNameId(hold['name'], hold['holdId'])],
|
|
Msg.ON_VAULT_HOLD)
|
|
else:
|
|
csvPF.WriteRow({'User': user, 'matterId': matterId, 'matterName': matterName,
|
|
'holdId': hold['holdId'], 'name': hold['name'],
|
|
'orgUnitId': orgUnitId, 'orgUnitPath': orgUnits[orgUnitId]})
|
|
totalHolds += 1
|
|
else:
|
|
for account in hold.get('accounts', []):
|
|
if user == account.get('email', '').lower() or user == account.get('accountId', ''):
|
|
if not csvPF:
|
|
printEntityMessage([Ent.USER, user,
|
|
Ent.VAULT_MATTER, formatVaultNameId(matter['name'], matter['matterId']),
|
|
Ent.VAULT_HOLD, formatVaultNameId(hold['name'], hold['holdId'])],
|
|
Msg.ON_VAULT_HOLD)
|
|
else:
|
|
csvPF.WriteRow({'User': user, 'matterId': matterId, 'matterName': matterName,
|
|
'holdId': hold['holdId'], 'name': hold['name']})
|
|
totalHolds += 1
|
|
break
|
|
if csvPF:
|
|
csvPF.writeCSVfile('User Vault Holds')
|
|
else:
|
|
printKeyValueList(['Total Holds', totalHolds])
|
|
|
|
def _cleanVaultQuery(query, cd):
|
|
if 'query' in query:
|
|
if cd is not None:
|
|
if 'orgUnitInfo' in query['query']:
|
|
query['query']['orgUnitInfo']['orgUnitPath'] = convertOrgUnitIDtoPath(cd, query['query']['orgUnitInfo']['orgUnitId'])
|
|
if 'sharedDriveInfo' in query['query']:
|
|
query['query']['sharedDriveInfo']['sharedDriveNames'] = []
|
|
for sharedDriveId in query['query']['sharedDriveInfo']['sharedDriveIds']:
|
|
query['query']['sharedDriveInfo']['sharedDriveNames'].append(_getSharedDriveNameFromId(sharedDriveId))
|
|
query['query'].pop('searchMethod', None)
|
|
query['query'].pop('teamDriveInfo', None)
|
|
|
|
VAULT_QUERY_TIME_OBJECTS = {'createTime', 'endTime', 'startTime', 'versionDate'}
|
|
|
|
def _showVaultQuery(matterNameId, query, cd, FJQC, k=0, kcount=0):
|
|
_cleanVaultQuery(query, cd)
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(query, timeObjects=VAULT_QUERY_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
|
|
return
|
|
printEntity([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, formatVaultNameId(query['displayName'], query['savedQueryId'])], k, kcount)
|
|
Ind.Increment()
|
|
showJSON(None, query, timeObjects=VAULT_QUERY_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
VAULT_QUERY_FIELDS_CHOICE_MAP = {
|
|
'createtime': 'createTime',
|
|
'displayname': 'displayName',
|
|
'matterid': 'matterId',
|
|
'name': 'displayName',
|
|
'query': 'query',
|
|
'queryid': 'savedQueryId',
|
|
'savedqueryid': 'savedQueryId',
|
|
}
|
|
|
|
# gam info vaultquery <QueryItem> matter <MatterItem>
|
|
# [fields <VaultQueryFieldNameList>] [shownames]
|
|
# [formatjson]
|
|
# gam info vaultquery <MatterItem> <QueryItem>
|
|
# [fields <VaultQueryFieldNameList>] [shownames]
|
|
# [formatjson]
|
|
def doInfoVaultQuery():
|
|
v = buildGAPIObject(API.VAULT)
|
|
if not Cmd.ArgumentIsAhead('matter'):
|
|
matterId, matterNameId = getMatterItem(v)
|
|
queryId, queryName, queryNameId = convertQueryNameToID(v, getString(Cmd.OB_QUERY_ITEM), matterId, matterNameId)
|
|
else:
|
|
queryName = getString(Cmd.OB_QUERY_ITEM)
|
|
cd = None
|
|
fieldsList = []
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'matter':
|
|
matterId, matterNameId = getMatterItem(v)
|
|
queryId, queryName, queryNameId = convertQueryNameToID(v, queryName, matterId, matterNameId)
|
|
elif myarg == 'shownames':
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
_, GM.Globals[GM.ADMIN_DRIVE] = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
|
|
if GM.Globals[GM.ADMIN_DRIVE] is None:
|
|
return
|
|
elif getFieldsList(myarg, VAULT_QUERY_FIELDS_CHOICE_MAP, fieldsList, initialField=['savedQueryId', 'displayName']):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
try:
|
|
query = callGAPI(v.matters().savedQueries(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, savedQueryId=queryId, fields=fields)
|
|
_showVaultQuery(matterNameId, query, cd, FJQC)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_QUERY, queryNameId], str(e))
|
|
|
|
PRINT_VAULT_QUERIES_TITLES = ['matterId', 'matterName', 'savedQueryId', 'displayName']
|
|
|
|
# gam print vaultqueries [todrive <ToDriveAttribute>*] [matters <MatterItemList>]
|
|
# [fields <VaultQueryFieldNameList>] [shownames]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show vaultqueries [matters <MatterItemList>]
|
|
# [fields <VaultQueryFieldNameList>] [shownames]
|
|
# [formatjson]
|
|
def doPrintShowVaultQueries():
|
|
v = buildGAPIObject(API.VAULT)
|
|
csvPF = CSVPrintFile(PRINT_VAULT_QUERIES_TITLES, 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
matters = []
|
|
cd = None
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'matter', 'matters'}:
|
|
matters = shlexSplitList(getString(Cmd.OB_MATTER_ITEM_LIST))
|
|
elif myarg == 'shownames':
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
_, GM.Globals[GM.ADMIN_DRIVE] = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
|
|
if GM.Globals[GM.ADMIN_DRIVE] is None:
|
|
return
|
|
elif getFieldsList(myarg, VAULT_QUERY_FIELDS_CHOICE_MAP, fieldsList, initialField=['savedQueryId', 'displayName']):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
fields = getItemFieldsFromFieldsList('savedQueries', fieldsList)
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(PRINT_VAULT_QUERIES_TITLES+['JSON'])
|
|
if not matters:
|
|
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=' (OPEN)')
|
|
try:
|
|
results = callGAPIpages(v.matters(), 'list', 'matters',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_QUERY, None], str(e))
|
|
return
|
|
else:
|
|
results = []
|
|
for matter in matters:
|
|
matterId, matterName, _, state = convertMatterNameToID(v, matter)
|
|
results.append({'matterId': matterId, 'name': matterName, 'state': state})
|
|
jcount = len(results)
|
|
if not csvPF:
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
j = 0
|
|
for matter in results:
|
|
j += 1
|
|
matterId = matter['matterId']
|
|
matterName = matter['name']
|
|
matterNameId = formatVaultNameId(matterName, matterId)
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(Ent.VAULT_QUERY, f'{Ent.Singular(Ent.VAULT_MATTER)}: {matterNameId}', j, jcount)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
if matter['state'] == 'OPEN':
|
|
try:
|
|
queries = callGAPIpages(v.matters().savedQueries(), 'list', 'savedQueries',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, fields=fields)
|
|
except GAPI.failedPrecondition:
|
|
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
|
|
continue
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_QUERY, None], str(e))
|
|
break
|
|
else:
|
|
warnMatterNotOpen(None, matter, matterNameId, j, jcount)
|
|
continue
|
|
kcount = len(queries)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], kcount, Ent.VAULT_QUERY, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for query in queries:
|
|
k += 1
|
|
_showVaultQuery(matterNameId, query, cd, FJQC, k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for query in queries:
|
|
_cleanVaultQuery(query, cd)
|
|
row = flattenJSON(query, flattened={'matterId': matterId, 'matterName': matterName}, timeObjects=VAULT_QUERY_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'matterId': matterId, 'matterName': matterName,
|
|
'savedQueryId': query['savedQueryId'], 'displayName': query['displayName'],
|
|
'JSON': json.dumps(cleanJSON(query, timeObjects=VAULT_QUERY_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Vault Saved Queries')
|
|
|
|
def validateCollaborators(cd):
|
|
collaborators = []
|
|
for collaborator in getEntityList(Cmd.OB_COLLABORATOR_ENTITY):
|
|
collaborators.append({'email': collaborator, 'id': convertEmailAddressToUID(collaborator, cd)})
|
|
return collaborators
|
|
|
|
def _cleanVaultMatter(matter, cd):
|
|
if cd is not None:
|
|
if 'matterPermissions' in matter:
|
|
for i in range(0, len(matter['matterPermissions'])):
|
|
matter['matterPermissions'][i]['email'] = convertUserIDtoEmail(matter["matterPermissions"][i]["accountId"], cd)
|
|
|
|
def _showVaultMatter(matter, cd, FJQC, j=0, jcount=0):
|
|
_cleanVaultMatter(matter, cd)
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(matter), ensure_ascii=False, sort_keys=False))
|
|
return
|
|
printEntity([Ent.VAULT_MATTER, formatVaultNameId(matter['name'], matter['matterId'])], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, matter)
|
|
Ind.Decrement()
|
|
|
|
# gam create vaultmatter|matter [name <String>] [description <string>]
|
|
# [collaborator|collaborators <CollaboratorItemList>] [sendemails <Boolean>] [ccme <Boolean>]
|
|
# [showdetails|returnidonly]
|
|
def doCreateVaultMatter():
|
|
v = buildGAPIObject(API.VAULT)
|
|
body = {}
|
|
cbody = {'matterPermission': {'role': 'COLLABORATOR', 'accountId': ''}, 'sendEmails': False, 'ccMe': False}
|
|
collaborators = []
|
|
cd = None
|
|
returnIdOnly = showDetails = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
body['name'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'description':
|
|
body['description'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'collaborator', 'collaborators'}:
|
|
if not cd:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
collaborators.extend(validateCollaborators(cd))
|
|
elif myarg == 'sendemails':
|
|
cbody['sendEmails'] = getBoolean()
|
|
elif myarg == 'ccme':
|
|
cbody['ccMe'] = getBoolean()
|
|
elif myarg == 'showdetails':
|
|
showDetails = True
|
|
returnIdOnly = False
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
showDetails = False
|
|
else:
|
|
unknownArgumentExit()
|
|
if 'name' not in body:
|
|
body['name'] = f'GAM Matter - {ISOformatTimeStamp(todaysTime())}'
|
|
try:
|
|
matter = callGAPI(v.matters(), 'create',
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
body=body)
|
|
matterId = matter['matterId']
|
|
matterNameId = formatVaultNameId(matter['name'], matterId)
|
|
if not returnIdOnly:
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId])
|
|
else:
|
|
writeStdout(f'{matterId}\n')
|
|
except (GAPI.alreadyExists, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, body['name']], str(e))
|
|
return
|
|
jcount = len(collaborators)
|
|
if jcount > 0:
|
|
Act.Set(Act.ADD)
|
|
if not returnIdOnly:
|
|
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], jcount, Ent.COLLABORATOR)
|
|
Ind.Increment()
|
|
j = 0
|
|
for collaborator in collaborators:
|
|
j += 1
|
|
cbody['matterPermission']['accountId'] = collaborator['id']
|
|
try:
|
|
callGAPI(v.matters(), 'addPermissions',
|
|
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, body=cbody)
|
|
if not returnIdOnly:
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.COLLABORATOR, collaborator['email']], j, jcount)
|
|
except (GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
|
|
break
|
|
Ind.Decrement()
|
|
if showDetails:
|
|
_showVaultMatter(matter, cd, None)
|
|
|
|
VAULT_MATTER_ACTIONS = {
|
|
'close': Act.CLOSE,
|
|
'reopen': Act.REOPEN,
|
|
'delete': Act.DELETE,
|
|
'undelete': Act.UNDELETE,
|
|
}
|
|
|
|
def doActionVaultMatter(action, matterId=None, matterNameId=None, v=None):
|
|
if v is None:
|
|
v = buildGAPIObject(API.VAULT)
|
|
matterId, matterNameId = getMatterItem(v)
|
|
else:
|
|
Act.Set(VAULT_MATTER_ACTIONS[action])
|
|
checkForExtraneousArguments()
|
|
action_kwargs = {} if action == 'delete' else {'body': {}}
|
|
try:
|
|
callGAPI(v.matters(), action,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, **action_kwargs)
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId])
|
|
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
|
|
|
|
# gam close vaultmatter|matter <MatterItem>
|
|
def doCloseVaultMatter():
|
|
doActionVaultMatter('close')
|
|
|
|
# gam reopen vaultmatter|matter <MatterItem>
|
|
def doReopenVaultMatter():
|
|
doActionVaultMatter('reopen')
|
|
|
|
# gam delete vaultmatter|matter <MatterItem>
|
|
def doDeleteVaultMatter():
|
|
doActionVaultMatter('delete')
|
|
|
|
# gam undelete vaultmatter|matter <MatterItem>
|
|
def doUndeleteVaultMatter():
|
|
doActionVaultMatter('undelete')
|
|
|
|
# gam update vaultmatter|matter <MatterItem> [name <String>] [description <string>]
|
|
# [addcollaborator|addcollaborators <CollaboratorItemList>] [removecollaborator|removecollaborators <CollaboratorItemList>]
|
|
def doUpdateVaultMatter():
|
|
v = buildGAPIObject(API.VAULT)
|
|
matterId, matterNameId = getMatterItem(v)
|
|
body = {}
|
|
addCollaborators = []
|
|
removeCollaborators = []
|
|
cd = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'action':
|
|
action = getChoice(VAULT_MATTER_ACTIONS)
|
|
doActionVaultMatter(action, matterId, matterNameId, v)
|
|
return
|
|
if myarg == 'name':
|
|
body['name'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'description':
|
|
body['description'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'addcollaborator', 'addcollaborators'}:
|
|
if not cd:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
addCollaborators.extend(validateCollaborators(cd))
|
|
elif myarg in {'removecollaborator', 'removecollaborators'}:
|
|
if not cd:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
removeCollaborators.extend(validateCollaborators(cd))
|
|
else:
|
|
unknownArgumentExit()
|
|
if body:
|
|
try:
|
|
if 'name' not in body or 'description' not in body:
|
|
# bah, API requires name/description to be sent on update even when it's not changing
|
|
result = callGAPI(v.matters(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, view='BASIC')
|
|
body.setdefault('name', result['name'])
|
|
body.setdefault('description', result.get('description'))
|
|
callGAPI(v.matters(), 'update',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN],
|
|
matterId=matterId, body=body)
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId])
|
|
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
|
|
return
|
|
jcount = len(addCollaborators)
|
|
if jcount > 0:
|
|
Act.Set(Act.ADD)
|
|
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], jcount, Ent.COLLABORATOR)
|
|
Ind.Increment()
|
|
j = 0
|
|
for collaborator in addCollaborators:
|
|
j += 1
|
|
try:
|
|
callGAPI(v.matters(), 'addPermissions',
|
|
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, body={'matterPermission': {'role': 'COLLABORATOR', 'accountId': collaborator['id']}})
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.COLLABORATOR, collaborator['email']], j, jcount)
|
|
except (GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
|
|
break
|
|
Ind.Decrement()
|
|
jcount = len(removeCollaborators)
|
|
if jcount > 0:
|
|
Act.Set(Act.REMOVE)
|
|
entityPerformActionNumItems([Ent.VAULT_MATTER, matterNameId], jcount, Ent.COLLABORATOR)
|
|
Ind.Increment()
|
|
j = 0
|
|
for collaborator in removeCollaborators:
|
|
j += 1
|
|
try:
|
|
callGAPI(v.matters(), 'removePermissions',
|
|
throwReasons=[GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, body={'accountId': collaborator['id']})
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.COLLABORATOR, collaborator['email']], j, jcount)
|
|
except (GAPI.failedPrecondition, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
|
|
break
|
|
Ind.Decrement()
|
|
|
|
VAULT_MATTER_FIELDS_CHOICE_MAP = {
|
|
'matterid': 'matterId',
|
|
'name': 'name',
|
|
'description': 'description',
|
|
'state': 'state',
|
|
'matterpermissions': 'matterPermissions',
|
|
}
|
|
|
|
# gam info vaultmatter|matter <MatterItem>
|
|
# [basic|full|(fields <VaultMatterFieldNameList>)]
|
|
# [formatjson]
|
|
def doInfoVaultMatter():
|
|
v = buildGAPIObject(API.VAULT)
|
|
matterId, matterNameId = getMatterItem(v)
|
|
view = 'FULL'
|
|
fieldsList = []
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in PROJECTION_CHOICE_MAP:
|
|
view = PROJECTION_CHOICE_MAP[myarg]
|
|
elif getFieldsList(myarg, VAULT_MATTER_FIELDS_CHOICE_MAP, fieldsList, initialField=['matterId', 'name']):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
try:
|
|
matter = callGAPI(v.matters(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, view=view, fields=fields)
|
|
cd = buildGAPIObject(API.DIRECTORY) if 'matterPermissions' in matter else None
|
|
_showVaultMatter(matter, cd, FJQC)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
|
|
|
|
VAULT_MATTER_STATE_MAP = {'open': 'OPEN', 'closed': 'CLOSED', 'deleted': 'DELETED'}
|
|
PRINT_VAULT_MATTERS_TITLES = ['matterId', 'name', 'description', 'state']
|
|
|
|
# gam print vaultmatters|matters [todrive <ToDriveAttribute>*] [matterstate <MatterStateList>]
|
|
# [basic|full|(fields <VaultMatterFieldNameList>)]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show vaultmatters|matters [matterstate <MatterStateList>]
|
|
# [basic|full|(fields <VaultMatterFieldNameList>)]
|
|
# [formatjson]
|
|
def doPrintShowVaultMatters():
|
|
v = buildGAPIObject(API.VAULT)
|
|
csvPF = CSVPrintFile(PRINT_VAULT_MATTERS_TITLES, 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
cd = None
|
|
view = 'FULL'
|
|
fieldsList = []
|
|
matterStatesList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'matterstate':
|
|
for state in getString(Cmd.OB_STATE_NAME_LIST).lower().replace('_', '').replace(',', ' ').split():
|
|
if state in VAULT_MATTER_STATE_MAP:
|
|
matterStatesList.append(VAULT_MATTER_STATE_MAP[state])
|
|
else:
|
|
invalidChoiceExit(state, list(VAULT_MATTER_STATE_MAP), True)
|
|
elif myarg in PROJECTION_CHOICE_MAP:
|
|
view = PROJECTION_CHOICE_MAP[myarg]
|
|
elif getFieldsList(myarg, VAULT_MATTER_FIELDS_CHOICE_MAP, fieldsList, initialField=['matterId', 'name']):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if fieldsList and matterStatesList:
|
|
fieldsList.append('state')
|
|
fields = f'nextPageToken,matters({getFieldsFromFieldsList(fieldsList)})' if fieldsList else None
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(PRINT_VAULT_MATTERS_TITLES+['JSON'])
|
|
# If no states are set, there is no filtering; if 1 state is set, the API can filter; else GAM filters
|
|
matterStates = set()
|
|
stateParm = None
|
|
if matterStatesList:
|
|
if len(matterStatesList) == 1:
|
|
stateParm = matterStatesList[0]
|
|
else:
|
|
matterStates = set(matterStatesList)
|
|
qualifier = f' ({",".join(matterStatesList)})'
|
|
else:
|
|
qualifier = ''
|
|
printGettingAllAccountEntities(Ent.VAULT_MATTER, qualifier=qualifier)
|
|
try:
|
|
matters = callGAPIpages(v.matters(), 'list', 'matters',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=[GAPI.FORBIDDEN, GAPI.INVALID_ARGUMENT, GAPI.INVALID_ARGUMENT],
|
|
view=view, state=stateParm, fields=fields)
|
|
except (GAPI.forbidden, GAPI.invalidArgument, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, None], str(e))
|
|
return
|
|
jcount = len(matters)
|
|
if view == 'FULL':
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
if not csvPF:
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(jcount, Ent.VAULT_MATTER)
|
|
Ind.Increment()
|
|
j = 0
|
|
for matter in matters:
|
|
j += 1
|
|
if not matterStates or matter['state'] in matterStates:
|
|
_showVaultMatter(matter, cd, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for matter in matters:
|
|
if not matterStates or matter['state'] in matterStates:
|
|
_cleanVaultMatter(matter, cd)
|
|
row = flattenJSON(matter)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'matterId': matter['matterId'], 'name': matter['name'],
|
|
'JSON': json.dumps(cleanJSON(matter), ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Vault Matters')
|
|
|
|
PRINT_VAULT_COUNTS_TITLES = ['account', 'count', 'error']
|
|
|
|
# gam print vaultcounts [todrive <ToDriveAttributes>*]
|
|
# matter <MatterItem> corpus mail|groups
|
|
# (accounts <EmailAddressEntity>) | (orgunit|org|ou <OrgUnitPath>) | everyone
|
|
# [scope <all_data|held_data|unprocessed_data>]
|
|
# [terms <String>] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>] [timezone <TimeZone>]
|
|
# [excludedrafts <Boolean>]
|
|
# [wait <Integer>]
|
|
# gam print vaultcounts [todrive <ToDriveAttributes>*]
|
|
# matter <MatterItem> operation <String> [wait <Integer>]
|
|
def doPrintVaultCounts():
|
|
v = buildGAPIObject(API.VAULT)
|
|
csvPF = CSVPrintFile(PRINT_VAULT_COUNTS_TITLES, 'sortall')
|
|
matterId = name = None
|
|
operationWait = 15
|
|
body = {'view': 'ALL'}
|
|
query = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'matter':
|
|
matterId, _ = getMatterItem(v)
|
|
elif myarg == 'operation':
|
|
name = getString(Cmd.OB_STRING)
|
|
elif myarg in VAULT_QUERY_ARGS:
|
|
_buildVaultQuery(myarg, query, VAULT_COUNTS_CORPUS_ARGUMENT_MAP)
|
|
elif myarg == 'wait':
|
|
operationWait = getInteger(minVal=1)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not matterId:
|
|
missingArgumentExit('matter')
|
|
if name:
|
|
operation = {'name': name}
|
|
doWait = False
|
|
else:
|
|
body['query'] = query
|
|
_validateVaultQuery(body, VAULT_COUNTS_CORPUS_ARGUMENT_MAP)
|
|
try:
|
|
operation = callGAPI(v.matters(), 'count',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT],
|
|
matterId=matterId, body=body)
|
|
except GAPI.invalidArgument as e:
|
|
entityActionFailedExit([Ent.VAULT_MATTER, matterId], str(e))
|
|
doWait = True
|
|
printGettingAllAccountEntities(Ent.VAULT_MATTER_ARTIFACT, qualifier=f' for {Ent.Singular(Ent.VAULT_OPERATION)}: {operation["name"]}',
|
|
accountType=Ent.VAULT_MATTER)
|
|
while not operation.get('done'):
|
|
if doWait:
|
|
stderrEntityMessage([Ent.VAULT_OPERATION, operation['name']], Msg.IS_NOT_DONE_CHECKING_IN_SECONDS.format(operationWait))
|
|
time.sleep(operationWait)
|
|
try:
|
|
operation = callGAPI(v.operations(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND],
|
|
name=operation['name'])
|
|
except GAPI.notFound as e:
|
|
entityActionFailedExit([Ent.VAULT_OPERATION, operation['name']], str(e))
|
|
doWait = True
|
|
response = operation.get('response', {})
|
|
query = operation['metadata']['query']
|
|
search_method = query.get('searchMethod')
|
|
# ARGH count results don't include accounts with zero items.
|
|
# so we keep track of which accounts we searched and can report
|
|
# zero data for them.
|
|
if search_method == 'ACCOUNT':
|
|
query_accounts = query.get('accountInfo', {}).get('emails', [])
|
|
elif search_method == 'ENTIRE_ORG':
|
|
query_accounts = getItemsToModify(Cmd.ENTITY_ALL_USERS, '')
|
|
elif search_method == 'ORG_UNIT':
|
|
query_accounts = getItemsToModify(Cmd.ENTITY_OU, query['orgUnitInfo']['orgUnitId'])
|
|
mailcounts = response.get('mailCountResult', {})
|
|
groupcounts = response.get('groupsCountResult', {})
|
|
for a_count in [mailcounts, groupcounts]:
|
|
for errored_account in a_count.get('accountCountErrors', []):
|
|
email = errored_account.get('account', {}).get('email', '')
|
|
if email:
|
|
csvPF.WriteRow({'account': email, 'error': errored_account.get('errorType')})
|
|
if email in query_accounts:
|
|
query_accounts.remove(email)
|
|
for account in a_count.get('nonQueryableAccounts', []):
|
|
csvPF.WriteRow({'account': account, 'error': 'Not queried because not on hold'})
|
|
if account in query_accounts:
|
|
query_accounts.remove(account)
|
|
for account in a_count.get('accountCounts', []):
|
|
email = account.get('account', {}).get('email', '')
|
|
if email:
|
|
csvPF.WriteRow({'account': email, 'count': account.get('count', 0)})
|
|
if email in query_accounts:
|
|
query_accounts.remove(email)
|
|
for account in query_accounts:
|
|
csvPF.WriteRow({'account': account, 'count': 0})
|
|
csvPF.writeCSVfile('Vault Counts')
|
|
|
|
# gam [<UserTypeEntity>] create site <SiteName> <SiteAttribute>*
|
|
# gam [<UserTypeEntity>] update site <SiteEntity> <SiteAttribute>+
|
|
# gam [<UserTypeEntity>] info site <SiteEntity> [withmappings] [role|roles all|<SiteACLRoleList>]
|
|
# gam [<UserTypeEntity>] print sites [todrive <ToDriveAttribute>*] [domain|domains <DomainNameEntity>] [includeallsites]
|
|
# [withmappings] [role|roles all|<SiteACLRoleList>] [startindex <Number>] [maxresults <Number>] [convertcrnl] [delimiter <Character>]
|
|
# gam [<UserTypeEntity>] show sites [domain|domains <DomainNameEntity>] [includeallsites]
|
|
# [withmappings] [role|roles all|<SiteACLRoleList>] [startindex <Number>] [maxresults <Number>] [convertcrnl]
|
|
# gam [<UserTypeEntity>] create siteacls <SiteEntity> <SiteACLRole> <SiteACLScopeEntity>
|
|
# gam [<UserTypeEntity>] update siteacls <SiteEntity> <SiteACLRole> <SiteACLScopeEntity>
|
|
# gam [<UserTypeEntity>] delete siteacls <SiteEntity> <SiteACLScopeEntity>
|
|
# gam [<UserTypeEntity>] info siteacls <SiteEntity> <SiteACLScopeEntity>
|
|
# gam [<UserTypeEntity>] show siteacls <SiteEntity>
|
|
# gam [<UserTypeEntity>] print siteacls <SiteEntity> [todrive <ToDriveAttribute>*]
|
|
# gam [<UserTypeEntity>] print siteactivity <SiteEntity> [todrive <ToDriveAttribute>*]
|
|
# [startindex <Number>] [maxresults <Number>] [updated_min <Date>] [updated_max <Date>]
|
|
def deprecatedUserSites(_):
|
|
deprecatedCommandExit()
|
|
|
|
def deprecatedDomainSites():
|
|
deprecatedCommandExit()
|
|
|
|
# <CSVFileInput> [keyfield <FieldName>] [datafield <FieldName>]
|
|
def _getGroupOrgUnitMap():
|
|
|
|
def getKeyFieldInfo(keyword, defaultField):
|
|
if not checkArgumentPresent(keyword):
|
|
field = defaultField
|
|
else:
|
|
field = getString(Cmd.OB_FIELD_NAME)
|
|
if field not in fieldnames:
|
|
csvFieldErrorExit(field, fieldnames, backupArg=True)
|
|
return field
|
|
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
f, csvFile, fieldnames = openCSVFileReader(filename)
|
|
keyField = getKeyFieldInfo('keyfield', 'Group')
|
|
dataField = getKeyFieldInfo('datafield', 'OrgUnit')
|
|
groupOrgUnitMap = {}
|
|
for row in csvFile:
|
|
group = row[keyField].strip().lower()
|
|
orgUnit = row[dataField].strip()
|
|
if not group or not orgUnit:
|
|
systemErrorExit(USAGE_ERROR_RC, Msg.GROUP_MAPS_TO_OU_INVALID_ROW.format(filename, group, orgUnit))
|
|
orgUnit = makeOrgUnitPathAbsolute(orgUnit)
|
|
if group in groupOrgUnitMap:
|
|
origOrgUnit = groupOrgUnitMap[group]
|
|
if origOrgUnit != orgUnit:
|
|
systemErrorExit(USAGE_ERROR_RC, Msg.GROUP_MAPS_TO_MULTIPLE_OUS.format(filename, group, ','.join([origOrgUnit, orgUnit])))
|
|
groupOrgUnitMap[group] = orgUnit
|
|
closeFile(f)
|
|
return groupOrgUnitMap
|
|
|
|
class PasswordOptions():
|
|
def __init__(self, updateCmd):
|
|
self.password = ''
|
|
self.notFoundPassword = ''
|
|
self.b64DecryptPassword = False
|
|
self.clearPassword = True
|
|
self.hashPassword = True
|
|
self.ignoreNullPassword = False
|
|
self.makeRandomPassword = not updateCmd
|
|
self.makeUniqueRandomPassword = False
|
|
self.makeCleanPassword = True
|
|
self.cleanPasswordLen = 25
|
|
self.randomPasswordChars = None
|
|
self.promptForPassword = False
|
|
self.promptForUniquePassword = False
|
|
self.notifyPasswordSet = False
|
|
self.updateCmd = updateCmd
|
|
self.filename = ''
|
|
|
|
def GetPassword(self):
|
|
return getString(Cmd.OB_PASSWORD, minLen=1 if not self.ignoreNullPassword else 0, maxLen=100)
|
|
|
|
def SetCleanPasswordLen(self):
|
|
self.cleanPasswordLen = getInteger(minVal=8, maxVal=100, default=25)
|
|
|
|
def CreateRandomPassword(self):
|
|
rnd = SystemRandom()
|
|
if not self.makeCleanPassword:
|
|
# Generate a password with unicode chars that are not allowed in passwords.
|
|
# We expect "password random nohash" to fail but no one should be using that.
|
|
# Our goal here is to purposefully block logins with this password.
|
|
if not self.randomPasswordChars:
|
|
self.randomPasswordChars = [chr(i) for i in range(1, 55296)]
|
|
return ''.join(rnd.choice(self.randomPasswordChars) for _ in range(4096))
|
|
# Generate a clean password that can be used for logins
|
|
return ''.join(rnd.choice(PASSWORD_SAFE_CHARS) for _ in range(self.cleanPasswordLen))
|
|
|
|
def ProcessArgument(self, myarg, notify, notFoundBody):
|
|
if myarg == 'ignorenullpassword':
|
|
self.ignoreNullPassword = True
|
|
elif myarg == 'notifypassword':
|
|
password = self.GetPassword()
|
|
if password:
|
|
notify['password'] = password
|
|
self.notifyPasswordSet = True
|
|
elif myarg == 'nohash':
|
|
self.hashPassword = False
|
|
elif self.updateCmd and myarg == 'notfoundpassword':
|
|
up = 'password'
|
|
password = self.GetPassword()
|
|
if password:
|
|
if password.lower() == 'blocklogin':
|
|
self.makeCleanPassword = False
|
|
notFoundBody[up] = self.CreateRandomPassword()
|
|
elif password.lower() in {'random', 'uniquerandom'}:
|
|
self.SetCleanPasswordLen()
|
|
self.makeCleanPassword = True
|
|
notFoundBody[up] = self.CreateRandomPassword()
|
|
else:
|
|
notFoundBody[up] = password
|
|
self.notFoundPassword = notFoundBody[up]
|
|
elif myarg in {'lograndompassword', 'logpassword'}:
|
|
self.filename = getString(Cmd.OB_FILE_NAME)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
HASH_FUNCTION_MAP = {
|
|
'base64-md5': 'MD5',
|
|
'base64-sha1': 'SHA-1',
|
|
'crypt': 'crypt',
|
|
'md5': 'MD5',
|
|
'sha': 'SHA-1',
|
|
'sha-1': 'SHA-1',
|
|
'sha1': 'SHA-1',
|
|
}
|
|
|
|
def ProcessPropertyArgument(self, myarg, up, body):
|
|
if up == 'password':
|
|
password = self.GetPassword()
|
|
if password:
|
|
body[up] = password
|
|
self.makeRandomPassword = self.makeUniqueRandomPassword =\
|
|
self.promptForPassword = self.promptForUniquePassword = False
|
|
if password.lower() == 'blocklogin':
|
|
self.makeRandomPassword = True
|
|
self.makeCleanPassword = False
|
|
elif password.lower() == 'random':
|
|
self.SetCleanPasswordLen()
|
|
self.makeRandomPassword = self.makeCleanPassword = True
|
|
elif password.lower() == 'uniquerandom':
|
|
self.SetCleanPasswordLen()
|
|
if self.updateCmd:
|
|
self.makeUniqueRandomPassword = self.makeCleanPassword = True
|
|
else:
|
|
self.makeRandomPassword = self.makeCleanPassword = True
|
|
elif password.lower() == 'prompt':
|
|
self.promptForPassword = True
|
|
elif password.lower() == 'uniqueprompt':
|
|
if self.updateCmd:
|
|
self.promptForUniquePassword = True
|
|
else:
|
|
self.promptForPassword = True
|
|
else:
|
|
self.password = password
|
|
elif up == 'hashFunction':
|
|
body[up] = self.HASH_FUNCTION_MAP[myarg]
|
|
self.clearPassword = self.hashPassword = False
|
|
self.b64DecryptPassword = myarg.startswith('base64')
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def FinalizePassword(self, body, notify, up):
|
|
if not self.notifyPasswordSet:
|
|
notify[up] = body[up] if self.clearPassword else Msg.CONTACT_ADMINISTRATOR_FOR_PASSWORD
|
|
if self.hashPassword:
|
|
body[up] = sha512_crypt.hash(body[up], rounds=10000)
|
|
body['hashFunction'] = 'crypt'
|
|
elif self.b64DecryptPassword:
|
|
if body[up].lower()[:5] in ['{md5}', '{sha}']:
|
|
body[up] = body[up][5:]
|
|
try:
|
|
body[up] = base64.b64decode(body[up]).hex()
|
|
except Exception:
|
|
pass
|
|
|
|
def AssignPassword(self, body, notify, notFoundBody, createIfNotFound, user=None):
|
|
up = 'password'
|
|
if self.makeRandomPassword or self.makeUniqueRandomPassword:
|
|
body[up] = self.CreateRandomPassword()
|
|
self.password = body[up]
|
|
elif user and (self.promptForPassword or self.promptForUniquePassword):
|
|
body[up] = readStdin(f'Enter password for {user}: ')
|
|
self.password = body[up]
|
|
elif self.promptForPassword:
|
|
body[up] = readStdin('Enter password: ')
|
|
self.password = body[up]
|
|
if up in body:
|
|
self.FinalizePassword(body, notify, up)
|
|
elif 'hashFunction' in body:
|
|
body.pop('hashFunction')
|
|
if createIfNotFound:
|
|
if not notFoundBody:
|
|
if up in body:
|
|
notFoundBody[up] = body[up]
|
|
if 'hashfunction' in body:
|
|
notFoundBody['hashfunction'] = body['hashFunction']
|
|
notify['notFoundPassword'] = notify[up]
|
|
self.notFoundPassword = self.password
|
|
else:
|
|
notify['notFoundPassword'] = notify[up] if notify[up] else notFoundBody[up] if self.clearPassword else Msg.CONTACT_ADMINISTRATOR_FOR_PASSWORD
|
|
self.FinalizePassword(notFoundBody, notify, up)
|
|
|
|
UPDATE_USER_ARGUMENT_TO_PROPERTY_MAP = {
|
|
'address': 'addresses',
|
|
'addresses': 'addresses',
|
|
'archive': 'archived',
|
|
'archived': 'archived',
|
|
'base64-md5': 'hashFunction',
|
|
'base64-sha1': 'hashFunction',
|
|
'changepassword': 'changePasswordAtNextLogin',
|
|
'changepasswordatnextlogin': 'changePasswordAtNextLogin',
|
|
'crypt': 'hashFunction',
|
|
'customerid': 'customerId',
|
|
'displayname': 'displayName',
|
|
'email': 'primaryEmail',
|
|
'emails': 'emails',
|
|
'externalid': 'externalIds',
|
|
'externalids': 'externalIds',
|
|
'familyname': 'familyName',
|
|
'firstname': 'givenName',
|
|
'gal': 'includeInGlobalAddressList',
|
|
'gender': 'gender',
|
|
'givenname': 'givenName',
|
|
'im': 'ims',
|
|
'ims': 'ims',
|
|
'includeinglobaladdresslist': 'includeInGlobalAddressList',
|
|
'ipwhitelisted': 'ipWhitelisted',
|
|
'keyword': 'keywords',
|
|
'keywords': 'keywords',
|
|
'language': 'languages',
|
|
'languages': 'languages',
|
|
'lastname': 'familyName',
|
|
'location': 'locations',
|
|
'locations': 'locations',
|
|
'md5': 'hashFunction',
|
|
'note': 'notes',
|
|
'notes': 'notes',
|
|
'org': 'orgUnitPath',
|
|
'organization': 'organizations',
|
|
'organizations': 'organizations',
|
|
'organisation': 'organizations',
|
|
'organisations': 'organizations',
|
|
'orgunitpath': 'orgUnitPath',
|
|
'otheremail': 'emails',
|
|
'otheremails': 'emails',
|
|
'ou': 'orgUnitPath',
|
|
'password': 'password',
|
|
'phone': 'phones',
|
|
'phones': 'phones',
|
|
'posix': 'posixAccounts',
|
|
'posixaccounts': 'posixAccounts',
|
|
'primaryemail': 'primaryEmail',
|
|
'recoveryemail': 'recoveryEmail',
|
|
'recoveryphone': 'recoveryPhone',
|
|
'relation': 'relations',
|
|
'relations': 'relations',
|
|
'sha': 'hashFunction',
|
|
'sha-1': 'hashFunction',
|
|
'sha1': 'hashFunction',
|
|
'ssh': 'sshPublicKeys',
|
|
'sshkeys': 'sshPublicKeys',
|
|
'sshpublickeys': 'sshPublicKeys',
|
|
'suspend': 'suspended',
|
|
'suspended': 'suspended',
|
|
'username': 'primaryEmail',
|
|
'website': 'websites',
|
|
'websites': 'websites',
|
|
}
|
|
|
|
ADDRESS_ARGUMENT_TO_FIELD_MAP = {
|
|
'country': 'country',
|
|
'countrycode': 'countryCode',
|
|
'extendedaddress': 'extendedAddress',
|
|
'locality': 'locality',
|
|
'pobox': 'poBox',
|
|
'postalcode': 'postalCode',
|
|
'region': 'region',
|
|
'streetaddress': 'streetAddress',
|
|
}
|
|
|
|
ORGANIZATION_ARGUMENT_TO_FIELD_MAP = {
|
|
'costcenter': 'costCenter',
|
|
'costcentre': 'costCenter',
|
|
'department': 'department',
|
|
'description': 'description',
|
|
'domain': 'domain',
|
|
'fulltimeequivalent': 'fullTimeEquivalent',
|
|
'location': 'location',
|
|
'name': 'name',
|
|
'symbol': 'symbol',
|
|
'title': 'title',
|
|
}
|
|
|
|
# (MultiValue, IgnoreEmpty)
|
|
SCHEMA_VALUE_PROCESS_MAP = {
|
|
'multivalued': (True, False),
|
|
'multivalue': (True, False),
|
|
'value': (True, False),
|
|
'multinonempty': (True, True),
|
|
'scalarnonempty': (False, True)
|
|
}
|
|
|
|
USER_JSON_SKIP_FIELDS = ['agreedToTerms', 'aliases', 'creationTime', 'customerId', 'deletionTime', 'groups', 'id',
|
|
'isAdmin', 'isDelegatedAdmin', 'isEnforcedIn2Sv', 'isEnrolledIn2Sv', 'isMailboxSetup',
|
|
'lastLoginTime', 'licenses', 'primaryEmail', 'thumbnailPhotoEtag', 'thumbnailPhotoUrl',
|
|
'ipWhiteListed', 'posixAccounts', 'suspensionReason', 'nonEditableAliases']
|
|
|
|
ALLOW_EMPTY_CUSTOM_TYPE = 'allowEmptyCustomType'
|
|
|
|
def getUserAttributes(cd, updateCmd, noUid=False):
|
|
def getKeywordAttribute(keywords, attrdict, **opts):
|
|
if Cmd.ArgumentsRemaining():
|
|
keyword = Cmd.Current().strip().lower()
|
|
if keyword in keywords[UProp.PTKW_KEYWORD_LIST]:
|
|
Cmd.Advance()
|
|
if keyword != keywords[UProp.PTKW_CL_CUSTOM_KEYWORD]:
|
|
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = keyword
|
|
attrdict.pop(keywords[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD], None)
|
|
return
|
|
if Cmd.ArgumentsRemaining():
|
|
customType = Cmd.Current().strip()
|
|
if customType or opts.get(ALLOW_EMPTY_CUSTOM_TYPE, False):
|
|
Cmd.Advance()
|
|
if keywords[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]:
|
|
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = keywords[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
|
|
else:
|
|
attrdict.pop(keywords[UProp.PTKW_ATTR_TYPE_KEYWORD], None)
|
|
attrdict[keywords[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]] = customType
|
|
return
|
|
missingArgumentExit('custom attribute type')
|
|
elif DEFAULT_CHOICE in opts:
|
|
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = opts[DEFAULT_CHOICE]
|
|
return
|
|
elif keywords[UProp.PTKW_CL_CUSTOM_KEYWORD]:
|
|
if keywords[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]:
|
|
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = keywords[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
|
|
else:
|
|
attrdict.pop(keywords[UProp.PTKW_ATTR_TYPE_KEYWORD], None)
|
|
attrdict[keywords[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]] = Cmd.Current()
|
|
Cmd.Advance()
|
|
return
|
|
invalidChoiceExit(keyword, keywords[UProp.PTKW_KEYWORD_LIST], False)
|
|
elif DEFAULT_CHOICE in opts:
|
|
attrdict[keywords[UProp.PTKW_ATTR_TYPE_KEYWORD]] = opts[DEFAULT_CHOICE]
|
|
return
|
|
missingChoiceExit(keywords[UProp.PTKW_KEYWORD_LIST])
|
|
|
|
def primaryNotPrimary(pnp, entry):
|
|
if pnp == 'notprimary':
|
|
return True
|
|
if pnp == 'primary':
|
|
entry['primary'] = True
|
|
primary['location'] = Cmd.Location()
|
|
return True
|
|
return False
|
|
|
|
def getPrimaryNotPrimaryChoice(entry, defaultChoice):
|
|
if getChoice({'primary': True, 'notprimary': False}, defaultChoice=defaultChoice, mapChoice=True):
|
|
entry['primary'] = True
|
|
primary['location'] = Cmd.Location()
|
|
|
|
def checkClearBodyList(body, itemName):
|
|
if checkArgumentPresent(Cmd.CLEAR_NONE_ARGUMENT):
|
|
body.pop(itemName, None)
|
|
body[itemName] = None
|
|
return True
|
|
return False
|
|
|
|
def appendItemToBodyList(body, itemName, itemValue, checkBlankField=None, checkSystemId=False):
|
|
if (itemName in body) and (body[itemName] is None):
|
|
del body[itemName]
|
|
body.setdefault(itemName, [])
|
|
if checkBlankField is None or itemValue[checkBlankField]:
|
|
# 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'):
|
|
Cmd.SetLocation(primary['location']-1)
|
|
usageErrorExit(Msg.MULTIPLE_ITEMS_MARKED_PRIMARY.format(itemName))
|
|
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)
|
|
invalidArgumentExit(Cmd.OB_SCHEMA_NAME_FIELD_NAME)
|
|
|
|
parameters = {
|
|
'verifyNotInvitable': False,
|
|
'createIfNotFound': False,
|
|
'noActionIfAlias': False,
|
|
'notifyOnUpdate': True,
|
|
'setChangePasswordOnCreate': False,
|
|
'immutableOUs': set(),
|
|
'addNumericSuffixOnDuplicate': 0,
|
|
'lic': None,
|
|
LICENSE_PRODUCT_SKUIDS: [],
|
|
}
|
|
if updateCmd:
|
|
body = {}
|
|
else:
|
|
body = {'name': {'givenName': UNKNOWN, 'familyName': UNKNOWN}}
|
|
body['primaryEmail'] = getEmailAddress(noUid=noUid)
|
|
notFoundBody = {}
|
|
notify = {'subject': '', 'message': '', 'html': False, 'charset': UTF8, 'password': ''}
|
|
primary = {}
|
|
updatePrimaryEmail = {}
|
|
groupOrgUnitMap = None
|
|
tagReplacements = _initTagReplacements()
|
|
addGroups = {}
|
|
addAliases = []
|
|
PwdOpts = PasswordOptions(updateCmd)
|
|
resolveConflictAccount = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'notify':
|
|
notify['recipients'] = getNormalizedEmailAddressEntity(shlexSplit=True, noLower=True)
|
|
elif myarg == 'subject':
|
|
notify['subject'] = getString(Cmd.OB_STRING)
|
|
elif myarg in SORF_MSG_FILE_ARGUMENTS:
|
|
notify['message'], notify['charset'], notify['html'] = getStringOrFile(myarg)
|
|
elif myarg == 'html':
|
|
notify['html'] = getBoolean()
|
|
elif myarg == 'from':
|
|
notify['from'] = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
elif myarg == 'replyto':
|
|
notify['replyto'] = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
elif myarg == 'mailbox':
|
|
notify['mailbox'] = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
elif PwdOpts.ProcessArgument(myarg, notify, notFoundBody):
|
|
pass
|
|
elif _getTagReplacement(myarg, tagReplacements, True):
|
|
pass
|
|
elif myarg == 'admin':
|
|
value = getBoolean()
|
|
if updateCmd or value:
|
|
Cmd.Backup()
|
|
unknownArgumentExit()
|
|
elif myarg == 'verifynotinvitable':
|
|
parameters['verifyNotInvitable'] = True
|
|
elif myarg == 'alwaysevict':
|
|
resolveConflictAccount = False
|
|
elif updateCmd and myarg == 'createifnotfound':
|
|
parameters['createIfNotFound'] = True
|
|
elif updateCmd and myarg == 'noactionifalias':
|
|
parameters['noActionIfAlias'] = True
|
|
elif updateCmd and myarg == 'notifyonupdate':
|
|
parameters['notifyOnUpdate'] = getBoolean()
|
|
elif updateCmd and myarg == 'setchangepasswordoncreate':
|
|
parameters['setChangePasswordOnCreate'] = getBoolean()
|
|
elif updateCmd and myarg in {'immutableous', 'immutableorgs', 'immutableorgunitpaths'}:
|
|
parameters['immutableOUs'] = set(getEntityList(Cmd.OB_ORGUNIT_ENTITY, shlexSplit=True))
|
|
elif not updateCmd and myarg == 'addnumericsuffixonduplicate':
|
|
parameters['addNumericSuffixOnDuplicate'] = getInteger(minVal=0, default=0)
|
|
elif not updateCmd and myarg in {'license', 'licence', 'licenses', 'licences'}:
|
|
if parameters['lic'] is None:
|
|
parameters['lic'] = buildGAPIObject(API.LICENSING)
|
|
parameters[LICENSE_PRODUCT_SKUIDS] = getGoogleSKUList(allowUnknownProduct=True)
|
|
if checkArgumentPresent(['product', 'productid']):
|
|
productId = getGoogleProduct()
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
productSku = (productId, productSku[1])
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
if not productSku[0]:
|
|
invalidChoiceExit(productSku[1], SKU.getSortedSKUList(), True)
|
|
elif updateCmd and myarg == 'updateoufromgroup':
|
|
groupOrgUnitMap = _getGroupOrgUnitMap()
|
|
elif updateCmd and myarg == 'updateprimaryemail':
|
|
updatePrimaryEmail = getREPatternSubstitution(re.IGNORECASE)
|
|
elif myarg == 'json':
|
|
body.update(getJSON(USER_JSON_SKIP_FIELDS))
|
|
if 'name' in body and 'fullName' in body['name']:
|
|
body['name'].pop('fullName')
|
|
if 'sshPublicKeys' in body and 'fingerprint' in body['sshPublicKeys']:
|
|
body['sshPublicKeys'].pop('fingerprint')
|
|
for location in body.get('locations', []):
|
|
location.pop('buildingName', None)
|
|
elif myarg == 'employeeid':
|
|
entry = {'type': 'organization', 'value': getString(Cmd.OB_STRING, minLen=0)}
|
|
appendItemToBodyList(body, 'externalIds', entry, 'value')
|
|
elif myarg == 'manager':
|
|
entry = {'type': 'manager', 'value': getString(Cmd.OB_STRING, minLen=0)}
|
|
appendItemToBodyList(body, 'relations', entry, 'value')
|
|
elif myarg in UPDATE_USER_ARGUMENT_TO_PROPERTY_MAP:
|
|
up = UPDATE_USER_ARGUMENT_TO_PROPERTY_MAP[myarg]
|
|
userProperty = UProp.PROPERTIES[up]
|
|
propertyClass = userProperty[UProp.CLASS]
|
|
if UProp.TYPE_KEYWORDS in userProperty:
|
|
typeKeywords = userProperty[UProp.TYPE_KEYWORDS]
|
|
clTypeKeyword = typeKeywords[UProp.PTKW_CL_TYPE_KEYWORD]
|
|
if up == 'givenName':
|
|
body.setdefault('name', {})
|
|
body['name'][up] = getString(Cmd.OB_STRING, minLen=0, maxLen=60)
|
|
elif up == 'familyName':
|
|
body.setdefault('name', {})
|
|
body['name'][up] = getString(Cmd.OB_STRING, minLen=0, maxLen=60)
|
|
elif up == 'displayName':
|
|
body.setdefault('name', {})
|
|
body['name']['displayName'] = getString(Cmd.OB_STRING, minLen=0, maxLen=256)
|
|
elif PwdOpts.ProcessPropertyArgument(myarg, up, body):
|
|
pass
|
|
elif propertyClass == UProp.PC_BOOLEAN:
|
|
body[up] = getBoolean()
|
|
elif up == 'primaryEmail':
|
|
if updateCmd:
|
|
body[up] = getEmailAddress(noUid=True)
|
|
elif body[up] != getEmailAddress(noUid=True):
|
|
Cmd.Backup()
|
|
unknownArgumentExit()
|
|
elif up == 'recoveryEmail':
|
|
rcvryEmail = getEmailAddress(noUid=True, optional=True)
|
|
body[up] = rcvryEmail if rcvryEmail is not None else ""
|
|
elif up == 'recoveryPhone':
|
|
body[up] = getString(Cmd.OB_STRING, minLen=0)
|
|
if body[up] and body[up][0] != '+':
|
|
body[up] = '+' + body[up]
|
|
elif up == 'customerId':
|
|
body[up] = getString(Cmd.OB_STRING)
|
|
elif up == 'orgUnitPath':
|
|
body[up] = getOrgUnitItem(pathOnly=True, cd=cd)
|
|
elif up == 'languages':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
for language in getString(Cmd.OB_LANGUAGE_LIST).replace('_', '-').replace(',', ' ').split():
|
|
langItem = {}
|
|
if language[-1] == '+':
|
|
suffix = '+'
|
|
language = language[:-1]
|
|
langItem['preference'] = 'preferred'
|
|
elif language[-1] == '-':
|
|
suffix = '-'
|
|
language = language[:-1]
|
|
langItem['preference'] = 'not_preferred'
|
|
else:
|
|
suffix = ''
|
|
if language.lower() in LANGUAGE_CODES_MAP:
|
|
langItem['languageCode'] = LANGUAGE_CODES_MAP[language.lower()]
|
|
else:
|
|
if suffix:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.SUFFIX_NOT_ALLOWED_WITH_CUSTOMLANGUAGE.format(suffix, language))
|
|
langItem['customLanguage'] = language
|
|
appendItemToBodyList(body, up, langItem)
|
|
elif up == 'gender':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
getChoice([clTypeKeyword], defaultChoice=None)
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
if checkArgumentPresent('addressmeas'):
|
|
entry['addressMeAs'] = getString(Cmd.OB_STRING, minLen=0)
|
|
body[up] = entry
|
|
elif up == 'addresses':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
getChoice([clTypeKeyword], defaultChoice=None)
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
if checkArgumentPresent(['unstructured', 'formatted']):
|
|
entry['sourceIsStructured'] = False
|
|
entry['formatted'] = getStringWithCRsNLs()
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument in ADDRESS_ARGUMENT_TO_FIELD_MAP:
|
|
value = getString(Cmd.OB_STRING, minLen=0)
|
|
if value:
|
|
entry[ADDRESS_ARGUMENT_TO_FIELD_MAP[argument]] = value
|
|
elif primaryNotPrimary(argument, entry):
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
appendItemToBodyList(body, up, entry)
|
|
elif up == 'ims':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
getChoice([clTypeKeyword], defaultChoice=None)
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
getChoice([UProp.IM_PROTOCOLS[UProp.PTKW_CL_TYPE_KEYWORD]])
|
|
getKeywordAttribute(UProp.IM_PROTOCOLS, entry)
|
|
# Backwards compatability: notprimary|primary on either side of IM address
|
|
getPrimaryNotPrimaryChoice(entry, False)
|
|
entry['im'] = getString(Cmd.OB_STRING, minLen=0)
|
|
getPrimaryNotPrimaryChoice(entry, entry.get('primary', False))
|
|
appendItemToBodyList(body, up, entry, 'im')
|
|
elif up == 'keywords':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
getChoice([clTypeKeyword], defaultChoice=None)
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
appendItemToBodyList(body, up, entry, 'value')
|
|
elif up == 'locations':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {'type': 'desk'}
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument == clTypeKeyword:
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
elif argument == 'area':
|
|
entry['area'] = getString(Cmd.OB_STRING)
|
|
elif argument in {'building', 'buildingid'}:
|
|
entry['buildingId'] = _getBuildingByNameOrId(cd, allowNV=True)
|
|
elif argument in {'floor', 'floorname'}:
|
|
entry['floorName'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif argument in {'section', 'floorsection'}:
|
|
entry['floorSection'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif argument in {'desk', 'deskcode'}:
|
|
entry['deskCode'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif argument == 'endlocation':
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
if 'area' not in entry:
|
|
missingArgumentExit('area <String>')
|
|
appendItemToBodyList(body, up, entry)
|
|
elif up == 'notes':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
getKeywordAttribute(typeKeywords, entry, defaultChoice='text_plain')
|
|
entry['value'] = getStringWithCRsNLsOrFile()[0]
|
|
body[up] = entry
|
|
elif up == 'organizations':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument == clTypeKeyword:
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
elif argument == typeKeywords[UProp.PTKW_CL_CUSTOMTYPE_KEYWORD]:
|
|
entry[typeKeywords[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]] = getString(Cmd.OB_STRING, minLen=0)
|
|
entry.pop(typeKeywords[UProp.PTKW_ATTR_TYPE_KEYWORD], None)
|
|
elif argument in ORGANIZATION_ARGUMENT_TO_FIELD_MAP:
|
|
argument = ORGANIZATION_ARGUMENT_TO_FIELD_MAP[argument]
|
|
if argument != 'fullTimeEquivalent':
|
|
value = getString(Cmd.OB_STRING, minLen=0)
|
|
if value:
|
|
entry[argument] = value
|
|
else:
|
|
entry[argument] = getInteger(minVal=0, maxVal=100000, default=0)
|
|
elif primaryNotPrimary(argument, entry):
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
appendItemToBodyList(body, up, entry)
|
|
elif up == 'phones':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument == clTypeKeyword:
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
elif argument == 'value':
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif primaryNotPrimary(argument, entry):
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
appendItemToBodyList(body, up, entry, 'value')
|
|
elif up == 'posixAccounts':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument in {'username', 'name'}:
|
|
entry['username'] = getString(Cmd.OB_STRING)
|
|
elif argument == 'uid':
|
|
entry['uid'] = getInteger(minVal=1000)
|
|
elif argument == 'gid':
|
|
entry['gid'] = getInteger(minVal=0)
|
|
elif argument in {'system', 'systemid'}:
|
|
entry['systemId'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif argument in {'home', 'homedirectory'}:
|
|
entry['homeDirectory'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif argument == 'shell':
|
|
entry['shell'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif argument == 'gecos':
|
|
entry['gecos'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif argument == 'primary':
|
|
primary['location'] = Cmd.Location()
|
|
entry['primary'] = getBoolean()
|
|
elif argument in {'os', 'operatingsystemtype'}:
|
|
entry['operatingSystemType'] = getChoice(['linux', 'unspecified', 'windows'])
|
|
elif argument == 'endposix':
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
if 'username' not in entry:
|
|
missingArgumentExit('username <String>')
|
|
if 'uid' not in entry:
|
|
missingArgumentExit('uid <Integer>')
|
|
appendItemToBodyList(body, up, entry, checkSystemId=True)
|
|
elif up == 'relations':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
getChoice([clTypeKeyword], defaultChoice=None)
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
appendItemToBodyList(body, up, entry, 'value')
|
|
elif up == 'emails':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
getChoice([clTypeKeyword], defaultChoice=None)
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
entry['address'] = getEmailAddress(noUid=True, minLen=0)
|
|
appendItemToBodyList(body, up, entry, 'address')
|
|
elif up == 'sshPublicKeys':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument == 'expires':
|
|
entry['expirationTimeUsec'] = getInteger(minVal=0)
|
|
elif argument == 'key':
|
|
entry['key'] = getString(Cmd.OB_STRING)
|
|
elif argument == 'endssh':
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
if 'key' not in entry:
|
|
missingArgumentExit('key <String>')
|
|
appendItemToBodyList(body, up, entry)
|
|
elif up == 'externalIds':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
getChoice([clTypeKeyword], defaultChoice=None)
|
|
getKeywordAttribute(typeKeywords, entry, allowEmptyCustomType=True)
|
|
entry['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
appendItemToBodyList(body, up, entry, 'value')
|
|
elif up == 'websites':
|
|
if checkClearBodyList(body, up):
|
|
continue
|
|
entry = {}
|
|
getChoice([clTypeKeyword], defaultChoice=None)
|
|
getKeywordAttribute(typeKeywords, entry)
|
|
entry['value'] = getString(Cmd.OB_URL, minLen=0)
|
|
getPrimaryNotPrimaryChoice(entry, False)
|
|
appendItemToBodyList(body, up, entry, 'value')
|
|
elif myarg in {'group', 'groups'}:
|
|
role = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
|
|
delivery_settings = getDeliverySettings()
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
addGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': delivery_settings}
|
|
elif myarg in {'alias', 'aliases'}:
|
|
addAliases.extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0)))
|
|
elif myarg == 'clearschema':
|
|
if not updateCmd:
|
|
unknownArgumentExit()
|
|
schemaName, fieldName = _splitSchemaNameDotFieldName(getString(Cmd.OB_SCHEMA_NAME_FIELD_NAME), False)
|
|
up = 'customSchemas'
|
|
body.setdefault(up, {})
|
|
body[up].setdefault(schemaName, {})
|
|
if fieldName is None:
|
|
try:
|
|
schema = callGAPI(cd.schemas(), 'get',
|
|
throwReasons=[GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customerId=GC.Values[GC.CUSTOMER_ID], schemaKey=schemaName, fields='fields(fieldName)')
|
|
for field in schema['fields']:
|
|
body[up][schemaName][field['fieldName']] = None
|
|
except (GAPI.invalid, GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
entityDoesNotExistWarning(Ent.USER_SCHEMA, schemaName)
|
|
unknownArgumentExit()
|
|
else:
|
|
body[up][schemaName][fieldName] = None
|
|
elif myarg.find('.') >= 0:
|
|
schemaName, fieldName = _splitSchemaNameDotFieldName(Cmd.Previous())
|
|
up = 'customSchemas'
|
|
body.setdefault(up, {})
|
|
body[up].setdefault(schemaName, {})
|
|
multivalue, ignoreEmpty = getChoice(SCHEMA_VALUE_PROCESS_MAP, defaultChoice=(False, False), mapChoice=True)
|
|
if multivalue:
|
|
body[up][schemaName].setdefault(fieldName, [])
|
|
typeKeywords = UProp.PROPERTIES[up][UProp.TYPE_KEYWORDS]
|
|
clTypeKeyword = typeKeywords[UProp.PTKW_CL_TYPE_KEYWORD]
|
|
schemaValue = {}
|
|
if checkArgumentPresent(clTypeKeyword):
|
|
getKeywordAttribute(typeKeywords, schemaValue)
|
|
else:
|
|
schemaValue['type'] = 'work'
|
|
schemaValue['value'] = getString(Cmd.OB_STRING, minLen=0)
|
|
if schemaValue['value'] or not ignoreEmpty:
|
|
body[up][schemaName][fieldName].append(schemaValue)
|
|
else:
|
|
schemaValue = getString(Cmd.OB_STRING, minLen=0)
|
|
if schemaValue or not ignoreEmpty:
|
|
body[up][schemaName][fieldName] = schemaValue
|
|
elif updateCmd:
|
|
body[up][schemaName][fieldName] = None
|
|
else:
|
|
unknownArgumentExit()
|
|
if PwdOpts.promptForPassword or PwdOpts.promptForUniquePassword:
|
|
if not updateCmd:
|
|
PwdOpts.AssignPassword(body, notify, notFoundBody, parameters['createIfNotFound'], body['primaryEmail'])
|
|
elif not PwdOpts.promptForUniquePassword:
|
|
PwdOpts.AssignPassword(body, notify, notFoundBody, parameters['createIfNotFound'])
|
|
elif not PwdOpts.makeUniqueRandomPassword:
|
|
PwdOpts.AssignPassword(body, notify, notFoundBody, parameters['createIfNotFound'])
|
|
return (body, notify, tagReplacements, addGroups, addAliases, PwdOpts,
|
|
updatePrimaryEmail, notFoundBody, groupOrgUnitMap, parameters,
|
|
resolveConflictAccount)
|
|
|
|
def createUserAddToGroups(cd, user, addGroups, i, count):
|
|
action = Act.Get()
|
|
Act.Set(Act.ADD)
|
|
Ind.Increment()
|
|
_addUserToGroups(cd, user, set(addGroups), addGroups, i, count)
|
|
Ind.Decrement()
|
|
Act.Set(action)
|
|
|
|
def createUserAddAliases(cd, user, aliasList, i, count):
|
|
action = Act.Get()
|
|
Act.Set(Act.ADD)
|
|
Ind.Increment()
|
|
_addUserAliases(cd, user, aliasList, i, count)
|
|
Ind.Decrement()
|
|
Act.Set(action)
|
|
|
|
# gam create user <EmailAddress> <UserAttribute>
|
|
# [verifynotinvitable|alwaysevict]
|
|
# (groups [<GroupRole>] [[delivery] <DeliverySetting>] <GroupEntity>)*
|
|
# [alias|aliases <EmailAddressList>]
|
|
# [license <SKUID> [product|productid <ProductID>]]
|
|
# [notify <EmailAddressList>
|
|
# [subject <String>]
|
|
# [notifypassword <String>]
|
|
# [from <EmailAaddress>]
|
|
# [replyto <EmailAaddress>]
|
|
# [<NotifyMessageContent>]
|
|
# (replace <Tag> <UserReplacement>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
|
|
# [logpassword <FileName>] [ignorenullpassword]
|
|
# [addnumericsuffixonduplicate <Number>]
|
|
def doCreateUser():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
body, notify, tagReplacements, addGroups, addAliases, PwdOpts, \
|
|
_, _, _, \
|
|
parameters, resolveConflictAccount = getUserAttributes(cd, False, noUid=True)
|
|
suffix = 0
|
|
originalEmail = body['primaryEmail']
|
|
atLoc = originalEmail.find('@')
|
|
fields = '*' if tagReplacements['subs'] else 'primaryEmail,name'
|
|
while True:
|
|
user = body['primaryEmail']
|
|
if parameters['verifyNotInvitable']:
|
|
isInvitableUser, _ = _getIsInvitableUser(None, user)
|
|
if isInvitableUser:
|
|
entityActionNotPerformedWarning([Ent.USER, user], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT)
|
|
return
|
|
try:
|
|
result = callGAPI(cd.users(), 'insert',
|
|
throwReasons=[GAPI.DUPLICATE, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN,
|
|
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.INVALID_PARAMETER,
|
|
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE, GAPI.CONDITION_NOT_MET,
|
|
GAPI.LIMIT_EXCEEDED],
|
|
body=body,
|
|
fields=fields,
|
|
resolveConflictAccount=resolveConflictAccount)
|
|
entityActionPerformed([Ent.USER, user])
|
|
break
|
|
except GAPI.duplicate:
|
|
if duplicateAliasGroupUserWarning(cd, [Ent.USER, user]) == Ent.USER and parameters['addNumericSuffixOnDuplicate'] > 0:
|
|
parameters['addNumericSuffixOnDuplicate'] -= 1
|
|
suffix +=1
|
|
body['primaryEmail'] = originalEmail[0:atLoc]+str(suffix)+originalEmail[atLoc:]
|
|
setSysExitRC(0)
|
|
continue
|
|
return
|
|
except GAPI.invalidSchemaValue:
|
|
entityActionFailedExit([Ent.USER, user], Msg.INVALID_SCHEMA_VALUE)
|
|
except GAPI.invalidOrgunit:
|
|
entityActionFailedExit([Ent.USER, user], Msg.INVALID_ORGUNIT)
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.invalid, GAPI.invalidInput, GAPI.invalidParameter, GAPI.conditionNotMet, GAPI.limitExceeded) as e:
|
|
entityActionFailedExit([Ent.USER, user], str(e))
|
|
if PwdOpts.filename and PwdOpts.password:
|
|
writeFile(PwdOpts.filename, f'{user},{PwdOpts.password}\n', mode='a', continueOnError=True)
|
|
if addGroups:
|
|
createUserAddToGroups(cd, result['primaryEmail'], addGroups, 0, 0)
|
|
if addAliases:
|
|
createUserAddAliases(cd, result['primaryEmail'], addAliases, 0, 0)
|
|
if notify.get('recipients'):
|
|
sendCreateUpdateUserNotification(result, notify, tagReplacements)
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
productId = productSku[0]
|
|
skuId = productSku[1]
|
|
try:
|
|
callGAPI(parameters['lic'].licenseAssignments(), 'insert',
|
|
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET, GAPI.INVALID,
|
|
GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
productId=productId, skuId=skuId, body={'userId': user}, fields='')
|
|
entityActionPerformed([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)])
|
|
except (GAPI.internalError, GAPI.duplicate, GAPI.conditionNotMet, GAPI.invalid,
|
|
GAPI.userNotFound, GAPI.forbidden, GAPI.backendError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], str(e))
|
|
|
|
def verifyUserPrimaryEmail(cd, user, createIfNotFound, i, count):
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=user, fields='id,primaryEmail')
|
|
if (result['primaryEmail'].lower() == user) or (result['id'] == user):
|
|
return True
|
|
entityActionNotPerformedWarning([Ent.USER, user], Msg.NOT_A_PRIMARY_EMAIL_ADDRESS, i, count)
|
|
return False
|
|
except GAPI.userNotFound:
|
|
if createIfNotFound:
|
|
return True
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError):
|
|
pass
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
return False
|
|
|
|
# gam <UserTypeEntity> update user <UserAttribute>*
|
|
# [verifynotinvitable|alwaysevict] [noactionifalias]
|
|
# [updateprimaryemail <RESEarchPattern> <RESubstitution>]
|
|
# [updateoufromgroup <CSVFileInput> [keyfield <FieldName>] [datafield <FieldName>]]
|
|
# [immutableous <OrgUnitEntity>]|
|
|
# [clearschema <SchemaName>|<SchemaNameField>]
|
|
# [createifnotfound] [notfoundpassword (random [<Integer>])|blocklogin|<Password>]
|
|
# (groups [<GroupRole>] [[delivery] <DeliverySetting>] <GroupEntity>)*
|
|
# [alias|aliases <EmailAddressList>]
|
|
# [notify <EmailAddressList>
|
|
# [subject <String>]
|
|
# [notifypassword <String>]
|
|
# [from <EmailAaddress>]
|
|
# [replyto <EmailAaddress>]
|
|
# [<NotifyMessageContent>
|
|
# (replace <Tag> <UserReplacement>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
|
|
# [notifyonupdate [<Boolean>]]
|
|
# [logpassword <FileName>] [ignorenullpassword]
|
|
def updateUsers(entityList):
|
|
def waitingForCreationToComplete(sleep_time):
|
|
writeStderr(Ind.Spaces()+Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.USER), sleep_time))
|
|
time.sleep(sleep_time)
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
errorRetries = 5
|
|
updateRetryDelay = 5
|
|
body, notify, tagReplacements, addGroups, addAliases, PwdOpts, \
|
|
updatePrimaryEmail, notFoundBody, groupOrgUnitMap, \
|
|
parameters, resolveConflictAccount = getUserAttributes(cd, True)
|
|
vfe = 'primaryEmail' in body and body['primaryEmail'][:4].lower() == 'vfe@'
|
|
if body.get('orgUnitPath', '') and parameters['immutableOUs']:
|
|
ubody = body.copy()
|
|
checkImmutableOUs = True
|
|
else:
|
|
checkImmutableOUs = False
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
fields = '*' if tagReplacements['subs'] else 'primaryEmail,name'
|
|
for user in entityList:
|
|
i += 1
|
|
user = userKey = normalizeEmailAddressOrUID(user)
|
|
if parameters['noActionIfAlias'] and not verifyUserPrimaryEmail(cd, user, parameters['createIfNotFound'], i, count):
|
|
continue
|
|
if checkImmutableOUs:
|
|
body = ubody.copy()
|
|
try:
|
|
if vfe:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=userKey, fields='primaryEmail,id')
|
|
userKey = result['id']
|
|
userPrimary = result['primaryEmail']
|
|
userName, userDomain = splitEmailAddress(userPrimary)
|
|
body['primaryEmail'] = f'vfe.{userName}.{random.randint(1, 99999):05d}@{userDomain}'
|
|
body['emails'] = [{'type': 'custom',
|
|
'customType': 'former_employee',
|
|
'primary': False, 'address': userPrimary}]
|
|
elif updatePrimaryEmail:
|
|
if updatePrimaryEmail[0].search(user) is not None:
|
|
body['primaryEmail'] = re.sub(updatePrimaryEmail[0], updatePrimaryEmail[1], user)
|
|
else:
|
|
body.pop('primaryEmail', None)
|
|
if not body:
|
|
entityActionNotPerformedWarning([Ent.USER, user], Msg.PRIMARY_EMAIL_DID_NOT_MATCH_PATTERN.format(updatePrimaryEmail[0].pattern), i, count)
|
|
if groupOrgUnitMap:
|
|
try:
|
|
groups = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userKey=userKey, orderBy='email', fields='nextPageToken,groups(email)')
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
entityUnknownWarning(Ent.USER, userKey, i, count)
|
|
continue
|
|
groupList = []
|
|
for group in groups:
|
|
orgUnit = groupOrgUnitMap.get(group['email'].lower())
|
|
if orgUnit:
|
|
groupList.append(group['email'])
|
|
jcount = len(groupList)
|
|
if jcount != 1:
|
|
entityActionNotPerformedWarning([Ent.USER, user], Msg.USER_BELONGS_TO_N_GROUPS_THAT_MAP_TO_ORGUNITS.format(jcount, ','.join(groupList)), i, count)
|
|
continue
|
|
body['orgUnitPath'] = orgUnit
|
|
if checkImmutableOUs:
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS,
|
|
userKey=userKey, fields='orgUnitPath')
|
|
if result['orgUnitPath'] in parameters['immutableOUs']:
|
|
body.pop('orgUnitPath')
|
|
except Exception:
|
|
pass
|
|
if body:
|
|
if 'primaryEmail' in body and parameters['verifyNotInvitable']:
|
|
isInvitableUser, ci = _getIsInvitableUser(ci, body['primaryEmail'])
|
|
if isInvitableUser:
|
|
entityActionNotPerformedWarning([Ent.USER, body['primaryEmail']], Msg.EMAIL_ADDRESS_IS_UNMANAGED_ACCOUNT, i, count)
|
|
continue
|
|
if PwdOpts.makeUniqueRandomPassword or PwdOpts.promptForUniquePassword:
|
|
PwdOpts.AssignPassword(body, notify, notFoundBody, parameters['createIfNotFound'], userKey)
|
|
retry = 0
|
|
while True:
|
|
try:
|
|
result = callGAPI(cd.users(), 'update',
|
|
throwReasons=[GAPI.CONDITION_NOT_MET, GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
|
|
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.INVALID_PARAMETER,
|
|
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE, GAPI.DUPLICATE,
|
|
GAPI.INSUFFICIENT_ARCHIVED_USER_LICENSES, GAPI.CONFLICT],
|
|
userKey=userKey, body=body, fields=fields)
|
|
entityActionPerformed([Ent.USER, user], i, count)
|
|
if PwdOpts.filename and PwdOpts.password:
|
|
writeFile(PwdOpts.filename, f'{userKey},{PwdOpts.password}\n', mode='a', continueOnError=True)
|
|
if parameters['notifyOnUpdate'] and notify.get('recipients') and notify['password']:
|
|
sendCreateUpdateUserNotification(result, notify, tagReplacements, i, count, createMessage=False)
|
|
break
|
|
except GAPI.conditionNotMet as e:
|
|
retry += 1
|
|
if ('User creation is not complete' not in str(e)) or retry > errorRetries:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
break
|
|
waitingForCreationToComplete(updateRetryDelay)
|
|
continue
|
|
except GAPI.userNotFound:
|
|
if parameters['createIfNotFound']:
|
|
if notFoundBody and (count == 1) and not vfe and ('password' in notFoundBody) and ('name' in body) and ('givenName' in body['name']) and ('familyName' in body['name']):
|
|
if 'primaryEmail' not in body:
|
|
body['primaryEmail'] = user
|
|
body.update(notFoundBody)
|
|
if parameters['setChangePasswordOnCreate']:
|
|
body['changePasswordAtNextLogin'] = True
|
|
Act.Set(Act.CREATE)
|
|
try:
|
|
result = callGAPI(cd.users(), 'insert',
|
|
throwReasons=[GAPI.DUPLICATE, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN,
|
|
GAPI.INVALID, GAPI.INVALID_INPUT, GAPI.INVALID_PARAMETER,
|
|
GAPI.INVALID_ORGUNIT, GAPI.INVALID_SCHEMA_VALUE, GAPI.CONDITION_NOT_MET],
|
|
body=body,
|
|
fields=fields,
|
|
resolveConflictAccount=resolveConflictAccount)
|
|
entityActionPerformed([Ent.USER, body['primaryEmail']], i, count)
|
|
if PwdOpts.filename and PwdOpts.notFoundPassword:
|
|
writeFile(PwdOpts.filename, f'{user},{PwdOpts.notFoundPassword}\n', mode='a', continueOnError=True)
|
|
if addGroups:
|
|
createUserAddToGroups(cd, result['primaryEmail'], addGroups, i, count)
|
|
if addAliases:
|
|
createUserAddAliases(cd, result['primaryEmail'], addAliases, i, count)
|
|
if notify.get('recipients'):
|
|
notify['password'] = notify['notFoundPassword']
|
|
sendCreateUpdateUserNotification(result, notify, tagReplacements, i, count)
|
|
except GAPI.duplicate:
|
|
duplicateAliasGroupUserWarning(cd, [Ent.USER, body['primaryEmail']], i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, user], Msg.UNABLE_TO_CREATE_NOT_FOUND_USER, i, count)
|
|
else:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
break
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER, user], Msg.NO_CHANGES, i, count)
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
except GAPI.invalidSchemaValue:
|
|
entityActionFailedWarning([Ent.USER, user], Msg.INVALID_SCHEMA_VALUE, i, count)
|
|
except GAPI.duplicate as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.USER, body['primaryEmail']], str(e), i, count)
|
|
except GAPI.invalidOrgunit:
|
|
entityActionFailedWarning([Ent.USER, user], Msg.INVALID_ORGUNIT, i, count)
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidInput, GAPI.invalidParameter, GAPI.insufficientArchivedUserLicenses,
|
|
GAPI.conflict, GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.conditionNotMet) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
|
|
# gam update users <UserTypeEntity> ...
|
|
def doUpdateUsers():
|
|
updateUsers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1])
|
|
|
|
# gam update user <UserItem> ...
|
|
def doUpdateUser():
|
|
updateUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
|
|
|
|
# gam <UserTypeEntity> delete users [noactionifalias]
|
|
def deleteUsers(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
noActionIfAlias = checkArgumentPresent('noactionifalias')
|
|
checkForExtraneousArguments()
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for user in entityList:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if noActionIfAlias and not verifyUserPrimaryEmail(cd, user, False, i, count):
|
|
continue
|
|
try:
|
|
callGAPI(cd.users(), 'delete',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN,
|
|
GAPI.CONDITION_NOT_MET],
|
|
userKey=user)
|
|
entityActionPerformed([Ent.USER, user], i, count)
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.conditionNotMet as e:
|
|
entityActionFailedWarning([Ent.USER, user], Msg.CAN_NOT_DELETE_USER_WITH_VAULT_HOLD.format(str(e), user), i, count)
|
|
|
|
# gam delete users <UserTypeEntity> [noactionifalias]
|
|
def doDeleteUsers():
|
|
deleteUsers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1])
|
|
|
|
# gam delete user <UserItem> [noactionifalias]
|
|
def doDeleteUser():
|
|
deleteUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
|
|
|
|
# gam <UserEntity> undelete users [ou|org|orgunit <OrgUnitPath>]
|
|
def undeleteUsers(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
if checkArgumentPresent(['ou', 'org', 'orgunit']):
|
|
entitySelector = getEntitySelector()
|
|
if entitySelector:
|
|
orgUnitPaths = getEntitySelection(entitySelector, True)
|
|
else:
|
|
orgUnitPaths = [getOrgUnitItem()]
|
|
userOrgUnitLists = orgUnitPaths if isinstance(orgUnitPaths, dict) else None
|
|
else:
|
|
orgUnitPaths = ['/']
|
|
userOrgUnitLists = None
|
|
checkForExtraneousArguments()
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for user in entityList:
|
|
i += 1
|
|
origUser = user
|
|
user = normalizeEmailAddressOrUID(user)
|
|
user_uid = user if user.find('@') == -1 else None
|
|
if not user_uid:
|
|
printEntityKVList([Ent.DELETED_USER, user],
|
|
[Msg.LOOKING_UP_GOOGLE_UNIQUE_ID, None],
|
|
i, count)
|
|
try:
|
|
deleted_users = callGAPIpages(cd.users(), 'list', 'users',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], showDeleted=True, orderBy='email',
|
|
maxResults=GC.Values[GC.USER_MAX_RESULTS])
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
matching_users = []
|
|
for deleted_user in deleted_users:
|
|
if str(deleted_user['primaryEmail']).lower() == user:
|
|
matching_users.append(deleted_user)
|
|
jcount = len(matching_users)
|
|
if jcount == 0:
|
|
entityUnknownWarning(Ent.DELETED_USER, user, i, count)
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
if jcount > 1:
|
|
entityActionNotPerformedWarning([Ent.DELETED_USER, user],
|
|
Msg.PLEASE_SELECT_ENTITY_TO_PROCESS.format(jcount, Ent.Plural(Ent.DELETED_USER), 'undelete', 'uid:<String>'),
|
|
i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for matching_user in matching_users:
|
|
printEntity([Ent.UNIQUE_ID, matching_user['id']], j, jcount)
|
|
Ind.Increment()
|
|
for attr_name in ['creationTime', 'lastLoginTime', 'deletionTime']:
|
|
if attr_name in matching_user:
|
|
printKeyValueList([attr_name, formatLocalTime(matching_user[attr_name])])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
setSysExitRC(MULTIPLE_DELETED_USERS_FOUND_RC)
|
|
continue
|
|
user_uid = matching_users[0]['id']
|
|
if userOrgUnitLists:
|
|
orgUnitPaths = userOrgUnitLists[origUser]
|
|
try:
|
|
callGAPI(cd.users(), 'undelete',
|
|
throwReasons=[GAPI.DELETED_USER_NOT_FOUND, GAPI.INVALID_ORGUNIT,
|
|
GAPI.DOMAIN_NOT_FOUND, GAPI.DOMAIN_CANNOT_USE_APIS,
|
|
GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.DUPLICATE,
|
|
GAPI.LIMIT_EXCEEDED],
|
|
userKey=user_uid, body={'orgUnitPath': makeOrgUnitPathAbsolute(orgUnitPaths[0])})
|
|
entityActionPerformed([Ent.DELETED_USER, user], i, count)
|
|
except GAPI.deletedUserNotFound:
|
|
entityUnknownWarning(Ent.DELETED_USER, user, i, count)
|
|
except GAPI.invalidOrgunit:
|
|
entityActionFailedWarning([Ent.USER, user], Msg.INVALID_ORGUNIT, i, count)
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.duplicate, GAPI.limitExceeded) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
|
|
# gam undelete users <UserEntity> [ou|org|orgunit <OrgUnitPath>]
|
|
def doUndeleteUsers():
|
|
undeleteUsers(getEntityList(Cmd.OB_USER_ENTITY))
|
|
|
|
# gam undelete user <UserItem> [ou|org|orgunit <OrgUnitPath>]
|
|
def doUndeleteUser():
|
|
undeleteUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
|
|
|
|
# gam <UserTypeEntity> suspend users [noactionifalias]
|
|
# gam <UserTypeEntity> unsuspend users [noactionifalias]
|
|
def suspendUnsuspendUsers(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
noActionIfAlias = checkArgumentPresent('noactionifalias')
|
|
checkForExtraneousArguments()
|
|
body = {'suspended': Act.Get() == Act.SUSPEND}
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for user in entityList:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if noActionIfAlias and not verifyUserPrimaryEmail(cd, user, False, i, count):
|
|
continue
|
|
try:
|
|
callGAPI(cd.users(), 'update',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN, GAPI.BAD_REQUEST],
|
|
userKey=user, body=body)
|
|
entityActionPerformed([Ent.USER, user], i, count)
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
|
|
# gam suspend users <UserTypeEntity> [noactionifalias]
|
|
# gam unsuspend users <UserTypeEntity> [noactionifalias]
|
|
def doSuspendUnsuspendUsers():
|
|
suspendUnsuspendUsers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)[1])
|
|
|
|
# gam suspend user <UserItem> [noactionifalias]
|
|
# gam unsuspend user <UserItem> [noactionifalias]
|
|
def doSuspendUnsuspendUser():
|
|
suspendUnsuspendUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
|
|
|
|
# gam <UserTypeEntity> signout
|
|
# gam <UserTypeEntity> turnoff2sv
|
|
def signoutTurnoff2SVUsers(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
if Act.Get() == Act.SIGNOUT:
|
|
service = cd.users()
|
|
function = 'signOut'
|
|
else:
|
|
service = cd.twoStepVerification()
|
|
function = 'turnOff'
|
|
checkForExtraneousArguments()
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for user in entityList:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
callGAPI(service, function,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_INPUT,
|
|
GAPI.DOMAIN_NOT_FOUND, GAPI.DOMAIN_CANNOT_USE_APIS,
|
|
GAPI.FORBIDDEN, GAPI.AUTH_ERROR],
|
|
userKey=user)
|
|
entityActionPerformed([Ent.USER, user], i, count)
|
|
except (GAPI.notFound, GAPI.userNotFound):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
except (GAPI.invalid, GAPI.invalidInput, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.authError) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
|
|
# gam <UserTypeEntity> waitformailbox [retries <Number>]
|
|
def waitForMailbox(entityList):
|
|
def showRetryStatus(user, exists, i, count):
|
|
kvList = ['Exists', exists, 'mailboxIsSetup', False]
|
|
if maxRetries:
|
|
kvList.extend(['Retry', f'{retries}/{maxRetries}'])
|
|
else:
|
|
kvList.extend(['Retry', f'{retries}'])
|
|
printEntityKVList([Ent.USER, user], kvList, i, count)
|
|
if maxRetries and retries >= maxRetries:
|
|
entityActionFailedWarning([Ent.USER, user], Msg.RETRIES_EXHAUSTED.format(maxRetries), i, count)
|
|
return False
|
|
time.sleep(3)
|
|
return True
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
maxRetries = 0
|
|
while Cmd.ArgumentsRemaining():
|
|
argument = getArgument()
|
|
if argument == 'retries':
|
|
maxRetries = getInteger(minVal=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for user in entityList:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
retries = 0
|
|
performAction(Ent.USER, user, i, count)
|
|
Ind.Increment()
|
|
while True:
|
|
retries += 1
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND],
|
|
userKey=user, fields='isMailboxSetup')
|
|
if result.get('isMailboxSetup', False):
|
|
entityActionPerformed([Ent.USER, user], i, count)
|
|
break
|
|
if not showRetryStatus(user, True, i, count):
|
|
break
|
|
except GAPI.userNotFound:
|
|
if not showRetryStatus(user, False, i, count):
|
|
break
|
|
except (GAPI.invalid, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.authError) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
break
|
|
except KeyboardInterrupt:
|
|
entityActionFailedWarning([Ent.USER, user], Msg.CHECK_INTERRUPTED, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def getUserLicenses(lic, user, skus):
|
|
def _callbackGetLicense(_, response, exception):
|
|
if exception is None:
|
|
if response and 'skuId' in response:
|
|
licenses.append(response['skuId'])
|
|
|
|
licenses = []
|
|
svcargs = dict([('userId', user['primaryEmail']), ('productId', None), ('skuId', None), ('fields', 'skuId')]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(lic.licenseAssignments(), 'get')
|
|
dbatch = lic.new_batch_http_request(callback=_callbackGetLicense)
|
|
for sku in skus:
|
|
svcparms = svcargs.copy()
|
|
svcparms['productId'] = sku[0]
|
|
svcparms['skuId'] = sku[1]
|
|
dbatch.add(method(**svcparms))
|
|
dbatch.execute()
|
|
return licenses
|
|
|
|
USER_NAME_PROPERTY_PRINT_ORDER = [
|
|
'givenName',
|
|
'familyName',
|
|
'fullName',
|
|
'displayName',
|
|
]
|
|
USER_LANGUAGE_PROPERTY_PRINT_ORDER = [
|
|
'languages',
|
|
]
|
|
USER_SCALAR_PROPERTY_PRINT_ORDER = [
|
|
'isAdmin',
|
|
'isDelegatedAdmin',
|
|
'isEnrolledIn2Sv',
|
|
'isEnforcedIn2Sv',
|
|
'agreedToTerms',
|
|
'ipWhitelisted',
|
|
'suspended',
|
|
'suspensionReason',
|
|
'archived',
|
|
'changePasswordAtNextLogin',
|
|
'id',
|
|
'customerId',
|
|
'isMailboxSetup',
|
|
'includeInGlobalAddressList',
|
|
'creationTime',
|
|
'lastLoginTime',
|
|
'deletionTime',
|
|
'orgUnitPath',
|
|
'recoveryEmail',
|
|
'recoveryPhone',
|
|
'thumbnailPhotoUrl',
|
|
]
|
|
USER_ARRAY_PROPERTY_PRINT_ORDER = [
|
|
'gender',
|
|
'keywords',
|
|
'notes',
|
|
'addresses',
|
|
'locations',
|
|
'organizations',
|
|
'relations',
|
|
'emails',
|
|
'ims',
|
|
'phones',
|
|
'posixAccounts',
|
|
'sshPublicKeys',
|
|
'externalIds',
|
|
'websites',
|
|
]
|
|
|
|
USER_ADDRESSES_PROPERTY_PRINT_ORDER = [
|
|
'primary',
|
|
'sourceIsStructured',
|
|
'formatted',
|
|
'extendedAddress',
|
|
'streetAddress',
|
|
'poBox',
|
|
'locality',
|
|
'region',
|
|
'postalCode',
|
|
'country',
|
|
'countryCode',
|
|
]
|
|
|
|
USER_LOCATIONS_PROPERTY_PRINT_ORDER = [
|
|
'area',
|
|
'buildingId',
|
|
'buildingName',
|
|
'floorName',
|
|
'floorSection',
|
|
'deskCode',
|
|
]
|
|
|
|
USER_ORGANIZATIONS_PROPERTY_PRINT_ORDER = [
|
|
'name',
|
|
'description',
|
|
'domain',
|
|
'symbol',
|
|
'location',
|
|
'costCenter',
|
|
'department',
|
|
'title',
|
|
'fullTimeEquivalent',
|
|
'primary',
|
|
]
|
|
|
|
USER_POSIX_PROPERTY_PRINT_ORDER = [
|
|
'accountId',
|
|
'uid',
|
|
'gid',
|
|
'systemId',
|
|
'operatingSystemType',
|
|
'homeDirectory',
|
|
'shell',
|
|
'gecos',
|
|
'primary',
|
|
]
|
|
|
|
USER_SSH_PROPERTY_PRINT_ORDER = [
|
|
'expirationTimeUsec',
|
|
'fingerprint',
|
|
]
|
|
|
|
USER_FIELDS_CHOICE_MAP = {
|
|
'address': 'addresses',
|
|
'addresses': 'addresses',
|
|
'admin': ['isAdmin', 'isDelegatedAdmin'],
|
|
'agreed2terms': 'agreedToTerms',
|
|
'agreedtoterms': 'agreedToTerms',
|
|
'aliases': ['aliases', 'nonEditableAliases'],
|
|
'archived': 'archived',
|
|
'changepassword': 'changePasswordAtNextLogin',
|
|
'changepasswordatnextlogin': 'changePasswordAtNextLogin',
|
|
'creationtime': 'creationTime',
|
|
'customerid': 'customerId',
|
|
'deletiontime': 'deletionTime',
|
|
'displayname': 'name.displayName',
|
|
'email': 'emails',
|
|
'emails': 'emails',
|
|
'employeeid': 'externalIds',
|
|
'externalid': 'externalIds',
|
|
'externalids': 'externalIds',
|
|
'familyname': 'name.familyName',
|
|
'firstname': 'name.givenName',
|
|
'fullname': 'name.fullName',
|
|
'gal': 'includeInGlobalAddressList',
|
|
'givenname': 'name.givenName',
|
|
'gender': ['gender.type', 'gender.customGender', 'gender.addressMeAs'],
|
|
'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',
|
|
'manager': 'relations',
|
|
'name': ['name.givenName', 'name.familyName', 'name.fullName', 'name.displayName'],
|
|
'nicknames': ['aliases', 'nonEditableAliases'],
|
|
'noneditablealiases': ['aliases', 'nonEditableAliases'],
|
|
'note': 'notes',
|
|
'notes': 'notes',
|
|
'org': 'orgUnitPath',
|
|
'organization': 'organizations',
|
|
'organizations': 'organizations',
|
|
'organisation': 'organizations',
|
|
'organisations': 'organizations',
|
|
'orgunit': 'orgUnitPath',
|
|
'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',
|
|
}
|
|
|
|
INFO_USER_OPTIONS = {'noaliases', 'nobuildingnames', 'nogroups', 'nolicenses', 'nolicences', 'noschemas', 'schemas', 'userview'}
|
|
USER_SKIP_OBJECTS = {'thumbnailPhotoEtag'}
|
|
USER_TIME_OBJECTS = {'creationTime', 'deletionTime', 'lastLoginTime'}
|
|
|
|
def _formatLanguagesList(propertyValue, delimiter):
|
|
languages = []
|
|
for language in propertyValue:
|
|
if 'languageCode' in language:
|
|
lang = language['languageCode']
|
|
if language.get('preference') == 'preferred':
|
|
lang += '+'
|
|
elif language.get('preference') == 'not_preferred':
|
|
lang += '-'
|
|
else:
|
|
lang = language.get('customLanguage')
|
|
if lang:
|
|
languages.append(lang)
|
|
return delimiter.join(languages)
|
|
|
|
def _initSchemaParms(projection):
|
|
return {'projection': projection, 'customFieldMask': None, 'selectedSchemaFields': {}}
|
|
|
|
def _getSchemaNameList(schemaParms):
|
|
customFieldMask = getString(Cmd.OB_SCHEMA_NAME_LIST).replace(' ', ',')
|
|
if customFieldMask.lower() == 'all':
|
|
schemaParms['projection'] = 'full'
|
|
schemaParms['customFieldMask'] = None
|
|
schemaParms['selectedSchemaFields'] = {}
|
|
else:
|
|
schemaParms['projection'] = 'custom'
|
|
customFieldMaskList = []
|
|
for schemaField in customFieldMask.split(','):
|
|
if schemaField.find('.') == -1:
|
|
customFieldMaskList.append(schemaField)
|
|
else:
|
|
schemaName, fieldName = schemaField.split('.', 1)
|
|
customFieldMaskList.append(schemaName)
|
|
schemaParms['selectedSchemaFields'] .setdefault(schemaName, set())
|
|
schemaParms['selectedSchemaFields'][schemaName].add(fieldName)
|
|
schemaParms['customFieldMask'] = ','.join(customFieldMaskList)
|
|
|
|
def _filterSchemaFields(userEntity, schemaParms):
|
|
schemas = userEntity.pop('customSchemas', None)
|
|
if schemas is None:
|
|
return
|
|
customSchemas = {}
|
|
for schema in sorted(schemas):
|
|
if schema in schemaParms['selectedSchemaFields']:
|
|
for field, value in sorted(iter(schemas[schema].items())):
|
|
if field not in schemaParms['selectedSchemaFields'][schema]:
|
|
continue
|
|
customSchemas.setdefault(schema, {})
|
|
customSchemas[schema][field] = value
|
|
else:
|
|
customSchemas[schema] = schemas[schema]
|
|
userEntity['customSchemas'] = customSchemas
|
|
|
|
def infoUsers(entityList):
|
|
def printUserCIGroupMap(parent, group_name_mappings, seen_group_count, edges, direction):
|
|
for a_parent, a_child in edges:
|
|
if a_parent == parent:
|
|
output = f'{Ind.Spaces()}{a_child}: {group_name_mappings[a_child]} ({direction})'
|
|
if seen_group_count[a_child] > 1:
|
|
output += ' *'
|
|
printLine(output)
|
|
Ind.Increment()
|
|
printUserCIGroupMap(a_child, group_name_mappings, seen_group_count, edges, 'inherited')
|
|
Ind.Decrement()
|
|
|
|
def _showType(row, typeKey, typeCustomValue, customTypeKey, defaultType=None):
|
|
if typeKey in row:
|
|
if row[typeKey] != typeCustomValue or not row.get(customTypeKey):
|
|
printKeyValueList([typeKey, row[typeKey]])
|
|
else:
|
|
printKeyValueList([typeKey, row[typeKey]])
|
|
Ind.Increment()
|
|
printKeyValueList([customTypeKey, row[customTypeKey]])
|
|
Ind.Decrement()
|
|
return True
|
|
if customTypeKey in row:
|
|
printKeyValueList([customTypeKey, row[customTypeKey]])
|
|
return True
|
|
if defaultType:
|
|
printKeyValueList([typeKey, defaultType])
|
|
return True
|
|
return False
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
getAliases = getBuildingNames = getCIGroupsTree = getGroups = getLicenses = getSchemas = not GC.Values[GC.QUICK_INFO_USER]
|
|
getGroupsTree = False
|
|
FJQC = FormatJSONQuoteChar()
|
|
schemaParms = _initSchemaParms('full')
|
|
viewType = 'admin_view'
|
|
fieldsList = []
|
|
groups = []
|
|
memberships = []
|
|
skus = SKU.getAllSKUs() if not GM.Globals[GM.LICENSE_SKUS] else GM.Globals[GM.LICENSE_SKUS]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'quick':
|
|
getAliases = getBuildingNames = getCIGroupsTree = getGroups = getGroupsTree = getLicenses = getSchemas = False
|
|
elif myarg in {'noaliases', 'aliases'}:
|
|
getAliases = myarg == 'aliases'
|
|
elif myarg in {'nobuildingnames', 'buildingnames'}:
|
|
getBuildingNames = myarg == 'buildingnames'
|
|
elif myarg in {'nogroups', 'groups', 'grouptree', 'cigrouptree'}:
|
|
getGroups = getGroupsTree = getCIGroupsTree = False
|
|
getGroups = myarg == 'groups'
|
|
getGroupsTree = myarg == 'grouptree'
|
|
getCIGroupsTree = myarg == 'cigrouptree'
|
|
elif myarg in {'nolicenses', 'nolicences', 'licenses', 'licences'}:
|
|
getLicenses = myarg in {'licenses', 'licences'}
|
|
elif myarg == 'noschemas':
|
|
getSchemas = False
|
|
schemaParms = _initSchemaParms('basic')
|
|
elif myarg in {'allschemas', 'custom', 'schemas', 'customschemas'}:
|
|
if myarg == 'allschemas':
|
|
schemaParms = _initSchemaParms('full')
|
|
else:
|
|
_getSchemaNameList(schemaParms)
|
|
getSchemas = True
|
|
elif myarg in {'products', 'product'}:
|
|
skus = SKU.convertProductListToSKUList(getGoogleProductList())
|
|
elif myarg in {'sku', 'skus'}:
|
|
skus = getGoogleSKUList()
|
|
elif myarg == 'userview':
|
|
viewType = 'domain_public'
|
|
getGroups = getLicenses = False
|
|
elif getFieldsList(myarg, USER_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
# Ignore info group arguments that may have come from whatis
|
|
elif myarg in INFO_GROUP_OPTIONS:
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if fieldsList:
|
|
fieldsList.append('primaryEmail')
|
|
if getSchemas:
|
|
fieldsList.append('customSchemas')
|
|
if getAliases:
|
|
fieldsList.extend(['aliases', 'nonEditableAliases'])
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
if getLicenses:
|
|
lic = buildGAPIObject(API.LICENSING)
|
|
# Special case; for info users, 'all users' means 'all users_ns_susp'
|
|
if isinstance(entityList, dict) and entityList.get('entityType') == Cmd.ENTITY_ALL_USERS:
|
|
entityList['entityType'] = Cmd.ENTITY_ALL_USERS_NS_SUSP
|
|
groupParents = {}
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
for userEmail in entityList:
|
|
i += 1
|
|
userEmail = normalizeEmailAddressOrUID(userEmail)
|
|
try:
|
|
user = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND],
|
|
userKey=userEmail, projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
|
|
viewType=viewType, fields=fields)
|
|
groups = []
|
|
memberships = []
|
|
if getGroups or getGroupsTree:
|
|
kwargs = {}
|
|
if GC.Values[GC.ENABLE_DASA]:
|
|
# Allows groups.list() to function but will limit
|
|
# returned groups to those in same domain as user
|
|
# so only do this for DASA admins
|
|
kwargs['domain'] = GC.Values[GC.DOMAIN]
|
|
try:
|
|
groups = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userKey=user['primaryEmail'], orderBy='email', fields='nextPageToken,groups(name,email)', **kwargs)
|
|
except (GAPI.forbidden, GAPI.domainNotFound):
|
|
### Print some message
|
|
pass
|
|
elif getCIGroupsTree:
|
|
ci, memberships = getCIGroupMembershipGraph(ci, user['primaryEmail'])
|
|
if memberships is None:
|
|
getCIGroupsTree = False
|
|
licenses = getUserLicenses(lic, user, skus) if getLicenses else []
|
|
if FJQC.formatJSON:
|
|
if getGroups or getGroupsTree:
|
|
user['groups'] = groups
|
|
if getGroupsTree:
|
|
for group in user['groups']:
|
|
groupEmail = group['email']
|
|
if groupEmail not in groupParents:
|
|
getGroupParents(cd, groupParents, groupEmail, group['name'], {})
|
|
addJsonGroupParents(groupParents, group, groupEmail)
|
|
if getLicenses:
|
|
user['licenses'] = [SKU.formatSKUIdDisplayName(u_license) for u_license in licenses]
|
|
if getBuildingNames:
|
|
for location in user.get('locations', []):
|
|
location['buildingName'] = _getBuildingNameById(cd, location.get('buildingId', ''))
|
|
if not getAliases:
|
|
user.pop('aliases', None)
|
|
user.pop('nonEditableAliases', None)
|
|
if not getSchemas:
|
|
user.pop('customSchemas', None)
|
|
printLine(json.dumps(cleanJSON(user, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
continue
|
|
printEntity([Ent.USER, user['primaryEmail']], i, count)
|
|
Ind.Increment()
|
|
printKeyValueList(['Settings', None])
|
|
Ind.Increment()
|
|
if 'name' in user:
|
|
for up in USER_NAME_PROPERTY_PRINT_ORDER:
|
|
if up in user['name']:
|
|
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], user['name'][up]])
|
|
up = 'languages'
|
|
if up in user:
|
|
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], _formatLanguagesList(user[up], ',')])
|
|
for up in USER_SCALAR_PROPERTY_PRINT_ORDER:
|
|
if up in user:
|
|
if up not in USER_TIME_OBJECTS:
|
|
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], user[up]])
|
|
else:
|
|
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], formatLocalTime(user[up])])
|
|
Ind.Decrement()
|
|
for up in USER_ARRAY_PROPERTY_PRINT_ORDER:
|
|
if up not in user:
|
|
continue
|
|
propertyValue = user[up]
|
|
userProperty = UProp.PROPERTIES[up]
|
|
propertyClass = userProperty[UProp.CLASS]
|
|
propertyTitle = userProperty[UProp.TITLE]
|
|
if UProp.TYPE_KEYWORDS in userProperty:
|
|
typeKey = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_TYPE_KEYWORD]
|
|
typeCustomValue = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
|
|
customTypeKey = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]
|
|
if propertyClass == UProp.PC_ARRAY:
|
|
if propertyValue:
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
for row in propertyValue:
|
|
_showType(row, typeKey, typeCustomValue, customTypeKey)
|
|
Ind.Increment()
|
|
for key in row:
|
|
if key in [typeKey, customTypeKey]:
|
|
continue
|
|
printKeyValueList([key, row[key]])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
elif propertyClass == UProp.PC_GENDER:
|
|
if propertyValue:
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
_showType(propertyValue, typeKey, typeCustomValue, customTypeKey)
|
|
if 'addressMeAs' in propertyValue:
|
|
printKeyValueList(['addressMeAs', propertyValue['addressMeAs']])
|
|
Ind.Decrement()
|
|
elif propertyClass == UProp.PC_ADDRESSES:
|
|
if propertyValue:
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
for row in propertyValue:
|
|
_showType(row, typeKey, typeCustomValue, customTypeKey)
|
|
Ind.Increment()
|
|
for key in USER_ADDRESSES_PROPERTY_PRINT_ORDER:
|
|
if key in row:
|
|
if key != 'formatted':
|
|
printKeyValueList([key, row[key]])
|
|
else:
|
|
printKeyValueWithCRsNLs(key, row[key])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
elif propertyClass == UProp.PC_EMAILS:
|
|
if propertyValue:
|
|
needTitle = True
|
|
for row in propertyValue:
|
|
if row['address'].lower() == user['primaryEmail'].lower():
|
|
continue
|
|
if needTitle:
|
|
needTitle = False
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
if not _showType(row, typeKey, typeCustomValue, customTypeKey):
|
|
if not getAliases:
|
|
continue
|
|
printKeyValueList([typeKey, 'alias'])
|
|
Ind.Increment()
|
|
for key in row:
|
|
if key in [typeKey, customTypeKey]:
|
|
continue
|
|
printKeyValueList([key, row[key]])
|
|
Ind.Decrement()
|
|
if not needTitle:
|
|
Ind.Decrement()
|
|
elif propertyClass == UProp.PC_IMS:
|
|
if propertyValue:
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
protocolKey = UProp.IM_PROTOCOLS[UProp.PTKW_ATTR_TYPE_KEYWORD]
|
|
protocolCustomValue = UProp.IM_PROTOCOLS[UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
|
|
customProtocolKey = UProp.IM_PROTOCOLS[UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]
|
|
for row in propertyValue:
|
|
_showType(row, typeKey, typeCustomValue, customTypeKey)
|
|
Ind.Increment()
|
|
_showType(row, protocolKey, protocolCustomValue, customProtocolKey)
|
|
for key in row:
|
|
if key in [typeKey, customTypeKey, protocolKey, customProtocolKey]:
|
|
continue
|
|
printKeyValueList([key, row[key]])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
elif propertyClass == UProp.PC_NOTES:
|
|
if propertyValue:
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
if isinstance(propertyValue, dict):
|
|
typeVal = propertyValue.get(typeKey, 'text_plain')
|
|
printKeyValueList([typeKey, typeVal])
|
|
Ind.Increment()
|
|
if typeVal == 'text_html':
|
|
printKeyValueWithCRsNLs('value', dehtml(propertyValue['value']))
|
|
else:
|
|
printKeyValueWithCRsNLs('value', propertyValue['value'])
|
|
Ind.Decrement()
|
|
else:
|
|
printKeyValueList([Ind.MultiLineText(propertyValue)])
|
|
Ind.Decrement()
|
|
elif propertyClass == UProp.PC_LOCATIONS:
|
|
if propertyValue:
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
if isinstance(propertyValue, list):
|
|
for row in propertyValue:
|
|
_showType(row, typeKey, typeCustomValue, customTypeKey)
|
|
Ind.Increment()
|
|
if getBuildingNames:
|
|
row['buildingName'] = _getBuildingNameById(cd, row.get('buildingId', ''))
|
|
for key in USER_LOCATIONS_PROPERTY_PRINT_ORDER:
|
|
if key in row:
|
|
printKeyValueList([key, row[key]])
|
|
Ind.Decrement()
|
|
else:
|
|
printKeyValueList([Ind.MultiLineText(propertyValue)])
|
|
Ind.Decrement()
|
|
elif propertyClass == UProp.PC_ORGANIZATIONS:
|
|
if propertyValue:
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
for row in propertyValue:
|
|
_showType(row, typeKey, typeCustomValue, customTypeKey)
|
|
Ind.Increment()
|
|
for key in USER_ORGANIZATIONS_PROPERTY_PRINT_ORDER:
|
|
if key in row:
|
|
printKeyValueList([key, row[key]])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
elif propertyClass == UProp.PC_POSIX:
|
|
if propertyValue:
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
if isinstance(propertyValue, list):
|
|
for row in propertyValue:
|
|
printKeyValueList(['username', row.get('username')])
|
|
Ind.Increment()
|
|
for key in USER_POSIX_PROPERTY_PRINT_ORDER:
|
|
if key in row:
|
|
printKeyValueList([key, row[key]])
|
|
Ind.Decrement()
|
|
else:
|
|
printKeyValueList([Ind.MultiLineText(propertyValue)])
|
|
Ind.Decrement()
|
|
elif propertyClass == UProp.PC_SSH:
|
|
if propertyValue:
|
|
printKeyValueList([propertyTitle, None])
|
|
Ind.Increment()
|
|
if isinstance(propertyValue, list):
|
|
for row in propertyValue:
|
|
printKeyValueList(['key', row['key']])
|
|
Ind.Increment()
|
|
for key in USER_SSH_PROPERTY_PRINT_ORDER:
|
|
if key in row:
|
|
printKeyValueList([key, row[key]])
|
|
Ind.Decrement()
|
|
else:
|
|
printKeyValueList([Ind.MultiLineText(propertyValue)])
|
|
Ind.Decrement()
|
|
if getSchemas:
|
|
up = 'customSchemas'
|
|
if up in user:
|
|
propertyValue = user[up]
|
|
userProperty = UProp.PROPERTIES[up]
|
|
propertyTitle = userProperty[UProp.TITLE]
|
|
typeKey = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_TYPE_KEYWORD]
|
|
typeCustomValue = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_TYPE_CUSTOM_VALUE]
|
|
customTypeKey = userProperty[UProp.TYPE_KEYWORDS][UProp.PTKW_ATTR_CUSTOMTYPE_KEYWORD]
|
|
if schemaParms['selectedSchemaFields']:
|
|
_filterSchemaFields(user, schemaParms)
|
|
propertyValue = user[up]
|
|
if propertyValue:
|
|
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], None])
|
|
Ind.Increment()
|
|
for schema in sorted(propertyValue):
|
|
printKeyValueList(['Schema', schema])
|
|
Ind.Increment()
|
|
for field in propertyValue[schema]:
|
|
if isinstance(propertyValue[schema][field], list):
|
|
printKeyValueList([field])
|
|
Ind.Increment()
|
|
for an_item in propertyValue[schema][field]:
|
|
_showType(an_item, typeKey, typeCustomValue, customTypeKey, defaultType='work')
|
|
Ind.Increment()
|
|
printKeyValueList(['value', an_item['value']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
printKeyValueList([field, propertyValue[schema][field]])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
if getAliases:
|
|
for up in ['aliases', 'nonEditableAliases']:
|
|
propertyValue = user.get(up, [])
|
|
if propertyValue:
|
|
printEntitiesCount([Ent.NONEDITABLE_ALIAS, Ent.EMAIL_ALIAS][up == 'aliases'], propertyValue)
|
|
Ind.Increment()
|
|
for alias in propertyValue:
|
|
printKeyValueList(['alias', alias])
|
|
Ind.Decrement()
|
|
if getGroups:
|
|
printEntitiesCount(Ent.GROUP, groups)
|
|
Ind.Increment()
|
|
for group in groups:
|
|
printKeyValueList([group['name'], group['email']])
|
|
Ind.Decrement()
|
|
elif getGroupsTree:
|
|
printEntity([Ent.GROUP_MEMBERSHIP_TREE, ''])
|
|
Ind.Increment()
|
|
for group in groups:
|
|
groupEmail = group['email']
|
|
if groupEmail not in groupParents:
|
|
getGroupParents(cd, groupParents, groupEmail, group['name'], {})
|
|
showGroupParents(groupParents, groupEmail, None, 0, 0)
|
|
Ind.Decrement()
|
|
elif getCIGroupsTree:
|
|
printEntity([Ent.GROUP_MEMBERSHIP_TREE, ''])
|
|
if memberships:
|
|
Ind.Increment()
|
|
group_name_mapping = {}
|
|
group_displayname_mapping = {}
|
|
groups = memberships.get('groups', [])
|
|
for group in groups:
|
|
group_name = group.get('name')
|
|
group_key = group.get('groupKey', {})
|
|
group_email = group_key.get('id', '')
|
|
group_display_name = group.get('displayName', '')
|
|
group_name_mapping[group_name] = group_email
|
|
group_displayname_mapping[group_email] = group_display_name
|
|
edges = []
|
|
seen_group_count = {}
|
|
for adj in memberships.get('adjacencyList', []):
|
|
group_name = adj.get('group', '')
|
|
group_email = group_name_mapping[group_name]
|
|
for edge in adj.get('edges', []):
|
|
seen_group_count[group_email] = seen_group_count.get(group_email, 0) + 1
|
|
member_email = edge.get('preferredMemberKey', {}).get('id')
|
|
edges.append((member_email, group_email))
|
|
printUserCIGroupMap(user['primaryEmail'], group_displayname_mapping, seen_group_count, edges, 'direct')
|
|
if max(seen_group_count.values()) > 1:
|
|
printLine(f'{Ind.Spaces()}* {Msg.USER_HAS_MULTIPLE_DIRECT_OR_INHERITED_MEMBERSHIPS_IN_GROUP}')
|
|
Ind.Decrement()
|
|
if licenses:
|
|
printEntitiesCount(Ent.LICENSE, licenses)
|
|
Ind.Increment()
|
|
for u_license in licenses:
|
|
printKeyValueList([SKU.formatSKUIdDisplayName(u_license)])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, userEmail, i, count)
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError) as e:
|
|
entityActionFailedWarning([Ent.USER, userEmail], str(e), i, count)
|
|
except (GAPI.invalidInput, GAPI.invalidMember) as e:
|
|
if schemaParms['customFieldMask']:
|
|
entityActionFailedWarning([Ent.USER, userEmail], invalidUserSchema(schemaParms['customFieldMask']), i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, userEmail], str(e), i, count)
|
|
|
|
# gam info users <UserTypeEntity>
|
|
# [quick]
|
|
# [noaliases|aliases]
|
|
# [nobuildingnames|buildingnames]
|
|
# [nogroups|groups|grouptree|cigrouptree]
|
|
# [nolicenses|nolicences|licenses|licences]
|
|
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
|
# [noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
|
|
# [userview] <UserFieldName>* [fields <UserFieldNameList>]
|
|
# [formatjson]
|
|
def doInfoUsers():
|
|
infoUsers(getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS, delayGet=True)[1])
|
|
|
|
# gam info user <UserItem>
|
|
# [quick]
|
|
# [noaliases|aliases]
|
|
# [nobuildingnames|buildingnames]
|
|
# [nogroups|groups|grouptree|cigrouptree]
|
|
# [nolicenses|nolicences|licenses|licences]
|
|
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
|
# [noschemas|allschemas|(schemas|custom|customschemas <SchemaNameList>)]
|
|
# [userview] <UserFieldName>* [fields <UserFieldNameList>]
|
|
# [formatjson]
|
|
# gam info user
|
|
def doInfoUser():
|
|
if Cmd.ArgumentsRemaining():
|
|
infoUsers(getStringReturnInList(Cmd.OB_USER_ITEM))
|
|
else:
|
|
infoUsers([_getAdminEmail()])
|
|
|
|
USERS_ORDERBY_CHOICE_MAP = {
|
|
'familyname': 'familyName',
|
|
'lastname': 'familyName',
|
|
'givenname': 'givenName',
|
|
'firstname': 'givenName',
|
|
'email': 'email',
|
|
}
|
|
USERS_INDEXED_TITLES = ['addresses', 'aliases', 'nonEditableAliases', 'emails', 'externalIds',
|
|
'ims', 'keywords', 'locations', 'organizations',
|
|
'phones', 'posixAccounts', 'relations', 'sshPublicKeys', 'websites']
|
|
|
|
# gam print users [todrive <ToDriveAttribute>*]
|
|
# ([domain|domains <DomainNameEntity>] [(query <QueryUser>)|(queries <QueryUserList>)]
|
|
# [limittoou <OrgUnitItem>] [deleted_only|only_deleted])|[select <UserTypeEntity>]
|
|
# [groups|groupsincolumns]
|
|
# [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser]
|
|
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
|
# [emailpart|emailparts|username] [schemas|custom all|<SchemaNameList>]
|
|
# [orderby <UserOrderByFieldName> [ascending|descending]]
|
|
# [userview] [basic|full|allfields | <UserFieldName>* | fields <UserFieldNameList>]
|
|
# [delimiter <Character>] [sortheaders] [formatjson [quotechar <Character>]] [quoteplusphonenumbers]
|
|
# [convertcrnl]
|
|
# [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
|
# [showitemcountonly]
|
|
# [showvalidcolumn] (addcsvdata <FieldName> <String>)*
|
|
#
|
|
# gam <UserTypeEntity> print users [todrive <ToDriveAttribute>*]
|
|
# [groups|groupsincolumns]
|
|
# [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser]
|
|
# [(products|product <ProductIDList>)|(skus|sku <SKUIDList>)]
|
|
# [emailpart|emailparts|username] [schemas|custom all|<SchemaNameList>]
|
|
# [orderby <UserOrderByFieldName> [ascending|descending]]
|
|
# [userview] [basic|full|allfields | <UserFieldName>* | fields <UserFieldNameList>]
|
|
# [delimiter <Character>] [sortheaders] [formatjson [quotechar <Character>]] [quoteplusphonenumbers]
|
|
# [convertcrnl]
|
|
# [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
|
# [showitemcountonly]
|
|
# [showvalidcolumn] (addcsvdata <FieldName> <String>)*
|
|
#
|
|
# gam print users [todrive <ToDriveAttribute>*]
|
|
# ([domain <DomainName>] [(query <QueryUser>)|(queries <QueryUserList>)]
|
|
# [limittoou <OrgUnitItem>] [deleted_only|only_deleted])|[select <UserTypeEntity>]
|
|
# [formatjson [quotechar <Character>]] [countonly]
|
|
# [issuspended <Boolean>] [aliasmatchpattern <REMatchPattern>]
|
|
# [showitemcountonly]
|
|
# [showvalidcolumn] (addcsvdata <FieldName> <String>)*
|
|
#
|
|
# gam <UserTypeEntity> print users [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]] [countonly]
|
|
# [issuspended <Boolean>]
|
|
# [showitemcountonly]
|
|
def doPrintUsers(entityList=None):
|
|
def _writeUserEntity(userEntity):
|
|
if addCSVData:
|
|
userEntity.update(addCSVData)
|
|
row = flattenJSON(userEntity, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'primaryEmail': userEntity['primaryEmail']}
|
|
if showValidColumn:
|
|
row[showValidColumn] = userEntity[showValidColumn]
|
|
row['JSON'] = json.dumps(cleanJSON(userEntity, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
def _printUser(userEntity, i, count):
|
|
if isSuspended is None or isSuspended == userEntity.get('suspended', isSuspended):
|
|
if showValidColumn:
|
|
userEntity[showValidColumn] = True
|
|
userEmail = userEntity['primaryEmail']
|
|
if printOptions['emailParts']:
|
|
if userEmail.find('@') != -1:
|
|
userEntity['primaryEmailLocal'], userEntity['primaryEmailDomain'] = splitEmailAddress(userEmail)
|
|
if 'languages' in userEntity and not FJQC.formatJSON:
|
|
userEntity['languages'] = _formatLanguagesList(userEntity.pop('languages'), delimiter)
|
|
for location in userEntity.get('locations', []):
|
|
location['buildingName'] = _getBuildingNameById(cd, location.get('buildingId', ''))
|
|
if quotePlusPhoneNumbers:
|
|
for phone in userEntity.get('phones', []):
|
|
phoneNumber = phone.get('value', '')
|
|
if phoneNumber.startswith('+'):
|
|
phone['value'] = "'"+phoneNumber
|
|
if schemaParms['selectedSchemaFields']:
|
|
_filterSchemaFields(userEntity, schemaParms)
|
|
if printOptions['getGroupFeed']:
|
|
printGettingAllEntityItemsForWhom(Ent.GROUP_MEMBERSHIP, userEmail, i, count)
|
|
try:
|
|
groups = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userKey=userEmail, orderBy='email', fields='nextPageToken,groups(email)')
|
|
numGroups = len(groups)
|
|
if not printOptions['groupsInColumns']:
|
|
userEntity['GroupsCount'] = numGroups
|
|
userEntity['Groups'] = delimiter.join([groupname['email'] for groupname in groups])
|
|
else:
|
|
if numGroups > printOptions['maxGroups']:
|
|
printOptions['maxGroups'] = numGroups
|
|
userEntity['Groups'] = numGroups
|
|
for j, group in enumerate(groups):
|
|
userEntity[f'Groups{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}'] = group['email']
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
badRequestWarning(Ent.GROUP, Ent.MEMBER, userEmail)
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
if aliasMatchPattern and 'aliases' in userEntity:
|
|
userEntity['aliases'] = [alias for alias in userEntity['aliases'] if aliasMatchPattern.match(alias)]
|
|
if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']:
|
|
if printOptions['getLicenseFeed']:
|
|
u_licenses = licenses.get(userEmail.lower(), [])
|
|
else:
|
|
u_licenses = getUserLicenses(lic, userEntity, skus)
|
|
if not oneLicensePerRow:
|
|
userEntity['LicensesCount'] = len(u_licenses)
|
|
if u_licenses:
|
|
userEntity['Licenses'] = delimiter.join(u_licenses)
|
|
userEntity['LicensesDisplay'] = delimiter.join([SKU.skuIdToDisplayName(skuId) for skuId in u_licenses])
|
|
else:
|
|
u_licenses = []
|
|
if not oneLicensePerRow:
|
|
_writeUserEntity(userEntity)
|
|
else:
|
|
if u_licenses:
|
|
for skuId in u_licenses:
|
|
userEntity['License'] = skuId
|
|
userEntity['LicenseDisplay'] = SKU.skuIdToDisplayName(skuId)
|
|
_writeUserEntity(userEntity)
|
|
else:
|
|
userEntity['License'] = userEntity['LicenseDisplay'] = ''
|
|
_writeUserEntity(userEntity)
|
|
|
|
def _updateDomainCounts(emailAddress):
|
|
nonlocal domainCounts
|
|
atLoc = emailAddress.find('@')
|
|
if atLoc == -1:
|
|
dom = UNKNOWN
|
|
else:
|
|
dom = emailAddress[atLoc+1:].lower()
|
|
domainCounts.setdefault(dom, 0)
|
|
domainCounts[dom] += 1
|
|
|
|
_PRINT_USER_REASON_TO_MESSAGE_MAP = {GAPI.RESOURCE_NOT_FOUND: Msg.DOES_NOT_EXIST}
|
|
def _callbackPrintUser(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
_printUser(response, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason in GAPI.USER_GET_THROW_REASONS:
|
|
if not showValidColumn:
|
|
entityUnknownWarning(Ent.USER, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
_writeUserEntity({'primaryEmail': ri[RI_ITEM], showValidColumn: False})
|
|
elif (reason == GAPI.INVALID_INPUT) and schemaParms['customFieldMask']:
|
|
entityActionFailedWarning([Ent.USER, ri[RI_ITEM]], invalidUserSchema(schemaParms['customFieldMask']), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
elif reason not in GAPI.DEFAULT_RETRY_REASONS:
|
|
errMsg = getHTTPError(_PRINT_USER_REASON_TO_MESSAGE_MAP, http_status, reason, message)
|
|
printKeyValueList([ERROR, errMsg])
|
|
else:
|
|
waitOnFailure(1, 10, reason, message)
|
|
try:
|
|
user = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.RATE_LIMIT_EXCEEDED],
|
|
userKey=ri[RI_ITEM], projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
|
|
viewType=viewType, fields=fields)
|
|
_printUser(user, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except (GAPI.userNotFound, GAPI.resourceNotFound):
|
|
if not showValidColumn:
|
|
entityUnknownWarning(Ent.USER, ri[RI_ITEM], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
_writeUserEntity({'primaryEmail': ri[RI_ITEM], showValidColumn: False})
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.rateLimitExceeded) as e:
|
|
entityActionFailedWarning([Ent.USER, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except GAPI.invalidInput as e:
|
|
if schemaParms['customFieldMask']:
|
|
entityActionFailedWarning([Ent.USER, ri[RI_ITEM]], invalidUserSchema(schemaParms['customFieldMask']), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
fieldsList = ['primaryEmail']
|
|
csvPF = CSVPrintFile(fieldsList, indexedTitles=USERS_INDEXED_TITLES)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
printOptions = {
|
|
'countOnly': False,
|
|
'emailParts': False,
|
|
'getGroupFeed': False,
|
|
'getLicenseFeed': False,
|
|
'getLicenseFeedByUser': False,
|
|
'groupsInColumns': False,
|
|
'scalarsFirst': False,
|
|
'sortHeaders': False,
|
|
'maxGroups': 0
|
|
}
|
|
kwargsDict = initUserGroupDomainQueryFilters()
|
|
licenses = {}
|
|
lic = None
|
|
skus = None
|
|
maxResults = GC.Values[GC.USER_MAX_RESULTS]
|
|
schemaParms = _initSchemaParms('basic')
|
|
projectionSet = False
|
|
oneLicensePerRow = quotePlusPhoneNumbers = showDeleted = False
|
|
aliasMatchPattern = isSuspended = orgUnitPath = orgUnitPathLower = orderBy = sortOrder = None
|
|
viewType = 'admin_view'
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
showValidColumn = ''
|
|
showItemCountOnly = False
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif entityList is None and myarg == 'limittoou':
|
|
orgUnitPath = getOrgUnitItem(pathOnly=True, cd=cd)
|
|
orgUnitPathLower = orgUnitPath.lower()
|
|
elif entityList is None and getUserGroupDomainQueryFilters(myarg, kwargsDict):
|
|
pass
|
|
elif entityList is None and myarg in {'deletedonly', 'onlydeleted'}:
|
|
showDeleted = True
|
|
elif entityList is None and myarg == 'select':
|
|
_, entityList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
elif myarg == 'issuspended':
|
|
isSuspended = getBoolean()
|
|
elif myarg == 'orderby':
|
|
orderBy, sortOrder = getOrderBySortOrder(USERS_ORDERBY_CHOICE_MAP)
|
|
elif myarg == 'userview':
|
|
viewType = 'domain_public'
|
|
elif myarg in {'allfields', 'basic'}:
|
|
schemaParms = _initSchemaParms('basic')
|
|
projectionSet = printOptions['sortHeaders'] = True
|
|
fieldsList = []
|
|
elif myarg == 'full':
|
|
if schemaParms['projection'] != 'custom':
|
|
schemaParms = _initSchemaParms(myarg)
|
|
projectionSet = printOptions['sortHeaders'] = True
|
|
fieldsList = []
|
|
elif myarg in {'custom', 'schemas', 'customschemas'}:
|
|
projectionSet = True
|
|
_getSchemaNameList(schemaParms)
|
|
if fieldsList:
|
|
fieldsList.append('customSchemas')
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg == 'sortheaders':
|
|
printOptions['sortHeaders'] = getBoolean()
|
|
elif myarg == 'scalarsfirst':
|
|
printOptions['scalarsFirst'] = getBoolean()
|
|
elif csvPF.GetFieldsListTitles(myarg, USER_FIELDS_CHOICE_MAP, fieldsList, initialField='primaryEmail'):
|
|
pass
|
|
elif myarg == 'groups':
|
|
printOptions['getGroupFeed'] = True
|
|
printOptions['groupsInColumns'] = False
|
|
elif myarg == 'groupsincolumns':
|
|
printOptions['getGroupFeed'] = True
|
|
printOptions['groupsInColumns'] = True
|
|
elif myarg in {'license', 'licenses', 'licence', 'licences'}:
|
|
printOptions['getLicenseFeed'] = True
|
|
printOptions['getLicenseFeedByUser'] = False
|
|
elif myarg in {'licensebyuser', 'licensesbyuser', 'licencebyuser', 'licencesbyuser'}:
|
|
printOptions['getLicenseFeedByUser'] = True
|
|
printOptions['getLicenseFeed'] = False
|
|
elif myarg in {'onelicenseperrow', 'onelicenceperrow'}:
|
|
oneLicensePerRow = True
|
|
elif myarg in {'products', 'product'}:
|
|
skus = SKU.convertProductListToSKUList(getGoogleProductList())
|
|
elif myarg in {'sku', 'skus'}:
|
|
skus = getGoogleSKUList()
|
|
elif myarg == 'aliasmatchpattern':
|
|
aliasMatchPattern = getREPattern(re.IGNORECASE)
|
|
elif myarg in {'emailpart', 'emailparts', 'username'}:
|
|
printOptions['emailParts'] = True
|
|
elif myarg in {'countonly', 'countsonly'}:
|
|
printOptions['countOnly'] = True
|
|
elif myarg == 'maxresults':
|
|
maxResults = getInteger(minVal=1, maxVal=500)
|
|
elif myarg == 'quoteplusphonenumbers':
|
|
quotePlusPhoneNumbers = True
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
elif myarg == 'showvalidcolumn':
|
|
showValidColumn = 'Valid'
|
|
elif myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
_, _, entityList = getEntityArgument(entityList)
|
|
if printOptions['countOnly']:
|
|
fieldsList = ['primaryEmail']
|
|
domainCounts = {}
|
|
if not FJQC.formatJSON:
|
|
csvPF.SetTitles(['domain', 'count'])
|
|
else:
|
|
csvPF.SetJSONTitles(['JSON'])
|
|
else:
|
|
if FJQC.formatJSON:
|
|
printOptions['sortHeaders'] = False
|
|
titles = ['primaryEmail']
|
|
if showValidColumn:
|
|
titles.append(showValidColumn)
|
|
titles.append('JSON')
|
|
csvPF.SetJSONTitles(titles)
|
|
else:
|
|
if showValidColumn:
|
|
csvPF.AddTitles([showValidColumn])
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if printOptions['getGroupFeed']:
|
|
if not printOptions['groupsInColumns']:
|
|
csvPF.AddTitles(['GroupsCount', 'Groups'])
|
|
else:
|
|
csvPF.AddTitles(['Groups'])
|
|
if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']:
|
|
if not oneLicensePerRow:
|
|
licenseTitles = ['LicensesCount', 'Licenses', 'LicensesDisplay']
|
|
else:
|
|
licenseTitles = ['License', 'LicenseDisplay']
|
|
csvPF.AddTitles(licenseTitles)
|
|
if printOptions['getLicenseFeed']:
|
|
if skus is None and GM.Globals[GM.LICENSE_SKUS]:
|
|
skus = GM.Globals[GM.LICENSE_SKUS]
|
|
licenses = doPrintLicenses(returnFields=['userId', 'skuId'], skus=skus)
|
|
elif printOptions['getLicenseFeedByUser']:
|
|
lic = buildGAPIObject(API.LICENSING)
|
|
if skus is None:
|
|
skus = SKU.getAllSKUs() if not GM.Globals[GM.LICENSE_SKUS] else GM.Globals[GM.LICENSE_SKUS]
|
|
if entityList is None:
|
|
sortRows = False
|
|
if orgUnitPath is not None and fieldsList:
|
|
fieldsList.append('orgUnitPath')
|
|
fields = getItemFieldsFromFieldsList('users', fieldsList)
|
|
itemCount = 0
|
|
for kwargsQuery in makeUserGroupDomainQueryFilters(kwargsDict):
|
|
kwargs = kwargsQuery[0]
|
|
query = kwargsQuery[1]
|
|
query, pquery = userFilters(kwargs, query, orgUnitPath, isSuspended)
|
|
printGettingAllAccountEntities(Ent.USER, pquery)
|
|
pageMessage = getPageMessage(showFirstLastItems=True)
|
|
try:
|
|
feed = yieldGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=pageMessage, messageAttribute='primaryEmail',
|
|
throwReasons=[GAPI.DOMAIN_NOT_FOUND, GAPI.INVALID_ORGUNIT, GAPI.INVALID_INPUT,
|
|
GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN,
|
|
GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR, GAPI.FAILED_PRECONDITION],
|
|
query=query, fields=fields,
|
|
showDeleted=showDeleted, orderBy=orderBy, sortOrder=sortOrder, viewType=viewType,
|
|
projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
|
|
maxResults=maxResults, **kwargs)
|
|
for users in feed:
|
|
if orgUnitPath is None:
|
|
if showItemCountOnly:
|
|
itemCount += len(users)
|
|
elif not printOptions['countOnly']:
|
|
for user in users:
|
|
_printUser(user, 0, 0)
|
|
else:
|
|
for user in users:
|
|
_updateDomainCounts(user['primaryEmail'])
|
|
else:
|
|
if showItemCountOnly:
|
|
for user in users:
|
|
if orgUnitPathLower == user.get('orgUnitPath', '').lower():
|
|
itemCount += 1
|
|
elif not printOptions['countOnly']:
|
|
for user in users:
|
|
if orgUnitPathLower == user.get('orgUnitPath', '').lower():
|
|
_printUser(user, 0, 0)
|
|
else:
|
|
for user in users:
|
|
if orgUnitPathLower == user.get('orgUnitPath', '').lower():
|
|
_updateDomainCounts(user['primaryEmail'])
|
|
except GAPI.domainNotFound:
|
|
entityActionFailedWarning([Ent.USER, None, Ent.DOMAIN, kwargs['domain']], Msg.NOT_FOUND)
|
|
continue
|
|
except (GAPI.invalidOrgunit, GAPI.invalidInput) as e:
|
|
if query and not schemaParms['customFieldMask']:
|
|
entityActionFailedWarning([Ent.USER, None], invalidQuery(query))
|
|
elif schemaParms['customFieldMask'] and not query:
|
|
entityActionFailedWarning([Ent.USER, None], invalidUserSchema(schemaParms['customFieldMask']))
|
|
elif query and schemaParms['customFieldMask']:
|
|
entityActionFailedWarning([Ent.USER, None], f'{invalidQuery(query)} or {invalidUserSchema(schemaParms["customFieldMask"])}')
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, None], str(e))
|
|
continue
|
|
except (GAPI.unknownError, GAPI.failedPrecondition) as e:
|
|
entityActionFailedExit([Ent.USER, None], str(e))
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{itemCount}\n')
|
|
return
|
|
else:
|
|
if showItemCountOnly:
|
|
writeStdout(f'{len(entityList)}\n')
|
|
return
|
|
sortRows = True
|
|
# If no individual fields were specified (allfields, basic, full) or individual fields other than primaryEmail were specified, look up each user
|
|
if isSuspended is not None and fieldsList:
|
|
fieldsList.append('suspended')
|
|
if projectionSet or len(set(fieldsList)) > 1 or showValidColumn:
|
|
jcount = len(entityList)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
if GC.Values[GC.BATCH_SIZE] > 1 and jcount > 1:
|
|
svcargs = dict([('userKey', None), ('fields', fields),
|
|
('projection', schemaParms['projection']), ('customFieldMask', schemaParms['customFieldMask']),
|
|
('viewType', viewType)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(cd.users(), 'get')
|
|
dbatch = cd.new_batch_http_request(callback=_callbackPrintUser)
|
|
bcount = 0
|
|
j = 0
|
|
for userEntity in entityList:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['userKey'] = normalizeEmailAddressOrUID(userEntity)
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID('', 0, 0, j, jcount, svcparms['userKey']))
|
|
bcount += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = cd.new_batch_http_request(callback=_callbackPrintUser)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
else:
|
|
j = 0
|
|
for userEntity in entityList:
|
|
j += 1
|
|
userEmail = normalizeEmailAddressOrUID(userEntity)
|
|
try:
|
|
user = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT, GAPI.RESOURCE_NOT_FOUND, GAPI.RATE_LIMIT_EXCEEDED],
|
|
userKey=userEmail, projection=schemaParms['projection'], customFieldMask=schemaParms['customFieldMask'],
|
|
viewType=viewType, fields=fields)
|
|
_printUser(user, j, jcount)
|
|
except (GAPI.userNotFound, GAPI.resourceNotFound):
|
|
if not showValidColumn:
|
|
entityUnknownWarning(Ent.USER, userEmail, j, jcount)
|
|
else:
|
|
_writeUserEntity({'primaryEmail': userEmail, showValidColumn: False})
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.rateLimitExceeded) as e:
|
|
entityActionFailedWarning([Ent.USER, userEmail], str(e), j, jcount)
|
|
except GAPI.invalidInput as e:
|
|
if schemaParms['customFieldMask']:
|
|
entityActionFailedWarning([Ent.USER, userEmail], invalidUserSchema(schemaParms['customFieldMask']), j, jcount)
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, userEmail], str(e), j, jcount)
|
|
# The only field specified was primaryEmail, just list the users/count the domains
|
|
elif not printOptions['countOnly']:
|
|
for userEntity in entityList:
|
|
_printUser({'primaryEmail': normalizeEmailAddressOrUID(userEntity)}, 0, 0)
|
|
else:
|
|
for userEntity in entityList:
|
|
_updateDomainCounts(normalizeEmailAddressOrUID(userEntity))
|
|
if not printOptions['countOnly']:
|
|
if not FJQC.formatJSON:
|
|
if printOptions['sortHeaders']:
|
|
sortTitles = ['primaryEmail']
|
|
if printOptions['scalarsFirst']:
|
|
sortTitles.extend([f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{field}' for field in USER_NAME_PROPERTY_PRINT_ORDER]+sorted(USER_LANGUAGE_PROPERTY_PRINT_ORDER+USER_SCALAR_PROPERTY_PRINT_ORDER))
|
|
csvPF.SetSortTitles(sortTitles)
|
|
csvPF.SortTitles()
|
|
csvPF.SetSortTitles([])
|
|
if printOptions['getGroupFeed']:
|
|
if not printOptions['groupsInColumns']:
|
|
csvPF.MoveTitlesToEnd(['GroupsCount', 'Groups'])
|
|
else:
|
|
csvPF.MoveTitlesToEnd(['Groups']+[f'Groups{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}' for j in range(printOptions['maxGroups'])])
|
|
if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']:
|
|
csvPF.MoveTitlesToEnd(licenseTitles)
|
|
if sortRows and orderBy:
|
|
orderBy = 'primaryEmail' if orderBy == 'email' else f'name.{orderBy}'
|
|
csvPF.SortRows(orderBy, reverse=sortOrder == 'DESCENDING')
|
|
else:
|
|
if sortRows and orderBy == 'email':
|
|
csvPF.SortRows('primaryEmail', reverse=sortOrder == 'DESCENDING')
|
|
elif not FJQC.formatJSON:
|
|
for domain, count in sorted(iter(domainCounts.items())):
|
|
csvPF.WriteRowNoFilter({'domain': domain, 'count': count})
|
|
else:
|
|
csvPF.WriteRowNoFilter({'JSON': json.dumps(cleanJSON(domainCounts), ensure_ascii=False, sort_keys=True)})
|
|
if printOptions['countOnly']:
|
|
csvPF.SetIndexedTitles([])
|
|
csvPF.writeCSVfile('Users' if not printOptions['countOnly'] else 'User Domain Counts')
|
|
|
|
# gam <UserTypeEntity> print users
|
|
def doPrintUserEntity(entityList):
|
|
if not Cmd.ArgumentsRemaining():
|
|
writeEntityNoHeaderCSVFile(Ent.USER, entityList)
|
|
else:
|
|
doPrintUsers(entityList)
|
|
|
|
# gam <UserTypeEntity> print userlist [todrive <ToDriveAttribute>*]
|
|
# [title <String>]
|
|
# [delimiter <Character>] [formatjson] [quotechar <Character>]
|
|
def doPrintUserList(entityList):
|
|
csvPF = CSVPrintFile(['title', 'count', 'users'])
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
title = 'Users'
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'title':
|
|
title = getString(Cmd.OB_STRING)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
_, count, entityList = getEntityArgument(entityList)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRow({'title': title, 'count': count, 'users': delimiter.join(entityList)})
|
|
else:
|
|
csvPF.WriteRow({'title': title, 'count': count, 'users': json.dumps(cleanJSON(entityList), ensure_ascii=False, sort_keys=True)})
|
|
csvPF.writeCSVfile('User List')
|
|
|
|
# gam print usercountsbyorgunit [todrive <ToDriveAttribute>*]
|
|
# [domain <String>]
|
|
def doPrintUserCountsByOrgUnit():
|
|
def _printUserCounts(title, v):
|
|
csvPF.WriteRow({'orgUnitPath': title, 'archived': v['archived'], 'active': v['active'], 'suspended': v['suspended'], 'total': v['total']})
|
|
|
|
USER_COUNTS_FIELDS = ['archived', 'active', 'suspended', 'total']
|
|
USER_COUNTS_ZERO_FIELDS = {'archived': 0, 'active': 0, 'suspended': 0, 'total': 0}
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['orgUnitPath']+USER_COUNTS_FIELDS)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'domain':
|
|
kwargs = {'domain': getString(Cmd.OB_DOMAIN_NAME)}
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if 'domain' in kwargs:
|
|
printGettingAllEntityItemsForWhom(Ent.USER, kwargs['domain'], entityType=Ent.DOMAIN)
|
|
pageMessage = getPageMessageForWhom()
|
|
title = f"Total({kwargs['domain']})"
|
|
else:
|
|
printGettingAllAccountEntities(Ent.USER)
|
|
pageMessage = getPageMessage()
|
|
title = f"Total({kwargs['customer']})"
|
|
userCounts = {}
|
|
try:
|
|
result = callGAPIpages(cd.users(), 'list', 'users',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND, GAPI.FORBIDDEN],
|
|
orderBy='email', fields='nextPageToken,users(orgUnitPath,archived,suspended)',
|
|
maxResults=GC.Values[GC.USER_MAX_RESULTS], **kwargs)
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.domainNotFound):
|
|
if 'domain' in kwargs:
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.DOMAIN, kwargs['domain'])
|
|
else:
|
|
checkEntityDNEorAccessErrorExit(cd, Ent.CUSTOMER_ID, kwargs['customer'])
|
|
return
|
|
for user in result:
|
|
orgUnitPath = user['orgUnitPath']
|
|
if orgUnitPath not in userCounts:
|
|
userCounts[orgUnitPath] = USER_COUNTS_ZERO_FIELDS.copy()
|
|
if user['suspended'] or user['archived']:
|
|
if user['archived']:
|
|
userCounts[orgUnitPath]['archived'] += 1
|
|
if user['suspended']:
|
|
userCounts[orgUnitPath]['suspended'] += 1
|
|
else:
|
|
userCounts[orgUnitPath]['active'] += 1
|
|
userCounts[orgUnitPath]['total'] += 1
|
|
totalCounts = USER_COUNTS_ZERO_FIELDS.copy()
|
|
for k, v in sorted(iter(userCounts.items())):
|
|
_printUserCounts(k, v)
|
|
for f in USER_COUNTS_FIELDS:
|
|
totalCounts[f] += v[f]
|
|
_printUserCounts(title, totalCounts)
|
|
csvPF.writeCSVfile('User Counts by OrgUnit')
|
|
|
|
def isolateCIUserInvitatonsEmail(name):
|
|
''' converts long name into email address'''
|
|
return name.split('/')[-1]
|
|
|
|
def quotedCIUserInvitatonsEmail(customer, email):
|
|
return f"{customer}/userinvitations/{quote_plus(email, safe='@')}"
|
|
|
|
def _getCIUserInvitationsEntity(ci=None, email=None):
|
|
if ci is None:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_USERINVITATIONS)
|
|
customer = _getCustomersCustomerIdWithC()
|
|
if email is None:
|
|
email = getString(Cmd.OB_EMAIL_ADDRESS)
|
|
pattern = re.compile(rf'^{customer}/userinvitations/(.+)$')
|
|
mg = pattern.match(email)
|
|
if mg:
|
|
email = mg.group(1)
|
|
else:
|
|
email = normalizeEmailAddressOrUID(email, noUid=True)
|
|
return (quotedCIUserInvitatonsEmail(customer, email), email, ci)
|
|
|
|
def _getIsInvitableUser(ci, email):
|
|
name, _, ci = _getCIUserInvitationsEntity(ci, email)
|
|
try:
|
|
result = callGAPI(ci.customers().userinvitations(), 'isInvitableUser',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name)
|
|
return (result['isInvitableUser'], ci)
|
|
except GAPI.notFound:
|
|
return (False, ci)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied):
|
|
return (False, ci)
|
|
|
|
# gam send userinvitation <EmailAddress>
|
|
# gam cancel userinvitation <EmailAddress>
|
|
def doCIUserInvitationsAction():
|
|
name, user, ci = _getCIUserInvitationsEntity()
|
|
checkForExtraneousArguments()
|
|
if Act.Get() == Act.CANCEL:
|
|
action = 'cancel'
|
|
else:
|
|
Act.Set(Act.SEND)
|
|
action = 'send'
|
|
entityPerformAction([Ent.USER_INVITATION, user])
|
|
try:
|
|
result = callGAPI(ci.customers().userinvitations(), action,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name)
|
|
name = result.get('response', {}).get('name')
|
|
if name:
|
|
result['response']['name'] = isolateCIUserInvitatonsEmail(name)
|
|
Ind.Increment()
|
|
showJSON(None, result)
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.USER_INVITATION, f'{user}')
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER_INVITATION, f'{user}'], str(e))
|
|
|
|
CI_USERINVITATION_TIME_OBJECTS = {'updateTime'}
|
|
|
|
def _showUserInvitation(invitation, FJQC, i=0, count=0):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
invitation['email'] = isolateCIUserInvitatonsEmail(invitation['name'])
|
|
printLine(json.dumps(cleanJSON(invitation, timeObjects=CI_USERINVITATION_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.USER_INVITATION, isolateCIUserInvitatonsEmail(invitation['name'])], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, invitation, timeObjects=CI_USERINVITATION_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
# gam check userinvitation|isinvitable <EmailAddress>
|
|
def doCheckCIUserInvitations():
|
|
name, user, ci = _getCIUserInvitationsEntity()
|
|
checkForExtraneousArguments()
|
|
try:
|
|
result = callGAPI(ci.customers().userinvitations(), 'isInvitableUser',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name)
|
|
printEntity([Ent.USER_INVITATION, user])
|
|
Ind.Increment()
|
|
showJSON(None, result)
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.USER_INVITATION, f'{user}')
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER_INVITATION, f'{user}'], str(e))
|
|
|
|
def infoCIUserInvitations(name, user, ci, FJQC):
|
|
try:
|
|
invitation = callGAPI(ci.customers().userinvitations(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name)
|
|
_showUserInvitation(invitation, FJQC)
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.USER_INVITATION, f'{user}')
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER_INVITATION, f'{user}'], str(e))
|
|
|
|
# gam info userinvitation <EmailAddress> [formatjson]
|
|
def doInfoCIUserInvitations():
|
|
name, user, ci = _getCIUserInvitationsEntity()
|
|
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
|
|
infoCIUserInvitations(name, user, ci, FJQC)
|
|
|
|
CI_USERINVITATION_ORDERBY_CHOICE_MAP = {
|
|
'email': 'email',
|
|
'updatetime': 'update_time',
|
|
}
|
|
|
|
CI_USERINVITATION_STATE_CHOICE_MAP = {
|
|
'accepted': 'ACCEPTED',
|
|
'declined': 'DECLINED',
|
|
'invited': 'INVITED',
|
|
'notyetsent': 'NOT_YET_SENT',
|
|
}
|
|
|
|
# gam show userinvitations
|
|
# [state notyetsent|invited|accepted|declined]
|
|
# [orderby email|updatetime [ascending|descending]]
|
|
# [formatjson]
|
|
# gam print userinvitations [todrive <ToDriveAttribute>*]
|
|
# [state notyetsent|invited|accepted|declined]
|
|
# [orderby email|updatetime [ascending|descending]]
|
|
# [[formatjson [quotechar <Character>]]
|
|
def doPrintShowCIUserInvitations():
|
|
def _printUserInvitation(invitation):
|
|
invitation['email'] = isolateCIUserInvitatonsEmail(invitation['name'])
|
|
row = flattenJSON(invitation, timeObjects=CI_USERINVITATION_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'email': invitation['email'],
|
|
'JSON': json.dumps(cleanJSON(invitation, timeObjects=CI_USERINVITATION_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_USERINVITATIONS)
|
|
customer = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile(['email']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
OBY = OrderBy(CI_USERINVITATION_ORDERBY_CHOICE_MAP)
|
|
ifilter = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'state':
|
|
state = getChoice(CI_USERINVITATION_STATE_CHOICE_MAP, mapChoice=True)
|
|
ifilter = f"state=='{state}'"
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
printGettingAllAccountEntities(Ent.USER_INVITATION, ifilter)
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
invitations = callGAPIpages(ci.customers().userinvitations(), 'list', 'userInvitations',
|
|
throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
pageMessage=pageMessage,
|
|
parent=customer, filter=ifilter, orderBy=OBY.orderBy)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER_INVITATION, None], str(e))
|
|
return
|
|
if not csvPF:
|
|
jcount = len(invitations)
|
|
performActionNumItems(jcount, Ent.USER_INVITATION)
|
|
Ind.Increment()
|
|
j = 0
|
|
for invitation in invitations:
|
|
j += 1
|
|
_showUserInvitation(invitation, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for invitation in invitations:
|
|
_printUserInvitation(invitation)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('User Invitations')
|
|
|
|
# gam <UserTypeEntity> check isinvitable [todrive <ToDriveAttribute>*]
|
|
# /batch is broken for Cloud Identity. Once fixed move this to using batch.
|
|
# Current serial implementation will be SLOW...
|
|
def checkCIUserIsInvitable(users):
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_USERINVITATIONS)
|
|
customer = _getCustomersCustomerIdWithC()
|
|
csvPF = CSVPrintFile(['invitableUsers'])
|
|
getTodriveOnly(csvPF)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = convertUIDtoEmailAddress(user)
|
|
name = quotedCIUserInvitatonsEmail(customer, user)
|
|
try:
|
|
result = callGAPI(ci.customers().userinvitations(), 'isInvitableUser',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.PERMISSION_DENIED],
|
|
name=name)
|
|
if result.get('isInvitableUser'):
|
|
csvPF.WriteRow({'invitableUsers': user})
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.USER_INVITATION, f'{user}', i, count)
|
|
except (GAPI.invalid, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER_INVITATION, user], str(e), i, count)
|
|
return
|
|
csvPF.writeCSVfile('Invitable Users')
|
|
|
|
INBOUNDSSO_MODE_CHOICE_MAP = {
|
|
'ssooff': 'SSO_OFF',
|
|
'samlsso': 'SAML_SSO',
|
|
'domainwidesamlifenabled': 'DOMAIN_WIDE_SAML_IF_ENABLED'
|
|
}
|
|
|
|
def getCIOrgunitID(cd, orgunit):
|
|
ou_id = getOrgUnitId(cd, orgunit)[1]
|
|
if ou_id.startswith('id:'):
|
|
ou_id = ou_id[3:]
|
|
return f'orgUnits/{ou_id}'
|
|
|
|
def _getInboundSSOProfiles(ci):
|
|
customer = normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])
|
|
try:
|
|
return callGAPIpages(ci.inboundSamlSsoProfiles(), 'list', 'inboundSamlSsoProfiles',
|
|
throwReasons=GAPI.CISSO_LIST_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
filter=f'customer=="{customer}"')
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.INBOUND_SSO_PROFILE, customer], str(e))
|
|
return []
|
|
|
|
def _convertInboundSSOProfileDisplaynameToName(ci=None, displayName=''):
|
|
if displayName.lower().startswith('id:') or displayName.lower().startswith('uid:'):
|
|
displayName = displayName.split(':', 1)[1]
|
|
if not displayName.startswith('inboundSamlSsoProfiles/'):
|
|
displayName = f'inboundSamlSsoProfiles/{displayName}'
|
|
return displayName
|
|
if not ci:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
profiles = _getInboundSSOProfiles(ci)
|
|
matches = []
|
|
for profile in profiles:
|
|
if displayName.lower() == profile.get('displayName', '').lower():
|
|
matches.append(profile)
|
|
if len(matches) == 1:
|
|
return matches[0]['name']
|
|
if len(matches) == 0:
|
|
usageErrorExit(Msg.NO_SSO_PROFILE_MATCHES.format(displayName))
|
|
errMsg = Msg.MULTIPLE_SSO_PROFILES_MATCH.format(displayName)
|
|
for m in matches:
|
|
errMsg += f' {m["name"]} {m["displayName"]}\n'
|
|
usageErrorExit(errMsg)
|
|
|
|
def _getInboundSSOProfileArguments(body):
|
|
returnNameOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
body['displayName'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'entityid':
|
|
body.setdefault('idpConfig', {})['entityId'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'loginurl':
|
|
body.setdefault('idpConfig', {})['singleSignOnServiceUri'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'logouturl':
|
|
body.setdefault('idpConfig', {})['logoutRedirectUri'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'changepasswordurl':
|
|
body.setdefault('idpConfig', {})['changePasswordUri'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'returnnameonly':
|
|
returnNameOnly = True
|
|
else:
|
|
unknownArgumentExit()
|
|
return (returnNameOnly, body)
|
|
|
|
def _showInboundSSOProfile(profile, FJQC, i=0, count=0):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(profile), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.INBOUND_SSO_PROFILE, profile['name']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, profile)
|
|
Ind.Decrement()
|
|
|
|
def _processInboundSSOProfileResult(result, returnNameOnly, kvlist, function):
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
writeStderr(f'inboundSSOProfileResult: {result}\n')
|
|
if result.get('done', False):
|
|
if 'error' not in result:
|
|
if 'response' in result:
|
|
if not returnNameOnly:
|
|
_showInboundSSOProfile(result['response'], None)
|
|
else:
|
|
writeStdout(f'{result["response"]["name"]}\n')
|
|
else:
|
|
entityActionPerformed(kvlist)
|
|
else:
|
|
entityActionFailedWarning(kvlist, result['error']['message'])
|
|
elif not returnNameOnly:
|
|
entityActionPerformedMessage(kvlist, Msg.ACTION_IN_PROGRESS.format(f'{function} inboundssoprofile'))
|
|
else:
|
|
writeStdout('inProgress\n')
|
|
|
|
# gam create inboundssoprofile [name <SSOProfileName>]
|
|
# [entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
|
|
# [returnnameonly]
|
|
def doCreateInboundSSOProfile():
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
body = {'customer': normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID]),
|
|
'displayName': 'SSO Profile'
|
|
}
|
|
returnNameOnly, body = _getInboundSSOProfileArguments(body)
|
|
kvlist = [Ent.INBOUND_SSO_PROFILE, body['displayName']]
|
|
try:
|
|
result = callGAPI(ci.inboundSamlSsoProfiles(), 'create',
|
|
throwReasons=GAPI.CISSO_CREATE_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
body=body)
|
|
_processInboundSSOProfileResult(result, returnNameOnly, kvlist, 'create')
|
|
except (GAPI.failedPrecondition, GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
|
|
# gam update inboundssoprofile <SSOProfileItem>
|
|
# [entityid <String>] [loginurl <URL>] [logouturl <URL>] [changepasswordurl <URL>]
|
|
# [returnnameonly]
|
|
def doUpdateInboundSSOProfile():
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
name = _convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))
|
|
returnNameOnly, body = _getInboundSSOProfileArguments({})
|
|
kvlist = [Ent.INBOUND_SSO_PROFILE, name]
|
|
try:
|
|
result = callGAPI(ci.inboundSamlSsoProfiles(), 'patch',
|
|
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
name=name, updateMask=','.join(body.keys()), body=body)
|
|
_processInboundSSOProfileResult(result, returnNameOnly, kvlist, 'update')
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
|
|
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
|
|
# gam delete inboundssoprofile <SSOProfileItem>
|
|
def doDeleteInboundSSOProfile():
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
name = _convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))
|
|
checkForExtraneousArguments()
|
|
kvlist = [Ent.INBOUND_SSO_PROFILE, name]
|
|
try:
|
|
result = callGAPI(ci.inboundSamlSsoProfiles(), 'delete',
|
|
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
name=name)
|
|
_processInboundSSOProfileResult(result, True, kvlist, 'delete')
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
|
|
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
|
|
def _getInboundSSOProfile(ci, name):
|
|
kvlist = [Ent.INBOUND_SSO_PROFILE, name]
|
|
try:
|
|
return callGAPI(ci.inboundSamlSsoProfiles(), 'get',
|
|
throwReasons=GAPI.CISSO_GET_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
name=name)
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
return None
|
|
|
|
# gam info inboundssoprofile <SSOProfileItem> [formatjson]
|
|
def doInfoInboundSSOProfile():
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
name = _convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))
|
|
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
|
|
profile = _getInboundSSOProfile(ci, name)
|
|
if profile:
|
|
_showInboundSSOProfile(profile, FJQC)
|
|
|
|
# gam show inboundssoprofile
|
|
# [formatjson]
|
|
# gam print inboundssoprofile [todrive <ToDriveAttribute>*]
|
|
# [[formatjson [quotechar <Character>]]
|
|
def doPrintShowInboundSSOProfiles():
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
customer = normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])
|
|
csvPF = CSVPrintFile(['name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
cfilter = f'customer=="{customer}"'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF:
|
|
printGettingAllAccountEntities(Ent.INBOUND_SSO_PROFILE, cfilter)
|
|
profiles = _getInboundSSOProfiles(ci)
|
|
if not csvPF:
|
|
count = len(profiles)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(count, Ent.INBOUND_SSO_PROFILE)
|
|
Ind.Increment()
|
|
i = 0
|
|
for profile in profiles:
|
|
i += 1
|
|
_showInboundSSOProfile(profile, FJQC, i, count)
|
|
Ind.Decrement()
|
|
else:
|
|
for profile in profiles:
|
|
row = flattenJSON(profile)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'name': profile['name'],
|
|
'JSON': json.dumps(cleanJSON(profile),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Inbound SSO Profiles')
|
|
|
|
def getInboundSSOProfileCredentials(ci, profile):
|
|
try:
|
|
return callGAPIpages(ci.inboundSamlSsoProfiles().idpCredentials(), 'list', 'idpCredentials',
|
|
throwReasons=GAPI.CISSO_LIST_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
parent=profile)
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.INBOUND_SSO_PROFILE, profile], str(e))
|
|
return None
|
|
|
|
def getInboundSSOCredentialsName():
|
|
name = getString(Cmd.OB_STRING)
|
|
if name.startswith('id:') or name.startswith('uid:'):
|
|
name = name.split(':', 1)[1]
|
|
return name
|
|
|
|
INBOUNDSSO_CREDENTIALS_TIME_OBJECTS = ['updateTime']
|
|
|
|
def _showInboundSSOCredentials(credentials, FJQC, i=0, count=0):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(credentials, timeObjects=INBOUNDSSO_CREDENTIALS_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.INBOUND_SSO_CREDENTIALS, credentials['name']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, credentials, timeObjects=INBOUNDSSO_CREDENTIALS_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
def _processInboundSSOCredentialsResult(result, kvlist, function):
|
|
if result.get('done', False):
|
|
if 'error' not in result:
|
|
if 'response' in result:
|
|
_showInboundSSOCredentials(result['response'], None)
|
|
else:
|
|
entityActionPerformed(kvlist)
|
|
else:
|
|
entityActionFailedWarning(kvlist, result['error']['message'])
|
|
else:
|
|
entityActionPerformedMessage(kvlist, Msg.ACTION_IN_PROGRESS.format(f'{function} inboundssocredentials'))
|
|
|
|
# gam create inboundssocredentials profile <SSOProfileItem>
|
|
# (pemfile <FileName>)|(generatekey [keysize 1024|2048|4096]) [replaceolddest]
|
|
def doCreateInboundSSOCredential():
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
profile = None
|
|
generateKey = replaceOldest = False
|
|
keySize = 2048
|
|
pemData = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'profile':
|
|
profile = _convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))
|
|
elif myarg == 'pemfile':
|
|
pemData = readFile(getString(Cmd.OB_FILE_NAME))
|
|
elif myarg == 'generatekey':
|
|
generateKey = True
|
|
elif myarg == 'replaceoldest':
|
|
replaceOldest = True
|
|
elif myarg == 'keysize':
|
|
keySize=int(getChoice([1024, 2048, 4096]))
|
|
else:
|
|
unknownArgumentExit()
|
|
if not profile:
|
|
missingArgumentExit('profile')
|
|
if not pemData and not generateKey:
|
|
missingArgumentExit('pemfile|generatekey')
|
|
if pemData and generateKey:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('pemfile', 'generatekey'))
|
|
if replaceOldest:
|
|
credentials = getInboundSSOProfileCredentials(ci, profile)
|
|
if credentials is None:
|
|
return
|
|
count = len(credentials)
|
|
if count == 2:
|
|
oldest_key = min(credentials, key=lambda x:x['updateTime'])
|
|
action = Act.Get()
|
|
Act.Set(Act.DELETE)
|
|
doDeleteInboundSSOCredential(ci=ci, name=oldest_key['name'])
|
|
Act.Set(action)
|
|
else:
|
|
writeStdout(Msg.NO_CREDENTIALS_REPLACEMENT.format(Ent.Singular(Ent.INBOUND_SSO_PROFILE), profile,
|
|
count, Ent.Choose(Ent.INBOUND_SSO_CREDENTIALS, count)))
|
|
if generateKey:
|
|
privKey, pemData = _generatePrivateKeyAndPublicCert('', '', 'GAM', keySize, b64enc_pub=False)
|
|
timestamp = datetime.datetime.now(GC.Values[GC.TIMEZONE]).strftime('%Y%m%d-%I%M%S')
|
|
priv_file = f'privatekey-{timestamp}.pem'
|
|
writeFile(priv_file, privKey)
|
|
writeStdout(Msg.WROTE_PRIVATE_KEY_DATA.format(priv_file))
|
|
pub_file = f'publiccert-{timestamp}.pem'
|
|
writeFile(pub_file, pemData)
|
|
writeStdout(Msg.WROTE_PUBLIC_CERTIFICATE.format(pub_file))
|
|
kvlist = [Ent.INBOUND_SSO_CREDENTIALS, profile]
|
|
try:
|
|
result = callGAPI(ci.inboundSamlSsoProfiles().idpCredentials(), 'add',
|
|
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
parent=profile, body={'pemData': pemData})
|
|
_processInboundSSOCredentialsResult(result, kvlist, 'create')
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.INBOUND_SSO_PROFILE, profile], str(e))
|
|
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
|
|
# gam delete inboundssocredential <SSOCredentialsName>
|
|
def doDeleteInboundSSOCredential(ci=None, name=None):
|
|
if not ci:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
if not name:
|
|
name = getInboundSSOCredentialsName()
|
|
checkForExtraneousArguments()
|
|
kvlist = [Ent.INBOUND_SSO_CREDENTIALS, name]
|
|
try:
|
|
result = callGAPI(ci.inboundSamlSsoProfiles().idpCredentials(), 'delete',
|
|
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
name=name)
|
|
_processInboundSSOCredentialsResult(result, kvlist, 'delete')
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
|
|
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
|
|
# gam info inboundssocredential <SSOCredentialsName> [formatjson]
|
|
def doInfoInboundSSOCredential():
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
name = getInboundSSOCredentialsName()
|
|
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
|
|
kvlist = [Ent.INBOUND_SSO_CREDENTIALS, name]
|
|
try:
|
|
credentials = callGAPI(ci.inboundSamlSsoProfiles().idpCredentials(), 'get',
|
|
throwReasons=GAPI.CISSO_GET_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
name=name)
|
|
_showInboundSSOCredentials(credentials, FJQC)
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
|
|
# gam show inboundssocredentials [profile|profiles <SSOProfileItemList>]
|
|
# [formatjson]
|
|
# gam print inboundssocredentials [profile|profiles <SSOProfileItemList>]
|
|
# [[formatjson [quotechar <Character>]]
|
|
def doPrintShowInboundSSOCredentials():
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
csvPF = CSVPrintFile(['name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
profiles = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'profile', 'profiles'}:
|
|
profiles = [_convertInboundSSOProfileDisplaynameToName(ci, profile) for profile in getString(Cmd.OB_STRING_LIST).split(',')]
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if not profiles:
|
|
profiles = [p['name'] for p in _getInboundSSOProfiles(ci)]
|
|
count = len(profiles)
|
|
i = 0
|
|
for profile in profiles:
|
|
i += 1
|
|
credentials = getInboundSSOProfileCredentials(ci, profile)
|
|
if credentials is None:
|
|
continue
|
|
if not csvPF:
|
|
jcount = len(credentials)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.INBOUND_SSO_PROFILE, profile], jcount, Ent.INBOUND_SSO_CREDENTIALS, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for credential in credentials:
|
|
j += 1
|
|
_showInboundSSOCredentials(credential, FJQC, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for credential in credentials:
|
|
row = flattenJSON(credential, timeObjects=INBOUNDSSO_CREDENTIALS_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'name': credential['name'],
|
|
'JSON': json.dumps(cleanJSON(credential, timeObjects=INBOUNDSSO_CREDENTIALS_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Inbound SSO Credentials')
|
|
|
|
def _getInboundSSOAssignment(ci, name):
|
|
kvlist = [Ent.INBOUND_SSO_ASSIGNMENT, name]
|
|
try:
|
|
return callGAPI(ci.inboundSsoAssignments(), 'get',
|
|
throwReasons=GAPI.CISSO_GET_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
name=name)
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
return None
|
|
|
|
def _getInboundSSOAssignments(ci):
|
|
customer = normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])
|
|
try:
|
|
return callGAPIpages(ci.inboundSsoAssignments(), 'list', 'inboundSsoAssignments',
|
|
throwReasons=GAPI.CISSO_LIST_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
filter=f'customer=="{customer}"')
|
|
except (GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalid,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.INBOUND_SSO_ASSIGNMENT, customer], str(e))
|
|
return None
|
|
|
|
def _getInboundSSOAssignmentName():
|
|
name = getString(Cmd.OB_STRING)
|
|
if name.startswith('id:') or name.startswith('uid:'):
|
|
name = name.split(':', 1)[1]
|
|
if not name.startswith('inboundSsoAssignments/'):
|
|
name = f'inboundSsoAssignments/{name}'
|
|
return name
|
|
|
|
def _getInboundSSOAssignmentByTarget(ci, cd, target):
|
|
targetType = 'name'
|
|
if target.startswith('id:') or target.startswith('uid:'):
|
|
target = target.split(':', 1)[1]
|
|
elif re.match(r'^groups/[^/]+$', target):
|
|
targetType = 'targetGroup'
|
|
elif re.match(r'^orgUnits/[^/]+$', target):
|
|
targetType = 'targetOrgUnit'
|
|
elif target.lower().startswith('group:'):
|
|
targetType = 'targetGroup'
|
|
_, target, _ = convertGroupEmailToCloudID(ci, target[6:])
|
|
elif target.lower().startswith('orgunit:'):
|
|
targetType = 'targetOrgUnit'
|
|
target = getCIOrgunitID(cd, target[8:])
|
|
elif not target.startswith('inboundSsoAssignments/'):
|
|
target = f'inboundSsoAssignments/{target}'
|
|
if targetType == 'name':
|
|
return _getInboundSSOAssignment(ci, target)
|
|
assignments = _getInboundSSOAssignments(ci)
|
|
if assignments is not None:
|
|
for assignment in assignments:
|
|
if targetType in assignment and assignment[targetType] == target:
|
|
return assignment
|
|
usageErrorExit(Msg.NO_SSO_PROFILE_ASSIGNED.format(targetType, target))
|
|
|
|
def _getInboundSSOAssignmentArguments(ci, cd, body):
|
|
rank = 0
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'rank':
|
|
rank = getInteger(minVal=1)
|
|
elif myarg == 'mode':
|
|
body['ssoMode'] = getChoice(INBOUNDSSO_MODE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'profile':
|
|
body['samlSsoInfo'] = {'inboundSamlSsoProfile':
|
|
_convertInboundSSOProfileDisplaynameToName(ci, getString(Cmd.OB_STRING))}
|
|
elif myarg == 'neverredirect':
|
|
body['signInBehavior'] = {'redirectCondition': 'NEVER'}
|
|
elif myarg == 'group':
|
|
_, body['targetGroup'], _ = convertGroupEmailToCloudID(ci, getString(Cmd.OB_STRING))
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
body['targetOrgUnit'] = getCIOrgunitID(cd, getString(Cmd.OB_ORGUNIT_ITEM))
|
|
else:
|
|
unknownArgumentExit()
|
|
if 'ssoMode' not in body:
|
|
missingArgumentExit('mode')
|
|
if body['ssoMode'] == 'SAML_SSO' and 'samlSsoInfo' not in body:
|
|
missingArgumentExit('profile')
|
|
if 'targetGroup' in body:
|
|
if 'targetOrgUnit' in body:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('group', 'ou|org|orgunit'))
|
|
if not rank:
|
|
missingArgumentExit('rank')
|
|
body['rank'] = rank
|
|
return body
|
|
|
|
def _updateInboundAssignmentTargetNames(ci, cd, assignment):
|
|
if 'targetGroup' in assignment:
|
|
_, _, assignment['targetGroupEmail'] = convertGroupCloudIDToEmail(ci, assignment['targetGroup'])
|
|
elif 'targetOrgUnit' in assignment:
|
|
assignment['targetOrgUnitPath'] = convertOrgUnitIDtoPath(cd, assignment['targetOrgUnit'])
|
|
|
|
def _showInboundSSOAssignment(assignment, FJQC, ci, cd, i=0, count=0):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(assignment), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.INBOUND_SSO_ASSIGNMENT, assignment['name']], i, count)
|
|
_updateInboundAssignmentTargetNames(ci, cd, assignment)
|
|
Ind.Increment()
|
|
showJSON(None, assignment)
|
|
Ind.Decrement()
|
|
|
|
def _processInboundSSOAssignmentResult(result, kvlist, ci, cd, function):
|
|
if result['done']:
|
|
if 'error' not in result:
|
|
if 'response' in result:
|
|
_showInboundSSOAssignment(result['response'], None, ci, cd)
|
|
else:
|
|
entityActionPerformed(kvlist)
|
|
else:
|
|
entityActionFailedWarning(kvlist, result['error']['message'])
|
|
else:
|
|
entityActionPerformedMessage(kvlist, Msg.ACTION_IN_PROGRESS.format(f'{function} inboundssoassignment'))
|
|
|
|
# gam create inboundssoassignment (group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)
|
|
# (mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled) [neverredirect]
|
|
def doCreateInboundSSOAssignment():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
body = {'customer': normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])}
|
|
body = _getInboundSSOAssignmentArguments(ci, cd, body)
|
|
kvlist = [Ent.INBOUND_SSO_ASSIGNMENT, body['customer']]
|
|
try:
|
|
result = callGAPI(ci.inboundSsoAssignments(), 'create',
|
|
throwReasons=GAPI.CISSO_CREATE_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
body=body)
|
|
_processInboundSSOAssignmentResult(result, kvlist, ci, cd, 'create')
|
|
except (GAPI.failedPrecondition, GAPI.notFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
|
|
# gam update inboundssoassignment [(group <GroupItem> rank <Number>)|(ou|org|orgunit <OrgUnitItem>)]
|
|
# [(mode sso_off)|(mode saml_sso profile <SSOProfileItem>)(mode domain_wide_saml_if_enabled)] [neverredirect]
|
|
def doUpdateInboundSSOAssignment():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
name = _getInboundSSOAssignmentName()
|
|
body = _getInboundSSOAssignmentArguments(ci,cd, {})
|
|
kvlist = [Ent.INBOUND_SSO_ASSIGNMENT, name]
|
|
try:
|
|
result = callGAPI(ci.inboundSsoAssignments(), 'patch',
|
|
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
name=name, updateMask=','.join(list(body.keys())), body=body)
|
|
_processInboundSSOAssignmentResult(result, kvlist, ci, cd, 'update')
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
|
|
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
|
|
# gam delete inboundssoassignment <SSOAssignmentSelector>
|
|
def doDeleteInboundSSOAssignment():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
target = getString(Cmd.OB_STRING)
|
|
assignment = _getInboundSSOAssignmentByTarget(ci, cd, target)
|
|
if assignment is None:
|
|
return
|
|
name = assignment['name']
|
|
checkForExtraneousArguments()
|
|
kvlist = [Ent.INBOUND_SSO_ASSIGNMENT, name]
|
|
try:
|
|
result = callGAPI(ci.inboundSsoAssignments(), 'delete',
|
|
throwReasons=GAPI.CISSO_UPDATE_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
bailOnInternalError=True,
|
|
name=name)
|
|
_processInboundSSOAssignmentResult(result, kvlist, None, None, 'delete')
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning(kvlist, Msg.DOES_NOT_EXIST)
|
|
except (GAPI.failedPrecondition, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.invalid, GAPI.invalidInput, GAPI.invalidArgument,
|
|
GAPI.systemError, GAPI.permissionDenied, GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning(kvlist, str(e))
|
|
|
|
# gam info inboundssoassignment <SSOAssignmentSelector> [formatjson]
|
|
def doInfoInboundSSOAssignment():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
target = getString(Cmd.OB_STRING)
|
|
FJQC = FormatJSONQuoteChar(formatJSONOnly=True)
|
|
assignment = _getInboundSSOAssignmentByTarget(ci, cd, target)
|
|
if assignment is None:
|
|
return
|
|
name = assignment.get('samlSsoInfo', {}).get('inboundSamlSsoProfile')
|
|
if name:
|
|
profile = _getInboundSSOProfile(ci, name)
|
|
if profile:
|
|
assignment['samlSsoInfo']['inboundSamlSsoProfile'] = profile
|
|
_showInboundSSOAssignment(assignment, FJQC, ci, cd)
|
|
|
|
# gam show inboundssoassignment
|
|
# [formatjson]
|
|
# gam print inboundssoassignment [todrive <ToDriveAttribute>*]
|
|
# [[formatjson [quotechar <Character>]]
|
|
def doPrintShowInboundSSOAssignments():
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_INBOUND_SSO)
|
|
customer = normalizeChannelCustomerID(GC.Values[GC.CUSTOMER_ID])
|
|
csvPF = CSVPrintFile(['name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
cfilter = f'customer=="{customer}"'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF:
|
|
printGettingAllAccountEntities(Ent.INBOUND_SSO_ASSIGNMENT, cfilter)
|
|
assignments = _getInboundSSOAssignments(ci)
|
|
if assignments is None:
|
|
return
|
|
if not csvPF:
|
|
count = len(assignments)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(count, Ent.INBOUND_SSO_ASSIGNMENT)
|
|
Ind.Increment()
|
|
i = 0
|
|
for assignment in assignments:
|
|
i += 1
|
|
_showInboundSSOAssignment(assignment, FJQC, ci, cd, i, count)
|
|
Ind.Decrement()
|
|
else:
|
|
for assignment in assignments:
|
|
_updateInboundAssignmentTargetNames(ci, cd, assignment)
|
|
row = flattenJSON(assignment)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'name': assignment['name'],
|
|
'JSON': json.dumps(cleanJSON(assignment),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Inbound SSO Assignments')
|
|
|
|
SITEVERIFICATION_METHOD_CHOICE_MAP = {
|
|
'cname': 'DNS_CNAME',
|
|
'dnscname': 'DNS_CNAME',
|
|
'dnstxt': 'DNS_TXT',
|
|
'txt': 'DNS_TXT',
|
|
'text': 'DNS_TXT',
|
|
'file': 'FILE',
|
|
'site': 'FILE',
|
|
}
|
|
|
|
# gam create verify|verification <DomainName>
|
|
def doCreateSiteVerification():
|
|
verif = buildGAPIObject(API.SITEVERIFICATION)
|
|
a_domain = getString(Cmd.OB_DOMAIN_NAME)
|
|
checkForExtraneousArguments()
|
|
txt_record = callGAPI(verif.webResource(), 'getToken',
|
|
body={'site': {'type': 'INET_DOMAIN', 'identifier': a_domain},
|
|
'verificationMethod': 'DNS_TXT'})
|
|
printKeyValueList(['TXT Record Name ', a_domain])
|
|
printKeyValueList(['TXT Record Value', txt_record['token']])
|
|
printBlankLine()
|
|
cname_record = callGAPI(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]
|
|
printKeyValueList(['CNAME Record Name ', f'{cname_subdomain}.{a_domain}'])
|
|
printKeyValueList(['CNAME Record Value', cname_value])
|
|
printBlankLine()
|
|
webserver_file_record = callGAPI(verif.webResource(), 'getToken',
|
|
body={'site': {'type': 'SITE', 'identifier': f'http://{a_domain}/'},
|
|
'verificationMethod': 'FILE'})
|
|
webserver_file_token = webserver_file_record['token']
|
|
printKeyValueList(['Saving web server verification file to', webserver_file_token])
|
|
writeFile(webserver_file_token, f'google-site-verification: {webserver_file_token}', continueOnError=True)
|
|
printKeyValueList(['Verification File URL', f'http://{a_domain}/{webserver_file_token}'])
|
|
printBlankLine()
|
|
webserver_meta_record = callGAPI(verif.webResource(), 'getToken',
|
|
body={'site': {'type': 'SITE', 'identifier': f'http://{a_domain}/'},
|
|
'verificationMethod': 'META'})
|
|
printKeyValueList(['Meta URL', f'//{a_domain}/'])
|
|
printKeyValueList(['Meta HTML Header Data', webserver_meta_record['token']])
|
|
printBlankLine()
|
|
|
|
def _showSiteVerificationInfo(site):
|
|
printKeyValueList(['Site', site['site']['identifier']])
|
|
Ind.Increment()
|
|
printKeyValueList(['ID', unquote(site['id'])])
|
|
printKeyValueList(['Type', site['site']['type']])
|
|
printKeyValueList(['All Owners', None])
|
|
if 'owners' in site:
|
|
Ind.Increment()
|
|
for owner in sorted(site['owners']):
|
|
printKeyValueList([owner])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
DNS_ERROR_CODES_MAP = {
|
|
1: 'DNS Query Format Error',
|
|
2: 'Server failed to complete the DNS request',
|
|
3: 'Domain name does not exist',
|
|
4: 'Function not implemented',
|
|
5: 'The server refused to answer for the query',
|
|
6: 'Name that should not exist, does exist',
|
|
7: 'RRset that should not exist, does exist',
|
|
8: 'Server not authoritative for the zone',
|
|
9: 'Name not in zone'
|
|
}
|
|
|
|
# gam update verify|verification <DomainName> cname|txt|text|file|site
|
|
def doUpdateSiteVerification():
|
|
def showDNSrecords():
|
|
try:
|
|
verify_data = callGAPI(verif.webResource(), 'getToken',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_PARAMETER],
|
|
body=body)
|
|
except (GAPI.badRequest, GAPI.invalidParameter) as e:
|
|
printKeyValueList([ERROR, str(e)])
|
|
return
|
|
printKeyValueList(['Method', verify_data['method']])
|
|
if verify_data['method'] in {'DNS_CNAME', 'DNS_TXT'}:
|
|
if verify_data['method'] == 'DNS_CNAME':
|
|
cname_subdomain, cname_target = verify_data['token'].split(' ')
|
|
query_params = {'name': f'{cname_subdomain}.{a_domain}', 'type': 'cname'}
|
|
printKeyValueList(['Expected Record',
|
|
f'{query_params["name"]} IN CNAME {cname_target}'])
|
|
else:
|
|
query_params = {'name': f'{a_domain}', 'type': 'txt'}
|
|
printKeyValueList(['Expected Record',
|
|
f'{query_params["name"]} IN TXT {verify_data["token"]}'])
|
|
_, content = getHttpObj().request('https://dns.google/resolve?' + urlencode(query_params), 'GET')
|
|
try:
|
|
result = json.loads(content.decode(UTF8))
|
|
status = result['Status']
|
|
if status == 0 and 'Answer' in result:
|
|
if verify_data['method'] == 'DNS_CNAME':
|
|
printKeyValueList(['DNS Record',
|
|
f'{result["Answer"][0]["name"].rstrip(".")} IN CNAME {result["Answer"][0]["data"]}'])
|
|
else:
|
|
found = False
|
|
for answer in result['Answer']:
|
|
answer['data'] = answer['data'].strip('"')
|
|
if answer['data'].startswith('google-site-verification'):
|
|
found = True
|
|
printKeyValueList(['DNS Record',
|
|
f'{answer["name"].rstrip(".")} IN TXT {answer["data"]}'])
|
|
if not found:
|
|
printKeyValueList(['DNS Record', 'No matching record found'])
|
|
elif status == 0:
|
|
systemErrorExit(NETWORK_ERROR_RC, Msg.DOMAIN_NOT_FOUND_IN_DNS)
|
|
else:
|
|
systemErrorExit(NETWORK_ERROR_RC, DNS_ERROR_CODES_MAP.get(status, f'Unknown error {status}'))
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
|
|
systemErrorExit(INVALID_JSON_RC, Msg.INVALID_JSON_INFORMATION)
|
|
|
|
verif = buildGAPIObject(API.SITEVERIFICATION)
|
|
a_domain = getString(Cmd.OB_DOMAIN_NAME)
|
|
verificationMethod = getChoice(SITEVERIFICATION_METHOD_CHOICE_MAP, mapChoice=True)
|
|
if verificationMethod in {'DNS_TXT', 'DNS_CNAME'}:
|
|
verify_type = 'INET_DOMAIN'
|
|
identifier = a_domain
|
|
showDNS = True
|
|
else:
|
|
verify_type = 'SITE'
|
|
identifier = f'http://{a_domain}/'
|
|
showDNS = False
|
|
checkForExtraneousArguments()
|
|
body = {'site': {'type': verify_type, 'identifier': identifier},
|
|
'verificationMethod': verificationMethod}
|
|
try:
|
|
verify_result = callGAPI(verif.webResource(), 'insert',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID_PARAMETER],
|
|
verificationMethod=verificationMethod, body=body)
|
|
except GAPI.badRequest as e:
|
|
printKeyValueList([ERROR, str(e)])
|
|
if showDNS:
|
|
showDNSrecords()
|
|
return
|
|
except GAPI.invalidParameter as e:
|
|
printKeyValueList([ERROR, str(e)])
|
|
return
|
|
printKeyValueList(['Verified!'])
|
|
if showDNS:
|
|
showDNSrecords()
|
|
_showSiteVerificationInfo(verify_result)
|
|
printKeyValueList([Msg.YOU_CAN_ADD_DOMAIN_TO_ACCOUNT.format(a_domain, GC.Values[GC.DOMAIN])])
|
|
|
|
# gam info verify|verification
|
|
def doInfoSiteVerification():
|
|
verif = buildGAPIObject(API.SITEVERIFICATION)
|
|
checkForExtraneousArguments()
|
|
sites = callGAPIitems(verif.webResource(), 'list', 'items')
|
|
if sites:
|
|
for site in sorted(sites, key=lambda k: (k['site']['type'], k['site']['identifier'])):
|
|
_showSiteVerificationInfo(site)
|
|
else:
|
|
printKeyValueList(['No Sites Verified.'])
|
|
|
|
def checkCourseExists(croom, courseId, i=0, count=0, entityType=Ent.COURSE):
|
|
courseId = addCourseIdScope(courseId)
|
|
try:
|
|
result = callGAPI(croom.courses(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
id=courseId, fields='id,ownerId')
|
|
return result
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([entityType, removeCourseIdScope(courseId)], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
|
|
return None
|
|
|
|
COURSE_MEMBER_ARGUMENTS = ['none', 'all', 'students', 'teachers']
|
|
COURSE_STATE_MAPS = {
|
|
Cmd.OB_COURSE_STATE_LIST: {
|
|
'active': 'ACTIVE',
|
|
'archived': 'ARCHIVED',
|
|
'provisioned': 'PROVISIONED',
|
|
'declined': 'DECLINED',
|
|
'suspended': 'SUSPENDED',
|
|
},
|
|
Cmd.OB_COURSE_ANNOUNCEMENT_STATE_LIST: {
|
|
'draft': 'DRAFT',
|
|
'published': 'PUBLISHED',
|
|
'deleted': 'DELETED',
|
|
},
|
|
Cmd.OB_COURSE_WORK_STATE_LIST: {
|
|
'draft': 'DRAFT',
|
|
'published': 'PUBLISHED',
|
|
'deleted': 'DELETED',
|
|
},
|
|
Cmd.OB_COURSE_MATERIAL_STATE_LIST: {
|
|
'draft': 'DRAFT',
|
|
'published': 'PUBLISHED',
|
|
'deleted': 'DELETED',
|
|
},
|
|
Cmd.OB_COURSE_SUBMISSION_STATE_LIST: {
|
|
'new': 'NEW',
|
|
'created': 'CREATED',
|
|
'turnedin': 'TURNED_IN',
|
|
'returned': 'RETURNED',
|
|
'reclaimedbystudent': 'RECLAIMED_BY_STUDENT',
|
|
},
|
|
}
|
|
|
|
def _getCourseStates(item, states):
|
|
stateMap = COURSE_STATE_MAPS[item]
|
|
for state in getString(item).lower().replace(',', ' ').split():
|
|
if state == 'all':
|
|
states.extend([stateMap[state] for state in stateMap])
|
|
elif state in stateMap:
|
|
states.append(stateMap[state])
|
|
else:
|
|
invalidChoiceExit(state, stateMap, True)
|
|
|
|
def _gettingCourseEntityQuery(entityType, courseWorkStates):
|
|
query = ''
|
|
if courseWorkStates:
|
|
query += f'{Ent.Choose(entityType, len(courseWorkStates))}: {",".join(courseWorkStates)}, '
|
|
if query:
|
|
query = query[:-2]
|
|
return query
|
|
|
|
class CourseAttributes():
|
|
|
|
def __init__(self, croom, updateMode):
|
|
self.croom = croom
|
|
self.ocroom = croom
|
|
self.tcroom = None
|
|
self.updateMode = updateMode
|
|
self.body = {}
|
|
self.courseId = None
|
|
self.ownerId = None
|
|
self.markDraftAsPublished = False
|
|
self.markPublishedAsDraft = False
|
|
self.removeDueDate = False
|
|
self.mapShareModeStudentCopy = None
|
|
self.copyMaterialsFiles = False
|
|
self.members = 'none'
|
|
self.teachers = []
|
|
self.students = []
|
|
self.announcementStates = []
|
|
self.courseAnnouncements = []
|
|
self.materialStates = []
|
|
self.courseMaterials = []
|
|
self.workStates = []
|
|
self.courseWorks = []
|
|
self.individualStudentAnnouncements = 'copy'
|
|
self.individualStudentMaterials = 'copy'
|
|
self.individualStudentCourseWork = 'copy'
|
|
self.copyTopics = False
|
|
self.topicsById = {}
|
|
self.reversedTopicIdList = []
|
|
self.currDateTime = None
|
|
self.csvPF = None
|
|
|
|
COURSE_ANNOUNCEMENT_READONLY_FIELDS = [
|
|
'alternateLink',
|
|
'courseId',
|
|
'creationTime',
|
|
'creatorUserId',
|
|
'updateTime',
|
|
]
|
|
COURSE_MATERIAL_READONLY_FIELDS = [
|
|
'alternateLink',
|
|
'courseId',
|
|
'creationTime',
|
|
'creatorUserId',
|
|
'updateTime',
|
|
]
|
|
|
|
COURSE_COURSEWORK_READONLY_FIELDS = [
|
|
'alternateLink',
|
|
'assignment',
|
|
'associatedWithDeveloper',
|
|
'courseId',
|
|
'creationTime',
|
|
'creatorUserId',
|
|
'updateTime',
|
|
]
|
|
|
|
MAX_TITLE_DISPLAY_LENGTH = 34
|
|
|
|
def trimTitle(self, title):
|
|
if len(title) <= self.MAX_TITLE_DISPLAY_LENGTH:
|
|
return title
|
|
return title[:self.MAX_TITLE_DISPLAY_LENGTH]+'...'
|
|
|
|
def CleanMaterials(self, body, entityType, entityId):
|
|
if 'materials' not in body:
|
|
return
|
|
materials = body.pop('materials')
|
|
body['materials'] = []
|
|
for material in materials:
|
|
if 'driveFile' in material:
|
|
material['driveFile']['driveFile'].pop('title', None)
|
|
material['driveFile']['driveFile'].pop('alternateLink', None)
|
|
material['driveFile']['driveFile'].pop('thumbnailUrl', None)
|
|
if material['driveFile'].get('shareMode', '') == 'STUDENT_COPY' and self.mapShareModeStudentCopy is not None:
|
|
material['driveFile']['shareMode'] = self.mapShareModeStudentCopy
|
|
body['materials'].append(material)
|
|
if self.csvPF and not self.copyMaterialsFiles:
|
|
self.csvPF.WriteRow({'courseId': self.courseId, 'ownerId': self.ownerId, 'fileId': material['driveFile']['driveFile']['id']})
|
|
elif 'youtubeVideo' in material:
|
|
material['youtubeVideo'].pop('title', None)
|
|
material['youtubeVideo'].pop('alternateLink', None)
|
|
material['youtubeVideo'].pop('thumbnailUrl', None)
|
|
body['materials'].append(material)
|
|
elif 'link' in material:
|
|
material['link'].pop('title', None)
|
|
material['link'].pop('thumbnailUrl', None)
|
|
body['materials'].append(material)
|
|
elif 'form' in material:
|
|
action = Act.Get()
|
|
Act.Set(Act.COPY)
|
|
entityActionNotPerformedWarning([Ent.COURSE, self.courseId, entityType, entityId,
|
|
Ent.COURSE_MATERIAL_FORM, self.trimTitle(material['form'].get('title', UNKNOWN))],
|
|
Msg.NOT_COPYABLE)
|
|
Act.Set(action)
|
|
|
|
@staticmethod
|
|
def CleanAssignments(body):
|
|
if 'assignment' in body and 'studentWorkFolder' in body['assignment']:
|
|
body['assignment']['studentWorkFolder'].pop('title', None)
|
|
body['assignment']['studentWorkFolder'].pop('alternateLink', None)
|
|
|
|
COURSE_MATERIAL_SHAREMODE_MAP = {
|
|
'edit': 'EDIT',
|
|
'none': None,
|
|
'view': 'VIEW'
|
|
}
|
|
|
|
COURSE_INDIVIDUAL_STUDENT_OPTIONS = {'copy', 'delete', 'maptoall'}
|
|
|
|
def GetAttributes(self):
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if not self.updateMode and myarg in {'alias', 'id'}:
|
|
self.body['id'] = getCourseAlias()
|
|
elif myarg == 'name':
|
|
self.body['name'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'section':
|
|
self.body['section'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'heading', 'descriptionheading'}:
|
|
self.body['descriptionHeading'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'description':
|
|
self.body['description'] = getStringWithCRsNLs()
|
|
elif myarg == 'room':
|
|
self.body['room'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'owner', 'ownerid', 'teacher'}:
|
|
self.body['ownerId'] = getEmailAddress()
|
|
elif myarg in {'state', 'status', 'coursestate'}:
|
|
self.body['courseState'] = getChoice(COURSE_STATE_MAPS[Cmd.OB_COURSE_STATE_LIST], mapChoice=True)
|
|
elif myarg == 'guardiansenabled':
|
|
self.body['guardiansEnabled'] = getBoolean()
|
|
elif myarg == 'copyfrom':
|
|
self.courseId = getString(Cmd.OB_COURSE_ID)
|
|
elif myarg in {'announcementstate', 'announcementstates'}:
|
|
_getCourseStates(Cmd.OB_COURSE_ANNOUNCEMENT_STATE_LIST, self.announcementStates)
|
|
elif myarg in {'workstate', 'workstates', 'courseworkstate', 'courseworkstates'}:
|
|
_getCourseStates(Cmd.OB_COURSE_WORK_STATE_LIST, self.workStates)
|
|
elif myarg in {'materialstate', 'materialstates', 'coursematerialstate', 'coursematerialstates'}:
|
|
_getCourseStates(Cmd.OB_COURSE_MATERIAL_STATE_LIST, self.materialStates)
|
|
elif myarg == 'individualstudentannouncements':
|
|
self.individualStudentAnnouncements = getChoice(self.COURSE_INDIVIDUAL_STUDENT_OPTIONS)
|
|
elif myarg == 'individualstudentmaterials':
|
|
self.individualStudentMaterials = getChoice(self.COURSE_INDIVIDUAL_STUDENT_OPTIONS)
|
|
elif myarg == 'individualstudentcoursework':
|
|
self.individualStudentCourseWork = getChoice(self.COURSE_INDIVIDUAL_STUDENT_OPTIONS)
|
|
elif myarg == 'individualstudentassignments':
|
|
self.individualStudentAnnouncements = self.individualStudentMaterials = self.individualStudentCourseWork =\
|
|
getChoice(self.COURSE_INDIVIDUAL_STUDENT_OPTIONS)
|
|
elif myarg == 'members':
|
|
self.members = getChoice(COURSE_MEMBER_ARGUMENTS)
|
|
elif myarg == 'markdraftaspublished':
|
|
self.markDraftAsPublished = getBoolean()
|
|
elif myarg == 'markpublishedasdraft':
|
|
self.markPublishedAsDraft = getBoolean()
|
|
elif myarg == 'removeduedate':
|
|
self.removeDueDate = getBoolean()
|
|
elif myarg == 'mapsharemodestudentcopy':
|
|
self.mapShareModeStudentCopy = getChoice(self.COURSE_MATERIAL_SHAREMODE_MAP, mapChoice=True)
|
|
elif myarg == 'copymaterialsfiles':
|
|
self.copyMaterialsFiles = getBoolean()
|
|
elif myarg == 'copytopics':
|
|
self.copyTopics = getBoolean()
|
|
elif myarg == 'logdrivefileids':
|
|
if getBoolean():
|
|
self.csvPF = CSVPrintFile(['courseId', 'ownerId', 'fileId'])
|
|
else:
|
|
self.csvPF = None
|
|
else:
|
|
unknownArgumentExit()
|
|
if not self.updateMode:
|
|
if 'ownerId' not in self.body:
|
|
missingArgumentExit('teacher <UserItem>')
|
|
if 'name' not in self.body:
|
|
missingArgumentExit('name <String>')
|
|
if self.courseId:
|
|
copyFromCourseInfo = checkCourseExists(self.croom, self.courseId, entityType=Ent.COPYFROM_COURSE)
|
|
if copyFromCourseInfo is None:
|
|
return False
|
|
self.ownerId = copyFromCourseInfo['ownerId']
|
|
if (self.announcementStates or self.materialStates or self.workStates) and self.copyMaterialsFiles:
|
|
self.body['courseState'] = 'ACTIVE'
|
|
elif self.members != 'none' or self.announcementStates or self.materialStates or self.workStates or self.copyTopics:
|
|
missingArgumentExit('copyfrom <CourseID>')
|
|
else:
|
|
return True
|
|
# ocroom - copyfrom course owner
|
|
self.ocroom = self.croom
|
|
if GC.Values[GC.USE_COURSE_OWNER_ACCESS]:
|
|
if self.announcementStates or self.materialStates or self.workStates or self.copyTopics or self.members != 'none':
|
|
_, self.ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{self.ownerId}')
|
|
if self.ocroom is None:
|
|
return False
|
|
if self.members != 'none':
|
|
_, self.teachers, self.students = _getCourseAliasesMembers(self.croom, self.ocroom, self.courseId, {'members': self.members},
|
|
'nextPageToken,teachers(profile(emailAddress,id))',
|
|
'nextPageToken,students(profile(emailAddress))')
|
|
if self.announcementStates:
|
|
printGettingAllEntityItemsForWhom(Ent.COURSE_ANNOUNCEMENT_ID, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0,
|
|
_gettingCourseEntityQuery(Ent.COURSE_ANNOUNCEMENT_STATE, self.announcementStates))
|
|
try:
|
|
self.courseAnnouncements = callGAPIpages(self.ocroom.courses().announcements(), 'list', 'announcements',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=self.courseId, announcementStates=self.announcementStates,
|
|
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
for courseAnnouncement in self.courseAnnouncements:
|
|
for field in self.COURSE_ANNOUNCEMENT_READONLY_FIELDS:
|
|
courseAnnouncement.pop(field, None)
|
|
self.CleanMaterials(courseAnnouncement, Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncement['id'])
|
|
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, self.courseId], str(e))
|
|
return False
|
|
if self.materialStates:
|
|
printGettingAllEntityItemsForWhom(Ent.COURSE_MATERIAL_ID, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0,
|
|
_gettingCourseEntityQuery(Ent.COURSE_MATERIAL_STATE, self.materialStates))
|
|
try:
|
|
self.courseMaterials = callGAPIpages(self.ocroom.courses().courseWorkMaterials(), 'list', 'courseWorkMaterial',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=self.courseId, courseWorkMaterialStates=self.materialStates,
|
|
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
for courseMaterial in self.courseMaterials:
|
|
for field in self.COURSE_MATERIAL_READONLY_FIELDS:
|
|
courseMaterial.pop(field, None)
|
|
if self.markPublishedAsDraft and courseMaterial['state'] == 'PUBLISHED':
|
|
courseMaterial['state'] = 'DRAFT'
|
|
elif self.markDraftAsPublished and courseMaterial['state'] == 'DRAFT':
|
|
courseMaterial['state'] = 'PUBLISHED'
|
|
self.CleanMaterials(courseMaterial, Ent.COURSE_MATERIAL_ID, courseMaterial['id'])
|
|
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, self.courseId], str(e))
|
|
return False
|
|
if self.workStates:
|
|
printGettingAllEntityItemsForWhom(Ent.COURSE_WORK_ID, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0,
|
|
_gettingCourseEntityQuery(Ent.COURSE_WORK_STATE, self.workStates))
|
|
try:
|
|
self.courseWorks = callGAPIpages(self.ocroom.courses().courseWork(), 'list', 'courseWork',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=self.courseId, courseWorkStates=self.workStates,
|
|
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
for courseWork in self.courseWorks:
|
|
for field in self.COURSE_COURSEWORK_READONLY_FIELDS:
|
|
courseWork.pop(field, None)
|
|
self.CleanMaterials(courseWork, Ent.COURSE_WORK_ID, courseWork['id'])
|
|
self.CleanAssignments(courseWork)
|
|
if self.markPublishedAsDraft and courseWork['state'] == 'PUBLISHED':
|
|
courseWork['state'] = 'DRAFT'
|
|
elif self.markDraftAsPublished and courseWork['state'] == 'DRAFT':
|
|
courseWork['state'] = 'PUBLISHED'
|
|
if self.removeDueDate:
|
|
courseWork.pop('dueDate', None)
|
|
courseWork.pop('dueTime', None)
|
|
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, self.courseId], str(e))
|
|
return False
|
|
if self.copyTopics:
|
|
printGettingAllEntityItemsForWhom(Ent.COURSE_TOPIC, Ent.TypeName(Ent.COURSE, self.courseId), 0, 0)
|
|
try:
|
|
courseTopics = callGAPIpages(self.ocroom.courses().topics(), 'list', 'topic',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=self.courseId, fields='nextPageToken,topic(topicId,name)',
|
|
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
for topic in courseTopics:
|
|
self.topicsById[topic['topicId']] = topic['name']
|
|
self.reversedTopicIdList.append(topic['topicId'])
|
|
self.reversedTopicIdList.reverse()
|
|
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, self.courseId], str(e))
|
|
return False
|
|
return True
|
|
|
|
def CopyMaterials(self, drive, newCourseId, body, entityType, entityId, teacherFolderId):
|
|
def _copyMaterialsError(fileId, errMsg):
|
|
entityModifierItemValueListActionNotPerformedWarning([Ent.COURSE, newCourseId, entityType, entityId, Ent.COURSE_MATERIAL_DRIVEFILE, ''], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId, Ent.COURSE_MATERIAL_DRIVEFILE, fileId], errMsg)
|
|
|
|
if 'materials' not in body:
|
|
return
|
|
action = Act.Get()
|
|
Act.Set(Act.COPY)
|
|
materials = body.pop('materials')
|
|
body['materials'] = []
|
|
for material in materials:
|
|
if 'driveFile' in material:
|
|
fileId = material['driveFile']['driveFile']['id']
|
|
try:
|
|
source = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId,
|
|
fields='name,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
|
|
'description,mimeType,modifiedTime,properties,starred,driveId,viewedByMeTime,writersCanShare',
|
|
supportsAllDrives=True)
|
|
if not source.pop('capabilities')['canCopy']:
|
|
_copyMaterialsError(fileId, Msg.NOT_COPYABLE)
|
|
continue
|
|
source['parents'] = [teacherFolderId]
|
|
result = callGAPI(drive.files(), 'copy',
|
|
throwReasons=GAPI.DRIVE_COPY_THROW_REASONS,
|
|
fileId=fileId, body=source, fields='id', supportsAllDrives=True)
|
|
material['driveFile']['driveFile']['id'] = result['id']
|
|
body['materials'].append(material)
|
|
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, entityType, entityId, Ent.COURSE_MATERIAL_DRIVEFILE, result['id']], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId, Ent.COURSE_MATERIAL_DRIVEFILE, fileId])
|
|
if self.csvPF:
|
|
self.csvPF.WriteRow({'courseId': self.courseId, 'ownerId': self.ownerId, 'fileId': result['id']})
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.invalid, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
|
|
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.rateLimitExceeded, GAPI.userRateLimitExceeded) as e:
|
|
_copyMaterialsError(fileId, str(e))
|
|
except (GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
|
|
_copyMaterialsError(fileId, str(e))
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
_copyMaterialsError(fileId, str(e))
|
|
break
|
|
else:
|
|
body['materials'].append(material)
|
|
Act.Set(action)
|
|
|
|
def checkDueDate(self, body):
|
|
if 'dueDate' in body and 'dueTime' in body:
|
|
try:
|
|
return self.currDateTime < datetime.datetime(body['dueDate']['year'], body['dueDate']['month'], body['dueDate']['day'],
|
|
body['dueTime'].get('hours', 0), body['dueTime'].get('minutes', 0), tzinfo=iso8601.UTC)
|
|
except ValueError:
|
|
pass
|
|
return False
|
|
|
|
def getItemIdTitle(self, body):
|
|
itemId = body.pop('id')
|
|
return self.trimTitle(body.get('title', itemId))
|
|
|
|
def checkItemCopyable(self, state, newCourseId, entityType, entityId, body, individualStudentOption, clarg, j, jcount):
|
|
if state == 'DELETED':
|
|
entityModifierItemValueListActionNotPerformedWarning([Ent.COURSE, newCourseId, entityType, entityId], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId], Msg.DELETED, j, jcount)
|
|
return False
|
|
if body['assigneeMode'] == 'INDIVIDUAL_STUDENTS':
|
|
if individualStudentOption == 'delete':
|
|
entityModifierItemValueListActionNotPerformedWarning([Ent.COURSE, newCourseId, entityType, entityId], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId], f'{clarg} delete', j, jcount)
|
|
return False
|
|
if individualStudentOption == 'maptoall':
|
|
body['assigneeMode'] = 'ALL_STUDENTS'
|
|
body.pop('individualStudentsOptions', None)
|
|
else: # individualStudentOption == 'copy':
|
|
if 'individualStudentsOptions' not in body:
|
|
body['assigneeMode'] = 'ALL_STUDENTS'
|
|
return True
|
|
|
|
def CopyAttributes(self, newCourse, i=0, count=0):
|
|
newCourseId = newCourse['id']
|
|
ownerId = newCourse['ownerId']
|
|
teacherFolderId = newCourse['teacherFolder']['id']
|
|
# tcroom - new/update course owner
|
|
if self.announcementStates or self.materialStates or self.workStates or self.copyTopics:
|
|
_, self.tcroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{ownerId}')
|
|
if self.tcroom is None:
|
|
return
|
|
if (self.announcementStates or self.materialStates or self.workStates) and self.copyMaterialsFiles:
|
|
_, tdrive = buildGAPIServiceObject(API.DRIVE3, f'uid:{ownerId}')
|
|
if tdrive is None:
|
|
return
|
|
# Adds are done with domain admin
|
|
if self.members in {'all', 'students'}:
|
|
addParticipants = [student['profile']['emailAddress'] for student in self.students if 'emailAddress' in student['profile']]
|
|
_batchAddItemsToCourse(self.croom, newCourseId, i, count, addParticipants, Ent.STUDENT)
|
|
if self.members in {'all', 'teachers'}:
|
|
addParticipants = [teacher['profile']['emailAddress'] for teacher in self.teachers if teacher['profile']['id'] != ownerId and 'emailAddress' in teacher['profile']]
|
|
_batchAddItemsToCourse(self.croom, newCourseId, i, count, addParticipants, Ent.TEACHER)
|
|
if self.copyTopics:
|
|
try:
|
|
newCourseTopics = callGAPIpages(self.tcroom.courses().topics(), 'list', 'topic',
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.FAILED_PRECONDITION, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=newCourseId, fields='nextPageToken,topic(topicId,name)',
|
|
pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
newTopicsByName = {}
|
|
for topic in newCourseTopics:
|
|
newTopicsByName[topic['name']] = topic['topicId']
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
|
|
return
|
|
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.failedPrecondition, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
|
|
jcount = len(self.topicsById)
|
|
j = 0
|
|
for topicId in self.reversedTopicIdList:
|
|
topicName = self.topicsById[topicId]
|
|
j += 1
|
|
if topicName in newTopicsByName:
|
|
entityModifierItemValueListActionNotPerformedWarning([Ent.COURSE, newCourseId, Ent.COURSE_TOPIC, topicName], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId], Msg.DUPLICATE, j, jcount)
|
|
continue
|
|
try:
|
|
result = callGAPI(self.tcroom.courses().topics(), 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.FAILED_PRECONDITION, GAPI.INVALID_ARGUMENT, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=newCourseId, body={'name': topicName}, fields='topicId')
|
|
newTopicsByName[topicName] = result['topicId']
|
|
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, Ent.COURSE_TOPIC, topicName], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId], j, jcount)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
|
|
return
|
|
except (GAPI.failedPrecondition, GAPI.invalidArgument, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
entityModifierItemValueListActionFailedWarning([Ent.COURSE, newCourseId], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId, Ent.COURSE_TOPIC, topicName], str(e), j, jcount)
|
|
if self.courseAnnouncements:
|
|
jcount = len(self.courseAnnouncements)
|
|
j = 0
|
|
for courseAnnouncement in self.courseAnnouncements:
|
|
j += 1
|
|
body = courseAnnouncement.copy()
|
|
courseAnnouncementId = self.getItemIdTitle(body)
|
|
if not self.checkItemCopyable(courseAnnouncement['state'], newCourseId, Ent.COURSE_ANNOUNCEMENT, courseAnnouncementId,
|
|
body, self.individualStudentAnnouncements, 'individualstudentannouncements', j, jcount):
|
|
continue
|
|
if self.copyMaterialsFiles:
|
|
self.CopyMaterials(tdrive, newCourseId, body, Ent.COURSE_ANNOUNCEMENT, courseAnnouncementId, teacherFolderId)
|
|
try:
|
|
result = callGAPI(self.tcroom.courses().announcements(), 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FORBIDDEN,
|
|
GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.BACKEND_ERROR, GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=newCourseId, body=body, fields='id')
|
|
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, Ent.COURSE_ANNOUNCEMENT_ID, result['id']], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId, Ent.COURSE_ANNOUNCEMENT, courseAnnouncementId], j, jcount)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
|
|
return
|
|
except (GAPI.badRequest, GAPI.failedPrecondition, GAPI.backendError, GAPI.internalError,
|
|
GAPI.permissionDenied, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
entityModifierItemValueListActionFailedWarning([Ent.COURSE, newCourseId], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId, Ent.COURSE_ANNOUNCEMENT, courseAnnouncementId], str(e), j, jcount)
|
|
if self.courseMaterials:
|
|
jcount = len(self.courseMaterials)
|
|
j = 0
|
|
for courseMaterial in self.courseMaterials:
|
|
j += 1
|
|
body = courseMaterial.copy()
|
|
courseMaterialId = self.getItemIdTitle(body)
|
|
if not self.checkItemCopyable(courseMaterial['state'], newCourseId, Ent.COURSE_MATERIAL, courseMaterialId,
|
|
body, self.individualStudentMaterials, 'individualstudentmaterials', j, jcount):
|
|
continue
|
|
if self.copyMaterialsFiles:
|
|
self.CopyMaterials(tdrive, newCourseId, body, Ent.COURSE_MATERIAL, courseMaterialId, teacherFolderId)
|
|
topicId = body.pop('topicId', None)
|
|
if self.copyTopics:
|
|
if topicId:
|
|
topicName = self.topicsById.get(topicId)
|
|
if topicName:
|
|
newTopicId = newTopicsByName.get(topicName)
|
|
if newTopicId:
|
|
body['topicId'] = newTopicId
|
|
try:
|
|
result = callGAPI(self.tcroom.courses().courseWorkMaterials(), 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FORBIDDEN,
|
|
GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.BACKEND_ERROR, GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=newCourseId, body=body, fields='id')
|
|
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, Ent.COURSE_MATERIAL_ID, result['id']], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId, Ent.COURSE_MATERIAL, courseMaterialId], j, jcount)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
|
|
return
|
|
except (GAPI.badRequest, GAPI.failedPrecondition, GAPI.backendError, GAPI.internalError,
|
|
GAPI.permissionDenied, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
entityModifierItemValueListActionFailedWarning([Ent.COURSE, newCourseId], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId, Ent.COURSE_MATERIAL, courseMaterialId], str(e), j, jcount)
|
|
if self.courseWorks:
|
|
jcount = len(self.courseWorks)
|
|
j = 0
|
|
for courseWork in self.courseWorks:
|
|
j += 1
|
|
body = courseWork.copy()
|
|
courseWorkId = self.getItemIdTitle(body)
|
|
if not self.checkItemCopyable(courseWork['state'], newCourseId, Ent.COURSE_WORK, courseWorkId,
|
|
body, self.individualStudentCourseWork, 'individualstudentcoursework', j, jcount):
|
|
continue
|
|
if self.copyMaterialsFiles:
|
|
self.CopyMaterials(tdrive, newCourseId, body, Ent.COURSE_WORK, courseWorkId, teacherFolderId)
|
|
topicId = body.pop('topicId', None)
|
|
if self.copyTopics:
|
|
if topicId:
|
|
topicName = self.topicsById.get(topicId)
|
|
if topicName:
|
|
newTopicId = newTopicsByName.get(topicName)
|
|
if newTopicId:
|
|
body['topicId'] = newTopicId
|
|
if not self.removeDueDate and not self.checkDueDate(body):
|
|
body.pop('dueDate', None)
|
|
body.pop('dueTime', None)
|
|
try:
|
|
result = callGAPI(self.tcroom.courses().courseWork(), 'create',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FORBIDDEN,
|
|
GAPI.BAD_REQUEST, GAPI.FAILED_PRECONDITION, GAPI.BACKEND_ERROR,
|
|
GAPI.INTERNAL_ERROR, GAPI.INVALID_ARGUMENT, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=newCourseId, body=body, fields='id')
|
|
entityModifierItemValueListActionPerformed([Ent.COURSE, newCourseId, Ent.COURSE_WORK_ID, result['id']], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId, Ent.COURSE_WORK, courseWorkId], j, jcount)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.COURSE, newCourseId], str(e), i, count)
|
|
return
|
|
except (GAPI.badRequest, GAPI.failedPrecondition, GAPI.backendError, GAPI.internalError, GAPI.invalidArgument,
|
|
GAPI.permissionDenied, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
entityModifierItemValueListActionFailedWarning([Ent.COURSE, newCourseId], Act.MODIFIER_FROM,
|
|
[Ent.COURSE, self.courseId, Ent.COURSE_WORK, courseWorkId], str(e), j, jcount)
|
|
|
|
def CopyFromCourse(self, newCourse, i=0, count=0):
|
|
action = Act.Get()
|
|
Act.Set(Act.COPY)
|
|
entityPerformActionModifierItemValueList([Ent.COURSE, newCourse['id']], Act.MODIFIER_FROM, [Ent.COURSE, self.courseId], i, count)
|
|
Ind.Increment()
|
|
if not self.removeDueDate:
|
|
self.currDateTime = datetime.datetime.now(iso8601.UTC)
|
|
self.CopyAttributes(newCourse, i, count)
|
|
if self.csvPF:
|
|
self.csvPF.writeCSVfile('Course Drive File IDs')
|
|
Ind.Decrement()
|
|
Act.Set(action)
|
|
|
|
# gam create course [id|alias <CourseAlias>] <CourseAttribute>*
|
|
# [copyfrom <CourseID>
|
|
# [announcementstates <CourseAnnouncementStateList>]
|
|
# [individualstudentannouncements copy|delete|maptoall]
|
|
# [materialstates <CourseMaterialStateList>]
|
|
# [individualstudentmaterials copy|delete|maptoall]
|
|
# [workstates <CourseWorkStateList>]
|
|
# [individualstudentcoursework copy|delete|maptoall]
|
|
# [removeduedate [<Boolean>]]
|
|
# [mapsharemodestudentcopy edit|none|view]
|
|
# [individualstudentassignments copy|delete|maptoall]
|
|
# [copymaterialsfiles [<Boolean>]]
|
|
# [copytopics [<Boolean>]]
|
|
# [markpublishedasdraft [<Boolean>]] [markdraftaspublished [<Boolean>]]
|
|
# [members none|all|students|teachers]]
|
|
# [logdrivefileids [<Boolean>]]
|
|
def doCreateCourse():
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
courseAttributes = CourseAttributes(croom, False)
|
|
if not courseAttributes.GetAttributes():
|
|
return
|
|
try:
|
|
result = callGAPI(croom.courses(), 'create',
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED,
|
|
GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
body=courseAttributes.body, fields='id,name,ownerId,courseState,teacherFolder(id)')
|
|
entityActionPerformed([Ent.COURSE_NAME, result['name'], Ent.COURSE, result['id']])
|
|
if courseAttributes.courseId:
|
|
courseAttributes.CopyFromCourse(result)
|
|
except (GAPI.alreadyExists, GAPI.notFound, GAPI.permissionDenied, GAPI.failedPrecondition, GAPI.forbidden, GAPI.badRequest, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE_NAME, courseAttributes.body['name'], Ent.TEACHER, courseAttributes.body['ownerId']], str(e))
|
|
|
|
def _doUpdateCourses(entityList):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
courseAttributes = CourseAttributes(croom, True)
|
|
if not courseAttributes.GetAttributes():
|
|
return
|
|
i = 0
|
|
count = len(entityList)
|
|
for course in entityList:
|
|
i += 1
|
|
courseId = addCourseIdScope(course)
|
|
body = courseAttributes.body.copy()
|
|
newOwner = body.get('ownerId')
|
|
modifier = Act.MODIFIER_WITH_COTEACHER_OWNER
|
|
complete = False
|
|
while not complete:
|
|
complete = True
|
|
try:
|
|
if body:
|
|
result = callGAPI(croom.courses(), 'patch',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION,
|
|
GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT,
|
|
GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
id=courseId, body=body, updateMask=','.join(list(body)), fields='id,name,ownerId,courseState,teacherFolder(id)')
|
|
else:
|
|
result = callGAPI(croom.courses(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
id=courseId, fields='id,name,ownerId,courseState,teacherFolder(id)')
|
|
if courseAttributes.body:
|
|
if not newOwner:
|
|
entityActionPerformed([Ent.COURSE_NAME, result['name'], Ent.COURSE, result['id']], i, count)
|
|
else:
|
|
entityModifierNewValueActionPerformed([Ent.COURSE_NAME, result['name'], Ent.COURSE, result['id']],
|
|
modifier, newOwner, i, count)
|
|
if courseAttributes.courseId:
|
|
courseAttributes.CopyFromCourse(result, i, count)
|
|
except (GAPI.notFound, GAPI.permissionDenied,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalidArgument,
|
|
GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
|
|
except GAPI.failedPrecondition as e:
|
|
errMsg = str(e)
|
|
if newOwner and '@UserAlreadyOwner Cannot transfer course to the user who is already the owner' in errMsg:
|
|
## Handle trying to update current owner to owner, we're done if nothing else was being updated
|
|
body.pop('ownerId')
|
|
modifier = Act.MODIFIER_WITH_CURRENT_OWNER
|
|
complete = False
|
|
elif newOwner and '@IneligibleOwner Only a co-teacher can be invited as owner of the course' in errMsg:
|
|
## Add new owner as teacher, then go back and do update
|
|
action = Act.Get()
|
|
Act.Set(Act.ADD)
|
|
try:
|
|
callGAPI(croom.courses().teachers(), 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR,
|
|
GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION,
|
|
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, body={'userId': newOwner}, fields='')
|
|
modifier = Act.MODIFIER_WITH_NEW_TEACHER_OWNER
|
|
time.sleep(10)
|
|
complete = False
|
|
except (GAPI.notFound, GAPI.backendError, GAPI.forbidden):
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, newOwner], getPhraseDNEorSNA(newOwner), i, count)
|
|
except GAPI.alreadyExists:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, newOwner], Msg.DUPLICATE, i, count)
|
|
except GAPI.failedPrecondition:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, newOwner], Msg.NOT_ALLOWED, i, count)
|
|
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable) as ei:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, newOwner], str(ei), i, count)
|
|
Act.Set(action)
|
|
else:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
|
|
|
|
# gam update courses <CourseEntity> <CourseAttribute>+
|
|
# [copyfrom <CourseID>
|
|
# [announcementstates <CourseAnnouncementStateList>]
|
|
# [individualstudentannouncements copy|delete|maptoall]
|
|
# [materialstates <CourseMaterialStateList>]
|
|
# [individualstudentmaterials copy|delete|maptoall]
|
|
# [workstates <CourseWorkStateList>]
|
|
# [individualstudentcoursework copy|delete|maptoall]
|
|
# [removeduedate [<Boolean>]]
|
|
# [mapsharemodestudentcopy edit|none|view]
|
|
# [individualstudentassignments copy|delete|maptoall]
|
|
# [copymaterialsfiles [<Boolean>]]
|
|
# [copytopics [<Boolean>]]
|
|
# [markpublishedasdraft [<Boolean>]] [markdraftaspublished [<Boolean>]]
|
|
# [members none|all|students|teachers]]
|
|
# [logdrivefileids [<Boolean>]]
|
|
def doUpdateCourses():
|
|
_doUpdateCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
|
|
|
|
# gam update course <CourseID> <CourseAttribute>+
|
|
# [copyfrom <CourseID>
|
|
# [announcementstates <CourseAnnouncementStateList>]
|
|
# [individualstudentannouncements copy|delete|maptoall]
|
|
# [materialstates <CourseMaterialStateList>]
|
|
# [individualstudentmaterials copy|delete|maptoall]
|
|
# [workstates <CourseWorkStateList>]
|
|
# [individualstudentcoursework copy|delete|maptoall]
|
|
# [removeduedate [<Boolean>]]
|
|
# [mapsharemodestudentcopy edit|none|view]
|
|
# [individualstudentassignments copy|delete|maptoall]
|
|
# [copymaterialsfiles [<Boolean>]]
|
|
# [copytopics [<Boolean>]]
|
|
# [markpublishedasdraft [<Boolean>]] [markdraftaspublished [<Boolean>]]
|
|
# [members none|all|students|teachers]]
|
|
def doUpdateCourse():
|
|
_doUpdateCourses(getStringReturnInList(Cmd.OB_COURSE_ID))
|
|
|
|
def _doDeleteCourses(entityList):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'archive', 'archived'}:
|
|
body['courseState'] = 'ARCHIVED'
|
|
updateMask = 'courseState'
|
|
else:
|
|
unknownArgumentExit()
|
|
i = 0
|
|
count = len(entityList)
|
|
for course in entityList:
|
|
i += 1
|
|
courseId = addCourseIdScope(course)
|
|
try:
|
|
if body:
|
|
callGAPI(croom.courses(), 'patch',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION,
|
|
GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT,
|
|
GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
id=courseId, body=body, updateMask=updateMask, fields='')
|
|
callGAPI(croom.courses(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION, GAPI.INTERNAL_ERROR],
|
|
id=courseId)
|
|
entityActionPerformed([Ent.COURSE, removeCourseIdScope(courseId)], i, count)
|
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.failedPrecondition,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalidArgument,
|
|
GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
|
|
|
|
# gam delete courses <CourseEntity> [archive|archived]
|
|
def doDeleteCourses():
|
|
_doDeleteCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
|
|
|
|
# gam delete course <CourseID> [archive|archived]
|
|
def doDeleteCourse():
|
|
_doDeleteCourses(getStringReturnInList(Cmd.OB_COURSE_ID))
|
|
|
|
COURSE_FIELDS_CHOICE_MAP = {
|
|
'alternatelink': 'alternateLink',
|
|
'calendarid': 'calendarId',
|
|
'coursegroupemail': 'courseGroupEmail',
|
|
'coursematerialsets': 'courseMaterialSets',
|
|
'coursestate': 'courseState',
|
|
'creationtime': 'creationTime',
|
|
'description': 'description',
|
|
'descriptionheading': 'descriptionHeading',
|
|
'enrollmentcode': 'enrollmentCode',
|
|
'gradebooksettings': 'gradebookSettings',
|
|
'guardiansenabled': 'guardiansEnabled',
|
|
'heading': 'descriptionHeading',
|
|
'id': 'id',
|
|
'name': 'name',
|
|
'owneremail': 'ownerId',
|
|
'ownerid': 'ownerId',
|
|
'room': 'room',
|
|
'section': 'section',
|
|
'teacherfolder': 'teacherFolder',
|
|
'teachergroupemail': 'teacherGroupEmail',
|
|
'updatetime': 'updateTime',
|
|
}
|
|
COURSE_TIME_OBJECTS = {'creationTime', 'updateTime'}
|
|
COURSE_NOLEN_OBJECTS = {'materials'}
|
|
COURSE_PROPERTY_PRINT_ORDER = [
|
|
'id',
|
|
'name',
|
|
'Aliases',
|
|
'courseState',
|
|
'descriptionHeading',
|
|
'description',
|
|
'section',
|
|
'room',
|
|
'enrollmentCode',
|
|
'guardiansEnabled',
|
|
'alternateLink',
|
|
'ownerEmail',
|
|
'ownerId',
|
|
'creationTime',
|
|
'updateTime',
|
|
'calendarId',
|
|
'courseGroupEmail',
|
|
'teacherGroupEmail',
|
|
'teacherFolder.id',
|
|
'teacherFolder.title',
|
|
'teacherFolder.alternateLink',
|
|
]
|
|
|
|
def _initCourseShowProperties(fields=None):
|
|
return {'aliases': False, 'aliasesInColumns': False, 'ownerEmail': False, 'ownerEmailMatchPattern': None, 'members': 'none', 'countsOnly': False,
|
|
'fields': fields if fields is not None else [], 'skips': []}
|
|
|
|
def _getCourseShowProperties(myarg, courseShowProperties):
|
|
if myarg in {'alias', 'aliases'}:
|
|
courseShowProperties['aliases'] = True
|
|
courseShowProperties['aliasesInColumns'] = False
|
|
elif myarg == 'aliasesincolumns':
|
|
courseShowProperties['aliases'] = True
|
|
courseShowProperties['aliasesInColumns'] = True
|
|
elif myarg == 'owneremail':
|
|
courseShowProperties['ownerEmail'] = True
|
|
elif myarg == 'owneremailmatchpattern':
|
|
courseShowProperties['ownerEmail'] = True
|
|
courseShowProperties['ownerEmailMatchPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'show':
|
|
courseShowProperties['members'] = getChoice(COURSE_MEMBER_ARGUMENTS)
|
|
elif myarg == 'countsonly':
|
|
courseShowProperties['countsOnly'] = True
|
|
elif myarg == 'fields':
|
|
for field in _getFieldsList():
|
|
if field in {'alias', 'aliases'}:
|
|
courseShowProperties['aliases'] = True
|
|
courseShowProperties['aliasesInColumns'] = False
|
|
elif field == 'aliasesincolumns':
|
|
courseShowProperties['aliases'] = True
|
|
courseShowProperties['aliasesInColumns'] = True
|
|
elif field == 'owneremail':
|
|
courseShowProperties['ownerEmail'] = True
|
|
courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field])
|
|
elif field == 'teachers':
|
|
if courseShowProperties['members'] == 'none':
|
|
courseShowProperties['members'] = field
|
|
elif courseShowProperties['members'] == 'students':
|
|
courseShowProperties['members'] = 'all'
|
|
elif field == 'students':
|
|
if courseShowProperties['members'] == 'none':
|
|
courseShowProperties['members'] = field
|
|
elif courseShowProperties['members'] == 'teachers':
|
|
courseShowProperties['members'] = 'all'
|
|
elif field in COURSE_FIELDS_CHOICE_MAP:
|
|
courseShowProperties['fields'].append(COURSE_FIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, COURSE_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'skipfields':
|
|
for field in _getFieldsList():
|
|
if field in {'alias', 'aliases'}:
|
|
courseShowProperties['aliases'] = False
|
|
elif field == 'teachers':
|
|
if courseShowProperties['members'] == 'all':
|
|
courseShowProperties['members'] = 'students'
|
|
elif courseShowProperties['members'] == field:
|
|
courseShowProperties['members'] = 'none'
|
|
elif field == 'students':
|
|
if courseShowProperties['members'] == 'all':
|
|
courseShowProperties['members'] = 'teachers'
|
|
elif courseShowProperties['members'] == field:
|
|
courseShowProperties['members'] = 'none'
|
|
elif field in COURSE_FIELDS_CHOICE_MAP:
|
|
if field != 'id':
|
|
courseShowProperties['skips'].append(COURSE_FIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, COURSE_FIELDS_CHOICE_MAP, True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _setCourseFields(courseShowProperties, pagesMode, getOwnerId=False):
|
|
if not courseShowProperties['fields']:
|
|
return None
|
|
courseShowProperties['fields'].append('id')
|
|
if courseShowProperties['ownerEmail'] or getOwnerId:
|
|
courseShowProperties['fields'].append('ownerId')
|
|
if not pagesMode:
|
|
return ','.join(set(courseShowProperties['fields']))
|
|
return f'nextPageToken,courses({",".join(set(courseShowProperties["fields"]))})'
|
|
|
|
def _convertCourseUserIdToEmail(croom, userId, emails, entityValueList, i, count):
|
|
userEmail = emails.get(userId)
|
|
if userEmail is None:
|
|
try:
|
|
userEmail = callGAPI(croom.userProfiles(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userId=userId, fields='emailAddress').get('emailAddress')
|
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.badRequest, GAPI.forbidden, GAPI.serviceNotAvailable):
|
|
pass
|
|
if userEmail is None:
|
|
entityDoesNotHaveItemWarning(entityValueList, i, count)
|
|
userEmail = 'Unknown user'
|
|
emails[userId] = userEmail
|
|
return userEmail
|
|
|
|
def _getCoursesOwnerInfo(croom, courseIds, useOwnerAccess, addCIIdScope=True):
|
|
coursesInfo = {}
|
|
for courseId in courseIds:
|
|
ciCourseId = courseId
|
|
courseId = addCourseIdScope(courseId)
|
|
if addCIIdScope:
|
|
ciCourseId = courseId
|
|
if courseId not in coursesInfo:
|
|
try:
|
|
course = callGAPI(croom.courses(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
id=courseId, fields='name,ownerId')
|
|
if useOwnerAccess:
|
|
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{course["ownerId"]}')
|
|
else:
|
|
ocroom = croom
|
|
if ocroom is not None:
|
|
coursesInfo[ciCourseId] = {'name': course['name'], 'croom': ocroom}
|
|
except GAPI.notFound:
|
|
entityDoesNotExistWarning(Ent.COURSE, courseId)
|
|
except (GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, courseId], str(e))
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
return 0, len(coursesInfo), coursesInfo
|
|
|
|
def _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, showGettings=False, i=0, count=0):
|
|
aliases = []
|
|
teachers = []
|
|
students = []
|
|
if not showGettings:
|
|
pageMessage = None
|
|
if courseShowProperties.get('aliases'):
|
|
if showGettings:
|
|
printGettingEntityItemForWhom(Ent.ALIAS, formatKeyValueList('', [Ent.Singular(Ent.COURSE), courseId], currentCount(i, count)))
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
aliases = callGAPIpages(croom.courses().aliases(), 'list', 'aliases',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE, GAPI.NOT_IMPLEMENTED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
except (GAPI.notFound, GAPI.serviceNotAvailable, GAPI.notImplemented):
|
|
pass
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
if courseShowProperties['members'] != 'none':
|
|
if courseShowProperties['members'] != 'students':
|
|
if showGettings:
|
|
printGettingEntityItemForWhom(Ent.TEACHER, formatKeyValueList('', [Ent.Singular(Ent.COURSE), courseId], currentCount(i, count)))
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
teachers = callGAPIpages(ocroom.courses().teachers(), 'list', 'teachers',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, fields=teachersFields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
except (GAPI.notFound, GAPI.serviceNotAvailable):
|
|
pass
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
if courseShowProperties['members'] != 'teachers':
|
|
if showGettings:
|
|
printGettingEntityItemForWhom(Ent.STUDENT, formatKeyValueList('', [Ent.Singular(Ent.COURSE), courseId], currentCount(i, count)))
|
|
pageMessage = getPageMessage()
|
|
try:
|
|
students = callGAPIpages(ocroom.courses().students(), 'list', 'students',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, fields=studentsFields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
except (GAPI.notFound, GAPI.serviceNotAvailable):
|
|
pass
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
return (aliases, teachers, students)
|
|
|
|
def _doInfoCourses(courseIdList):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
courseShowProperties = _initCourseShowProperties()
|
|
courseShowProperties['ownerEmail'] = True
|
|
ownerEmails = {}
|
|
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getCourseShowProperties(myarg, courseShowProperties):
|
|
pass
|
|
elif myarg in OWNER_ACCESS_OPTIONS:
|
|
useOwnerAccess = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = _setCourseFields(courseShowProperties, False)
|
|
if courseShowProperties['members'] != 'none':
|
|
if courseShowProperties['countsOnly']:
|
|
teachersFields = 'nextPageToken,teachers(profile(id))'
|
|
studentsFields = 'nextPageToken,students(profile(id))'
|
|
else:
|
|
teachersFields = 'nextPageToken,teachers(profile)'
|
|
studentsFields = 'nextPageToken,students(profile)'
|
|
else:
|
|
teachersFields = studentsFields = None
|
|
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, useOwnerAccess)
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
i += 1
|
|
try:
|
|
course = callGAPI(croom.courses(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
id=courseId, fields=fields)
|
|
if courseShowProperties['ownerEmail']:
|
|
course['ownerEmail'] = _convertCourseUserIdToEmail(croom, course['ownerId'], ownerEmails,
|
|
[Ent.COURSE, course['id'], Ent.OWNER_ID, course['ownerId']], i, count)
|
|
aliases, teachers, students = _getCourseAliasesMembers(croom, courseInfo['croom'], courseId, courseShowProperties, teachersFields, studentsFields)
|
|
if FJQC.formatJSON:
|
|
if courseShowProperties['aliases']:
|
|
course.update({'aliases': list(aliases)})
|
|
if courseShowProperties['members'] != 'none':
|
|
if courseShowProperties['members'] != 'students':
|
|
if not courseShowProperties['countsOnly']:
|
|
course.update({'teachers': list(teachers)})
|
|
else:
|
|
course.update({'teachers': len(teachers)})
|
|
if courseShowProperties['members'] != 'teachers':
|
|
if not courseShowProperties['countsOnly']:
|
|
course.update({'students': list(students)})
|
|
else:
|
|
course.update({'students': len(students)})
|
|
printLine(json.dumps(cleanJSON(course, skipObjects=courseShowProperties['skips'],
|
|
timeObjects=COURSE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
continue
|
|
printEntity([Ent.COURSE, course['id']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, course, courseShowProperties['skips'], COURSE_TIME_OBJECTS)
|
|
if courseShowProperties['aliases']:
|
|
printKeyValueList(['Aliases', len(aliases)])
|
|
Ind.Increment()
|
|
for alias in aliases:
|
|
printKeyValueList([removeCourseAliasScope(alias['alias'])])
|
|
Ind.Decrement()
|
|
if courseShowProperties['members'] != 'none':
|
|
printKeyValueList(['Participants', None])
|
|
Ind.Increment()
|
|
if courseShowProperties['members'] != 'students':
|
|
if teachers:
|
|
printKeyValueList(['Teachers', len(teachers)])
|
|
if not courseShowProperties['countsOnly']:
|
|
Ind.Increment()
|
|
for teacher in teachers:
|
|
if 'emailAddress' in teacher['profile']:
|
|
printKeyValueList([f'{teacher["profile"]["name"]["fullName"]} - {teacher["profile"]["emailAddress"]}'])
|
|
else:
|
|
printKeyValueList([teacher['profile']['name']['fullName']])
|
|
Ind.Decrement()
|
|
if courseShowProperties['members'] != 'teachers':
|
|
if students:
|
|
printKeyValueList(['Students', len(students)])
|
|
if not courseShowProperties['countsOnly']:
|
|
Ind.Increment()
|
|
for student in students:
|
|
if 'emailAddress' in student['profile']:
|
|
printKeyValueList([f'{student["profile"]["name"]["fullName"]} - {student["profile"]["emailAddress"]}'])
|
|
else:
|
|
printKeyValueList([student['profile']['name']['fullName']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
|
|
# gam info courses <CourseEntity> [owneraccess]
|
|
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
|
|
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
|
|
def doInfoCourses():
|
|
_doInfoCourses(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
|
|
|
|
# gam info course <CourseID> [owneraccess]
|
|
# [owneremail] [alias|aliases] [show none|all|students|teachers] [countsonly]
|
|
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>] [formatjson]
|
|
def doInfoCourse():
|
|
_doInfoCourses(getStringReturnInList(Cmd.OB_COURSE_ID))
|
|
|
|
def _initCourseSelectionParameters():
|
|
return {'courseIds': [], 'teacherId': None, 'studentId': None, 'courseStates': []}
|
|
|
|
def _getCourseSelectionParameters(myarg, courseSelectionParameters):
|
|
if myarg in {'course', 'courses', 'class', 'classes'}:
|
|
courseSelectionParameters['courseIds'].extend(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
|
|
elif myarg == 'teacher':
|
|
courseSelectionParameters['teacherId'] = getEmailAddress()
|
|
elif myarg == 'student':
|
|
courseSelectionParameters['studentId'] = getEmailAddress()
|
|
elif myarg in {'state', 'states', 'status'}:
|
|
_getCourseStates(Cmd.OB_COURSE_STATE_LIST, courseSelectionParameters['courseStates'])
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
COURSE_CU_FILTER_FIELDS_MAP = {'creationtime': 'creationTime', 'updatetime': 'updateTime'}
|
|
COURSE_CUS_FILTER_FIELDS_MAP = {'creationtime': 'creationTime', 'updatetime': 'updateTime', 'scheduledtime': 'scheduledTime'}
|
|
COURSE_U_FILTER_FIELDS_MAP = {'updatetime': 'updateTime'}
|
|
COURSE_START_ARGUMENTS = ['start', 'startdate', 'oldestdate']
|
|
COURSE_END_ARGUMENTS = ['end', 'enddate']
|
|
|
|
def _initCourseItemFilter():
|
|
return {'timefilter': None, 'startTime': None, 'endTime': None}
|
|
|
|
def _getCourseItemFilter(myarg, courseItemFilter, courseFilterFields):
|
|
if myarg == 'timefilter':
|
|
courseItemFilter['timefilter'] = getChoice(courseFilterFields, mapChoice=True)
|
|
elif myarg in COURSE_START_ARGUMENTS:
|
|
courseItemFilter['startTime'], _, _ = getTimeOrDeltaFromNow(True)
|
|
elif myarg in COURSE_END_ARGUMENTS:
|
|
courseItemFilter['endTime'], _, _ = getTimeOrDeltaFromNow(True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _setApplyCourseItemFilter(courseItemFilter, fieldsList):
|
|
if courseItemFilter['timefilter'] and (courseItemFilter['startTime'] or courseItemFilter['endTime']):
|
|
if fieldsList:
|
|
fieldsList.append(courseItemFilter['timefilter'])
|
|
return True
|
|
return False
|
|
|
|
def _courseItemPassesFilter(item, courseItemFilter):
|
|
timeStr = item.get(courseItemFilter['timefilter'])
|
|
if not timeStr:
|
|
return False
|
|
startTime = courseItemFilter['startTime']
|
|
endTime = courseItemFilter['endTime']
|
|
timeValue, _ = iso8601.parse_date(timeStr)
|
|
return ((startTime is None) or (timeValue >= startTime)) and ((endTime is None) or (timeValue <= endTime))
|
|
|
|
def _gettingCoursesQuery(courseSelectionParameters):
|
|
query = ''
|
|
if courseSelectionParameters['teacherId']:
|
|
query += f'{Ent.Singular(Ent.TEACHER)}: {courseSelectionParameters["teacherId"]}, '
|
|
if courseSelectionParameters['studentId']:
|
|
query += f'{Ent.Singular(Ent.STUDENT)}: {courseSelectionParameters["studentId"]}, '
|
|
if courseSelectionParameters['courseStates']:
|
|
query += f'{Ent.Choose(Ent.COURSE_STATE, len(courseSelectionParameters["courseStates"]))}: {",".join(courseSelectionParameters["courseStates"])}, '
|
|
if query:
|
|
query = query[:-2]
|
|
return query
|
|
|
|
def _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, getOwnerId=False):
|
|
if not courseSelectionParameters['courseIds']:
|
|
fields = _setCourseFields(courseShowProperties, True, getOwnerId)
|
|
printGettingAllAccountEntities(Ent.COURSE, _gettingCoursesQuery(courseSelectionParameters))
|
|
try:
|
|
return callGAPIpages(croom.courses(), 'list', 'courses',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
teacherId=courseSelectionParameters['teacherId'],
|
|
studentId=courseSelectionParameters['studentId'],
|
|
courseStates=courseSelectionParameters['courseStates'],
|
|
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
except (GAPI.invalid, GAPI.notFound):
|
|
if (not courseSelectionParameters['studentId']) and courseSelectionParameters['teacherId']:
|
|
entityUnknownWarning(Ent.TEACHER, courseSelectionParameters['teacherId'])
|
|
elif (not courseSelectionParameters['teacherId']) and courseSelectionParameters['studentId']:
|
|
entityUnknownWarning(Ent.STUDENT, courseSelectionParameters['studentId'])
|
|
elif courseSelectionParameters['studentId'] and courseSelectionParameters['teacherId']:
|
|
entityOrEntityUnknownWarning(Ent.TEACHER, courseSelectionParameters['teacherId'], Ent.STUDENT, courseSelectionParameters['studentId'])
|
|
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.badRequest, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, None], str(e))
|
|
return None
|
|
fields = _setCourseFields(courseShowProperties, False, getOwnerId)
|
|
coursesInfo = []
|
|
for courseId in courseSelectionParameters['courseIds']:
|
|
courseId = addCourseIdScope(courseId)
|
|
try:
|
|
info = callGAPI(croom.courses(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
id=courseId, fields=fields)
|
|
coursesInfo.append(info)
|
|
except GAPI.notFound:
|
|
entityDoesNotExistWarning(Ent.COURSE, courseId)
|
|
except GAPI.serviceNotAvailable as e:
|
|
entityActionFailedWarning([Ent.COURSE, courseId], str(e))
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
return coursesInfo
|
|
|
|
# gam print courses [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
|
# [owneremail] [owneremailmatchpattern <REMatchPattern>]
|
|
# [alias|aliases|aliasesincolumns [delimiter <Character>]]
|
|
# [show none|all|students|teachers] [countsonly]
|
|
# [fields <CourseFieldNameList>] [skipfields <CourseFieldNameList>]
|
|
# [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
|
# [showitemcountonly] [formatjson [quotechar <Character>]]
|
|
def doPrintCourses():
|
|
def _saveParticipants(course, participants, role, rtitles):
|
|
jcount = len(participants)
|
|
course[role] = jcount
|
|
if courseShowProperties['countsOnly']:
|
|
return
|
|
j = 0
|
|
for member in participants:
|
|
memberTitles = []
|
|
prefix = f'{role}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{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)
|
|
for title in memberTitles:
|
|
if title not in rtitles['set']:
|
|
rtitles['set'].add(title)
|
|
rtitles['list'].append(title)
|
|
j += 1
|
|
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
csvPF = CSVPrintFile('id')
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
courseSelectionParameters = _initCourseSelectionParameters()
|
|
courseItemFilter = _initCourseItemFilter()
|
|
courseShowProperties = _initCourseShowProperties()
|
|
ownerEmails = {}
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
showItemCountOnly = False
|
|
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
|
|
pass
|
|
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_CU_FILTER_FIELDS_MAP):
|
|
pass
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif _getCourseShowProperties(myarg, courseShowProperties):
|
|
pass
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, None)
|
|
if applyCourseItemFilter:
|
|
if courseShowProperties['fields']:
|
|
courseShowProperties['fields'].append(courseItemFilter['timefilter'])
|
|
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, useOwnerAccess)
|
|
if coursesInfo is None:
|
|
if showItemCountOnly:
|
|
writeStdout('0\n')
|
|
return
|
|
if courseShowProperties['aliases']:
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles('JSON-aliases')
|
|
if courseShowProperties['members'] != 'none':
|
|
ttitles = {'set': set(), 'list': []}
|
|
stitles = {'set': set(), 'list': []}
|
|
if courseShowProperties['members'] != 'students':
|
|
ttitles['set'].add('teachers')
|
|
ttitles['list'].append('teachers')
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles('JSON-teachers')
|
|
if courseShowProperties['members'] != 'teachers':
|
|
stitles['set'].add('students')
|
|
stitles['list'].append('students')
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles('JSON-students')
|
|
if courseShowProperties['countsOnly']:
|
|
teachersFields = 'nextPageToken,teachers(profile(id))'
|
|
studentsFields = 'nextPageToken,students(profile(id))'
|
|
else:
|
|
teachersFields = 'nextPageToken,teachers(profile)'
|
|
studentsFields = 'nextPageToken,students(profile)'
|
|
else:
|
|
teachersFields = studentsFields = None
|
|
itemCount = 0
|
|
count = len(coursesInfo)
|
|
i = 0
|
|
for course in coursesInfo:
|
|
i += 1
|
|
if applyCourseItemFilter and not _courseItemPassesFilter(course, courseItemFilter):
|
|
continue
|
|
for field in courseShowProperties['skips']:
|
|
course.pop(field, None)
|
|
courseId = course['id']
|
|
if useOwnerAccess:
|
|
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{course["ownerId"]}')
|
|
if not ocroom:
|
|
continue
|
|
else:
|
|
ocroom = croom
|
|
if courseShowProperties['ownerEmail']:
|
|
course['ownerEmail'] = _convertCourseUserIdToEmail(croom, course['ownerId'], ownerEmails,
|
|
[Ent.COURSE, courseId, Ent.OWNER_ID, course['ownerId']], i, count)
|
|
if courseShowProperties['ownerEmailMatchPattern'] and not courseShowProperties['ownerEmailMatchPattern'].match(course['ownerEmail']):
|
|
continue
|
|
if showItemCountOnly:
|
|
itemCount += 1
|
|
continue
|
|
aliases, teachers, students = _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, True, i, count)
|
|
if courseShowProperties['aliases']:
|
|
if not courseShowProperties['aliasesInColumns']:
|
|
course['Aliases'] = delimiter.join([removeCourseAliasScope(alias['alias']) for alias in aliases])
|
|
else:
|
|
course['Aliases'] = [removeCourseAliasScope(alias['alias']) for alias in aliases]
|
|
if courseShowProperties['members'] != 'none':
|
|
if courseShowProperties['members'] != 'students':
|
|
_saveParticipants(course, teachers, 'teachers', ttitles)
|
|
if courseShowProperties['members'] != 'teachers':
|
|
_saveParticipants(course, students, 'students', stitles)
|
|
row = flattenJSON(course, timeObjects=COURSE_TIME_OBJECTS, noLenObjects=COURSE_NOLEN_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'id': courseId, 'JSON': json.dumps(cleanJSON(course, timeObjects=COURSE_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)}
|
|
if courseShowProperties['aliases']:
|
|
row['JSON-aliases'] = json.dumps(list(aliases))
|
|
if courseShowProperties['members'] != 'none':
|
|
if courseShowProperties['members'] != 'students':
|
|
if not courseShowProperties['countsOnly']:
|
|
row['JSON-teachers'] = json.dumps(list(teachers))
|
|
else:
|
|
row['JSON-teachers'] = json.dumps(len(teachers))
|
|
if courseShowProperties['members'] != 'teachers':
|
|
if not courseShowProperties['countsOnly']:
|
|
row['JSON-students'] = json.dumps(list(students))
|
|
else:
|
|
row['JSON-students'] = json.dumps(len(students))
|
|
csvPF.WriteRowNoFilter(row)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{itemCount}\n')
|
|
return
|
|
if not FJQC.formatJSON:
|
|
if courseShowProperties['aliases']:
|
|
csvPF.AddTitles('Aliases')
|
|
csvPF.SetSortTitles(COURSE_PROPERTY_PRINT_ORDER)
|
|
if courseShowProperties['aliases'] and courseShowProperties['aliasesInColumns']:
|
|
csvPF.FixCourseAliasesTitles()
|
|
csvPF.SortTitles()
|
|
csvPF.SetSortTitles([])
|
|
if courseShowProperties['members'] != 'none':
|
|
csvPF.RearrangeCourseTitles(ttitles, stitles)
|
|
csvPF.writeCSVfile('Courses')
|
|
|
|
def _printCourseItemCount(course, results, title, applyCourseItemFilter, courseItemFilter, csvPF):
|
|
if applyCourseItemFilter:
|
|
numItems = 0
|
|
for item in results:
|
|
if _courseItemPassesFilter(item, courseItemFilter):
|
|
numItems += 1
|
|
else:
|
|
numItems = len(results)
|
|
if csvPF:
|
|
csvPF.WriteRowTitles({'courseId': course['id'], 'courseName': course['name'], title: numItems})
|
|
return numItems
|
|
|
|
COURSE_ANNOUNCEMENTS_FIELDS_CHOICE_MAP = {
|
|
'alternatelink': 'alternateLink',
|
|
'announcementid': 'id',
|
|
'assigneemode': 'assigneeMode',
|
|
'courseid': 'courseId',
|
|
'courseannouncementid': 'id',
|
|
'creationtime': 'creationTime',
|
|
'creator': 'creatorUserId',
|
|
'creatoruserid': 'creatorUserId',
|
|
'id': 'id',
|
|
'individualstudentsoptions': 'individualStudentsOptions',
|
|
'materials': 'materials',
|
|
'scheduledtime': 'scheduledTime',
|
|
'state': 'state',
|
|
'text': 'text',
|
|
'updatetime': 'updateTime',
|
|
}
|
|
COURSE_ANNOUNCEMENTS_ORDERBY_CHOICE_MAP = {
|
|
'updatetime': 'updateTime',
|
|
'updatedate': 'updateTime',
|
|
}
|
|
COURSE_ANNOUNCEMENTS_TIME_OBJECTS = {'creationTime', 'scheduledTime', 'updateTime'}
|
|
COURSE_ANNOUNCEMENTS_SORT_TITLES = ['courseId', 'courseName', 'id', 'text', 'state']
|
|
COURSE_ANNOUNCEMENTS_INDEXED_TITLES = ['materials']
|
|
|
|
# gam print course-announcements [todrive <ToDriveAttribute>*]
|
|
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
|
# (announcementids <CourseAnnouncementIDEntity>)|((announcementstates <CourseAnnouncementStateList>)*
|
|
# (orderby <CourseAnnouncementOrderByFieldName> [ascending|descending])*)
|
|
# [showcreatoremails|creatoremail] [fields <CourseAnnouncementFieldNameList>]
|
|
# [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
|
# [countsonly] [formatjson [quotechar <Character>]]
|
|
def doPrintCourseAnnouncements():
|
|
def _printCourseAnnouncement(course, courseAnnouncement, i, count):
|
|
if applyCourseItemFilter and not _courseItemPassesFilter(courseAnnouncement, courseItemFilter):
|
|
return
|
|
if showCreatorEmail:
|
|
courseAnnouncement['creatorUserEmail'] = _convertCourseUserIdToEmail(croom, courseAnnouncement['creatorUserId'], creatorEmails,
|
|
[Ent.COURSE, course['id'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncement['id'],
|
|
Ent.CREATOR_ID, courseAnnouncement['creatorUserId']], i, count)
|
|
row = flattenJSON(courseAnnouncement, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_ANNOUNCEMENTS_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
|
|
'JSON': json.dumps(cleanJSON(courseAnnouncement, timeObjects=COURSE_ANNOUNCEMENTS_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
csvPF = CSVPrintFile(['courseId', 'courseName'], COURSE_ANNOUNCEMENTS_SORT_TITLES, COURSE_ANNOUNCEMENTS_INDEXED_TITLES)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
courseSelectionParameters = _initCourseSelectionParameters()
|
|
courseItemFilter = _initCourseItemFilter()
|
|
courseShowProperties = _initCourseShowProperties(['name'])
|
|
courseAnnouncementIds = []
|
|
courseAnnouncementStates = []
|
|
OBY = OrderBy(COURSE_ANNOUNCEMENTS_ORDERBY_CHOICE_MAP)
|
|
creatorEmails = {}
|
|
countsOnly = showCreatorEmail = False
|
|
items = 'courseAnnouncements'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
|
|
pass
|
|
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_CUS_FILTER_FIELDS_MAP):
|
|
pass
|
|
elif myarg in {'announcementid', 'announcementids'}:
|
|
courseAnnouncementIds = getEntityList(Cmd.OB_COURSE_ANNOUNCEMENT_ID_ENTITY)
|
|
elif myarg in {'announcementstate', 'announcementstates'}:
|
|
_getCourseStates(Cmd.OB_COURSE_ANNOUNCEMENT_STATE_LIST, courseAnnouncementStates)
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg in {'showcreatoremails', 'creatoremail'}:
|
|
showCreatorEmail = True
|
|
elif getFieldsList(myarg, COURSE_ANNOUNCEMENTS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
|
pass
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
csvPF.AddTitles(items)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
|
|
if coursesInfo is None:
|
|
return
|
|
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
|
|
if showCreatorEmail and fieldsList:
|
|
fieldsList.append('creatorUserId')
|
|
courseAnnouncementIdsLists = courseAnnouncementIds if isinstance(courseAnnouncementIds, dict) else None
|
|
i = 0
|
|
count = len(coursesInfo)
|
|
for course in coursesInfo:
|
|
i += 1
|
|
courseId = course['id']
|
|
if courseAnnouncementIdsLists:
|
|
courseAnnouncementIds = courseAnnouncementIdsLists[courseId]
|
|
if not courseAnnouncementIds:
|
|
fields = getItemFieldsFromFieldsList('announcements', fieldsList)
|
|
printGettingAllEntityItemsForWhom(Ent.COURSE_ANNOUNCEMENT_ID, Ent.TypeName(Ent.COURSE, courseId), i, count,
|
|
_gettingCourseEntityQuery(Ent.COURSE_ANNOUNCEMENT_STATE, courseAnnouncementStates))
|
|
try:
|
|
results = callGAPIpages(croom.courses().announcements(), 'list', 'announcements',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, announcementStates=courseAnnouncementStates, orderBy=OBY.orderBy,
|
|
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
if not countsOnly:
|
|
for courseAnnouncement in results:
|
|
_printCourseAnnouncement(course, courseAnnouncement, i, count)
|
|
else:
|
|
_printCourseItemCount(course, results, items, applyCourseItemFilter, courseItemFilter, csvPF)
|
|
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
|
|
else:
|
|
jcount = len(courseAnnouncementIds)
|
|
if jcount == 0:
|
|
continue
|
|
fields = f'{",".join(set(fieldsList))}' if fieldsList else None
|
|
j = 0
|
|
for courseAnnouncementId in courseAnnouncementIds:
|
|
j += 1
|
|
try:
|
|
courseAnnouncement = callGAPI(croom.courses().announcements(), 'get',
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, id=courseAnnouncementId, fields=fields)
|
|
_printCourseAnnouncement(course, courseAnnouncement, i, count)
|
|
except GAPI.notFound:
|
|
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncementId], j, jcount)
|
|
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_ANNOUNCEMENT_ID, courseAnnouncementId], str(e), j, jcount)
|
|
csvPF.writeCSVfile('Course Announcements')
|
|
|
|
COURSE_TOPICS_TIME_OBJECTS = {'updateTime'}
|
|
COURSE_TOPICS_SORT_TITLES = ['courseId', 'courseName', 'topicId', 'name', 'updateTime']
|
|
|
|
# gam print course-topics [todrive <ToDriveAttribute>*]
|
|
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
|
# [topicids <CourseTopicIDEntity>]
|
|
# [timefilter updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
|
# [countsonly] [formatjson [quotechar <Character>]]
|
|
def doPrintCourseTopics():
|
|
def _printCourseTopic(course, courseTopic):
|
|
if applyCourseItemFilter and not _courseItemPassesFilter(courseTopic, courseItemFilter):
|
|
return
|
|
row = flattenJSON(courseTopic, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_TOPICS_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
|
|
'JSON': json.dumps(cleanJSON(courseTopic, timeObjects=COURSE_TOPICS_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
csvPF = CSVPrintFile(['courseId', 'courseName'], COURSE_TOPICS_SORT_TITLES)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = ['topicId', 'name', 'updateTime']
|
|
courseSelectionParameters = _initCourseSelectionParameters()
|
|
courseItemFilter = _initCourseItemFilter()
|
|
courseShowProperties = _initCourseShowProperties(['name'])
|
|
courseTopicIds = []
|
|
countsOnly = False
|
|
items = 'courseTopics'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
|
|
pass
|
|
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_U_FILTER_FIELDS_MAP):
|
|
pass
|
|
elif myarg in {'topicid', 'topicids'}:
|
|
courseTopicIds = getEntityList(Cmd.OB_COURSE_TOPIC_ID_ENTITY)
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
csvPF.AddTitles(items)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
|
|
if coursesInfo is None:
|
|
return
|
|
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
|
|
courseTopicIdsLists = courseTopicIds if isinstance(courseTopicIds, dict) else None
|
|
i = 0
|
|
count = len(coursesInfo)
|
|
for course in coursesInfo:
|
|
i += 1
|
|
courseId = course['id']
|
|
if courseTopicIdsLists:
|
|
courseTopicIds = courseTopicIdsLists[courseId]
|
|
if not courseTopicIds:
|
|
fields = getItemFieldsFromFieldsList('topic', fieldsList)
|
|
printGettingAllEntityItemsForWhom(Ent.COURSE_TOPIC, Ent.TypeName(Ent.COURSE, courseId), i, count)
|
|
try:
|
|
results = callGAPIpages(croom.courses().topics(), 'list', 'topic',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId,
|
|
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
if not countsOnly:
|
|
for courseTopic in results:
|
|
_printCourseTopic(course, courseTopic)
|
|
else:
|
|
_printCourseItemCount(course, results, items, applyCourseItemFilter, courseItemFilter, csvPF)
|
|
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
|
|
else:
|
|
jcount = len(courseTopicIds)
|
|
if jcount == 0:
|
|
continue
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
j = 0
|
|
for courseTopicId in courseTopicIds:
|
|
j += 1
|
|
try:
|
|
courseTopic = callGAPI(croom.courses().topics(), 'get',
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, id=courseTopicId, fields=fields)
|
|
_printCourseTopic(course, courseTopic)
|
|
except GAPI.notFound:
|
|
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_TOPIC_ID, courseTopicId], j, jcount)
|
|
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_TOPIC_ID, courseTopicId], str(e), j, jcount)
|
|
csvPF.writeCSVfile('Course Topics')
|
|
|
|
def _initCourseWMSelectionParameters():
|
|
return {'courseWMIds': [], 'courseWMStates': []}
|
|
|
|
def _getCourseWMSelectionParameters(myarg, courseWMSelectionParameters,
|
|
IDArguments, OBIDEntity,
|
|
StateArguments, OBStateList):
|
|
if myarg in IDArguments:
|
|
courseWMSelectionParameters['courseWMIds'] = getEntityList(OBIDEntity)
|
|
elif myarg in StateArguments:
|
|
_getCourseStates(OBStateList, courseWMSelectionParameters['courseWMStates'])
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
COURSE_MATERIAL_ID_ARGUMENTS = {'materialid', 'materialids', 'coursematerialid', 'coursematerialids'}
|
|
COURSE_MATERIAL_STATE_ARGUMENTS = {'materialstate', 'materialstates', 'coursematerialstate', 'coursematerialstates'}
|
|
COURSE_MATERIAL_FIELDS_CHOICE_MAP = {
|
|
'alternatelink': 'alternateLink',
|
|
'assigneemode': 'assigneeMode',
|
|
'courseid': 'courseId',
|
|
'coursematerialid': 'id',
|
|
'creationtime': 'creationTime',
|
|
'creator': 'creatorUserId',
|
|
'creatoruserid': 'creatorUserId',
|
|
'description': 'description',
|
|
'id': 'id',
|
|
'individualstudentsoptions': 'individualStudentsOptions',
|
|
'materialid': 'id',
|
|
'materials': 'materials',
|
|
'scheduledtime': 'scheduledTime',
|
|
'state': 'state',
|
|
'title': 'title',
|
|
'topicid': 'topicId',
|
|
'updatetime': 'updateTime',
|
|
}
|
|
COURSE_MATERIAL_ORDERBY_CHOICE_MAP = {'updatetime': 'updateTime', 'updatedate': 'updateTime'}
|
|
COURSE_MATERIAL_TIME_OBJECTS = {'creationTime', 'scheduledTime', 'updateTime'}
|
|
COURSE_MATERIAL_SORT_TITLES = ['courseId', 'courseName', 'id', 'title', 'description', 'state']
|
|
COURSE_MATERIAL_INDEXED_TITLES = ['materials']
|
|
|
|
COURSE_WORK_ID_ARGUMENTS = {'workid', 'workids', 'courseworkid', 'courseworkids'}
|
|
COURSE_WORK_STATE_ARGUMENTS = {'workstate', 'workstates', 'courseworkstate', 'courseworkstates'}
|
|
COURSE_WORK_FIELDS_CHOICE_MAP = {
|
|
'alternatelink': 'alternateLink',
|
|
'assigneemode': 'assigneeMode',
|
|
'courseid': 'courseId',
|
|
'courseworkid': 'id',
|
|
'courseworktype': 'workType',
|
|
'creationtime': 'creationTime',
|
|
'creator': 'creatorUserId',
|
|
'creatoruserid': 'creatorUserId',
|
|
'description': 'description',
|
|
'duedate': 'dueDate',
|
|
'duetime': 'dueTime',
|
|
'id': 'id',
|
|
'individualstudentsoptions': 'individualStudentsOptions',
|
|
'materials': 'materials',
|
|
'maxpoints': 'maxPoints',
|
|
'scheduledtime': 'scheduledTime',
|
|
'state': 'state',
|
|
'submissionmodificationmode': 'submissionModificationMode',
|
|
'title': 'title',
|
|
'topicid': 'topicId',
|
|
'updatetime': 'updateTime',
|
|
'workid': 'id',
|
|
'worktype': 'workType',
|
|
}
|
|
COURSE_WORK_ORDERBY_CHOICE_MAP = {'duedate': 'dueDate', 'updatetime': 'updateTime', 'updatedate': 'updateTime'}
|
|
COURSE_WORK_TIME_OBJECTS = {'creationTime', 'scheduledTime', 'updateTime'}
|
|
COURSE_WORK_SORT_TITLES = ['courseId', 'courseName', 'id', 'title', 'description', 'state']
|
|
COURSE_WORK_INDEXED_TITLES = ['materials']
|
|
|
|
def doPrintCourseWM(entityIDType, entityStateType):
|
|
def _getTopicNames(croom, courseId):
|
|
topicNames = {}
|
|
try:
|
|
results = callGAPIpages(croom.courses().topics(), 'list', 'topic',
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId,
|
|
fields='nextPageToken,topic(topicId,name)', pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
for courseTopic in results:
|
|
topicNames[courseTopic['topicId']] = courseTopic['name']
|
|
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable):
|
|
pass
|
|
return topicNames
|
|
|
|
def _printCourseWM(course, courseWM, i, count):
|
|
if applyCourseItemFilter and not _courseItemPassesFilter(courseWM, courseItemFilter):
|
|
return
|
|
if showCreatorEmail:
|
|
courseWM['creatorUserEmail'] = _convertCourseUserIdToEmail(croom, courseWM['creatorUserId'], creatorEmails,
|
|
[Ent.COURSE, course['id'], entityIDType, courseWM['id'],
|
|
Ent.CREATOR_ID, courseWM['creatorUserId']], i, count)
|
|
if showTopicNames:
|
|
topicId = courseWM.get('topicId')
|
|
if topicId:
|
|
courseWM['topicName'] = topicNames.get(topicId, topicId)
|
|
row = flattenJSON(courseWM, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=TimeObjects,
|
|
simpleLists=['studentIds'] if showStudentsAsList else None, delimiter=delimiter)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
|
|
'JSON': json.dumps(cleanJSON(courseWM, timeObjects=TimeObjects),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
if entityIDType == Ent.COURSE_WORK_ID:
|
|
SortTitles = COURSE_WORK_SORT_TITLES
|
|
IndexedTitles = COURSE_WORK_INDEXED_TITLES
|
|
TimeObjects = COURSE_WORK_TIME_OBJECTS
|
|
OrderbyChoiceMap = COURSE_WORK_ORDERBY_CHOICE_MAP
|
|
FieldsChoiceMap = COURSE_WORK_FIELDS_CHOICE_MAP
|
|
IDArguments = COURSE_WORK_ID_ARGUMENTS
|
|
OBIDEntity = Cmd.OB_COURSE_WORK_ID_ENTITY
|
|
StateArguments = COURSE_WORK_STATE_ARGUMENTS
|
|
OBStateList = Cmd.OB_COURSE_WORK_STATE_LIST
|
|
CSVTitle = 'Course Work'
|
|
service = croom.courses().courseWork()
|
|
items = 'courseWork'
|
|
else:
|
|
SortTitles = COURSE_MATERIAL_SORT_TITLES
|
|
IndexedTitles = COURSE_MATERIAL_INDEXED_TITLES
|
|
TimeObjects = COURSE_MATERIAL_TIME_OBJECTS
|
|
OrderbyChoiceMap = COURSE_MATERIAL_ORDERBY_CHOICE_MAP
|
|
FieldsChoiceMap = COURSE_MATERIAL_FIELDS_CHOICE_MAP
|
|
IDArguments = COURSE_MATERIAL_ID_ARGUMENTS
|
|
OBIDEntity = Cmd.OB_COURSE_MATERIAL_ID_ENTITY
|
|
StateArguments = COURSE_MATERIAL_STATE_ARGUMENTS
|
|
OBStateList = Cmd.OB_COURSE_MATERIAL_STATE_LIST
|
|
CSVTitle = 'Course Work Material'
|
|
service = croom.courses().courseWorkMaterials()
|
|
items = 'courseWorkMaterial'
|
|
csvPF = CSVPrintFile(['courseId', 'courseName'], SortTitles, IndexedTitles)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
courseSelectionParameters = _initCourseSelectionParameters()
|
|
courseWMSelectionParameters = _initCourseWMSelectionParameters()
|
|
courseItemFilter = _initCourseItemFilter()
|
|
courseShowProperties = _initCourseShowProperties(['name'])
|
|
OBY = OrderBy(OrderbyChoiceMap)
|
|
creatorEmails = {}
|
|
showCreatorEmail = showTopicNames = False
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
countsOnly = showStudentsAsList = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
|
|
pass
|
|
elif _getCourseWMSelectionParameters(myarg, courseWMSelectionParameters,
|
|
IDArguments, OBIDEntity,
|
|
StateArguments, OBStateList):
|
|
pass
|
|
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_CUS_FILTER_FIELDS_MAP):
|
|
pass
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg in {'showcreatoremails', 'creatoremail'}:
|
|
showCreatorEmail = True
|
|
elif myarg == 'showtopicnames':
|
|
showTopicNames = True
|
|
elif getFieldsList(myarg, FieldsChoiceMap, fieldsList, initialField='id'):
|
|
pass
|
|
elif myarg == 'showstudentsaslist':
|
|
showStudentsAsList = getBoolean()
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
csvPF.AddTitles(items)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if showCreatorEmail and fieldsList:
|
|
fieldsList.append('creatorUserId')
|
|
if showTopicNames and fieldsList:
|
|
fieldsList.append('topicId')
|
|
if entityIDType == Ent.COURSE_WORK_ID:
|
|
kwargs = {'courseWorkStates': courseWMSelectionParameters['courseWMStates']}
|
|
else:
|
|
kwargs = {'courseWorkMaterialStates': courseWMSelectionParameters['courseWMStates']}
|
|
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
|
|
if coursesInfo is None:
|
|
return
|
|
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
|
|
courseWMIds = courseWMSelectionParameters['courseWMIds']
|
|
courseWMIdsLists = courseWMIds if isinstance(courseWMIds, dict) else {}
|
|
i = 0
|
|
count = len(coursesInfo)
|
|
for course in coursesInfo:
|
|
i += 1
|
|
courseId = course['id']
|
|
if showTopicNames:
|
|
topicNames = _getTopicNames(croom, courseId)
|
|
if courseWMIdsLists:
|
|
courseWMIds = courseWMIdsLists[courseId]
|
|
if not courseWMIds:
|
|
fields = getItemFieldsFromFieldsList(items, fieldsList)
|
|
printGettingAllEntityItemsForWhom(entityIDType, Ent.TypeName(Ent.COURSE, courseId), i, count,
|
|
_gettingCourseEntityQuery(entityStateType, courseWMSelectionParameters['courseWMStates']))
|
|
try:
|
|
results = callGAPIpages(service, 'list', items,
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS,
|
|
courseId=courseId, orderBy=OBY.orderBy,
|
|
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS], **kwargs)
|
|
if not countsOnly:
|
|
for courseWM in results:
|
|
_printCourseWM(course, courseWM, i, count)
|
|
else:
|
|
_printCourseItemCount(course, results, items, applyCourseItemFilter, courseItemFilter, csvPF)
|
|
except (GAPI.notFound, GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
|
|
else:
|
|
jcount = len(courseWMIds)
|
|
if jcount == 0:
|
|
continue
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
j = 0
|
|
for courseWMId in courseWMIds:
|
|
j += 1
|
|
try:
|
|
courseWM = callGAPI(service, 'get',
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS,
|
|
courseId=courseId, id=courseWMId, fields=fields)
|
|
_printCourseWM(course, courseWM, i, count)
|
|
except GAPI.notFound:
|
|
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], entityIDType, courseWMId], j, jcount)
|
|
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], entityIDType, courseWMId], str(e), j, jcount)
|
|
csvPF.writeCSVfile(CSVTitle)
|
|
|
|
# gam print course-materials [todrive <ToDriveAttribute>*]
|
|
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
|
# (materialids <CourseMaterialIDEntity>)|((materialstates <CourseMaterialStateList>)*
|
|
# (orderby <CourseMaterialsOrderByFieldName> [ascending|descending])*)
|
|
# [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseMaterialFieldNameList>]
|
|
# [timefilter creationtime|updatetime|scheduledtime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
|
# [countsonly] [formatjson [quotechar <Character>]]
|
|
def doPrintCourseMaterials():
|
|
doPrintCourseWM(Ent.COURSE_MATERIAL_ID, Ent.COURSE_MATERIAL_STATE)
|
|
|
|
# gam print course-work [todrive <ToDriveAttribute>*]
|
|
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
|
# (workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
|
|
# (orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
|
|
# [showcreatoremails|creatoremail] [showtopicnames] [fields <CourseWorkFieldNameList>]
|
|
# [showstudentsaslist [<Boolean>]] [delimiter <Character>]
|
|
# [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
|
# [countsonly] [formatjson [quotechar <Character>]]
|
|
def doPrintCourseWork():
|
|
doPrintCourseWM(Ent.COURSE_WORK_ID, Ent.COURSE_WORK_STATE)
|
|
|
|
COURSE_SUBMISSION_FIELDS_CHOICE_MAP = {
|
|
'alternatelink': 'alternateLink',
|
|
'assignedgrade': 'assignedGrade',
|
|
'courseid': 'courseId',
|
|
'coursesubmissionid': 'id',
|
|
'courseworkid': 'courseWorkId',
|
|
'courseworktype': 'courseWorkType',
|
|
'creationtime': 'creationTime',
|
|
'draftgrade': 'draftGrade',
|
|
'id': 'id',
|
|
'late': 'late',
|
|
'state': 'state',
|
|
'submissionhistory': 'submissionHistory',
|
|
'submissionid': 'id',
|
|
'updatetime': 'updateTime',
|
|
'userid': 'userId',
|
|
'workid': 'courseWorkId',
|
|
'worktype': 'courseWorkType',
|
|
}
|
|
COURSE_SUBMISSION_TIME_OBJECTS = {'creationTime', 'updateTime', 'gradeTimestamp', 'stateTimestamp'}
|
|
|
|
def _gettingCourseSubmissionQuery(courseSubmissionStates, late, userId):
|
|
query = ''
|
|
if courseSubmissionStates:
|
|
query += f'{Ent.Choose(Ent.COURSE_SUBMISSION_STATE, len(courseSubmissionStates))}: {",".join(courseSubmissionStates)}, '
|
|
if late:
|
|
query += f'{late}, '
|
|
if userId:
|
|
query += f'{Ent.Singular(Ent.USER)}: {userId}, '
|
|
if query:
|
|
query = query[:-2]
|
|
return query
|
|
|
|
# gam print course-submissions [todrive <ToDriveAttribute>*]
|
|
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] states <CourseStateList>])
|
|
# (workids <CourseWorkIDEntity>)|((workstates <CourseWorkStateList>)*
|
|
# (orderby <CourseWorkOrderByFieldName> [ascending|descending])*)
|
|
# (submissionids <CourseSubmissionIDEntity>)|((submissionstates <CourseSubmissionStateList>)*) [late|notlate]
|
|
# [fields <CourseSubmissionFieldNameList>] [showuserprofile]
|
|
# [timefilter creationtime|updatetime] [start|starttime <Date>|<Time>] [end|endtime <Date>|<Time>]
|
|
# [countsonly] [formatjson [quotechar <Character>]]
|
|
def doPrintCourseSubmissions():
|
|
def _printCourseSubmission(course, courseSubmission):
|
|
if applyCourseItemFilter and not _courseItemPassesFilter(courseSubmission, courseItemFilter):
|
|
return
|
|
if showUserProfile:
|
|
userId = courseSubmission.get('userId')
|
|
if userId:
|
|
if userId not in userProfiles:
|
|
try:
|
|
userProfile = callGAPI(tcroom.userProfiles(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userId=userId, fields='emailAddress,name')
|
|
userProfiles[userId] = {'profile': {'emailAddress': userProfile.get('emailAddress', ''), 'name': userProfile['name']}}
|
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.serviceNotAvailable):
|
|
userProfiles[userId] = {'profile': {'emailAddress': '', 'name': {'givenName': '', 'familyName': '', 'fullName': ''}}}
|
|
courseSubmission.update(userProfiles[userId])
|
|
row = flattenJSON(courseSubmission, flattened={'courseId': course['id'], 'courseName': course['name']}, timeObjects=COURSE_SUBMISSION_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'courseId': course['id'], 'courseName': course['name'],
|
|
'JSON': json.dumps(cleanJSON(courseSubmission, timeObjects=COURSE_SUBMISSION_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
csvPF = CSVPrintFile(['courseId', 'courseName'],
|
|
['courseId', 'courseName', 'courseWorkId', 'id', 'userId',
|
|
f'profile{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}emailAddress',
|
|
f'profile{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}givenName',
|
|
f'profile{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}familyName',
|
|
f'profile{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}fullName',
|
|
'state'],
|
|
['submissionHistory'])
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
courseSelectionParameters = _initCourseSelectionParameters()
|
|
courseWMSelectionParameters = _initCourseWMSelectionParameters()
|
|
courseItemFilter = _initCourseItemFilter()
|
|
courseShowProperties = _initCourseShowProperties(['name'])
|
|
courseSubmissionStates = []
|
|
courseSubmissionIds = []
|
|
OBY = OrderBy(COURSE_WORK_ORDERBY_CHOICE_MAP)
|
|
late = None
|
|
userProfiles = {}
|
|
countsOnly = showUserProfile = False
|
|
items = 'courseSubmissions'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
|
|
pass
|
|
elif _getCourseWMSelectionParameters(myarg, courseWMSelectionParameters,
|
|
COURSE_WORK_ID_ARGUMENTS, Cmd.OB_COURSE_WORK_ID_ENTITY,
|
|
COURSE_WORK_STATE_ARGUMENTS, Cmd.OB_COURSE_WORK_STATE_LIST):
|
|
pass
|
|
elif _getCourseItemFilter(myarg, courseItemFilter, COURSE_CU_FILTER_FIELDS_MAP):
|
|
pass
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg in {'submissionid', 'submissionids', 'coursesubmissionid', 'coursesubmissionids'}:
|
|
courseSubmissionIds = getEntityList(Cmd.OB_COURSE_SUBMISSION_ID_ENTITY)
|
|
elif myarg in {'submissionstate', 'submissionstates', 'coursesubmissionstate', 'coursesubmissionstates'}:
|
|
_getCourseStates(Cmd.OB_COURSE_SUBMISSION_STATE_LIST, courseSubmissionStates)
|
|
elif myarg == 'late':
|
|
late = 'LATE_ONLY'
|
|
elif myarg == 'notlate':
|
|
late = 'NOT_LATE_ONLY'
|
|
elif myarg == 'showuserprofile':
|
|
showUserProfile = True
|
|
elif getFieldsList(myarg, COURSE_SUBMISSION_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
|
pass
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
csvPF.AddTitles(items)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, getOwnerId=True)
|
|
if coursesInfo is None:
|
|
return
|
|
applyCourseItemFilter = _setApplyCourseItemFilter(courseItemFilter, fieldsList)
|
|
courseWorkIds = courseWMSelectionParameters['courseWMIds']
|
|
courseWorkIdsLists = courseWorkIds if isinstance(courseWorkIds, dict) else {}
|
|
courseSubmissionIdsLists = courseSubmissionIds if isinstance(courseSubmissionIds, dict) else {}
|
|
i = 0
|
|
count = len(coursesInfo)
|
|
for course in coursesInfo:
|
|
i += 1
|
|
_, tcroom = buildGAPIServiceObject(API.CLASSROOM, f"uid:{course['ownerId']}")
|
|
if tcroom is None:
|
|
continue
|
|
submissionsCount = 0
|
|
courseId = course['id']
|
|
if courseWorkIdsLists:
|
|
courseWorkIds = courseWorkIdsLists[courseId]
|
|
if not courseWorkIds:
|
|
printGettingAllEntityItemsForWhom(Ent.COURSE_WORK_ID, Ent.TypeName(Ent.COURSE, courseId), i, count,
|
|
_gettingCourseEntityQuery(Ent.COURSE_WORK_STATE, courseWMSelectionParameters['courseWMStates']))
|
|
try:
|
|
results = callGAPIpages(croom.courses().courseWork(), 'list', 'courseWork',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, courseWorkStates=courseWMSelectionParameters['courseWMStates'], orderBy=OBY.orderBy,
|
|
fields='nextPageToken,courseWork(id)', pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
courseWorkIdsForCourse = [courseWork['id'] for courseWork in results]
|
|
except GAPI.notFound:
|
|
continue
|
|
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId)], str(e), i, count)
|
|
continue
|
|
else:
|
|
courseWorkIdsForCourse = courseWorkIds
|
|
jcount = len(courseWorkIdsForCourse)
|
|
if jcount == 0:
|
|
continue
|
|
j = 0
|
|
for courseWorkId in courseWorkIdsForCourse:
|
|
j += 1
|
|
if not courseSubmissionIds:
|
|
fields = getItemFieldsFromFieldsList('studentSubmissions', fieldsList)
|
|
printGettingAllEntityItemsForWhom(Ent.COURSE_SUBMISSION_ID, Ent.TypeName(Ent.COURSE_WORK_ID, courseWorkId), j, jcount,
|
|
_gettingCourseSubmissionQuery(courseSubmissionStates, late, courseSelectionParameters['studentId']))
|
|
try:
|
|
results = callGAPIpages(croom.courses().courseWork().studentSubmissions(), 'list', 'studentSubmissions',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, courseWorkId=courseWorkId, states=courseSubmissionStates, late=late, userId=courseSelectionParameters['studentId'],
|
|
fields=fields, pageSize=GC.Values[GC.CLASSROOM_MAX_RESULTS])
|
|
if not countsOnly:
|
|
for submission in results:
|
|
_printCourseSubmission(course, submission)
|
|
else:
|
|
submissionsCount += _printCourseItemCount(course, results, items, applyCourseItemFilter, courseItemFilter, None)
|
|
except GAPI.notFound:
|
|
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_WORK_ID, courseWorkId], j, jcount)
|
|
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_WORK_ID, courseWorkId], str(e), j, jcount)
|
|
else:
|
|
if courseSubmissionIdsLists:
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
courseSubmissionIds = courseSubmissionIdsLists[courseWorkId]
|
|
else:
|
|
courseSubmissionIds = courseSubmissionIdsLists[courseId][courseWorkId]
|
|
kcount = len(courseSubmissionIds)
|
|
if kcount == 0:
|
|
continue
|
|
fields = f'{",".join(set(fieldsList))}' if fieldsList else None
|
|
k = 0
|
|
for courseSubmissionId in courseSubmissionIds:
|
|
k += 1
|
|
try:
|
|
submission = callGAPI(croom.courses().courseWork().studentSubmissions(), 'get',
|
|
throwReasons=GAPI.COURSE_ACCESS_THROW_REASONS+[GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
courseId=courseId, courseWorkId=courseWorkId, id=courseSubmissionId,
|
|
fields=fields)
|
|
_printCourseSubmission(course, submission)
|
|
except GAPI.notFound:
|
|
entityDoesNotHaveItemWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_WORK_ID, courseWorkId, Ent.COURSE_SUBMISSION_ID, courseSubmissionId], k, kcount)
|
|
except (GAPI.insufficientPermissions, GAPI.permissionDenied, GAPI.forbidden, GAPI.invalidArgument, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE_NAME, course['name'], Ent.COURSE_WORK_ID, courseWorkId, Ent.COURSE_SUBMISSION_ID, courseSubmissionId], str(e), k, kcount)
|
|
if countsOnly:
|
|
csvPF.WriteRowTitles({'courseId': course['id'], 'courseName': course['name'], items: submissionsCount})
|
|
csvPF.writeCSVfile('Course Submissions')
|
|
|
|
COURSE_PARTICIPANTS_SORT_TITLES = ['courseId', 'courseName', 'userRole', 'userId']
|
|
|
|
# gam print course-participants [todrive <ToDriveAttribute>*]
|
|
# (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
|
# [show all|students|teachers]
|
|
# [showitemcountonly] [formatjson [quotechar <Character>]]
|
|
def doPrintCourseParticipants():
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
csvPF = CSVPrintFile(['courseId', 'courseName'])
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
courseSelectionParameters = _initCourseSelectionParameters()
|
|
courseShowProperties = _initCourseShowProperties(['name'])
|
|
courseShowProperties['members'] = 'all'
|
|
showItemCountOnly = False
|
|
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
|
|
pass
|
|
elif myarg == 'show':
|
|
courseShowProperties['members'] = getChoice(COURSE_MEMBER_ARGUMENTS)
|
|
elif myarg == 'showitemcountonly':
|
|
showItemCountOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties, useOwnerAccess)
|
|
if coursesInfo is None:
|
|
if showItemCountOnly:
|
|
writeStdout('0\n')
|
|
return
|
|
if courseShowProperties['members'] != 'none':
|
|
if courseShowProperties['members'] != 'students':
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles('JSON-teachers')
|
|
if courseShowProperties['members'] != 'teachers':
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles('JSON-students')
|
|
teachersFields = 'nextPageToken,teachers(userId,profile)'
|
|
studentsFields = 'nextPageToken,students(userId,profile)'
|
|
else:
|
|
teachersFields = studentsFields = None
|
|
itemCount = 0
|
|
count = len(coursesInfo)
|
|
i = 0
|
|
for course in coursesInfo:
|
|
i += 1
|
|
courseId = course['id']
|
|
if useOwnerAccess:
|
|
_, ocroom = buildGAPIServiceObject(API.CLASSROOM, f'uid:{course["ownerId"]}')
|
|
if not ocroom:
|
|
continue
|
|
else:
|
|
ocroom = croom
|
|
_, teachers, students = _getCourseAliasesMembers(croom, ocroom, courseId, courseShowProperties, teachersFields, studentsFields, True, i, count)
|
|
if showItemCountOnly:
|
|
if courseShowProperties['members'] != 'students':
|
|
itemCount += len(teachers)
|
|
if courseShowProperties['members'] != 'teachers':
|
|
itemCount += len(students)
|
|
continue
|
|
if not FJQC.formatJSON:
|
|
if courseShowProperties['members'] != 'none':
|
|
if courseShowProperties['members'] != 'students':
|
|
for member in teachers:
|
|
csvPF.WriteRowTitles(flattenJSON(member, flattened={'courseId': courseId, 'courseName': course['name'], 'userRole': 'TEACHER'}))
|
|
if courseShowProperties['members'] != 'teachers':
|
|
for member in students:
|
|
csvPF.WriteRowTitles(flattenJSON(member, flattened={'courseId': courseId, 'courseName': course['name'], 'userRole': 'STUDENT'}))
|
|
else:
|
|
row = {'courseId': courseId, 'courseName': course['name']}
|
|
if courseShowProperties['members'] != 'none':
|
|
if courseShowProperties['members'] != 'students':
|
|
row['JSON-teachers'] = json.dumps(list(teachers))
|
|
if courseShowProperties['members'] != 'teachers':
|
|
row['JSON-students'] = json.dumps(list(students))
|
|
csvPF.WriteRowNoFilter(row)
|
|
if showItemCountOnly:
|
|
writeStdout(f'{itemCount}\n')
|
|
return
|
|
if not FJQC.formatJSON:
|
|
csvPF.SetSortTitles(COURSE_PARTICIPANTS_SORT_TITLES)
|
|
csvPF.writeCSVfile('Course Participants')
|
|
|
|
def _batchAddItemsToCourse(croom, courseId, i, count, addParticipants, role):
|
|
_ADD_PART_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST,
|
|
GAPI.ALREADY_EXISTS: Msg.DUPLICATE,
|
|
GAPI.FAILED_PRECONDITION: Msg.NOT_ALLOWED}
|
|
def _callbackAddItemsToCourse(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
entityActionPerformed([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if (reason not in {GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE}) and ((reason != GAPI.NOT_FOUND) or (ri[RI_ROLE] == Ent.COURSE_ALIAS)):
|
|
if reason in [GAPI.FORBIDDEN, GAPI.BACKEND_ERROR]:
|
|
errMsg = getPhraseDNEorSNA(ri[RI_ITEM])
|
|
else:
|
|
errMsg = getHTTPError(_ADD_PART_REASON_TO_MESSAGE_MAP, http_status, reason, message)
|
|
if (reason == GAPI.PERMISSION_DENIED) and (ri[RI_ROLE] in {Ent.STUDENT, Ent.TEACHER}) and ('CannotDirectAddUser' in errMsg):
|
|
errMsg += f' Add external user with: gam user {ri[RI_ITEM]} create classroominvitation courses {ri[RI_ENTITY]} role {Ent.Singular(ri[RI_ROLE])}'
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
return
|
|
waitOnFailure(1, 10, reason, message)
|
|
try:
|
|
callGAPI(service, 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR,
|
|
GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION,
|
|
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], triesLimit=0 if reason != GAPI.NOT_FOUND else 3,
|
|
courseId=addCourseIdScope(ri[RI_ENTITY]),
|
|
body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])},
|
|
fields='')
|
|
except (GAPI.notFound, GAPI.backendError, GAPI.forbidden):
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], getPhraseDNEorSNA(ri[RI_ITEM]), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except GAPI.alreadyExists:
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.DUPLICATE, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except GAPI.failedPrecondition:
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.NOT_ALLOWED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
if role == Ent.STUDENT:
|
|
service = croom.courses().students()
|
|
attribute = 'userId'
|
|
elif role == Ent.TEACHER:
|
|
service = croom.courses().teachers()
|
|
attribute = 'userId'
|
|
elif role == Ent.COURSE_ALIAS:
|
|
service = croom.courses().aliases()
|
|
attribute = 'alias'
|
|
else: # role == Ent.COURSE_TOPIC:
|
|
service = croom.courses().topics()
|
|
attribute = 'name'
|
|
method = getattr(service, 'create')
|
|
Act.Set(Act.ADD)
|
|
jcount = len(addParticipants)
|
|
noScopeCourseId = removeCourseIdScope(courseId)
|
|
entityPerformActionNumItems([Ent.COURSE, noScopeCourseId], jcount, role, i, count)
|
|
Ind.Increment()
|
|
svcargs = dict([('courseId', courseId), ('body', {attribute: None}), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
dbatch = croom.new_batch_http_request(callback=_callbackAddItemsToCourse)
|
|
bcount = 0
|
|
j = 0
|
|
for participant in addParticipants:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
if role in {Ent.STUDENT, Ent.TEACHER}:
|
|
svcparms['body'][attribute] = cleanItem = normalizeEmailAddressOrUID(participant)
|
|
elif role == Ent.COURSE_ALIAS:
|
|
svcparms['body'][attribute] = addCourseAliasScope(participant)
|
|
cleanItem = removeCourseAliasScope(svcparms['body'][attribute])
|
|
else: # role == Ent.COURSE_TOPIC:
|
|
svcparms['body'][attribute] = cleanItem = participant
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(noScopeCourseId, 0, 0, j, jcount, cleanItem, role))
|
|
bcount += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = croom.new_batch_http_request(callback=_callbackAddItemsToCourse)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
Ind.Decrement()
|
|
|
|
def _batchRemoveItemsFromCourse(croom, courseId, i, count, removeParticipants, role):
|
|
_REMOVE_PART_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST,
|
|
GAPI.FORBIDDEN: Msg.FORBIDDEN,
|
|
GAPI.PERMISSION_DENIED: Msg.PERMISSION_DENIED}
|
|
def _callbackRemoveItemsFromCourse(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
entityActionPerformed([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason not in {GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE}:
|
|
if reason == GAPI.NOT_FOUND and ri[RI_ROLE] != Ent.COURSE_ALIAS:
|
|
errMsg = f'{Msg.NOT_A} {Ent.Singular(ri[RI_ROLE])}'
|
|
else:
|
|
errMsg = getHTTPError(_REMOVE_PART_REASON_TO_MESSAGE_MAP, http_status, reason, message)
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
return
|
|
waitOnFailure(1, 10, reason, message)
|
|
try:
|
|
callGAPI(service, 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED,
|
|
GAPI.QUOTA_EXCEEDED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=[GAPI.NOT_FOUND, GAPI.SERVICE_NOT_AVAILABLE], triesLimit=0 if reason != GAPI.NOT_FOUND else 3,
|
|
courseId=addCourseIdScope(ri[RI_ENTITY]),
|
|
body={attribute: ri[RI_ITEM] if ri[RI_ROLE] != Ent.COURSE_ALIAS else addCourseAliasScope(ri[RI_ITEM])},
|
|
fields='')
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.DOES_NOT_EXIST, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except GAPI.forbidden:
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.FORBIDDEN, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except GAPI.permissionDenied:
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], Msg.PERMISSION_DENIED, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except (GAPI.quotaExceeded, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.COURSE, ri[RI_ENTITY], ri[RI_ROLE], ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
if role == Ent.STUDENT:
|
|
service = croom.courses().students()
|
|
attribute = 'userId'
|
|
elif role == Ent.TEACHER:
|
|
service = croom.courses().teachers()
|
|
attribute = 'userId'
|
|
elif role == Ent.COURSE_ALIAS:
|
|
service = croom.courses().aliases()
|
|
attribute = 'alias'
|
|
else: # role == Ent.COURSE_TOPIC:
|
|
service = croom.courses().topics()
|
|
attribute = 'id'
|
|
method = getattr(service, 'delete')
|
|
Act.Set(Act.REMOVE)
|
|
jcount = len(removeParticipants)
|
|
noScopeCourseId = removeCourseIdScope(courseId)
|
|
entityPerformActionNumItems([Ent.COURSE, noScopeCourseId], jcount, role, i, count)
|
|
Ind.Increment()
|
|
svcargs = dict([('courseId', courseId), ('fields', ''), (attribute, None)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
dbatch = croom.new_batch_http_request(callback=_callbackRemoveItemsFromCourse)
|
|
bcount = 0
|
|
j = 0
|
|
for participant in removeParticipants:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
if role in {Ent.STUDENT, Ent.TEACHER}:
|
|
svcparms[attribute] = cleanItem = normalizeEmailAddressOrUID(participant)
|
|
elif role == Ent.COURSE_ALIAS:
|
|
svcparms[attribute] = addCourseAliasScope(participant)
|
|
cleanItem = removeCourseAliasScope(svcparms[attribute])
|
|
else: # role == Ent.COURSE_TOPIC:
|
|
svcparms[attribute] = cleanItem = participant
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(noScopeCourseId, 0, 0, j, jcount, cleanItem, role))
|
|
bcount += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = croom.new_batch_http_request(callback=_callbackRemoveItemsFromCourse)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
Ind.Decrement()
|
|
|
|
def _updateCourseOwner(croom, courseId, owner, i, count):
|
|
action = Act.Get()
|
|
Act.Set(Act.UPDATE_OWNER)
|
|
try:
|
|
callGAPI(croom.courses(), 'patch',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION,
|
|
GAPI.FORBIDDEN, GAPI.BAD_REQUEST, GAPI.INVALID_ARGUMENT,
|
|
GAPI.INTERNAL_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
id=courseId, body={'ownerId': owner}, updateMask='ownerId', fields='ownerId')
|
|
entityActionPerformed([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, owner], i, count)
|
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.failedPrecondition,
|
|
GAPI.forbidden, GAPI.badRequest, GAPI.invalidArgument,
|
|
GAPI.internalError, GAPI.serviceNotAvailable) as e:
|
|
errMsg = str(e)
|
|
if '@UserAlreadyOwner' not in errMsg:
|
|
entityActionFailedWarning([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, owner], errMsg, i, count)
|
|
else:
|
|
entityActionPerformedMessage([Ent.COURSE, removeCourseIdScope(courseId), Ent.TEACHER, owner], Msg.ALREADY_WAS_OWNER, i, count)
|
|
Act.Set(action)
|
|
|
|
ADD_REMOVE_PARTICIPANT_TYPES_MAP = {
|
|
'alias': Ent.COURSE_ALIAS,
|
|
'aliases': Ent.COURSE_ALIAS,
|
|
'student': Ent.STUDENT,
|
|
'students': Ent.STUDENT,
|
|
'teacher': Ent.TEACHER,
|
|
'teachers': Ent.TEACHER,
|
|
'topic': Ent.COURSE_TOPIC,
|
|
'topics': Ent.COURSE_TOPIC,
|
|
}
|
|
CLEAR_SYNC_PARTICIPANT_TYPES_MAP = {
|
|
'student': Ent.STUDENT,
|
|
'students': Ent.STUDENT,
|
|
'teacher': Ent.TEACHER,
|
|
'teachers': Ent.TEACHER,
|
|
}
|
|
PARTICIPANT_EN_MAP = {
|
|
Ent.STUDENT: Cmd.ENTITY_STUDENTS,
|
|
Ent.TEACHER: Cmd.ENTITY_TEACHERS,
|
|
}
|
|
|
|
# gam courses <CourseEntity> create alias <CourseAliasEntity>
|
|
# gam course <CourseID> create alias <CourseAlias>
|
|
# gam courses <CourseEntity> create topic <CourseTopicEntity>
|
|
# gam course <CourseID> create topic <CourseTopic>
|
|
# gam courses <CourseEntity> create students <UserTypeEntity>
|
|
# gam course <CourseID> create student <EmailAddress>
|
|
# gam courses <CourseEntity> create teachers [makefirstteacherowner] <UserTypeEntity>
|
|
# gam course <CourseID> create teacher [makefirstteacherowner] <EmailAddress>
|
|
def doCourseAddItems(courseIdList, getEntityListArg):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
role = getChoice(ADD_REMOVE_PARTICIPANT_TYPES_MAP, mapChoice=True)
|
|
if role == Ent.TEACHER:
|
|
makeFirstTeacherOwner = checkArgumentPresent(['makefirstteacherowner'])
|
|
else:
|
|
makeFirstTeacherOwner = False
|
|
if not getEntityListArg:
|
|
if role in {Ent.STUDENT, Ent.TEACHER}:
|
|
addItems = getStringReturnInList(Cmd.OB_EMAIL_ADDRESS)
|
|
elif role == Ent.COURSE_ALIAS:
|
|
addItems = getStringReturnInList(Cmd.OB_COURSE_ALIAS)
|
|
else: # role == Ent.COURSE_TOPIC:
|
|
addItems = getStringReturnInList(Cmd.OB_COURSE_TOPIC)
|
|
courseParticipantLists = None
|
|
else:
|
|
if role in {Ent.STUDENT, Ent.TEACHER}:
|
|
_, addItems = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[role]},
|
|
isSuspended=False, isArchived=False)
|
|
elif role == Ent.COURSE_ALIAS:
|
|
addItems = getEntityList(Cmd.OB_COURSE_ALIAS_ENTITY, shlexSplit=True)
|
|
else: # role == Ent.COURSE_TOPIC:
|
|
addItems = getEntityList(Cmd.OB_COURSE_TOPIC_ENTITY, shlexSplit=True)
|
|
courseParticipantLists = addItems if isinstance(addItems, dict) else None
|
|
if courseParticipantLists is None:
|
|
firstTeacher = None
|
|
if makeFirstTeacherOwner and addItems:
|
|
firstTeacher = normalizeEmailAddressOrUID(addItems[0])
|
|
checkForExtraneousArguments()
|
|
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, role == Ent.COURSE_TOPIC,
|
|
addCIIdScope=courseParticipantLists is None)
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
i += 1
|
|
if courseParticipantLists:
|
|
addItems = courseParticipantLists[courseId]
|
|
firstTeacher = None
|
|
if makeFirstTeacherOwner and addItems:
|
|
firstTeacher = normalizeEmailAddressOrUID(addItems[0])
|
|
courseId = addCourseIdScope(courseId)
|
|
_batchAddItemsToCourse(courseInfo['croom'], courseId, i, count, addItems, role)
|
|
if makeFirstTeacherOwner and firstTeacher:
|
|
_updateCourseOwner(courseInfo['croom'], courseId, firstTeacher, i, count)
|
|
|
|
# gam courses <CourseEntity> remove alias <CourseAliasEntity>
|
|
# gam course <CourseID> remove alias <CourseAlias>
|
|
# gam courses <CourseEntity> remove topic <CourseTopicIDEntity>
|
|
# gam course <CourseID> remove topic <CourseTopicID>
|
|
# gam courses <CourseEntity> remove teachers|students [owneracccess] <UserTypeEntity>
|
|
# gam course <CourseID> remove teacher|student [owneracccess] <EmailAddress>
|
|
def doCourseRemoveItems(courseIdList, getEntityListArg):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
role = getChoice(ADD_REMOVE_PARTICIPANT_TYPES_MAP, mapChoice=True)
|
|
if not getEntityListArg:
|
|
if role in {Ent.STUDENT, Ent.TEACHER}:
|
|
useOwnerAccess = GC.Values[GC.USE_COURSE_OWNER_ACCESS]
|
|
if checkArgumentPresent(OWNER_ACCESS_OPTIONS):
|
|
useOwnerAccess = True
|
|
removeItems = getStringReturnInList(Cmd.OB_EMAIL_ADDRESS)
|
|
elif role == Ent.COURSE_ALIAS:
|
|
useOwnerAccess = False
|
|
removeItems = getStringReturnInList(Cmd.OB_COURSE_ALIAS)
|
|
else: # role == Ent.COURSE_TOPIC:
|
|
useOwnerAccess = True
|
|
removeItems = getStringReturnInList(Cmd.OB_COURSE_TOPIC_ID)
|
|
courseParticipantLists = None
|
|
else:
|
|
if role in {Ent.STUDENT, Ent.TEACHER}:
|
|
useOwnerAccess = checkArgumentPresent(OWNER_ACCESS_OPTIONS)
|
|
_, removeItems = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[role]})
|
|
elif role == Ent.COURSE_ALIAS:
|
|
useOwnerAccess = False
|
|
removeItems = getEntityList(Cmd.OB_COURSE_ALIAS_ENTITY, shlexSplit=True)
|
|
else: # role == Ent.COURSE_TOPIC:
|
|
useOwnerAccess = True
|
|
removeItems = getEntityList(Cmd.OB_COURSE_TOPIC_ID_ENTITY, shlexSplit=True)
|
|
courseParticipantLists = removeItems if isinstance(removeItems, dict) else None
|
|
checkForExtraneousArguments()
|
|
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, useOwnerAccess,
|
|
addCIIdScope=courseParticipantLists is None)
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
i += 1
|
|
if courseParticipantLists:
|
|
removeItems = courseParticipantLists[courseId]
|
|
courseId = addCourseIdScope(courseId)
|
|
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeItems, role)
|
|
|
|
# gam courses <CourseEntity> clear teachers|students
|
|
# gam course <CourseID> clear teacher|student
|
|
def doCourseClearParticipants(courseIdList, _):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
role = getChoice(CLEAR_SYNC_PARTICIPANT_TYPES_MAP, mapChoice=True)
|
|
checkForExtraneousArguments()
|
|
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, GC.Values[GC.USE_COURSE_OWNER_ACCESS])
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
i += 1
|
|
removeParticipants = getItemsToModify(PARTICIPANT_EN_MAP[role], courseId, noListConversion=True)
|
|
if GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE]:
|
|
continue
|
|
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeParticipants, role)
|
|
|
|
# gam courses <CourseEntity> sync students [addonly|removeonly] <UserTypeEntity>
|
|
# gam course <CourseID> sync students [addonly|removeonly] <UserTypeEntity>
|
|
# gam courses <CourseEntity> sync teachers [addonly|removeonly] [makefirstteacherowner] <UserTypeEntity>
|
|
# gam course <CourseID> sync teachers [addonly|removeonly] [makefirstteacherowner] <UserTypeEntity>
|
|
def doCourseSyncParticipants(courseIdList, _):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
role = getChoice(CLEAR_SYNC_PARTICIPANT_TYPES_MAP, mapChoice=True)
|
|
if role == Ent.TEACHER:
|
|
makeFirstTeacherOwner = checkArgumentPresent(['makefirstteacherowner'])
|
|
else:
|
|
makeFirstTeacherOwner = False
|
|
syncOperation = getSyncOperation()
|
|
_, syncParticipants = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS,
|
|
typeMap={Cmd.ENTITY_COURSEPARTICIPANTS: PARTICIPANT_EN_MAP[role]},
|
|
isSuspended=False, isArchived=False)
|
|
checkForExtraneousArguments()
|
|
courseParticipantLists = syncParticipants if isinstance(syncParticipants, dict) else None
|
|
if courseParticipantLists is None:
|
|
syncParticipantsSet = set()
|
|
firstTeacher = None
|
|
if syncParticipants:
|
|
for user in syncParticipants:
|
|
syncParticipantsSet.add(normalizeEmailAddressOrUID(user))
|
|
if makeFirstTeacherOwner:
|
|
firstTeacher = normalizeEmailAddressOrUID(syncParticipants[0])
|
|
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, GC.Values[GC.USE_COURSE_OWNER_ACCESS],
|
|
addCIIdScope=courseParticipantLists is None)
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
i += 1
|
|
if courseParticipantLists:
|
|
syncParticipantsSet = set()
|
|
firstTeacher = None
|
|
if courseParticipantLists[courseId]:
|
|
for user in courseParticipantLists[courseId]:
|
|
syncParticipantsSet.add(normalizeEmailAddressOrUID(user))
|
|
if makeFirstTeacherOwner:
|
|
firstTeacher = normalizeEmailAddressOrUID(courseParticipantLists[courseId][0])
|
|
courseId = addCourseIdScope(courseId)
|
|
currentParticipantsSet = set()
|
|
currentParticipants = getItemsToModify(PARTICIPANT_EN_MAP[role], courseId, noListConversion=True)
|
|
if GM.Globals[GM.CLASSROOM_SERVICE_NOT_AVAILABLE]:
|
|
continue
|
|
for user in currentParticipants:
|
|
currentParticipantsSet.add(normalizeEmailAddressOrUID(user))
|
|
if syncOperation != 'removeonly':
|
|
_batchAddItemsToCourse(croom, courseId, i, count, list(syncParticipantsSet-currentParticipantsSet), role)
|
|
if makeFirstTeacherOwner and firstTeacher:
|
|
_updateCourseOwner(croom, courseId, firstTeacher, i, count)
|
|
if syncOperation != 'addonly':
|
|
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, list(currentParticipantsSet-syncParticipantsSet), role)
|
|
|
|
def studentUnknownWarning(studentId, errMsg, i, count):
|
|
setSysExitRC(SERVICE_NOT_APPLICABLE_RC)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(Ent.STUDENT), studentId, f'{Msg.SERVICE_NOT_APPLICABLE}: {errMsg}'],
|
|
currentCountNL(i, count)))
|
|
|
|
def getGuardianEntity():
|
|
guardians = getEntityList(Cmd.OB_GUARDIAN_ENTITY)
|
|
if isinstance(guardians, dict):
|
|
return (guardians, guardians)
|
|
return ([normalizeEmailAddressOrUID(guardian) for guardian in guardians], None)
|
|
|
|
def getGuardianEmails(user, guardianEntity, guardianEntityList):
|
|
studentId = normalizeStudentGuardianEmailAddressOrUID(user)
|
|
if guardianEntityList:
|
|
guardianEmails = [normalizeEmailAddressOrUID(guardian) for guardian in guardianEntityList[user]]
|
|
else:
|
|
guardianEmails = guardianEntity
|
|
return (studentId, guardianEmails, len(guardianEmails))
|
|
|
|
def getGuardianInvitationEntity():
|
|
invitations = getEntityList(Cmd.OB_GUARDIAN_INVITATION_ID_ENTITY)
|
|
if isinstance(invitations, dict):
|
|
return (invitations, invitations)
|
|
return (invitations, None)
|
|
|
|
def getGuardianInvitationIds(user, invitationEntity, invitationEntityList):
|
|
studentId = normalizeStudentGuardianEmailAddressOrUID(user)
|
|
if invitationEntityList:
|
|
invitationIds = invitationEntityList[user]
|
|
else:
|
|
invitationIds = invitationEntity
|
|
return (studentId, invitationIds, len(invitationIds))
|
|
|
|
GUARDIAN_CLASS_UNDEFINED = 0
|
|
GUARDIAN_CLASS_ACCEPTED = 1
|
|
GUARDIAN_CLASS_INVITATIONS = 2
|
|
GUARDIAN_CLASS_ALL = 3
|
|
|
|
GUARDIAN_CLASS_MAP = {
|
|
'all': GUARDIAN_CLASS_ALL,
|
|
'accepted': GUARDIAN_CLASS_ACCEPTED,
|
|
'invitation': GUARDIAN_CLASS_INVITATIONS,
|
|
'invitations': GUARDIAN_CLASS_INVITATIONS,
|
|
}
|
|
GUARDIAN_CLASS_ENTITY = {
|
|
GUARDIAN_CLASS_ALL: Ent.GUARDIAN_AND_INVITATION,
|
|
GUARDIAN_CLASS_ACCEPTED: Ent.GUARDIAN,
|
|
GUARDIAN_CLASS_INVITATIONS: Ent.GUARDIAN_INVITATION,
|
|
}
|
|
|
|
def _inviteGuardian(croom, studentId, guardianEmail, i=0, count=0, j=0, jcount=0):
|
|
body = {'invitedEmailAddress': guardianEmail}
|
|
try:
|
|
result = callGAPI(croom.userProfiles().guardianInvitations(), 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS,
|
|
GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN,
|
|
GAPI.PERMISSION_DENIED, GAPI.RESOURCE_EXHAUSTED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, body=body, fields='invitationId')
|
|
entityActionPerformed([Ent.STUDENT, studentId, Ent.GUARDIAN, body['invitedEmailAddress'], Ent.GUARDIAN_INVITATION, result['invitationId']], j, jcount)
|
|
return 1
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
|
|
return -1
|
|
except GAPI.alreadyExists:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN, body['invitedEmailAddress']], Msg.DUPLICATE, j, jcount)
|
|
return 0
|
|
except GAPI.resourceExhausted as e:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN, body['invitedEmailAddress']], str(e), j, jcount)
|
|
return -1
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
studentUnknownWarning(studentId, str(e), i, count)
|
|
return -1
|
|
|
|
# gam create guardian|guardianinvite|inviteguardian <EmailAddress> <StudentItem>
|
|
def doInviteGuardian():
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
guardianEmail = getEmailAddress()
|
|
studentId = normalizeStudentGuardianEmailAddressOrUID(getString(Cmd.OB_STUDENT_ITEM))
|
|
checkForExtraneousArguments()
|
|
_inviteGuardian(croom, studentId, guardianEmail)
|
|
|
|
# gam <UserTypeEntity> create guardian|guardianinvite|inviteguardian <GuardianEntity>
|
|
def inviteGuardians(users):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
guardianEntity, guardianEntityList = getGuardianEntity()
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
studentId, guardianEmails, jcount = getGuardianEmails(user, guardianEntity, guardianEntityList)
|
|
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN_INVITATION, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for guardianEmail in guardianEmails:
|
|
j += 1
|
|
if _inviteGuardian(croom, studentId, guardianEmail, i, count, j, jcount) < 0:
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def _cancelGuardianInvitation(croom, studentId, invitationId, i=0, count=0, j=0, jcount=0):
|
|
try:
|
|
result = callGAPI(croom.userProfiles().guardianInvitations(), 'patch',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION,
|
|
GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN,
|
|
GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, invitationId=invitationId, updateMask='state', body={'state': 'COMPLETE'}, fields='invitedEmailAddress')
|
|
entityActionPerformed([Ent.STUDENT, studentId, Ent.GUARDIAN_INVITATION, result['invitedEmailAddress']], j, jcount)
|
|
return 1
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN_INVITATION, invitationId], Msg.NOT_FOUND, j, jcount)
|
|
return 0
|
|
except GAPI.failedPrecondition:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN_INVITATION, invitationId], Msg.GUARDIAN_INVITATION_STATUS_NOT_PENDING, j, jcount)
|
|
return 1
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN_INVITATION, invitationId], str(e), j, jcount)
|
|
return -1
|
|
except (GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
studentUnknownWarning(studentId, str(e), i, count)
|
|
return -1
|
|
|
|
# gam cancel guardianinvitation|guardianinvitations <GuardianInvitationID> <StudentItem>
|
|
def doCancelGuardianInvitation():
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
invitationId = getString(Cmd.OB_GUARDIAN_INVITATION_ID)
|
|
studentId = normalizeStudentGuardianEmailAddressOrUID(getString(Cmd.OB_STUDENT_ITEM))
|
|
checkForExtraneousArguments()
|
|
_cancelGuardianInvitation(croom, studentId, invitationId)
|
|
|
|
# gam <UserTypeEntity> cancel guardianinvitation|guardianinvitations <GuardianInvitationIDEntity>
|
|
def cancelGuardianInvitations(users):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
invitationEntity, invitationEntityList = getGuardianInvitationEntity()
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
studentId, invitationIds, jcount = getGuardianInvitationIds(user, invitationEntity, invitationEntityList)
|
|
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN_INVITATION, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for invitationId in invitationIds:
|
|
j += 1
|
|
if _cancelGuardianInvitation(croom, studentId, invitationId, i, count, j, jcount) == -1:
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def _deleteGuardian(croom, studentId, guardianId, guardianEmail, i, count, j, jcount):
|
|
try:
|
|
callGAPI(croom.userProfiles().guardians(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, guardianId=guardianId)
|
|
entityActionPerformed([Ent.STUDENT, studentId, Ent.GUARDIAN, guardianEmail], j, jcount)
|
|
return 1
|
|
except GAPI.notFound:
|
|
if guardianId == guardianEmail:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN, guardianEmail], Msg.NOT_FOUND, j, jcount)
|
|
return 0
|
|
except GAPI.serviceNotAvailable as e:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId, Ent.GUARDIAN, guardianEmail], str(e), j, jcount)
|
|
return -1
|
|
except (GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
studentUnknownWarning(studentId, str(e), i, count)
|
|
return -1
|
|
|
|
def _doDeleteGuardian(croom, studentId, guardianId, guardianClass, i=0, count=0, j=0, jcount=0):
|
|
guardianIdIsEmail = guardianId.find('@') != -1
|
|
guardianFound = False
|
|
try:
|
|
if guardianClass != GUARDIAN_CLASS_ACCEPTED:
|
|
Act.Set(Act.CANCEL)
|
|
if guardianIdIsEmail:
|
|
invitations = callGAPIpages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
|
|
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, invitedEmailAddress=guardianId, states=['PENDING'],
|
|
fields='nextPageToken,guardianInvitations(studentId,invitationId)')
|
|
for invitation in invitations:
|
|
result = _cancelGuardianInvitation(croom, invitation['studentId'], invitation['invitationId'], i, count, j, jcount)
|
|
if result < 0:
|
|
return result
|
|
if result > 0:
|
|
guardianFound = True
|
|
else:
|
|
result = _cancelGuardianInvitation(croom, studentId, guardianId, i, count, j, jcount)
|
|
if result != 0:
|
|
return result
|
|
if guardianClass != GUARDIAN_CLASS_INVITATIONS:
|
|
Act.Set(Act.DELETE)
|
|
if guardianIdIsEmail:
|
|
guardians = callGAPIpages(croom.userProfiles().guardians(), 'list', 'guardians',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
|
|
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, invitedEmailAddress=guardianId,
|
|
fields='nextPageToken,guardians(studentId,guardianId)')
|
|
for guardian in guardians:
|
|
result = _deleteGuardian(croom, guardian['studentId'], guardian['guardianId'], guardianId, i, count, j, jcount)
|
|
if result < 0:
|
|
return result
|
|
if result > 0:
|
|
guardianFound = True
|
|
else:
|
|
result = _deleteGuardian(croom, studentId, guardianId, guardianId, i, count, j, jcount)
|
|
if result != 0:
|
|
return result
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
|
|
return -1
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
studentUnknownWarning(studentId, str(e), i, count)
|
|
return -1
|
|
except GAPI.serviceNotAvailable as e:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId, GUARDIAN_CLASS_ENTITY[guardianClass], guardianId], str(e))
|
|
return -1
|
|
if not guardianFound:
|
|
Act.Set(Act.DELETE)
|
|
entityActionFailedWarning([Ent.STUDENT, studentId, GUARDIAN_CLASS_ENTITY[guardianClass], guardianId], Msg.NOT_FOUND)
|
|
return 0
|
|
return 1
|
|
|
|
# gam delete guardian|guardians <GuardianItem> <StudentItem> [accepted|invitations|all]
|
|
def doDeleteGuardian():
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
guardianId = normalizeStudentGuardianEmailAddressOrUID(getString(Cmd.OB_GUARDIAN_ITEM))
|
|
studentId = normalizeStudentGuardianEmailAddressOrUID(getString(Cmd.OB_STUDENT_ITEM), allowDash=True)
|
|
guardianClass = getChoice(GUARDIAN_CLASS_MAP, mapChoice=True, defaultChoice=GUARDIAN_CLASS_ALL)
|
|
checkForExtraneousArguments()
|
|
_doDeleteGuardian(croom, studentId, guardianId, guardianClass)
|
|
|
|
# gam <UserTypeEntity> delete guardian|guardians <GuardianEntity> [accepted|invitations|all]
|
|
def deleteGuardians(users):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
guardianEntity, guardianEntityList = getGuardianEntity()
|
|
guardianClass = getChoice(GUARDIAN_CLASS_MAP, mapChoice=True, defaultChoice=GUARDIAN_CLASS_ALL)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
studentId, guardianEmails, jcount = getGuardianEmails(user, guardianEntity, guardianEntityList)
|
|
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, GUARDIAN_CLASS_ENTITY[guardianClass], i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for guardianEmail in guardianEmails:
|
|
j += 1
|
|
if _doDeleteGuardian(croom, studentId, guardianEmail, guardianClass, i, count, j, jcount) < 0:
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> clear guardian|guardians [accepted|invitations|all]
|
|
def clearGuardians(users):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
guardianClass = getChoice(GUARDIAN_CLASS_MAP, mapChoice=True, defaultChoice=GUARDIAN_CLASS_ALL)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
studentId = normalizeStudentGuardianEmailAddressOrUID(user)
|
|
try:
|
|
if guardianClass != GUARDIAN_CLASS_ACCEPTED:
|
|
invitations = callGAPIpages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
|
|
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, states=['PENDING'], fields='nextPageToken,guardianInvitations(invitationId)')
|
|
Act.Set(Act.CANCEL)
|
|
jcount = len(invitations)
|
|
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN_INVITATION, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for invitation in invitations:
|
|
j += 1
|
|
_cancelGuardianInvitation(croom, studentId, invitation['invitationId'], i, count, j, jcount)
|
|
Ind.Decrement()
|
|
if guardianClass != GUARDIAN_CLASS_INVITATIONS:
|
|
guardians = callGAPIpages(croom.userProfiles().guardians(), 'list', 'guardians',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
|
studentId=studentId, fields='nextPageToken,guardians(guardianId,invitedEmailAddress)')
|
|
Act.Set(Act.DELETE)
|
|
jcount = len(guardians)
|
|
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for guardian in guardians:
|
|
j += 1
|
|
_deleteGuardian(croom, studentId, guardian['guardianId'], guardian['invitedEmailAddress'], i, count, j, jcount)
|
|
Ind.Decrement()
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
|
|
except GAPI.serviceNotAvailable as e:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId], str(e))
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
studentUnknownWarning(studentId, str(e), i, count)
|
|
|
|
# gam <UserTypeEntity> sync guardian|guardians <GuardianEntity>
|
|
def syncGuardians(users):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
guardianEntity, guardianEntityList = getGuardianEntity()
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
Act.Set(Act.SYNC)
|
|
studentId, guardianEmails, jcount = getGuardianEmails(user, guardianEntity, guardianEntityList)
|
|
entityPerformActionNumItems([Ent.STUDENT, studentId], jcount, Ent.GUARDIAN, i, count)
|
|
try:
|
|
invitations = callGAPIpages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
|
|
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, states=['PENDING'], fields='nextPageToken,guardianInvitations(invitationId,invitedEmailAddress)')
|
|
guardians = callGAPIpages(croom.userProfiles().guardians(), 'list', 'guardians',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
|
|
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, fields='nextPageToken,guardians(guardianId,invitedEmailAddress)')
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable as e:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId], str(e), i, count)
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
studentUnknownWarning(studentId, str(e), i, count)
|
|
continue
|
|
Ind.Increment()
|
|
Act.Set(Act.CANCEL)
|
|
jcount = len(invitations)
|
|
j = 0
|
|
for invitation in invitations:
|
|
j += 1
|
|
if invitation['invitedEmailAddress'] not in guardianEmails:
|
|
_cancelGuardianInvitation(croom, studentId, invitation['invitationId'], i, count, j, jcount)
|
|
Act.Set(Act.DELETE)
|
|
jcount = len(guardians)
|
|
j = 0
|
|
for guardian in guardians:
|
|
j += 1
|
|
if guardian['invitedEmailAddress'] not in guardianEmails:
|
|
_deleteGuardian(croom, studentId, guardian['guardianId'], guardian['invitedEmailAddress'], i, count, j, jcount)
|
|
Act.Set(Act.CREATE)
|
|
for guardianEmail in guardianEmails:
|
|
for guardian in guardians:
|
|
if guardianEmail == guardian['invitedEmailAddress']:
|
|
break
|
|
else:
|
|
for invitation in invitations:
|
|
if guardianEmail == invitation['invitedEmailAddress']:
|
|
break
|
|
else:
|
|
_inviteGuardian(croom, studentId, guardianEmail, i, count)
|
|
Ind.Decrement()
|
|
|
|
def _getCourseName(croom, courseNames, courseId):
|
|
courseName = courseNames.get(courseId)
|
|
if courseName is None:
|
|
try:
|
|
courseName = callGAPI(croom.courses(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
|
|
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
id=courseId, fields='name')['name']
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied, GAPI.serviceNotAvailable):
|
|
pass
|
|
if courseName is None:
|
|
courseName = courseId
|
|
courseNames[courseId] = courseName
|
|
return courseName
|
|
|
|
def _getClassroomEmail(croom, classroomEmails, userId, user):
|
|
if user.find('@') != -1:
|
|
return user
|
|
userEmail = classroomEmails.get(userId)
|
|
if userEmail is None:
|
|
try:
|
|
userEmail = callGAPI(croom.userProfiles(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST, GAPI.FORBIDDEN,
|
|
GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userId=userId, fields='emailAddress').get('emailAddress')
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied, GAPI.serviceNotAvailable):
|
|
pass
|
|
if userEmail is None:
|
|
userEmail = userId
|
|
classroomEmails[userId] = userEmail
|
|
return userEmail
|
|
|
|
GUARDIAN_TIME_OBJECTS = {'creationTime'}
|
|
GUARDIAN_STATES = ['complete', 'pending']
|
|
|
|
def _printShowGuardians(entityList=None):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
if entityList is None:
|
|
studentIds = ['-']
|
|
allStudents = True
|
|
else:
|
|
studentIds = entityList
|
|
allStudents = False
|
|
showStudentEmails = False
|
|
classroomEmails = {}
|
|
invitedEmailAddress = None
|
|
states = []
|
|
guardianClass = GUARDIAN_CLASS_ACCEPTED
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'invitedguardian':
|
|
invitedEmailAddress = getEmailAddress()
|
|
elif myarg in {'invitation', 'invitations'}:
|
|
guardianClass = GUARDIAN_CLASS_INVITATIONS
|
|
if not states:
|
|
states = [state.upper() for state in GUARDIAN_STATES]
|
|
elif myarg == 'accepted':
|
|
guardianClass = GUARDIAN_CLASS_ACCEPTED
|
|
elif myarg == 'all':
|
|
guardianClass = GUARDIAN_CLASS_ALL
|
|
if not states:
|
|
states.append('PENDING')
|
|
elif myarg in {'state', 'states', 'status'}:
|
|
statesList = getString(Cmd.OB_GUARDIAN_STATE_LIST).lower().split(',')
|
|
states = []
|
|
for state in statesList:
|
|
if state in GUARDIAN_STATES:
|
|
states.append(state.upper())
|
|
else:
|
|
invalidChoiceExit(state, GUARDIAN_STATES, True)
|
|
elif myarg == 'showstudentemails':
|
|
showStudentEmails = True
|
|
elif entityList is None and myarg == 'student':
|
|
studentIds = [getString(Cmd.OB_STUDENT_ITEM)]
|
|
allStudents = studentIds[0] == '-'
|
|
elif FJQC.GetFormatJSONQuoteChar(myarg, False, True):
|
|
pass
|
|
elif entityList is None:
|
|
Cmd.Backup()
|
|
_, studentIds = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
allStudents = False
|
|
else:
|
|
unknownArgumentExit()
|
|
if csvPF:
|
|
if not FJQC.formatJSON:
|
|
sortTitles = ['studentEmail', 'studentId', 'invitedEmailAddress']
|
|
if guardianClass != GUARDIAN_CLASS_ACCEPTED:
|
|
sortTitles.extend(['invitationId', 'creationTime', 'state'])
|
|
if guardianClass != GUARDIAN_CLASS_INVITATIONS:
|
|
sortTitles.append('guardianId')
|
|
csvPF.SetTitles(sortTitles)
|
|
csvPF.SetSortAllTitles()
|
|
else:
|
|
csvPF.SetJSONTitles(['studentEmail', 'studentId', 'JSON'])
|
|
i, count, studentIds = getEntityArgument(studentIds)
|
|
for studentId in studentIds:
|
|
i += 1
|
|
if not allStudents:
|
|
studentId = normalizeStudentGuardianEmailAddressOrUID(studentId)
|
|
if showStudentEmails:
|
|
studentId = _getClassroomEmail(croom, classroomEmails, studentId, studentId)
|
|
try:
|
|
if guardianClass != GUARDIAN_CLASS_ACCEPTED:
|
|
if csvPF:
|
|
if states:
|
|
qualifier = f' ({",".join(states)})'
|
|
else:
|
|
qualifier = ''
|
|
if not allStudents:
|
|
printGettingAllEntityItemsForWhom(Ent.GUARDIAN_INVITATION, studentId, i, count, qualifier=qualifier)
|
|
else:
|
|
printGettingAllEntityItemsForWhom(Ent.GUARDIAN_INVITATION, 'All students', qualifier=qualifier)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
invitations = callGAPIpages(croom.userProfiles().guardianInvitations(), 'list', 'guardianInvitations',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
|
|
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, invitedEmailAddress=invitedEmailAddress, states=states)
|
|
jcount = len(invitations)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.STUDENT, studentId if not allStudents else 'All'], jcount, Ent.GUARDIAN_INVITATION, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for invitation in invitations:
|
|
j += 1
|
|
printKeyValueListWithCount(['invitedEmailAddress', invitation['invitedEmailAddress']], j, jcount)
|
|
Ind.Increment()
|
|
if showStudentEmails:
|
|
invitation['studentEmail'] = _getClassroomEmail(croom, classroomEmails, invitation['studentId'], studentId)
|
|
showJSON(None, invitation, ['invitedEmailAddress'], GUARDIAN_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(invitations, timeObjects=GUARDIAN_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
if not FJQC.formatJSON:
|
|
for invitation in invitations:
|
|
if showStudentEmails:
|
|
invitation['studentEmail'] = _getClassroomEmail(croom, classroomEmails, invitation['studentId'], studentId)
|
|
else:
|
|
invitation['studentEmail'] = studentId
|
|
csvPF.WriteRowTitles(flattenJSON(invitation, timeObjects=GUARDIAN_TIME_OBJECTS))
|
|
else:
|
|
csvPF.WriteRow({'studentId': studentId, 'studentEmail': _getClassroomEmail(croom, classroomEmails, studentId, studentId),
|
|
'JSON': json.dumps(cleanJSON(invitations, timeObjects=GUARDIAN_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
|
|
if guardianClass != GUARDIAN_CLASS_INVITATIONS:
|
|
if csvPF:
|
|
if not allStudents:
|
|
printGettingAllEntityItemsForWhom(Ent.GUARDIAN, studentId, i, count)
|
|
else:
|
|
printGettingAllEntityItemsForWhom(Ent.GUARDIAN, 'All students')
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
guardians = callGAPIpages(croom.userProfiles().guardians(), 'list', 'guardians',
|
|
pageMessage=pageMessage,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
|
|
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
studentId=studentId, invitedEmailAddress=invitedEmailAddress)
|
|
jcount = len(guardians)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.STUDENT, studentId if not allStudents else 'All'], jcount, Ent.GUARDIAN, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for guardian in guardians:
|
|
j += 1
|
|
printKeyValueListWithCount(['invitedEmailAddress', guardian['invitedEmailAddress']], j, jcount)
|
|
Ind.Increment()
|
|
if showStudentEmails:
|
|
guardian['studentEmail'] = _getClassroomEmail(croom, classroomEmails, guardian['studentId'], studentId)
|
|
showJSON(None, guardian, ['invitedEmailAddress'])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(guardians), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
if not FJQC.formatJSON:
|
|
for guardian in guardians:
|
|
if showStudentEmails:
|
|
guardian['studentEmail'] = _getClassroomEmail(croom, classroomEmails, guardian['studentId'], studentId)
|
|
else:
|
|
guardian['studentEmail'] = studentId
|
|
csvPF.WriteRowTitles(flattenJSON(guardian))
|
|
else:
|
|
csvPF.WriteRowNoFilter({'studentId': studentId, 'studentEmail': _getClassroomEmail(croom, classroomEmails, studentId, studentId),
|
|
'JSON': json.dumps(cleanJSON(guardians), ensure_ascii=False, sort_keys=True)})
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.STUDENT, studentId, i, count)
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
studentUnknownWarning(studentId, str(e), i, count)
|
|
except GAPI.serviceNotAvailable as e:
|
|
entityActionFailedWarning([Ent.STUDENT, studentId], str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Guardians')
|
|
|
|
# gam show guardian|guardians [accepted|invitations|all] [states <GuardianInvitationStateList>] [invitedguardian <EmailAddress>]
|
|
# [student <StudentItem>] [<UserTypeEntity>]
|
|
# [showstudentemails] [formatjson]
|
|
# gam print guardian|guardians [todrive <ToDriveAttribute>*] [accepted|invitations|all] [states <GuardianInvitationStateList>] [invitedguardian <EmailAddress>]
|
|
# [student <StudentItem>] [<UserTypeEntity>]
|
|
# [showstudentemails] [formatjson [quotechar <Character>]]
|
|
def doPrintShowGuardians():
|
|
_printShowGuardians()
|
|
|
|
# gam <UserTypeEntity> show guardian|guardians [accepted|invitations|all] [states <GuardianInvitationStateList>] [invitedguardian <EmailAddress>]
|
|
# [showstudentemails] [formatjson]
|
|
# gam <UserTypeEntity> print guardian|guardians [todrive <ToDriveAttribute>*] [accepted|invitations|all] [states <GuardianInvitationStateList>] [invitedguardian <EmailAddress>]
|
|
# [showstudentemails] [formatjson [quotechar <Character>]]
|
|
def printShowGuardians(users):
|
|
_printShowGuardians(users)
|
|
|
|
CLASSROOM_ROLE_ALL = 'ALL'
|
|
CLASSROOM_ROLE_OWNER = 'OWNER'
|
|
CLASSROOM_ROLE_STUDENT = 'STUDENT'
|
|
CLASSROOM_ROLE_TEACHER = 'TEACHER'
|
|
|
|
def _getClassroomInvitations(croom, userId, courseId, role, i, count, j=0, jcount=0):
|
|
try:
|
|
invitations = callGAPIpages(croom.invitations(), 'list', 'invitations',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.BAD_REQUEST,
|
|
GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userId=userId, courseId=courseId)
|
|
except GAPI.notFound:
|
|
if userId is not None:
|
|
entityUnknownWarning(Ent.USER, userId, i, count)
|
|
return (-1, None)
|
|
entityUnknownWarning(Ent.COURSE, courseId, j, jcount)
|
|
return (0, [])
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.forbidden, GAPI.permissionDenied, GAPI.serviceNotAvailable) as e:
|
|
if userId is not None:
|
|
entityActionFailedWarning([Ent.USER, userId], str(e), i, count)
|
|
return (-1, None)
|
|
entityActionFailedWarning([Ent.COURSE, courseId], str(e), j, jcount)
|
|
return (0, [])
|
|
if role == CLASSROOM_ROLE_ALL:
|
|
return (1, invitations)
|
|
return (1, [invitation for invitation in invitations if invitation['role'] == role])
|
|
|
|
def _getClassroomInvitationIds(croom, userId, courseIds, role, i, count):
|
|
invitationIds = []
|
|
if courseIds is not None:
|
|
jcount = len(courseIds)
|
|
j = 0
|
|
for courseId in courseIds:
|
|
j += 1
|
|
courseId = addCourseIdScope(courseId)
|
|
status, invitations = _getClassroomInvitations(croom, userId, courseId, role, i, count, j, jcount)
|
|
if status < 0:
|
|
return (status, None)
|
|
if status > 0:
|
|
invitationIds.extend([invitation['id'] for invitation in invitations])
|
|
else:
|
|
status, invitations = _getClassroomInvitations(croom, userId, None, role, i, count)
|
|
if status < 0:
|
|
return (status, None)
|
|
if status > 0:
|
|
invitationIds.extend([invitation['id'] for invitation in invitations])
|
|
return (1, invitationIds)
|
|
|
|
CLASSROOM_CREATE_ROLE_MAP = {
|
|
'owner': CLASSROOM_ROLE_OWNER,
|
|
'student': CLASSROOM_ROLE_STUDENT,
|
|
'teacher': CLASSROOM_ROLE_TEACHER,
|
|
}
|
|
|
|
CLASSROOM_ROLE_MAP = {
|
|
'all': CLASSROOM_ROLE_ALL,
|
|
'owner': CLASSROOM_ROLE_OWNER,
|
|
'student': CLASSROOM_ROLE_STUDENT,
|
|
'teacher': CLASSROOM_ROLE_TEACHER,
|
|
}
|
|
|
|
CLASSROOM_ROLE_ENTITY_MAP = {
|
|
CLASSROOM_ROLE_ALL: Ent.CLASSROOM_INVITATION,
|
|
CLASSROOM_ROLE_OWNER: Ent.CLASSROOM_INVITATION_OWNER,
|
|
CLASSROOM_ROLE_STUDENT: Ent.CLASSROOM_INVITATION_STUDENT,
|
|
CLASSROOM_ROLE_TEACHER: Ent.CLASSROOM_INVITATION_TEACHER,
|
|
}
|
|
|
|
# gam <UserTypeEntity> create classroominvitation courses <CourseEntity> [role owner|student|teacher]
|
|
# [asadmin] [csv|csvformat] [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]
|
|
def createClassroomInvitations(users):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
classroomEmails = {}
|
|
courseIds = None
|
|
role = CLASSROOM_ROLE_STUDENT
|
|
useOwnerAccess = True
|
|
csvPF = None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'course', 'courses', 'class', 'classes'}:
|
|
courseIds = getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True)
|
|
elif myarg == 'role':
|
|
role = getChoice(CLASSROOM_CREATE_ROLE_MAP, mapChoice=True)
|
|
elif myarg in {'csv', 'csvformat'}:
|
|
csvPF = CSVPrintFile()
|
|
FJQC.SetCsvPF(csvPF)
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useOwnerAccess = False
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if courseIds is None:
|
|
missingArgumentExit('courses <CourseEntity>')
|
|
if csvPF:
|
|
if FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(['userEmail', 'JSON'])
|
|
else:
|
|
csvPF.SetTitles(['userEmail', 'courseId', 'courseName', 'id', 'role'])
|
|
csvPF.SetSortAllTitles()
|
|
courseIdsLists = courseIds if isinstance(courseIds, dict) else None
|
|
if courseIdsLists is None:
|
|
j, jcount, coursesInfo = _getCoursesOwnerInfo(croom, courseIds, useOwnerAccess)
|
|
entityType = CLASSROOM_ROLE_ENTITY_MAP[role]
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
userId = normalizeEmailAddressOrUID(user)
|
|
userEmail = _getClassroomEmail(croom, classroomEmails, userId, userId)
|
|
if courseIdsLists:
|
|
j, jcount, coursesInfo = _getCoursesOwnerInfo(croom, courseIdsLists[user], useOwnerAccess)
|
|
if csvPF or not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, userId], jcount, entityType, i, count)
|
|
if jcount == 0:
|
|
continue
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
j += 1
|
|
courseNameId = f'{courseInfo["name"]} ({courseId})'
|
|
try:
|
|
invitation = callGAPI(courseInfo['croom'].invitations(), 'create',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.ALREADY_EXISTS, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
|
body={'userId': userId, 'courseId': courseId, 'role': role})
|
|
invitation['courseName'] = courseInfo['name']
|
|
invitation['userEmail'] = userEmail
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
Ind.Increment()
|
|
entityActionPerformed([Ent.USER, userEmail, Ent.COURSE, courseNameId, entityType, invitation['id']], j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(invitation), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRow(invitation)
|
|
else:
|
|
csvPF.WriteRowNoFilter({'userEmail': userEmail,
|
|
'JSON': json.dumps(cleanJSON(invitation), ensure_ascii=False, sort_keys=True)})
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedWarning([Ent.USER, userId, Ent.COURSE, courseNameId, entityType, None], str(e), j, jcount)
|
|
break
|
|
except GAPI.notFound:
|
|
entityUnknownWarning(Ent.COURSE, courseNameId, j, jcount)
|
|
except (GAPI.failedPrecondition, GAPI.alreadyExists, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER, userId, Ent.COURSE, courseNameId, entityType, None], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('ClassroomInvitations')
|
|
|
|
def acceptDeleteClassroomInvitations(users, function):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
if function == 'delete':
|
|
ucroom = croom
|
|
courseIds = invitationIds = None
|
|
role = CLASSROOM_ROLE_ALL
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'id', 'ids'}:
|
|
invitationIds = getEntityList(Cmd.OB_CLASSROOM_INVITATION_ID_ENTITY)
|
|
courseIds = None
|
|
elif myarg in {'course', 'courses', 'class', 'classes'}:
|
|
courseIds = getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True)
|
|
invitationIds = None
|
|
elif myarg == 'role':
|
|
role = getChoice(CLASSROOM_ROLE_MAP, mapChoice=True)
|
|
else:
|
|
unknownArgumentExit()
|
|
courseIdsLists = courseIds if isinstance(courseIds, dict) else None
|
|
invitationIdsLists = invitationIds if isinstance(invitationIds, dict) else None
|
|
entityType = CLASSROOM_ROLE_ENTITY_MAP[role]
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if function == 'delete':
|
|
userId = normalizeEmailAddressOrUID(user)
|
|
else:
|
|
userId, ucroom = buildGAPIServiceObject(API.CLASSROOM, user, i, count)
|
|
if not ucroom:
|
|
continue
|
|
if invitationIdsLists:
|
|
userInvitationIds = invitationIdsLists[user]
|
|
elif invitationIds is not None:
|
|
userInvitationIds = invitationIds
|
|
else:
|
|
if courseIdsLists:
|
|
courseIds = courseIdsLists[user]
|
|
status, userInvitationIds = _getClassroomInvitationIds(croom, userId, courseIds, role, i, count)
|
|
if status < 0:
|
|
continue
|
|
jcount = len(userInvitationIds)
|
|
entityPerformActionNumItems([Ent.USER, userId], jcount, entityType, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for invitationId in userInvitationIds:
|
|
j += 1
|
|
try:
|
|
callGAPI(ucroom.invitations(), function,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
|
id=invitationId)
|
|
entityActionPerformed([Ent.USER, userId, entityType, invitationId], j, jcount)
|
|
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, userId, entityType, invitationId], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> accept classroominvitation (ids <ClassroomInvitationIDEntity>)|([courses <CourseEntity>] [role all|owner|student|teacher])
|
|
def acceptClassroomInvitations(users):
|
|
acceptDeleteClassroomInvitations(users, 'accept')
|
|
|
|
# gam <UserTypeEntity> delete classroominvitation (ids <ClassroomInvitationIDEntity>)|([courses <CourseEntity>] [role all|owner|student|teacher])
|
|
def deleteClassroomInvitations(users):
|
|
acceptDeleteClassroomInvitations(users, 'delete')
|
|
|
|
# gam <UserTypeEntity> show classroominvitations [role all|owner|student|teacher]
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> print classroominvitations [todrive <ToDriveAttribute>*] [role all|owner|student|teacher]
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowClassroomInvitations(users):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
classroomEmails = {}
|
|
courseNames = {}
|
|
role = CLASSROOM_ROLE_ALL
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'role':
|
|
role = getChoice(CLASSROOM_ROLE_MAP, mapChoice=True)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if csvPF:
|
|
if FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(['userEmail', 'JSON'])
|
|
else:
|
|
csvPF.SetTitles(['userId', 'userEmail', 'courseId', 'courseName', 'id', 'role'])
|
|
csvPF.SetSortAllTitles()
|
|
entityType = CLASSROOM_ROLE_ENTITY_MAP[role]
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
userId = normalizeEmailAddressOrUID(user)
|
|
userEmail = _getClassroomEmail(croom, classroomEmails, userId, userId)
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(entityType, userId, i, count)
|
|
status, invitations = _getClassroomInvitations(croom, userId, None, role, i, count)
|
|
if status > 0:
|
|
jcount = len(invitations)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, userId], jcount, entityType, i, count)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
Ind.Increment()
|
|
j = 0
|
|
for invitation in invitations:
|
|
j += 1
|
|
courseId = invitation['courseId']
|
|
courseName = _getCourseName(croom, courseNames, courseId)
|
|
printKeyValueListWithCount([Ent.Singular(Ent.COURSE), f'{courseName} ({courseId})'], j, jcount)
|
|
Ind.Increment()
|
|
printKeyValueList(['id', invitation['id']])
|
|
printKeyValueList(['role', invitation['role']])
|
|
printKeyValueList(['userid', invitation['userId']])
|
|
printKeyValueList(['userEmail', userEmail])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(invitations), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
if not FJQC.formatJSON:
|
|
for invitation in invitations:
|
|
invitation['courseName'] = _getCourseName(croom, courseNames, invitation['courseId'])
|
|
invitation['userEmail'] = userEmail
|
|
csvPF.WriteRow(invitation)
|
|
else:
|
|
csvPF.WriteRowNoFilter({'userEmail': userEmail,
|
|
'JSON': json.dumps(cleanJSON(invitations), ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('ClassroomInvitations')
|
|
|
|
# gam delete classroominvitation courses <CourseEntity> (ids <ClassroomInvitationIDEntity>)|(role all|owner|student|teacher)
|
|
def doDeleteClassroomInvitations():
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
courseIdList = invitationIds = None
|
|
role = CLASSROOM_ROLE_ALL
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'course', 'courses', 'class', 'classes'}:
|
|
courseIdList = getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True)
|
|
elif myarg in {'id', 'ids'}:
|
|
invitationIds = getEntityList(Cmd.OB_CLASSROOM_INVITATION_ID_ENTITY)
|
|
elif myarg == 'role':
|
|
role = getChoice(CLASSROOM_ROLE_MAP, mapChoice=True)
|
|
else:
|
|
unknownArgumentExit()
|
|
if courseIdList is None:
|
|
missingArgumentExit('courses <CourseEntity>')
|
|
entityType = Ent.CLASSROOM_INVITATION
|
|
i, count, coursesInfo = _getCoursesOwnerInfo(croom, courseIdList, True)
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
i += 1
|
|
courseNameId = f'{courseInfo["name"]} ({courseId})'
|
|
if invitationIds is not None:
|
|
userInvitationIds = invitationIds
|
|
else:
|
|
status, userInvitationIds = _getClassroomInvitationIds(courseInfo['croom'], None, [courseId], role, i, count)
|
|
if status < 0:
|
|
continue
|
|
jcount = len(userInvitationIds)
|
|
entityPerformActionNumItems([Ent.COURSE, courseNameId], jcount, entityType, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for invitationId in userInvitationIds:
|
|
j += 1
|
|
try:
|
|
callGAPI(courseInfo['croom'].invitations(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
|
id=invitationId)
|
|
entityActionPerformed([Ent.COURSE, courseNameId, entityType, invitationId], j, jcount)
|
|
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.COURSE, courseNameId, entityType, invitationId], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
|
|
# gam show classroominvitations (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
|
# [role all|owner|student|teacher] [formatjson]
|
|
# gam print classroominvitations [todrive <ToDriveAttribute>*] (course|class <CourseEntity>)*|([teacher <UserItem>] [student <UserItem>] [states <CourseStateList>])
|
|
# [role all|owner|student|teacher] [formatjson [quotechar <Character>]]
|
|
def doPrintShowClassroomInvitations():
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
classroomEmails = {}
|
|
courseSelectionParameters = _initCourseSelectionParameters()
|
|
courseShowProperties = _initCourseShowProperties(['id', 'name'])
|
|
role = CLASSROOM_ROLE_ALL
|
|
csvPF = CSVPrintFile(['courseId', 'courseName']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getCourseSelectionParameters(myarg, courseSelectionParameters):
|
|
pass
|
|
elif myarg == 'role':
|
|
role = getChoice(CLASSROOM_ROLE_MAP, mapChoice=True)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
coursesInfo = _getCoursesInfo(croom, courseSelectionParameters, courseShowProperties)
|
|
if coursesInfo is None:
|
|
return
|
|
if csvPF and not FJQC.formatJSON:
|
|
csvPF.AddTitles(['id', 'role', 'userId', 'userEmail'])
|
|
csvPF.SetSortAllTitles()
|
|
entityType = CLASSROOM_ROLE_ENTITY_MAP[role]
|
|
i = 0
|
|
count = len(coursesInfo)
|
|
for course in coursesInfo:
|
|
i += 1
|
|
courseId = course['id']
|
|
courseName = course['name']
|
|
status, invitations = _getClassroomInvitations(croom, None, courseId, role, i, count)
|
|
if status > 0:
|
|
jcount = len(invitations)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.COURSE, f'{courseName} ({courseId})'], jcount, entityType, i, count)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
Ind.Increment()
|
|
j = 0
|
|
for invitation in invitations:
|
|
j += 1
|
|
printKeyValueListWithCount(['id', invitation['id']], j, jcount)
|
|
Ind.Increment()
|
|
printKeyValueList(['role', invitation['role']])
|
|
userId = invitation.get('userId')
|
|
if userId is not None:
|
|
printKeyValueList(['userid', userId])
|
|
printKeyValueList(['userEmail', _getClassroomEmail(croom, classroomEmails, userId, userId)])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(invitations), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
if not FJQC.formatJSON:
|
|
for invitation in invitations:
|
|
invitation['courseName'] = courseName
|
|
userId = invitation.get('userId')
|
|
if userId is not None:
|
|
invitation['userEmail'] = _getClassroomEmail(croom, classroomEmails, userId, userId)
|
|
csvPF.WriteRowNoFilter(invitation)
|
|
else:
|
|
csvPF.WriteRowNoFilter({'courseId': courseId, 'courseName': courseName,
|
|
'JSON': json.dumps(cleanJSON(invitations), ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('ClassroomInvitations')
|
|
|
|
# gam <UserTypeEntity> print classroomprofile [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show classroomprofile
|
|
def printShowClassroomProfile(users):
|
|
croom = buildGAPIObject(API.CLASSROOM)
|
|
csvPF = CSVPrintFile(['emailAddress', 'id',
|
|
f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}givenName',
|
|
f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}familyName',
|
|
f'name{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}fullName',
|
|
'photoUrl'], indexedTitles=['permissions']) if Act.csvFormat() else None
|
|
getTodriveOnly(csvPF)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
userId = normalizeEmailAddressOrUID(user)
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.CLASSROOM_USER_PROFILE, user, i, count)
|
|
try:
|
|
result = callGAPI(croom.userProfiles(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.BAD_REQUEST, GAPI.FORBIDDEN, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userId=userId, fields='*')
|
|
result.setdefault('verifiedTeacher', False)
|
|
if not csvPF:
|
|
printEntity([Ent.USER, userId], i, count)
|
|
Ind.Increment()
|
|
printKeyValueList(['email', result['emailAddress']])
|
|
printKeyValueList([UProp.PROPERTIES['id'][UProp.TITLE], result['id']])
|
|
for up in USER_NAME_PROPERTY_PRINT_ORDER:
|
|
if up in result['name']:
|
|
printKeyValueList([UProp.PROPERTIES[up][UProp.TITLE], result['name'][up]])
|
|
printKeyValueList([UProp.PROPERTIES['thumbnailPhotoUrl'][UProp.TITLE], result['photoUrl']])
|
|
printKeyValueList(['Permissions', ','.join([permission['permission'] for permission in result.get('permissions', [])])])
|
|
printKeyValueList(['Verified Teacher', result['verifiedTeacher']])
|
|
Ind.Decrement()
|
|
else:
|
|
csvPF.WriteRowTitles(flattenJSON(result))
|
|
except (GAPI.notFound, GAPI.permissionDenied, GAPI.badRequest, GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.USER, userId], str(e))
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Classroom User Profiles')
|
|
|
|
def _showASPs(user, asps, i=0, count=0):
|
|
Act.Set(Act.SHOW)
|
|
jcount = len(asps)
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.APPLICATION_SPECIFIC_PASSWORD, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return
|
|
Ind.Increment()
|
|
for asp in asps:
|
|
if asp['creationTime'] == '0':
|
|
created_date = UNKNOWN
|
|
else:
|
|
created_date = formatLocalTimestamp(asp['creationTime'])
|
|
if asp['lastTimeUsed'] == '0':
|
|
used_date = GC.NEVER
|
|
else:
|
|
used_date = formatLocalTimestamp(asp['lastTimeUsed'])
|
|
printKeyValueList(['ID', asp['codeId']])
|
|
Ind.Increment()
|
|
printKeyValueList(['Name', asp['name']])
|
|
printKeyValueList(['Created', created_date])
|
|
printKeyValueList(['Last Used', used_date])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delete asps|applicationspecificpasswords all|<AspIDList>
|
|
def deleteASP(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
codeIdList = getString(Cmd.OB_ASP_ID_LIST).lower()
|
|
if codeIdList == 'all':
|
|
allCodeIds = True
|
|
else:
|
|
allCodeIds = False
|
|
codeIds = codeIdList.replace(',', ' ').split()
|
|
for codeId in codeIds:
|
|
if not codeId.isdigit():
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.INVALID_ENTITY.format(Ent.Singular(Ent.APPLICATION_SPECIFIC_PASSWORD), Msg.MUST_BE_NUMERIC))
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if allCodeIds:
|
|
try:
|
|
asps = callGAPIitems(cd.asps(), 'list', 'items',
|
|
throwReasons=[GAPI.USER_NOT_FOUND],
|
|
userKey=user, fields='items(codeId)')
|
|
codeIds = [asp['codeId'] for asp in asps]
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
continue
|
|
jcount = len(codeIds)
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.APPLICATION_SPECIFIC_PASSWORD, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for codeId in codeIds:
|
|
j += 1
|
|
try:
|
|
callGAPI(cd.asps(), 'delete',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_PARAMETER, GAPI.FORBIDDEN],
|
|
userKey=user, codeId=codeId)
|
|
entityActionPerformed([Ent.USER, user, Ent.APPLICATION_SPECIFIC_PASSWORD, codeId], j, jcount)
|
|
except (GAPI.invalid, GAPI.invalidParameter, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.APPLICATION_SPECIFIC_PASSWORD, codeId], str(e), j, jcount)
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print asps|applicationspecificpasswords [todrive <ToDriveAttribute>*]
|
|
# [oneitemperrow]
|
|
# gam <UserTypeEntity> show asps|applicationspecificpasswords
|
|
def printShowASPs(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['User']) if Act.csvFormat() else None
|
|
oneItemPerRow = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.APPLICATION_SPECIFIC_PASSWORD, user, i, count)
|
|
try:
|
|
asps = callGAPIitems(cd.asps(), 'list', 'items',
|
|
throwReasons=[GAPI.USER_NOT_FOUND],
|
|
userKey=user)
|
|
if not csvPF:
|
|
_showASPs(user, asps, i, count)
|
|
else:
|
|
for asp in asps:
|
|
asp.pop('userKey', None)
|
|
if asp['creationTime'] == '0':
|
|
asp['creationTime'] = UNKNOWN
|
|
else:
|
|
asp['creationTime'] = formatLocalTimestamp(asp['creationTime'])
|
|
if asp['lastTimeUsed'] == '0':
|
|
asp['lastTimeUsed'] = GC.NEVER
|
|
else:
|
|
asp['lastTimeUsed'] = formatLocalTimestamp(asp['lastTimeUsed'])
|
|
if not oneItemPerRow:
|
|
csvPF.WriteRowTitles(flattenJSON({'asps': asps}, flattened={'User': user}))
|
|
else:
|
|
for asp in asps:
|
|
csvPF.WriteRowTitles(flattenJSON({'asp': asp}, flattened={'User': user}))
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Application Specific Passwords')
|
|
|
|
def _showBackupCodes(user, codes, i, count):
|
|
Act.Set(Act.SHOW)
|
|
jcount = 0
|
|
for code in codes:
|
|
if code.get('verificationCode'):
|
|
jcount += 1
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.BACKUP_VERIFICATION_CODES, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return
|
|
Ind.Increment()
|
|
j = 0
|
|
for code in codes:
|
|
j += 1
|
|
printKeyValueList([f'{j:2}', code.get('verificationCode')])
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> update backupcodes|verificationcodes
|
|
def updateBackupCodes(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
userSuspended = checkUserSuspended(cd, user, Ent.USER, i, count)
|
|
if userSuspended is None:
|
|
continue
|
|
if not userSuspended:
|
|
try:
|
|
callGAPI(cd.verificationCodes(), 'generate',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_INPUT],
|
|
userKey=user)
|
|
codes = callGAPIitems(cd.verificationCodes(), 'list', 'items',
|
|
throwReasons=[GAPI.USER_NOT_FOUND],
|
|
userKey=user, fields='items(verificationCode)')
|
|
_showBackupCodes(user, codes, i, count)
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
except (GAPI.invalid, GAPI.invalidInput) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None], str(e), i, count)
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None],
|
|
Msg.IS_SUSPENDED_NO_BACKUPCODES, i, count)
|
|
|
|
# gam <UserTypeEntity> delete backupcodes|verificationcodes
|
|
def deleteBackupCodes(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
userSuspended = checkUserSuspended(cd, user, Ent.USER, i, count)
|
|
if userSuspended is None:
|
|
continue
|
|
if not userSuspended:
|
|
try:
|
|
callGAPI(cd.verificationCodes(), 'invalidate',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_INPUT],
|
|
userKey=user)
|
|
printEntityKVList([Ent.USER, user], [Ent.Plural(Ent.BACKUP_VERIFICATION_CODES), '', 'Invalidated'], i, count)
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
except (GAPI.invalid, GAPI.invalidInput) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None], str(e), i, count)
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None],
|
|
Msg.IS_SUSPENDED_NO_BACKUPCODES, i, count)
|
|
|
|
# gam <UserTypeEntity> print backupcodes|verificationcodes [todrive <ToDriveAttribute>*]
|
|
# [delimiter <Character>] [countsonly]
|
|
# gam <UserTypeEntity> show backupcodes|verificationcodes
|
|
def printShowBackupCodes(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile(['User', 'verificationCodesCount', 'verificationCodes']) if Act.csvFormat() else None
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
counts_only = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg == 'countsonly':
|
|
counts_only = True
|
|
else:
|
|
unknownArgumentExit()
|
|
# if we're only getting counts, we don't want actual codes pulled down
|
|
if counts_only:
|
|
csvPF.RemoveTitles('verificationCodes')
|
|
fields = 'items(etag)'
|
|
else:
|
|
fields = 'items(verificationCode)'
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.BACKUP_VERIFICATION_CODES, user, i, count)
|
|
try:
|
|
codes = callGAPIitems(cd.verificationCodes(), 'list', 'items',
|
|
throwReasons=[GAPI.USER_NOT_FOUND],
|
|
userKey=user, fields=fields)
|
|
if not csvPF:
|
|
_showBackupCodes(user, codes, i, count)
|
|
elif counts_only:
|
|
csvPF.WriteRow({'User': user, 'verificationCodesCount': len(codes)})
|
|
else:
|
|
csvPF.WriteRow({'User': user,
|
|
'verificationCodesCount': len(codes),
|
|
'verificationCodes': delimiter.join([code['verificationCode'] for code in codes if 'verificationCode' in code])})
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Backup Verification Codes')
|
|
|
|
def _getCalendarSelectProperty(myarg, kwargs):
|
|
if myarg == 'minaccessrole':
|
|
kwargs['minAccessRole'] = getChoice(CALENDAR_ACL_ROLES_MAP, mapChoice=True)
|
|
elif myarg == 'showdeleted':
|
|
kwargs['showDeleted'] = True
|
|
elif myarg == 'showhidden':
|
|
kwargs['showHidden'] = True
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def initUserCalendarEntity():
|
|
return {'list': [], 'kwargs': {}, 'dict': None, 'all': False, 'primary': False, 'resourceIds': []}
|
|
|
|
def getUserCalendarEntity(default='primary', noSelectionKwargs=None):
|
|
|
|
def _initCourseCalendarSelectionParameters():
|
|
return {'courseIds': [], 'teacherId': None, 'myCoursesAsTeacher': False,
|
|
'studentId': None, 'myCoursesAsStudent': False, 'courseStates': []}
|
|
|
|
def _getCourseCalendarSelectionParameters(myarg):
|
|
if myarg in {'course', 'courses', 'class', 'classes'}:
|
|
courseSelectionParameters['courseIds'].extend(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True))
|
|
elif myarg == 'courseswithteacher':
|
|
courseSelectionParameters['teacherId'] = getEmailAddress()
|
|
courseSelectionParameters['myCoursesAsTeacher'] = False
|
|
elif myarg == 'mycoursesasteacher':
|
|
courseSelectionParameters['myCoursesAsTeacher'] = True
|
|
courseSelectionParameters['teacherId'] = None
|
|
elif myarg == 'courseswithstudent':
|
|
courseSelectionParameters['studentId'] = getEmailAddress()
|
|
courseSelectionParameters['myCoursesAsStudent'] = False
|
|
elif myarg == 'mycoursesasstudent':
|
|
courseSelectionParameters['myCoursesAsStudent'] = True
|
|
courseSelectionParameters['studentId'] = None
|
|
elif myarg in {'coursestate', 'coursestates', 'coursestatus'}:
|
|
_getCourseStates(Cmd.OB_COURSE_STATE_LIST, courseSelectionParameters['courseStates'])
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _noSelectionMade():
|
|
return (not calendarEntity['list'] and not calendarEntity['kwargs'] and calendarEntity['dict'] is None and
|
|
not calendarEntity['all'] and not calendarEntity['primary'] and not calendarEntity['resourceIds'] and
|
|
not courseCalendarSelected)
|
|
|
|
calendarEntity = initUserCalendarEntity()
|
|
courseSelectionParameters = _initCourseCalendarSelectionParameters()
|
|
courseCalendarSelected = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'calendar', 'calendars'}:
|
|
entitySelector = getEntitySelector()
|
|
if entitySelector:
|
|
entityList = getEntitySelection(entitySelector, False)
|
|
if isinstance(entityList, dict):
|
|
calendarEntity['dict'] = entityList
|
|
else:
|
|
calendarEntity['list'] = entityList
|
|
else:
|
|
calendarEntity['list'].extend(convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST)))
|
|
elif myarg == 'allcalendars':
|
|
calendarEntity['all'] = True
|
|
elif myarg == 'primary':
|
|
calendarEntity['primary'] = True
|
|
elif _getCalendarSelectProperty(myarg, calendarEntity['kwargs']):
|
|
pass
|
|
elif myarg == 'resource':
|
|
calendarEntity['resourceIds'].append(getString(Cmd.OB_RESOURCE_ID))
|
|
elif myarg == 'resources':
|
|
calendarEntity['resourceIds'].extend(convertEntityToList(getString(Cmd.OB_RESOURCE_ID, minLen=0), shlexSplit=True))
|
|
elif _getCourseCalendarSelectionParameters(myarg):
|
|
courseCalendarSelected = True
|
|
elif _noSelectionMade() and (myarg.find('@') != -1 or myarg.find('id:') != -1):
|
|
Cmd.Backup()
|
|
calendarEntity['list'].append(getEmailAddress())
|
|
else:
|
|
Cmd.Backup()
|
|
break
|
|
if _noSelectionMade():
|
|
if not noSelectionKwargs:
|
|
calendarEntity[default] = True
|
|
else:
|
|
calendarEntity['all'] = True
|
|
calendarEntity['kwargs'].update(noSelectionKwargs)
|
|
elif (courseCalendarSelected and
|
|
(courseSelectionParameters['courseIds'] or
|
|
courseSelectionParameters['teacherId'] or courseSelectionParameters['myCoursesAsTeacher'] or
|
|
courseSelectionParameters['studentId'] or courseSelectionParameters['myCoursesAsStudent'])):
|
|
calendarEntity['courseSelectionParameters'] = courseSelectionParameters
|
|
calendarEntity['courseShowProperties'] = _initCourseShowProperties(['calendarId'])
|
|
calendarEntity['croom'] = buildGAPIObject(API.CLASSROOM)
|
|
return calendarEntity
|
|
|
|
def _validateUserGetCalendarIds(user, i, count, calendarEntity,
|
|
itemType=None, modifier=None, showAction=True, setRC=True, newCalId=None, secondaryCalendarsOnly=False):
|
|
if user and calendarEntity['dict']:
|
|
calIds = calendarEntity['dict'][user][:]
|
|
else:
|
|
calIds = calendarEntity['list'][:]
|
|
user, cal = validateCalendar(user, i, count, noClientAccess=True)
|
|
if not cal:
|
|
return (user, None, None, 0)
|
|
if calendarEntity['resourceIds']:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
for resourceId in calendarEntity['resourceIds']:
|
|
try:
|
|
calIds.append(callGAPI(cd.resources().calendars(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId,
|
|
fields='resourceEmail')['resourceEmail'])
|
|
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
|
|
return (user, None, None, 0)
|
|
courseSelectionParameters = calendarEntity.get('courseSelectionParameters')
|
|
if courseSelectionParameters is not None:
|
|
if courseSelectionParameters['myCoursesAsTeacher']:
|
|
courseSelectionParameters['teacherId'] = user
|
|
if courseSelectionParameters['myCoursesAsStudent']:
|
|
courseSelectionParameters['studentId'] = user
|
|
coursesInfo = _getCoursesInfo(calendarEntity['croom'], courseSelectionParameters,
|
|
calendarEntity['courseShowProperties'])
|
|
if coursesInfo is None:
|
|
return (user, None, None, 0)
|
|
calIds.extend([course['calendarId'] for course in coursesInfo if 'calendarId' in course])
|
|
if calendarEntity['primary']:
|
|
calIds.append(user)
|
|
try:
|
|
if calendarEntity['kwargs'] or calendarEntity['all']:
|
|
result = callGAPIpages(cal.calendarList(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS,
|
|
fields='nextPageToken,items/id', **calendarEntity['kwargs'])
|
|
calIds.extend([calId['id'] for calId in result if not secondaryCalendarsOnly or calId['id'].find('@group.calendar.google.com') != -1])
|
|
else:
|
|
callGAPI(cal.calendars(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS,
|
|
calendarId='primary', fields='')
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
return (user, None, None, 0)
|
|
if newCalId:
|
|
newcal = buildGAPIObject(API.CALENDAR)
|
|
if not checkCalendarExists(newcal, newCalId, i, count):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, newCalId], Msg.DOES_NOT_EXIST, i, count)
|
|
return (user, None, None, 0)
|
|
jcount = len(calIds)
|
|
if setRC and jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if showAction:
|
|
if not itemType:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.CALENDAR, i, count)
|
|
elif not newCalId:
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, user], itemType, modifier, jcount, Ent.CALENDAR, i, count)
|
|
else:
|
|
entityPerformActionSubItemModifierNumItemsModifierNewValue([Ent.USER, user], itemType, modifier, jcount, Ent.CALENDAR, Act.MODIFIER_TO, newCalId, i, count)
|
|
return (user, cal, calIds, jcount)
|
|
|
|
CALENDAR_NOTIFICATION_METHODS = ['email']
|
|
CALENDAR_NOTIFICATION_TYPES_MAP = {
|
|
'eventcreation': 'eventCreation',
|
|
'eventchange': 'eventChange',
|
|
'eventcancellation': 'eventCancellation',
|
|
'eventresponse': 'eventResponse',
|
|
'agenda': 'agenda',
|
|
}
|
|
|
|
def _getCalendarAttributes(body, returnOnUnknownArgument=False):
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'selected':
|
|
body['selected'] = getBoolean()
|
|
elif myarg == 'hidden':
|
|
body['hidden'] = getBoolean()
|
|
elif myarg == 'summary':
|
|
body['summaryOverride'] = getString(Cmd.OB_STRING)
|
|
elif myarg in {'color', 'colour'}:
|
|
body['colorId'] = getChoice(CALENDAR_COLOR_MAP, mapChoice=True)
|
|
elif myarg in {'colorindex', 'colorid', 'colourindex', 'colourid'}:
|
|
body['colorId'] = getInteger(minVal=CALENDAR_MIN_COLOR_INDEX, maxVal=CALENDAR_MAX_COLOR_INDEX)
|
|
elif myarg in {'backgroundcolor', 'backgroundcolour'}:
|
|
body['backgroundColor'] = getColor()
|
|
body.setdefault('foregroundColor', '#000000')
|
|
elif myarg in {'foregroundcolor', 'foregroundcolour'}:
|
|
body['foregroundColor'] = getColor()
|
|
elif myarg == 'reminder':
|
|
body.setdefault('defaultReminders', [])
|
|
if not checkArgumentPresent(Cmd.CLEAR_NONE_ARGUMENT):
|
|
body['defaultReminders'].append(getCalendarReminder(True))
|
|
elif myarg == 'notification':
|
|
body.setdefault('notificationSettings', {'notifications': []})
|
|
method = getChoice(CALENDAR_NOTIFICATION_METHODS+Cmd.CLEAR_NONE_ARGUMENT)
|
|
if method not in Cmd.CLEAR_NONE_ARGUMENT:
|
|
for ntype in _getFieldsList():
|
|
if ntype in CALENDAR_NOTIFICATION_TYPES_MAP:
|
|
body['notificationSettings']['notifications'].append({'method': method,
|
|
'type': CALENDAR_NOTIFICATION_TYPES_MAP[ntype]})
|
|
else:
|
|
invalidChoiceExit(ntype, CALENDAR_NOTIFICATION_TYPES_MAP, True)
|
|
else:
|
|
body['notificationSettings']['notifications'] = []
|
|
elif returnOnUnknownArgument:
|
|
Cmd.Backup()
|
|
return
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
def _showCalendar(calendar, j, jcount, FJQC, acls=None):
|
|
if FJQC.formatJSON:
|
|
if acls:
|
|
calendar['acls'] = [{'id': rule['id'], 'role': rule['role']} for rule in acls]
|
|
printLine(json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
_showCalendarSettings(calendar, j, jcount)
|
|
Ind.Increment()
|
|
if 'primary' in calendar:
|
|
printKeyValueList(['Primary', calendar['primary']])
|
|
if 'accessRole' in calendar:
|
|
printKeyValueList(['Access Level', calendar['accessRole']])
|
|
if 'deleted' in calendar:
|
|
printKeyValueList(['Deleted', calendar['deleted']])
|
|
if 'hidden' in calendar:
|
|
printKeyValueList(['Hidden', calendar['hidden']])
|
|
if 'selected' in calendar:
|
|
printKeyValueList(['Selected', calendar['selected']])
|
|
if 'colorId' in calendar:
|
|
printKeyValueList(['Color ID', calendar['colorId'], 'Background Color', calendar['backgroundColor'], 'Foreground Color', calendar['foregroundColor']])
|
|
if 'defaultReminders' in calendar:
|
|
printKeyValueList(['Default Reminders', None])
|
|
Ind.Increment()
|
|
for reminder in calendar['defaultReminders']:
|
|
printKeyValueList(['Method', reminder['method'], 'Minutes', reminder['minutes']])
|
|
Ind.Decrement()
|
|
if 'notificationSettings' in calendar:
|
|
printKeyValueList(['Notifications', None])
|
|
Ind.Increment()
|
|
for notification in calendar['notificationSettings'].get('notifications', []):
|
|
printKeyValueList(['Method', notification['method'], 'Type', notification['type']])
|
|
Ind.Decrement()
|
|
if acls:
|
|
j = 0
|
|
jcount = len(acls)
|
|
printEntitiesCount(Ent.CALENDAR_ACL, acls)
|
|
Ind.Increment()
|
|
for rule in acls:
|
|
j += 1
|
|
printKeyValueListWithCount(ACLRuleKeyValueList(rule), j, jcount)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# Process CalendarList functions
|
|
def _processCalendarList(user, i, count, calId, j, jcount, cal, function, **kwargs):
|
|
try:
|
|
callGAPI(cal.calendarList(), function,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.DUPLICATE, GAPI.UNKNOWN_ERROR,
|
|
GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION],
|
|
**kwargs)
|
|
entityActionPerformed([Ent.USER, user, Ent.CALENDAR, calId], j, jcount)
|
|
except (GAPI.notFound, GAPI.duplicate, GAPI.unknownError, GAPI.serviceNotAvailable,
|
|
GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnPrimarySubscription) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> add calendars <UserCalendarAddEntity> <CalendarAttribute>*
|
|
def addCalendars(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
body = {'selected': True, 'hidden': False}
|
|
_getCalendarAttributes(body)
|
|
colorRgbFormat = 'backgroundColor' in body or 'foregroundColor' in body
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for calId in calIds:
|
|
j += 1
|
|
body['id'] = calId = normalizeCalendarId(calId, user)
|
|
_processCalendarList(user, i, count, calId, j, jcount, cal, 'insert',
|
|
body=body, colorRgbFormat=colorRgbFormat, fields='')
|
|
Ind.Decrement()
|
|
|
|
def _updateDeleteCalendars(users, calendarEntity, function, **kwargs):
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for calId in calIds:
|
|
j += 1
|
|
calId = normalizeCalendarId(calId, user)
|
|
_processCalendarList(user, i, count, calId, j, jcount, cal, function,
|
|
calendarId=calId, **kwargs)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> update calendars <UserCalendarEntity> <CalendarAttribute>+
|
|
def updateCalendars(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
body = {}
|
|
_getCalendarAttributes(body)
|
|
colorRgbFormat = 'backgroundColor' in body or 'foregroundColor' in body
|
|
_updateDeleteCalendars(users, calendarEntity, 'patch', body=body, colorRgbFormat=colorRgbFormat, fields='')
|
|
|
|
# gam <UserTypeEntity> delete calendars <UserCalendarEntity>
|
|
def deleteCalendars(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
checkForExtraneousArguments()
|
|
_updateDeleteCalendars(users, calendarEntity, 'delete')
|
|
|
|
|
|
# gam <UserTypeEntity> create calendars <CalendarSettings>
|
|
def createCalendar(users):
|
|
calendarEntity = initUserCalendarEntity()
|
|
body = getCalendarSettings(summaryRequired=True)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal, _, _ = _validateUserGetCalendarIds(user, i, count, calendarEntity, showAction=False, setRC=False)
|
|
if not cal:
|
|
continue
|
|
try:
|
|
calId = callGAPI(cal.calendars(), 'insert',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.FORBIDDEN],
|
|
body=body, fields='id')['id']
|
|
entityActionPerformed([Ent.USER, user, Ent.CALENDAR, calId], i, count)
|
|
except GAPI.forbidden as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
|
|
def addCreateCalendars(users):
|
|
if Act.Get() == Act.ADD:
|
|
addCalendars(users)
|
|
else:
|
|
createCalendar(users)
|
|
|
|
def _modifyRemoveCalendars(users, calendarEntity, function, **kwargs):
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for calId in calIds:
|
|
j += 1
|
|
calId = normalizeCalendarId(calId, user)
|
|
try:
|
|
callGAPI(cal.calendars(), function,
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.CANNOT_DELETE_PRIMARY_CALENDAR,
|
|
GAPI.FORBIDDEN, GAPI.INVALID, GAPI.REQUIRED_ACCESS_LEVEL],
|
|
calendarId=calId, **kwargs)
|
|
entityActionPerformed([Ent.USER, user, Ent.CALENDAR, calId], j, jcount)
|
|
except (GAPI.notFound, GAPI.cannotDeletePrimaryCalendar, GAPI.forbidden, GAPI.invalid, GAPI.requiredAccessLevel) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> modify calendars <UserCalendarEntity> <CalendarSettings>
|
|
def modifyCalendars(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
body = getCalendarSettings(summaryRequired=False)
|
|
_modifyRemoveCalendars(users, calendarEntity, 'patch', body=body)
|
|
|
|
# gam <UserTypeEntity> remove calendars <UserCalendarEntity>
|
|
def removeCalendars(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
checkForExtraneousArguments()
|
|
_modifyRemoveCalendars(users, calendarEntity, 'delete')
|
|
|
|
def _getCalendarPermissions(cal, calendar):
|
|
if calendar['accessRole'] == 'owner':
|
|
try:
|
|
return callGAPIpages(cal.acl(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
calendarId=calendar['id'], fields='nextPageToken,items(id,role,scope)')
|
|
except (GAPI.notACalendarUser, GAPI.notFound):
|
|
pass
|
|
return []
|
|
|
|
CALENDAR_LIST_FIELDS_CHOICE_MAP = {
|
|
"accessrole": 'accessRole',
|
|
"backgroundcolor": 'backgroundColor',
|
|
"backgroundcolour": 'backgroundColor',
|
|
"colorid": 'colorId',
|
|
"conferenceproperties": 'conferenceProperties',
|
|
"defaultreminders": 'defaultReminders',
|
|
"deleted": 'deleted',
|
|
"description": 'description',
|
|
"foregroundcolor": 'foregroundColor',
|
|
"foregroundcolour": 'foregroundColor',
|
|
"hidden": 'hidden',
|
|
"id": 'id',
|
|
"location": 'location',
|
|
"notificationsettings": 'notificationSettings',
|
|
"primary": 'primary',
|
|
"selected": 'selected',
|
|
"summary": ['summary', 'summaryOverride'],
|
|
"summaryoverride": ['summary', 'summaryOverride'],
|
|
"timezone": 'timeZone',
|
|
}
|
|
|
|
# gam <UserTypeEntity> info calendars <UserCalendarEntity>
|
|
# [fields <CalendarFieldList>] [permissions]
|
|
# [formatjson]
|
|
def infoCalendars(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
FJQC = FormatJSONQuoteChar()
|
|
fieldsList = []
|
|
acls = []
|
|
getCalPermissions = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
|
|
getCalPermissions = True
|
|
elif getFieldsList(myarg, CALENDAR_LIST_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if fieldsList:
|
|
if getCalPermissions:
|
|
fieldsList.append('accessRole')
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, showAction=not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for calId in calIds:
|
|
j += 1
|
|
calId = normalizeCalendarId(calId, user)
|
|
try:
|
|
result = callGAPI(cal.calendarList(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
calendarId=calId, fields=fields)
|
|
if getCalPermissions:
|
|
acls = _getCalendarPermissions(cal, result)
|
|
_showCalendar(result, j, jcount, FJQC, acls)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
CALENDAR_SIMPLE_LISTS = {'allowedConferenceSolutionTypes'}
|
|
CALENDAR_EXCLUDE_OPTIONS = {'noprimary', 'nogroups', 'noresources', 'nosystem', 'nousers'}
|
|
CALENDAR_EXCLUDE_DOMAINS = {
|
|
'nogroups': 'group.calendar.google.com',
|
|
'noresources': 'resource.calendar.google.com',
|
|
'nosystem': 'group.v.calendar.google.com',
|
|
}
|
|
|
|
# gam <UserTypeEntity> print calendars <UserCalendarEntity> [todrive <ToDriveAttribute>*]
|
|
# [primary] <CalendarSelectProperty>* [noprimary] [nogroups] [noresources] [nosystem] [nousers]
|
|
# [fields <CalendarFieldList>] [permissions]
|
|
# [formatjson [quotechar <Character>]] [delimiter <Character>]
|
|
# gam <UserTypeEntity> show calendars <UserCalendarEntity>
|
|
# [primary] <CalendarSelectProperty>* [noprimary] [nogroups] [noresources] [nosystem] [nousers]
|
|
# [fields <CalendarFieldList>] [permissions]
|
|
# [formatjson]
|
|
def printShowCalendars(users):
|
|
acls = []
|
|
getCalPermissions = noPrimary = primaryOnly = False
|
|
excludes = set()
|
|
excludeDomains = set()
|
|
csvPF = CSVPrintFile(['primaryEmail', 'calendarId'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
kwargs = {}
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in [Cmd.ARG_ACLS, Cmd.ARG_CALENDARACLS, Cmd.ARG_PERMISSIONS]:
|
|
getCalPermissions = True
|
|
elif myarg == 'allcalendars':
|
|
pass
|
|
elif myarg == 'primary':
|
|
primaryOnly = True
|
|
elif _getCalendarSelectProperty(myarg, kwargs):
|
|
pass
|
|
elif myarg in CALENDAR_EXCLUDE_OPTIONS:
|
|
excludes.add(myarg)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif getFieldsList(myarg, CALENDAR_LIST_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
for exclude in excludes:
|
|
if exclude == 'noprimary':
|
|
noPrimary = True
|
|
elif exclude == 'nousers':
|
|
excludeDomains.add(GC.Values[GC.DOMAIN])
|
|
else:
|
|
excludeDomains.add(CALENDAR_EXCLUDE_DOMAINS[exclude])
|
|
if fieldsList:
|
|
if getCalPermissions:
|
|
fieldsList.append('accessRole')
|
|
if noPrimary or primaryOnly:
|
|
fieldsList.append('primary')
|
|
fields = getItemFieldsFromFieldsList('items', fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal = validateCalendar(user, i, count, noClientAccess=True)
|
|
if not cal:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.CALENDAR, user, i, count)
|
|
try:
|
|
calendars = callGAPIpages(cal.calendarList(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS,
|
|
fields=fields, **kwargs)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
if primaryOnly:
|
|
for calendar in calendars:
|
|
if calendar.get('primary', False):
|
|
calendars = [calendar]
|
|
break
|
|
else:
|
|
calendars = []
|
|
elif noPrimary or excludeDomains:
|
|
allCalendars = calendars[:]
|
|
calendars = []
|
|
for calendar in allCalendars:
|
|
primary = calendar.get('primary', False)
|
|
if noPrimary and primary:
|
|
continue
|
|
if not primary and excludeDomains:
|
|
_, domain = splitEmailAddress(calendar['id'])
|
|
if domain in excludeDomains:
|
|
continue
|
|
calendars.append(calendar)
|
|
jcount = len(calendars)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.CALENDAR, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for calendar in calendars:
|
|
j += 1
|
|
if getCalPermissions:
|
|
acls = _getCalendarPermissions(cal, calendar)
|
|
_showCalendar(calendar, j, jcount, FJQC, acls)
|
|
Ind.Decrement()
|
|
else:
|
|
if calendars:
|
|
for calendar in calendars:
|
|
row = {'primaryEmail': user, 'calendarId': calendar['id']}
|
|
if getCalPermissions:
|
|
flattenJSON({'permissions': _getCalendarPermissions(cal, calendar)}, flattened=row)
|
|
flattenJSON(calendar, flattened=row, simpleLists=CALENDAR_SIMPLE_LISTS, delimiter=delimiter)
|
|
if not FJQC.formatJSON:
|
|
row.pop('id')
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'primaryEmail': user, 'calendarId': calendar['id'],
|
|
'JSON': json.dumps(cleanJSON(calendar), ensure_ascii=False, sort_keys=True)})
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'primaryEmail': user})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Calendars')
|
|
|
|
USER_CALENDAR_SETTINGS_FIELDS_CHOICE_MAP = {
|
|
'autoaddhangouts': 'autoAddHangouts',
|
|
'datefieldorder': 'dateFieldOrder',
|
|
'defaulteventlength': 'defaultEventLength',
|
|
'format24hourtime': 'format24HourTime',
|
|
'hideinvitations': 'hideInvitations',
|
|
'hideweekends': 'hideWeekends',
|
|
'locale': 'locale',
|
|
'remindonrespondedeventsonly': 'remindOnRespondedEventsOnly',
|
|
'showdeclinedevents': 'showDeclinedEvents',
|
|
'timezone': 'timezone',
|
|
'usekeyboardshortcuts': 'useKeyboardShortcuts',
|
|
'weekstart': 'weekStart'
|
|
}
|
|
|
|
# gam <UserTypeEntity> print calsettings [todrive <ToDriveAttribute>*]
|
|
# [fields <UserCalendarSettingsFieldList>]
|
|
# [formatjson] [quotechar <Character>}
|
|
# gam <UserTypeEntity> show calsettings
|
|
# [formatjson]
|
|
def printShowCalSettings(users):
|
|
csvPF = CSVPrintFile(['User'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getFieldsList(myarg, USER_CALENDAR_SETTINGS_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
fields = set(fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal = validateCalendar(user, i, count, noClientAccess=True)
|
|
if not cal:
|
|
continue
|
|
try:
|
|
feed = callGAPIpages(cal.settings(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
settings = {}
|
|
for setting in feed:
|
|
if not fields or setting['id'] in fields:
|
|
settings[setting['id']] = setting['value']
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
printEntityKVList([Ent.USER, user], [Ent.Plural(Ent.CALENDAR_SETTINGS), None], i, count)
|
|
Ind.Increment()
|
|
for attr in sorted(settings):
|
|
printKeyValueList([attr, settings[attr]])
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps({'User': user, 'settings': settings}, ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
row = flattenJSON(settings, flattened={'User': user})
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'User': user, 'JSON': json.dumps(settings, ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Calendar Settings')
|
|
|
|
# gam <UserTypeEntity> create calendaracls <UserCalendarEntity> <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
|
|
def createCalendarACLs(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.CALENDAR_ACL, Act.MODIFIER_TO)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_doCalendarsCreateACLs(origUser, user, cal, calIds, jcount, role, ACLScopeEntity, sendNotifications)
|
|
Ind.Decrement()
|
|
|
|
def updateDeleteCalendarACLs(users, calendarEntity, function, modifier, ACLScopeEntity, role, sendNotifications):
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.CALENDAR_ACL, modifier)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_doUpdateDeleteCalendarACLs(origUser, user, cal, function, calIds, jcount, ACLScopeEntity, role, sendNotifications)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> update calendaracls <UserCalendarEntity> <CalendarACLRole> <CalendarACLScopeEntity> [sendnotifications <Boolean>]
|
|
def updateCalendarACLs(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
|
|
updateDeleteCalendarACLs(users, calendarEntity, 'patch', Act.MODIFIER_IN, ACLScopeEntity, role, sendNotifications)
|
|
|
|
# gam <UserTypeEntity> delete calendaracls <UserCalendarEntity> [<CalendarACLRole>] <CalendarACLScopeEntity>
|
|
def deleteCalendarACLs(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
role, ACLScopeEntity = getCalendarDeleteACLsOptions(True)
|
|
updateDeleteCalendarACLs(users, calendarEntity, 'delete', Act.MODIFIER_FROM, ACLScopeEntity, role, False)
|
|
|
|
# gam <UserTypeEntity> info calendaracls <UserCalendarEntity> <CalendarACLScopeEntity>
|
|
# [formatjson]
|
|
def infoCalendarACLs(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
ACLScopeEntity = getCalendarSiteACLScopeEntity()
|
|
FJQC = _getCalendarInfoACLOptions()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.CALENDAR_ACL, Act.MODIFIER_FROM, showAction=not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_doInfoCalendarACLs(origUser, user, cal, calIds, jcount, ACLScopeEntity, FJQC)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print calendaracls <UserCalendarEntity> [todrive <ToDriveAttribute>*]
|
|
# [noselfowner] (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show calendaracls <UserCalendarEntity>
|
|
# [noselfowner]
|
|
# [formatjson]
|
|
def printShowCalendarACLs(users):
|
|
calendarEntity = getUserCalendarEntity(default='all')
|
|
csvPF, FJQC, noSelfOwner, addCSVData = _getCalendarPrintShowACLOptions(['primaryEmail', 'calendarId'])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.CALENDAR_ACL, Act.MODIFIER_FROM, showAction=not csvPF and not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for calId in calIds:
|
|
j += 1
|
|
calId = convertUIDtoEmailAddress(calId)
|
|
_printShowCalendarACLs(cal, user, Ent.CALENDAR, calId, j, jcount, csvPF, FJQC, noSelfOwner, addCSVData)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Calendar ACLs')
|
|
|
|
TRANSFER_CALENDAR_APPEND_FIELDS = ['description', 'location', 'summary']
|
|
|
|
# gam <UserTypeEntity> transfer calendars|seccals <UserItem> [<UserCalendarEntity>]
|
|
# [keepuser | (retainrole <CalendarACLRole>)] [sendnotifications <Boolean>] [noretentionmessages]
|
|
# <CalendarSettings>* [append description|location|summary] [noupdatemessages]
|
|
# [deletefromoldowner] [addtonewowner <CalendarAttribute>*] [nolistmessages]
|
|
def transferCalendars(users):
|
|
targetUser = getEmailAddress()
|
|
calendarEntity = getUserCalendarEntity(noSelectionKwargs={'minAccessRole': 'owner', 'showHidden': True})
|
|
notAllowedForbidden = Msg.FORBIDDEN if (not calendarEntity['all']) and (not calendarEntity.get('kwargs', {}).get('minAccessRole', '') == 'owner') else Msg.NOT_ALLOWED
|
|
retainRoleBody = {'role': 'none'}
|
|
sendNotifications = showListMessages = showRetentionMessages = showUpdateMessages = True
|
|
updateBody = {}
|
|
appendFieldsList = []
|
|
addToNewOwner = deleteFromOldOwner = False
|
|
targetListBody = {'selected': True, 'hidden': False}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'keepuser':
|
|
retainRoleBody['role'] = 'owner'
|
|
elif myarg == 'retainrole':
|
|
retainRoleBody['role'] = getChoice(CALENDAR_ACL_ROLES_MAP, mapChoice=True)
|
|
elif myarg == 'sendnotifications':
|
|
sendNotifications = getBoolean()
|
|
elif _getCalendarSetting(myarg, updateBody):
|
|
pass
|
|
elif myarg == 'append':
|
|
for field in _getFieldsList():
|
|
if field in TRANSFER_CALENDAR_APPEND_FIELDS:
|
|
appendFieldsList.append(field)
|
|
else:
|
|
invalidChoiceExit(field, TRANSFER_CALENDAR_APPEND_FIELDS, True)
|
|
elif myarg == 'nolistmessages':
|
|
showListMessages = False
|
|
elif myarg == 'noretentionmessages':
|
|
showRetentionMessages = False
|
|
elif myarg == 'noupdatemessages':
|
|
showUpdateMessages = False
|
|
elif myarg == 'deletefromoldowner':
|
|
deleteFromOldOwner = True
|
|
elif myarg == 'addtonewowner':
|
|
addToNewOwner = True
|
|
_getCalendarAttributes(targetListBody, returnOnUnknownArgument=True)
|
|
else:
|
|
unknownArgumentExit()
|
|
targetUser, targetCal = validateCalendar(targetUser, noClientAccess=True)
|
|
if not targetCal:
|
|
return
|
|
colorRgbFormat = 'backgroundColor' in targetListBody or 'foregroundColor' in targetListBody
|
|
if 'summaryOverride' in targetListBody and 'summary' not in updateBody:
|
|
updateBody['summary'] = targetListBody.pop('summaryOverride')
|
|
if updateBody:
|
|
timestamp = currentISOformatTimeStamp('seconds')
|
|
appendFields = ','.join(set(appendFieldsList))
|
|
targetRoleBody = {'role': 'owner', 'scope': {'type': 'user', 'value': targetUser}}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
Act.Set(Act.TRANSFER_OWNERSHIP)
|
|
user, sourceCal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, secondaryCalendarsOnly=True)
|
|
if jcount == 0:
|
|
continue
|
|
if updateBody:
|
|
userName, domain = splitEmailAddress(user)
|
|
for field in updateBody:
|
|
updateBody[field] = _substituteForUser(updateBody[field], user, userName)
|
|
updateBody[field] = updateBody[field].replace('#domain#', domain)
|
|
updateBody[field] = updateBody[field].replace('#timestamp#', timestamp)
|
|
sourceRuleId = f'user:{user}'
|
|
Ind.Increment()
|
|
j = 0
|
|
for calId in calIds:
|
|
j += 1
|
|
Act.Set(Act.TRANSFER_OWNERSHIP)
|
|
if calId.find('@group.calendar.google.com') == -1:
|
|
entityActionNotPerformedWarning([Ent.CALENDAR, calId], notAllowedForbidden, j, jcount)
|
|
continue
|
|
try:
|
|
callGAPI(sourceCal.acl(), 'insert',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.REQUIRED_ACCESS_LEVEL],
|
|
calendarId=calId, body=targetRoleBody, sendNotifications=sendNotifications, fields='')
|
|
entityModifierNewValueItemValueListActionPerformed([Ent.CALENDAR, calId], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount)
|
|
except (GAPI.forbidden, GAPI.requiredAccessLevel) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, calId], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.notFound, GAPI.invalid):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
continue
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
if updateBody:
|
|
Act.Set(Act.UPDATE)
|
|
try:
|
|
if appendFields:
|
|
body = callGAPI(targetCal.calendars(), 'get',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
calendarId=calId, fields=appendFields)
|
|
for field in appendFieldsList:
|
|
if field in updateBody:
|
|
if field in body:
|
|
body[field] += updateBody[field]
|
|
else:
|
|
body[field] = updateBody[field]
|
|
else:
|
|
body = {}
|
|
for field, updateField in iter(updateBody.items()):
|
|
if field not in appendFieldsList:
|
|
body[field] = updateField
|
|
callGAPI(targetCal.calendars(), 'patch',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
calendarId=calId, body=body)
|
|
if showUpdateMessages:
|
|
entityActionPerformed([Ent.CALENDAR, calId], j, jcount)
|
|
except (GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(targetUser, i, count)
|
|
break
|
|
if addToNewOwner:
|
|
Act.Set(Act.ADD)
|
|
targetListBody['id'] = calId
|
|
try:
|
|
callGAPI(targetCal.calendarList(), 'insert',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DUPLICATE, GAPI.UNKNOWN_ERROR,
|
|
GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION],
|
|
body=targetListBody, colorRgbFormat=colorRgbFormat, fields='')
|
|
if showListMessages:
|
|
entityModifierNewValueItemValueListActionPerformed([Ent.CALENDAR, calId], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount)
|
|
except (GAPI.notFound, GAPI.duplicate, GAPI.unknownError,
|
|
GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnPrimarySubscription) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(targetUser, i, count)
|
|
break
|
|
Act.Set(Act.RETAIN)
|
|
if retainRoleBody['role'] == 'owner':
|
|
if showRetentionMessages:
|
|
entityActionPerformed([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], j, jcount)
|
|
elif retainRoleBody['role'] != 'none':
|
|
try:
|
|
callGAPI(targetCal.acl(), 'patch',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_PARAMETER,
|
|
GAPI.INVALID_SCOPE_VALUE, GAPI.ILLEGAL_ACCESS_ROLE_FOR_DEFAULT,
|
|
GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWNER_ACL, GAPI.FORBIDDEN],
|
|
calendarId=calId, ruleId=sourceRuleId, body=retainRoleBody, sendNotifications=sendNotifications, fields='')
|
|
if showRetentionMessages:
|
|
entityActionPerformed([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], j, jcount)
|
|
except GAPI.notFound as e:
|
|
if not checkCalendarExists(targetCal, calId, i, count):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
else:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], str(e), j, jcount)
|
|
except (GAPI.invalid, GAPI.invalidParameter, GAPI.invalidScopeValue, GAPI.illegalAccessRoleForDefault, GAPI.forbidden, GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnerAcl) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(targetUser, i, count)
|
|
break
|
|
else:
|
|
try:
|
|
callGAPI(targetCal.acl(), 'delete',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID],
|
|
calendarId=calId, ruleId=sourceRuleId)
|
|
if showRetentionMessages:
|
|
entityActionPerformed([Ent.CALENDAR, calId, Ent.CALENDAR_ACL, formatACLScopeRole(sourceRuleId, retainRoleBody['role'])], j, jcount)
|
|
except (GAPI.notFound, GAPI.invalid):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(targetUser, i, count)
|
|
break
|
|
if deleteFromOldOwner:
|
|
Act.Set(Act.DELETE)
|
|
try:
|
|
callGAPI(sourceCal.calendarList(), 'delete',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DUPLICATE, GAPI.UNKNOWN_ERROR,
|
|
GAPI.CANNOT_CHANGE_OWN_ACL, GAPI.CANNOT_CHANGE_OWN_PRIMARY_SUBSCRIPTION],
|
|
calendarId=calId)
|
|
entityModifierNewValueItemValueListActionPerformed([Ent.CALENDAR, calId], Act.MODIFIER_FROM, None, [Ent.USER, user], j, jcount)
|
|
except GAPI.notFound as e:
|
|
if retainRoleBody['role'] == 'none':
|
|
if showListMessages:
|
|
entityModifierNewValueItemValueListActionPerformed([Ent.CALENDAR, calId], Act.MODIFIER_FROM, None, [Ent.USER, user], j, jcount)
|
|
else:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except (GAPI.duplicate, GAPI.unknownError,
|
|
GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnPrimarySubscription) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def _createImportCalendarEvent(users, function):
|
|
calendarEntity = getUserCalendarEntity()
|
|
body, parameters = _getCalendarCreateImportUpdateEventOptions(function, Ent.USER)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_TO)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_createCalendarEvents(user, cal, function, calIds, jcount, body, parameters)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> create event <UserCalendarEntity> [id <String>] <EventAddAttribute>+
|
|
# [showdayofweek]
|
|
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
|
|
def createCalendarEvent(users):
|
|
_createImportCalendarEvent(users, 'insert')
|
|
|
|
# gam <UserTypeEntity> import event <UserCalendarEntity> icaluid <iCalUID> <EventImportAttribute>+
|
|
# [showdayofweek]
|
|
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
|
|
def importCalendarEvent(users):
|
|
_createImportCalendarEvent(users, 'import')
|
|
|
|
# gam <UserTypeEntity> update events <UserCalendarEntity> [<EventEntity>] [replacemode] <EventUpdateAttribute>+ [<EventNotificationAttribute>]
|
|
# [showdayofweek]
|
|
# [csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]]]
|
|
def updateCalendarEvents(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
body, parameters = _getCalendarCreateImportUpdateEventOptions('update', Ent.USER)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_IN)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_updateCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, body, parameters)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delete events <UserCalendarEntity> <EventEntity> [doit] [<EventNotificationAttribute>]
|
|
def deleteCalendarEvents(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
parameters = _getCalendarDeleteEventOptions()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_deleteCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, parameters)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> purge events <UserCalendarEntity> <EventEntity> [doit] [<EventNotificationAttribute>]
|
|
def purgeCalendarEvents(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
parameters = _getCalendarDeleteEventOptions()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_purgeCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, parameters, False)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> wipe events <UserCalendarEntity>
|
|
def wipeCalendarEvents(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_wipeCalendarEvents(user, cal, calIds, jcount)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> move events <UserCalendarEntity> <EventEntity> to|destination <CalendarItem> [<EventNotificationAttribute>]
|
|
def moveCalendarEvents(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
checkArgumentPresent(['to', 'destination'])
|
|
newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM))
|
|
parameters, _ = _getCalendarMoveEventsOptions()
|
|
if not checkCalendarExists(None, newCalId, 0, 0, True):
|
|
return
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM, newCalId=newCalId)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_moveCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, newCalId, parameters)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> empty calendartrash <UserCalendarEntity>
|
|
def emptyCalendarTrash(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
Act.Set(Act.PURGE)
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.TRASHED_EVENT, Act.MODIFIER_FROM)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_emptyCalendarTrash(user, cal, calIds, jcount)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> update calattendees <UserCalendarEntity> <EventEntity> [anyorganizer]
|
|
# [<EventNotificationAttribute>] [splitupdate] [doit]
|
|
# (<CSVFileSelector>)
|
|
# (delete <EmailAddress>)*
|
|
# (deleteentity <EmailAddressEntity>)*
|
|
# (add <EmailAddress>)*
|
|
# (addentity <EmailAddressEntity>)*
|
|
# (addstatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddress>)*
|
|
# (addentitystatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddressEntity>)*
|
|
# (replace <EmailAddress> <EmailAddress>)*
|
|
# (replacestatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddress> <EmailAddress>)*
|
|
# (updatestatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddress>)*
|
|
# (updateentitystatus [<AttendeeAttendance>] [<AttendeeStatus>] <EmailAddressEntity>)*
|
|
def updateCalendarAttendees(users):
|
|
def getStatus(option):
|
|
if option.endswith('status'):
|
|
return(getChoice(CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP, defaultChoice=None, mapChoice=True),
|
|
getChoice(CALENDAR_ATTENDEE_STATUS_CHOICE_MAP, defaultChoice=None, mapChoice=True))
|
|
return (None, None)
|
|
|
|
calendarEntity = getUserCalendarEntity()
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
anyOrganizer = doIt = splitUpdate = False
|
|
parameters = {'sendUpdates': 'none'}
|
|
attendeeMap = {}
|
|
errors = 0
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'csv', 'csvfile'}:
|
|
errors = 0
|
|
f, csvFile, _ = openCSVFileReader(getString(Cmd.OB_FILE_NAME), fieldnames=['addr', 'op', 'optional', 'status'])
|
|
for row in csvFile:
|
|
updAddr = row['addr']
|
|
updOp = row['op'].lower()
|
|
updOptional = row['optional']
|
|
updStatus = row['status']
|
|
if not updAddr and not updOp:
|
|
continue
|
|
if updOptional:
|
|
updOptional = updOptional.lower()
|
|
if updStatus:
|
|
updStatus = updStatus.lower()
|
|
if (not updAddr or not updOp or
|
|
(updOptional and updOptional not in CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP) or
|
|
(updStatus and updStatus not in CALENDAR_ATTENDEE_STATUS_CHOICE_MAP)):
|
|
stderrErrorMsg(Msg.INVALID_ATTENDEE_CHANGE.format(','.join([updAddr, updOp, updStatus, updOptional])))
|
|
errors += 1
|
|
continue
|
|
updAddr = normalizeEmailAddressOrUID(updAddr, noUid=True)
|
|
if updOp == 'delete':
|
|
attendeeMap[updAddr] = {'op': updOp, 'done': False}
|
|
else:
|
|
updOptional = CALENDAR_ATTENDEE_OPTIONAL_CHOICE_MAP[updOptional] if updOptional else None
|
|
updStatus = CALENDAR_ATTENDEE_STATUS_CHOICE_MAP[updStatus] if updStatus else None
|
|
if updOp == 'add':
|
|
attendeeMap[updAddr] = {'op': updOp, 'status': updStatus, 'optional': updOptional, 'done': False}
|
|
elif updOp == 'update':
|
|
attendeeMap[updAddr] = {'op': updOp, 'status': updStatus, 'optional': updOptional, 'done': False}
|
|
else: #replace
|
|
attendeeMap[updAddr] = {'op': 'replace', 'status': updStatus, 'optional': updOptional, 'email': normalizeEmailAddressOrUID(updOp, noUid=True), 'done': False}
|
|
closeFile(f)
|
|
elif myarg == 'delete':
|
|
updAddr = getEmailAddress(noUid=True)
|
|
attendeeMap[updAddr] = {'op': 'delete'}
|
|
elif myarg == 'deleteentity':
|
|
for updAddr in getNormalizedEmailAddressEntity(noUid=True):
|
|
attendeeMap[updAddr] = {'op': 'delete'}
|
|
elif myarg in {'add', 'addstatus'}:
|
|
updOptional, updStatus = getStatus(myarg)
|
|
updAddr = getEmailAddress(noUid=True)
|
|
attendeeMap[updAddr] = {'op': 'add', 'status': updStatus, 'optional': updOptional, 'done': False}
|
|
elif myarg in {'addentity', 'addentitystatus'}:
|
|
updOptional, updStatus = getStatus(myarg)
|
|
for updAddr in getNormalizedEmailAddressEntity(noUid=True):
|
|
attendeeMap[updAddr] = {'op': 'add', 'status': updStatus, 'optional': updOptional, 'done': False}
|
|
elif myarg in {'update', 'updatestatus'}:
|
|
updOptional, updStatus = getStatus(myarg)
|
|
updAddr = getEmailAddress(noUid=True)
|
|
attendeeMap[updAddr] = {'op': 'update', 'status': updStatus, 'optional': updOptional, 'done': False}
|
|
elif myarg in {'updateentity', 'updateentitystatus'}:
|
|
updOptional, updStatus = getStatus(myarg)
|
|
for updAddr in getNormalizedEmailAddressEntity(noUid=True):
|
|
attendeeMap[updAddr] = {'op': 'update', 'status': updStatus, 'optional': updOptional, 'done': False}
|
|
elif myarg in {'replace', 'replacestatus'}:
|
|
updOptional, updStatus = getStatus(myarg)
|
|
updAddr = getEmailAddress(noUid=True)
|
|
newAddr = getEmailAddress(noUid=True)
|
|
attendeeMap[updAddr] = {'op': 'replace', 'status': updStatus, 'optional': updOptional, 'email': newAddr, 'done': False}
|
|
elif myarg in {'anyorganizer', 'allevents'}:
|
|
anyOrganizer = True
|
|
elif _getCalendarSendUpdates(myarg, parameters):
|
|
pass
|
|
elif myarg == 'doit':
|
|
doIt = True
|
|
elif myarg == 'dryrun':
|
|
doIt = False
|
|
elif myarg == 'splitupdate':
|
|
splitUpdate = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not attendeeMap:
|
|
missingArgumentExit(Msg.UPDATE_ATTENDEE_CHANGES)
|
|
ucount = len(attendeeMap)
|
|
if errors:
|
|
systemErrorExit(USAGE_ERROR_RC, '')
|
|
removeMessage = Msg.ATTENDEES_REMOVE
|
|
addMessage = Msg.ATTENDEES_ADD_REMOVE if not splitUpdate else Msg.ATTENDEES_ADD
|
|
fieldsList = ['attendees', 'id', 'organizer', 'status', 'summary']
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for calId in calIds:
|
|
j += 1
|
|
Act.Set(Act.UPDATE)
|
|
calId, cal, events, kcount = _validateCalendarGetEvents(origUser, user, cal, calId, j, jcount, calendarEventEntity,
|
|
fieldsList, True)
|
|
if kcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
k = 0
|
|
for event in events:
|
|
k += 1
|
|
eventSummary = event.get('summary', event['id'])
|
|
if event['status'] == 'cancelled':
|
|
entityActionNotPerformedWarning([Ent.EVENT, eventSummary], Msg.EVENT_IS_CANCELED, k, kcount)
|
|
continue
|
|
if not anyOrganizer and not event.get('organizer', {}).get('self'):
|
|
entityActionNotPerformedWarning([Ent.EVENT, eventSummary], Msg.USER_IS_NOT_ORGANIZER, k, kcount)
|
|
continue
|
|
needsUpdate = False
|
|
for _, v in sorted(iter(attendeeMap.items())):
|
|
v['done'] = False
|
|
updatedAttendeesAdd = []
|
|
updatedAttendeesRemove = []
|
|
entityPerformActionNumItems([Ent.EVENT, eventSummary], ucount, Ent.ATTENDEE, k, kcount)
|
|
Ind.Increment()
|
|
u = 0
|
|
for attendee in event.get('attendees', []):
|
|
oldAddr = attendee.get('email', '').lower()
|
|
if not oldAddr:
|
|
updatedAttendeesAdd.append(attendee)
|
|
if splitUpdate:
|
|
updatedAttendeesRemove.append(attendee)
|
|
continue
|
|
update = attendeeMap.get(oldAddr)
|
|
if not update:
|
|
updatedAttendeesAdd.append(attendee)
|
|
if splitUpdate:
|
|
updatedAttendeesRemove.append(attendee)
|
|
continue
|
|
updOp = update['op']
|
|
if updOp == 'delete':
|
|
u += 1
|
|
update['done'] = True
|
|
Act.Set(Act.DELETE)
|
|
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], u, ucount)
|
|
needsUpdate = True
|
|
else:
|
|
oldStatus = attendee.get('responseStatus')
|
|
oldOptional = attendee.get('optional', False)
|
|
updStatus = update['status']
|
|
updOptional = update['optional']
|
|
if updOp in {'add', 'update'}:
|
|
u += 1
|
|
update['done'] = True
|
|
if ((updStatus is not None and updStatus != oldStatus) or
|
|
(updOptional is not None and updOptional != oldOptional)):
|
|
attendee['responseStatus'] = updStatus if updStatus is not None else oldStatus
|
|
attendee['optional'] = updOptional if updOptional is not None else oldOptional
|
|
Act.Set(Act.UPDATE)
|
|
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], u, ucount)
|
|
needsUpdate = True
|
|
else:
|
|
Act.Set(Act.SKIP)
|
|
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], u, ucount)
|
|
updatedAttendeesAdd.append(attendee)
|
|
else: #replace
|
|
u += 1
|
|
update['done'] = True
|
|
attendee['email'] = update['email']
|
|
attendee['responseStatus'] = updStatus if updStatus is not None else oldStatus
|
|
attendee['optional'] = updOptional if updOptional is not None else oldOptional
|
|
Act.Set(Act.REPLACE)
|
|
entityPerformActionModifierNewValue([Ent.EVENT, eventSummary, Ent.ATTENDEE, oldAddr], Act.MODIFIER_WITH, update['email'], u, ucount)
|
|
updatedAttendeesAdd.append(attendee)
|
|
needsUpdate = True
|
|
for newAddr, v in sorted(iter(attendeeMap.items())):
|
|
if v['op'] == 'add' and not v['done']:
|
|
u += 1
|
|
v['done'] = True
|
|
attendee = {'email': newAddr}
|
|
if v['status'] is not None:
|
|
attendee['responseStatus'] = v['status']
|
|
if v['optional'] is not None:
|
|
attendee['optional'] = v['optional']
|
|
Act.Set(Act.ADD)
|
|
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, newAddr], u, ucount)
|
|
updatedAttendeesAdd.append(attendee)
|
|
needsUpdate = True
|
|
for newAddr, v in sorted(iter(attendeeMap.items())):
|
|
if not v['done']:
|
|
u += 1
|
|
Act.Set(Act.SKIP)
|
|
entityPerformAction([Ent.EVENT, eventSummary, Ent.ATTENDEE, newAddr], u, ucount)
|
|
Ind.Decrement()
|
|
if needsUpdate:
|
|
Act.Set(Act.UPDATE)
|
|
if doIt:
|
|
status = True
|
|
if splitUpdate:
|
|
try:
|
|
callGAPI(cal.events(), 'patch',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
calendarId=calId, eventId=event['id'], body={'attendees': updatedAttendeesRemove},
|
|
sendUpdates=parameters['sendUpdates'], fields='')
|
|
entityActionPerformedMessage([Ent.EVENT, eventSummary], removeMessage, j, jcount)
|
|
except GAPI.notFound as e:
|
|
if not checkCalendarExists(cal, calId, i, count):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
break
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventSummary], str(e), k, kcount)
|
|
status = False
|
|
except (GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
break
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
if status:
|
|
try:
|
|
callGAPI(cal.events(), 'patch',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID],
|
|
calendarId=calId, eventId=event['id'], body={'attendees': updatedAttendeesAdd},
|
|
sendUpdates=parameters['sendUpdates'], fields='')
|
|
entityActionPerformedMessage([Ent.EVENT, eventSummary], addMessage, jcount)
|
|
except GAPI.notFound as e:
|
|
if not checkCalendarExists(cal, calId, i, count):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
break
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventSummary], str(e), k, kcount)
|
|
except (GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
break
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.EVENT, eventSummary], Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, j, jcount)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> info events <UserCalendarEntity> <EventEntity> [maxinstances <Number>]
|
|
# [fields <EventFieldNameList>] [showdayofweek]
|
|
# [formatjson]
|
|
def infoCalendarEvents(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
FJQC, fieldsList = _getCalendarInfoEventOptions(calendarEventEntity)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_IN, showAction=not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_infoCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity, FJQC, fieldsList)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print events <UserCalendarEntity> <EventEntity> <EventDisplayProperties>*
|
|
# [fields <EventFieldNameList>] [showdayofweek]
|
|
# [countsonly] [formatjson [quotechar <Character>]] [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show events <UserCalendarEntity> <EventEntity> <EventDisplayProperties>*
|
|
# [fields <EventFieldNameList>] [showdayofweek]
|
|
# [countsonly] [formatjson]
|
|
def printShowCalendarEvents(users):
|
|
calendarEntity = getUserCalendarEntity()
|
|
calendarEventEntity = getCalendarEventEntity()
|
|
csvPF, FJQC, fieldsList = _getCalendarPrintShowEventOptions(calendarEventEntity, Ent.USER)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, cal, calIds, jcount = _validateUserGetCalendarIds(user, i, count, calendarEntity, Ent.EVENT, Act.MODIFIER_FROM,
|
|
showAction=not csvPF and not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
_printShowCalendarEvents(origUser, user, cal, calIds, jcount, calendarEventEntity,
|
|
csvPF, FJQC, fieldsList)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Calendar Events')
|
|
|
|
def getStatusEventDateTime(dateType, dateList):
|
|
if dateType == 'timerange':
|
|
startTime = getTimeOrDeltaFromNow(returnDateTime=True)[0]
|
|
endTime = getTimeOrDeltaFromNow(returnDateTime=True)[0]
|
|
recurrence = []
|
|
while checkArgumentPresent(['recurrence']):
|
|
recurrence.append(getString(Cmd.OB_RECURRENCE))
|
|
dateList.append({'type': dateType, 'first': startTime, 'last': endTime, 'ulast': endTime, 'recurrence': recurrence})
|
|
return
|
|
firstDate = getYYYYMMDD(minLen=1, returnDateTime=True).replace(tzinfo=GC.Values[GC.TIMEZONE])
|
|
if dateType == 'range':
|
|
lastDate = getYYYYMMDD(minLen=1, returnDateTime=True).replace(tzinfo=GC.Values[GC.TIMEZONE])
|
|
deltaDay = datetime.timedelta(days=1)
|
|
deltaWeek = datetime.timedelta(weeks=1)
|
|
if dateType in {'date', 'allday'}:
|
|
dateList.append({'type': 'date', 'first': firstDate, 'last': firstDate+deltaDay,
|
|
'ulast': firstDate+deltaDay, 'udelta': deltaDay})
|
|
elif dateType == 'range':
|
|
dateList.append({'type': dateType, 'first': firstDate, 'last': lastDate+deltaDay,
|
|
'ulast': lastDate, 'udelta': deltaDay})
|
|
elif dateType == 'daily':
|
|
argRepeat = getInteger(minVal=1, maxVal=366)
|
|
dateList.append({'type': dateType, 'first': firstDate, 'last': firstDate+datetime.timedelta(days=argRepeat),
|
|
'ulast': firstDate+datetime.timedelta(days=argRepeat), 'udelta': deltaDay})
|
|
else: #weekly
|
|
argRepeat = getInteger(minVal=1, maxVal=52)
|
|
dateList.append({'type': dateType, 'first': firstDate, 'last': firstDate+deltaDay, 'pdelta': deltaWeek, 'repeats': argRepeat,
|
|
'ulast': firstDate+datetime.timedelta(weeks=argRepeat), 'udelta': deltaWeek})
|
|
|
|
def _showCalendarStatusEvent(primaryEmail, calId, eventEntityType, event, k, kcount, FJQC):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON({'primaryEmail': primaryEmail, 'calendarId': calId, 'event': event},
|
|
timeObjects=EVENT_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([eventEntityType, event['id']], k, kcount)
|
|
skipObjects = {'id'}
|
|
Ind.Increment()
|
|
showJSON(None, event, skipObjects, EVENT_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
EVENT_AUTO_DECLINE_MODE_CHOICE_MAP = {
|
|
'declinenone': 'declineNone',
|
|
'declineallconflictinginvitations': 'declineAllConflictingInvitations',
|
|
'declineonlynewconflictinginvitations': 'declineOnlyNewConflictingInvitations',
|
|
'none': 'declineNone',
|
|
'all': 'declineAllConflictingInvitations',
|
|
'new': 'declineOnlyNewConflictingInvitations',
|
|
}
|
|
|
|
EVENT_CHAT_STATUS_CHOICE_MAP = {
|
|
'available': 'available',
|
|
'dnd': 'doNotDisturb',
|
|
'donotdisturb': 'doNotDisturb',
|
|
}
|
|
|
|
def getFocusTimeProperties(body, parameters, dateList):
|
|
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[EVENT_TYPE_FOCUSTIME]
|
|
body.update({'eventType': EVENT_TYPE_FOCUSTIME, 'summary': 'Focus time',
|
|
eventProperties: {'autoDeclineMode': 'declineNone', 'chatStatus': 'available', 'declineMessage': 'Declined'},
|
|
'transparency':'opaque'})
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'summary':
|
|
body['summary'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'declinemode':
|
|
body[eventProperties]['autoDeclineMode'] = getChoice(EVENT_AUTO_DECLINE_MODE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'declinemessage':
|
|
body[eventProperties]['declineMessage'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'chatstatus':
|
|
body[eventProperties]['chatStatus'] = getChoice(EVENT_CHAT_STATUS_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'timerange':
|
|
getStatusEventDateTime(myarg, dateList)
|
|
elif myarg == 'timezone':
|
|
parameters['timeZone'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
def getOutOfOfficeProperties(body, parameters, dateList):
|
|
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[EVENT_TYPE_OUTOFOFFICE]
|
|
body.update({'eventType': EVENT_TYPE_OUTOFOFFICE, 'summary': 'Out of office',
|
|
eventProperties: {'autoDeclineMode': 'declineNone', 'declineMessage': 'Declined'},
|
|
'transparency':'opaque'})
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'summary':
|
|
body['summary'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'declinemode':
|
|
body[eventProperties]['autoDeclineMode'] = getChoice(EVENT_AUTO_DECLINE_MODE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'declinemessage':
|
|
body[eventProperties]['declineMessage'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'timerange':
|
|
getStatusEventDateTime(myarg, dateList)
|
|
elif myarg == 'timezone':
|
|
parameters['timeZone'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
STATUS_EVENTS_DATETIME_CHOICES = {'date', 'allday', 'range', 'daily', 'weekly', 'timerange'}
|
|
|
|
WORKING_LOCATION_CHOICE_MAP = {
|
|
'custom': 'customLocation',
|
|
'home': 'homeOffice',
|
|
'office': 'officeLocation',
|
|
}
|
|
|
|
def getWorkingLocationProperties(body, parameters, dateList):
|
|
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[EVENT_TYPE_WORKINGLOCATION]
|
|
body.update({'eventType': EVENT_TYPE_WORKINGLOCATION, eventProperties: {},
|
|
'visibility': 'public', 'transparency':'transparent'})
|
|
location = ''
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in WORKING_LOCATION_CHOICE_MAP:
|
|
location = WORKING_LOCATION_CHOICE_MAP[myarg]
|
|
body[eventProperties]['type'] = location
|
|
if location == 'homeOffice':
|
|
pass
|
|
elif location == 'customLocation':
|
|
body[eventProperties][location] = {'label': getString(Cmd.OB_STRING)}
|
|
else: #officeLocation
|
|
body[eventProperties][location] = {'label': getString(Cmd.OB_STRING)}
|
|
entry = body[eventProperties][location]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'building', 'buildingid'}:
|
|
entry['buildingId'] = _getBuildingByNameOrId(None)
|
|
elif myarg in {'floor', 'floorname'}:
|
|
entry['floorId'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'section', 'floorsection'}:
|
|
entry['floorSectionId'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in {'desk', 'deskcode'}:
|
|
entry['deskId'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'endlocation':
|
|
break
|
|
else:
|
|
Cmd.Backup()
|
|
break
|
|
elif myarg in STATUS_EVENTS_DATETIME_CHOICES:
|
|
getStatusEventDateTime(myarg, dateList)
|
|
elif myarg == 'timezone':
|
|
parameters['timeZone'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
return location
|
|
|
|
# gam <UserTypeEntity> create focustime
|
|
# [chatstatus available|donotdisturb]
|
|
# [declinemode none|all|new]
|
|
# [declinemessage <String>]
|
|
# [summary <String>]
|
|
# (timerange <Time> <Time> [recurrence <String>])+
|
|
# [timezone <String>]
|
|
# gam <UserTypeEntity> create outofoffice
|
|
# [declinemode none|all|new]
|
|
# [declinemessage <String>]
|
|
# [summary <String>]
|
|
# (timerange <Time> <Time> [recurrence <String>])+
|
|
# [timezone <String>]
|
|
# gam <UserTypeEntity> create workinglocation
|
|
# (home|
|
|
# (custom <String>)|
|
|
# (office <String> [building|buildingid <String>] [floor|floorname <String>]
|
|
# [section|floorsection <String>] [desk|deskcode <String>]))
|
|
# ((date yyyy-mm-dd)|
|
|
# (range yyyy-mm-dd yyyy-mm-dd)|
|
|
# (daily yyyy-mm-dd N)|
|
|
# (weekly yyyy-mm-dd N)|
|
|
# (timerange <Time> <Time>))+
|
|
# [timezone <String>]
|
|
def createStatusEvent(users, eventType):
|
|
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[eventType]
|
|
entityType = EVENT_TYPE_ENTITY_MAP[eventType]
|
|
body = {'start': {}, 'end': {}, 'recurrence': None}
|
|
calId = 'primary'
|
|
parameters = {}
|
|
dateList = []
|
|
if eventType == EVENT_TYPE_WORKINGLOCATION:
|
|
location = getWorkingLocationProperties(body, parameters, dateList)
|
|
if not location:
|
|
missingArgumentExit('|'.join(WORKING_LOCATION_CHOICE_MAP))
|
|
elif eventType == EVENT_TYPE_OUTOFOFFICE:
|
|
getOutOfOfficeProperties(body, parameters, dateList)
|
|
else: # elif eventType == EVENT_TYPE_FOCUSTIME:
|
|
getFocusTimeProperties(body, parameters, dateList)
|
|
if not dateList:
|
|
missingChoiceExit(STATUS_EVENTS_DATETIME_CHOICES)
|
|
datekvList = [Ent.CALENDAR, '', Ent.EVENT, '', Ent.DATE, '']
|
|
timekvList = [Ent.CALENDAR, '', Ent.EVENT, '', Ent.START_TIME, '', Ent.END_TIME, '']
|
|
if eventType == EVENT_TYPE_WORKINGLOCATION:
|
|
location = body[eventProperties]['type']
|
|
if location in body[eventProperties] and 'label' in body[eventProperties][location]:
|
|
location += f"/{body[eventProperties][location]['label']}"
|
|
datekvList.extend([Ent.LOCATION, location])
|
|
timekvList.extend([Ent.LOCATION, location])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal = buildGAPIServiceObject(API.CALENDAR, user, i, count)
|
|
if not cal:
|
|
continue
|
|
jcount = len(dateList)
|
|
entityPerformAction([Ent.CALENDAR, user, entityType, None], i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for wlDate in dateList:
|
|
j += 1
|
|
first = wlDate['first']
|
|
last = wlDate['ulast']
|
|
kvList = datekvList if wlDate['type'] != 'timerange' else timekvList
|
|
kvList[1] = user
|
|
while first < last:
|
|
body.pop('recurrence', None)
|
|
if wlDate['type'] != 'timerange':
|
|
body['start']['date'] = first.strftime(YYYYMMDD_FORMAT)
|
|
kvList[5] = body['start']['date']
|
|
body['end']['date'] = (first+datetime.timedelta(days=1)).strftime(YYYYMMDD_FORMAT)
|
|
else:
|
|
body['start']['dateTime'] = ISOformatTimeStamp(first)
|
|
kvList[5] = body['start']['dateTime']
|
|
body['end']['dateTime'] = ISOformatTimeStamp(last)
|
|
kvList[7] = body['end']['dateTime']
|
|
if wlDate['recurrence']:
|
|
body['recurrence'] = wlDate['recurrence']
|
|
if not _setEventRecurrenceTimeZone(cal, calId, body, parameters, i, count):
|
|
break
|
|
try:
|
|
event = callGAPI(cal.events(), 'insert',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID, GAPI.BAD_REQUEST,
|
|
GAPI.TIME_RANGE_EMPTY, GAPI.MALFORMED_WORKING_LOCATION_EVENT],
|
|
calendarId=calId, body=body, fields='id')
|
|
kvList[3] = event['id']
|
|
entityActionPerformed(kvList, j, jcount)
|
|
if wlDate['type'] == 'timerange':
|
|
break
|
|
first += wlDate['udelta']
|
|
except (GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, user], str(e), i, count)
|
|
break
|
|
except (GAPI.badRequest, GAPI.timeRangeEmpty, GAPI.malformedWorkingLocationEvent) as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
break
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def createFocusTime(users):
|
|
createStatusEvent(users, EVENT_TYPE_FOCUSTIME)
|
|
|
|
def createOutOfOffice(users):
|
|
createStatusEvent(users, EVENT_TYPE_OUTOFOFFICE)
|
|
|
|
def createWorkingLocation(users):
|
|
createStatusEvent(users, EVENT_TYPE_WORKINGLOCATION)
|
|
|
|
# gam <UserTypeEntity> delete focustime|outofoffice|workinglocation
|
|
# ((date yyyy-mm-dd)|
|
|
# (range yyyy-mm-dd yyyy-mm-dd)|
|
|
# (daily yyyy-mm-dd N)|
|
|
# (weekly yyyy-mm-dd N)|
|
|
# (timerange <Time> <Time>))+
|
|
def deleteStatusEvent(users, eventType):
|
|
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[eventType]
|
|
entityType = EVENT_TYPE_ENTITY_MAP[eventType]
|
|
kwargs = {'eventTypes': [eventType], 'showDeleted': False, 'singleEvents': True,
|
|
'timeMax': None, 'timeMin': None, 'orderBy': 'startTime'}
|
|
calId = 'primary'
|
|
dateList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in STATUS_EVENTS_DATETIME_CHOICES:
|
|
getStatusEventDateTime(myarg, dateList)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not dateList:
|
|
missingChoiceExit(STATUS_EVENTS_DATETIME_CHOICES)
|
|
basekvList = [Ent.CALENDAR, '', Ent.EVENT, '']
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal = buildGAPIServiceObject(API.CALENDAR, user, i, count)
|
|
if not cal:
|
|
continue
|
|
jcount = len(dateList)
|
|
entityPerformAction([Ent.CALENDAR, user, entityType, None], i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for wlDate in dateList:
|
|
j += 1
|
|
first = wlDate['first']
|
|
last = wlDate['last']
|
|
basekvList[1] = user
|
|
events = []
|
|
for _ in range(1, wlDate.get('repeats', 1)+1):
|
|
kwargs['timeMin'] = ISOformatTimeStamp(first)
|
|
kwargs['timeMax'] = ISOformatTimeStamp(last)
|
|
try:
|
|
events = callGAPIpages(cal.events(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID, GAPI.BAD_REQUEST],
|
|
calendarId=calId, fields=f'nextPageToken,items(id,start,end,{eventProperties})', **kwargs)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, user], str(e), j, jcount)
|
|
break
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
kcount = len(events)
|
|
k = 0
|
|
for event in events:
|
|
k += 1
|
|
eventId = event['id']
|
|
basekvList[3] = eventId
|
|
kvList = basekvList[:]
|
|
if eventType == EVENT_TYPE_WORKINGLOCATION:
|
|
location = event[eventProperties]['type']
|
|
if location in event[eventProperties] and 'label' in event[eventProperties][location]:
|
|
location += f"/{event[eventProperties][location]['label']}"
|
|
try:
|
|
callGAPI(cal.events(), 'delete',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.DELETED, GAPI.FORBIDDEN,
|
|
GAPI.INVALID, GAPI.REQUIRED, GAPI.REQUIRED_ACCESS_LEVEL],
|
|
calendarId=calId, eventId=eventId, sendUpdates='none')
|
|
if 'date' in event['start']:
|
|
kvList.extend([Ent.DATE, event['start']['date']])
|
|
if eventType == EVENT_TYPE_WORKINGLOCATION:
|
|
kvList.extend([Ent.LOCATION, location])
|
|
else:
|
|
kvList.extend([Ent.START_TIME, formatLocalTime(event['start']['dateTime']),
|
|
Ent.END_TIME, formatLocalTime(event['end']['dateTime'])])
|
|
if eventType == EVENT_TYPE_WORKINGLOCATION:
|
|
kvList.extend([Ent.LOCATION, location])
|
|
entityActionPerformed(kvList, k, kcount)
|
|
except (GAPI.notFound, GAPI.deleted) as e:
|
|
if not checkCalendarExists(cal, calId, i, count):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, k, kcount)
|
|
break
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), k, kcount)
|
|
except (GAPI.forbidden, GAPI.invalid, GAPI.required, GAPI.requiredAccessLevel) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), k, kcount)
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def deleteFocusTime(users):
|
|
deleteStatusEvent(users, EVENT_TYPE_FOCUSTIME)
|
|
|
|
def deleteOutOfOffice(users):
|
|
deleteStatusEvent(users, EVENT_TYPE_OUTOFOFFICE)
|
|
|
|
def deleteWorkingLocation(users):
|
|
deleteStatusEvent(users, EVENT_TYPE_WORKINGLOCATION)
|
|
|
|
# gam <UserTypeEntity> show focustime|outofoffice|workinglocation
|
|
# ((date yyyy-mm-dd)|
|
|
# (range yyyy-mm-dd yyyy-mm-dd)|
|
|
# (daily yyyy-mm-dd N)|
|
|
# (weekly yyyy-mm-dd N)|
|
|
# (timerange <Time> <Time>))+
|
|
# [showdayofweek]
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> print focustime|outofoffice|workinglocation
|
|
# ((date yyyy-mm-dd)|
|
|
# (range yyyy-mm-dd yyyy-mm-dd)|
|
|
# (daily yyyy-mm-dd N)|
|
|
# (weekly yyyy-mm-dd N)|
|
|
# (timerange <Time> <Time>))+
|
|
# [showdayofweek]
|
|
# [formatjson [quotechar <Character>]] [todrive <ToDriveAttribute>*]
|
|
def printShowStatusEvent(users, eventType):
|
|
eventProperties = EVENT_TYPE_PROPERTIES_NAME_MAP[eventType]
|
|
entityType = EVENT_TYPE_ENTITY_MAP[eventType]
|
|
csvPF = CSVPrintFile(['primaryEmail', 'calendarId', 'id'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
kwargs = {'eventTypes': [eventType], 'showDeleted': False, 'singleEvents': True,
|
|
'timeMax': None, 'timeMin': None, 'orderBy': 'startTime'}
|
|
calId = 'primary'
|
|
showDayOfWeek = False
|
|
dateList = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in STATUS_EVENTS_DATETIME_CHOICES:
|
|
getStatusEventDateTime(myarg, dateList)
|
|
elif myarg == 'showdayofweek':
|
|
showDayOfWeek = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if not dateList:
|
|
missingChoiceExit(STATUS_EVENTS_DATETIME_CHOICES)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, cal = buildGAPIServiceObject(API.CALENDAR, user, i, count)
|
|
if not cal:
|
|
continue
|
|
jcount = len(dateList)
|
|
if not csvPF and not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.CALENDAR, user], jcount, Ent.DATE, i, count)
|
|
j = 0
|
|
for wlDate in dateList:
|
|
j += 1
|
|
first = wlDate['first']
|
|
last = wlDate['last']
|
|
for _ in range(1, wlDate.get('repeats', 1)+1):
|
|
kwargs['timeMin'] = ISOformatTimeStamp(first)
|
|
kwargs['timeMax'] = ISOformatTimeStamp(last)
|
|
try:
|
|
events = callGAPIpages(cal.events(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID, GAPI.BAD_REQUEST],
|
|
calendarId=calId, fields=f'nextPageToken,items(id,start,end,eventType,{eventProperties},transparency,visibility)',
|
|
**kwargs)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, user], str(e), j, jcount)
|
|
break
|
|
except GAPI.notACalendarUser:
|
|
userCalServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
if not csvPF:
|
|
kcount = len(events)
|
|
Ind.Increment()
|
|
k = 0
|
|
for event in events:
|
|
k += 1
|
|
if showDayOfWeek:
|
|
_getEventDaysOfWeek(event)
|
|
_showCalendarStatusEvent(user, calId, Ent.EVENT, event, k, kcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
for event in events:
|
|
if showDayOfWeek:
|
|
_getEventDaysOfWeek(event)
|
|
_printCalendarEvent(user, calId, event, csvPF, FJQC)
|
|
if 'pdelta' in wlDate:
|
|
first += wlDate['pdelta']
|
|
last += wlDate['pdelta']
|
|
if csvPF:
|
|
csvPF.writeCSVfile(f'Calendar {Ent.Plural(entityType)}')
|
|
|
|
def printShowFocusTime(users):
|
|
printShowStatusEvent(users, EVENT_TYPE_FOCUSTIME)
|
|
|
|
def printShowOutOfOffice(users):
|
|
printShowStatusEvent(users, EVENT_TYPE_OUTOFOFFICE)
|
|
|
|
def printShowWorkingLocation(users):
|
|
printShowStatusEvent(users, EVENT_TYPE_WORKINGLOCATION)
|
|
|
|
YOUTUBE_CHANNEL_FIELDS_CHOICE_MAP = {
|
|
'brandingsettings': 'brandingSettings',
|
|
'contentdetails': 'contentDetails',
|
|
'contentownerdetails': 'contentOwnerDetails',
|
|
'id': 'id',
|
|
'localizations': 'localizations',
|
|
'snippet': 'snippet',
|
|
'statistics': 'statistics',
|
|
'status': 'status',
|
|
'topicdetails': 'topicDetails',
|
|
}
|
|
|
|
YOUTUBE_CHANNEL_TIME_OBJECTS = {'publishedAt'}
|
|
|
|
# gam <UserTypeEntity> show youtubechannels
|
|
# (mine|
|
|
# (ids|channels <YouTubeChannelIDList>)|
|
|
# (forusername <String>)|
|
|
# (managedbyme <String>))
|
|
# [languagecode <BCP47LanguageCode>]
|
|
# [allfields|(fields <YouTubeChannelFieldNameList>)]
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> print youtubechannels [todrive <ToDriveAttribute>*]
|
|
# (mine|
|
|
# (ids|channels <YouTubeChannelIDList>)|
|
|
# (forusername <String>)|
|
|
# (managedbyme <String>))
|
|
# [languagecode <BCP47LanguageCode>]
|
|
# [allfields|(fields <YouTubeChannelFieldNameList>)]
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowYouTubeChannel(users):
|
|
csvPF = CSVPrintFile(['User', 'id'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
kwargs = {'mine': True}
|
|
languageCode = ''
|
|
fieldsList = ['id']
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'mine':
|
|
kwargs = {'mine': True}
|
|
elif myarg in {'id', 'ids', 'channel', 'channels'}:
|
|
kwargs = {'id': ','.join(getEntityList(Cmd.OB_YOUTUBE_CHANNEL_ID_LIST))}
|
|
elif myarg == 'forusername':
|
|
kwargs = {'forUsername': getString(Cmd.OB_USER_NAME)}
|
|
elif myarg == 'managedbyme':
|
|
kwargs = {'managedByMe': True, 'onBehalfOfContentOwner': getString(Cmd.OB_USER_NAME)}
|
|
elif getFieldsList(myarg, YOUTUBE_CHANNEL_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
elif myarg == 'allfields':
|
|
for field in YOUTUBE_CHANNEL_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, YOUTUBE_CHANNEL_FIELDS_CHOICE_MAP, fieldsList)
|
|
elif myarg in {'languagecode', 'hl'}:
|
|
languageCode = getLanguageCode(BCP47_LANGUAGE_CODES_MAP)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
kwargs['part'] = ','.join(set(fieldsList))
|
|
if languageCode:
|
|
kwargs['hl'] = languageCode
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, yt = buildGAPIServiceObject(API.YOUTUBE, user, i, count)
|
|
if not yt:
|
|
continue
|
|
try:
|
|
channels = callGAPIpages(yt.channels(), 'list', 'items',
|
|
throwReasons=GAPI.YOUTUBE_THROW_REASONS,
|
|
fields='nextPageToken,items', **kwargs)
|
|
except (GAPI.unsupportedSupervisedAccount, GAPI.unsupportedLanguageCode) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
continue
|
|
except GAPI.contentOwnerAccountNotFound as e:
|
|
if 'managedByMe' in kwargs:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.OWNER, kwargs['onBehalfOfContentOwner']], str(e), i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
userYouTubeServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
if not csvPF:
|
|
jcount = len(channels)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.YOUTUBE_CHANNEL, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for channel in channels:
|
|
j += 1
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(channel, timeObjects=YOUTUBE_CHANNEL_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True))
|
|
break
|
|
printEntity([Ent.YOUTUBE_CHANNEL, channel['id']], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, channel, skipObjects={'id'}, timeObjects=YOUTUBE_CHANNEL_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
for channel in channels:
|
|
row = {'User': user, 'id': channel['id']}
|
|
flattenJSON(channel, flattened=row, timeObjects=YOUTUBE_CHANNEL_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'User': user, 'id': channel['id'],
|
|
'JSON': json.dumps(cleanJSON(channel, timeObjects=YOUTUBE_CHANNEL_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)}
|
|
csvPF.WriteRowNoFilter(row)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('YouTube Channels')
|
|
|
|
def _getEntityMimeType(fileEntry):
|
|
if fileEntry['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
return Ent.DRIVE_FOLDER
|
|
if fileEntry['mimeType'].startswith(MIMETYPE_GA_3P_SHORTCUT):
|
|
return Ent.DRIVE_3PSHORTCUT
|
|
if fileEntry['mimeType'] != MIMETYPE_GA_SHORTCUT:
|
|
return Ent.DRIVE_FILE
|
|
if 'shortcutDetails' not in fileEntry or 'targetMimeType' not in fileEntry['shortcutDetails']:
|
|
return Ent.DRIVE_SHORTCUT
|
|
return Ent.DRIVE_FOLDER_SHORTCUT if fileEntry['shortcutDetails']['targetMimeType'] == MIMETYPE_GA_FOLDER else Ent.DRIVE_FILE_SHORTCUT
|
|
|
|
def _getTargetEntityMimeType(fileEntry):
|
|
return Ent.DRIVE_FOLDER if fileEntry['shortcutDetails']['targetMimeType'] == MIMETYPE_GA_FOLDER else Ent.DRIVE_FILE
|
|
|
|
CORPORA_ALL_DRIVES = 'allDrives'
|
|
CORPORA_CHOICE_MAP = {
|
|
'alldrives': CORPORA_ALL_DRIVES,
|
|
'allshareddrives': CORPORA_ALL_DRIVES,
|
|
'allteamdrives': CORPORA_ALL_DRIVES,
|
|
'domain': 'domain',
|
|
'onlyshareddrives': CORPORA_ALL_DRIVES,
|
|
'onlyteamdrives': CORPORA_ALL_DRIVES,
|
|
'user': 'user',
|
|
}
|
|
|
|
QUERY_SHORTCUTS_MAP = {
|
|
'allfiles': f"mimeType != '{MIMETYPE_GA_FOLDER}'",
|
|
'allfolders': f"mimeType = '{MIMETYPE_GA_FOLDER}'",
|
|
'allforms': f"mimeType = '{MIMETYPE_GA_FORM}'",
|
|
'allgooglefiles': f"mimeType != '{MIMETYPE_GA_FOLDER}' and mimeType contains 'vnd.google'",
|
|
'allnongooglefiles': "not mimeType contains 'vnd.google'",
|
|
'allshortcuts': f"mimeType = '{MIMETYPE_GA_SHORTCUT}'",
|
|
'all3pshortcuts': f"mimeType = '{MIMETYPE_GA_3P_SHORTCUT}'",
|
|
'allitems': 'allitems',
|
|
'mycommentableitems': ME_IN_OWNERS_AND+f"(mimeType = '{MIMETYPE_GA_DOCUMENT}' or mimeType = '{MIMETYPE_GA_SPREADSHEET}' or mimeType = '{MIMETYPE_GA_PRESENTATION}')",
|
|
'mydocs': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_DOCUMENT}'",
|
|
'myfiles': ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}'",
|
|
'myfolders': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FOLDER}'",
|
|
'myforms': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FORM}'",
|
|
'mygooglefiles': ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}' and mimeType contains 'vnd.google'",
|
|
'mynongooglefiles': ME_IN_OWNERS_AND+"not mimeType contains 'vnd.google'",
|
|
'mypresentations': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_PRESENTATION}'",
|
|
'mypublishableitems': ME_IN_OWNERS_AND+f"(mimeType = '{MIMETYPE_GA_DOCUMENT}' or mimeType = '{MIMETYPE_GA_SPREADSHEET}' or mimeType = '{MIMETYPE_GA_FORM}' or mimeType = '{MIMETYPE_GA_PRESENTATION}')",
|
|
'mysheets': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_SPREADSHEET}'",
|
|
'myshortcuts': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_SHORTCUT}'",
|
|
'myslides': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_PRESENTATION}'",
|
|
'my3pshortcuts': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_3P_SHORTCUT}'",
|
|
'myitems': ME_IN_OWNERS,
|
|
'mytopfiles': ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}' and 'root' in parents",
|
|
'mytopfolders': ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FOLDER}' and 'root' in parents",
|
|
'mytopitems': ME_IN_OWNERS_AND+"'root' in parents",
|
|
'othersfiles': NOT_ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}'",
|
|
'othersfolders': NOT_ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FOLDER}'",
|
|
'othersforms': NOT_ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_FORM}'",
|
|
'othersgooglefiles': NOT_ME_IN_OWNERS_AND+f"mimeType != '{MIMETYPE_GA_FOLDER}' and mimeType contains 'vnd.google'",
|
|
'othersnongooglefiles': NOT_ME_IN_OWNERS_AND+"not mimeType contains 'vnd.google'",
|
|
'othersshortcuts': NOT_ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_SHORTCUT}'",
|
|
'others3pshortcuts': NOT_ME_IN_OWNERS_AND+f"mimeType = '{MIMETYPE_GA_3P_SHORTCUT}'",
|
|
'othersitems': NOT_ME_IN_OWNERS,
|
|
'writablefiles': f"'me' in writers and mimeType != '{MIMETYPE_GA_FOLDER}'",
|
|
}
|
|
SHAREDDRIVE_QUERY_SHORTCUTS_MAP = {
|
|
'allfiles': f"mimeType != '{MIMETYPE_GA_FOLDER}'",
|
|
'allfolders': f"mimeType = '{MIMETYPE_GA_FOLDER}'",
|
|
'allgooglefiles': f"mimeType != '{MIMETYPE_GA_FOLDER}' and mimeType contains 'vnd.google'",
|
|
'allnongooglefiles': "not mimeType contains 'vnd.google'",
|
|
'allitems': 'allitems',
|
|
}
|
|
|
|
def doDriveSearch(drive, user, i, count, query=None, parentQuery=False, emptyQueryOK=False, orderBy=None, sharedDriveOnly=False, **kwargs):
|
|
if query == 'allitems':
|
|
query = None
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count, query=query)
|
|
try:
|
|
files = callGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
|
|
GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=query, orderBy=orderBy, fields='nextPageToken,files(id,driveId)', pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **kwargs)
|
|
if files or not parentQuery:
|
|
return [f_file['id'] for f_file in files if not sharedDriveOnly or f_file.get('driveId')]
|
|
if emptyQueryOK:
|
|
return []
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None],
|
|
emptyQuery(query, Ent.DRIVE_FILE_OR_FOLDER if not parentQuery else Ent.DRIVE_PARENT_FOLDER), i, count)
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], invalidQuery(query), i, count)
|
|
except GAPI.fileNotFound:
|
|
printGotEntityItemsForWhom(0)
|
|
if emptyQueryOK:
|
|
return []
|
|
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, kwargs['driveId']], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return None
|
|
|
|
def doSharedDriveSearch(drive, user, i, count, query, useDomainAdminAccess):
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, user, i, count, query=query)
|
|
try:
|
|
files = callGAPIpages(drive.drives(), 'list', 'drives',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.QUERY_REQUIRES_ADMIN_CREDENTIALS,
|
|
GAPI.NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE],
|
|
q=query, useDomainAdminAccess=useDomainAdminAccess,
|
|
fields='nextPageToken,drives(id)', pageSize=100)
|
|
if files:
|
|
return [f_file['id'] for f_file in files]
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], emptyQuery(query, Ent.SHAREDDRIVE), i, count)
|
|
except (GAPI.invalidQuery, GAPI.invalid):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, None], invalidQuery(query), i, count)
|
|
except (GAPI.queryRequiresAdminCredentials, GAPI.noListTeamDrivesAdministratorPrivilege) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, None], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return None
|
|
|
|
def _getFileIdFromURL(fileId):
|
|
loc = fileId.find('/d/')
|
|
if loc > 0:
|
|
fileId = fileId[loc+3:]
|
|
loc = fileId.find('/')
|
|
return fileId[:loc] if loc != -1 else fileId
|
|
loc = fileId.find('?id=')
|
|
if loc > 0:
|
|
fileId = fileId[loc+4:]
|
|
loc = fileId.find('&')
|
|
return fileId[:loc] if loc != -1 else fileId
|
|
loc = fileId.find('/files/')
|
|
if loc > 0:
|
|
fileId = fileId[loc+7:]
|
|
loc = fileId.find('&')
|
|
if loc > 0:
|
|
return fileId[:loc]
|
|
loc = fileId.find('?')
|
|
return fileId[:loc] if loc != -1 else fileId
|
|
loc = fileId.find('/folders/')
|
|
if loc > 0:
|
|
fileId = fileId[loc+9:]
|
|
loc = fileId.find('&')
|
|
if loc > 0:
|
|
return fileId[:loc]
|
|
loc = fileId.find('?')
|
|
return fileId[:loc] if loc != -1 else fileId
|
|
return None
|
|
|
|
def cleanFileIDsList(fileIdEntity, fileIds):
|
|
fileIdEntity['list'] = []
|
|
fileIdEntity[ROOT] = []
|
|
i = 0
|
|
for fileId in fileIds:
|
|
if fileId[:8].lower() == 'https://' or fileId[:7].lower() == 'http://':
|
|
fileId = _getFileIdFromURL(fileId)
|
|
if fileId is None:
|
|
continue
|
|
elif fileId.lower() in {ROOT, ROOTID}:
|
|
fileIdEntity[ROOT].append(i)
|
|
fileId = fileId.lower()
|
|
fileIdEntity['list'].append(fileId)
|
|
i += 1
|
|
|
|
TITLE_QUERY_PATTERN = re.compile(r'title((?: *!?=)|(?: +contains))', flags=re.IGNORECASE)
|
|
|
|
def _mapDrive2QueryToDrive3(query):
|
|
if query:
|
|
query = TITLE_QUERY_PATTERN.sub(r'name\1', query).replace('modifiedDate', 'modifiedTime').replace('lastViewedByMeDate', 'viewedByMeTime')
|
|
query = mapQueryRelativeTimes(query, ['modifiedTime', 'viewedByMeTime'])
|
|
return query
|
|
|
|
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 getEscapedDriveFileName():
|
|
return escapeDriveFileName(getString(Cmd.OB_DRIVE_FILE_NAME))
|
|
|
|
def getEscapedDriveFolderName():
|
|
return escapeDriveFileName(getString(Cmd.OB_DRIVE_FOLDER_NAME))
|
|
|
|
def initDriveFileEntity():
|
|
return {'list': [], 'shareddrivename': None, 'shareddriveadminquery': None, 'shareddrivefilequery': None,
|
|
'query': None, 'dict': None, ROOT: [], 'shareddrive': {},
|
|
'location': None, 'nonDomainAdminAccess': False}
|
|
|
|
DRIVE_BY_NAME_CHOICE_MAP = {
|
|
'anyname': WITH_ANY_FILE_NAME,
|
|
'anydrivefilename': WITH_ANY_FILE_NAME,
|
|
'anyownername': WITH_ANY_FILE_NAME,
|
|
'anyownerdrivefilename': WITH_ANY_FILE_NAME,
|
|
'sharedname': WITH_ANY_FILE_NAME,
|
|
'shareddrivefilename': WITH_ANY_FILE_NAME,
|
|
'name': WITH_MY_FILE_NAME,
|
|
'drivefilename': WITH_MY_FILE_NAME,
|
|
'othername': WITH_OTHER_FILE_NAME,
|
|
'otherdrivefilename': WITH_ANY_FILE_NAME,
|
|
}
|
|
|
|
LOCATION_ALL_DRIVES = 0
|
|
LOCATION_MYDRIVE = 1
|
|
LOCATION_ORPHANS = 2
|
|
LOCATION_OWNEDBY_ANY = 3
|
|
LOCATION_OWNEDBY_OTHERS = 4
|
|
LOCATION_SHARED_WITHME = 5
|
|
LOCATION_ONLY_SHARED_DRIVES = 6
|
|
|
|
LOCATION_CHOICE_MAP = {
|
|
'alldrives': {'fileids': [ROOT, ORPHANS, SHARED_WITHME, SHARED_DRIVES], 'owner': None, 'location': LOCATION_ALL_DRIVES, 'setShowOwnedBy': True},
|
|
'root': {'fileids': [ROOT], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': False},
|
|
'rootwithorphans': {'fileids': [ROOT, ORPHANS], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': False},
|
|
'mydrive': {'fileids': [ROOT], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': False},
|
|
'mydrivewithorphans': {'fileids': [ROOT, ORPHANS], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': False},
|
|
'mydriveany': {'fileids': [ROOT, ORPHANS], 'owner': None, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
|
|
'mydriveme': {'fileids': [ROOT, ORPHANS], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
|
|
'mydriveothers': {'fileids': [ROOT], 'owner': False, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
|
|
'onlyshareddrives': {'fileids': [SHARED_DRIVES], 'owner': False, 'location': LOCATION_ONLY_SHARED_DRIVES, 'setShowOwnedBy': True},
|
|
'onlyteamdrives': {'fileids': [SHARED_DRIVES], 'owner': False, 'location': LOCATION_ONLY_SHARED_DRIVES, 'setShowOwnedBy': True},
|
|
'orphans': {'fileids': [ORPHANS], 'owner': True, 'location': LOCATION_ORPHANS, 'setShowOwnedBy': True},
|
|
'ownedbyany': {'fileids': [ROOT, ORPHANS, SHARED_WITHME], 'owner': None, 'location': LOCATION_OWNEDBY_ANY, 'setShowOwnedBy': True},
|
|
'ownedbyme': {'fileids': [ROOT, ORPHANS, SHARED_WITHME], 'owner': True, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
|
|
'ownedbyothers': {'fileids': [ROOT, ORPHANS, SHARED_WITHME], 'owner': False, 'location': LOCATION_OWNEDBY_OTHERS, 'setShowOwnedBy': True},
|
|
'sharedwithme': {'fileids': [ROOT, SHARED_WITHME], 'owner': False, 'location': LOCATION_OWNEDBY_OTHERS, 'setShowOwnedBy': True},
|
|
'sharedwithmemydrive': {'fileids': [ROOT], 'owner': False, 'location': LOCATION_MYDRIVE, 'setShowOwnedBy': True},
|
|
'sharedwithmenotmydrive': {'fileids': [SHARED_WITHME], 'owner': False, 'location': LOCATION_SHARED_WITHME, 'setShowOwnedBy': True},
|
|
}
|
|
|
|
def getDriveFileEntity(queryShortcutsOK=True, DLP=None):
|
|
def _getKeywordColonValue(kwColonValue):
|
|
kw, value = kwColonValue.split(':', 1)
|
|
kw = kw.lower().replace('_', '').replace('-', '')
|
|
if kw == 'id':
|
|
cleanFileIDsList(fileIdEntity, [value])
|
|
elif kw == 'ids':
|
|
cleanFileIDsList(fileIdEntity, value.replace(',', ' ').split())
|
|
elif kw == 'query':
|
|
fileIdEntity['query'] = _mapDrive2QueryToDrive3(value)
|
|
fileIdEntity['nonDomainAdminAccess'] = True
|
|
elif kw in DRIVE_BY_NAME_CHOICE_MAP:
|
|
fileIdEntity['query'] = DRIVE_BY_NAME_CHOICE_MAP[kw].format(escapeDriveFileName(value))
|
|
fileIdEntity['nonDomainAdminAccess'] = True
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _getTDKeywordColonValue(kwColonValue):
|
|
kw, value = kwColonValue.split(':', 1)
|
|
kw = kw.lower().replace('_', '').replace('-', '')
|
|
if kw in {'teamdriveid', 'shareddriveid'}:
|
|
fileIdEntity['shareddrive']['driveId'] = value
|
|
elif kw in {'teamdrive', 'shareddrive'}:
|
|
fileIdEntity['shareddrivename'] = value
|
|
elif kw in {'teamdriveadminquery', 'shareddriveadminquery'}:
|
|
fileIdEntity['shareddriveadminquery'] = value
|
|
elif kw in {'teamdrivefilename', 'shareddrivefilename'}:
|
|
fileIdEntity['shareddrivefilequery'] = WITH_ANY_FILE_NAME.format(escapeDriveFileName(value))
|
|
elif kw in {'teamdrivequery', 'shareddrivequery'}:
|
|
fileIdEntity['shareddrivefilequery'] = _mapDrive2QueryToDrive3(value)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
fileIdEntity = initDriveFileEntity()
|
|
entitySelector = getEntitySelector()
|
|
if entitySelector:
|
|
entityList = getEntitySelection(entitySelector, False)
|
|
if isinstance(entityList, dict):
|
|
fileIdEntity['dict'] = entityList
|
|
else:
|
|
cleanFileIDsList(fileIdEntity, entityList)
|
|
return fileIdEntity
|
|
fileIdEntity['location'] = Cmd.Location()
|
|
myarg = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
|
|
mycmd = myarg.lower().replace('_', '').replace('-', '')
|
|
if mycmd == 'id':
|
|
cleanFileIDsList(fileIdEntity, getStringReturnInList(Cmd.OB_DRIVE_FILE_ID))
|
|
elif mycmd == 'ids':
|
|
cleanFileIDsList(fileIdEntity, getString(Cmd.OB_DRIVE_FILE_ID).replace(',', ' ').split())
|
|
elif mycmd == 'query':
|
|
fileIdEntity['query'] = _mapDrive2QueryToDrive3(getString(Cmd.OB_QUERY))
|
|
fileIdEntity['nonDomainAdminAccess'] = True
|
|
elif queryShortcutsOK and mycmd in QUERY_SHORTCUTS_MAP:
|
|
fileIdEntity['query'] = QUERY_SHORTCUTS_MAP[mycmd]
|
|
fileIdEntity['nonDomainAdminAccess'] = True
|
|
elif mycmd in DRIVE_BY_NAME_CHOICE_MAP:
|
|
fileIdEntity['query'] = DRIVE_BY_NAME_CHOICE_MAP[mycmd].format(getEscapedDriveFileName())
|
|
fileIdEntity['nonDomainAdminAccess'] = True
|
|
elif mycmd in {'rootid', 'mydriveid'}:
|
|
cleanFileIDsList(fileIdEntity, [ROOTID])
|
|
fileIdEntity['nonDomainAdminAccess'] = True
|
|
elif not DLP and mycmd in {'root', 'mydrive'}:
|
|
cleanFileIDsList(fileIdEntity, [ROOT])
|
|
fileIdEntity['nonDomainAdminAccess'] = True
|
|
elif DLP and mycmd in LOCATION_CHOICE_MAP:
|
|
DLP.SetLocation(LOCATION_CHOICE_MAP[mycmd])
|
|
cleanFileIDsList(fileIdEntity, DLP.locationFileIds)
|
|
fileIdEntity['nonDomainAdminAccess'] = True
|
|
elif mycmd.startswith('teamdrive') or mycmd.startswith('shareddrive'):
|
|
fileIdEntity['shareddrive'] = {'driveId': None,
|
|
'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
while True:
|
|
if mycmd in {'teamdriveid', 'shareddriveid'}:
|
|
fileIdEntity['shareddrive']['driveId'] = getString(Cmd.OB_SHAREDDRIVE_ID).strip()
|
|
elif mycmd in {'teamdrive', 'shareddrive'}:
|
|
fileIdEntity['shareddrivename'] = getString(Cmd.OB_SHAREDDRIVE_NAME)
|
|
elif mycmd in {'teamdriveadminquery', 'shareddriveadminquery'}:
|
|
fileIdEntity['shareddriveadminquery'] = getString(Cmd.OB_QUERY)
|
|
elif mycmd in {'teamdrivefilename', 'shareddrivefilename'}:
|
|
fileIdEntity['shareddrivefilequery'] = WITH_ANY_FILE_NAME.format(getEscapedDriveFileName())
|
|
elif mycmd in {'teamdrivequery', 'shareddrivequery'}:
|
|
fileIdEntity['shareddrivefilequery'] = _mapDrive2QueryToDrive3(getString(Cmd.OB_QUERY))
|
|
elif queryShortcutsOK and mycmd in SHAREDDRIVE_QUERY_SHORTCUTS_MAP:
|
|
fileIdEntity['shareddrivefilequery'] = SHAREDDRIVE_QUERY_SHORTCUTS_MAP[mycmd]
|
|
elif (mycmd.find(':') > 0) and _getTDKeywordColonValue(myarg):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
if Cmd.ArgumentsRemaining():
|
|
myarg = getString(Cmd.OB_STRING)
|
|
mycmd = myarg.lower().replace('_', '').replace('-', '')
|
|
if (mycmd.startswith('teamdriveparent') or mycmd.startswith('shareddriveparent') or
|
|
((not (mycmd.startswith('teamdrive') or mycmd.startswith('shareddrive'))) and
|
|
(not (queryShortcutsOK and mycmd in SHAREDDRIVE_QUERY_SHORTCUTS_MAP)))):
|
|
Cmd.Backup()
|
|
break
|
|
else:
|
|
break
|
|
if not fileIdEntity['shareddrive'].get('driveId'):
|
|
fileIdEntity['shareddrive']['corpora'] = CORPORA_ALL_DRIVES
|
|
elif (mycmd.find(':') > 0) and _getKeywordColonValue(myarg):
|
|
pass
|
|
else:
|
|
cleanFileIDsList(fileIdEntity, [myarg])
|
|
return fileIdEntity
|
|
|
|
def getDriveFileEntitySharedDriveOnly():
|
|
def _getTDKeywordColonValue(kwColonValue):
|
|
kw, value = kwColonValue.split(':', 1)
|
|
kw = kw.lower().replace('_', '').replace('-', '')
|
|
if kw in {'teamdriveid', 'shareddriveid'}:
|
|
fileIdEntity['shareddrive']['driveId'] = value
|
|
elif kw in {'teamdrive', 'shareddrive'}:
|
|
fileIdEntity['shareddrivename'] = value
|
|
elif kw in {'teamdriveadminquery', 'shareddriveadminquery'}:
|
|
fileIdEntity['shareddriveadminquery'] = value
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
fileIdEntity = initDriveFileEntity()
|
|
myarg = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
|
|
mycmd = myarg.lower().replace('_', '').replace('-', '')
|
|
if mycmd.startswith('teamdrive') or mycmd.startswith('shareddrive'):
|
|
fileIdEntity['shareddrive'] = {'driveId': None,
|
|
'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
if mycmd in {'teamdriveid', 'shareddriveid'}:
|
|
fileIdEntity['shareddrive']['driveId'] = getString(Cmd.OB_SHAREDDRIVE_ID)
|
|
elif mycmd in {'teamdrive', 'shareddrive'}:
|
|
fileIdEntity['shareddrivename'] = getString(Cmd.OB_SHAREDDRIVE_NAME)
|
|
elif mycmd in {'teamdriveadminquery', 'shareddriveadminquery'}:
|
|
fileIdEntity['shareddriveadminquery'] = getString(Cmd.OB_QUERY)
|
|
elif (mycmd.find(':') > 0) and _getTDKeywordColonValue(myarg):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
if not fileIdEntity['shareddrive'].get('driveId'):
|
|
fileIdEntity['shareddrive']['corpora'] = CORPORA_ALL_DRIVES
|
|
elif (mycmd.find(':') > 0) and _getTDKeywordColonValue(myarg):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
return fileIdEntity
|
|
|
|
def getSharedDriveEntity():
|
|
def _getTDKeywordColonValue(kwColonValue):
|
|
kw, value = kwColonValue.split(':', 1)
|
|
kw = kw.lower().replace('_', '').replace('-', '')
|
|
if kw in {'teamdriveid', 'shareddriveid'}:
|
|
fileIdEntity['shareddrive']['driveId'] = value
|
|
elif kw in {'teamdrive', 'shareddrive', 'name'}:
|
|
fileIdEntity['shareddrivename'] = value
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
fileIdEntity = initDriveFileEntity()
|
|
myarg = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
|
|
mycmd = myarg.lower().replace('_', '').replace('-', '')
|
|
fileIdEntity['shareddrive'] = {'driveId': None,
|
|
'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
if mycmd in {'teamdriveid', 'shareddriveid'}:
|
|
fileIdEntity['shareddrive']['driveId'] = getString(Cmd.OB_SHAREDDRIVE_ID)
|
|
elif mycmd in {'teamdrive', 'shareddrive', 'name'}:
|
|
fileIdEntity['shareddrivename'] = getString(Cmd.OB_SHAREDDRIVE_NAME)
|
|
elif (mycmd.find(':') > 0) and _getTDKeywordColonValue(myarg):
|
|
pass
|
|
else:
|
|
fileIdEntity['shareddrive']['driveId'] = myarg
|
|
return fileIdEntity
|
|
|
|
def _convertSharedDriveNameToId(drive, user, i, count, fileIdEntity, useDomainAdminAccess=False):
|
|
try:
|
|
if "\\'" in fileIdEntity['shareddrivename']:
|
|
name = fileIdEntity['shareddrivename']
|
|
else:
|
|
name = fileIdEntity['shareddrivename'].replace("'", "\\'")
|
|
tdlist = callGAPIpages(drive.drives(), 'list', 'drives',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
q=f"name='{name}'",
|
|
fields='nextPageToken,drives(id,name)', pageSize=100)
|
|
if "\\'" in fileIdEntity['shareddrivename']:
|
|
fileIdEntity['shareddrivename'] = fileIdEntity['shareddrivename'].replace("\\'", "'")
|
|
if not tdlist:
|
|
name = fileIdEntity['shareddrivename'].lower()
|
|
feed = callGAPIpages(drive.drives(), 'list', 'drives',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
fields='nextPageToken,drives(id,name)', pageSize=100)
|
|
tdlist = [td for td in feed if td['name'].lower() == name]
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_NAME, fileIdEntity['shareddrivename']], Msg.DOES_NOT_EXIST, i, count)
|
|
return False
|
|
jcount = len(tdlist)
|
|
if jcount == 1:
|
|
fileIdEntity['shareddrive']['driveId'] = tdlist[0]['id']
|
|
return True
|
|
if jcount == 0:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_NAME, fileIdEntity['shareddrivename']], Msg.DOES_NOT_EXIST, i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_NAME, fileIdEntity['shareddrivename']],
|
|
Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.SHAREDDRIVE_ID), jcount,
|
|
','.join([td['id'] for td in tdlist])), i, count)
|
|
return False
|
|
|
|
def _getSharedDriveNameFromId(sharedDriveId):
|
|
sharedDriveName = GM.Globals[GM.MAP_SHAREDDRIVE_ID_TO_NAME].get(sharedDriveId)
|
|
if not sharedDriveName:
|
|
if not GM.Globals[GM.ADMIN_DRIVE]:
|
|
_, GM.Globals[GM.ADMIN_DRIVE] = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
|
|
if GM.Globals[GM.ADMIN_DRIVE]:
|
|
try:
|
|
sharedDriveName = callGAPI(GM.Globals[GM.ADMIN_DRIVE].drives(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
useDomainAdminAccess=True,
|
|
driveId=sharedDriveId, fields='name')['name']
|
|
except (GAPI.notFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
sharedDriveName = TEAM_DRIVE
|
|
else:
|
|
sharedDriveName = TEAM_DRIVE
|
|
GM.Globals[GM.MAP_SHAREDDRIVE_ID_TO_NAME][sharedDriveId] = sharedDriveName
|
|
return sharedDriveName
|
|
|
|
def _getDriveFileNameFromId(drive, fileId, combineTitleId=True, useDomainAdminAccess=False):
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
|
|
fileId=fileId, fields='name,mimeType,driveId', supportsAllDrives=True)
|
|
if result:
|
|
fileName = result['name']
|
|
if (result['mimeType'] == MIMETYPE_GA_FOLDER) and result.get('driveId') and (result['name'] == TEAM_DRIVE):
|
|
fileName = _getSharedDriveNameFromId(result['driveId'])
|
|
if combineTitleId:
|
|
fileName += '('+fileId+')'
|
|
return (fileName, _getEntityMimeType(result), result['mimeType'])
|
|
except GAPI.fileNotFound:
|
|
if useDomainAdminAccess:
|
|
try:
|
|
result = callGAPI(drive.drives(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
driveId=fileId, fields='name')
|
|
if result:
|
|
fileName = result['name']
|
|
if combineTitleId:
|
|
fileName += '('+fileId+')'
|
|
return (fileName, Ent.DRIVE_FOLDER, MIMETYPE_GA_FOLDER)
|
|
except GAPI.notFound:
|
|
pass
|
|
except (GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.internalError,
|
|
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
pass
|
|
return (fileId, Ent.DRIVE_FILE_OR_FOLDER_ID, None)
|
|
|
|
def _simpleFileIdEntityList(fileIdEntityList):
|
|
for fileId in fileIdEntityList:
|
|
if fileId not in {ROOT, ORPHANS, SHARED_WITHME, SHARED_DRIVES}:
|
|
return False
|
|
return True
|
|
|
|
def _validateUserGetFileIDs(user, i, count, fileIdEntity, drive=None, entityType=None, orderBy=None, useDomainAdminAccess=False):
|
|
def _identifyRoot():
|
|
try:
|
|
rootFolderId = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=ROOT, fields='id')['id']
|
|
for j in fileIdEntity[ROOT]:
|
|
fileIdEntity['list'][j] = rootFolderId
|
|
return True
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return False
|
|
|
|
if fileIdEntity['dict']:
|
|
cleanFileIDsList(fileIdEntity, fileIdEntity['dict'][user])
|
|
if not drive:
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
return (user, None, 0)
|
|
else:
|
|
user = convertUIDtoEmailAddress(user)
|
|
if fileIdEntity['list'] and _simpleFileIdEntityList(fileIdEntity['list']):
|
|
l = len(fileIdEntity['list'])
|
|
if ROOT in fileIdEntity['list'] and fileIdEntity[ROOT]:
|
|
if not _identifyRoot():
|
|
return (user, None, 0)
|
|
if entityType:
|
|
entityPerformActionNumItems([Ent.USER, user], l, entityType, i, count)
|
|
return (user, drive, l)
|
|
if fileIdEntity['shareddrivename'] and 'driveId' not in fileIdEntity:
|
|
if not _convertSharedDriveNameToId(drive, user, i, count, fileIdEntity, useDomainAdminAccess):
|
|
return (user, None, 0)
|
|
if not fileIdEntity['shareddrivefilequery']:
|
|
fileIdEntity['list'] = [fileIdEntity['shareddrive']['driveId']]
|
|
fileIdEntity['shareddrive']['corpora'] = 'drive'
|
|
elif fileIdEntity['shareddrive'].get('driveId'):
|
|
if not fileIdEntity['shareddrivefilequery']:
|
|
fileIdEntity['list'] = [fileIdEntity['shareddrive']['driveId']]
|
|
fileIdEntity['shareddrive']['corpora'] = 'drive'
|
|
if fileIdEntity['query']:
|
|
fileIdEntity['list'] = doDriveSearch(drive, user, i, count, query=fileIdEntity['query'], orderBy=orderBy)
|
|
if fileIdEntity['list'] is None:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (user, None, 0)
|
|
elif fileIdEntity['shareddriveadminquery']:
|
|
fileIdEntity['list'] = doSharedDriveSearch(drive, user, i, count, fileIdEntity['shareddriveadminquery'], useDomainAdminAccess)
|
|
if fileIdEntity['list'] is None:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (user, None, 0)
|
|
elif fileIdEntity['shareddrivefilequery']:
|
|
if not fileIdEntity['shareddrive'].get('driveId'):
|
|
fileIdEntity['shareddrive']['corpora'] = CORPORA_ALL_DRIVES
|
|
fileIdEntity['list'] = doDriveSearch(drive, user, i, count, query=fileIdEntity['shareddrivefilequery'], orderBy=orderBy, sharedDriveOnly=True, **fileIdEntity['shareddrive'])
|
|
if fileIdEntity['list'] is None or not fileIdEntity['list']:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (user, None, 0)
|
|
fileIdEntity['shareddrive'].pop('driveId', None)
|
|
fileIdEntity['shareddrive'].pop('corpora', None)
|
|
elif fileIdEntity[ROOT]:
|
|
if not _identifyRoot():
|
|
return (user, None, 0)
|
|
l = len(fileIdEntity['list'])
|
|
if l == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if entityType:
|
|
entityPerformActionNumItems([Ent.USER, user], l, entityType, i, count)
|
|
return (user, drive, l)
|
|
|
|
# Disc File Access paramaters
|
|
DFA_CREATED_TIME = 'createdTime'
|
|
DFA_MODIFIED_TIME = 'modifiedTime'
|
|
DFA_PRESERVE_FILE_TIMES = 'preserveFileTimes'
|
|
DFA_IGNORE_DEFAULT_VISIBILITY = 'ignoreDefaultVisibility'
|
|
DFA_KEEP_REVISION_FOREVER = 'keepRevisionForever'
|
|
DFA_URL = 'url'
|
|
DFA_LOCALFILEPATH = 'localFilepath'
|
|
DFA_LOCALFILENAME = 'localFilename'
|
|
DFA_LOCALMIMETYPE = 'localMimeType'
|
|
DFA_STRIPNAMEPREFIX = 'stripNamePrefix'
|
|
DFA_REPLACEFILENAME = 'replaceFileName'
|
|
DFA_OCRLANGUAGE = 'ocrLanguage'
|
|
DFA_PARENTID = 'parentId'
|
|
DFA_PARENTQUERY = 'parentQuery'
|
|
DFA_ADD_PARENT_IDS = 'addParentIds'
|
|
DFA_ADD_PARENT_NAMES = 'addParentNames'
|
|
DFA_REMOVE_PARENT_IDS = 'removeParentIds'
|
|
DFA_REMOVE_PARENT_NAMES = 'removeParentNames'
|
|
DFA_SHAREDDRIVE_PARENT = 'sharedDriveParent'
|
|
DFA_SHAREDDRIVE_PARENTID = 'sharedDriveParentId'
|
|
DFA_SHAREDDRIVE_PARENTQUERY = 'sharedDriveParentQuery'
|
|
DFA_KWARGS = 'kwargs'
|
|
DFA_SEARCHARGS = 'searchargs'
|
|
DFA_USE_CONTENT_AS_INDEXABLE_TEXT = 'useContentAsIndexableText'
|
|
DFA_TIMESTAMP = 'timestamp'
|
|
DFA_TIMEFORMAT = 'timeformat'
|
|
|
|
def _driveFileParentSpecified(parameters):
|
|
return (parameters[DFA_PARENTID] or parameters[DFA_PARENTQUERY] or
|
|
parameters[DFA_SHAREDDRIVE_PARENT] or parameters[DFA_SHAREDDRIVE_PARENTID] or
|
|
parameters[DFA_SHAREDDRIVE_PARENTQUERY])
|
|
|
|
def _getDriveFileParentInfo(drive, user, i, count, body, parameters, emptyQueryOK=False, defaultToRoot=True, entityType=Ent.DRIVE_FILE):
|
|
def _setSearchArgs(driveId):
|
|
parameters[DFA_SEARCHARGS] = {'driveId': driveId, 'corpora': 'drive',
|
|
'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
body.pop('parents', None)
|
|
if parameters[DFA_PARENTID]:
|
|
body.setdefault('parents', [])
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND],
|
|
fileId=parameters[DFA_PARENTID], fields='id,name,mimeType,driveId', supportsAllDrives=True)
|
|
if result['mimeType'] != MIMETYPE_GA_FOLDER:
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
|
|
f'parentid: {parameters[DFA_PARENTID]}, {Msg.NOT_AN_ENTITY.format((Ent.Singular(Ent.DRIVE_FOLDER)))}', i, count)
|
|
return False
|
|
body['parents'].append(result['id'])
|
|
if result.get('driveId'):
|
|
_setSearchArgs(result['driveId'])
|
|
except GAPI.fileNotFound as e:
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
|
|
f'parentid: {parameters[DFA_PARENTID]}, {str(e)}', i, count)
|
|
return False
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return False
|
|
if parameters[DFA_PARENTQUERY]:
|
|
parents = doDriveSearch(drive, user, i, count, query=parameters[DFA_PARENTQUERY], parentQuery=True, emptyQueryOK=emptyQueryOK)
|
|
if parents is None:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return False
|
|
body.setdefault('parents', [])
|
|
for parent in parents:
|
|
body['parents'].append(parent)
|
|
if parameters[DFA_SHAREDDRIVE_PARENTID]:
|
|
try:
|
|
if not parameters[DFA_SHAREDDRIVE_PARENTQUERY]:
|
|
body.setdefault('parents', [])
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND],
|
|
fileId=parameters[DFA_SHAREDDRIVE_PARENTID], fields='id,mimeType,driveId', supportsAllDrives=True)
|
|
if result['mimeType'] != MIMETYPE_GA_FOLDER:
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
|
|
f'shareddriveparentid: {parameters[DFA_SHAREDDRIVE_PARENTID]}, {Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FOLDER))}', i, count)
|
|
return False
|
|
if not result.get('driveId'):
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
|
|
f'shareddriveparentid: {parameters[DFA_SHAREDDRIVE_PARENTID]}, {Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.SHAREDDRIVE_FOLDER))}', i, count)
|
|
return False
|
|
body['parents'].append(result['id'])
|
|
_setSearchArgs(result['driveId'])
|
|
else:
|
|
result = callGAPI(drive.drives(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
driveId=parameters[DFA_SHAREDDRIVE_PARENTID], fields='id')
|
|
parameters[DFA_KWARGS]['corpora'] = 'drive'
|
|
parameters[DFA_KWARGS]['driveId'] = result['id']
|
|
_setSearchArgs(result['id'])
|
|
except (GAPI.fileNotFound, GAPI.notFound) as e:
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
|
|
f'shareddriveparentid: {parameters[DFA_SHAREDDRIVE_PARENTID]}, {str(e)}', i, count)
|
|
return False
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return False
|
|
if parameters[DFA_SHAREDDRIVE_PARENT]:
|
|
tempIdEntity = {'shareddrivename': parameters[DFA_SHAREDDRIVE_PARENT], 'shareddrive': {}}
|
|
if not _convertSharedDriveNameToId(drive, user, i, count, tempIdEntity):
|
|
return False
|
|
if not parameters[DFA_SHAREDDRIVE_PARENTQUERY]:
|
|
body.setdefault('parents', [])
|
|
body['parents'].append(tempIdEntity['shareddrive']['driveId'])
|
|
else:
|
|
parameters[DFA_KWARGS]['corpora'] = 'drive'
|
|
parameters[DFA_KWARGS]['driveId'] = tempIdEntity['shareddrive']['driveId']
|
|
_setSearchArgs(tempIdEntity['shareddrive']['driveId'])
|
|
if parameters[DFA_SHAREDDRIVE_PARENTQUERY]:
|
|
parents = doDriveSearch(drive, user, i, count, query=parameters[DFA_SHAREDDRIVE_PARENTQUERY], parentQuery=True, emptyQueryOK=emptyQueryOK,
|
|
sharedDriveOnly=True, **parameters[DFA_KWARGS])
|
|
if parents is None:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return False
|
|
body.setdefault('parents', [])
|
|
for parent in parents:
|
|
body['parents'].append(parent)
|
|
if defaultToRoot and ('parents' not in body or not body['parents']):
|
|
body['parents'] = [ROOT]
|
|
numParents = len(body.get('parents', []))
|
|
if numParents > 1:
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, None],
|
|
Msg.MULTIPLE_PARENTS_SPECIFIED.format(numParents), i, count)
|
|
return False
|
|
return True
|
|
|
|
def _getDriveFileAddRemoveParentInfo(user, i, count, parameters, drive):
|
|
addParents = parameters[DFA_ADD_PARENT_IDS][:]
|
|
for query in parameters[DFA_ADD_PARENT_NAMES]:
|
|
parents = doDriveSearch(drive, user, i, count, query=query, parentQuery=True)
|
|
if parents is None:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (False, None, None)
|
|
addParents.extend(parents)
|
|
removeParents = parameters[DFA_REMOVE_PARENT_IDS][:]
|
|
for query in parameters[DFA_REMOVE_PARENT_NAMES]:
|
|
parents = doDriveSearch(drive, user, i, count, query=query, parentQuery=True)
|
|
if parents is None:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (False, None, None)
|
|
removeParents.extend(parents)
|
|
return (True, addParents, removeParents)
|
|
|
|
def _validateUserGetSharedDriveFileIDs(user, i, count, fileIdEntity, drive=None, entityType=None):
|
|
if fileIdEntity['dict']:
|
|
cleanFileIDsList(fileIdEntity, fileIdEntity['dict'][user])
|
|
if not drive:
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
return (user, None, 0)
|
|
else:
|
|
user = convertUIDtoEmailAddress(user)
|
|
if fileIdEntity.get('shareddrivename') and not _convertSharedDriveNameToId(drive, user, i, count, fileIdEntity):
|
|
return (user, None, 0)
|
|
if fileIdEntity['shareddrivefilequery']:
|
|
fileIdEntity['list'] = doDriveSearch(drive, user, i, count, query=fileIdEntity['shareddrivefilequery'], sharedDriveOnly=True, **fileIdEntity['shareddrive'])
|
|
if fileIdEntity['list'] is None or not fileIdEntity['list']:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (user, None, 0)
|
|
fileIdEntity['shareddrive'].pop('driveId', None)
|
|
fileIdEntity['shareddrive'].pop('corpora', None)
|
|
l = len(fileIdEntity['list'])
|
|
if l == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if entityType:
|
|
entityPerformActionNumItems([Ent.USER, user], l, entityType, i, count)
|
|
return (user, drive, l)
|
|
|
|
def _validateUserSharedDrive(user, i, count, fileIdEntity, useDomainAdminAccess=False):
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
return (user, None)
|
|
if fileIdEntity.get('shareddrivename'):
|
|
if not _convertSharedDriveNameToId(drive, user, i, count, fileIdEntity, useDomainAdminAccess):
|
|
return (user, None)
|
|
fileIdEntity['shareddrive']['corpora'] = 'drive'
|
|
return (user, drive)
|
|
|
|
DRIVE_LABEL_CHOICE_MAP = {
|
|
'modified': 'modifiedByMe',
|
|
'modifiedbyme': 'modifiedByMe',
|
|
'restrict': 'copyRequiresWriterPermission',
|
|
'restricted': 'copyRequiresWriterPermission',
|
|
'star': 'starred',
|
|
'starred': 'starred',
|
|
'trash': 'trashed',
|
|
'trashed': 'trashed',
|
|
'view': 'viewedByMe',
|
|
'viewed': 'viewedByMe',
|
|
'viewedbyme': 'viewedByMe',
|
|
}
|
|
|
|
MIMETYPE_CHOICE_MAP = {
|
|
'gdoc': MIMETYPE_GA_DOCUMENT,
|
|
'gdocument': MIMETYPE_GA_DOCUMENT,
|
|
'gdrawing': MIMETYPE_GA_DRAWING,
|
|
'gfile': MIMETYPE_GA_FILE,
|
|
'gfolder': MIMETYPE_GA_FOLDER,
|
|
'gdirectory': MIMETYPE_GA_FOLDER,
|
|
'gform': MIMETYPE_GA_FORM,
|
|
'gfusion': MIMETYPE_GA_FUSIONTABLE,
|
|
'gfusiontable': MIMETYPE_GA_FUSIONTABLE,
|
|
'gjam': MIMETYPE_GA_JAM,
|
|
'gmap': MIMETYPE_GA_MAP,
|
|
'gpresentation': MIMETYPE_GA_PRESENTATION,
|
|
'gscript': MIMETYPE_GA_SCRIPT,
|
|
'gshortcut': MIMETYPE_GA_SHORTCUT,
|
|
'g3pshortcut': MIMETYPE_GA_3P_SHORTCUT,
|
|
'gsite': MIMETYPE_GA_SITE,
|
|
'gsheet': MIMETYPE_GA_SPREADSHEET,
|
|
'gspreadsheet': MIMETYPE_GA_SPREADSHEET,
|
|
'shortcut': MIMETYPE_GA_SHORTCUT,
|
|
}
|
|
|
|
MIMETYPE_TYPES = ['application', 'audio', 'font', 'image', 'message', 'model', 'multipart', 'text', 'video']
|
|
|
|
def validateMimeType(mimeType):
|
|
if mimeType in MIMETYPE_CHOICE_MAP:
|
|
return MIMETYPE_CHOICE_MAP[mimeType]
|
|
if mimeType.startswith(APPLICATION_VND_GOOGLE_APPS):
|
|
return mimeType
|
|
if mimeType.find('/') > 0:
|
|
mediaType, subType = mimeType.split('/', 1)
|
|
if mediaType in MIMETYPE_TYPES and subType:
|
|
return mimeType
|
|
invalidChoiceExit(mimeType, list(MIMETYPE_CHOICE_MAP)+[f'({formatChoiceList(MIMETYPE_TYPES)})/mediatype'], True)
|
|
|
|
def getMimeType():
|
|
return validateMimeType(getString(Cmd.OB_MIMETYPE).lower())
|
|
|
|
class MimeTypeCheck():
|
|
|
|
def __init__(self):
|
|
self.mimeTypes = set()
|
|
self.reverse = False
|
|
self.category = set()
|
|
|
|
def Get(self):
|
|
if checkArgumentPresent('category'):
|
|
for mimeType in getString(Cmd.OB_MIMETYPE_LIST).lower().replace(',', ' ').split():
|
|
if mimeType in MIMETYPE_TYPES:
|
|
self.category.add(mimeType)
|
|
else:
|
|
invalidChoiceExit(mimeType, MIMETYPE_TYPES, True)
|
|
return
|
|
self.reverse = checkArgumentPresent('not')
|
|
for mimeType in getString(Cmd.OB_MIMETYPE_LIST).lower().replace(',', ' ').split():
|
|
self.mimeTypes.add(validateMimeType(mimeType))
|
|
|
|
def AddMimeTypeToQuery(self, query):
|
|
if query:
|
|
query += ' and ('
|
|
else:
|
|
query = '('
|
|
if not self.reverse:
|
|
for mimeType in self.mimeTypes:
|
|
query += f"mimeType = '{mimeType}' or "
|
|
for mimeType in self.category:
|
|
query += f"mimeType contains '{mimeType}' or "
|
|
query = query[:-4]
|
|
else:
|
|
for mimeType in self.mimeTypes:
|
|
query += f"mimeType != '{mimeType}' and "
|
|
query = query[:-5]
|
|
query += ')'
|
|
return query
|
|
|
|
def Check(self, fileEntry):
|
|
if not self.mimeTypes and not self.category:
|
|
return True
|
|
for mimeType in self.category:
|
|
if fileEntry['mimeType'].startswith(mimeType):
|
|
return True
|
|
if not self.reverse:
|
|
return fileEntry['mimeType'] in self.mimeTypes
|
|
return fileEntry['mimeType'] not in self.mimeTypes
|
|
|
|
def initDriveFileAttributes():
|
|
return {DFA_CREATED_TIME: None,
|
|
DFA_MODIFIED_TIME: None,
|
|
DFA_PRESERVE_FILE_TIMES: False,
|
|
DFA_IGNORE_DEFAULT_VISIBILITY: False,
|
|
DFA_KEEP_REVISION_FOREVER: False,
|
|
DFA_URL: None,
|
|
DFA_LOCALFILEPATH: None,
|
|
DFA_LOCALFILENAME: None,
|
|
DFA_LOCALMIMETYPE: None,
|
|
DFA_STRIPNAMEPREFIX: None,
|
|
DFA_REPLACEFILENAME: [],
|
|
DFA_OCRLANGUAGE: None,
|
|
DFA_PARENTID: None,
|
|
DFA_PARENTQUERY: None,
|
|
DFA_ADD_PARENT_IDS: [],
|
|
DFA_ADD_PARENT_NAMES: [],
|
|
DFA_REMOVE_PARENT_IDS: [],
|
|
DFA_REMOVE_PARENT_NAMES: [],
|
|
DFA_SHAREDDRIVE_PARENT: None,
|
|
DFA_SHAREDDRIVE_PARENTID: None,
|
|
DFA_SHAREDDRIVE_PARENTQUERY: None,
|
|
DFA_KWARGS: {},
|
|
DFA_SEARCHARGS: {},
|
|
DFA_USE_CONTENT_AS_INDEXABLE_TEXT: False,
|
|
DFA_TIMESTAMP: False,
|
|
DFA_TIMEFORMAT: None}
|
|
|
|
DRIVEFILE_PROPERTY_VISIBILITY_CHOICE_MAP = {
|
|
'private': 'appProperties',
|
|
'public': 'properties'
|
|
}
|
|
|
|
DRIVE_FILE_CONTENT_RESTRICTIONS_CHOICE_MAP = {
|
|
'readonly': 'readOnly',
|
|
'ownerrestricted': 'ownerRestricted',
|
|
}
|
|
|
|
def getDriveFileProperty(visibility=None):
|
|
key = getString(Cmd.OB_PROPERTY_KEY)
|
|
value = getString(Cmd.OB_PROPERTY_VALUE, minLen=0) or None
|
|
if visibility is None:
|
|
if Cmd.PeekArgumentPresent(list(DRIVEFILE_PROPERTY_VISIBILITY_CHOICE_MAP.keys())):
|
|
visibility = getChoice(DRIVEFILE_PROPERTY_VISIBILITY_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
visibility = 'properties'
|
|
return {'key': key, 'value': value, 'visibility': visibility}
|
|
|
|
def getDriveFileParentAttribute(myarg, parameters):
|
|
if myarg == 'parentid':
|
|
parameters[DFA_PARENTID] = getString(Cmd.OB_DRIVE_FOLDER_ID)
|
|
elif myarg == 'parentname':
|
|
parameters[DFA_PARENTQUERY] = MY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName())
|
|
elif myarg in {'anyownerparentname', 'sharedparentname'}:
|
|
parameters[DFA_PARENTQUERY] = ANY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName())
|
|
elif myarg in {'teamdriveparent', 'shareddriveparent'}:
|
|
parameters[DFA_SHAREDDRIVE_PARENT] = getString(Cmd.OB_SHAREDDRIVE_NAME)
|
|
elif myarg in {'teamdriveparentid', 'shareddriveparentid'}:
|
|
parameters[DFA_SHAREDDRIVE_PARENTID] = getString(Cmd.OB_DRIVE_FOLDER_ID)
|
|
elif myarg in {'teamdriveparentname', 'shareddriveparentname'}:
|
|
parameters[DFA_SHAREDDRIVE_PARENTQUERY] = ANY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName())
|
|
parameters[DFA_KWARGS]['corpora'] = CORPORA_ALL_DRIVES
|
|
parameters[DFA_KWARGS]['includeItemsFromAllDrives'] = True
|
|
parameters[DFA_KWARGS]['supportsAllDrives'] = True
|
|
elif myarg == 'enforcesingleparent':
|
|
deprecatedArgument(myarg)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def getDriveFileAddRemoveParentAttribute(myarg, parameters):
|
|
if myarg in {'addparent', 'addparents'}:
|
|
parameters[DFA_ADD_PARENT_IDS].extend(getString(Cmd.OB_DRIVE_FOLDER_ID_LIST).replace(',', ' ').split())
|
|
elif myarg in {'removeparent', 'removeparents'}:
|
|
parameters[DFA_REMOVE_PARENT_IDS].extend(getString(Cmd.OB_DRIVE_FOLDER_ID_LIST).replace(',', ' ').split())
|
|
elif myarg == 'addparentname':
|
|
parameters[DFA_ADD_PARENT_NAMES].append(MY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName()))
|
|
elif myarg == 'removeparentname':
|
|
parameters[DFA_REMOVE_PARENT_NAMES].append(MY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName()))
|
|
elif myarg in {'addanyownerparentname', 'addsharedparentname'}:
|
|
parameters[DFA_ADD_PARENT_NAMES].append(ANY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName()))
|
|
elif myarg in {'removeanyownerparentname', 'removesharedparentname'}:
|
|
parameters[DFA_REMOVE_PARENT_NAMES].append(ANY_NON_TRASHED_FOLDER_NAME.format(getEscapedDriveFolderName()))
|
|
elif myarg == 'enforcesingleparent':
|
|
deprecatedArgument(myarg)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def getDriveFileCopyAttribute(myarg, body, parameters):
|
|
if myarg == 'ignoredefaultvisibility':
|
|
parameters[DFA_IGNORE_DEFAULT_VISIBILITY] = getBoolean()
|
|
elif myarg in {'keeprevisionforever', 'pinned'}:
|
|
parameters[DFA_KEEP_REVISION_FOREVER] = getBoolean()
|
|
elif myarg == 'ocrlanguage':
|
|
parameters[DFA_OCRLANGUAGE] = getLanguageCode(LANGUAGE_CODES_MAP)
|
|
elif myarg == 'description':
|
|
body['description'] = getStringWithCRsNLs()
|
|
elif myarg == 'mimetype':
|
|
body['mimeType'] = getMimeType()
|
|
elif myarg in {'lastviewedbyme', 'lastviewedbyuser', 'lastviewedbymedate', 'lastviewedbymetime'}:
|
|
body['viewedByMeTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg in {'modifieddate', 'modifiedtime'}:
|
|
body['modifiedTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg == 'viewerscancopycontent':
|
|
body['copyRequiresWriterPermission'] = not getBoolean()
|
|
elif myarg in {'copyrequireswriterpermission', 'restrict', 'restricted'}:
|
|
body['copyRequiresWriterPermission'] = getBoolean()
|
|
elif myarg == 'writerscanshare':
|
|
body['writersCanShare'] = getBoolean()
|
|
elif myarg == 'writerscantshare':
|
|
body['writersCanShare'] = not getBoolean()
|
|
elif myarg == 'contentrestrictions':
|
|
while Cmd.PeekArgumentPresent(list(DRIVE_FILE_CONTENT_RESTRICTIONS_CHOICE_MAP.keys())):
|
|
body.setdefault('contentRestrictions', [{}])
|
|
restriction = getChoice(DRIVE_FILE_CONTENT_RESTRICTIONS_CHOICE_MAP, mapChoice=True)
|
|
body['contentRestrictions'][0][restriction] = getBoolean()
|
|
if restriction == 'readOnly':
|
|
if checkArgumentPresent(['reason']):
|
|
if body['contentRestrictions'][0][restriction]:
|
|
body['contentRestrictions'][0]['reason'] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.REASON_ONLY_VALID_WITH_CONTENTRESTRICTIONS_READONLY_TRUE)
|
|
elif myarg == 'inheritedpermissionsdisabled':
|
|
body['inheritedPermissionsDisabled'] = getBoolean()
|
|
elif myarg == 'property':
|
|
driveprop = getDriveFileProperty()
|
|
body.setdefault(driveprop['visibility'], {})
|
|
body[driveprop['visibility']][driveprop['key']] = driveprop['value']
|
|
elif myarg == 'privateproperty':
|
|
driveprop = getDriveFileProperty('appProperties')
|
|
body.setdefault(driveprop['visibility'], {})
|
|
body[driveprop['visibility']][driveprop['key']] = driveprop['value']
|
|
elif myarg == 'publicproperty':
|
|
driveprop = getDriveFileProperty('properties')
|
|
body.setdefault(driveprop['visibility'], {})
|
|
body[driveprop['visibility']][driveprop['key']] = driveprop['value']
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def getDriveFileAttribute(myarg, body, parameters, updateCmd):
|
|
if myarg == 'localfile':
|
|
parameters[DFA_URL] = None
|
|
parameters[DFA_LOCALFILEPATH] = os.path.expanduser(getString(Cmd.OB_FILE_NAME))
|
|
if parameters[DFA_LOCALFILEPATH] != '-':
|
|
try:
|
|
f = open(parameters[DFA_LOCALFILEPATH], 'rb')
|
|
f.close()
|
|
# See http://stackoverflow.com/a/39501288/1709587 for explanation.
|
|
mtime = os.path.getmtime(parameters[DFA_LOCALFILEPATH])
|
|
parameters[DFA_MODIFIED_TIME] = formatLocalSecondsTimestamp(mtime)
|
|
if not updateCmd:
|
|
if platform.system() == 'Windows':
|
|
ctime = os.path.getctime(parameters[DFA_LOCALFILEPATH])
|
|
else:
|
|
stat = os.stat(parameters[DFA_LOCALFILEPATH])
|
|
if hasattr(stat, 'st_birthtime'):
|
|
ctime = stat.st_birthtime
|
|
else:
|
|
# We're probably on Linux. No easy way to get creation dates here,
|
|
# so we'll settle for when its content was last modified.
|
|
ctime = stat.st_mtime
|
|
parameters[DFA_CREATED_TIME] = formatLocalSecondsTimestamp(ctime)
|
|
except IOError as e:
|
|
Cmd.Backup()
|
|
usageErrorExit(f'{parameters[DFA_LOCALFILEPATH]}: {str(e)}')
|
|
parameters[DFA_LOCALFILENAME] = os.path.basename(parameters[DFA_LOCALFILEPATH])
|
|
if not updateCmd:
|
|
body.setdefault('name', parameters[DFA_LOCALFILENAME])
|
|
body['mimeType'] = mimetypes.guess_type(parameters[DFA_LOCALFILEPATH])[0]
|
|
if body['mimeType'] is None:
|
|
body['mimeType'] = 'application/octet-stream'
|
|
else:
|
|
parameters[DFA_LOCALFILENAME] = '-'
|
|
if body.get('mimeType') is None:
|
|
body['mimeType'] = 'application/octet-stream'
|
|
parameters[DFA_LOCALMIMETYPE] = body['mimeType']
|
|
elif myarg == 'url':
|
|
parameters[DFA_LOCALFILEPATH] = None
|
|
parameters[DFA_URL] = getString(Cmd.OB_URL)
|
|
elif myarg =='stripnameprefix':
|
|
parameters[DFA_STRIPNAMEPREFIX] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'replacefilename':
|
|
parameters[DFA_REPLACEFILENAME].append(getREPatternSubstitution(re.IGNORECASE))
|
|
elif myarg in {'convert', 'ocr'}:
|
|
deprecatedArgument(myarg)
|
|
stderrWarningMsg(Msg.USE_MIMETYPE_TO_SPECIFY_GOOGLE_FORMAT)
|
|
elif myarg in DRIVE_LABEL_CHOICE_MAP:
|
|
myarg = DRIVE_LABEL_CHOICE_MAP[myarg]
|
|
body[myarg] = getBoolean()
|
|
elif not updateCmd and myarg in {'createddate', 'createdtime'}:
|
|
body['createdTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg == 'preservefiletimes':
|
|
parameters[DFA_PRESERVE_FILE_TIMES] = getBoolean()
|
|
elif myarg == 'shortcut':
|
|
body['mimeType'] = MIMETYPE_GA_SHORTCUT
|
|
body['shortcutDetails'] = {'targetId': getString(Cmd.OB_DRIVE_FOLDER_ID)}
|
|
elif getDriveFileParentAttribute(myarg, parameters):
|
|
pass
|
|
elif myarg == 'foldercolorrgb':
|
|
body['folderColorRgb'] = getColor()
|
|
elif myarg == 'usecontentasindexabletext':
|
|
parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT] = getBoolean()
|
|
elif myarg == 'indexabletext':
|
|
body.setdefault('contentHints', {})
|
|
body['contentHints']['indexableText'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'securityupdate':
|
|
body['linkShareMetadata'] = {'securityUpdateEnabled': getBoolean(), 'securityUpdateEligible': True}
|
|
elif myarg == 'timestamp':
|
|
parameters[DFA_TIMESTAMP] = getBoolean()
|
|
elif myarg == 'timeformat':
|
|
parameters[DFA_TIMEFORMAT] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif getDriveFileCopyAttribute(myarg, body, parameters):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
def setPreservedFileTimes(body, parameters, updateCmd):
|
|
body['modifiedTime'] = parameters[DFA_MODIFIED_TIME]
|
|
if not updateCmd:
|
|
body['createdTime'] = parameters[DFA_CREATED_TIME]
|
|
|
|
def getMediaBody(parameters):
|
|
if parameters[DFA_URL]:
|
|
try:
|
|
status, c = getHttpObj(timeout=10).request(parameters[DFA_URL])
|
|
if status['status'] != '200':
|
|
entityActionFailedExit([Ent.URL, parameters[DFA_URL]], Msg.URL_ERROR.format(status['status']))
|
|
parameters[DFA_LOCALMIMETYPE] = status['content-type']
|
|
return googleapiclient.http.MediaIoBaseUpload(io.BytesIO(c), mimetype=status['content-type'], resumable=True)
|
|
except (IOError, httplib2.error.ServerNotFoundError) as e:
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(parameters[DFA_URL], str(e), entityType=Ent.URL))
|
|
else:
|
|
try:
|
|
if parameters[DFA_LOCALFILEPATH] != '-':
|
|
media_body = googleapiclient.http.MediaFileUpload(parameters[DFA_LOCALFILEPATH], mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True)
|
|
else:
|
|
media_body = googleapiclient.http.MediaIoBaseUpload(io.BytesIO(sys.stdin.buffer.read()), mimetype=parameters[DFA_LOCALMIMETYPE], resumable=True)
|
|
if media_body.size() == 0:
|
|
media_body = None
|
|
return media_body
|
|
except IOError as e:
|
|
systemErrorExit(FILE_ERROR_RC, fileErrorMessage(parameters[DFA_LOCALFILEPATH], str(e)))
|
|
|
|
DRIVE_ACTIVITY_ACTION_MAP = {
|
|
'comment': 'COMMENT',
|
|
'create': 'CREATE',
|
|
'delete': 'DELETE',
|
|
'dlpchange': 'DLP_CHANGE',
|
|
'edit': 'EDIT',
|
|
'emptytrash': 'DELETE',
|
|
'move': 'MOVE',
|
|
'permissionchange': 'PERMISSION_CHANGE',
|
|
'reference': 'REFERENCE',
|
|
'rename': 'RENAME',
|
|
'restore': 'RESTORE',
|
|
'settingschange': 'SETTINGS_CHANGE',
|
|
'trash': 'DELETE',
|
|
'untrash': 'RESTORE',
|
|
'upload': 'CREATE',
|
|
}
|
|
|
|
CONSOLIDATION_GROUPING_STRATEGY_CHOICE_MAP = {'driveui': 'legacy', 'legacy': 'legacy', 'none': 'none'}
|
|
|
|
# gam <UserTypeEntity> print driveactivity [todrive <ToDriveAttribute>*]
|
|
# [(fileid <DriveFileID>) | (folderid <DriveFolderID>) |
|
|
# (drivefilename <DriveFileName>) | (drivefoldername <DriveFolderName>) | (query <QueryDriveFile>)]
|
|
# [([start <Date>|<Time>] [end <Date>|<Time>])|(range <Date>|<Time> <Date>|<Time>)|
|
|
# yesterday|today|thismonth|(previousmonths <Integer>)]
|
|
# [action|actions [not] <DriveActivityActionList>]
|
|
# [consolidationstrategy legacy|none]
|
|
# [idmapfile <CSVFileInput> endcsv]
|
|
# [stripcrsfromname] [formatjson [quotechar <Character>]]
|
|
def printDriveActivity(users):
|
|
def _getUserInfo(userId):
|
|
if userId.startswith('people/'):
|
|
userId = userId[7:]
|
|
entry = userInfo.get(userId)
|
|
if entry is None:
|
|
try:
|
|
result = callGAPI(cd.users(), 'get',
|
|
throwReasons=GAPI.USER_GET_THROW_REASONS+[GAPI.INVALID_INPUT],
|
|
userKey=userId, fields='primaryEmail,name.fullName')
|
|
entry = (result['primaryEmail'], result['name']['fullName'])
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden,
|
|
GAPI.badRequest, GAPI.backendError, GAPI.systemError, GAPI.invalidInput):
|
|
entry = (f'uid:{userId}', UNKNOWN)
|
|
userInfo[userId] = entry
|
|
return entry
|
|
|
|
def _updateKnownUsers(structure):
|
|
if isinstance(structure, list):
|
|
for v in structure:
|
|
if isinstance(v, (dict, list)):
|
|
_updateKnownUsers(v)
|
|
elif isinstance(structure, dict):
|
|
for k, v in sorted(iter(structure.items())):
|
|
if k != 'knownUser':
|
|
if isinstance(v, (dict, list)):
|
|
_updateKnownUsers(v)
|
|
else:
|
|
entry = _getUserInfo(v['personName'])
|
|
v['emailAddress'] = entry[0]
|
|
v['personName'] = entry[1]
|
|
break
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
startEndTime = StartEndTime()
|
|
baseFileList = []
|
|
query = ''
|
|
activityFilter = ''
|
|
actions = set()
|
|
strategy = 'none'
|
|
negativeAction = stripCRsFromName = False
|
|
checkArgumentPresent(['v2'])
|
|
csvPF = CSVPrintFile([f'user{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name',
|
|
f'user{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}emailAddress',
|
|
f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}id',
|
|
f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name',
|
|
f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}mimeType',
|
|
'eventTime'],
|
|
'sortall')
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
userInfo = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'fileid':
|
|
baseFileList.append({'id': getString(Cmd.OB_DRIVE_FILE_ID), 'mimeType': MIMETYPE_GA_DOCUMENT})
|
|
elif myarg == 'folderid':
|
|
baseFileList.append({'id': getString(Cmd.OB_DRIVE_FOLDER_ID), 'mimeType': MIMETYPE_GA_FOLDER})
|
|
elif myarg == 'drivefilename':
|
|
query = f"mimeType != '{MIMETYPE_GA_FOLDER}' and name = '{getEscapedDriveFileName()}'"
|
|
elif myarg == 'drivefoldername':
|
|
query = f"mimeType = '{MIMETYPE_GA_FOLDER}' and name = '{getEscapedDriveFileName()}'"
|
|
elif myarg == 'query':
|
|
query = _mapDrive2QueryToDrive3(getString(Cmd.OB_QUERY))
|
|
elif myarg in {'start', 'starttime', 'end', 'endtime', 'yesterday', 'today', 'range', 'thismonth', 'previousmonths'}:
|
|
startEndTime.Get(myarg)
|
|
elif myarg in {'action', 'actions'}:
|
|
negativeAction = checkArgumentPresent('not')
|
|
for action in _getFieldsList():
|
|
if action in DRIVE_ACTIVITY_ACTION_MAP:
|
|
mappedAction = DRIVE_ACTIVITY_ACTION_MAP[action]
|
|
if mappedAction:
|
|
actions.add(mappedAction)
|
|
else:
|
|
invalidChoiceExit(action, DRIVE_ACTIVITY_ACTION_MAP, True)
|
|
elif myarg in {'allevents', 'combinedevents', 'singleevents'}:
|
|
pass
|
|
elif myarg in {'consolidationstrategy', 'groupingstrategy'}:
|
|
strategy = getChoice(CONSOLIDATION_GROUPING_STRATEGY_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'idmapfile':
|
|
f, csvFile, _ = openCSVFileReader(getString(Cmd.OB_FILE_NAME))
|
|
for row in csvFile:
|
|
userInfo[row['id']] = (row['primaryEmail'], row.get('name.fullName', UNKNOWN))
|
|
closeFile(f)
|
|
elif myarg == 'stripcrsfromname':
|
|
stripCRsFromName = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if not baseFileList and not query:
|
|
baseFileList = [{'id': ROOT, 'mimeType': MIMETYPE_GA_FOLDER}]
|
|
if startEndTime.startTime:
|
|
if activityFilter:
|
|
activityFilter += ' AND '
|
|
activityFilter += f'time >= "{startEndTime.startTime}"'
|
|
if startEndTime.endTime:
|
|
if activityFilter:
|
|
activityFilter += ' AND '
|
|
activityFilter += f'time <= "{startEndTime.endTime}"'
|
|
if actions:
|
|
if activityFilter:
|
|
activityFilter += ' AND '
|
|
activityFilter += f'{"-" if negativeAction else ""}detail.action_detail_case:({" ".join(actions)})'
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, activity = buildGAPIServiceObject(API.DRIVEACTIVITY, user, i, count)
|
|
if not activity:
|
|
continue
|
|
_, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
fileList = baseFileList[:]
|
|
if query:
|
|
if GC.Values[GC.SHOW_GETTINGS]:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count, query=query)
|
|
try:
|
|
fileList.extend(callGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=query, fields='nextPageToken,files(id,mimeType)', pageSize=GC.Values[GC.DRIVE_MAX_RESULTS]))
|
|
if not fileList:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], emptyQuery(query, Ent.DRIVE_FILE_OR_FOLDER), i, count)
|
|
continue
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(query), i, count)
|
|
break
|
|
except GAPI.fileNotFound:
|
|
printGotEntityItemsForWhom(0)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
for f_file in fileList:
|
|
fileId = f_file['id']
|
|
entityType = Ent.DRIVE_FOLDER_ID if f_file['mimeType'] == MIMETYPE_GA_FOLDER else Ent.DRIVE_FILE_ID
|
|
if entityType == Ent.DRIVE_FILE_ID:
|
|
drive_key = 'itemName'
|
|
else:
|
|
drive_key = 'ancestorName'
|
|
qualifier = f' for {Ent.Singular(entityType)}: {fileId}'
|
|
printGettingAllEntityItemsForWhom(Ent.ACTIVITY, user, i, count, qualifier=qualifier)
|
|
pageMessage = getPageMessageForWhom()
|
|
kwargs = {
|
|
'consolidationStrategy': {strategy: {}},
|
|
'pageSize': GC.Values[GC.ACTIVITY_MAX_RESULTS],
|
|
'pageToken': None,
|
|
drive_key: f'items/{fileId}',
|
|
'filter': activityFilter}
|
|
try:
|
|
feed = yieldGAPIpages(activity.activity(), 'query', 'activities',
|
|
pageMessage=pageMessage,
|
|
throwReasons=GAPI.ACTIVITY_THROW_REASONS,
|
|
pageArgsInBody=True,
|
|
fields='nextPageToken,activities', body=kwargs)
|
|
for activities in feed:
|
|
for activityEvent in activities:
|
|
eventRow = {}
|
|
actors = activityEvent.get('actors', [])
|
|
if actors:
|
|
userId = actors[0].get('user', {}).get('knownUser', {}).get('personName', '')
|
|
if not userId:
|
|
userId = actors[0].get('impersonation', {}).get('impersonatedUser', {}).get('knownUser', {}).get('personName', '')
|
|
if userId:
|
|
entry = _getUserInfo(userId)
|
|
eventRow[f'user{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name'] = entry[1]
|
|
eventRow[f'user{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}emailAddress'] = entry[0]
|
|
targets = activityEvent.get('targets', [])
|
|
if targets:
|
|
driveItem = targets[0].get('driveItem')
|
|
if driveItem:
|
|
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}id'] = driveItem['name'][6:]
|
|
if stripCRsFromName:
|
|
driveItem['title'] = _stripControlCharsFromName(driveItem['title'])
|
|
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name'] = driveItem['title']
|
|
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}mimeType'] = driveItem['mimeType']
|
|
else:
|
|
sharedDrive = targets[0].get('teamDrive')
|
|
if sharedDrive:
|
|
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}id'] = sharedDrive['name'][11:]
|
|
if stripCRsFromName:
|
|
sharedDrive['title'] = _stripControlCharsFromName(sharedDrive['title'])
|
|
eventRow[f'target{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name'] = sharedDrive['title']
|
|
if 'timestamp' in activityEvent:
|
|
eventRow['eventTime'] = formatLocalTime(activityEvent['timestamp'])
|
|
elif 'timeRange' in activityEvent:
|
|
timeRange = activityEvent['timeRange']
|
|
eventRow['eventTime'] = f'{formatLocalTime(timeRange["startTime"])}-{formatLocalTime(timeRange["endTime"])}'
|
|
_updateKnownUsers(activityEvent)
|
|
if not FJQC.formatJSON:
|
|
activityEvent.pop('timestamp', None)
|
|
activityEvent.pop('timeRange', None)
|
|
flattenJSON(activityEvent, flattened=eventRow)
|
|
csvPF.WriteRowTitles(eventRow)
|
|
else:
|
|
checkRow = eventRow.copy()
|
|
flattenJSON(activityEvent, flattened=checkRow)
|
|
if csvPF.CheckRowTitles(checkRow):
|
|
eventRow['JSON'] = json.dumps(cleanJSON(activityEvent), ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(eventRow)
|
|
except GAPI.badRequest as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, fileId], str(e), i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
csvPF.writeCSVfile('Drive Activity')
|
|
|
|
DRIVESETTINGS_FIELDS_CHOICE_MAP = {
|
|
'appinstalled': 'appInstalled',
|
|
'exportformats': 'exportFormats',
|
|
'foldercolorpalette': 'folderColorPalette',
|
|
'importformats': 'importFormats',
|
|
'largestchangeid': 'largestChangeId',
|
|
'limit': 'limit',
|
|
'maximportsizes': 'maxImportSizes',
|
|
'maxuploadsize': 'maxUploadSize',
|
|
'name': 'name',
|
|
'permissionid': 'permissionId',
|
|
'rootfolderid': 'rootFolderId',
|
|
'drivethemes': 'driveThemes',
|
|
'shareddrivethemes': 'driveThemes',
|
|
'teamdrivethemes': 'driveThemes',
|
|
'usage': 'usage',
|
|
'usageindrive': 'usageInDrive',
|
|
'usageindrivetrash': 'usageInDriveTrash',
|
|
}
|
|
|
|
DRIVESETTINGS_SCALAR_FIELDS = [
|
|
'name',
|
|
'appInstalled',
|
|
'largestChangeId',
|
|
'limit',
|
|
'maxUploadSize',
|
|
'permissionId',
|
|
'rootFolderId',
|
|
'usage',
|
|
'usageInDrive',
|
|
'usageInDriveTrash',
|
|
]
|
|
|
|
DRIVESETTINGS_USAGE_BYTES_FIELDS = {
|
|
'usage': 'usageBytes',
|
|
'usageInDrive': 'usageInDriveBytes',
|
|
'usageInDriveTrash': 'usageInDriveTrashBytes',
|
|
}
|
|
|
|
def _showSharedDriveThemeSettings(themes):
|
|
Ind.Increment()
|
|
for theme in themes:
|
|
printKeyValueList(['id', theme['id']])
|
|
Ind.Increment()
|
|
printKeyValueList(['backgroundImageLink', theme['backgroundImageLink']])
|
|
printKeyValueList(['colorRgb', theme['colorRgb']])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print drivesettings [todrive <ToDriveAttribute>*]
|
|
# [allfields|<DriveSettingsFieldName>*|(fields <DriveSettingsFieldNameList>)]
|
|
# [delimiter <Character>] [showusagebytes]
|
|
# gam <UserTypeEntity> show drivesettings
|
|
# [allfields|<DriveSettingsFieldName>*|(fields <DriveSettingsFieldNameList>)]
|
|
# [delimiter <Character>] [showusagebytes]
|
|
def printShowDriveSettings(users):
|
|
def _showFormats(title):
|
|
if title in fieldsList and title in feed:
|
|
printKeyValueList([title, None])
|
|
Ind.Increment()
|
|
for item, value in sorted(iter(feed[title].items())):
|
|
printKeyValueList([item, delimiter.join(value)])
|
|
Ind.Decrement()
|
|
|
|
def _showSetting(title):
|
|
if title in fieldsList and title in feed:
|
|
if not isinstance(feed[title], list):
|
|
printKeyValueList([title, feed[title]])
|
|
else:
|
|
printKeyValueList([title, delimiter.join(feed[title])])
|
|
|
|
def _addFormats(row, title):
|
|
if title in fieldsList and title in feed:
|
|
jcount = len(feed[title])
|
|
row[title] = jcount
|
|
j = 0
|
|
for item, value in sorted(iter(feed[title].items())):
|
|
row[f'{title}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j:02d}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{item}'] = delimiter.join(value)
|
|
j += 1
|
|
|
|
def _addSetting(row, title):
|
|
if title in fieldsList and title in feed:
|
|
if not isinstance(feed[title], list):
|
|
row[title] = feed[title]
|
|
else:
|
|
row[title] = delimiter.join(feed[title])
|
|
|
|
csvPF = CSVPrintFile(['email'], ['email']+DRIVESETTINGS_SCALAR_FIELDS) if Act.csvFormat() else None
|
|
fieldsList = []
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
showUsageBytes = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg == 'allfields':
|
|
fieldsList.extend(DRIVESETTINGS_FIELDS_CHOICE_MAP.values())
|
|
elif getFieldsList(myarg, DRIVESETTINGS_FIELDS_CHOICE_MAP, fieldsList):
|
|
pass
|
|
elif myarg == 'showusagebytes':
|
|
showUsageBytes = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not fieldsList:
|
|
fieldsList = DRIVESETTINGS_SCALAR_FIELDS[:]
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.DRIVE_SETTINGS, user, i, count)
|
|
try:
|
|
feed = callGAPI(drive.about(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='*')
|
|
feed['name'] = feed['user']['displayName']
|
|
feed['maxUploadSize'] = formatFileSize(int(feed['maxUploadSize']))
|
|
feed['permissionId'] = feed['user']['permissionId']
|
|
if 'limit' in feed['storageQuota']:
|
|
feed['limit'] = formatFileSize(int(feed['storageQuota']['limit']))
|
|
else:
|
|
feed['limit'] = 'UNLIMITED'
|
|
for setting in ['usage', 'usageInDrive', 'usageInDriveTrash']:
|
|
uval = int(feed['storageQuota'].get(setting, '0'))
|
|
feed[setting] = formatFileSize(uval)
|
|
if showUsageBytes:
|
|
feed[DRIVESETTINGS_USAGE_BYTES_FIELDS[setting]] = uval
|
|
if 'rootFolderId' in fieldsList:
|
|
feed['rootFolderId'] = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=ROOT, fields='id')['id']
|
|
if 'largestChangeId' in fieldsList:
|
|
feed['largestChangeId'] = callGAPI(drive.changes(), 'getStartPageToken',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='startPageToken')['startPageToken']
|
|
if not csvPF:
|
|
entityPerformActionNumItems([Ent.USER, user], 1, Ent.DRIVE_SETTINGS, i, count)
|
|
Ind.Increment()
|
|
for setting in DRIVESETTINGS_SCALAR_FIELDS:
|
|
_showSetting(setting)
|
|
if showUsageBytes:
|
|
for title, setting in DRIVESETTINGS_USAGE_BYTES_FIELDS.items():
|
|
if title in fieldsList and setting in feed:
|
|
printKeyValueList([setting, feed[setting]])
|
|
_showSetting('folderColorPalette')
|
|
_showFormats('exportFormats')
|
|
_showFormats('importFormats')
|
|
if 'maxImportSizes' in fieldsList and 'maxImportSizes' in fieldsList:
|
|
printKeyValueList(['maxImportSizes', None])
|
|
Ind.Increment()
|
|
for setting, value in iter(feed['maxImportSizes'].items()):
|
|
printKeyValueList([setting, formatFileSize(int(value))])
|
|
Ind.Decrement()
|
|
if 'driveThemes' in fieldsList and 'driveThemes' in feed:
|
|
printKeyValueList(['driveThemes', None])
|
|
_showSharedDriveThemeSettings(feed['driveThemes'])
|
|
Ind.Decrement()
|
|
else:
|
|
row = {'email': user}
|
|
for setting in DRIVESETTINGS_SCALAR_FIELDS:
|
|
_addSetting(row, setting)
|
|
if showUsageBytes:
|
|
for title, setting in DRIVESETTINGS_USAGE_BYTES_FIELDS.items():
|
|
if title in fieldsList and setting in feed:
|
|
row[setting] = feed[setting]
|
|
_addSetting(row, 'folderColorPalette')
|
|
_addFormats(row, 'exportFormats')
|
|
_addFormats(row, 'importFormats')
|
|
if 'maxImportSizes' in fieldsList and 'maxImportSizes' in fieldsList:
|
|
jcount = len(feed['maxImportSizes'])
|
|
row['maxImportSizes'] = jcount
|
|
j = 0
|
|
for setting, value in iter(feed['maxImportSizes'].items()):
|
|
row[f'maxImportSizes{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{setting}'] = formatFileSize(int(value))
|
|
j += 1
|
|
if 'driveThemes' in fieldsList and 'driveThemes' in feed:
|
|
jcount = len(feed['driveThemes'])
|
|
row['driveThemes'] = jcount
|
|
j = 0
|
|
for setting in feed['driveThemes']:
|
|
row[f'driveThemes{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j:02d}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}id'] = setting['id']
|
|
row[f'driveThemes{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j:02d}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}backgroundImageLink'] = setting['backgroundImageLink']
|
|
row[f'driveThemes{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j:02d}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}colorRgb'] = setting['colorRgb']
|
|
j += 1
|
|
csvPF.WriteRowTitles(row)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('User Drive Settings')
|
|
|
|
# gam <UserTypeEntity> show shareddrivethemes
|
|
def showSharedDriveThemes(users):
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
try:
|
|
themes = callGAPIitems(drive.about(), 'get', 'driveThemes',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='driveThemes')
|
|
jcount = len(themes)
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.SHAREDDRIVE_THEME, i, count)
|
|
_showSharedDriveThemeSettings(themes)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
|
|
def doShowSharedDriveThemes():
|
|
showSharedDriveThemes([_getAdminEmail()])
|
|
|
|
def initFilePathInfo(delimiter):
|
|
return {'ids': {}, 'allPaths': {}, 'localPaths': None, 'delimiter': delimiter}
|
|
|
|
def getFilePaths(drive, fileTree, initialResult, filePathInfo, addParentsToTree=False,
|
|
fullpath=False, showDepth=False, folderPathOnly=False):
|
|
def _getParentName(result):
|
|
if (result['mimeType'] == MIMETYPE_GA_FOLDER) and result.get('driveId') and (result['name'] == TEAM_DRIVE):
|
|
parentName = _getSharedDriveNameFromId(result['driveId'])
|
|
if parentName != TEAM_DRIVE:
|
|
return f'{SHARED_DRIVES}{filePathInfo["delimiter"]}{parentName}'
|
|
return result['name']
|
|
|
|
def _followParent(paths, parentId):
|
|
result = None
|
|
paths.setdefault(parentId, {})
|
|
if fileTree:
|
|
parentEntry = fileTree.get(parentId)
|
|
if not parentEntry:
|
|
if not addParentsToTree:
|
|
return
|
|
parentEntry = fileTree[parentId] = {'info': {'id': parentId, 'name': parentId, 'mimeType': MIMETYPE_GA_FOLDER}, 'children': []}
|
|
if parentEntry['info']['name'] == parentEntry['info']['id'] and parentEntry['info']['id'] not in {ORPHANS, SHARED_WITHME, SHARED_DRIVES}:
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=parentId, fields='name,parents,mimeType,driveId', supportsAllDrives=True)
|
|
parentEntry['info']['name'] = _getParentName(result)
|
|
parentEntry['info']['parents'] = result.get('parents', [])
|
|
except (GAPI.fileNotFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
pass
|
|
filePathInfo['ids'][parentId] = parentEntry['info']['name']
|
|
parents = parentEntry['info'].get('parents', [])
|
|
else:
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=parentId, fields='name,parents,mimeType,driveId', supportsAllDrives=True)
|
|
filePathInfo['ids'][parentId] = _getParentName(result)
|
|
parents = result.get('parents', [])
|
|
except (GAPI.fileNotFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
return
|
|
for lparentId in parents:
|
|
if lparentId not in filePathInfo['allPaths']:
|
|
_followParent(paths[parentId], lparentId)
|
|
filePathInfo['allPaths'][lparentId] = paths[parentId][lparentId]
|
|
else:
|
|
paths[parentId][lparentId] = filePathInfo['allPaths'][lparentId]
|
|
|
|
def _makeFilePaths(localPaths, fplist, filePaths, name, maxDepth):
|
|
for k, v in iter(localPaths.items()):
|
|
fplist.append(filePathInfo['ids'].get(k, ''))
|
|
if not v:
|
|
fp = fplist[:]
|
|
if showDepth:
|
|
depth = len(fp)
|
|
if depth > maxDepth:
|
|
maxDepth = depth-1
|
|
fp.reverse()
|
|
if initialMimeType == MIMETYPE_GA_FOLDER or not folderPathOnly:
|
|
fp.append(name)
|
|
filePaths.append(filePathInfo['delimiter'].join(fp))
|
|
else:
|
|
maxDepth = _makeFilePaths(v, fplist, filePaths, name, maxDepth)
|
|
fplist.pop()
|
|
return maxDepth
|
|
|
|
filePaths = []
|
|
parents = initialResult.get('parents', [])
|
|
initialMimeType = initialResult['mimeType']
|
|
if parents:
|
|
filePathInfo['localPaths'] = {}
|
|
for parentId in parents:
|
|
if parentId not in filePathInfo['allPaths']:
|
|
_followParent(filePathInfo['allPaths'], parentId)
|
|
filePathInfo['localPaths'][parentId] = filePathInfo['allPaths'][parentId]
|
|
fplist = []
|
|
maxDepth = _makeFilePaths(filePathInfo['localPaths'], fplist, filePaths, initialResult['name'], -1)
|
|
else:
|
|
if (fullpath and initialMimeType == MIMETYPE_GA_FOLDER and
|
|
((initialResult['name'] == MY_DRIVE) or
|
|
(initialResult.get('driveId') and initialResult['name'].startswith(SHARED_DRIVES)))):
|
|
filePaths.append(initialResult['name'])
|
|
maxDepth = 0
|
|
return (_getEntityMimeType(initialResult), filePaths, maxDepth)
|
|
|
|
DRIVEFILE_ORDERBY_CHOICE_MAP = {
|
|
'createddate': 'createdTime',
|
|
'createdtime': 'createdTime',
|
|
'folder': 'folder',
|
|
'lastviewedbyme': 'viewedByMeTime',
|
|
'lastviewedbymedate': 'viewedByMeTime',
|
|
'lastviewedbymetime': 'viewedByMeTime',
|
|
'lastviewedbyuser': 'viewedByMeTime',
|
|
'modifiedbyme': 'modifiedByMeTime',
|
|
'modifiedbymedate': 'modifiedByMeTime',
|
|
'modifiedbymetime': 'modifiedByMeTime',
|
|
'modifiedbyuser': 'modifiedByMeTime',
|
|
'modifieddate': 'modifiedTime',
|
|
'modifiedtime': 'modifiedTime',
|
|
'name': 'name',
|
|
'namenatural': 'name_natural',
|
|
'quotabytesused': 'quotaBytesUsed',
|
|
'quotaused': 'quotaBytesUsed',
|
|
'recency': 'recency',
|
|
'sharedwithmedate': 'sharedWithMeTime',
|
|
'sharedwithmetime': 'sharedWithMeTime',
|
|
'starred': 'starred',
|
|
'title': 'name',
|
|
'titlenatural': 'name_natural',
|
|
'viewedbymedate': 'viewedByMeTime',
|
|
'viewedbymetime': 'viewedByMeTime',
|
|
}
|
|
|
|
def _mapDrive3TitlesToDrive2(titles, drive3TitlesMap):
|
|
for i, title in enumerate(titles):
|
|
if title in drive3TitlesMap:
|
|
titles[i] = drive3TitlesMap[title]
|
|
|
|
def _mapDriveUser(field):
|
|
if 'me' in field:
|
|
field['isAuthenticatedUser'] = field.pop('me')
|
|
if 'photoLink' in field:
|
|
field['picture'] = {'url': field.pop('photoLink')}
|
|
|
|
def _mapDrivePermissionNames(permission):
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
if 'displayName' in permission:
|
|
permission['name'] = permission.pop('displayName')
|
|
if 'expirationTime' in permission:
|
|
permission['expirationDate'] = formatLocalTime(permission.pop('expirationTime'))
|
|
if 'allowFileDiscovery' in permission:
|
|
permission['withLink'] = not permission.pop('allowFileDiscovery')
|
|
emailAddress = permission.get('emailAddress')
|
|
if emailAddress:
|
|
_, permission['domain'] = splitEmailAddress(emailAddress)
|
|
|
|
def _mapDriveParents(f_file, parentsSubFields):
|
|
if 'parents' in f_file:
|
|
parents = f_file.pop('parents')
|
|
if len(parents) == 1 and parents[0] == ORPHANS:
|
|
return
|
|
f_file['parents'] = []
|
|
for parentId in parents:
|
|
parent = {}
|
|
if parentsSubFields['id']:
|
|
parent['id'] = parentId
|
|
if parentsSubFields['isRoot']:
|
|
parent['isRoot'] = parentId == parentsSubFields['rootFolderId']
|
|
f_file['parents'].append(parent)
|
|
|
|
def _mapDriveProperties(f_file):
|
|
appProperties = f_file.pop('appProperties', [])
|
|
properties = f_file.pop('properties', [])
|
|
if appProperties:
|
|
f_file.setdefault('properties', [])
|
|
for key, value in sorted(iter(appProperties.items())):
|
|
f_file['properties'].append({'key': key, 'value': value, 'visibility': 'PRIVATE'})
|
|
if properties:
|
|
f_file.setdefault('properties', [])
|
|
for key, value in sorted(iter(properties.items())):
|
|
f_file['properties'].append({'key': key, 'value': value, 'visibility': 'PUBLIC'})
|
|
|
|
def _mapDriveFieldNames(f_file, user, parentsSubFields, mapToLabels):
|
|
if mapToLabels:
|
|
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_LABELS_MAP.items()):
|
|
if attrib in f_file:
|
|
f_file.setdefault('labels', {})
|
|
f_file['labels'][v2attrib] = f_file.pop(attrib)
|
|
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP.items()):
|
|
if attrib in f_file:
|
|
f_file[v2attrib] = f_file.pop(attrib)
|
|
capabilities = f_file.get('capabilities')
|
|
if capabilities:
|
|
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_CAPABILITIES_FIELDS_MAP.items()):
|
|
if attrib in capabilities:
|
|
f_file[v2attrib] = capabilities[attrib]
|
|
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_CAPABILITIES_NAMES_MAP.items()):
|
|
if attrib in capabilities:
|
|
capabilities[v2attrib] = capabilities.pop(attrib)
|
|
if 'spaces' in f_file:
|
|
f_file['appDataContents'] = 'appDataFolder' in f_file['spaces']
|
|
if 'lastModifyingUser' in f_file:
|
|
if 'displayName' in f_file['lastModifyingUser']:
|
|
f_file['lastModifyingUserName'] = f_file['lastModifyingUser']['displayName']
|
|
_mapDriveUser(f_file['lastModifyingUser'])
|
|
if 'owners' in f_file:
|
|
for owner in f_file['owners']:
|
|
_mapDriveUser(owner)
|
|
if 'displayName' in owner:
|
|
f_file.setdefault('ownerNames', [])
|
|
f_file['ownerNames'].append(owner['displayName'])
|
|
_mapDriveUser(f_file.get('sharingUser', {}))
|
|
_mapDriveParents(f_file, parentsSubFields)
|
|
_mapDriveProperties(f_file)
|
|
for permission in f_file.get('permissions', []):
|
|
if (permission.get('type') == 'user') and (permission.get('emailAddress', '').lower() == user) and ('role' in permission):
|
|
f_file['userPermission'] = {'id': 'me', 'role': permission['role'], 'type': permission['type']}
|
|
_mapDrivePermissionNames(permission)
|
|
|
|
def _mapDriveRevisionNames(revision):
|
|
for attrib, v2attrib in iter(API.DRIVE3_TO_DRIVE2_REVISIONS_FIELDS_MAP.items()):
|
|
if attrib in revision:
|
|
revision[v2attrib] = revision.pop(attrib)
|
|
if 'lastModifyingUser' in revision:
|
|
if 'displayName' in revision['lastModifyingUser']:
|
|
revision['lastModifyingUserName'] = revision['lastModifyingUser']['displayName']
|
|
_mapDriveUser(revision['lastModifyingUser'])
|
|
|
|
DRIVEFILE_BASIC_PERMISSION_FIELDS = [
|
|
'displayName', 'id', 'emailAddress', 'domain', 'role', 'type',
|
|
'allowFileDiscovery', 'expirationTime', 'deleted', 'permissionDetails' #permissionDetails must be last
|
|
]
|
|
|
|
DRIVE_FIELDS_CHOICE_MAP = {
|
|
'alternatelink': 'webViewLink',
|
|
'appdatacontents': 'spaces',
|
|
'appproperties': 'appProperties',
|
|
'basicpermissions': ['permissions.displayName', 'permissions.id', 'permissions.emailAddress', 'permissions.domain',
|
|
'permissions.role', 'permissions.type', 'permissions.allowFileDiscovery',
|
|
'permissions.expirationTime', 'permissions.deleted'],
|
|
'cancomment': 'capabilities.canComment',
|
|
'canreadrevisions': 'capabilities.canReadRevisions',
|
|
'capabilities': 'capabilities',
|
|
'contenthints': 'contentHints',
|
|
'contentrestrictions': 'contentRestrictions',
|
|
'copyable': 'capabilities.canCopy',
|
|
'copyrequireswriterpermission': 'copyRequiresWriterPermission',
|
|
'createddate': 'createdTime',
|
|
'createdtime': 'createdTime',
|
|
'description': 'description',
|
|
'driveid': 'driveId',
|
|
'drivename': 'driveId',
|
|
'editable': 'capabilities.canEdit',
|
|
'explicitlytrashed': 'explicitlyTrashed',
|
|
'exportlinks': 'exportLinks',
|
|
'fileextension': 'fileExtension',
|
|
'filesize': 'size',
|
|
'foldercolorrgb': 'folderColorRgb',
|
|
'fullfileextension': 'fullFileExtension',
|
|
'hasaugmentedpermissions': 'hasAugmentedPermissions',
|
|
'hasthumbnail': 'hasThumbnail',
|
|
'headrevisionid': 'headRevisionId',
|
|
'iconlink': 'iconLink',
|
|
'id': 'id',
|
|
'imagemediametadata': 'imageMediaMetadata',
|
|
'inheritedpermissionsdisabled': 'inheritedPermissionsDisabled',
|
|
'isappauthorized': 'isAppAuthorized',
|
|
'labelinfo': 'labelInfo',
|
|
'labels': ['modifiedByMe', 'copyRequiresWriterPermission', 'starred', 'trashed', 'viewedByMe'],
|
|
'lastmodifyinguser': 'lastModifyingUser',
|
|
'lastmodifyingusername': 'lastModifyingUser.displayName',
|
|
'lastviewedbyme': 'viewedByMeTime',
|
|
'lastviewedbymedate': 'viewedByMeTime',
|
|
'lastviewedbymetime': 'viewedByMeTime',
|
|
'lastviewedbyuser': 'viewedByMeTime',
|
|
'linksharemetadata': 'linkShareMetadata',
|
|
'md5': 'md5Checksum',
|
|
'md5checksum': 'md5Checksum',
|
|
'md5sum': 'md5Checksum',
|
|
'mime': 'mimeType',
|
|
'mimetype': 'mimeType',
|
|
'modifiedbymedate': 'modifiedByMeTime',
|
|
'modifiedbymetime': 'modifiedByMeTime',
|
|
'modifiedbyuser': 'modifiedByMeTime',
|
|
'modifieddate': 'modifiedTime',
|
|
'modifiedtime': 'modifiedTime',
|
|
'name': 'name',
|
|
'originalfilename': 'originalFilename',
|
|
'ownedbyme': 'ownedByMe',
|
|
'ownernames': 'owners.displayName',
|
|
'owners': 'owners',
|
|
'parents': 'parents',
|
|
'permissiondetails': 'permissions.permissionDetails',
|
|
'permissionids': 'permissionIds',
|
|
'permissions': 'permissions',
|
|
'properties': 'properties',
|
|
'quotabytesused': 'quotaBytesUsed',
|
|
'quotaused': 'quotaBytesUsed',
|
|
'resourcekey': 'resourceKey',
|
|
'shareable': 'capabilities.canShare',
|
|
'shared': 'shared',
|
|
'shareddriveid': 'driveId',
|
|
'shareddrivename': 'driveId',
|
|
'sharedwithmedate': 'sharedWithMeTime',
|
|
'sharedwithmetime': 'sharedWithMeTime',
|
|
'sharinguser': 'sharingUser',
|
|
'shortcutdetails': 'shortcutDetails',
|
|
'sha1checksum': 'sha1Checksum',
|
|
'sha256checksum': 'sha256Checksum',
|
|
'size': 'size',
|
|
'spaces': 'spaces',
|
|
'teamdriveid': 'driveId',
|
|
'teamdrivename': 'driveId',
|
|
'thumbnaillink': 'thumbnailLink',
|
|
'thumbnailversion': 'thumbnailVersion',
|
|
'title': 'name',
|
|
'trasheddate': 'trashedTime',
|
|
'trashedtime': 'trashedTime',
|
|
'trashinguser': 'trashingUser',
|
|
'userpermission': 'ownedByMe,capabilities.canEdit,capabilities.canComment',
|
|
'version': 'version',
|
|
'videomediametadata': 'videoMediaMetadata',
|
|
'viewedbymedate': 'viewedByMeTime',
|
|
'viewedbymetime': 'viewedByMeTime',
|
|
'viewerscancopycontent': 'copyRequiresWriterPermission',
|
|
'webcontentlink': 'webContentLink',
|
|
'webviewlink': 'webViewLink',
|
|
'writerscanshare': 'writersCanShare',
|
|
}
|
|
|
|
DRIVE_CAPABILITIES_SUBFIELDS_CHOICE_MAP = {
|
|
'canacceptownership': 'canAcceptOwnership',
|
|
'canaddchildren': 'canAddChildren',
|
|
'canaddfolderfromanotherdrive': 'canAddFolderFromAnotherDrive',
|
|
'canaddmydriveparent': 'canAddMyDriveParent',
|
|
'canchangecopyrequireswriterpermission': 'canChangeCopyRequiresWriterPermission',
|
|
'canchangecopyrequireswriterpermissionrestriction': 'canChangeCopyRequiresWriterPermissionRestriction',
|
|
'canchangedomainusersonlyrestriction': 'canChangeDomainUsersOnlyRestriction',
|
|
'canchangedrivebackground': 'canChangeDriveBackground',
|
|
'canchangedrivemembersonlyrestriction': 'canChangeDriveMembersOnlyRestriction',
|
|
'canchangesecurityupdateenabled': 'canChangeSecurityUpdateEnabled',
|
|
'canchangesharingfoldersrequiresorganizerpermissionrestriction': 'canChangeSharingFoldersRequiresOrganizerPermissionRestriction',
|
|
'canchangeviewerscancopycontent': 'canChangeViewersCanCopyContent',
|
|
'cancomment': 'canComment',
|
|
'cancopy': 'canCopy',
|
|
'candelete': 'canDelete',
|
|
'candeletechildren': 'canDeleteChildren',
|
|
'candeletedrive': 'canDeleteDrive',
|
|
'candisableinheritedpermissions': 'canDisableInheritedPermissions',
|
|
'candownload': 'canDownload',
|
|
'canedit': 'canEdit',
|
|
'canenableinheritedpermissions': 'canEnableInheritedPermissions',
|
|
'canlistchildren': 'canListChildren',
|
|
'canmanagemembers': 'canManageMembers',
|
|
'canmodifycontent': 'canModifyContent',
|
|
'canmodifycontentrestriction': 'canModifyContentRestriction',
|
|
'canmodifyeditorcontentrestriction': 'canModifyEditorContentRestriction',
|
|
'canmodifylabels': 'canModifyLabels',
|
|
'canmodifyownercontentrestriction': 'canModifyOwnerContentRestriction',
|
|
'canmovechildrenoutofdrive': 'canMoveChildrenOutOfDrive',
|
|
'canmovechildrenoutofteamdrive': 'canMoveChildrenOutOfDrive',
|
|
'canmovechildrenwithindrive': 'canMoveChildrenWithinDrive',
|
|
'canmovechildrenwithinteamdrive': 'canMoveChildrenWithinDrive',
|
|
'canmoveitemintodrive': 'canMoveItemIntoDrive',
|
|
'canmoveitemintoteamdrive': 'canMoveItemIntoDrive',
|
|
'canmoveitemoutofdrive': 'canMoveItemOutOfDrive',
|
|
'canmoveitemoutofteamdrive': 'canMoveItemOutOfDrive',
|
|
'canmoveitemwithindrive': 'canMoveItemWithinDrive',
|
|
'canmoveitemwithinteamdrive': 'canMoveItemWithinDrive',
|
|
'canmoveteamdriveitem': 'canMoveTeamDriveItem',
|
|
'canreaddrive': 'canReadDrive',
|
|
'canreadlabels': 'canReadLabels',
|
|
'canreadrevisions': 'canReadRevisions',
|
|
'canreadteamdrive': 'canReadDrive',
|
|
'canremovechildren': 'canRemoveChildren',
|
|
'canremovecontentrestriction': 'canRemoveContentRestriction',
|
|
'canremovemydriveparent': 'canRemoveMyDriveParent',
|
|
'canrename': 'canRename',
|
|
'canrenamedrive': 'canRenameDrive',
|
|
'canresetdriverestrictions': 'canResetDriveRestrictions',
|
|
'canshare': 'canShare',
|
|
'cantrash': 'canTrash',
|
|
'cantrashchildren': 'canTrashChildren',
|
|
'canuntrash': 'canUntrash',
|
|
}
|
|
|
|
DRIVE_CONTENT_RESTRICTIONS_SUBFIELDS_CHOICE_MAP = {
|
|
'ownerrestricted': 'ownerRestricted',
|
|
'readonly': 'readOnly',
|
|
'reason': 'reason',
|
|
'restrictinguser': 'restrictingUser',
|
|
'restrictiontime': 'restrictionTime',
|
|
'type': 'type',
|
|
}
|
|
|
|
DRIVE_LABELINFO_SUBFIELDS_CHOICE_MAP = {
|
|
'id': 'labels(id)',
|
|
'fields': 'labels(fields)',
|
|
'revisionid': 'labels(revisionId)',
|
|
}
|
|
|
|
DRIVE_OWNERS_SUBFIELDS_CHOICE_MAP = {
|
|
'displayname': 'displayName',
|
|
'emailaddress': 'emailAddress',
|
|
'isauthenticateduser': 'me',
|
|
'me': 'me',
|
|
'permissionid': 'permissionId',
|
|
'photolink': 'photoLink',
|
|
'picture': 'photoLink',
|
|
}
|
|
|
|
DRIVE_PARENTS_SUBFIELDS_CHOICE_MAP = {
|
|
'id': 'id',
|
|
'isroot': 'isRoot',
|
|
}
|
|
|
|
DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP = {
|
|
'additionalroles': 'role',
|
|
'allowfilediscovery': 'allowFileDiscovery',
|
|
'deleted': 'deleted',
|
|
'displayname': 'displayName',
|
|
'domain': 'domain',
|
|
'emailaddress': 'emailAddress',
|
|
'expirationdate': 'expirationTime',
|
|
'expirationtime': 'expirationTime',
|
|
'id': 'id',
|
|
'inheritedpermissionsdisabled': 'inheritedPermissionsDisabled',
|
|
'name': 'displayName',
|
|
'pendingowner': 'pendingOwner',
|
|
'permissiondetails': 'permissionDetails',
|
|
'photolink': 'photoLink',
|
|
'role': 'role',
|
|
'shareddrivepermissiondetails': 'permissionDetails',
|
|
'teamdrivepermissiondetails': 'permissionDetails',
|
|
'type': 'type',
|
|
'view': 'view',
|
|
'withlink': 'allowFileDiscovery',
|
|
}
|
|
|
|
DRIVE_SHARINGUSER_SUBFIELDS_CHOICE_MAP = {
|
|
'displayname': 'displayName',
|
|
'emailaddress': 'emailAddress',
|
|
'isauthenticateduser': 'me',
|
|
'me': 'me',
|
|
'name': 'displayName',
|
|
'permissionid': 'permissionId',
|
|
'photolink': 'photoLink',
|
|
'picture': 'photoLink',
|
|
}
|
|
|
|
DRIVE_SHORTCUTDETAILS_SUBFIELDS_CHOICE_MAP = {
|
|
'targetid': 'targetId',
|
|
'targetmimetype': 'targetMimeType',
|
|
'targetresourcekey': 'targetResourceKey',
|
|
}
|
|
|
|
DRIVE_SUBFIELDS_CHOICE_MAP = {
|
|
'capabilities': DRIVE_CAPABILITIES_SUBFIELDS_CHOICE_MAP,
|
|
'contentrestrictions': DRIVE_CONTENT_RESTRICTIONS_SUBFIELDS_CHOICE_MAP,
|
|
'labelinfo': DRIVE_LABELINFO_SUBFIELDS_CHOICE_MAP,
|
|
'labels': DRIVE_LABEL_CHOICE_MAP,
|
|
'lastmodifyinguser': DRIVE_SHARINGUSER_SUBFIELDS_CHOICE_MAP,
|
|
'owners': DRIVE_OWNERS_SUBFIELDS_CHOICE_MAP,
|
|
'parents': DRIVE_PARENTS_SUBFIELDS_CHOICE_MAP,
|
|
'permissions': DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP,
|
|
'sharinguser': DRIVE_SHARINGUSER_SUBFIELDS_CHOICE_MAP,
|
|
'trashinguser': DRIVE_SHARINGUSER_SUBFIELDS_CHOICE_MAP,
|
|
'shortcutdetails': DRIVE_SHORTCUTDETAILS_SUBFIELDS_CHOICE_MAP,
|
|
}
|
|
|
|
DRIVE_LIST_FIELDS = {'owners', 'parents', 'permissions', 'permissionIds', 'spaces'}
|
|
|
|
FILEINFO_FIELDS_TITLES = ['name', 'mimeType']
|
|
FILEPATH_FIELDS_TITLES = ['name', 'id', 'mimeType', 'ownedByMe', 'parents', 'sharedWithMeTime', 'driveId']
|
|
FILEPATH_FIELDS = ','.join(FILEPATH_FIELDS_TITLES)
|
|
|
|
def _getDriveTimeObjects():
|
|
timeObjects = ['createdTime', 'viewedByMeTime', 'modifiedByMeTime', 'modifiedTime', 'restrictionTime', 'sharedWithMeTime', 'trashedTime']
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
_mapDrive3TitlesToDrive2(timeObjects, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
|
|
return set(timeObjects)
|
|
|
|
def _getDriveFieldSubField(field, fieldsList, parentsSubFields):
|
|
field, subField = field.split('.', 1)
|
|
if field in DRIVE_SUBFIELDS_CHOICE_MAP:
|
|
if field == 'parents':
|
|
fieldsList.append(DRIVE_FIELDS_CHOICE_MAP[field])
|
|
parentsSubFields[DRIVE_SUBFIELDS_CHOICE_MAP[field][subField]] = True
|
|
elif subField in DRIVE_SUBFIELDS_CHOICE_MAP[field]:
|
|
if field != 'labels':
|
|
if not isinstance(DRIVE_SUBFIELDS_CHOICE_MAP[field][subField], list):
|
|
fieldsList.append(f'{DRIVE_FIELDS_CHOICE_MAP[field]}.{DRIVE_SUBFIELDS_CHOICE_MAP[field][subField]}')
|
|
else:
|
|
for subSubField in DRIVE_SUBFIELDS_CHOICE_MAP[field][subField]:
|
|
fieldsList.append(f'{DRIVE_FIELDS_CHOICE_MAP[field]}.{subSubField}')
|
|
else:
|
|
fieldsList.append(DRIVE_SUBFIELDS_CHOICE_MAP[field][subField])
|
|
else:
|
|
invalidChoiceExit(subField, list(DRIVE_SUBFIELDS_CHOICE_MAP[field]), True)
|
|
else:
|
|
invalidChoiceExit(field, list(DRIVE_SUBFIELDS_CHOICE_MAP), True)
|
|
|
|
class DriveFileFields():
|
|
def __init__(self):
|
|
self.showSharedDriveNames = False
|
|
self.allFields = False
|
|
self.OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
|
|
self.fieldsList = []
|
|
self.includeLabels = []
|
|
self.parentsSubFields = {'id': False, 'isRoot': False, 'rootFolderId': None}
|
|
|
|
def SetAllParentsSubFields(self):
|
|
self.parentsSubFields['id'] = self.parentsSubFields['isRoot'] = True
|
|
|
|
def ProcessArgument(self, myarg):
|
|
if myarg == 'allfields':
|
|
self.fieldsList = []
|
|
self.allFields = True
|
|
elif myarg in DRIVE_LABEL_CHOICE_MAP:
|
|
addFieldToFieldsList(myarg, DRIVE_LABEL_CHOICE_MAP, self.fieldsList)
|
|
elif myarg in DRIVE_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(myarg, DRIVE_FIELDS_CHOICE_MAP, self.fieldsList)
|
|
if myarg == 'parents':
|
|
self.SetAllParentsSubFields()
|
|
elif myarg in {'drivename', 'shareddrivename', 'teamdrivename'}:
|
|
self.showSharedDriveNames = True
|
|
elif myarg == 'fields':
|
|
for field in _getFieldsList():
|
|
if field in DRIVE_LABEL_CHOICE_MAP:
|
|
addFieldToFieldsList(field, DRIVE_LABEL_CHOICE_MAP, self.fieldsList)
|
|
elif field.find('.') == -1:
|
|
if field in DRIVE_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, DRIVE_FIELDS_CHOICE_MAP, self.fieldsList)
|
|
if field == 'parents':
|
|
self.SetAllParentsSubFields()
|
|
elif field in {'drivename', 'shareddrivename', 'teamdrivename'}:
|
|
self.showSharedDriveNames = True
|
|
else:
|
|
invalidChoiceExit(field, list(DRIVE_FIELDS_CHOICE_MAP)+list(DRIVE_LABEL_CHOICE_MAP), True)
|
|
else:
|
|
_getDriveFieldSubField(field, self.fieldsList, self.parentsSubFields)
|
|
elif myarg == 'includelabels':
|
|
labelIds = getEntityList(Cmd.OB_CLASSIFICATION_LABEL_ID, shlexSplit=True)
|
|
for labelId in labelIds:
|
|
self.includeLabels.append(normalizeDriveLabelID(labelId))
|
|
elif myarg.find('.') != -1:
|
|
_getDriveFieldSubField(myarg, self.fieldsList, self.parentsSubFields)
|
|
elif myarg == 'orderby':
|
|
self.OBY.GetChoice()
|
|
elif myarg in {'showdrivename', 'showshareddrivename', 'showteamdrivename'}:
|
|
self.showSharedDriveNames = True
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
@property
|
|
def orderBy(self):
|
|
return self.OBY.orderBy
|
|
|
|
def _setSkipObjects(skipObjects, skipTitles, fieldsList):
|
|
for field in skipTitles:
|
|
if field != 'parents':
|
|
if field not in fieldsList:
|
|
skipObjects.add(field)
|
|
fieldsList.append(field)
|
|
else:
|
|
for xfield in fieldsList:
|
|
if xfield.startswith('parents'):
|
|
break
|
|
else:
|
|
skipObjects.add(field)
|
|
fieldsList.append('parents')
|
|
|
|
def _setGetPermissionsForSharedDrives(fieldsList):
|
|
getPermissionsForSharedDrives = False
|
|
permissionsFieldsList = []
|
|
permissionsFields = None
|
|
for field in fieldsList:
|
|
if field.startswith('permissions'):
|
|
getPermissionsForSharedDrives = True
|
|
if field.find('.') != -1:
|
|
field, subField = field.split('.', 1)
|
|
permissionsFieldsList.append(subField)
|
|
if getPermissionsForSharedDrives:
|
|
permissionsFields = getItemFieldsFromFieldsList('permissions', permissionsFieldsList, True)
|
|
return (getPermissionsForSharedDrives, permissionsFields)
|
|
|
|
SHOWLABELS_CHOICES = {'details', 'ids'}
|
|
|
|
def _formatFileDriveLabels(showLabels, labels, result, printMode, delimiter):
|
|
if showLabels == 'details':
|
|
result['labels'] = labels
|
|
else:
|
|
if printMode:
|
|
result['labels'] = len(labels)
|
|
result['labelsIds'] = delimiter.join([label['id'] for label in labels])
|
|
|
|
# gam <UserTypeEntity> info drivefile <DriveFileEntity>
|
|
# [returnidonly]
|
|
# [filepath|fullpath] [folderpathonly [<Boolean>]] [pathdelimiter <Character>]
|
|
# [allfields|<DriveFieldName>*|(fields <DriveFieldNameList>)] [formatjson]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [showdrivename] [showshareddrivepermissions]
|
|
# [(showlabels details|ids)|(includelabels <DriveLabelIDList>)]
|
|
# [showparentsidsaslist] [followshortcuts [<Boolean>]]
|
|
# [stripcrsfromname] [formatjson]
|
|
# gam <UserTypeEntity> show fileinfo <DriveFileEntity>
|
|
# [returnidonly]
|
|
# [filepath|fullpath] [folderpathonly [<Boolean>]] [pathdelimiter <Character>]
|
|
# [allfields|<DriveFieldName>*|(fields <DriveFieldNameList>)] [formatjson]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [showdrivename] [showshareddrivepermissions]
|
|
# [(showlabels details|ids)|(includelabels <DriveLabelIDList>)]
|
|
# [showparentsidsaslist] [followshortcuts [<Boolean>]]
|
|
# [stripcrsfromname] [formatjson]
|
|
def showFileInfo(users):
|
|
def _setSelectionFields():
|
|
_setSkipObjects(skipObjects, FILEINFO_FIELDS_TITLES, DFF.fieldsList)
|
|
if filepath:
|
|
_setSkipObjects(skipObjects, FILEPATH_FIELDS_TITLES, DFF.fieldsList)
|
|
if getPermissionsForSharedDrives or DFF.showSharedDriveNames:
|
|
_setSkipObjects(skipObjects, ['driveId'], DFF.fieldsList)
|
|
if followShortcuts:
|
|
_setSkipObjects(skipObjects, ['mimeType', 'shortcutDetails'], DFF.fieldsList)
|
|
|
|
getPermissionsForSharedDrives = filepath = fullpath = folderPathOnly = followShortcuts = \
|
|
returnIdOnly = showParentsIdsAsList = showNoParents = stripCRsFromName = False
|
|
pathDelimiter = '/'
|
|
showLabels = None
|
|
simpleLists = []
|
|
skipObjects = set()
|
|
fileIdEntity = getDriveFileEntity()
|
|
DFF = DriveFileFields()
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'filepath':
|
|
filepath = True
|
|
elif myarg == 'fullpath':
|
|
filepath = fullpath = True
|
|
elif myarg == 'folderpathonly':
|
|
folderPathOnly = getBoolean()
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
elif myarg == 'showparentsidsaslist':
|
|
showParentsIdsAsList = True
|
|
simpleLists.append('parentsIds')
|
|
elif myarg == 'stripcrsfromname':
|
|
stripCRsFromName = True
|
|
elif myarg == 'showlabels':
|
|
showLabels = getChoice(SHOWLABELS_CHOICES)
|
|
elif myarg == 'showshareddrivepermissions':
|
|
getPermissionsForSharedDrives = True
|
|
permissionsFields = f'nextPageToken,permissions({",".join(DRIVEFILE_BASIC_PERMISSION_FIELDS)})'
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
elif myarg == 'followshortcuts':
|
|
followShortcuts = getBoolean()
|
|
elif DFF.ProcessArgument(myarg):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if DFF.fieldsList:
|
|
if not getPermissionsForSharedDrives:
|
|
getPermissionsForSharedDrives, permissionsFields = _setGetPermissionsForSharedDrives(DFF.fieldsList)
|
|
_setSelectionFields()
|
|
if followShortcuts:
|
|
DFF.fieldsList.extend(['mimeType', 'shortcutDetails'])
|
|
fields = getFieldsFromFieldsList(DFF.fieldsList)
|
|
showNoParents = 'parents' in DFF.fieldsList
|
|
else:
|
|
fields = '*'
|
|
DFF.SetAllParentsSubFields()
|
|
skipObjects = skipObjects.union(DEFAULT_SKIP_OBJECTS)
|
|
showNoParents = True
|
|
includeLabels = ','.join(DFF.includeLabels)
|
|
pathFields = FILEPATH_FIELDS
|
|
timeObjects = _getDriveTimeObjects()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if returnIdOnly:
|
|
GC.Values[GC.SHOW_GETTINGS] = False
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=[Ent.DRIVE_FILE_OR_FOLDER, None][FJQC.formatJSON or returnIdOnly],
|
|
orderBy=DFF.orderBy)
|
|
if jcount == 0:
|
|
continue
|
|
if returnIdOnly:
|
|
for fileId in fileIdEntity['list']:
|
|
writeStdout(f'{fileId}\n')
|
|
continue
|
|
if not showParentsIdsAsList and DFF.parentsSubFields['isRoot']:
|
|
try:
|
|
DFF.parentsSubFields['rootFolderId'] = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=ROOT, fields='id')['id']
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
if filepath:
|
|
filePathInfo = initFilePathInfo(pathDelimiter)
|
|
if fullpath:
|
|
fileTree, status = initFileTree(drive, fileIdEntity.get('shareddrive'), None, [], True, user, i, count)
|
|
if not status:
|
|
continue
|
|
else:
|
|
fileTree = None
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS+[GAPI.INVALID],
|
|
fileId=fileId, includeLabels=includeLabels, fields=fields, supportsAllDrives=True)
|
|
if followShortcuts and result['mimeType'] == MIMETYPE_GA_SHORTCUT:
|
|
fileId = result['shortcutDetails']['targetId']
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS+[GAPI.INVALID],
|
|
fileId=fileId, includeLabels=includeLabels, fields=fields, supportsAllDrives=True)
|
|
if stripCRsFromName:
|
|
result['name'] = _stripControlCharsFromName(result['name'])
|
|
driveId = result.get('driveId')
|
|
if driveId:
|
|
if result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == TEAM_DRIVE:
|
|
result['name'] = _getSharedDriveNameFromId(driveId)
|
|
if DFF.showSharedDriveNames:
|
|
result['driveName'] = _getSharedDriveNameFromId(driveId)
|
|
if showNoParents:
|
|
result.setdefault('parents', [])
|
|
if getPermissionsForSharedDrives and driveId and 'permissions' not in result:
|
|
try:
|
|
result['permissions'] = callGAPIpages(drive.permissions(), 'list', 'permissions',
|
|
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
fileId=fileId, fields=permissionsFields, supportsAllDrives=True)
|
|
for permission in result['permissions']:
|
|
permission.pop('teamDrivePermissionDetails', None)
|
|
except (GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions) as e:
|
|
if fields != '*':
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
|
|
continue
|
|
if showLabels is not None:
|
|
labels = callGAPIitems(drive.files(), 'listLabels', 'labels',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
|
|
fileId=fileId)
|
|
_formatFileDriveLabels(showLabels, labels, result, False, ' ')
|
|
if not FJQC.formatJSON:
|
|
printEntity([_getEntityMimeType(result), f'{result["name"]} ({fileId})'], j, jcount)
|
|
Ind.Increment()
|
|
if filepath:
|
|
if fullpath:
|
|
extendFileTree(fileTree, [result], None, False)
|
|
extendFileTreeParents(drive, fileTree, pathFields)
|
|
if not FJQC.formatJSON:
|
|
_, paths, _ = getFilePaths(drive, fileTree, result, filePathInfo, addParentsToTree=True,
|
|
fullpath=fullpath, folderPathOnly=folderPathOnly)
|
|
kcount = len(paths)
|
|
printKeyValueList(['paths', kcount])
|
|
Ind.Increment()
|
|
for path in sorted(paths):
|
|
printKeyValueList(['path', path])
|
|
Ind.Decrement()
|
|
else:
|
|
addFilePathsToInfo(drive, fileTree, result, filePathInfo,
|
|
addParentsToTree=True, folderPathOnly=folderPathOnly)
|
|
if fullpath:
|
|
# Save simple parents list as mappings turn it into a list of dicts
|
|
fpparents = result['parents'][:]
|
|
if showParentsIdsAsList and 'parents' in result:
|
|
result['parentsIds'] = result.pop('parents')
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
_mapDriveFieldNames(result, user, DFF.parentsSubFields, True)
|
|
else:
|
|
_mapDriveParents(result, DFF.parentsSubFields)
|
|
_mapDriveProperties(result)
|
|
for permission in result.get('permissions', []):
|
|
_mapDrivePermissionNames(permission)
|
|
if not FJQC.formatJSON:
|
|
showJSON(None, result, skipObjects=skipObjects, timeObjects=timeObjects, simpleLists=simpleLists,
|
|
dictObjectsKey={'owners': 'displayName', 'fields': 'id', 'labels': 'id', 'user': 'emailAddress', 'parents': 'id',
|
|
'permissions': ['name', 'displayName'][GC.Values[GC.DRIVE_V3_NATIVE_NAMES]]})
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(result, skipObjects=skipObjects, timeObjects=timeObjects), ensure_ascii=False, sort_keys=True))
|
|
if fullpath:
|
|
# Restore simple parents list
|
|
fileTree[fileId]['info']['parents'] = fpparents[:]
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def getRevisionsEntity():
|
|
revisionsEntity = {'list': [], 'dict': None, 'count': None, 'time': None, 'range': None}
|
|
startEndTime = StartEndTime()
|
|
entitySelector = getEntitySelector()
|
|
if entitySelector:
|
|
entityList = getEntitySelection(entitySelector, False)
|
|
if isinstance(entityList, dict):
|
|
revisionsEntity['dict'] = entityList
|
|
else:
|
|
revisionsEntity['list'] = entityList
|
|
else:
|
|
myarg = getString(Cmd.OB_DRIVE_FILE_REVISION_ID, checkBlank=True)
|
|
mycmd = myarg.lower()
|
|
if mycmd == 'id':
|
|
revisionsEntity['list'] = getStringReturnInList(Cmd.OB_DRIVE_FILE_REVISION_ID)
|
|
elif mycmd[:3] == 'id:':
|
|
revisionsEntity['list'] = [myarg[3:]]
|
|
elif mycmd == 'ids':
|
|
revisionsEntity['list'] = getString(Cmd.OB_DRIVE_FILE_REVISION_ID).replace(',', ' ').split()
|
|
elif mycmd[:4] == 'ids:':
|
|
revisionsEntity['list'] = myarg[4:].replace(',', ' ').split()
|
|
elif mycmd in {'first', 'last', 'allexceptfirst', 'allexceptlast'}:
|
|
revisionsEntity['count'] = (mycmd, getInteger(minVal=1))
|
|
elif mycmd in {'before', 'after'}:
|
|
dateTime, _, _ = getTimeOrDeltaFromNow(True)
|
|
revisionsEntity['time'] = (mycmd, dateTime)
|
|
elif mycmd == 'range':
|
|
startEndTime.Get(mycmd)
|
|
revisionsEntity['range'] = (mycmd, startEndTime.startDateTime, startEndTime.endDateTime)
|
|
else:
|
|
revisionsEntity['list'] = [myarg]
|
|
return revisionsEntity
|
|
|
|
def _selectRevisionIds(drive, fileId, origUser, user, i, count, j, jcount, revisionsEntity):
|
|
if revisionsEntity['list']:
|
|
return revisionsEntity['list']
|
|
if revisionsEntity['dict']:
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
return revisionsEntity['dict'][fileId]
|
|
return revisionsEntity['dict'][origUser][fileId]
|
|
try:
|
|
results = callGAPIpages(drive.revisions(), 'list', 'revisions',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.REVISIONS_NOT_SUPPORTED],
|
|
fileId=fileId, fields='nextPageToken,revisions(id,modifiedTime)',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.revisionsNotSupported) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
|
|
return []
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return []
|
|
numRevisions = len(results)
|
|
if numRevisions == 0:
|
|
return []
|
|
if revisionsEntity['count']:
|
|
countType = revisionsEntity['count'][0]
|
|
count = revisionsEntity['count'][1]
|
|
revisionIds = [revision['id'] for revision in results]
|
|
if countType == 'first':
|
|
if count >= numRevisions:
|
|
return revisionIds
|
|
return revisionIds[:count]
|
|
if countType == 'last':
|
|
if count >= numRevisions:
|
|
return revisionIds
|
|
return revisionIds[-count:]
|
|
if countType == 'allexceptfirst':
|
|
if count >= numRevisions:
|
|
return []
|
|
return revisionIds[count:]
|
|
# count: allexceptlast
|
|
if count >= numRevisions:
|
|
return []
|
|
return revisionIds[:-count]
|
|
revisionIds = []
|
|
if revisionsEntity['time']:
|
|
dateTime = revisionsEntity['time'][1]
|
|
count = 0
|
|
if revisionsEntity['time'][0] == 'before':
|
|
for revision in results:
|
|
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
|
|
if modifiedDateTime >= dateTime:
|
|
break
|
|
revisionIds.append(revision['id'])
|
|
count += 1
|
|
if count >= numRevisions:
|
|
return revisionIds[:-1]
|
|
return revisionIds
|
|
# time: after
|
|
for revision in results:
|
|
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
|
|
if modifiedDateTime >= dateTime:
|
|
revisionIds.append(revision['id'])
|
|
count += 1
|
|
if count >= numRevisions:
|
|
return revisionIds[1:]
|
|
return revisionIds
|
|
# range
|
|
startDateTime = revisionsEntity['range'][1]
|
|
endDateTime = revisionsEntity['range'][2]
|
|
count = 0
|
|
for revision in results:
|
|
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
|
|
if modifiedDateTime >= startDateTime:
|
|
if modifiedDateTime >= endDateTime:
|
|
break
|
|
revisionIds.append(revision['id'])
|
|
count += 1
|
|
if count >= numRevisions:
|
|
return revisionIds[1:]
|
|
return revisionIds
|
|
|
|
# gam <UserTypeEntity> delete filerevisions <DriveFileEntity> select <DriveFileRevisionIdEntity> [previewdelete]
|
|
# [showtitles] [doit] [max_to_delete <Number>]
|
|
def deleteFileRevisions(users):
|
|
fileIdEntity = getDriveFileEntity()
|
|
revisionsEntity = None
|
|
previewDelete = showTitles = doIt = False
|
|
maxToProcess = 1
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'select':
|
|
revisionsEntity = getRevisionsEntity()
|
|
elif myarg == 'previewdelete':
|
|
previewDelete = True
|
|
elif myarg == 'showtitles':
|
|
showTitles = True
|
|
elif myarg == 'doit':
|
|
doIt = True
|
|
elif myarg in {'maxtodelete', 'maxtoprocess'}:
|
|
maxToProcess = getInteger(minVal=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not revisionsEntity:
|
|
missingArgumentExit('select <DriveFileRevisionIdEntity>')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
|
|
if jcount == 0:
|
|
continue
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
fileName = fileId
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
|
|
if showTitles:
|
|
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId)
|
|
revisionIds = _selectRevisionIds(drive, fileId, origUser, user, i, count, j, jcount, revisionsEntity)
|
|
kcount = len(revisionIds)
|
|
if kcount == 0:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
|
|
Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(Ent.DRIVE_FILE_REVISION)), j, jcount)
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
if not previewDelete:
|
|
if maxToProcess and kcount > maxToProcess:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
|
|
Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(kcount, Act.ToPerform(), maxToProcess), j, jcount)
|
|
continue
|
|
if not doIt:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
|
|
Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, j, jcount)
|
|
continue
|
|
entityPerformActionNumItems([Ent.USER, user, entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, j, jcount)
|
|
else:
|
|
entityPerformActionNumItemsModifier([Ent.USER, user, entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, Msg.PREVIEW_ONLY, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for revisionId in revisionIds:
|
|
k += 1
|
|
if not previewDelete:
|
|
try:
|
|
callGAPI(drive.revisions(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.REVISION_NOT_FOUND, GAPI.REVISION_DELETION_NOT_SUPPORTED,
|
|
GAPI.CANNOT_DELETE_ONLY_REVISION, GAPI.REVISIONS_NOT_SUPPORTED],
|
|
fileId=fileId, revisionId=revisionId)
|
|
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], k, kcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.revisionDeletionNotSupported, GAPI.cannotDeleteOnlyRevision, GAPI.revisionsNotSupported) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
|
|
except GAPI.revisionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], k, kcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], Msg.PREVIEW_ONLY, k, kcount)
|
|
Ind.Decrement()
|
|
|
|
REVISIONS_FIELDS_CHOICE_MAP = {
|
|
'keepforever': 'keepForever',
|
|
'published': 'published',
|
|
'publishauto': 'publishAuto',
|
|
'publishedoutsidedomain': 'publishedOutsideDomain'
|
|
}
|
|
# gam <UserTypeEntity> update filerevisions <DriveFileEntity> select <DriveFileRevisionIdEntity> [previewupdate]
|
|
# [published [<Boolean>]] [publishauto [<Boolean>]] [publishedoutsidedomain [<Boolean>]]
|
|
# [keepforever [<Boolean>]}
|
|
# [showtitles] [doit] [max_to_update <Number>]
|
|
def updateFileRevisions(users):
|
|
fileIdEntity = getDriveFileEntity()
|
|
revisionsEntity = None
|
|
previewUpdate = showTitles = doIt = False
|
|
maxToProcess = 1
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'select':
|
|
revisionsEntity = getRevisionsEntity()
|
|
elif myarg in REVISIONS_FIELDS_CHOICE_MAP:
|
|
body[REVISIONS_FIELDS_CHOICE_MAP[myarg]] = getBoolean()
|
|
elif myarg == 'previewupdate':
|
|
previewUpdate = True
|
|
elif myarg == 'showtitles':
|
|
showTitles = True
|
|
elif myarg == 'doit':
|
|
doIt = True
|
|
elif myarg in {'maxtoupdate', 'maxtoprocess'}:
|
|
maxToProcess = getInteger(minVal=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not revisionsEntity:
|
|
missingArgumentExit('select <DriveFileRevisionIdEntity>')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
|
|
if jcount == 0:
|
|
continue
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
fileName = fileId
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
|
|
if showTitles:
|
|
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId)
|
|
revisionIds = _selectRevisionIds(drive, fileId, origUser, user, i, count, j, jcount, revisionsEntity)
|
|
kcount = len(revisionIds)
|
|
if kcount == 0:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
|
|
Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(Ent.DRIVE_FILE_REVISION)), j, jcount)
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
if not previewUpdate:
|
|
if maxToProcess and kcount > maxToProcess:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
|
|
Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(kcount, Act.ToPerform(), maxToProcess), j, jcount)
|
|
continue
|
|
if not doIt:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user, entityType, fileName], Ent.DRIVE_FILE_REVISION, kcount,
|
|
Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, j, jcount)
|
|
continue
|
|
entityPerformActionNumItems([Ent.USER, user, entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, j, jcount)
|
|
else:
|
|
entityPerformActionNumItemsModifier([Ent.USER, user, entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, Msg.PREVIEW_ONLY, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for revisionId in revisionIds:
|
|
k += 1
|
|
if not previewUpdate:
|
|
try:
|
|
callGAPI(drive.revisions(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.REVISION_NOT_FOUND, GAPI.REVISIONS_NOT_SUPPORTED],
|
|
fileId=fileId, revisionId=revisionId, body=body)
|
|
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], k, kcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.revisionsNotSupported) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
|
|
except GAPI.revisionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], k, kcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, fileName, Ent.DRIVE_FILE_REVISION, revisionId], Msg.PREVIEW_ONLY, k, kcount)
|
|
Ind.Decrement()
|
|
|
|
def _selectRevisionResults(results, fileId, origUser, revisionsEntity, previewDelete):
|
|
numRevisions = len(results)
|
|
if numRevisions == 0:
|
|
return results
|
|
if revisionsEntity['count']:
|
|
countType = revisionsEntity['count'][0]
|
|
count = revisionsEntity['count'][1]
|
|
if countType == 'first':
|
|
if count >= numRevisions:
|
|
if previewDelete:
|
|
return results[:-1]
|
|
return results
|
|
return results[:count]
|
|
if countType == 'last':
|
|
if count >= numRevisions:
|
|
if previewDelete:
|
|
return results[1:]
|
|
return results
|
|
return results[-count:]
|
|
if countType == 'allexceptfirst':
|
|
if count >= numRevisions:
|
|
return []
|
|
return results[count:]
|
|
# count: allexceptlast
|
|
if count >= numRevisions:
|
|
return []
|
|
return results[:-count]
|
|
if revisionsEntity['time']:
|
|
dateTime = revisionsEntity['time'][1]
|
|
count = 0
|
|
if revisionsEntity['time'][0] == 'before':
|
|
for revision in results:
|
|
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
|
|
if modifiedDateTime >= dateTime:
|
|
break
|
|
count += 1
|
|
if count >= numRevisions:
|
|
if previewDelete:
|
|
return results[:-1]
|
|
return results
|
|
return results[:count]
|
|
# time: after
|
|
for revision in results:
|
|
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
|
|
if modifiedDateTime >= dateTime:
|
|
break
|
|
count += 1
|
|
if count == 0:
|
|
if previewDelete:
|
|
return results[1:]
|
|
return results
|
|
if count >= numRevisions:
|
|
return []
|
|
return results[count:]
|
|
if revisionsEntity['range']:
|
|
startDateTime = revisionsEntity['range'][1]
|
|
endDateTime = revisionsEntity['range'][2]
|
|
count = 0
|
|
selectedResults = []
|
|
for revision in results:
|
|
modifiedDateTime, _ = iso8601.parse_date(revision['modifiedTime'])
|
|
if modifiedDateTime >= startDateTime:
|
|
if modifiedDateTime >= endDateTime:
|
|
break
|
|
selectedResults.append(revision)
|
|
count += 1
|
|
if count >= numRevisions:
|
|
if previewDelete:
|
|
return selectedResults[1:]
|
|
return selectedResults
|
|
# revisionsIds
|
|
selectedResults = []
|
|
if revisionsEntity['dict']:
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
revisionIds = revisionsEntity['dict'][fileId]
|
|
else:
|
|
revisionIds = revisionsEntity['dict'][origUser][fileId]
|
|
else:
|
|
revisionIds = revisionsEntity['list']
|
|
return [revision for revision in results if revision['id'] in revisionIds]
|
|
|
|
FILEREVISIONS_FIELDS_CHOICE_MAP = {
|
|
'filesize': 'size',
|
|
'id': 'id',
|
|
'keepforever': 'keepForever',
|
|
'lastmodifyinguser': 'lastModifyingUser',
|
|
'lastmodifyingusername': 'lastModifyingUser.displayName',
|
|
'md5checksum': 'md5Checksum',
|
|
'mimetype': 'mimeType',
|
|
'modifieddate': 'modifiedTime',
|
|
'modifiedtime': 'modifiedTime',
|
|
'originalfilename': 'originalFilename',
|
|
'pinned': 'keepForever',
|
|
'published': 'published',
|
|
'publishauto': 'publishAuto',
|
|
'publishedoutsidedomain': 'publishedOutsideDomain',
|
|
'size': 'size',
|
|
}
|
|
|
|
def _getFileRevisionsTimeObjects():
|
|
timeObjects = ['modifiedTime']
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
_mapDrive3TitlesToDrive2(timeObjects, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
|
|
return set(timeObjects)
|
|
|
|
def _showRevision(revision, timeObjects, i=0, count=0):
|
|
printEntity([Ent.DRIVE_FILE_REVISION, revision['id']], i, count)
|
|
Ind.Increment()
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
_mapDriveRevisionNames(revision)
|
|
showJSON(None, revision, ['id'], timeObjects)
|
|
Ind.Decrement()
|
|
|
|
DRIVE_REVISIONS_INDEXED_TITLES = ['revisions']
|
|
|
|
# gam <UserTypeEntity> show filerevisions <DriveFileEntity>
|
|
# [select <DriveFileRevisionIDEntity>]
|
|
# [previewdelete] [showtitles]
|
|
# [<RevisionsFieldName>*|(fields <RevisionsFieldNameList>)]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [stripcrsfromname]
|
|
# gam <UserTypeEntity> print filerevisions <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# [select <DriveFileRevisionIDEntity>]
|
|
# [previewdelete] [showtitles] [oneitemperrow]
|
|
# [<RevisionsFieldName>*|(fields <RevisionsFieldNameList>)]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [stripcrsfromname]
|
|
def printShowFileRevisions(users):
|
|
csvPF = CSVPrintFile(['Owner', 'id']) if Act.csvFormat() else None
|
|
fieldsList = []
|
|
fileIdEntity = getDriveFileEntity()
|
|
revisionsEntity = None
|
|
oneItemPerRow = previewDelete = showTitles = stripCRsFromName = False
|
|
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
|
|
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'select':
|
|
revisionsEntity = getRevisionsEntity()
|
|
elif csvPF and myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
if csvPF:
|
|
csvPF.AddTitles('revision.id')
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg == 'previewdelete':
|
|
previewDelete = True
|
|
elif myarg == 'showtitles':
|
|
showTitles = True
|
|
if csvPF:
|
|
csvPF.AddTitles(fileNameTitle)
|
|
elif myarg == 'stripcrsfromname':
|
|
stripCRsFromName = True
|
|
elif getFieldsList(myarg, FILEREVISIONS_FIELDS_CHOICE_MAP, fieldsList, initialField='id'):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
if fieldsList:
|
|
fields = getItemFieldsFromFieldsList('revisions', fieldsList)
|
|
else:
|
|
fields = '*'
|
|
timeObjects = _getFileRevisionsTimeObjects()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=[Ent.DRIVE_FILE_OR_FOLDER, None][csvPF is not None],
|
|
orderBy=OBY.orderBy)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
fileName = fileId
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
|
|
if showTitles:
|
|
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId, not csvPF)
|
|
if stripCRsFromName:
|
|
fileName = _stripControlCharsFromName(fileName)
|
|
try:
|
|
results = callGAPIpages(drive.revisions(), 'list', 'revisions',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.REVISIONS_NOT_SUPPORTED],
|
|
fileId=fileId, fields=fields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.revisionsNotSupported) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
if revisionsEntity:
|
|
results = _selectRevisionResults(results, fileId, origUser, revisionsEntity, previewDelete)
|
|
if not csvPF:
|
|
kcount = len(results)
|
|
entityPerformActionNumItems([entityType, fileName], kcount, Ent.DRIVE_FILE_REVISION, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for revision in results:
|
|
k += 1
|
|
_showRevision(revision, timeObjects, k, kcount)
|
|
Ind.Decrement()
|
|
elif results:
|
|
if oneItemPerRow:
|
|
for revision in results:
|
|
row = {'Owner': user, 'id': fileId}
|
|
if showTitles:
|
|
row[fileNameTitle] = fileName
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
_mapDriveRevisionNames(revision)
|
|
csvPF.WriteRowTitles(flattenJSON({'revision': revision}, flattened=row, timeObjects=timeObjects))
|
|
else:
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
for revision in results:
|
|
_mapDriveRevisionNames(revision)
|
|
if showTitles:
|
|
csvPF.WriteRowTitles(flattenJSON({'revisions': results}, flattened={'Owner': user, 'id': fileId, fileNameTitle: fileName}, timeObjects=timeObjects))
|
|
else:
|
|
csvPF.WriteRowTitles(flattenJSON({'revisions': results}, flattened={'Owner': user, 'id': fileId}, timeObjects=timeObjects))
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
if oneItemPerRow:
|
|
csvPF.SetSortTitles(['Owner', 'id', fileNameTitle, 'revision.id'])
|
|
else:
|
|
csvPF.SetSortTitles(['Owner', 'id', 'revisions'])
|
|
csvPF.SetIndexedTitles(DRIVE_REVISIONS_INDEXED_TITLES)
|
|
csvPF.writeCSVfile('Drive File Revisions')
|
|
|
|
def _stripMeInOwners(query):
|
|
if not query:
|
|
return query
|
|
query = query.replace(ME_IN_OWNERS_AND, '')
|
|
query = query.replace(AND_ME_IN_OWNERS, '')
|
|
return query.replace(ME_IN_OWNERS, '').strip()
|
|
|
|
def _stripNotMeInOwners(query):
|
|
if not query:
|
|
return query
|
|
query = query.replace(NOT_ME_IN_OWNERS_AND, '')
|
|
query = query.replace(AND_NOT_ME_IN_OWNERS, '')
|
|
return query.replace(NOT_ME_IN_OWNERS, '').strip()
|
|
|
|
def _updateAnyOwnerQuery(query):
|
|
query = _stripNotMeInOwners(query)
|
|
return _stripMeInOwners(query)
|
|
|
|
def initFileTree(drive, shareddrive, DLP, shareddriveFields, showParent, user, i, count):
|
|
fileTree = {
|
|
ORPHANS: {'info': {'id': ORPHANS, 'name': ORPHANS, 'mimeType': MIMETYPE_GA_FOLDER, 'ownedByMe': True, 'parents': []},
|
|
'noParents': True, 'children': []},
|
|
SHARED_WITHME: {'info': {'id': SHARED_WITHME, 'name': SHARED_WITHME, 'mimeType': MIMETYPE_GA_FOLDER, 'ownedByMe': False, 'parents': []},
|
|
'noParents': True, 'children': []},
|
|
SHARED_DRIVES: {'info': {'id': SHARED_DRIVES, 'name': SHARED_DRIVES, 'mimeType': MIMETYPE_GA_FOLDER, 'ownedByMe': False, 'parents': [], 'driveId': SHARED_DRIVES},
|
|
'noParents': True, 'children': []},
|
|
}
|
|
try:
|
|
if not shareddrive:
|
|
fileId = ROOT
|
|
f_file = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=fileId, fields=FILEPATH_FIELDS)
|
|
f_file['parents'] = []
|
|
fileTree[f_file['id']] = {'info': f_file, 'noParents': True, 'children': []}
|
|
elif 'driveId' in shareddrive:
|
|
fileId = shareddrive['driveId']
|
|
f_file = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FILE_NOT_FOUND],
|
|
fileId=fileId, supportsAllDrives=True, fields=FILEPATH_FIELDS)
|
|
f_file['parents'] = []
|
|
fileTree[f_file['id']] = {'info': f_file, 'noParents': True, 'children': []}
|
|
name = callGAPI(drive.drives(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
driveId=fileId, fields='name')['name']
|
|
fileTree[f_file['id']]['info']['name'] = f'{SHARED_DRIVES}/{name}'
|
|
else:
|
|
fileId = None
|
|
if DLP and (DLP.getSharedDriveNames or DLP.checkLocation in {LOCATION_ALL_DRIVES, LOCATION_ONLY_SHARED_DRIVES}):
|
|
tdrives = callGAPIpages(drive.drives(), 'list', 'drives',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID, GAPI.NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE],
|
|
fields='nextPageToken,drives(id,name)', pageSize=100)
|
|
for tdrive in tdrives:
|
|
fileId = tdrive['id']
|
|
if fileId not in fileTree:
|
|
fileTree[fileId] = {'info': {'id': fileId, 'name': f'{SHARED_DRIVES}/{tdrive["name"]}', 'mimeType': MIMETYPE_GA_FOLDER},
|
|
'noParents': True, 'children': []}
|
|
for field in shareddriveFields:
|
|
if field in tdrive:
|
|
fileTree[fileId]['info'][field] = tdrive[field]
|
|
if showParent:
|
|
fileTree[fileId]['info']['driveId'] = fileId
|
|
fileTree[SHARED_DRIVES]['children'].append(fileId)
|
|
except (GAPI.notFound, GAPI.fileNotFound, GAPI.invalid, GAPI.noListTeamDrivesAdministratorPrivilege) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileId], str(e), i, count)
|
|
return (None, False)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return (None, False)
|
|
return (fileTree, True)
|
|
|
|
def extendFileTree(fileTree, feed, DLP, stripCRsFromName):
|
|
for f_file in feed:
|
|
if DLP and (not DLP.CheckOnlySharedDrives(f_file) or not DLP.CheckExcludeTrashed(f_file)):
|
|
continue
|
|
if stripCRsFromName:
|
|
f_file['name'] = _stripControlCharsFromName(f_file['name'])
|
|
fileId = f_file['id']
|
|
if not f_file.get('parents', []):
|
|
if not f_file.get('driveId'):
|
|
if f_file['mimeType'] == MIMETYPE_GA_FOLDER and f_file['name'] == MY_DRIVE:
|
|
f_file['parents'] = []
|
|
else:
|
|
f_file['parents'] = [ORPHANS] if f_file.get('ownedByMe', False) and 'sharedWithMeTime' not in f_file else [SHARED_WITHME]
|
|
else:
|
|
f_file['parents'] = [SHARED_DRIVES] if 'sharedWithMeTime' not in f_file else [SHARED_WITHME]
|
|
if fileId not in fileTree:
|
|
fileTree[fileId] = {'info': f_file, 'children': []}
|
|
else:
|
|
fileTree[fileId]['info'] = f_file
|
|
for parentId in f_file['parents']:
|
|
if parentId not in fileTree:
|
|
fileTree[parentId] = {'info': {'id': parentId, 'name': parentId, 'mimeType': MIMETYPE_GA_FOLDER}, 'children': []}
|
|
fileTree[parentId]['children'].append(fileId)
|
|
|
|
def extendFileTreeParents(drive, fileTree, fields):
|
|
def _followParent(fileId):
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields=fields, supportsAllDrives=True)
|
|
if not result.get('parents', []):
|
|
if not result.get('driveId'):
|
|
result['parents'] = [ORPHANS] if result.get('ownedByMe', False) and 'sharedWithMeTime' not in result else [SHARED_WITHME]
|
|
else:
|
|
if result['name'] == TEAM_DRIVE:
|
|
result['name'] = _getSharedDriveNameFromId(result['driveId'])
|
|
result['parents'] = [SHARED_DRIVES] if 'sharedWithMeTime' not in result else [SHARED_WITHME]
|
|
fileTree[fileId]['info'] = result
|
|
fileTree[fileId]['info']['noDisplay'] = True
|
|
for parentId in result['parents']:
|
|
if parentId not in fileTree:
|
|
fileTree[parentId] = {'info': {'id': parentId, 'name': parentId, 'mimeType': MIMETYPE_GA_FOLDER}, 'children': []}
|
|
_followParent(parentId)
|
|
fileTree[parentId]['children'].append(fileId)
|
|
except (GAPI.fileNotFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
fileTree[fileId] = {'info': {'id': fileId, 'name': fileId, 'mimeType': MIMETYPE_GA_FOLDER, 'parents': [], 'noDisplay': True},
|
|
'children': []}
|
|
|
|
for fileId in list(fileTree):
|
|
f_file = fileTree[fileId]
|
|
if 'parents' not in f_file['info'] and not f_file.get('noParents', False):
|
|
f_file['info']['noDisplay'] = True
|
|
_followParent(fileId)
|
|
|
|
def buildFileTree(feed, drive):
|
|
fileTree = {
|
|
ORPHANS: {'info': {'id': ORPHANS, 'name': ORPHANS, 'mimeType': MIMETYPE_GA_FOLDER, 'ownedByMe': True}, 'children': []},
|
|
}
|
|
try:
|
|
f_file = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=ROOT, fields=FILEPATH_FIELDS)
|
|
fileTree[f_file['id']] = {'info': f_file, 'children': []}
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
|
|
GAPI.notFound, GAPI.invalid, GAPI.noListTeamDrivesAdministratorPrivilege):
|
|
pass
|
|
for f_file in feed:
|
|
if 'parents' not in f_file:
|
|
f_file['parents'] = []
|
|
fileId = f_file['id']
|
|
if fileId not in fileTree:
|
|
fileTree[fileId] = {'info': f_file, 'children': []}
|
|
else:
|
|
fileTree[fileId]['info'] = f_file
|
|
parents = f_file['parents']
|
|
if not parents:
|
|
parents = [ORPHANS]
|
|
for parentId in parents:
|
|
if parentId not in fileTree:
|
|
fileTree[parentId] = {'info': {'id': parentId, 'name': parentId, 'mimeType': MIMETYPE_GA_FOLDER}, 'children': []}
|
|
fileTree[parentId]['children'].append(fileId)
|
|
return fileTree
|
|
|
|
def addFilePathsToRow(drive, fileTree, fileEntryInfo, filePathInfo, csvPF, row,
|
|
fullpath=False, showDepth=False, folderPathOnly=False):
|
|
_, paths, maxDepth = getFilePaths(drive, fileTree, fileEntryInfo, filePathInfo,
|
|
fullpath=fullpath, showDepth=showDepth, folderPathOnly=folderPathOnly)
|
|
kcount = len(paths)
|
|
if showDepth:
|
|
row['depth'] = maxDepth
|
|
row['paths'] = kcount
|
|
k = 0
|
|
for path in sorted(paths):
|
|
key = f'path{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{k}'
|
|
csvPF.AddTitles(key)
|
|
if GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL] and (path.find('\n') >= 0 or path.find('\r') >= 0):
|
|
row[key] = escapeCRsNLs(path)
|
|
else:
|
|
row[key] = path
|
|
k += 1
|
|
|
|
def addFilePathsToInfo(drive, fileTree, fileEntryInfo, filePathInfo, addParentsToTree=False, folderPathOnly=False):
|
|
_, paths, _ = getFilePaths(drive, fileTree, fileEntryInfo, filePathInfo, addParentsToTree=addParentsToTree,
|
|
showDepth=False, folderPathOnly=folderPathOnly)
|
|
fileEntryInfo['paths'] = []
|
|
for path in sorted(paths):
|
|
if GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL] and (path.find('\n') >= 0 or path.find('\r') >= 0):
|
|
fileEntryInfo['paths'].append(escapeCRsNLs(path))
|
|
else:
|
|
fileEntryInfo['paths'].append(path)
|
|
|
|
def _validateACLOwnerType(location, body):
|
|
if body.get('role', '') == 'owner' and body['type'] != 'user':
|
|
Cmd.SetLocation(location)
|
|
usageErrorExit(Msg.INVALID_PERMISSION_ATTRIBUTE_TYPE.format(f'role {body["role"]}', body['type']))
|
|
|
|
def _validateACLAttributes(myarg, location, body, field, validTypes):
|
|
if field in body and body['type'] not in validTypes:
|
|
Cmd.SetLocation(location-1)
|
|
usageErrorExit(Msg.INVALID_PERMISSION_ATTRIBUTE_TYPE.format(myarg, body['type']))
|
|
|
|
def _validatePermissionOwnerType(location, body):
|
|
if 'role' in body:
|
|
badTypes = body['type']-{'user'}
|
|
if 'owner' in body['role'] and badTypes:
|
|
Cmd.SetLocation(location)
|
|
usageErrorExit(Msg.INVALID_PERMISSION_ATTRIBUTE_TYPE.format('role owner', ','.join(badTypes)))
|
|
|
|
def _validatePermissionAttributes(myarg, location, body, field, validTypes):
|
|
if field in body:
|
|
badTypes = body['type']-validTypes
|
|
if badTypes:
|
|
Cmd.SetLocation(location-1)
|
|
usageErrorExit(Msg.INVALID_PERMISSION_ATTRIBUTE_TYPE.format(myarg, ','.join(badTypes)))
|
|
|
|
DRIVEFILE_ACL_ROLES_MAP = {
|
|
'commenter': 'commenter',
|
|
'contentmanager': 'fileOrganizer',
|
|
'contributor': 'writer',
|
|
'editor': 'writer',
|
|
'fileorganizer': 'fileOrganizer',
|
|
'fileorganiser': 'fileOrganizer',
|
|
'manager': 'organizer',
|
|
'organizer': 'organizer',
|
|
'organiser': 'organizer',
|
|
'owner': 'owner',
|
|
'read': 'reader',
|
|
'reader': 'reader',
|
|
'viewer': 'reader',
|
|
'writer': 'writer',
|
|
}
|
|
|
|
DRIVEFILE_ACL_PERMISSION_TYPES = ['anyone', 'domain', 'group', 'user'] # anyone must be first element
|
|
DRIVEFILE_ACL_PERMISSION_DETAILS_TYPES = ['file', 'member']
|
|
|
|
class PermissionMatch():
|
|
_PERMISSION_MATCH_ACTION_MAP = {'process': True, 'skip': False}
|
|
_PERMISSION_MATCH_MODE_MAP = {'or': True, 'and': False}
|
|
|
|
def __init__(self):
|
|
self.permissionMatches = []
|
|
self.permissionMatchKeep = self.permissionMatchOr = True
|
|
self.permissionFields = set()
|
|
self.clearDefaultMatch = False
|
|
|
|
def GetMatch(self):
|
|
startEndTime = StartEndTime('expirationstart', 'expirationend')
|
|
roleLocation = withLinkLocation = expirationStartLocation = expirationEndLocation = deletedLocation = None
|
|
requiredMatch = not checkArgumentPresent('not')
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'type', 'nottype'}:
|
|
body[myarg] = set()
|
|
body[myarg].add(getChoice(DRIVEFILE_ACL_PERMISSION_TYPES))
|
|
self.permissionFields.add('type')
|
|
elif myarg in {'typelist', 'nottypelist'}:
|
|
arg = 'type' if myarg == 'typelist' else 'nottype'
|
|
body[arg] = set()
|
|
for ptype in getString(Cmd.OB_PERMISSION_TYPE_LIST).lower().replace(',', ' ').split():
|
|
if ptype in DRIVEFILE_ACL_PERMISSION_TYPES:
|
|
body[arg].add(ptype)
|
|
else:
|
|
invalidChoiceExit(ptype, DRIVEFILE_ACL_PERMISSION_TYPES, True)
|
|
self.permissionFields.add('type')
|
|
elif myarg in {'role', 'notrole'}:
|
|
roleLocation = Cmd.Location()
|
|
body[myarg] = set()
|
|
body[myarg].add(getChoice(DRIVEFILE_ACL_ROLES_MAP, mapChoice=True))
|
|
self.permissionFields.add('role')
|
|
elif myarg in {'rolelist', 'notrolelist'}:
|
|
arg = 'role' if myarg == 'rolelist' else 'notrole'
|
|
body[arg] = set()
|
|
roleLocation = Cmd.Location()
|
|
for prole in getString(Cmd.OB_PERMISSION_ROLE_LIST).lower().replace(',', ' ').split():
|
|
if prole in DRIVEFILE_ACL_ROLES_MAP:
|
|
body[arg].add(DRIVEFILE_ACL_ROLES_MAP[prole])
|
|
else:
|
|
invalidChoiceExit(prole, DRIVEFILE_ACL_ROLES_MAP, True)
|
|
self.permissionFields.add('role')
|
|
elif myarg == 'emailaddress':
|
|
body['emailAddress'] = getREPattern(re.IGNORECASE)
|
|
self.permissionFields.add('emailAddress')
|
|
elif myarg == 'emailaddresslist':
|
|
body[myarg] = set(getString(Cmd.OB_EMAIL_ADDRESS_LIST).replace(',', ' ').lower().split())
|
|
self.permissionFields.add('emailAddress')
|
|
elif myarg == 'permissionidlist':
|
|
body[myarg] = set(getString(Cmd.OB_PERMISSION_ID_LIST).replace(',', ' ').split())
|
|
self.permissionFields.add('id')
|
|
elif myarg in {'domain', 'notdomain'}:
|
|
body[myarg] = getREPattern(re.IGNORECASE)
|
|
self.permissionFields.add('domain')
|
|
self.permissionFields.add('emailAddress')
|
|
elif myarg in {'domainlist', 'notdomainlist'}:
|
|
body[myarg] = set(getString(Cmd.OB_DOMAIN_NAME_LIST).replace(',', ' ').lower().split())
|
|
self.permissionFields.add('domain')
|
|
self.permissionFields.add('emailAddress')
|
|
elif myarg == 'withlink':
|
|
withLinkLocation = Cmd.Location()
|
|
body['allowFileDiscovery'] = not getBoolean()
|
|
self.permissionFields.add('allowFileDiscovery')
|
|
elif myarg in {'allowfilediscovery', 'discoverable'}:
|
|
withLinkLocation = Cmd.Location()
|
|
body['allowFileDiscovery'] = getBoolean()
|
|
self.permissionFields.add('allowFileDiscovery')
|
|
elif myarg in {'name', 'displayname'}:
|
|
body['displayName'] = getREPattern(re.IGNORECASE)
|
|
self.permissionFields.add('displayName')
|
|
elif myarg == 'expirationstart':
|
|
expirationStartLocation = Cmd.Location()
|
|
startEndTime.Get(myarg)
|
|
body[myarg] = startEndTime.startDateTime
|
|
self.permissionFields.add('expirationTime')
|
|
elif myarg == 'expirationend':
|
|
expirationEndLocation = Cmd.Location()
|
|
startEndTime.Get(myarg)
|
|
body[myarg] = startEndTime.endDateTime
|
|
self.permissionFields.add('expirationTime')
|
|
elif myarg == 'deleted':
|
|
deletedLocation = Cmd.Location()
|
|
body[myarg] = getBoolean()
|
|
self.permissionFields.add('deleted')
|
|
elif myarg == 'inherited':
|
|
body[myarg] = getBoolean()
|
|
self.permissionFields.add('permissionDetails')
|
|
elif myarg == 'permtype':
|
|
body['permissionType'] = getChoice(DRIVEFILE_ACL_PERMISSION_DETAILS_TYPES)
|
|
self.permissionFields.add('permissionDetails')
|
|
elif myarg in {'em', 'endmatch'}:
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
if self.clearDefaultMatch:
|
|
self.permissionMatches = []
|
|
self.clearDefaultMatch = False
|
|
if body:
|
|
if 'type' in body:
|
|
_validatePermissionOwnerType(roleLocation, body)
|
|
_validatePermissionAttributes('allowfilediscovery/withlink', withLinkLocation, body, 'allowFileDiscovery', {'anyone', 'domain'})
|
|
_validatePermissionAttributes('expirationstart', expirationStartLocation, body, 'expirationstart', {'user', 'group'})
|
|
_validatePermissionAttributes('expirationend', expirationEndLocation, body, 'expirationend', {'user', 'group'})
|
|
_validatePermissionAttributes('deleted', deletedLocation, body, 'deleted', {'user', 'group'})
|
|
self.permissionMatches.append((requiredMatch, body))
|
|
|
|
def SetDefaultMatch(self, requiredMatch, body):
|
|
self.clearDefaultMatch = True
|
|
self.permissionMatches.append((requiredMatch, body))
|
|
|
|
def ProcessArgument(self, myarg):
|
|
if myarg in {'pm', 'permissionmatch'}:
|
|
self.GetMatch()
|
|
elif myarg in {'pma', 'permissionmatchaction'}:
|
|
self.permissionMatchKeep = getChoice(PermissionMatch._PERMISSION_MATCH_ACTION_MAP, mapChoice=True)
|
|
elif myarg in {'pmm', 'permissionmatchmode'}:
|
|
self.permissionMatchOr = getChoice(PermissionMatch._PERMISSION_MATCH_MODE_MAP, mapChoice=True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
@staticmethod
|
|
def CheckPermissionMatch(permission, permissionMatch):
|
|
match = False
|
|
for field, value in iter(permissionMatch[1].items()):
|
|
if field in {'type', 'role'}:
|
|
if permission.get(field, '') not in value:
|
|
break
|
|
elif field == 'nottype':
|
|
if permission.get('type', '') in value:
|
|
break
|
|
elif field == 'notrole':
|
|
if permission.get('role', '') in value:
|
|
break
|
|
elif field in {'allowFileDiscovery', 'deleted'}:
|
|
if value != permission.get(field, False):
|
|
break
|
|
elif field == 'inherited':
|
|
if 'permissionDetails' in permission:
|
|
if value != permission['permissionDetails'][0].get(field, False):
|
|
break
|
|
else:
|
|
break
|
|
elif field == 'permissionType':
|
|
if 'permissionDetails' in permission:
|
|
if value != permission['permissionDetails'][0].get(field, ''):
|
|
break
|
|
else:
|
|
break
|
|
elif field in {'expirationstart', 'expirationend'}:
|
|
if 'expirationTime' in permission:
|
|
expirationDateTime, _ = iso8601.parse_date(permission['expirationTime'])
|
|
if field == 'expirationstart':
|
|
if expirationDateTime < value:
|
|
break
|
|
else:
|
|
if expirationDateTime > value:
|
|
break
|
|
else:
|
|
break
|
|
elif field == 'emailaddresslist':
|
|
emailAddress = permission.get('emailAddress')
|
|
if emailAddress:
|
|
if emailAddress.lower() not in value:
|
|
break
|
|
else:
|
|
break
|
|
elif field == 'permissionidlist':
|
|
permissionId = permission.get('id')
|
|
if permissionId:
|
|
if permissionId not in value:
|
|
break
|
|
else:
|
|
break
|
|
elif field not in {'domain', 'notdomain', 'domainlist', 'notdomainlist'}:
|
|
if not value.match(permission.get(field, '')):
|
|
break
|
|
else:
|
|
if 'domain' in permission:
|
|
domain = permission['domain'].lower()
|
|
elif 'emailAddress' in permission and permission['emailAddress']:
|
|
_, domain = splitEmailAddress(permission['emailAddress'].lower())
|
|
else:
|
|
break
|
|
if ((field == 'domain' and not value.match(domain)) or
|
|
(field == 'notdomain' and value.match(domain)) or
|
|
(field == 'domainlist' and domain not in value) or
|
|
(field == 'notdomainlist' and domain in value)):
|
|
break
|
|
else:
|
|
match = True
|
|
return match == permissionMatch[0]
|
|
|
|
def GetMatchingPermissions(self, permissions):
|
|
if not self.permissionMatches:
|
|
return permissions
|
|
matchingPermissions = []
|
|
for permission in permissions:
|
|
requiredMatches = 1 if self.permissionMatchOr else len(self.permissionMatches)
|
|
for permissionMatch in self.permissionMatches:
|
|
if self.CheckPermissionMatch(permission, permissionMatch):
|
|
requiredMatches -= 1
|
|
if requiredMatches == 0:
|
|
if self.permissionMatchKeep:
|
|
matchingPermissions.append(permission)
|
|
break
|
|
else:
|
|
if not self.permissionMatchKeep:
|
|
matchingPermissions.append(permission)
|
|
return matchingPermissions
|
|
|
|
def CheckPermissionMatches(self, permissions):
|
|
if not self.permissionMatches:
|
|
return True
|
|
requiredMatches = 1 if self.permissionMatchOr else len(self.permissionMatches)
|
|
for permission in permissions:
|
|
for permissionMatch in self.permissionMatches:
|
|
if self.CheckPermissionMatch(permission, permissionMatch):
|
|
requiredMatches -= 1
|
|
if requiredMatches == 0:
|
|
return self.permissionMatchKeep
|
|
return not self.permissionMatchKeep
|
|
|
|
def noFileSelectFileIdEntity(fileIdEntity):
|
|
return (not fileIdEntity
|
|
or (not fileIdEntity['dict']
|
|
and not fileIdEntity['query']
|
|
and not fileIdEntity['shareddrivefilequery']
|
|
and not fileIdEntity['list']))
|
|
|
|
SHOW_OWNED_BY_CHOICE_MAP = {'any': None, 'me': True, 'others': False}
|
|
|
|
class DriveListParameters():
|
|
def __init__(self, myargOptions):
|
|
self.PM = PermissionMatch()
|
|
self.myargOptions = myargOptions
|
|
self.checkLocation = None
|
|
self.excludeTrashed = False
|
|
self.filenameMatchPattern = None
|
|
self.getSharedDriveNames = False
|
|
self.kwargs = {}
|
|
self.fileIdEntity = {}
|
|
self.locationFileIds = []
|
|
self.locationSet = False
|
|
self.maxItems = 0
|
|
self.mimeTypeCheck = MimeTypeCheck()
|
|
self.maximumFileSize = None
|
|
self.minimumFileSize = None
|
|
self.onlySharedDrives = False
|
|
self.queryTimes = {}
|
|
self.showOwnedBy = True
|
|
self.showSharedByMe = None
|
|
|
|
SHOW_SHARED_BY_ME_CHOICE_MAP = {'any': None, 'true': True, 'false': False}
|
|
|
|
def ProcessArgument(self, myarg, fileIdEntity):
|
|
if myarg == 'maxfiles':
|
|
self.maxItems = getInteger(minVal=0)
|
|
elif myarg == 'maximumfilesize':
|
|
self.maximumFileSize = getInteger(minVal=0)
|
|
elif myarg == 'minimumfilesize':
|
|
self.minimumFileSize = getInteger(minVal=0)
|
|
elif myarg == 'showsharedbyme':
|
|
self.showSharedByMe = getChoice(self.SHOW_SHARED_BY_ME_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'filenamematchpattern':
|
|
self.filenameMatchPattern = getREPattern(re.IGNORECASE)
|
|
elif self.PM.ProcessArgument(myarg):
|
|
pass
|
|
elif myarg == 'anyowner':
|
|
self.showOwnedBy = None
|
|
self.UpdateAnyOwnerQuery()
|
|
elif myarg == 'showownedby':
|
|
self.showOwnedBy = getChoice(SHOW_OWNED_BY_CHOICE_MAP, mapChoice=True)
|
|
self.UpdateQueryWithShowOwnedBy()
|
|
elif myarg == 'showmimetype':
|
|
self.mimeTypeCheck.Get()
|
|
if self.myargOptions['mimeTypeInQuery']:
|
|
self.AppendToQuery(self.mimeTypeCheck.AddMimeTypeToQuery(self.fileIdEntity.get('query', '')))
|
|
elif myarg == 'excludetrashed':
|
|
self.excludeTrashed = True
|
|
elif myarg.startswith('querytime'):
|
|
self.queryTimes[myarg] = getTimeOrDeltaFromNow()
|
|
elif noFileSelectFileIdEntity(fileIdEntity):
|
|
if self.myargOptions['allowQuery'] and myarg == 'query':
|
|
self.AppendToQuery(getString(Cmd.OB_QUERY))
|
|
elif self.myargOptions['allowQuery'] and myarg.startswith('query:'):
|
|
self.AppendToQuery(Cmd.Previous().strip()[6:])
|
|
elif self.myargOptions['allowQuery'] and myarg == 'fullquery':
|
|
self.SetQuery(getString(Cmd.OB_QUERY, minLen=0))
|
|
elif self.myargOptions['allowQuery'] and myarg in QUERY_SHORTCUTS_MAP:
|
|
self.UpdateAnyOwnerQuery()
|
|
self.AppendToQuery(QUERY_SHORTCUTS_MAP[myarg])
|
|
elif self.myargOptions['allowChoose'] and myarg == 'choose':
|
|
myarg = checkGetArgument()
|
|
if myarg in DRIVE_BY_NAME_CHOICE_MAP:
|
|
self.SetQuery(DRIVE_BY_NAME_CHOICE_MAP[myarg].format(getEscapedDriveFileName()))
|
|
elif myarg in LOCATION_CHOICE_MAP:
|
|
self.locationSet = True
|
|
self.SetLocation(LOCATION_CHOICE_MAP[myarg])
|
|
elif myarg.find(':') > 0:
|
|
kw, value = myarg.split(':', 1)
|
|
if kw in DRIVE_BY_NAME_CHOICE_MAP:
|
|
self.SetQuery(DRIVE_BY_NAME_CHOICE_MAP[kw].format(escapeDriveFileName(value)))
|
|
else:
|
|
invalidChoiceExit(myarg, list(DRIVE_BY_NAME_CHOICE_MAP), True)
|
|
else:
|
|
invalidChoiceExit(myarg, list(DRIVE_BY_NAME_CHOICE_MAP)+list(LOCATION_CHOICE_MAP), True)
|
|
elif self.myargOptions['allowCorpora'] and myarg == 'corpora':
|
|
corpora = getChoice(CORPORA_CHOICE_MAP)
|
|
self.kwargs['corpora'] = CORPORA_CHOICE_MAP[corpora]
|
|
self.kwargs['includeItemsFromAllDrives'] = self.kwargs['supportsAllDrives'] = True
|
|
self.onlySharedDrives = corpora in {'onlyshareddrives', 'onlyteamdrives'}
|
|
self.getSharedDriveNames = True
|
|
self.UpdateAnyOwnerQuery()
|
|
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['alldrives' if not self.onlySharedDrives else 'onlyshareddrives'])
|
|
else:
|
|
return False
|
|
else:
|
|
if (myarg == 'query' or
|
|
myarg.startswith('query:') or
|
|
myarg == 'fullquery' or
|
|
myarg in QUERY_SHORTCUTS_MAP or
|
|
myarg in DRIVE_BY_NAME_CHOICE_MAP):
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('select', myarg))
|
|
return False
|
|
return True
|
|
|
|
def InitDriveIdEntity(self):
|
|
if not self.fileIdEntity:
|
|
self.fileIdEntity = initDriveFileEntity()
|
|
self.fileIdEntity['query'] = ME_IN_OWNERS
|
|
|
|
def AppendToQuery(self, query):
|
|
self.InitDriveIdEntity()
|
|
if self.fileIdEntity['query']:
|
|
self.fileIdEntity['query'] += ' and ('+query+')'
|
|
else:
|
|
self.fileIdEntity['query'] = query
|
|
|
|
def SetQuery(self, query):
|
|
self.InitDriveIdEntity()
|
|
self.fileIdEntity['query'] = query
|
|
|
|
def UpdateAnyOwnerQuery(self):
|
|
self.InitDriveIdEntity()
|
|
self.fileIdEntity['query'] = _updateAnyOwnerQuery(self.fileIdEntity['query'])
|
|
|
|
def UpdateQueryWithShowOwnedBy(self):
|
|
self.InitDriveIdEntity()
|
|
if self.showOwnedBy is None:
|
|
self.fileIdEntity['query'] = _updateAnyOwnerQuery(self.fileIdEntity['query'])
|
|
elif not self.showOwnedBy:
|
|
if self.fileIdEntity['query'].find(NOT_ME_IN_OWNERS) >= 0:
|
|
pass
|
|
else:
|
|
self.fileIdEntity['query'] = _stripMeInOwners(self.fileIdEntity['query'])
|
|
if self.fileIdEntity['query']:
|
|
self.fileIdEntity['query'] = NOT_ME_IN_OWNERS_AND+self.fileIdEntity['query']
|
|
else:
|
|
self.fileIdEntity['query'] = NOT_ME_IN_OWNERS
|
|
else:
|
|
if self.fileIdEntity['query'].find(ME_IN_OWNERS) >= 0:
|
|
pass
|
|
else:
|
|
self.fileIdEntity['query'] = _stripNotMeInOwners(self.fileIdEntity['query'])
|
|
if self.fileIdEntity['query']:
|
|
self.fileIdEntity['query'] = ME_IN_OWNERS_AND+self.fileIdEntity['query']
|
|
else:
|
|
self.fileIdEntity['query'] = ME_IN_OWNERS
|
|
|
|
def CheckShowOwnedBy(self, fileInfo):
|
|
return self.showOwnedBy is None or fileInfo.get('ownedByMe', self.showOwnedBy) == self.showOwnedBy
|
|
|
|
def CheckShowSharedByMe(self, fileInfo):
|
|
return self.showSharedByMe is None or (fileInfo.get('shared', self.showSharedByMe) == self.showSharedByMe and fileInfo.get('ownedByMe', False))
|
|
|
|
def SetLocationFileIDsList(self, location):
|
|
self.locationFileIds = location['fileids']
|
|
self.checkLocation = location['location']
|
|
self.InitDriveIdEntity()
|
|
cleanFileIDsList(self.fileIdEntity, self.locationFileIds)
|
|
|
|
def SetLocation(self, location):
|
|
self.SetLocationFileIDsList(location)
|
|
if self.checkLocation in {LOCATION_ALL_DRIVES, LOCATION_ONLY_SHARED_DRIVES}:
|
|
self.kwargs = {'corpora': 'allDrives', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
self.showOwnedBy = None
|
|
self.fileIdEntity['query'] = ''
|
|
self.onlySharedDrives = self.checkLocation == LOCATION_ONLY_SHARED_DRIVES
|
|
elif location['setShowOwnedBy']:
|
|
self.showOwnedBy = location['owner']
|
|
if self.showOwnedBy is None:
|
|
self.fileIdEntity['query'] = ''
|
|
elif self.showOwnedBy:
|
|
self.fileIdEntity['query'] = ME_IN_OWNERS
|
|
else:
|
|
self.fileIdEntity['query'] = NOT_ME_IN_OWNERS
|
|
else:
|
|
if self.fileIdEntity['query'].find(NOT_ME_IN_OWNERS) >= 0:
|
|
self.fileIdEntity['query'] = NOT_ME_IN_OWNERS
|
|
elif self.fileIdEntity['query'].find(ME_IN_OWNERS) >= 0:
|
|
self.fileIdEntity['query'] = ME_IN_OWNERS
|
|
|
|
def SetShowOwnedBy(self, showOwnedBy):
|
|
self.showOwnedBy = showOwnedBy
|
|
|
|
def GetFileIdEntity(self):
|
|
if not self.fileIdEntity:
|
|
self.fileIdEntity = initDriveFileEntity()
|
|
self.fileIdEntity['query'] = ME_IN_OWNERS
|
|
return self.fileIdEntity
|
|
|
|
def AddMimeTypeToQuery(self):
|
|
if not self.fileIdEntity:
|
|
self.fileIdEntity = initDriveFileEntity()
|
|
if self.mimeTypeCheck.mimeTypes or self.mimeTypeCheck.category:
|
|
self.fileIdEntity['query'] = self.mimeTypeCheck.AddMimeTypeToQuery(self.fileIdEntity['query'])
|
|
|
|
def Finalize(self, fileIdEntity):
|
|
self.fileIdEntity.setdefault('query', '')
|
|
if self.excludeTrashed:
|
|
self.AppendToQuery('trashed=false')
|
|
if self.fileIdEntity['query']:
|
|
for queryTimeName, queryTimeValue in iter(self.queryTimes.items()):
|
|
self.fileIdEntity['query'] = self.fileIdEntity['query'].replace(f'#{queryTimeName}#', queryTimeValue)
|
|
self.fileIdEntity['query'] = _mapDrive2QueryToDrive3(self.fileIdEntity['query'])
|
|
if not fileIdEntity.get('shareddrive'):
|
|
if self.fileIdEntity['query']:
|
|
if self.fileIdEntity['query'].find(NOT_ME_IN_OWNERS) >= 0 or (not self.showOwnedBy and self.showOwnedBy is not None):
|
|
if not self.locationFileIds:
|
|
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['ownedbyothers'])
|
|
self.SetShowOwnedBy(False)
|
|
elif self.fileIdEntity['query'].find(ME_IN_OWNERS) >= 0 or self.showOwnedBy:
|
|
if not self.locationFileIds:
|
|
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['ownedbyme'])
|
|
self.SetShowOwnedBy(True)
|
|
else:
|
|
if not self.locationFileIds:
|
|
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['ownedbyany'])
|
|
self.SetShowOwnedBy(None)
|
|
else:
|
|
if not self.locationFileIds:
|
|
self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['ownedbyany'])
|
|
self.SetShowOwnedBy(None)
|
|
else:
|
|
self.UpdateAnyOwnerQuery()
|
|
# if not self.locationFileIds:
|
|
# self.SetLocationFileIDsList(LOCATION_CHOICE_MAP['onlyshareddrives'])
|
|
self.SetShowOwnedBy(None)
|
|
|
|
def GetLocationFileIdsFromTree(self, fileTree, fileIdEntity):
|
|
cleanList = []
|
|
for fileId in self.locationFileIds:
|
|
if fileId == ROOT or (fileId in fileTree and fileTree[fileId]['children']):
|
|
cleanList.append(fileId)
|
|
cleanFileIDsList(fileIdEntity, cleanList)
|
|
|
|
def CheckExcludeTrashed(self, fileInfo):
|
|
return not self.excludeTrashed or not fileInfo.get('trashed', False)
|
|
|
|
def CheckFilenameMatch(self, fileInfo):
|
|
return not self.filenameMatchPattern or self.filenameMatchPattern.match(fileInfo['name'])
|
|
|
|
def CheckMimeType(self, fileInfo):
|
|
return self.mimeTypeCheck.Check(fileInfo)
|
|
|
|
def CheckFileSize(self, fileInfo, sizeField):
|
|
size = int(fileInfo.get(sizeField, '0'))
|
|
if self.minimumFileSize is not None and size < self.minimumFileSize:
|
|
return False
|
|
if self.maximumFileSize is not None and size > self.maximumFileSize:
|
|
return False
|
|
return True
|
|
|
|
def CheckOnlySharedDrives(self, fileInfo):
|
|
return not self.onlySharedDrives or fileInfo.get('driveId') is not None
|
|
|
|
def CheckFilePermissionMatches(self, fileInfo):
|
|
return self.PM.CheckPermissionMatches(fileInfo.get('permissions', []))
|
|
|
|
def GetFileMatchingPermission(self, fileInfo):
|
|
return self.PM.GetMatchingPermissions(fileInfo.get('permissions', []))
|
|
|
|
def _getGettingEntity(user, fileIdEntity):
|
|
driveId = fileIdEntity.get('shareddrive', {}).get('driveId', None)
|
|
if not driveId:
|
|
return user
|
|
return f"{user} on {Ent.Singular(Ent.SHAREDDRIVE_ID)}: {driveId}"
|
|
|
|
OWNED_BY_ME_FIELDS_TITLES = ['ownedByMe']
|
|
FILELIST_FIELDS_TITLES = ['id', 'name', 'mimeType', 'parents']
|
|
DRIVE_INDEXED_TITLES = ['parents', 'path', 'permissions']
|
|
CHECK_LOCATION_FIELDS_TITLES = ['driveId', 'id', 'mimeType', 'ownedByMe', 'parents', 'sharedWithMeTime', 'shared']
|
|
|
|
FILECOUNT_SUMMARY_NONE = 0
|
|
FILECOUNT_SUMMARY_ONLY = -1
|
|
FILECOUNT_SUMMARY_PLUS = 1
|
|
FILECOUNT_SUMMARY_CHOICE_MAP = {
|
|
'none': FILECOUNT_SUMMARY_NONE,
|
|
'only': FILECOUNT_SUMMARY_ONLY,
|
|
'plus': FILECOUNT_SUMMARY_PLUS
|
|
}
|
|
FILECOUNT_SUMMARY_USER = 'Summary'
|
|
|
|
SIZE_FIELD_CHOICE_MAP = {
|
|
'size': 'size',
|
|
'quotabytesused': 'quotaBytesUsed'
|
|
}
|
|
|
|
# gam <UserTypeEntity> print filelist [todrive <ToDriveAttribute>*]
|
|
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
|
|
# [continueoninvalidquery [<Boolean>]]
|
|
# [choose <DriveFileNameEntity>|<DriveFileEntityShortcut>]
|
|
# [corpora <CorporaAttribute>]
|
|
# [select <DriveFileEntity> [selectsubquery <QueryDriveFile>]
|
|
# [(norecursion [<Boolean>])|(depth <Number>)] [showparent]]
|
|
# [anyowner|(showownedby any|me|others)]
|
|
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>] [mimetypeinquery [<Boolean>]]
|
|
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
|
|
# [filenamematchpattern <REMatchPattern>]
|
|
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>] [pmfilter] [oneitemperrow]
|
|
# [excludetrashed]
|
|
# [maxfiles <Integer>] [nodataheaders <String>]
|
|
# [countsonly [summary none|only|plus] [summaryuser <String>]
|
|
# [showsource] [showsize] [showmimetypesize]]
|
|
# [countsrowfilter]
|
|
# [filepath|fullpath [folderpathonly [<Boolean>]] [pathdelimiter <Character>] [addpathstojson] [showdepth]] [buildtree]
|
|
# [allfields|<DriveFieldName>*|(fields <DriveFieldNameList>)]
|
|
# [showdrivename] [showshareddrivepermissions]
|
|
# (showlabels details|ids)|(includelabels <DriveLabelIDList>)]
|
|
# [showparentsidsaslist] [showpermissionslast]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])* [delimiter <Character>]
|
|
# [stripcrsfromname]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
def printFileList(users):
|
|
def _setSelectionFields():
|
|
if fileIdEntity or filepath:
|
|
_setSkipObjects(skipObjects, FILELIST_FIELDS_TITLES, DFF.fieldsList)
|
|
if DLP.showOwnedBy is not None:
|
|
_setSkipObjects(skipObjects, OWNED_BY_ME_FIELDS_TITLES, DFF.fieldsList)
|
|
if DLP.showSharedByMe is not None or DLP.checkLocation is not None:
|
|
_setSkipObjects(skipObjects, CHECK_LOCATION_FIELDS_TITLES, DFF.fieldsList)
|
|
if DLP.mimeTypeCheck.mimeTypes:
|
|
_setSkipObjects(skipObjects, ['mimeType'], DFF.fieldsList)
|
|
if countsOnly:
|
|
skipObjects.discard('mimeType')
|
|
if 'mimeType' not in DFF.fieldsList:
|
|
DFF.fieldsList.append('mimeType')
|
|
skipObjects.discard(sizeField)
|
|
if showSize and sizeField not in DFF.fieldsList:
|
|
DFF.fieldsList.append(sizeField)
|
|
if (DLP.minimumFileSize is not None) or (DLP.maximumFileSize is not None):
|
|
_setSkipObjects(skipObjects, [sizeField], DFF.fieldsList)
|
|
if DLP.filenameMatchPattern or showParent:
|
|
_setSkipObjects(skipObjects, ['name'], DFF.fieldsList)
|
|
if DLP.excludeTrashed:
|
|
_setSkipObjects(skipObjects, ['trashed'], DFF.fieldsList)
|
|
if DLP.PM.permissionMatches:
|
|
for field in DFF.fieldsList:
|
|
if field.startswith('permissions'):
|
|
break
|
|
else:
|
|
skipObjects.add('permissions')
|
|
if 'permissions' not in DFF.fieldsList:
|
|
for field in DLP.PM.permissionFields:
|
|
permfield = 'permissions.'+field
|
|
if permfield not in DFF.fieldsList:
|
|
DFF.fieldsList.append(permfield)
|
|
if DLP.onlySharedDrives or getPermissionsForSharedDrives or DFF.showSharedDriveNames:
|
|
_setSkipObjects(skipObjects, ['driveId'], DFF.fieldsList)
|
|
|
|
def _printFileInfoRow(baserow, fileInfo):
|
|
row = baserow.copy()
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(flattenJSON(fileInfo, flattened=row, skipObjects=skipObjects, timeObjects=timeObjects,
|
|
simpleLists=simpleLists, delimiter=delimiter))
|
|
else:
|
|
row['JSON'] = json.dumps(cleanJSON(fileInfo, skipObjects=skipObjects, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowTitlesJSONNoFilter(row)
|
|
|
|
def _printFileInfo(drive, user, f_file, cleanFileName):
|
|
nonlocal getSharedDriveACLsCount, getSharedDriveACLsCountMsg
|
|
driveId = f_file.get('driveId')
|
|
checkSharedDrivePermissions = getPermissionsForSharedDrives and driveId and 'permissions' not in f_file
|
|
if (f_file.get('noDisplay', False) or
|
|
not DLP.CheckShowOwnedBy(f_file) or
|
|
not DLP.CheckShowSharedByMe(f_file) or
|
|
not DLP.CheckExcludeTrashed(f_file) or
|
|
not DLP.CheckMimeType(f_file) or
|
|
not DLP.CheckFileSize(f_file, sizeField) or
|
|
not DLP.CheckFilenameMatch(f_file) or
|
|
(not checkSharedDrivePermissions and not DLP.CheckFilePermissionMatches(f_file)) or
|
|
(DLP.onlySharedDrives and not driveId)):
|
|
return
|
|
if checkSharedDrivePermissions:
|
|
if not incrementalPrint:
|
|
getSharedDriveACLsCount += 1
|
|
if getSharedDriveACLsCount % 100 == 0:
|
|
writeStderr(f'{Msg.GOT} {getSharedDriveACLsCount} {getSharedDriveACLsCountMsg}')
|
|
try:
|
|
f_file['permissions'] = callGAPIpages(drive.permissions(), 'list', 'permissions',
|
|
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
fileId=f_file['id'], fields=permissionsFields, supportsAllDrives=True)
|
|
if not DLP.CheckFilePermissionMatches(f_file):
|
|
return
|
|
for permission in f_file['permissions']:
|
|
permission.pop('teamDrivePermissionDetails', None)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
|
|
GAPI.unknownError, GAPI.invalid,
|
|
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
pass
|
|
row = {'Owner': user}
|
|
fileInfo = f_file.copy()
|
|
if cleanFileName:
|
|
fileInfo['name'] = _stripControlCharsFromName(fileInfo['name'])
|
|
if not pmselect and 'permissions' in fileInfo:
|
|
fileInfo['permissions'] = DLP.GetFileMatchingPermission(fileInfo)
|
|
if DFF.showSharedDriveNames and driveId:
|
|
fileInfo['driveName'] = _getSharedDriveNameFromId(driveId)
|
|
if filepath:
|
|
if not FJQC.formatJSON or not addPathsToJSON:
|
|
addFilePathsToRow(drive, fileTree, fileInfo, filePathInfo, csvPF, row,
|
|
fullpath=fullpath, showDepth=showDepth, folderPathOnly=folderPathOnly)
|
|
else:
|
|
addFilePathsToInfo(drive, fileTree, fileInfo, filePathInfo, folderPathOnly=folderPathOnly)
|
|
if showParentsIdsAsList and 'parents' in fileInfo:
|
|
fileInfo['parentsIds'] = fileInfo.pop('parents')
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
_mapDriveFieldNames(fileInfo, user, DFF.parentsSubFields, False)
|
|
else:
|
|
_mapDriveParents(fileInfo, DFF.parentsSubFields)
|
|
_mapDriveProperties(fileInfo)
|
|
for permission in fileInfo.get('permissions', []):
|
|
_mapDrivePermissionNames(permission)
|
|
if showParentsIdsAsList and 'parentsIds' in fileInfo:
|
|
fileInfo['parents'] = len(fileInfo['parentsIds'])
|
|
if addCSVData:
|
|
fileInfo.update(addCSVData)
|
|
if not countsOnly:
|
|
if not oneItemPerRow or 'permissions' not in fileInfo:
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(flattenJSON(fileInfo, flattened=row, skipObjects=skipObjects, timeObjects=timeObjects,
|
|
simpleLists=simpleLists, delimiter=delimiter))
|
|
else:
|
|
if 'id' in fileInfo:
|
|
row['id'] = fileInfo['id']
|
|
if fileNameTitle in fileInfo:
|
|
row[fileNameTitle] = fileInfo[fileNameTitle]
|
|
if 'owners' in fileInfo:
|
|
flattenJSON({'owners': fileInfo['owners']}, flattened=row, skipObjects=skipObjects)
|
|
row['JSON'] = json.dumps(cleanJSON(fileInfo, skipObjects=skipObjects, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowTitlesJSONNoFilter(row)
|
|
else:
|
|
baserow = row.copy()
|
|
if 'id' in fileInfo:
|
|
baserow['id'] = fileInfo['id']
|
|
if fileNameTitle in fileInfo:
|
|
baserow[fileNameTitle] = fileInfo[fileNameTitle]
|
|
if 'owners' in fileInfo:
|
|
flattenJSON({'owners': fileInfo['owners']}, flattened=baserow, skipObjects=skipObjects)
|
|
for permission in fileInfo.pop('permissions'):
|
|
fileInfo['permission'] = permission
|
|
pdetails = fileInfo['permission'].pop('permissionDetails', [])
|
|
if not pdetails:
|
|
_printFileInfoRow(baserow, fileInfo)
|
|
else:
|
|
for pdetail in pdetails:
|
|
fileInfo['permission']['permissionDetails'] = pdetail
|
|
_printFileInfoRow(baserow, fileInfo)
|
|
else:
|
|
if not countsRowFilter:
|
|
csvPFco.UpdateMimeTypeCounts(flattenJSON(fileInfo, flattened=row, skipObjects=skipObjects, timeObjects=timeObjects,
|
|
simpleLists=simpleLists, delimiter=delimiter), mimeTypeInfo, sizeField)
|
|
else:
|
|
mimeTypeInfo.setdefault(fileInfo['mimeType'], {'count': 0, 'size': 0})
|
|
mimeTypeInfo[fileInfo['mimeType']]['count'] += 1
|
|
mimeTypeInfo[fileInfo['mimeType']]['size'] += int(fileInfo.get(sizeField, '0'))
|
|
|
|
def _printChildDriveFolderContents(drive, fileEntry, user, i, count, depth):
|
|
parentFileEntry = fileTree.get(fileEntry['id'])
|
|
if parentFileEntry and 'children' in parentFileEntry:
|
|
for childFileId in parentFileEntry['children']:
|
|
childEntry = fileTree.get(childFileId)
|
|
if childEntry:
|
|
if not DLP.CheckExcludeTrashed(childEntry['info']):
|
|
continue
|
|
if childFileId not in filesPrinted:
|
|
filesPrinted.add(childFileId)
|
|
# Don't show My Drive/Shared Drive unless asked when parent is 'SharedDrives'
|
|
if showParent or parentFileEntry['info']['id'] != SHARED_DRIVES:
|
|
_printFileInfo(drive, user, childEntry['info'].copy(), False)
|
|
if childEntry['info']['mimeType'] == MIMETYPE_GA_FOLDER and (maxdepth == -1 or depth < maxdepth):
|
|
_printChildDriveFolderContents(drive, childEntry['info'], user, i, count, depth+1)
|
|
return
|
|
q = WITH_PARENTS.format(fileEntry['id'])
|
|
if selectSubQuery:
|
|
q += ' and ('+selectSubQuery+')'
|
|
if depth == 0:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count, query=q)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
setGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, query=q)
|
|
pageMessage = getPageMessageForWhom(clearLastGotMsgLen=False)
|
|
try:
|
|
children = callGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=pageMessage, noFinalize=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.BAD_REQUEST],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
|
|
q=q, orderBy=DFF.orderBy, includeLabels=includeLabels, fields=pagesFields,
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], includeItemsFromAllDrives=True, supportsAllDrives=True)
|
|
for childEntryInfo in children:
|
|
childFileId = childEntryInfo['id']
|
|
if showLabels is not None:
|
|
labels = callGAPIitems(drive.files(), 'listLabels', 'labels',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
|
|
fileId=childFileId)
|
|
_formatFileDriveLabels(showLabels, labels, childEntryInfo, True, delimiter)
|
|
if filepath:
|
|
fileTree.setdefault(childFileId, {'info': childEntryInfo})
|
|
if childFileId not in filesPrinted:
|
|
filesPrinted.add(childFileId)
|
|
_printFileInfo(drive, user, childEntryInfo.copy(), stripCRsFromName)
|
|
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and (maxdepth == -1 or depth < maxdepth):
|
|
_printChildDriveFolderContents(drive, childEntryInfo, user, i, count, depth+1)
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(selectSubQuery), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
|
|
def writeMimeTypeCountsRow(user, sourceId, sourceName, mimeTypeInfo):
|
|
countTotal = sizeTotal = 0
|
|
for mtinfo in iter(mimeTypeInfo.values()):
|
|
countTotal += mtinfo['count']
|
|
sizeTotal += mtinfo['size']
|
|
row = {'Owner': user, 'Total': countTotal}
|
|
if showSource:
|
|
row['Source'] = sourceId
|
|
row['Name'] = sourceName
|
|
if showSize:
|
|
row['Size'] = sizeTotal
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
for mimeType, mtinfo in sorted(iter(mimeTypeInfo.items())):
|
|
row[f'{mimeType}'] = mtinfo['count']
|
|
if showMimeTypeSize:
|
|
row[f'{mimeType}:Size'] = mtinfo['size']
|
|
if not countsRowFilter:
|
|
csvPFco.WriteRowTitlesNoFilter(row)
|
|
else:
|
|
csvPFco.WriteRowTitles(row)
|
|
|
|
csvPF = CSVPrintFile('Owner', indexedTitles=DRIVE_INDEXED_TITLES)
|
|
csvPFco = None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
addPathsToJSON = continueOnInvalidQuery = countsRowFilter = buildTree = countsOnly = filepath = fullpath = folderPathOnly = \
|
|
getPermissionsForSharedDrives = mimeTypeInQuery = noRecursion = oneItemPerRow = stripCRsFromName = \
|
|
showParentsIdsAsList = showDepth = showParent = showSize = showMimeTypeSize = showSource = False
|
|
sizeField = 'quotaBytesUsed'
|
|
pathDelimiter = '/'
|
|
pmselect = True
|
|
showLabels = None
|
|
rootFolderId = ROOT
|
|
rootFolderName = MY_DRIVE
|
|
maxdepth = -1
|
|
nodataFields = []
|
|
simpleLists = ['permissionIds', 'spaces']
|
|
skipObjects = set()
|
|
fileIdEntity = {}
|
|
getSharedDriveACLsCountMsg = selectSubQuery = ''
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
DLP = DriveListParameters({'allowChoose': True, 'allowCorpora': True, 'allowQuery': True, 'mimeTypeInQuery': False})
|
|
DFF = DriveFileFields()
|
|
summary = FILECOUNT_SUMMARY_NONE
|
|
summaryUser = FILECOUNT_SUMMARY_USER
|
|
summaryMimeTypeInfo = {}
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif DLP.ProcessArgument(myarg, fileIdEntity):
|
|
pass
|
|
elif DFF.ProcessArgument(myarg):
|
|
pass
|
|
elif myarg == 'select':
|
|
if fileIdEntity:
|
|
usageErrorExit(Msg.CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE.format('select'))
|
|
if not DLP.fileIdEntity:
|
|
fileIdEntity = getDriveFileEntity(DLP=DLP)
|
|
else:
|
|
fileIdEntity = getDriveFileEntitySharedDriveOnly()
|
|
elif myarg == 'selectsubquery':
|
|
selectSubQuery = getString(Cmd.OB_QUERY, minLen=0)
|
|
elif myarg == 'norecursion':
|
|
noRecursion = getBoolean()
|
|
elif myarg == 'mimetypeinquery':
|
|
mimeTypeInQuery = getBoolean()
|
|
elif myarg == 'depth':
|
|
maxdepth = getInteger(minVal=-1)
|
|
elif myarg == 'showdepth':
|
|
showDepth = True
|
|
elif myarg == 'showparent':
|
|
showParent = getBoolean()
|
|
elif myarg == 'nodataheaders':
|
|
nodataFields = getString(Cmd.OB_FIELD_NAME_LIST).replace('_', '').replace(',', ' ').split()
|
|
elif myarg in {'filepath', 'fullpath'}:
|
|
filepath = True
|
|
fullpath = myarg == 'fullpath'
|
|
elif myarg == 'folderpathonly':
|
|
folderPathOnly = getBoolean()
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
elif myarg == 'addpathstojson':
|
|
addPathsToJSON = True
|
|
elif myarg == 'buildtree':
|
|
buildTree = True
|
|
elif myarg == 'countsrowfilter':
|
|
countsRowFilter = True
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
csvPFco = CSVPrintFile(['Owner', 'Total'], 'sortall')
|
|
csvPFco.SetZeroBlankMimeTypeCounts(True)
|
|
elif myarg == 'summary':
|
|
summary = getChoice(FILECOUNT_SUMMARY_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'summaryuser':
|
|
summaryUser = getString(Cmd.OB_STRING)
|
|
elif myarg == 'showsource':
|
|
showSource = True
|
|
if countsOnly:
|
|
if not showSize:
|
|
csvPFco.SetTitles(['Owner', 'Source', 'Name', 'Total'])
|
|
else:
|
|
csvPFco.SetTitles(['Owner', 'Source', 'Name', 'Total', 'Size'])
|
|
csvPFco.SetSortAllTitles()
|
|
elif myarg in {'showsize', 'showmimetypesize'}:
|
|
showSize = True
|
|
if countsOnly:
|
|
if not showSource:
|
|
csvPFco.SetTitles(['Owner', 'Total', 'Size'])
|
|
else:
|
|
csvPFco.SetTitles(['Owner', 'Source', 'Name', 'Total', 'Size'])
|
|
csvPFco.SetSortAllTitles()
|
|
if myarg == 'showmimetypesize':
|
|
showMimeTypeSize = True
|
|
elif myarg == 'sizefield':
|
|
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg == 'showparentsidsaslist':
|
|
showParentsIdsAsList = True
|
|
simpleLists.append('parentsIds')
|
|
elif myarg == 'showpermissionslast':
|
|
csvPF.SetShowPermissionsLast(True)
|
|
elif myarg == 'stripcrsfromname':
|
|
stripCRsFromName = True
|
|
elif myarg == 'showlabels':
|
|
showLabels = getChoice(SHOWLABELS_CHOICES)
|
|
elif myarg == 'showshareddrivepermissions':
|
|
getPermissionsForSharedDrives = True
|
|
permissionsFields = f'nextPageToken,permissions({",".join(DRIVEFILE_BASIC_PERMISSION_FIELDS)})'
|
|
getSharedDriveACLsCountMsg = f'{Ent.Plural(Ent.DRIVE_FILE_OR_FOLDER_ACL)} {Msg.FOR} {Ent.Plural(Ent.SHAREDDRIVE)}\n'
|
|
elif myarg == 'pmfilter':
|
|
pmselect = False
|
|
elif myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
csvPF.RemoveIndexedTitles('permissions')
|
|
elif myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'continueoninvalidquery':
|
|
continueOnInvalidQuery = getBoolean()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg)
|
|
if not filepath and not fullpath:
|
|
showDepth = False
|
|
noSelect = noFileSelectFileIdEntity(fileIdEntity)
|
|
if noSelect:
|
|
buildTree = True
|
|
if maxdepth != -1 or filepath:
|
|
if not fileIdEntity:
|
|
fileIdEntity = initDriveFileEntity()
|
|
DLP.GetFileIdEntity()
|
|
if not fileIdEntity['shareddrive']:
|
|
cleanFileIDsList(fileIdEntity, [ROOT, ORPHANS])
|
|
if maxdepth != -1:
|
|
noSelect = False
|
|
elif not fileIdEntity:
|
|
fileIdEntity = DLP.GetFileIdEntity()
|
|
elif not buildTree:
|
|
buildTree = (not noRecursion
|
|
and not fileIdEntity['dict']
|
|
and not fileIdEntity['query']
|
|
and not fileIdEntity['shareddrivefilequery']
|
|
and _simpleFileIdEntityList(fileIdEntity['list']))
|
|
incrementalPrint = buildTree and (not filepath) and noSelect and not DLP.locationSet and not showParent
|
|
if buildTree and ((not filepath) or mimeTypeInQuery) and noSelect and not DLP.locationSet and not showParent:
|
|
DLP.AddMimeTypeToQuery()
|
|
if buildTree:
|
|
if not fileIdEntity.get('shareddrive'):
|
|
btkwargs = DLP.kwargs
|
|
else:
|
|
btkwargs = fileIdEntity['shareddrive']
|
|
DLP.Finalize(fileIdEntity)
|
|
if DLP.PM.permissionMatches:
|
|
getPermissionsForSharedDrives = True
|
|
permissionsFields = f'nextPageToken,permissions({",".join(DRIVEFILE_BASIC_PERMISSION_FIELDS)})'
|
|
elif DFF.fieldsList:
|
|
if not getPermissionsForSharedDrives:
|
|
getPermissionsForSharedDrives, permissionsFields = _setGetPermissionsForSharedDrives(DFF.fieldsList)
|
|
if DFF.fieldsList:
|
|
_setSelectionFields()
|
|
fields = getFieldsFromFieldsList(DFF.fieldsList)
|
|
pagesFields = getItemFieldsFromFieldsList('files', DFF.fieldsList)
|
|
elif not DFF.allFields:
|
|
_setSelectionFields()
|
|
if not countsOnly and not set(DFF.fieldsList)-skipObjects:
|
|
for field in ['name', 'webviewlink']:
|
|
skipObjects.discard(DRIVE_FIELDS_CHOICE_MAP[field])
|
|
csvPF.AddField(field, DRIVE_FIELDS_CHOICE_MAP, DFF.fieldsList)
|
|
fields = getFieldsFromFieldsList(DFF.fieldsList)
|
|
pagesFields = getItemFieldsFromFieldsList('files', DFF.fieldsList)
|
|
else:
|
|
fields = pagesFields = '*'
|
|
DFF.SetAllParentsSubFields()
|
|
skipObjects = skipObjects.union(DEFAULT_SKIP_OBJECTS)
|
|
if stripCRsFromName:
|
|
if (countsOnly and not showSource) or (fields != '*' and 'name' not in DFF.fieldsList):
|
|
stripCRsFromName = False
|
|
shareddriveFields = []
|
|
for field in ['capabilities', 'createdTime']:
|
|
if fields == '*' or field in DFF.fieldsList:
|
|
shareddriveFields.append(field)
|
|
if filepath and not countsOnly:
|
|
csvPF.AddTitles('paths')
|
|
csvPF.SetFixPaths(True)
|
|
includeLabels = ','.join(DFF.includeLabels)
|
|
timeObjects = _getDriveTimeObjects()
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
fileNameTitle = 'title'
|
|
csvPF.SetMapDrive3Titles(True)
|
|
else:
|
|
fileNameTitle = 'name'
|
|
csvPF.RemoveTitles(['capabilities'])
|
|
if DLP.queryTimes and selectSubQuery:
|
|
for queryTimeName, queryTimeValue in iter(DLP.queryTimes.items()):
|
|
selectSubQuery = selectSubQuery.replace(f'#{queryTimeName}#', queryTimeValue)
|
|
selectSubQuery = _mapDrive2QueryToDrive3(selectSubQuery)
|
|
if addCSVData:
|
|
if not countsOnly:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
else:
|
|
csvPFco.AddTitles(sorted(addCSVData.keys()))
|
|
if showSize:
|
|
csvPFco.MoveTitlesToEnd(['Size'])
|
|
csvPFco.MoveTitlesToEnd(['Total'])
|
|
csvPFco.SetSortAllTitles()
|
|
if not nodataFields:
|
|
if DFF.fieldsList:
|
|
if not FJQC.formatJSON:
|
|
nodataFields = ['Owner']+list(set(DFF.fieldsList)-skipObjects)
|
|
else:
|
|
nodataFields = ['Owner', 'id', fileNameTitle, 'owners.emailAddress']
|
|
else:
|
|
nodataFields = ['Owner', 'id', fileNameTitle, 'owners.emailAddress']
|
|
if not FJQC.formatJSON:
|
|
nodataFields.append('permissions')
|
|
if filepath:
|
|
nodataFields.append('paths')
|
|
if FJQC.formatJSON:
|
|
nodataFields.append('JSON')
|
|
csvPF.SetNodataFields(True, nodataFields, DRIVE_LIST_FIELDS, DRIVE_SUBFIELDS_CHOICE_MAP, oneItemPerRow)
|
|
else:
|
|
csvPF.SetNodataFields(False, nodataFields, None, None, False)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
if ((incrementalPrint and countsOnly) or
|
|
(not showParentsIdsAsList and DFF.parentsSubFields['isRoot'])):
|
|
try:
|
|
if not fileIdEntity['shareddrive']:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=ROOT, fields='id,name')
|
|
rootFolderId = result['id']
|
|
rootFolderName = result['name']
|
|
else:
|
|
rootFolderId = fileIdEntity['shareddrive']['driveId']
|
|
if not fileIdEntity['shareddrivename']:
|
|
fileIdEntity['shareddrivename'] = callGAPI(drive.drives(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FILE_NOT_FOUND],
|
|
driveId=rootFolderId, fields='name')['name']
|
|
rootFolderName = fileIdEntity['shareddrivename']
|
|
if not showParentsIdsAsList and DFF.parentsSubFields['isRoot']:
|
|
DFF.parentsSubFields['rootFolderId'] = rootFolderId
|
|
except (GAPI.notFound, GAPI.fileNotFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
if filepath:
|
|
filePathInfo = initFilePathInfo(pathDelimiter)
|
|
filesPrinted = set()
|
|
mimeTypeInfo = {}
|
|
getSharedDriveACLsCount = 0
|
|
if buildTree:
|
|
gettingEntity = _getGettingEntity(user, fileIdEntity)
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count, query=DLP.fileIdEntity['query'])
|
|
if not incrementalPrint:
|
|
fileTree, status = initFileTree(drive, fileIdEntity.get('shareddrive'), DLP, shareddriveFields, showParent, user, i, count)
|
|
if not status:
|
|
continue
|
|
try:
|
|
feed = yieldGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(), maxItems=DLP.maxItems,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
|
|
GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
|
|
q=DLP.fileIdEntity['query'], orderBy=DFF.orderBy, includeLabels=includeLabels,
|
|
fields=pagesFields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **btkwargs)
|
|
for files in feed:
|
|
if showLabels is not None:
|
|
for f_file in files:
|
|
labels = callGAPIitems(drive.files(), 'listLabels', 'labels',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
|
|
fileId=f_file['id'])
|
|
_formatFileDriveLabels(showLabels, labels, f_file, True, delimiter)
|
|
if not incrementalPrint:
|
|
extendFileTree(fileTree, files, DLP, stripCRsFromName)
|
|
else:
|
|
for f_file in files:
|
|
if stripCRsFromName:
|
|
f_file['name'] = _stripControlCharsFromName(f_file['name'])
|
|
_printFileInfo(drive, user, f_file, False)
|
|
if incrementalPrint:
|
|
if countsOnly:
|
|
if summary != FILECOUNT_SUMMARY_NONE:
|
|
for mimeType, mtinfo in iter(mimeTypeInfo.items()):
|
|
summaryMimeTypeInfo.setdefault(mimeType, {'count': 0, 'size': 0})
|
|
summaryMimeTypeInfo[mimeType]['count'] += mtinfo['count']
|
|
summaryMimeTypeInfo[mimeType]['size'] += mtinfo['size']
|
|
if summary != FILECOUNT_SUMMARY_ONLY:
|
|
writeMimeTypeCountsRow(user, rootFolderId, rootFolderName, mimeTypeInfo)
|
|
continue
|
|
extendFileTreeParents(drive, fileTree, fields)
|
|
DLP.GetLocationFileIdsFromTree(fileTree, fileIdEntity)
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest) as e:
|
|
errMsg = str(e)
|
|
if 'Invalid field selection' in errMsg:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], errMsg, i, count)
|
|
break
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], invalidQuery(DLP.fileIdEntity['query']), i, count)
|
|
if not continueOnInvalidQuery:
|
|
break
|
|
continue
|
|
except GAPI.fileNotFound:
|
|
printGotEntityItemsForWhom(0)
|
|
continue
|
|
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
else:
|
|
fileTree = {}
|
|
user, drive, jcount = _validateUserGetFileIDs(origUser, i, count, fileIdEntity, drive=drive)
|
|
if jcount == 0:
|
|
continue
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
if showSource:
|
|
mimeTypeInfo = {}
|
|
fileEntry = fileTree.get(fileId)
|
|
if fileEntry:
|
|
fileEntryInfo = fileEntry['info']
|
|
else:
|
|
try:
|
|
fileEntryInfo = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, includeLabels=includeLabels, fields=fields, supportsAllDrives=True)
|
|
if stripCRsFromName:
|
|
fileEntryInfo['name'] = _stripControlCharsFromName(fileEntryInfo['name'])
|
|
if showLabels is not None:
|
|
labels = callGAPIitems(drive.files(), 'listLabels', 'labels',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS+[GAPI.UNKNOWN_ERROR],
|
|
fileId=fileId)
|
|
_formatFileDriveLabels(showLabels, labels, fileEntryInfo, True, delimiter)
|
|
if filepath:
|
|
fileTree[fileId] = {'info': fileEntryInfo}
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId], Msg.NOT_FOUND, j, jcount)
|
|
continue
|
|
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
if fullpath:
|
|
getFilePaths(drive, fileTree, fileEntryInfo, filePathInfo, addParentsToTree=True,
|
|
fullpath=fullpath, showDepth=showDepth, folderPathOnly=folderPathOnly)
|
|
if ((showParent and (fileEntryInfo['id'] not in {ORPHANS, SHARED_WITHME, SHARED_DRIVES})) or
|
|
fileEntryInfo['mimeType'] != MIMETYPE_GA_FOLDER or noRecursion):
|
|
if fileId not in filesPrinted:
|
|
filesPrinted.add(fileId)
|
|
_printFileInfo(drive, user, fileEntryInfo.copy(), False)
|
|
if fileEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and not noRecursion:
|
|
_printChildDriveFolderContents(drive, fileEntryInfo, user, i, count, 0)
|
|
if GC.Values[GC.SHOW_GETTINGS] and not GC.Values[GC.SHOW_GETTINGS_GOT_NL]:
|
|
writeStderr('\r\n')
|
|
flushStderr()
|
|
if countsOnly:
|
|
if showSource:
|
|
if summary != FILECOUNT_SUMMARY_NONE:
|
|
for mimeType, mtinfo in iter(mimeTypeInfo.items()):
|
|
summaryMimeTypeInfo.setdefault(mimeType, {'count': 0, 'size': 0})
|
|
summaryMimeTypeInfo[mimeType]['count'] += mtinfo['count']
|
|
summaryMimeTypeInfo[mimeType]['size'] += mtinfo['size']
|
|
if summary != FILECOUNT_SUMMARY_ONLY:
|
|
writeMimeTypeCountsRow(user, fileId, fileEntryInfo['name'], mimeTypeInfo)
|
|
if countsOnly:
|
|
if not showSource:
|
|
if summary != FILECOUNT_SUMMARY_NONE:
|
|
for mimeType, mtinfo in iter(mimeTypeInfo.items()):
|
|
summaryMimeTypeInfo.setdefault(mimeType, {'count': 0, 'size': 0})
|
|
summaryMimeTypeInfo[mimeType]['count'] += mtinfo['count']
|
|
summaryMimeTypeInfo[mimeType]['size'] += mtinfo['size']
|
|
if summary != FILECOUNT_SUMMARY_ONLY:
|
|
writeMimeTypeCountsRow(user, 'Various', 'Various', mimeTypeInfo)
|
|
if not countsOnly:
|
|
if not csvPF.rows:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not FJQC.formatJSON:
|
|
csvPF.SetSortTitles(['Owner', 'id', fileNameTitle])
|
|
else:
|
|
if 'JSON' in csvPF.JSONtitlesList:
|
|
csvPF.MoveJSONTitlesToEnd(['JSON'])
|
|
if GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] is None:
|
|
csvPF.writeCSVfile(f'{Cmd.Argument(GM.Globals[GM.ENTITY_CL_START])} {Cmd.Argument(GM.Globals[GM.ENTITY_CL_START]+1)} Drive Files')
|
|
else:
|
|
csvPF.writeCSVfile('Drive Files')
|
|
else:
|
|
if not csvPFco.rows:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if summary != FILECOUNT_SUMMARY_NONE:
|
|
writeMimeTypeCountsRow(summaryUser, 'Various', 'Various', summaryMimeTypeInfo)
|
|
csvPFco.todrive = csvPF.todrive
|
|
if not countsRowFilter:
|
|
csvPFco.SetRowFilter([], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
|
|
if GM.Globals[GM.CSVFILE][GM.REDIRECT_QUEUE] is None:
|
|
csvPFco.writeCSVfile(f'{Cmd.Argument(GM.Globals[GM.ENTITY_CL_START])} {Cmd.Argument(GM.Globals[GM.ENTITY_CL_START]+1)} Drive File Counts')
|
|
else:
|
|
csvPFco.writeCSVfile('Drive File Counts')
|
|
|
|
FILECOMMENTS_FIELDS_CHOICE_MAP = {
|
|
'action': 'action',
|
|
'author': 'author',
|
|
'content': 'content',
|
|
'createddate': 'createdTime',
|
|
'createdtime': 'createdTime',
|
|
'deleted': 'deleted',
|
|
'htmlcontent': 'htmlContent',
|
|
'id': 'id',
|
|
'modifieddate': 'modifiedTime',
|
|
'modifiedtime': 'modifiedTime',
|
|
'quotedfilecontent': 'quotedFileContent',
|
|
'reply': 'replies',
|
|
'replies': 'replies',
|
|
'resolved': 'resolved',
|
|
}
|
|
|
|
FILECOMMENTS_AUTHOR_SUBFIELDS_CHOICE_MAP = {
|
|
'displayname': 'displayName',
|
|
'emailaddress': 'emailAddress',
|
|
'me': 'me',
|
|
'permissionid': 'permissionId',
|
|
'photolink': 'photoLink',
|
|
}
|
|
|
|
FILECOMMENTS_REPLIES_SUBFIELDS_CHOICE_MAP = {
|
|
'action': 'action',
|
|
'author': 'author',
|
|
'content': 'content',
|
|
'createddate': 'createdTime',
|
|
'createdtime': 'createdTime',
|
|
'deleted': 'deleted',
|
|
'htmlcontent': 'htmlContent',
|
|
'id': 'id',
|
|
'modifieddate': 'modifiedTime',
|
|
'modifiedtime': 'modifiedTime',
|
|
}
|
|
|
|
FILECOMMENTS_SUBFIELDS_CHOICE_MAP = {
|
|
'author': FILECOMMENTS_AUTHOR_SUBFIELDS_CHOICE_MAP,
|
|
'reply': FILECOMMENTS_REPLIES_SUBFIELDS_CHOICE_MAP,
|
|
'replies': FILECOMMENTS_REPLIES_SUBFIELDS_CHOICE_MAP,
|
|
}
|
|
|
|
def _getCommentFields(fieldsList):
|
|
for field in _getFieldsList():
|
|
if field.find('.') == -1:
|
|
if field in FILECOMMENTS_FIELDS_CHOICE_MAP:
|
|
addFieldToFieldsList(field, FILECOMMENTS_FIELDS_CHOICE_MAP, fieldsList)
|
|
else:
|
|
invalidChoiceExit(field, FILECOMMENTS_FIELDS_CHOICE_MAP, True)
|
|
else:
|
|
field, subField = field.split('.', 1)
|
|
if field in FILECOMMENTS_SUBFIELDS_CHOICE_MAP:
|
|
if subField.find('.') == -1:
|
|
if subField in FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field]:
|
|
fieldsList.append(f'{FILECOMMENTS_FIELDS_CHOICE_MAP[field]}.{FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field][subField]}')
|
|
else:
|
|
invalidChoiceExit(subField, FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field], True)
|
|
else:
|
|
subField, subSubField = subField.split('.', 1)
|
|
if subField in FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field]:
|
|
if subSubField in FILECOMMENTS_SUBFIELDS_CHOICE_MAP[subField]:
|
|
fieldsList.append(f'{FILECOMMENTS_FIELDS_CHOICE_MAP[field]}.{FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field][subField]}.{FILECOMMENTS_SUBFIELDS_CHOICE_MAP[subField][subSubField]}')
|
|
else:
|
|
invalidChoiceExit(subSubField, FILECOMMENTS_SUBFIELDS_CHOICE_MAP[subField], True)
|
|
else:
|
|
invalidChoiceExit(subField, FILECOMMENTS_SUBFIELDS_CHOICE_MAP[field], True)
|
|
else:
|
|
invalidChoiceExit(field, FILECOMMENTS_SUBFIELDS_CHOICE_MAP, True)
|
|
|
|
FILECOMMENTS_INDEXED_TITLES = ['replies']
|
|
FILECOMMENTS_TIME_OBJECTS = {'createdTime', 'modifiedTime'}
|
|
|
|
def _stripCommentPhotoLinks(comment):
|
|
if 'author' in comment:
|
|
comment['author'].pop('photoLink', None)
|
|
for reply in comment.get('replies', []):
|
|
if 'author' in reply:
|
|
reply['author'].pop('photoLink', None)
|
|
|
|
def _showComment(comment, stripPhotoLinks, timeObjects, i=0, count=0, FJQC=None):
|
|
if stripPhotoLinks:
|
|
_stripCommentPhotoLinks(comment)
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(comment, timeObjects=FILECOMMENTS_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.DRIVE_FILE_COMMENT, comment['id']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, comment, ['id'], timeObjects)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> show filecomments <DriveFileEntity>
|
|
# [showdeleted] [start <Date>|<Time>] [countsonly|positivecountsonly]
|
|
# [fields <CommentsFieldNameList>] [showphotolinks]
|
|
# [countsonly]
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> print filecomments <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# [showdeleted] [start <Date>|<Time>]
|
|
# [fields <CommentsFieldNameList>] [showphotolinks]
|
|
# [countsonly|positivecountsonly]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowFileComments(users):
|
|
def _printComment(comment, commentId, replyId, baserow):
|
|
row = flattenJSON(comment, flattened=baserow.copy(), timeObjects=timeObjects)
|
|
row['commentId'] = commentId
|
|
row['replyId'] = replyId
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = baserow.copy()
|
|
row['commentId'] = commentId
|
|
comment['id'] = commentId
|
|
row['replyId'] = replyId
|
|
if replyId:
|
|
comment['reply']['id'] = replyId
|
|
row['JSON'] = json.dumps(cleanJSON(comment, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
csvPF = CSVPrintFile(['User', 'fileId']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fieldsList = []
|
|
fileIdEntity = getDriveFileEntity()
|
|
countsOnly = positiveCountsOnly = False
|
|
stripPhotoLinks = True
|
|
kwargs = {}
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'showdeleted':
|
|
kwargs['includeDeleted'] = True
|
|
elif myarg == 'start':
|
|
kwargs['startModifiedTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg == 'showphotolinks':
|
|
stripPhotoLinks = False
|
|
elif myarg == 'fields':
|
|
_getCommentFields(fieldsList)
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
elif myarg == 'positivecountsonly':
|
|
countsOnly = positiveCountsOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg)
|
|
if csvPF:
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if not countsOnly:
|
|
csvPF.AddTitles(['commentId', 'replyId'])
|
|
else:
|
|
csvPF.AddTitles(['comments', 'replies'])
|
|
if FJQC.formatJSON:
|
|
csvPF.AddTitles(['JSON'])
|
|
csvPF.SetJSONTitles(csvPF.titlesList)
|
|
if fieldsList:
|
|
if 'id' not in fieldsList:
|
|
fieldsList.append('id')
|
|
if 'replies' not in fieldsList:
|
|
for field in fieldsList.copy():
|
|
if field.startswith('replies.'):
|
|
fieldsList.append('replies.id')
|
|
break
|
|
fields = getItemFieldsFromFieldsList('comments', fieldsList)
|
|
else:
|
|
fields = '*'
|
|
timeObjects = FILECOMMENTS_TIME_OBJECTS
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=[Ent.DRIVE_FILE_COMMENT, None][csvPF is not None])
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
comments = callGAPIpages(drive.comments(), 'list', 'comments',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST],
|
|
fileId=fileId, fields=fields, **kwargs)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_ID, fileId], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
kcount = len(comments)
|
|
if countsOnly:
|
|
numReplies = 0
|
|
for comment in comments:
|
|
numReplies += len(comment['replies'])
|
|
if not csvPF:
|
|
if countsOnly:
|
|
if not positiveCountsOnly or kcount > 0:
|
|
printKeyValueList([Ent.Singular(Ent.DRIVE_FILE_ID), fileId, 'comments', kcount, 'replies', numReplies])
|
|
else:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.DRIVE_FILE_ID, fileId], kcount, Ent.DRIVE_FILE_COMMENT, j, jcount)
|
|
Ind.Increment()
|
|
if not countsOnly:
|
|
k = 0
|
|
for comment in comments:
|
|
k += 1
|
|
_showComment(comment, stripPhotoLinks, timeObjects, k, kcount, FJQC)
|
|
Ind.Decrement()
|
|
elif countsOnly:
|
|
if not positiveCountsOnly or kcount > 0:
|
|
row = {'User': user, 'fileId': fileId}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
row['comments'] = kcount
|
|
row['replies'] = numReplies
|
|
csvPF.WriteRowTitles(row)
|
|
elif comments:
|
|
baserow = {'User': user, 'fileId': fileId}
|
|
if addCSVData:
|
|
baserow.update(addCSVData)
|
|
for comment in comments:
|
|
if stripPhotoLinks:
|
|
_stripCommentPhotoLinks(comment)
|
|
commentId = comment.pop('id')
|
|
replies = comment.pop('replies')
|
|
if not replies:
|
|
_printComment(comment, commentId, '', baserow)
|
|
else:
|
|
for reply in replies:
|
|
replyId = reply.pop('id')
|
|
baserow['replyId'] = replyId
|
|
comment['reply'] = reply
|
|
_printComment(comment, commentId, replyId, baserow)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.SetIndexedTitles(FILECOMMENTS_INDEXED_TITLES)
|
|
csvPF.writeCSVfile('Drive File Comments')
|
|
|
|
# gam <UserTypeEntity> print filepaths <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [stripcrsfromname] [oneitemperrow]
|
|
# [fullpath] [folderpathonly [<Boolean>]] [pathdelimiter <Character>]
|
|
# [followshortcuts [<Boolean>]]
|
|
# gam <UserTypeEntity> show filepaths <DriveFileEntity>
|
|
# [returnpathonly]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [stripcrsfromname]
|
|
# [fullpath] [folderpathonly [<Boolean>]] [pathdelimiter <Character>]
|
|
# [followshortcuts [<Boolean>]]
|
|
def printShowFilePaths(users):
|
|
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
|
|
csvPF = CSVPrintFile(['Owner', 'id', fileNameTitle, 'paths'], 'sortall', ['paths']) if Act.csvFormat() else None
|
|
fileIdEntity = getDriveFileEntity()
|
|
fullpath = folderPathOnly = followShortcuts = oneItemPerRow = returnPathOnly = stripCRsFromName = False
|
|
pathDelimiter = '/'
|
|
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'fullpath':
|
|
fullpath = True
|
|
elif myarg == 'folderpathonly':
|
|
folderPathOnly = getBoolean()
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
elif myarg == 'stripcrsfromname':
|
|
stripCRsFromName = True
|
|
elif csvPF is None and myarg == 'returnpathonly':
|
|
returnPathOnly = True
|
|
elif myarg == 'followshortcuts':
|
|
followShortcuts = getBoolean()
|
|
elif csvPF and myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
csvPF.RemoveTitles('paths')
|
|
csvPF.AddTitles('path')
|
|
csvPF.SetSortAllTitles()
|
|
csvPF.SetIndexedTitles([])
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
else:
|
|
unknownArgumentExit()
|
|
fieldsList = FILEPATH_FIELDS_TITLES
|
|
if followShortcuts:
|
|
fieldsList.append('shortcutDetails')
|
|
pathFields = ','.join(fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if returnPathOnly:
|
|
GC.Values[GC.SHOW_GETTINGS] = False
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=[Ent.DRIVE_FILE_OR_FOLDER, None][(csvPF is not None) or returnPathOnly],
|
|
orderBy=OBY.orderBy)
|
|
if jcount == 0:
|
|
continue
|
|
filePathInfo = initFilePathInfo(pathDelimiter)
|
|
if fullpath:
|
|
fileTree, status = initFileTree(drive, fileIdEntity.get('shareddrive'), None, [], True, user, i, count)
|
|
if not status:
|
|
continue
|
|
else:
|
|
fileTree = None
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields=pathFields, supportsAllDrives=True)
|
|
if followShortcuts and result['mimeType'] == MIMETYPE_GA_SHORTCUT:
|
|
fileId = result['shortcutDetails']['targetId']
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields=pathFields, supportsAllDrives=True)
|
|
if returnPathOnly and result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == MY_DRIVE and not result.get('parents'):
|
|
writeStdout(f'{MY_DRIVE}\n')
|
|
continue
|
|
if stripCRsFromName:
|
|
result['name'] = _stripControlCharsFromName(result['name'])
|
|
driveId = result.get('driveId')
|
|
if driveId:
|
|
if result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == TEAM_DRIVE:
|
|
result['name'] = _getSharedDriveNameFromId(driveId)
|
|
if returnPathOnly:
|
|
if fullpath:
|
|
writeStdout(f'{SHARED_DRIVES}/{result["name"]}\n')
|
|
else:
|
|
writeStdout(f'{result["name"]}\n')
|
|
continue
|
|
if fullpath:
|
|
extendFileTree(fileTree, [result], None, False)
|
|
extendFileTreeParents(drive, fileTree, pathFields)
|
|
entityType, paths, _ = getFilePaths(drive, fileTree, result, filePathInfo, addParentsToTree=True,
|
|
fullpath=fullpath, folderPathOnly=folderPathOnly)
|
|
if returnPathOnly:
|
|
for path in paths:
|
|
writeStdout(f'{path}\n')
|
|
elif not csvPF:
|
|
kcount = len(paths)
|
|
entityPerformActionNumItems([entityType, f'{result["name"]} ({fileId})'], kcount, Ent.DRIVE_PATH, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for path in paths:
|
|
k += 1
|
|
printEntity([Ent.DRIVE_PATH, path], k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
if oneItemPerRow:
|
|
if paths:
|
|
for path in paths:
|
|
csvPF.WriteRow({'Owner': user, 'id': fileId, fileNameTitle: result['name'], 'path': path})
|
|
else:
|
|
csvPF.WriteRow({'Owner': user, 'id': fileId, fileNameTitle: result['name']})
|
|
else:
|
|
csvPF.WriteRowTitles(flattenJSON({'paths': paths}, flattened={'Owner': user, 'id': fileId, fileNameTitle: result['name']}))
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], Msg.DOES_NOT_EXIST, j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive File Paths')
|
|
|
|
# gam <UserTypeEntity> print fileparenttree <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# [stripcrsfromname]
|
|
def printFileParentTree(users):
|
|
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
|
|
csvPF = CSVPrintFile(['Owner', 'isBase', 'baseId', 'id', fileNameTitle, 'parentId', 'depth', 'isRoot'], 'sortall')
|
|
fileIdEntity = getDriveFileEntity()
|
|
stripCRsFromName = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'stripcrsfromname':
|
|
stripCRsFromName = True
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.FILE_PARENT_TREE)
|
|
if jcount == 0:
|
|
continue
|
|
try:
|
|
rootId = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=ROOT, fields='id')['id']
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
fileList = []
|
|
baseId = fileId
|
|
while True:
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='id,name,mimeType,parents,driveId', supportsAllDrives=True)
|
|
if stripCRsFromName:
|
|
result['name'] = _stripControlCharsFromName(result['name'])
|
|
result['isRoot'] = False
|
|
if not result.get('parents', []):
|
|
if fileId == rootId:
|
|
result['isRoot'] = True
|
|
else:
|
|
driveId = result.get('driveId')
|
|
if driveId:
|
|
if result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == TEAM_DRIVE:
|
|
result['name'] = _getSharedDriveNameFromId(driveId)
|
|
result['isRoot'] = True
|
|
result['parents'] = ['']
|
|
fileList.append(result)
|
|
break
|
|
fileList.append(result)
|
|
fileId = result['parents'][0]
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], Msg.DOES_NOT_EXIST, j, jcount)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
kcount = len(fileList)
|
|
isBase = True
|
|
for result in fileList:
|
|
csvPF.WriteRow({'Owner': user, 'isBase': isBase, 'baseId': baseId, 'id': result['id'], fileNameTitle: result['name'],
|
|
'parentId': result['parents'][0], 'depth': kcount, 'isRoot': result['isRoot']})
|
|
isBase = False
|
|
kcount -= 1
|
|
csvPF.writeCSVfile('Drive File Parent Tree')
|
|
|
|
# Last file modification utilities
|
|
def _initLastModification():
|
|
return {'lastModifiedFileId': '', 'lastModifiedFileName': '',
|
|
'lastModifiedFileMimeType': '', 'lastModifiedFilePath': '',
|
|
'lastModifyingUser': '', 'lastModifiedTime': NEVER_TIME,
|
|
'fileEntryInfo': {}}
|
|
|
|
def _checkUpdateLastModifiction(f_file, userLastModification):
|
|
if f_file.get('modifiedTime', NEVER_TIME) > userLastModification['lastModifiedTime'] and 'lastModifyingUser' in f_file:
|
|
userLastModification['lastModifiedFileId'] = f_file['id']
|
|
userLastModification['lastModifiedFileName'] = _stripControlCharsFromName(f_file['name'])
|
|
userLastModification['lastModifiedFileMimeType'] = f_file['mimeType']
|
|
userLastModification['lastModifiedTime'] = f_file['modifiedTime']
|
|
userLastModification['lastModifyingUser'] = f_file['lastModifyingUser'].get('emailAddress',
|
|
f_file['lastModifyingUser'].get('displayName', UNKNOWN))
|
|
userLastModification['fileEntryInfo'] = f_file.copy()
|
|
|
|
def _getLastModificationPath(drive, userLastModification, pathDelimiter):
|
|
if userLastModification['fileEntryInfo']:
|
|
filePathInfo = initFilePathInfo(pathDelimiter)
|
|
_, paths, _ = getFilePaths(drive, {}, userLastModification['fileEntryInfo'], filePathInfo)
|
|
userLastModification['lastModifiedFilePath'] = paths[0] if paths else UNKNOWN
|
|
|
|
def _showLastModification(lastModification):
|
|
printKeyValueList(['lastModifiedFileId', lastModification['lastModifiedFileId']])
|
|
printKeyValueList(['lastModifiedFileName', lastModification['lastModifiedFileName']])
|
|
printKeyValueList(['lastModifiedFileMimeType', lastModification['lastModifiedFileMimeType']])
|
|
printKeyValueList(['lastModifiedFilePath', lastModification['lastModifiedFilePath']])
|
|
printKeyValueList(['lastModifyingUser', lastModification['lastModifyingUser']])
|
|
printKeyValueList(['lastModifiedTime', formatLocalTime(lastModification['lastModifiedTime'])])
|
|
|
|
def _updateLastModificationRow(row, lastModification):
|
|
row.update({'lastModifiedFileId': lastModification['lastModifiedFileId'],
|
|
'lastModifiedFileName': lastModification['lastModifiedFileName'],
|
|
'lastModifiedFileMimeType': lastModification['lastModifiedFileMimeType'],
|
|
'lastModifiedFilePath': lastModification['lastModifiedFilePath'],
|
|
'lastModifyingUser': lastModification['lastModifyingUser'],
|
|
'lastModifiedTime': formatLocalTime(lastModification['lastModifiedTime'])})
|
|
|
|
# gam <UserTypeEntity> print filecounts [todrive <ToDriveAttribute>*]
|
|
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
|
|
# [continueoninvalidquery [<Boolean>]]
|
|
# [corpora <CorporaAttribute>]
|
|
# [select <SharedDriveEntity>]
|
|
# [anyowner|(showownedby any|me|others)]
|
|
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>]
|
|
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
|
|
# [filenamematchpattern <REMatchPattern>]
|
|
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
|
# [excludetrashed] (addcsvdata <FieldName> <String>)*
|
|
# [showsize] [showmimetypesize]
|
|
# [showlastmodification] [pathdelimiter <Character>]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [summary none|only|plus] [summaryuser <String>]
|
|
# gam <UserTypeEntity> show filecounts
|
|
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
|
|
# [continueoninvalidquery [<Boolean>]]
|
|
# [corpora <CorporaAttribute>]
|
|
# [select <SharedDriveEntity>]
|
|
# [anyowner|(showownedby any|me|others)]
|
|
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>]
|
|
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
|
|
# [filenamematchpattern <REMatchPattern>]
|
|
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
|
# [excludetrashed]
|
|
# [showsize] [showmimetypesize]
|
|
# [showlastmodification] [pathdelimiter <Character>]
|
|
# [summary none|only|plus] [summaryuser <String>]
|
|
def printShowFileCounts(users):
|
|
def _setSelectionFields():
|
|
if DLP.showOwnedBy is not None:
|
|
fieldsList.extend(OWNED_BY_ME_FIELDS_TITLES)
|
|
if showSize or (DLP.minimumFileSize is not None) or (DLP.maximumFileSize is not None):
|
|
fieldsList.append(sizeField)
|
|
if showLastModification:
|
|
fieldsList.extend(['id,name,modifiedTime,lastModifyingUser(me, displayName, emailAddress),parents'])
|
|
if DLP.filenameMatchPattern:
|
|
fieldsList.append('name')
|
|
if DLP.excludeTrashed:
|
|
fieldsList.append('trashed')
|
|
if DLP.PM.permissionMatches:
|
|
fieldsList.extend(['id', 'permissions'])
|
|
if DLP.onlySharedDrives or getPermissionsForSharedDrives:
|
|
fieldsList.append('driveId')
|
|
|
|
def showMimeTypeInfo(user, mimeTypeInfo, sharedDriveId, sharedDriveName, lastModification, i, count):
|
|
if summary != FILECOUNT_SUMMARY_NONE:
|
|
if count != 0:
|
|
for mimeType, mtinfo in iter(mimeTypeInfo.items()):
|
|
summaryMimeTypeInfo.setdefault(mimeType, {'count': 0, 'size': 0})
|
|
summaryMimeTypeInfo[mimeType]['count'] += mtinfo['count']
|
|
summaryMimeTypeInfo[mimeType]['size'] += mtinfo['size']
|
|
if summary == FILECOUNT_SUMMARY_ONLY:
|
|
return
|
|
countTotal = sizeTotal = 0
|
|
for mtinfo in iter(mimeTypeInfo.values()):
|
|
countTotal += mtinfo['count']
|
|
sizeTotal += mtinfo['size']
|
|
if not csvPF:
|
|
if sharedDriveId:
|
|
kvList = [Ent.USER, user, Ent.SHAREDDRIVE, f'{sharedDriveName} ({sharedDriveId})']
|
|
else:
|
|
kvList = [Ent.USER, user]
|
|
dataList = [Ent.Choose(Ent.DRIVE_FILE_OR_FOLDER, countTotal), countTotal]
|
|
if showSize:
|
|
dataList.extend([Ent.Singular(Ent.SIZE), sizeTotal])
|
|
if sharedDriveId:
|
|
dataList.extend(['Item cap', f"{countTotal/SHARED_DRIVE_MAX_FILES_FOLDERS:.2%}"])
|
|
printEntityKVList(kvList, dataList, i, count)
|
|
Ind.Increment()
|
|
if showLastModification:
|
|
_showLastModification(lastModification)
|
|
for mimeType, mtinfo in sorted(iter(mimeTypeInfo.items())):
|
|
if not showMimeTypeSize:
|
|
printKeyValueList([mimeType, mtinfo['count']])
|
|
else:
|
|
printKeyValueList([mimeType, f"{mtinfo['count']}, {mtinfo['size']}"])
|
|
Ind.Decrement()
|
|
else:
|
|
if sharedDriveId:
|
|
row = {'User': user, 'id': sharedDriveId, 'name': sharedDriveName, 'Total': countTotal, 'Item cap': f"{countTotal/SHARED_DRIVE_MAX_FILES_FOLDERS:.2%}"}
|
|
else:
|
|
row = {'User': user, 'Total': countTotal}
|
|
if showSize:
|
|
row['Size'] = sizeTotal
|
|
if showLastModification:
|
|
_updateLastModificationRow(row, lastModification)
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
for mimeType, mtinfo in sorted(iter(mimeTypeInfo.items())):
|
|
row[f'{mimeType}'] = mtinfo['count']
|
|
if showMimeTypeSize:
|
|
row[f'{mimeType}:Size'] = mtinfo['size']
|
|
csvPF.WriteRowTitles(row)
|
|
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
if csvPF:
|
|
csvPF.SetZeroBlankMimeTypeCounts(True)
|
|
fieldsList = ['mimeType']
|
|
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': True, 'allowQuery': True, 'mimeTypeInQuery': True})
|
|
pathDelimiter = '/'
|
|
sharedDriveId = sharedDriveName = ''
|
|
continueOnInvalidQuery = showSize = showLastModification = showMimeTypeSize = False
|
|
sizeField = 'quotaBytesUsed'
|
|
summary = FILECOUNT_SUMMARY_NONE
|
|
summaryUser = FILECOUNT_SUMMARY_USER
|
|
summaryMimeTypeInfo = {}
|
|
fileIdEntity = {}
|
|
addCSVData = {}
|
|
summaryLastModification = _initLastModification()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif DLP.ProcessArgument(myarg, fileIdEntity):
|
|
pass
|
|
elif myarg == 'select':
|
|
if fileIdEntity:
|
|
usageErrorExit(Msg.CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE.format('select'))
|
|
fileIdEntity = getSharedDriveEntity()
|
|
elif myarg == 'showsize':
|
|
showSize = True
|
|
elif myarg == 'sizefield':
|
|
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'showlastmodification':
|
|
showLastModification = True
|
|
elif myarg == 'showmimetypesize':
|
|
showMimeTypeSize = showSize = True
|
|
elif myarg == 'summary':
|
|
summary = getChoice(FILECOUNT_SUMMARY_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'summaryuser':
|
|
summaryUser = getString(Cmd.OB_STRING)
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'continueoninvalidquery':
|
|
continueOnInvalidQuery = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
if not fileIdEntity:
|
|
fileIdEntity = DLP.GetFileIdEntity()
|
|
if not fileIdEntity.get('shareddrive'):
|
|
btkwargs = DLP.kwargs
|
|
else:
|
|
btkwargs = fileIdEntity['shareddrive']
|
|
fieldsList.append('driveId')
|
|
DLP.Finalize(fileIdEntity)
|
|
if DLP.PM.permissionMatches:
|
|
getPermissionsForSharedDrives = True
|
|
permissionsFields = 'nextPageToken,permissions'
|
|
else:
|
|
getPermissionsForSharedDrives = False
|
|
_setSelectionFields()
|
|
if csvPF:
|
|
sortTitles = ['User', 'id', 'name', 'Total', 'Item cap'] if fileIdEntity.get('shareddrive') else ['User', 'Total']
|
|
if showSize:
|
|
sortTitles.insert(sortTitles.index('Total')+1, 'Size')
|
|
if showLastModification:
|
|
sortTitles.extend(['lastModifiedFileId', 'lastModifiedFileName',
|
|
'lastModifiedFileMimeType', 'lastModifiedFilePath',
|
|
'lastModifyingUser', 'lastModifiedTime'])
|
|
if addCSVData:
|
|
sortTitles.extend(sorted(addCSVData.keys()))
|
|
csvPF.SetTitles(sortTitles)
|
|
csvPF.SetSortAllTitles()
|
|
pagesFields = getItemFieldsFromFieldsList('files', fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
sharedDriveId = fileIdEntity.get('shareddrive', {}).get('driveId', '')
|
|
sharedDriveName = _getSharedDriveNameFromId(sharedDriveId) if sharedDriveId else ''
|
|
mimeTypeInfo = {}
|
|
userLastModification = _initLastModification()
|
|
gettingEntity = _getGettingEntity(user, fileIdEntity)
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count, query=DLP.fileIdEntity['query'])
|
|
try:
|
|
feed = yieldGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
|
|
GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=DLP.fileIdEntity['query'],
|
|
fields=pagesFields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **btkwargs)
|
|
for files in feed:
|
|
for f_file in files:
|
|
driveId = f_file.get('driveId')
|
|
checkSharedDrivePermissions = getPermissionsForSharedDrives and driveId and 'permissions' not in f_file
|
|
if (not DLP.CheckShowOwnedBy(f_file) or
|
|
not DLP.CheckShowSharedByMe(f_file) or
|
|
not DLP.CheckExcludeTrashed(f_file) or
|
|
not DLP.CheckFileSize(f_file, sizeField) or
|
|
not DLP.CheckFilenameMatch(f_file) or
|
|
(not checkSharedDrivePermissions and not DLP.CheckFilePermissionMatches(f_file)) or
|
|
(DLP.onlySharedDrives and not driveId)):
|
|
continue
|
|
if checkSharedDrivePermissions:
|
|
try:
|
|
f_file['permissions'] = callGAPIpages(drive.permissions(), 'list', 'permissions',
|
|
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS+[GAPI.BAD_REQUEST],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
fileId=f_file['id'], fields=permissionsFields, supportsAllDrives=True)
|
|
if not DLP.CheckFilePermissionMatches(f_file):
|
|
continue
|
|
for permission in f_file['permissions']:
|
|
permission.pop('teamDrivePermissionDetails', None)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
|
|
GAPI.unknownError, GAPI.invalid, GAPI.badRequest,
|
|
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
continue
|
|
mimeTypeInfo.setdefault(f_file['mimeType'], {'count': 0, 'size': 0})
|
|
mimeTypeInfo[f_file['mimeType']]['count'] += 1
|
|
mimeTypeInfo[f_file['mimeType']]['size'] += int(f_file.get(sizeField, '0'))
|
|
if showLastModification:
|
|
_checkUpdateLastModifiction(f_file, userLastModification)
|
|
_getLastModificationPath(drive, userLastModification, pathDelimiter)
|
|
showMimeTypeInfo(user, mimeTypeInfo, sharedDriveId, sharedDriveName, userLastModification, i, count)
|
|
if showLastModification and userLastModification['lastModifiedTime'] > summaryLastModification['lastModifiedTime']:
|
|
summaryLastModification = userLastModification.copy()
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], invalidQuery(DLP.fileIdEntity['query']), i, count)
|
|
if not continueOnInvalidQuery:
|
|
break
|
|
continue
|
|
except GAPI.fileNotFound:
|
|
printGotEntityItemsForWhom(0)
|
|
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, sharedDriveId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
if summary != FILECOUNT_SUMMARY_NONE:
|
|
showMimeTypeInfo(summaryUser, summaryMimeTypeInfo,
|
|
'' if count > 1 else sharedDriveId,
|
|
'' if count > 1 else sharedDriveName,
|
|
summaryLastModification, 0, 0)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive File Counts')
|
|
|
|
# gam <UserTypeEntity> print drivelastmodification [todrive <ToDriveAttribute>*]
|
|
# [select <SharedDriveEntity>]
|
|
# [pathdelimiter <Character>]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# gam <UserTypeEntity> show drivelastmodification
|
|
# [select <SharedDriveEntity>]
|
|
# [pathdelimiter <Character>]
|
|
def printShowDrivelastModifications(users):
|
|
def showLastModificationInfo(user, sharedDriveId, sharedDriveName, lastModification, i, count):
|
|
if not csvPF:
|
|
if sharedDriveId:
|
|
kvList = [Ent.USER, user, Ent.SHAREDDRIVE, f'{sharedDriveName} ({sharedDriveId})']
|
|
else:
|
|
kvList = [Ent.USER, user]
|
|
printEntity(kvList, i, count)
|
|
Ind.Increment()
|
|
_showLastModification(lastModification)
|
|
Ind.Decrement()
|
|
else:
|
|
if sharedDriveId:
|
|
row = {'User': user, 'id': sharedDriveId, 'name': sharedDriveName}
|
|
else:
|
|
row = {'User': user}
|
|
_updateLastModificationRow(row, lastModification)
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRowTitles(row)
|
|
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
fieldsList = ['id', 'driveId', 'name', 'mimeType', 'lastModifyingUser', 'modifiedTime', 'parents']
|
|
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': False, 'allowQuery': False, 'mimeTypeInQuery': True})
|
|
pathDelimiter = '/'
|
|
sharedDriveId = sharedDriveName = ''
|
|
fileIdEntity = {}
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'select':
|
|
if fileIdEntity:
|
|
usageErrorExit(Msg.CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE.format('select'))
|
|
fileIdEntity = getSharedDriveEntity()
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not fileIdEntity:
|
|
fileIdEntity = DLP.GetFileIdEntity()
|
|
if not fileIdEntity.get('shareddrive'):
|
|
btkwargs = DLP.kwargs
|
|
else:
|
|
btkwargs = fileIdEntity['shareddrive']
|
|
fieldsList.append('driveId')
|
|
DLP.Finalize(fileIdEntity)
|
|
if csvPF:
|
|
sortTitles = ['User', 'id', 'name'] if fileIdEntity.get('shareddrive') else ['User']
|
|
sortTitles.extend(['lastModifiedFileId', 'lastModifiedFileName',
|
|
'lastModifiedFileMimeType', 'lastModifiedFilePath',
|
|
'lastModifyingUser', 'lastModifiedTime'])
|
|
if addCSVData:
|
|
sortTitles.extend(sorted(addCSVData.keys()))
|
|
csvPF.SetTitles(sortTitles)
|
|
csvPF.SetSortAllTitles()
|
|
pagesFields = getItemFieldsFromFieldsList('files', fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
sharedDriveId = fileIdEntity.get('shareddrive', {}).get('driveId', '')
|
|
sharedDriveName = _getSharedDriveNameFromId(sharedDriveId) if sharedDriveId else ''
|
|
userLastModification = _initLastModification()
|
|
gettingEntity = _getGettingEntity(user, fileIdEntity)
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count)
|
|
try:
|
|
feed = yieldGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
|
|
GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
fields=pagesFields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **btkwargs)
|
|
for files in feed:
|
|
for f_file in files:
|
|
_checkUpdateLastModifiction(f_file, userLastModification)
|
|
_getLastModificationPath(drive, userLastModification, pathDelimiter)
|
|
showLastModificationInfo(user, sharedDriveId, sharedDriveName, userLastModification, i, count)
|
|
except (GAPI.invalid, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, None], str(e), i, count)
|
|
continue
|
|
except GAPI.fileNotFound:
|
|
printGotEntityItemsForWhom(0)
|
|
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, sharedDriveId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive File Last Modification')
|
|
|
|
DISKUSAGE_SHOW_CHOICES = {'all', 'summary', 'summaryandtrash'}
|
|
|
|
# gam <UserTypeEntity> print diskusage <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# [anyowner|(showownedby any|me|others)]
|
|
# [sizefield quotabytesused|size]
|
|
# [pathdelimiter <Character>] [excludetrashed] [stripcrsfromname]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [noprogress] [show all|summary|summaryandtrash]
|
|
def printDiskUsage(users):
|
|
def _getChildDriveFolderInfo(drive, fileEntry, user, i, count, depth):
|
|
fileEntry['depth'] = depth
|
|
q = WITH_PARENTS.format(fileEntry['id'])
|
|
try:
|
|
children = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=q, orderBy=orderBy, fields=pagesFields,
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], supportsAllDrives=True, includeItemsFromAllDrives=True)
|
|
except (GAPI.invalidQuery, GAPI.invalid):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, None], invalidQuery(q), i, count)
|
|
return
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return
|
|
Ind.Increment()
|
|
if showProgress:
|
|
entityActionPerformed([Ent.USER, user, Ent.DRIVE_FOLDER, fileEntry['path']])
|
|
for childEntryInfo in children:
|
|
trashed = childEntryInfo['trashed']
|
|
if trashed and excludeTrashed:
|
|
continue
|
|
mimeType = childEntryInfo.pop('mimeType')
|
|
if mimeType == MIMETYPE_GA_FOLDER:
|
|
fileEntry['directFolderCount'] += 1
|
|
fileEntry['totalFolderCount'] += 1
|
|
if trashed:
|
|
trashFolder['totalFolderCount'] += 1
|
|
if childEntryInfo['explicitlyTrashed']:
|
|
trashFolder['directFolderCount'] += 1
|
|
childEntryInfo['User'] = user
|
|
if includeOwner:
|
|
owners = childEntryInfo.pop('owners', [])
|
|
if owners:
|
|
childEntryInfo['Owner'] = owners[0].get('emailAddress', 'Unknown')
|
|
childEntryInfo.update(zeroFolderInfo)
|
|
if stripCRsFromName:
|
|
childEntryInfo['name'] = _stripControlCharsFromName(childEntryInfo['name'])
|
|
childEntryInfo['path'] = fileEntry['path']+pathDelimiter+childEntryInfo['name']
|
|
childEntryInfo.pop(sizeField, None)
|
|
foldersList.append(childEntryInfo)
|
|
_getChildDriveFolderInfo(drive, childEntryInfo, user, i, count, depth+1)
|
|
fileEntry['totalFileCount'] += childEntryInfo['totalFileCount']
|
|
fileEntry['totalFileSize'] += childEntryInfo['totalFileSize']
|
|
fileEntry['totalFolderCount'] += childEntryInfo['totalFolderCount']
|
|
elif mimeType != MIMETYPE_GA_SHORTCUT:
|
|
if includeOwner and showOwnedBy is not None and childEntryInfo['ownedByMe'] != showOwnedBy:
|
|
continue
|
|
fsize = int(childEntryInfo.get(sizeField, '0'))
|
|
fileEntry['directFileCount'] += 1
|
|
fileEntry['directFileSize'] += fsize
|
|
fileEntry['totalFileCount'] += 1
|
|
fileEntry['totalFileSize'] += fsize
|
|
if trashed:
|
|
trashFolder['totalFileCount'] += 1
|
|
trashFolder['totalFileSize'] += fsize
|
|
if childEntryInfo['explicitlyTrashed']:
|
|
trashFolder['directFileCount'] += 1
|
|
trashFolder['directFileSize'] += fsize
|
|
Ind.Decrement()
|
|
|
|
csvPF = CSVPrintFile(['User', 'Owner', 'id', 'name', 'ownedByMe', 'trashed', 'explicitlyTrashed',
|
|
'directFileCount', 'directFileSize', 'directFolderCount',
|
|
'totalFileCount', 'totalFileSize', 'totalFolderCount', 'depth', 'path'])
|
|
excludeTrashed = stripCRsFromName = False
|
|
includeOwner = True
|
|
orderBy = 'folder,name'
|
|
zeroFolderInfo = {'directFileCount': 0, 'directFileSize': 0, 'directFolderCount': 0,
|
|
'totalFileCount': 0, 'totalFileSize': 0, 'totalFolderCount': 0}
|
|
sizeField = 'quotaBytesUsed'
|
|
showOwnedBy = showProgress = True
|
|
pathDelimiter = '/'
|
|
fileIdEntity = getDriveFileEntity()
|
|
addCSVData = {}
|
|
showResults = 'all'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'anyowner':
|
|
showOwnedBy = None
|
|
elif myarg == 'showownedby':
|
|
showOwnedBy = getChoice(SHOW_OWNED_BY_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'sizefield':
|
|
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
elif myarg == 'excludetrashed':
|
|
excludeTrashed = True
|
|
elif myarg == 'stripcrsfromname':
|
|
stripCRsFromName = True
|
|
elif myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'show':
|
|
showResults = getChoice(DISKUSAGE_SHOW_CHOICES)
|
|
elif myarg == 'noprogress':
|
|
showProgress = False
|
|
else:
|
|
unknownArgumentExit()
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
fieldsList = ['id', 'name', 'mimeType', sizeField, 'trashed', 'explicitlyTrashed', 'owners(emailAddress)', 'ownedByMe']
|
|
pagesFields = getItemFieldsFromFieldsList('files', fieldsList)
|
|
topFieldsList = fieldsList[:]
|
|
topFieldsList.extend(['driveId', 'parents'])
|
|
topFields = getFieldsFromFieldsList(topFieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
i = 0
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive, jcount = _validateUserGetFileIDs(origUser, i, count, fileIdEntity, entityType=Ent.DRIVE_DISK_USAGE)
|
|
if jcount == 0:
|
|
continue
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
foldersList = []
|
|
trashFolder = {'User': user, 'id': 'Trash', 'name': 'Trash', 'path': 'Trash'}
|
|
trashFolder.update(zeroFolderInfo)
|
|
try:
|
|
topFolder = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields=topFields, supportsAllDrives=True)
|
|
if stripCRsFromName:
|
|
topFolder['name'] = _stripControlCharsFromName(topFolder['name'])
|
|
mimeType = topFolder.pop('mimeType')
|
|
if mimeType != MIMETYPE_GA_FOLDER:
|
|
entityValueList = [Ent.USER, user, _getEntityMimeType(topFolder), topFolder['name']]
|
|
entityActionNotPerformedWarning(entityValueList, Msg.INVALID_MIMETYPE.format(mimeType, MIMETYPE_GA_FOLDER), i, count)
|
|
continue
|
|
if topFolder['trashed']:
|
|
if excludeTrashed:
|
|
entityValueList = [Ent.USER, user, Ent.DRIVE_FOLDER, topFolder['name']]
|
|
entityActionNotPerformedWarning(entityValueList, Msg.IN_TRASH_AND_EXCLUDE_TRASHED, i, count)
|
|
continue
|
|
trashFolder['totalFolderCount'] += 1
|
|
if topFolder['explicitlyTrashed']:
|
|
trashFolder['directFolderCount'] += 1
|
|
driveId = topFolder.pop('driveId', None)
|
|
if driveId:
|
|
includeOwner = False
|
|
csvPF.RemoveTitles(['Owner', 'ownedByMe'])
|
|
if topFolder['name'] == TEAM_DRIVE and not topFolder.get('parents'):
|
|
topFolder['name'] = _getSharedDriveNameFromId(driveId)
|
|
topFolder['path'] = f'{SHARED_DRIVES}{pathDelimiter}{topFolder["name"]}'
|
|
else:
|
|
topFolder['path'] = topFolder['name']
|
|
elif topFolder['name'] == MY_DRIVE and not topFolder.get('parents'):
|
|
topFolder['path'] = MY_DRIVE
|
|
else:
|
|
topFolder['path'] = topFolder['name']
|
|
topFolder['User'] = user
|
|
if includeOwner:
|
|
owners = topFolder.pop('owners', [])
|
|
if owners:
|
|
topFolder['Owner'] = owners[0].get('emailAddress', 'Unknown')
|
|
trashFolder['Owner'] = topFolder['Owner']
|
|
topFolder.pop('ownedByMe', None)
|
|
topFolder.pop('parents', None)
|
|
topFolder.update(zeroFolderInfo)
|
|
topFolder.pop(sizeField, None)
|
|
foldersList.append(topFolder)
|
|
_getChildDriveFolderInfo(drive, topFolder, user, i, count, -1)
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, fileId], Msg.NOT_FOUND, j, jcount)
|
|
continue
|
|
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
if showResults == 'all':
|
|
for folder in foldersList:
|
|
if addCSVData:
|
|
folder.update(addCSVData)
|
|
csvPF.WriteRow(folder)
|
|
else:
|
|
folder = foldersList[0]
|
|
if addCSVData:
|
|
folder.update(addCSVData)
|
|
csvPF.WriteRow(folder)
|
|
if showResults != 'summary' and not excludeTrashed:
|
|
trashFolder['trashed'] = trashFolder['totalFileCount']+trashFolder['totalFolderCount'] > 0
|
|
trashFolder['explicitlyTrashed'] = trashFolder['directFileCount']+trashFolder['directFolderCount'] > 0
|
|
if addCSVData:
|
|
trashFolder.update(addCSVData)
|
|
trashFolder['depth'] = -1
|
|
csvPF.WriteRow(trashFolder)
|
|
csvPF.writeCSVfile('Drive Disk Usage')
|
|
|
|
FILESHARECOUNTS_OWNER = 'Owner'
|
|
FILESHARECOUNTS_TOTAL = 'Total'
|
|
FILESHARECOUNTS_SHARED = 'Shared'
|
|
FILESHARECOUNTS_SHARED_EXTERNAL = 'Shared External'
|
|
FILESHARECOUNTS_SHARED_INTERNAL = 'Shared Internal'
|
|
|
|
FILESHARECOUNTS_ZEROCOUNTS = {
|
|
FILESHARECOUNTS_TOTAL: 0,
|
|
FILESHARECOUNTS_SHARED: 0,
|
|
FILESHARECOUNTS_SHARED_EXTERNAL: 0,
|
|
FILESHARECOUNTS_SHARED_INTERNAL: 0,
|
|
'anyone': 0, 'anyoneWithLink': 0,
|
|
'externalDomain': 0, 'externalDomainWithLink': 0,
|
|
'internalDomain': 0, 'internalDomainWithLink': 0,
|
|
'externalGroup': 0, 'internalGroup': 0,
|
|
'externalUser': 0, 'internalUser': 0,
|
|
'deletedGroup': 0, 'deletedUser': 0,
|
|
}
|
|
|
|
FILESHARECOUNTS_CATEGORIES = {
|
|
'anyone': {False: 'anyone', True: 'anyoneWithLink'},
|
|
'domain': {False: {False: 'externalDomain', True: 'externalDomainWithLink'}, True: {False: 'internalDomain', True: 'internalDomainWithLink'}},
|
|
'group': {False: 'externalGroup', True: 'internalGroup'},
|
|
'user': {False: 'externalUser', True: 'internalUser'},
|
|
'deleted': {'group': 'deletedGroup', 'user': 'deletedUser'},
|
|
}
|
|
|
|
# gam <UserTypeEntity> print filesharecounts [todrive <ToDriveAttribute>*]
|
|
# [excludetrashed]
|
|
# [internaldomains <DomainNameList>]
|
|
# [summary none|only|plus] [summaryuser <String>]
|
|
# gam <UserTypeEntity> show filesharecounts
|
|
# [excludetrashed]
|
|
# [internaldomains <DomainNameList>]
|
|
# [summary none|only|plus] [summaryuser <String>]
|
|
def printShowFileShareCounts(users):
|
|
def incrementCounter(counter):
|
|
if not counterSet[counter]:
|
|
userShareCounts[counter] += 1
|
|
counterSet[counter] = True
|
|
|
|
def showShareCounts(user, shareCounts, i, count):
|
|
if summary != FILECOUNT_SUMMARY_NONE:
|
|
if count != 0:
|
|
for field, shareCount in iter(shareCounts.items()):
|
|
summaryShareCounts[field] += shareCount
|
|
if summary == FILECOUNT_SUMMARY_ONLY:
|
|
return
|
|
if not csvPF:
|
|
printEntity([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, shareCounts[FILESHARECOUNTS_TOTAL]], i, count)
|
|
Ind.Increment()
|
|
for field, shareCount in iter(shareCounts.items()):
|
|
printKeyValueList([field, shareCount])
|
|
Ind.Decrement()
|
|
else:
|
|
row = {FILESHARECOUNTS_OWNER: user}
|
|
row.update(shareCounts)
|
|
csvPF.WriteRow(row)
|
|
|
|
csvPF = CSVPrintFile([FILESHARECOUNTS_OWNER]+list(FILESHARECOUNTS_ZEROCOUNTS.keys())) if Act.csvFormat() else None
|
|
query = ME_IN_OWNERS
|
|
summary = FILECOUNT_SUMMARY_NONE
|
|
summaryUser = FILECOUNT_SUMMARY_USER
|
|
fileIdEntity = {}
|
|
internalDomains = [GC.Values[GC.DOMAIN]]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'excludetrashed':
|
|
query += ' and trashed=false'
|
|
elif myarg == 'internaldomains':
|
|
internalDomains = getString(Cmd.OB_DOMAIN_NAME_LIST).replace(',', ' ').split()
|
|
elif myarg == 'summary':
|
|
summary = getChoice(FILECOUNT_SUMMARY_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'summaryuser':
|
|
summaryUser = getString(Cmd.OB_STRING)
|
|
else:
|
|
unknownArgumentExit()
|
|
summaryShareCounts = FILESHARECOUNTS_ZEROCOUNTS.copy()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
userShareCounts = FILESHARECOUNTS_ZEROCOUNTS.copy()
|
|
gettingEntity = _getGettingEntity(user, fileIdEntity)
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count, query=query)
|
|
try:
|
|
feed = yieldGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=query, fields='nextPageToken,files(permissions(type,role,emailAddress,domain,allowFileDiscovery,deleted))',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
for files in feed:
|
|
for f_file in files:
|
|
counterSet = {FILESHARECOUNTS_TOTAL: False, FILESHARECOUNTS_SHARED: False,
|
|
FILESHARECOUNTS_SHARED_EXTERNAL: False, FILESHARECOUNTS_SHARED_INTERNAL: False}
|
|
for permission in f_file['permissions']:
|
|
if permission['role'] == 'owner':
|
|
incrementCounter(FILESHARECOUNTS_TOTAL)
|
|
else:
|
|
incrementCounter(FILESHARECOUNTS_SHARED)
|
|
ptype = permission['type']
|
|
if ptype == 'anyone':
|
|
incrementCounter(FILESHARECOUNTS_SHARED_EXTERNAL)
|
|
userShareCounts[FILESHARECOUNTS_CATEGORIES[ptype][not permission['allowFileDiscovery']]] += 1
|
|
else:
|
|
domain = permission.get('domain', '')
|
|
if not domain and ptype in ['user', 'group']:
|
|
if permission.get('deleted', False):
|
|
userShareCounts[FILESHARECOUNTS_CATEGORIES['deleted'][ptype]] += 1
|
|
continue
|
|
emailAddress = permission['emailAddress']
|
|
domain = emailAddress[emailAddress.find('@')+1:]
|
|
internal = domain in internalDomains
|
|
incrementCounter([FILESHARECOUNTS_SHARED_EXTERNAL, FILESHARECOUNTS_SHARED_INTERNAL][internal])
|
|
if ptype == 'domain':
|
|
userShareCounts[FILESHARECOUNTS_CATEGORIES[ptype][internal][not permission['allowFileDiscovery']]] +=1
|
|
else: # group, user
|
|
userShareCounts[FILESHARECOUNTS_CATEGORIES[ptype][internal]] += 1
|
|
showShareCounts(user, userShareCounts, i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
if summary != FILECOUNT_SUMMARY_NONE:
|
|
showShareCounts(summaryUser, summaryShareCounts, 0, 0)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive File Share Counts')
|
|
|
|
FILETREE_FIELDS_CHOICE_MAP = {
|
|
'explicitlytrashed': 'explicitlyTrashed',
|
|
'filesize': 'size',
|
|
'id': 'id',
|
|
'mime': 'mimeType',
|
|
'mimetype': 'mimeType',
|
|
'owners': 'owners',
|
|
'parents': 'parents',
|
|
'size': 'size',
|
|
'trashed': 'trashed',
|
|
}
|
|
|
|
FILETREE_FIELDS_PRINT_ORDER = ['id', 'parents', 'owners', 'mimeType', 'size', 'explicitlyTrashed', 'trashed']
|
|
|
|
# gam <UserTypeEntity> print filetree [todrive <ToDriveAttribute>*]
|
|
# [select <DriveFileEntity> [selectsubquery <QueryDriveFile>] [depth <Number>]]
|
|
# [anyowner|(showownedby any|me|others)]
|
|
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>]
|
|
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
|
|
# [filenamematchpattern <REMatchPattern>]
|
|
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
|
# [excludetrashed]
|
|
# [fields <FileTreeFieldNameList>]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])* [delimiter <Character>]
|
|
# [noindent] [stripcrsfromname]
|
|
# gam <UserTypeEntity> show filetree
|
|
# [select <DriveFileEntity> [selectsubquery <QueryDriveFile>] [depth <Number>]]
|
|
# [anyowner|(showownedby any|me|others)]
|
|
# [showmimetype [not] <MimeTypeList>] [showmimetype category <MimeTypeNameList>]
|
|
# [sizefield quotabytesused|size] [minimumfilesize <Integer>] [maximumfilesize <Integer>]
|
|
# [filenamematchpattern <REMatchPattern>]
|
|
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
|
# [excludetrashed]
|
|
# [fields <FileTreeFieldNameList>]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])* [delimiter <Character>]
|
|
# [stripcrsfromname]
|
|
def printShowFileTree(users):
|
|
def _showFileInfo(fileEntry, depth, j=0, jcount=0):
|
|
if not DLP.CheckExcludeTrashed(fileEntry):
|
|
return
|
|
if stripCRsFromName:
|
|
fileEntry['name'] = _stripControlCharsFromName(fileEntry['name'])
|
|
if not csvPF:
|
|
fileInfoList = []
|
|
for field in FILETREE_FIELDS_PRINT_ORDER:
|
|
if showFields[field]:
|
|
if field == 'parents':
|
|
parents = fileEntry.get(field, [])
|
|
fileInfoList.extend([field, f'{len(parents)} [{delimiter.join(parents)}]'])
|
|
elif field == 'owners':
|
|
owners = [owner['emailAddress'] for owner in fileEntry.get(field, [])]
|
|
if owners:
|
|
fileInfoList.extend([field, delimiter.join(owners)])
|
|
elif field in {'explicitlyTrashed', 'trashed'}:
|
|
trashed = fileEntry.get(field, False)
|
|
if trashed:
|
|
fileInfoList.extend([field, trashed])
|
|
elif field == 'size':
|
|
fileInfoList.extend([field, fileEntry.get(sizeField, 0)])
|
|
else:
|
|
fileInfoList.extend([field, fileEntry.get(field, '')])
|
|
if fileInfoList:
|
|
printKeyValueListWithCount([fileEntry['name'], formatKeyValueList('(', fileInfoList, ')')], j, jcount)
|
|
else:
|
|
printKeyValueList([fileEntry['name']])
|
|
else:
|
|
userInfo['index'] += 1
|
|
row = userInfo.copy()
|
|
row['depth'] = depth
|
|
row[fileNameTitle] = ('' if noindent else Ind.SpacesSub1())+fileEntry['name']
|
|
for field in FILETREE_FIELDS_PRINT_ORDER:
|
|
if showFields[field]:
|
|
if field == 'parents':
|
|
row[field] = delimiter.join(fileEntry.get(field, []))
|
|
elif field == 'owners':
|
|
row[field] = delimiter.join([owner['emailAddress'] for owner in fileEntry.get(field, [])])
|
|
elif field == 'size':
|
|
row[fileSize] = fileEntry.get(sizeField, 0)
|
|
else:
|
|
row[field] = fileEntry.get(field, '')
|
|
csvPF.WriteRow(row)
|
|
|
|
def _showDriveFolderContents(fileEntry, depth):
|
|
for childId in fileEntry['children']:
|
|
childEntry = fileTree.get(childId)
|
|
if childEntry:
|
|
if not DLP.CheckExcludeTrashed(childEntry['info']):
|
|
continue
|
|
if (DLP.CheckMimeType(childEntry['info']) and
|
|
DLP.CheckFileSize(childEntry['info'], sizeField) and
|
|
DLP.CheckFilenameMatch(childEntry['info']) and
|
|
DLP.CheckFilePermissionMatches(childEntry['info'])):
|
|
_showFileInfo(childEntry['info'], depth)
|
|
if childEntry['info']['mimeType'] == MIMETYPE_GA_FOLDER and (maxdepth == -1 or depth < maxdepth):
|
|
Ind.Increment()
|
|
_showDriveFolderContents(childEntry, depth+1)
|
|
Ind.Decrement()
|
|
|
|
def _showChildDriveFolderContents(drive, fileEntry, user, i, count, depth):
|
|
if not DLP.CheckExcludeTrashed(fileEntry):
|
|
return
|
|
q = WITH_PARENTS.format(fileEntry['id'])
|
|
if selectSubQuery:
|
|
q += ' and ('+selectSubQuery+')'
|
|
try:
|
|
children = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=q, orderBy=OBY.orderBy, fields=pagesFields,
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], supportsAllDrives=True, includeItemsFromAllDrives=True)
|
|
except (GAPI.invalidQuery, GAPI.invalid):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(selectSubQuery), i, count)
|
|
return
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return
|
|
for childEntryInfo in children:
|
|
if not DLP.CheckExcludeTrashed(childEntryInfo):
|
|
continue
|
|
if (DLP.CheckShowOwnedBy(childEntryInfo) and
|
|
DLP.CheckMimeType(childEntryInfo) and
|
|
DLP.CheckFileSize(childEntryInfo, sizeField) and
|
|
DLP.CheckFilenameMatch(childEntryInfo) and
|
|
DLP.CheckFilePermissionMatches(childEntryInfo)):
|
|
_showFileInfo(childEntryInfo, depth)
|
|
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and (maxdepth == -1 or depth < maxdepth):
|
|
Ind.Increment()
|
|
_showChildDriveFolderContents(drive, childEntryInfo, user, i, count, depth+1)
|
|
Ind.Decrement()
|
|
|
|
csvPF = CSVPrintFile(['User', 'index', 'depth', 'name']) if Act.csvFormat() else None
|
|
maxdepth = -1
|
|
fileIdEntity = {}
|
|
selectSubQuery = ''
|
|
sizeField = 'quotaBytesUsed'
|
|
showFields = {}
|
|
for mappedField in FILETREE_FIELDS_CHOICE_MAP.values():
|
|
showFields[mappedField] = False
|
|
buildTree = noindent = stripCRsFromName = False
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
|
|
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': False, 'allowQuery': False, 'mimeTypeInQuery': False})
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif DLP.ProcessArgument(myarg, fileIdEntity):
|
|
pass
|
|
elif myarg == 'select':
|
|
if fileIdEntity:
|
|
usageErrorExit(Msg.CAN_NOT_BE_SPECIFIED_MORE_THAN_ONCE.format('select'))
|
|
fileIdEntity = getDriveFileEntity(DLP=DLP)
|
|
elif myarg == 'selectsubquery':
|
|
selectSubQuery = getString(Cmd.OB_QUERY, minLen=0)
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg == 'depth':
|
|
maxdepth = getInteger(minVal=-1)
|
|
elif myarg == 'sizefield':
|
|
sizeField = getChoice(SIZE_FIELD_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'fields':
|
|
for field in _getFieldsList():
|
|
if field in FILETREE_FIELDS_CHOICE_MAP:
|
|
showFields[FILETREE_FIELDS_CHOICE_MAP[field]] = True
|
|
if csvPF:
|
|
csvPF.AddTitle(FILETREE_FIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, FILETREE_FIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif csvPF and myarg == 'noindent':
|
|
noindent = True
|
|
elif myarg == 'stripcrsfromname':
|
|
stripCRsFromName = True
|
|
else:
|
|
unknownArgumentExit()
|
|
fieldsList = ['driveId', 'id', 'name', 'parents', 'mimeType', 'ownedByMe', 'owners(emailAddress)',
|
|
'shared', sizeField, 'explicitlyTrashed', 'trashed']
|
|
if csvPF:
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
fileNameTitle = 'title'
|
|
fileSize = 'fileSize'
|
|
csvPF.SetMapDrive3Titles(True)
|
|
else:
|
|
fileNameTitle = 'name'
|
|
fileSize = 'size'
|
|
buildTree = (not fileIdEntity
|
|
or (not fileIdEntity['dict']
|
|
and not fileIdEntity['query']
|
|
and not fileIdEntity['shareddrivefilequery']
|
|
and _simpleFileIdEntityList(fileIdEntity['list'])))
|
|
if buildTree:
|
|
if not fileIdEntity:
|
|
fileIdEntity = initDriveFileEntity()
|
|
DLP.GetFileIdEntity()
|
|
if not fileIdEntity.get('shareddrive'):
|
|
btkwargs = DLP.kwargs
|
|
btkwargs['q'] = DLP.fileIdEntity['query']
|
|
cleanFileIDsList(fileIdEntity, [ROOT, ORPHANS])
|
|
else:
|
|
btkwargs = fileIdEntity['shareddrive']
|
|
DLP.Finalize(fileIdEntity)
|
|
elif not fileIdEntity:
|
|
fileIdEntity = initDriveFileEntity()
|
|
if DLP.PM.permissionMatches:
|
|
fieldsList.append('permissions')
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
pagesFields = getItemFieldsFromFieldsList('files', fieldsList)
|
|
shareddriveFields = []
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
if buildTree:
|
|
fileTree, status = initFileTree(drive, fileIdEntity.get('shareddrive'), DLP, shareddriveFields, True, user, i, count)
|
|
if not status:
|
|
continue
|
|
gettingEntity = _getGettingEntity(user, fileIdEntity)
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, gettingEntity, i, count, query=DLP.fileIdEntity['query'])
|
|
try:
|
|
feed = yieldGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy,
|
|
fields=pagesFields, pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **btkwargs)
|
|
for files in feed:
|
|
extendFileTree(fileTree, files, DLP, stripCRsFromName)
|
|
extendFileTreeParents(drive, fileTree, fields)
|
|
DLP.GetLocationFileIdsFromTree(fileTree, fileIdEntity)
|
|
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
else:
|
|
fileTree = {}
|
|
user, drive, jcount = _validateUserGetFileIDs(origUser, i, count, fileIdEntity, drive=drive, entityType=Ent.DRIVE_FILE_OR_FOLDER)
|
|
if jcount == 0:
|
|
continue
|
|
if csvPF:
|
|
userInfo = {'User': user, 'index': 0, 'depth': 0, fileNameTitle: ''}
|
|
j = 0
|
|
Ind.Increment()
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
fileEntry = fileTree.get(fileId)
|
|
if fileEntry:
|
|
fileEntryInfo = fileEntry['info']
|
|
else:
|
|
try:
|
|
fileEntryInfo = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields=fields, supportsAllDrives=True)
|
|
if (fileEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and fileEntryInfo.get('driveId') and
|
|
fileEntryInfo['name'] == TEAM_DRIVE and not fileEntryInfo.get('parents', [])):
|
|
fileEntryInfo['name'] = f"{SHARED_DRIVES}/{_getSharedDriveNameFromId(fileId)}"
|
|
if stripCRsFromName:
|
|
fileEntryInfo['name'] = _stripControlCharsFromName(fileEntryInfo['name'])
|
|
if buildTree:
|
|
fileTree[fileId] = {'info': fileEntryInfo}
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId], Msg.NOT_FOUND, j, jcount)
|
|
continue
|
|
except (GAPI.notFound, GAPI.teamDriveMembershipRequired) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileIdEntity['shareddrive']['driveId']], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
_showFileInfo(fileEntryInfo, -1, j, jcount)
|
|
Ind.Increment()
|
|
if buildTree:
|
|
_showDriveFolderContents(fileEntry, 0)
|
|
else:
|
|
_showChildDriveFolderContents(drive, fileEntryInfo, user, i, count, 0)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive File Tree')
|
|
|
|
def getCreationModificationTimes(path_to_file):
|
|
"""
|
|
Try to get the date that a file was created, falling back to when it was
|
|
last modified if that isn't possible.
|
|
See http://stackoverflow.com/a/39501288/1709587 for explanation.
|
|
"""
|
|
mtime = os.path.getmtime(path_to_file)
|
|
if platform.system() == 'Windows':
|
|
ctime = os.path.getctime(path_to_file)
|
|
else:
|
|
stat = os.stat(path_to_file)
|
|
try:
|
|
ctime = stat.st_birthtime
|
|
except AttributeError:
|
|
# We're probably on Linux. No easy way to get creation dates here,
|
|
# so we'll settle for when its content was last modified.
|
|
ctime = stat.st_mtime
|
|
return (formatLocalSecondsTimestamp(ctime), formatLocalSecondsTimestamp(mtime))
|
|
|
|
def writeReturnIdLink(returnIdLink, mimeType, result):
|
|
if returnIdLink != 'editLink':
|
|
writeStdout(f'{result[returnIdLink]}\n')
|
|
return
|
|
if mimeType is None:
|
|
writeStdout(f'{result["webViewLink"]}\n')
|
|
return
|
|
for mt in MICROSOFT_FORMATS_LIST:
|
|
if mimeType == mt['mime']:
|
|
if mt['ext'][1] == 'd':
|
|
writeStdout(f'https://docs.google.com/document/d/{result["id"]}/edit\n')
|
|
return
|
|
if mt['ext'][1] == 'x':
|
|
writeStdout(f'https://docs.google.com/spreadsheets/d/{result["id"]}/edit\n')
|
|
return
|
|
if mt['ext'][1] == 'p':
|
|
writeStdout(f'https://docs.google.com/presentation/d/{result["id"]}/edit\n')
|
|
return
|
|
writeStdout(f'https://drive.google.com/file/d/{result["id"]}/edit\n')
|
|
|
|
def processFilenameReplacements(name, replacements):
|
|
for replacement in replacements:
|
|
name = re.sub(replacement[0], replacement[1], name)
|
|
return name
|
|
|
|
def addTimestampToFilename(parameters, body):
|
|
tdtime = datetime.datetime.now(GC.Values[GC.TIMEZONE])
|
|
body['name'] += ' - '
|
|
if not parameters[DFA_TIMEFORMAT]:
|
|
body['name'] += ISOformatTimeStamp(tdtime)
|
|
else:
|
|
body['name'] += tdtime.strftime(parameters[DFA_TIMEFORMAT])
|
|
|
|
createReturnItemMap = {
|
|
'returnidonly': 'id',
|
|
'returnlinkonly': 'webViewLink',
|
|
'returneditlinkonly': 'editLink'
|
|
}
|
|
|
|
# gam <UserTypeEntity> create drivefile
|
|
# [(localfile <FileName>|-)|(url <URL>)]
|
|
# [(drivefilename|newfilename <DriveFileName>) | (replacefilename <REMatchPattern> <RESubstitution>)*]
|
|
# [stripnameprefix <String>]
|
|
# [timestamp <Boolean>]] [timeformat <String>]
|
|
# <DriveFileCreateAttribute>* [noduplicate]
|
|
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*)) |
|
|
# (returnidonly|returnlinkonly|returneditlinkonly|showdetails)]
|
|
def createDriveFile(users):
|
|
csvPF = media_body = None
|
|
addCSVData = {}
|
|
returnIdLink = None
|
|
noDuplicate = showDetails = False
|
|
body = {}
|
|
newName = None
|
|
assignLocalName = True
|
|
parameters = initDriveFileAttributes()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'drivefilename', 'newfilename'}:
|
|
newName = getString(Cmd.OB_DRIVE_FILE_NAME)
|
|
assignLocalName = False
|
|
elif myarg in createReturnItemMap:
|
|
returnIdLink = createReturnItemMap[myarg]
|
|
showDetails = False
|
|
elif myarg == 'showdetails':
|
|
returnIdLink = None
|
|
showDetails = True
|
|
elif myarg == 'noduplicate':
|
|
noDuplicate = True
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile()
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
getDriveFileAttribute(myarg, body, parameters, False)
|
|
if assignLocalName and parameters[DFA_LOCALFILENAME] and parameters[DFA_LOCALFILENAME] != '-':
|
|
newName = parameters[DFA_LOCALFILENAME]
|
|
if newName:
|
|
if parameters[DFA_STRIPNAMEPREFIX] and newName.startswith(parameters[DFA_STRIPNAMEPREFIX]):
|
|
newName = newName[len(parameters[DFA_STRIPNAMEPREFIX]):]
|
|
if parameters[DFA_REPLACEFILENAME]:
|
|
body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
|
|
else:
|
|
body['name'] = newName
|
|
else:
|
|
body['name'] = 'Untitled'
|
|
if parameters[DFA_TIMESTAMP]:
|
|
addTimestampToFilename(parameters, body)
|
|
if parameters[DFA_LOCALFILEPATH]:
|
|
if parameters[DFA_LOCALFILEPATH] != '-' and parameters[DFA_PRESERVE_FILE_TIMES]:
|
|
setPreservedFileTimes(body, parameters, False)
|
|
if body.get('mimeType') == MIMETYPE_GA_SCRIPT_JSON:
|
|
parameters[DFA_LOCALMIMETYPE] = body['mimeType']
|
|
media_body = getMediaBody(parameters)
|
|
elif parameters[DFA_URL]:
|
|
media_body = getMediaBody(parameters)
|
|
body['mimeType'] = parameters[DFA_LOCALMIMETYPE]
|
|
if csvPF:
|
|
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
|
|
csvPF.SetTitles(['User', fileNameTitle, 'id'])
|
|
if showDetails:
|
|
csvPF.AddTitles(['parentId', 'mimeType'])
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
Act.Set(Act.CREATE)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, body, parameters):
|
|
continue
|
|
entityType = _getEntityMimeType(body) if 'mimeType' in body else Ent.DRIVE_FILE
|
|
try:
|
|
if noDuplicate:
|
|
# Check for existing file/folder, do not duplicate
|
|
files = callGAPIitems(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
|
|
q=f"name = '{escapeDriveFileName(body['name'])}'and '{body['parents'][0]}' in parents and trashed = false",
|
|
fields='files(id)', **parameters['searchargs'])
|
|
if files:
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, body['name'], Ent.DRIVE_PARENT_FOLDER_ID, body['parents'][0]],
|
|
f"{Msg.DUPLICATE} IDs {','.join([file['id'] for file in files])}", i, count)
|
|
continue
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.PERMISSION_DENIED, GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
|
|
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.INTERNAL_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
|
|
GAPI.UPLOAD_TOO_LARGE, GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED],
|
|
ocrLanguage=parameters[DFA_OCRLANGUAGE],
|
|
ignoreDefaultVisibility=parameters[DFA_IGNORE_DEFAULT_VISIBILITY],
|
|
keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
|
|
useContentAsIndexableText=parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT],
|
|
media_body=media_body, body=body, fields='id,name,mimeType,parents,webViewLink', supportsAllDrives=True)
|
|
parentId = result['parents'][0] if 'parents' in result and result['parents'] else UNKNOWN
|
|
if returnIdLink:
|
|
writeReturnIdLink(returnIdLink, parameters[DFA_LOCALMIMETYPE], result)
|
|
elif not csvPF:
|
|
kvList = [Ent.USER, user]
|
|
if not showDetails:
|
|
titleInfo = f'{result["name"]}({result["id"]})'
|
|
if parameters[DFA_LOCALFILENAME]:
|
|
kvList.extend([Ent.DRIVE_FILE, titleInfo])
|
|
else:
|
|
kvList.extend([_getEntityMimeType(result), titleInfo])
|
|
else:
|
|
if result['mimeType'] != MIMETYPE_GA_FOLDER:
|
|
kvList.extend([Ent.DRIVE_FILE, result['name'], Ent.DRIVE_FILE_ID, result['id']])
|
|
else:
|
|
kvList.extend([Ent.DRIVE_FOLDER, result['name'], Ent.DRIVE_FOLDER_ID, result['id']])
|
|
kvList.extend([Ent.DRIVE_PARENT_FOLDER_ID, parentId, Ent.MIMETYPE, result['mimeType']])
|
|
if media_body:
|
|
entityModifierNewValueActionPerformed(kvList, Act.MODIFIER_WITH_CONTENT_FROM, parameters[DFA_LOCALFILENAME] or parameters[DFA_URL], i, count)
|
|
else:
|
|
entityActionPerformed(kvList, i, count)
|
|
else:
|
|
row = {'User': user, fileNameTitle: result['name'], 'id': result['id']}
|
|
if showDetails:
|
|
row.update({'parentId': parentId, 'mimeType': result['mimeType']})
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.invalidQuery, GAPI.permissionDenied, GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,
|
|
GAPI.uploadTooLarge, GAPI.teamDrivesShortcutFileNotSupported) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, body['name']], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Files')
|
|
|
|
# gam <UserTypeEntity> create drivefolderpath
|
|
# [pathdelimiter <Character>]
|
|
# ((fullpath <DriveFolderPath) |
|
|
# (path <DriveFolderPath [<DriveFileParentAttribute>]) |
|
|
# (list <DriveFolderNameList>) [<DriveFileParentAttribute>]))
|
|
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*) |
|
|
# returnidonly]
|
|
def createDriveFolderPath(users):
|
|
csvPF = None
|
|
addCSVData = {}
|
|
fullPath = parentSpecified = returnIdOnly = False
|
|
parentBody = {}
|
|
parentParms = initDriveFileAttributes()
|
|
driveFolderNameList = []
|
|
pathDelimiter = '/'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
elif myarg == 'fullpath':
|
|
folderPathLocation = Cmd.Location()
|
|
driveFolderNameList = getString(Cmd.OB_DRIVE_FOLDER_PATH).lstrip(pathDelimiter).strip(' ').split(pathDelimiter)
|
|
if len(driveFolderNameList) > 0:
|
|
if driveFolderNameList[0].lower() == MY_DRIVE.lower():
|
|
parentParms[DFA_PARENTID] = ROOT
|
|
driveFolderNameList = driveFolderNameList[1:]
|
|
elif driveFolderNameList[0].lower() == SHARED_DRIVES.lower() and len(driveFolderNameList) > 1:
|
|
parentParms[DFA_SHAREDDRIVE_PARENT] = driveFolderNameList[1]
|
|
driveFolderNameList = driveFolderNameList[2:]
|
|
else:
|
|
usageErrorExit(Msg.FULL_PATH_MUST_START_WITH_DRIVE.format(MY_DRIVE, f'{SHARED_DRIVES}{pathDelimiter}<SharedDriveName>'))
|
|
fullPath = True
|
|
elif myarg == 'path':
|
|
folderPathLocation = Cmd.Location()
|
|
driveFolderNameList = getString(Cmd.OB_DRIVE_FOLDER_PATH).strip(' ').split(pathDelimiter)
|
|
elif myarg == 'list':
|
|
folderPathLocation = Cmd.Location()
|
|
driveFolderNameList = shlexSplitList(getString(Cmd.OB_DRIVE_FOLDER_NAME_LIST), dataDelimiter=',')
|
|
elif getDriveFileParentAttribute(myarg, parentParms):
|
|
parentSpecified = True
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile(['User', 'name', 'id', 'status', 'pathIndex', 'pathLength'], 'sortall')
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not driveFolderNameList:
|
|
Cmd.SetLocation(folderPathLocation)
|
|
emptyArgumentExit('fullpath|path|list')
|
|
if fullPath and parentSpecified:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('fullpath', '<DriveFileParentAttribute>'))
|
|
for folderName in driveFolderNameList:
|
|
if not folderName.strip():
|
|
Cmd.SetLocation(folderPathLocation)
|
|
usageErrorExit(Msg.ALL_FOLDER_NAMES_MUST_BE_NON_BLANK.format(driveFolderNameList))
|
|
if csvPF:
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms,
|
|
defaultToRoot=True, entityType=Ent.DRIVE_FOLDER):
|
|
continue
|
|
parentId = parentBody['parents'][0]
|
|
if parentParms.get('searchargs', {}).get('corpora', ''):
|
|
query = ANY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS
|
|
else:
|
|
query = MY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS
|
|
errors = False
|
|
createOnly = False
|
|
if not returnIdOnly and not csvPF:
|
|
entityPerformAction([Ent.USER, user, Ent.DRIVE_FOLDER_PATH, ''], i, count)
|
|
jcount = len(driveFolderNameList)
|
|
Ind.Increment()
|
|
j = 0
|
|
for folderName in driveFolderNameList:
|
|
j += 1
|
|
try:
|
|
folderFound = False
|
|
if not createOnly:
|
|
op = 'Find Folder'
|
|
result = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INSUFFICIENT_PERMISSIONS],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=query.format(escapeDriveFileName(folderName), parentId),
|
|
supportsAllDrives=True, includeItemsFromAllDrives=True,
|
|
fields='nextPageToken,files(id,name)')
|
|
if result:
|
|
if len(result) > 1:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, folderName], f'{op}: {len(result)} Folders with same name')
|
|
errors = True
|
|
break
|
|
parentId = result[0]['id']
|
|
parentName = result[0]['name']
|
|
folderFound = True
|
|
Act.Set(Act.EXISTS)
|
|
if not folderFound:
|
|
op = 'Create Folder'
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.UNKNOWN_ERROR, GAPI.BAD_REQUEST,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
|
|
body={'parents': [parentId], 'name': folderName, 'mimeType': MIMETYPE_GA_FOLDER}, fields='id,name', supportsAllDrives=True)
|
|
parentId = result['id']
|
|
parentName = result['name']
|
|
createOnly = True
|
|
Act.Set(Act.CREATE)
|
|
except (GAPI.forbidden, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.unknownError, GAPI.badRequest, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, folderName], f'{op}: {str(e)}', j, jcount)
|
|
errors = True
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
errors = True
|
|
break
|
|
if not returnIdOnly:
|
|
if not csvPF:
|
|
entityActionPerformed([Ent.USER, user, Ent.DRIVE_FOLDER_NAME, f'{parentName}({parentId})'],
|
|
j, jcount)
|
|
else:
|
|
row = {'User': user, 'name': parentName, 'id': parentId, 'status': Act.Performed(), 'pathIndex': j, 'pathLength': jcount}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
if returnIdOnly and not errors:
|
|
writeStdout(f'{parentId}\n')
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Folders')
|
|
|
|
# gam <UserTypeEntity> create drivefileshortcut <DriveFileEntity> [shortcutname <String>]
|
|
# [<DriveFileParentAttribute>|convertparents]
|
|
# [(csv [todrive <ToDriveAttribute>*]) | returnidonly]
|
|
def createDriveFileShortcut(users):
|
|
csvPF = baseShortcutName = None
|
|
convertParents = newParentsSpecified = returnIdOnly = False
|
|
fileIdEntity = getDriveFileEntity()
|
|
parentBody = {}
|
|
parentParms = initDriveFileAttributes()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'shortcutname':
|
|
baseShortcutName = getString(Cmd.OB_DRIVE_FILE_NAME)
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile(['User', 'name', 'id', 'targetName', 'targetId'], 'sortall')
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getDriveFileParentAttribute(myarg, parentParms):
|
|
if convertParents:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'convertparents'))
|
|
newParentsSpecified = True
|
|
elif myarg == 'convertparents':
|
|
if newParentsSpecified:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, '<DriveFileParentAttribute>'))
|
|
convertParents = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if fileIdEntity['query']:
|
|
fileIdEntity['query'] = fileIdEntity['query']+AND_NOT_SHORTCUT
|
|
elif fileIdEntity['shareddrivefilequery']:
|
|
fileIdEntity['shareddrivefilequery'] = fileIdEntity['shareddrivefilequery']+AND_NOT_SHORTCUT
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
|
|
if not returnIdOnly and not csvPF:
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.DRIVE_FILE_SHORTCUT,
|
|
Act.MODIFIER_FOR, jcount, Ent.DRIVE_FILE_OR_FOLDER, i, count)
|
|
if jcount == 0:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms):
|
|
continue
|
|
if newParentsSpecified:
|
|
newParents = parentBody['parents']
|
|
numNewParents = len(newParents)
|
|
elif not convertParents:
|
|
try:
|
|
rootFolderId = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=ROOT, fields='id')['id']
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
newParents = [rootFolderId]
|
|
numNewParents = 1
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
Act.Set(Act.CREATE)
|
|
try:
|
|
target = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='mimeType,name,parents', supportsAllDrives=True)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.invalid, GAPI.badRequest,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.teamDrivesSharingRestrictionNotAllowed) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
targetName = target['name']
|
|
if baseShortcutName:
|
|
shortcutName = baseShortcutName.replace('#filename#', targetName)
|
|
else:
|
|
shortcutName = targetName
|
|
targetEntityType = _getEntityMimeType(target)
|
|
if convertParents:
|
|
newParents = target.get('parents', [])[:-1]
|
|
numNewParents = len(newParents)
|
|
if numNewParents <= 1:
|
|
entityActionNotPerformedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, None],
|
|
Msg.NO_PARENTS_TO_CONVERT_TO_SHORTCUTS, j, jcount)
|
|
continue
|
|
removeParents = []
|
|
body = {'name': shortcutName, 'mimeType': MIMETYPE_GA_SHORTCUT, 'parents': None, 'shortcutDetails': {'targetId': fileId}}
|
|
if not returnIdOnly and not csvPF:
|
|
entityPerformActionNumItems([Ent.USER, user, targetEntityType, targetName], numNewParents, Ent.DRIVE_FILE_SHORTCUT, j, jcount)
|
|
try:
|
|
existingShortcuts = callGAPI(drive.files(), 'list',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
supportsAllDrives=True, includeItemsFromAllDrives=True,
|
|
q=f"shortcutDetails.targetId = '{fileId}' and trashed = False", fields='files(id,name,parents)')['files']
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
existingShortcuts = []
|
|
Ind.Increment()
|
|
k = 0
|
|
for parentId in newParents:
|
|
k += 1
|
|
duplicateShortcut = False
|
|
for shortcut in existingShortcuts:
|
|
if parentId in shortcut['parents'] and shortcutName == shortcut['name']:
|
|
entityActionNotPerformedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, f'{shortcut["name"]}({shortcut["id"]})'],
|
|
Msg.DUPLICATE, k, numNewParents)
|
|
duplicateShortcut = True
|
|
break
|
|
if duplicateShortcut:
|
|
continue
|
|
body['parents'] = [parentId]
|
|
try:
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
|
|
GAPI.SHORTCUT_TARGET_INVALID],
|
|
body=body, fields='id,name', supportsAllDrives=True)
|
|
removeParents.append(parentId)
|
|
if returnIdOnly:
|
|
writeStdout(f'{result["id"]}\n')
|
|
elif not csvPF:
|
|
entityActionPerformed([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, f'{result["name"]}({result["id"]})'],
|
|
k, numNewParents)
|
|
else:
|
|
csvPF.WriteRow({'User': user, 'name': result['name'], 'id': result['id'], 'targetName': targetName, 'targetId': fileId})
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e:
|
|
entityActionFailedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_FILE_SHORTCUT, body['name']], str(e), k, numNewParents)
|
|
Ind.Decrement()
|
|
if convertParents and removeParents:
|
|
if not returnIdOnly and not csvPF:
|
|
lcount = len(removeParents)
|
|
Act.Set(Act.DELETE)
|
|
entityPerformActionNumItems([Ent.USER, user, targetEntityType, targetName], lcount, Ent.DRIVE_PARENT_FOLDER_REFERENCE, j, jcount)
|
|
try:
|
|
callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST],
|
|
fileId=fileId,
|
|
removeParents=','.join(removeParents), body={}, fields='id', supportsAllDrives=True)
|
|
if not returnIdOnly and not csvPF:
|
|
Ind.Increment()
|
|
for l, parent in enumerate(removeParents):
|
|
entityActionPerformed([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_PARENT_FOLDER_REFERENCE, parent], l+1, lcount)
|
|
Ind.Decrement()
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.invalid, GAPI.badRequest,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.teamDrivesSharingRestrictionNotAllowed) as e:
|
|
entityActionFailedWarning([Ent.USER, user, targetEntityType, targetName, Ent.DRIVE_PARENT_FOLDER_REFERENCE, str(l)], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Shortcuts')
|
|
|
|
SHORTCUT_CODE_VALID = 0
|
|
SHORTCUT_CODE_SHORTCUT_NOT_FOUND = 1
|
|
SHORTCUT_CODE_NOT_A_SHORTCUT = 2
|
|
SHORTCUT_CODE_TARGET_NOT_FOUND = 3
|
|
SHORTCUT_CODE_MIMETYPE_MISMATCH = 4
|
|
|
|
# gam <UserTypeEntity> check drivefileshortcut <DriveFileEntity>
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
def checkDriveFileShortcut(users):
|
|
csvPF = None
|
|
fileIdEntity = getDriveFileEntity()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'csv':
|
|
csvPF = CSVPrintFile(['User', 'name', 'id', 'owner', 'parentId', 'shortcutDetails.targetId', 'shortcutDetails.targetMimeType',
|
|
'targetName', 'targetId', 'targetMimeType', 'code'], 'sortall')
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
unknownArgumentExit()
|
|
scfields = 'id,name,mimeType,owners(emailAddress),parents,shortcutDetails'
|
|
trfields = 'id,name,mimeType'
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
|
|
if not csvPF:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.DRIVE_FILE_SHORTCUT, i, count)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
row = {'User': user, 'id': fileId}
|
|
try:
|
|
scresult = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields=scfields, supportsAllDrives=True)
|
|
row['name'] = scresult['name']
|
|
if scresult['mimeType'] != MIMETYPE_GA_SHORTCUT:
|
|
if not csvPF:
|
|
entityActionFailedWarning([Ent.USER, user, _getEntityMimeType(scresult), fileId],
|
|
Msg.INVALID_MIMETYPE.format(scresult['mimeType'], MIMETYPE_GA_SHORTCUT), j, jcount)
|
|
else:
|
|
row['code'] = SHORTCUT_CODE_NOT_A_SHORTCUT
|
|
csvPF.WriteRow(row)
|
|
continue
|
|
if 'owners' in scresult:
|
|
row['owner'] = scresult['owners'][0]['emailAddress']
|
|
row['parentId'] = scresult['parents'][0]
|
|
row[f'shortcutDetails{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}targetId'] = scresult['shortcutDetails']['targetId']
|
|
row[f'shortcutDetails{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}targetMimeType'] = scresult['shortcutDetails']['targetMimeType']
|
|
trfileId = scresult['shortcutDetails']['targetId']
|
|
try:
|
|
trresult = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=trfileId, fields=trfields, supportsAllDrives=True)
|
|
row['targetName'] = trresult['name']
|
|
row['targetId'] = trresult['id']
|
|
row['targetMimeType'] = trresult['mimeType']
|
|
entityList = [Ent.USER, user, Ent.DRIVE_FILE_SHORTCUT, f"{scresult['name']}({fileId})",
|
|
_getEntityMimeType(trresult), f"{trresult['name']}({trfileId})"]
|
|
if scresult['shortcutDetails']['targetMimeType'] == trresult['mimeType']:
|
|
if not csvPF:
|
|
entityActionPerformed(entityList, j, jcount)
|
|
else:
|
|
row['code'] = SHORTCUT_CODE_VALID
|
|
else:
|
|
if not csvPF:
|
|
entityActionFailedWarning(entityList, Msg.MIMETYPE_MISMATCH.format(scresult['shortcutDetails']['targetMimeType'], trresult['mimeType']), j, jcount)
|
|
else:
|
|
row['code'] = SHORTCUT_CODE_MIMETYPE_MISMATCH
|
|
except GAPI.fileNotFound:
|
|
if not csvPF:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_SHORTCUT, f"{scresult['name']}({fileId})",
|
|
_getTargetEntityMimeType(scresult), trfileId], Msg.NOT_FOUND, j, jcount)
|
|
else:
|
|
row['code'] = SHORTCUT_CODE_TARGET_NOT_FOUND
|
|
except GAPI.fileNotFound:
|
|
if not csvPF:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_SHORTCUT, fileId], Msg.NOT_FOUND, j, jcount)
|
|
else:
|
|
row['code'] = SHORTCUT_CODE_SHORTCUT_NOT_FOUND
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
if csvPF:
|
|
csvPF.WriteRow(row)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Check Shortcuts')
|
|
|
|
# gam <UserTypeEntity> update drivefile <DriveFileEntity> [copy] [returnidonly|returnlinkonly]
|
|
# [(localfile <FileName>|-)|(url <URL>)]
|
|
# [retainname | (newfilename <DriveFileName>) | (replacefilename <REMatchPattern> <RESubstitution>)*]
|
|
# [stripnameprefix <String>]
|
|
# [timestamp <Boolean>]] [timeformat <String>]
|
|
# <DriveFileUpdateAttribute>*
|
|
# [(gsheet|csvsheet <SheetEntity> [clearfilter])|(addsheet <String>)]
|
|
# [charset <String>] [columndelimiter <Character>]
|
|
def updateDriveFile(users):
|
|
fileIdEntity = getDriveFileEntity()
|
|
body = {}
|
|
newName = None
|
|
assignLocalName = True
|
|
parameters = initDriveFileAttributes()
|
|
media_body = None
|
|
addSheetBody = addSheetEntity = None
|
|
updateSheetEntity = None
|
|
clearFilter = False
|
|
preserveModifiedTime = False
|
|
encoding = GC.Values[GC.CHARSET]
|
|
columnDelimiter = GC.Values[GC.CSV_INPUT_COLUMN_DELIMITER]
|
|
returnIdLink = None
|
|
operation = 'update'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'copy':
|
|
operation = 'copy'
|
|
Act.Set(Act.COPY)
|
|
elif myarg == 'returnidonly':
|
|
returnIdLink = 'id'
|
|
elif myarg == 'returnlinkonly':
|
|
returnIdLink = 'webViewLink'
|
|
elif myarg == 'retainname':
|
|
assignLocalName = False
|
|
elif myarg == 'newfilename':
|
|
newName = getString(Cmd.OB_DRIVE_FILE_NAME)
|
|
assignLocalName = False
|
|
elif getDriveFileAddRemoveParentAttribute(myarg, parameters):
|
|
pass
|
|
elif myarg == 'addsheet':
|
|
if updateSheetEntity:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'csvsheet'))
|
|
sheetTitle = getString(Cmd.OB_STRING)
|
|
addSheetEntity = {'sheetType': Ent.SHEET, 'sheetValue': sheetTitle, 'sheetId': None, 'sheetTitle': sheetTitle}
|
|
addSheetBody = {'requests': [{'addSheet': {'properties': {'title': sheetTitle, 'sheetType': 'GRID'}}}]}
|
|
elif myarg in {'gsheet', 'csvsheet'}:
|
|
if addSheetEntity:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format(myarg, 'addsheet'))
|
|
updateSheetEntity = getSheetEntity(False)
|
|
elif myarg == 'clearfilter':
|
|
clearFilter = getBoolean()
|
|
elif myarg == 'charset':
|
|
encoding = getString(Cmd.OB_CHAR_SET)
|
|
elif myarg == 'columndelimiter':
|
|
columnDelimiter = getCharacter()
|
|
else:
|
|
getDriveFileAttribute(myarg, body, parameters, True)
|
|
if assignLocalName and parameters[DFA_LOCALFILENAME] and parameters[DFA_LOCALFILENAME] != '-':
|
|
newName = parameters[DFA_LOCALFILENAME]
|
|
if newName:
|
|
if parameters[DFA_STRIPNAMEPREFIX] and newName.startswith(parameters[DFA_STRIPNAMEPREFIX]):
|
|
newName = newName[len(parameters[DFA_STRIPNAMEPREFIX]):]
|
|
if operation == 'update' and parameters[DFA_LOCALFILEPATH]:
|
|
if parameters[DFA_LOCALFILEPATH] != '-' and parameters[DFA_PRESERVE_FILE_TIMES]:
|
|
setPreservedFileTimes(body, parameters, True)
|
|
if not addSheetEntity and not updateSheetEntity:
|
|
media_body = getMediaBody(parameters)
|
|
elif operation == 'update' and parameters[DFA_URL]:
|
|
addSheetEntity = updateSheetEntity = None
|
|
media_body = getMediaBody(parameters)
|
|
body['mimeType'] = parameters[DFA_LOCALMIMETYPE]
|
|
elif operation == 'update' and parameters[DFA_PRESERVE_FILE_TIMES]:
|
|
preserveModifiedTime = True
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=Ent.DRIVE_FILE_OR_FOLDER if returnIdLink is None else None)
|
|
if jcount == 0:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, body, parameters, defaultToRoot=False):
|
|
continue
|
|
newParents = body.pop('parents', [])
|
|
if operation == 'update':
|
|
status, addParentsBase, removeParentsBase = _getDriveFileAddRemoveParentInfo(user, i, count, parameters, drive)
|
|
if not status:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
addParents = addParentsBase[:]
|
|
removeParents = removeParentsBase[:]
|
|
if newParents or (not newName and parameters[DFA_REPLACEFILENAME]) or preserveModifiedTime:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='name,parents,modifiedTime', supportsAllDrives=True)
|
|
if newParents:
|
|
addParents.extend(newParents)
|
|
removeParents.extend(result.get('parents', []))
|
|
if not newName and parameters[DFA_REPLACEFILENAME]:
|
|
body['name'] = processFilenameReplacements(result['name'], parameters[DFA_REPLACEFILENAME])
|
|
if preserveModifiedTime:
|
|
body['modifiedTime'] = result['modifiedTime']
|
|
if newName:
|
|
if parameters[DFA_REPLACEFILENAME]:
|
|
body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
|
|
else:
|
|
body['name'] = newName
|
|
if parameters[DFA_TIMESTAMP]:
|
|
addTimestampToFilename(parameters, body)
|
|
if addSheetEntity or updateSheetEntity:
|
|
entityValueList = [Ent.USER, user, Ent.DRIVE_FILE_ID, fileId]
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND],
|
|
fileId=fileId, fields='id,mimeType,capabilities(canEdit)', supportsAllDrives=True)
|
|
if result['mimeType'] != MIMETYPE_GA_SPREADSHEET:
|
|
entityActionNotPerformedWarning(entityValueList, f'{Msg.NOT_A} {Ent.Singular(Ent.SPREADSHEET)}', j, jcount)
|
|
continue
|
|
if not result['capabilities']['canEdit']:
|
|
entityActionNotPerformedWarning(entityValueList, Msg.NOT_WRITABLE, j, jcount)
|
|
continue
|
|
_, sheet = buildGAPIServiceObject(API.SHEETS, user)
|
|
if sheet is None:
|
|
continue
|
|
if addSheetEntity:
|
|
addresult = callGAPI(sheet.spreadsheets(), 'batchUpdate',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=fileId, body=addSheetBody)
|
|
sheetEntity = addSheetEntity.copy()
|
|
sheetEntity['sheetId'] = addresult['replies'][0]['addSheet']['properties']['sheetId']
|
|
entityValueList.extend([sheetEntity['sheetType'], sheetEntity['sheetTitle']])
|
|
sheetEntity['sheetType'] = Ent.SHEET_ID # Temporarily set addsheet type to ID
|
|
else:
|
|
sheetEntity = updateSheetEntity.copy()
|
|
entityValueList.extend([sheetEntity['sheetType'], sheetEntity['sheetValue']])
|
|
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=fileId,
|
|
fields='spreadsheetUrl,sheets(properties(sheetId,title),protectedRanges(range(sheetId),requestingUserCanEdit))')
|
|
sheetId = getSheetIdFromSheetEntity(spreadsheet, sheetEntity)
|
|
if sheetId is None:
|
|
entityActionNotPerformedWarning(entityValueList, Msg.NOT_FOUND, j, jcount)
|
|
continue
|
|
if protectedSheetId(spreadsheet, sheetId):
|
|
entityActionNotPerformedWarning(entityValueList, Msg.NOT_WRITABLE, j, jcount)
|
|
continue
|
|
if addSheetEntity: # Restore addsheet type
|
|
sheetEntity['sheetType'] = Ent.SHEET
|
|
result = callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
|
|
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT,
|
|
GAPI.TEAMDRIVES_PARENT_LIMIT,
|
|
GAPI.TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED,
|
|
GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED],
|
|
fileId=fileId,
|
|
ocrLanguage=parameters[DFA_OCRLANGUAGE],
|
|
keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
|
|
useContentAsIndexableText=parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT],
|
|
addParents=','.join(addParents), removeParents=','.join(removeParents),
|
|
body=body, fields='id,name,mimeType,webViewLink', supportsAllDrives=True)
|
|
### File size check??
|
|
sbody = {'requests': []}
|
|
if clearFilter and updateSheetEntity:
|
|
sbody['requests'].append({'clearBasicFilter': {'sheetId': sheetId}})
|
|
sbody['requests'].append({'updateCells': {'range': {'sheetId': sheetId}, 'fields': '*'}})
|
|
sbody['requests'].append({'pasteData': {'coordinate': {'sheetId': sheetId, 'rowIndex': '0', 'columnIndex': '0'},
|
|
'data': readFile(parameters[DFA_LOCALFILEPATH], encoding=encoding),
|
|
'type': 'PASTE_NORMAL', 'delimiter': columnDelimiter}})
|
|
callGAPI(sheet.spreadsheets(), 'batchUpdate',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=fileId, body=sbody)
|
|
if returnIdLink:
|
|
writeStdout(f'{result[returnIdLink]}\n')
|
|
else:
|
|
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, result['name'], sheetEntity['sheetType'], sheetEntity['sheetValue']],
|
|
Act.MODIFIER_WITH_CONTENT_FROM, parameters[DFA_LOCALFILENAME], j, jcount)
|
|
except GAPI.fileNotFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_ID, fileId], str(e), j, jcount)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning(entityValueList, str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
else:
|
|
result = callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
|
|
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.FILE_NEVER_WRITABLE, GAPI.CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT,
|
|
GAPI.SHARE_OUT_NOT_PERMITTED, GAPI.SHARE_OUT_NOT_PERMITTED_TO_USER,
|
|
GAPI.TEAMDRIVES_PARENT_LIMIT, GAPI.TEAMDRIVES_FOLDER_MOVE_IN_NOT_SUPPORTED,
|
|
GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.CROSS_DOMAIN_MOVE_RESTRICTION, GAPI.UPLOAD_TOO_LARGE,
|
|
GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED,
|
|
GAPI.FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN,
|
|
GAPI.FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED],
|
|
fileId=fileId,
|
|
ocrLanguage=parameters[DFA_OCRLANGUAGE],
|
|
keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
|
|
useContentAsIndexableText=parameters[DFA_USE_CONTENT_AS_INDEXABLE_TEXT],
|
|
addParents=','.join(addParents), removeParents=','.join(removeParents),
|
|
media_body=media_body, body=body, fields='id,name,mimeType,webViewLink',
|
|
supportsAllDrives=True)
|
|
if result:
|
|
if returnIdLink:
|
|
writeStdout(f'{result[returnIdLink]}\n')
|
|
elif media_body:
|
|
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, result['name']],
|
|
Act.MODIFIER_WITH_CONTENT_FROM, parameters[DFA_LOCALFILENAME] or parameters[DFA_URL], j, jcount)
|
|
else:
|
|
entityActionPerformed([Ent.USER, user, _getEntityMimeType(result), result['name']], j, jcount)
|
|
else:
|
|
if returnIdLink:
|
|
writeStdout(f'{fileId}\n')
|
|
else:
|
|
entityActionPerformed([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], j, jcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.unknownError, GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
|
|
GAPI.fileNeverWritable, GAPI.cannotModifyViewersCanCopyContent,
|
|
GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
|
|
GAPI.teamDrivesParentLimit, GAPI.teamDrivesFolderMoveInNotSupported,
|
|
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.crossDomainMoveRestriction,
|
|
GAPI.uploadTooLarge, GAPI.teamDrivesShortcutFileNotSupported,
|
|
GAPI.fileOwnerNotMemberOfWriterDomain, GAPI.fileWriterTeamDriveMoveInDisabled) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
else:
|
|
if newName:
|
|
if parameters[DFA_REPLACEFILENAME]:
|
|
body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
|
|
else:
|
|
body['name'] = newName
|
|
if parameters[DFA_TIMESTAMP]:
|
|
addTimestampToFilename(parameters, body)
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(drive.files(), 'copy',
|
|
throwReasons=GAPI.DRIVE_COPY_THROW_REASONS+[GAPI.CANNOT_MODIFY_VIEWERS_CAN_COPY_CONTENT],
|
|
fileId=fileId,
|
|
ignoreDefaultVisibility=parameters[DFA_IGNORE_DEFAULT_VISIBILITY],
|
|
keepRevisionForever=parameters[DFA_KEEP_REVISION_FOREVER],
|
|
body=body, fields='id,name,webViewLink', supportsAllDrives=True)
|
|
if returnIdLink:
|
|
writeReturnIdLink(returnIdLink, parameters[DFA_LOCALMIMETYPE], result)
|
|
else:
|
|
entityModifierNewValueItemValueListActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, fileId],
|
|
Act.MODIFIER_TO, result['name'], [Ent.DRIVE_FILE_ID, result['id']], j, jcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.invalid, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
|
|
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.rateLimitExceeded, GAPI.userRateLimitExceeded,
|
|
GAPI.cannotModifyViewersCanCopyContent) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, fileId], str(e), j, jcount)
|
|
except (GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, fileId], str(e), j, jcount)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
STAT_FOLDER_TOTAL = 0
|
|
STAT_FOLDER_COPIED_MOVED = 1
|
|
STAT_FOLDER_SHORTCUT_CREATED = 2
|
|
STAT_FOLDER_SHORTCUT_EXISTS = 3
|
|
STAT_FOLDER_MERGED = 4
|
|
STAT_FOLDER_DUPLICATE = 5
|
|
STAT_FOLDER_FAILED = 6
|
|
STAT_FOLDER_NOT_WRITABLE = 7
|
|
STAT_FOLDER_PERMISSIONS_FAILED = 8
|
|
STAT_FILE_TOTAL = 9
|
|
STAT_FILE_COPIED_MOVED = 10
|
|
STAT_FILE_SHORTCUT_CREATED = 11
|
|
STAT_FILE_SHORTCUT_EXISTS = 12
|
|
STAT_FILE_DUPLICATE = 13
|
|
STAT_FILE_FAILED = 14
|
|
STAT_FILE_NOT_COPYABLE_MOVABLE = 15
|
|
STAT_FILE_IN_SKIPIDS = 16
|
|
STAT_FILE_PERMISSIONS_FAILED = 17
|
|
STAT_FILE_PROTECTEDRANGES_FAILED = 18
|
|
STAT_USER_NOT_ORGANIZER = 19
|
|
STAT_LENGTH = 20
|
|
|
|
FOLDER_SUBTOTAL_STATS = [STAT_FOLDER_COPIED_MOVED, STAT_FOLDER_SHORTCUT_CREATED, STAT_FOLDER_SHORTCUT_EXISTS,
|
|
STAT_FOLDER_DUPLICATE, STAT_FOLDER_MERGED, STAT_FOLDER_FAILED, STAT_FOLDER_NOT_WRITABLE]
|
|
FILE_SUBTOTAL_STATS = [STAT_FILE_COPIED_MOVED, STAT_FILE_SHORTCUT_CREATED, STAT_FILE_SHORTCUT_EXISTS,
|
|
STAT_FILE_DUPLICATE, STAT_FILE_FAILED, STAT_FILE_NOT_COPYABLE_MOVABLE, STAT_FILE_IN_SKIPIDS]
|
|
|
|
def _initStatistics():
|
|
return [0] * STAT_LENGTH
|
|
|
|
def _incrStatistic(statistics, stat):
|
|
statistics[stat] += 1
|
|
if stat in FOLDER_SUBTOTAL_STATS:
|
|
statistics[STAT_FOLDER_TOTAL] += 1
|
|
elif stat in FILE_SUBTOTAL_STATS:
|
|
statistics[STAT_FILE_TOTAL] += 1
|
|
|
|
def _printStatistics(user, statistics, i, count, copy):
|
|
if statistics[STAT_FOLDER_TOTAL]:
|
|
if copy:
|
|
printEntityKVList([Ent.USER, user],
|
|
[Ent.Plural(Ent.DRIVE_FOLDER),
|
|
Msg.STATISTICS_COPY_FOLDER.format(statistics[STAT_FOLDER_TOTAL],
|
|
statistics[STAT_FOLDER_COPIED_MOVED],
|
|
statistics[STAT_FOLDER_SHORTCUT_CREATED],
|
|
statistics[STAT_FOLDER_SHORTCUT_EXISTS],
|
|
statistics[STAT_FOLDER_DUPLICATE],
|
|
statistics[STAT_FOLDER_MERGED],
|
|
statistics[STAT_FOLDER_FAILED],
|
|
statistics[STAT_FOLDER_NOT_WRITABLE],
|
|
statistics[STAT_FOLDER_PERMISSIONS_FAILED])],
|
|
i, count)
|
|
else:
|
|
printEntityKVList([Ent.USER, user],
|
|
[Ent.Plural(Ent.DRIVE_FOLDER),
|
|
Msg.STATISTICS_MOVE_FOLDER.format(statistics[STAT_FOLDER_TOTAL],
|
|
statistics[STAT_FOLDER_COPIED_MOVED],
|
|
statistics[STAT_FOLDER_SHORTCUT_CREATED],
|
|
statistics[STAT_FOLDER_SHORTCUT_EXISTS],
|
|
statistics[STAT_FOLDER_DUPLICATE],
|
|
statistics[STAT_FOLDER_MERGED],
|
|
statistics[STAT_FOLDER_FAILED],
|
|
statistics[STAT_FOLDER_NOT_WRITABLE])],
|
|
i, count)
|
|
if statistics[STAT_FILE_TOTAL]:
|
|
if copy:
|
|
printEntityKVList([Ent.USER, user],
|
|
[Ent.Plural(Ent.DRIVE_FILE),
|
|
Msg.STATISTICS_COPY_FILE.format(statistics[STAT_FILE_TOTAL],
|
|
statistics[STAT_FILE_COPIED_MOVED],
|
|
statistics[STAT_FILE_SHORTCUT_CREATED],
|
|
statistics[STAT_FILE_SHORTCUT_EXISTS],
|
|
statistics[STAT_FILE_DUPLICATE],
|
|
statistics[STAT_FILE_FAILED],
|
|
statistics[STAT_FILE_NOT_COPYABLE_MOVABLE],
|
|
statistics[STAT_FILE_IN_SKIPIDS],
|
|
statistics[STAT_FILE_PERMISSIONS_FAILED],
|
|
statistics[STAT_FILE_PROTECTEDRANGES_FAILED])],
|
|
i, count)
|
|
else:
|
|
printEntityKVList([Ent.USER, user],
|
|
[Ent.Plural(Ent.DRIVE_FILE),
|
|
Msg.STATISTICS_MOVE_FILE.format(statistics[STAT_FILE_TOTAL],
|
|
statistics[STAT_FILE_COPIED_MOVED],
|
|
statistics[STAT_FILE_SHORTCUT_CREATED],
|
|
statistics[STAT_FILE_SHORTCUT_EXISTS],
|
|
statistics[STAT_FILE_DUPLICATE],
|
|
statistics[STAT_FILE_FAILED],
|
|
statistics[STAT_FILE_NOT_COPYABLE_MOVABLE])],
|
|
i, count)
|
|
if statistics[STAT_USER_NOT_ORGANIZER]:
|
|
printEntityKVList([Ent.USER, user],
|
|
[Ent.Plural(Ent.DRIVE_FILE_OR_FOLDER),
|
|
Msg.STATISTICS_USER_NOT_ORGANIZER.format(statistics[STAT_USER_NOT_ORGANIZER])],
|
|
i, count)
|
|
|
|
DUPLICATE_FILE_OVERWRITE_OLDER = 0
|
|
DUPLICATE_FILE_OVERWRITE_ALL = 1
|
|
DUPLICATE_FILE_DUPLICATE_NAME = 2
|
|
DUPLICATE_FILE_UNIQUE_NAME = 3
|
|
DUPLICATE_FILE_SKIP = 4
|
|
DUPLICATE_FOLDER_MERGE = 0
|
|
DUPLICATE_FOLDER_DUPLICATE_NAME = 1
|
|
DUPLICATE_FOLDER_UNIQUE_NAME = 2
|
|
DUPLICATE_FOLDER_SKIP = 3
|
|
|
|
DEST_PARENT_MYDRIVE_ROOT = 0
|
|
DEST_PARENT_MYDRIVE_FOLDER = 1
|
|
DEST_PARENT_SHAREDDRIVE_ROOT = 2
|
|
DEST_PARENT_SHAREDDRIVE_FOLDER = 3
|
|
|
|
COPY_NONINHERITED_PERMISSIONS_NEVER = 0
|
|
COPY_NONINHERITED_PERMISSIONS_ALWAYS = 1
|
|
COPY_NONINHERITED_PERMISSIONS_SYNC_ALL_FOLDERS = 2
|
|
COPY_NONINHERITED_PERMISSIONS_SYNC_UPDATED_FOLDERS = 3
|
|
COPY_NONINHERITED_PERMISSIONS_CHOICES_MAP = {
|
|
'never': COPY_NONINHERITED_PERMISSIONS_NEVER,
|
|
'always': COPY_NONINHERITED_PERMISSIONS_ALWAYS,
|
|
'syncallfolders': COPY_NONINHERITED_PERMISSIONS_SYNC_ALL_FOLDERS,
|
|
'syncupdatedfolders': COPY_NONINHERITED_PERMISSIONS_SYNC_UPDATED_FOLDERS,
|
|
}
|
|
|
|
def initCopyMoveOptions(copyCmd):
|
|
return {
|
|
'copyCmd': copyCmd,
|
|
'sourceDriveId': None,
|
|
'destDriveId': None,
|
|
'destParentType': False,
|
|
'newFilename': None,
|
|
'stripNamePrefix': None,
|
|
'replaceFilename': [],
|
|
'summary': False,
|
|
'mergeWithParent': False,
|
|
'mergeWithParentRetain': False,
|
|
'retainSourceFolders': False,
|
|
'sourceIsMyDriveSharedDrive': False,
|
|
'showPermissionMessages': False,
|
|
'sendEmailIfRequired': False,
|
|
'useDomainAdminAccess': False,
|
|
'enforceExpansiveAccess': False,
|
|
'copiedShortcutsPointToCopiedFiles': True,
|
|
'createShortcutsForNonmovableFiles': False,
|
|
'duplicateFiles': DUPLICATE_FILE_OVERWRITE_OLDER,
|
|
'duplicateFolders': DUPLICATE_FOLDER_MERGE,
|
|
'copyFilePermissions': False,
|
|
'copyFileInheritedPermissions': True,
|
|
'copyFileNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_ALWAYS,
|
|
'copyMergeWithParentFolderPermissions': False,
|
|
'copyMergedTopFolderPermissions': copyCmd,
|
|
'copyMergedSubFolderPermissions': copyCmd,
|
|
'copyTopFolderPermissions': True,
|
|
'copySubFolderPermissions': True,
|
|
'copyTopFolderInheritedPermissions': True,
|
|
'copySubFolderInheritedPermissions': True,
|
|
'copyTopFolderNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_ALWAYS,
|
|
'copySubFolderNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_ALWAYS,
|
|
'noCopyNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_NEVER,
|
|
'excludePermissionsFromDomains': set(),
|
|
'includePermissionsFromDomains': set(),
|
|
'mapPermissionsDomains': {},
|
|
'copySheetProtectedRangesInheritedPermissions': False,
|
|
'copySheetProtectedRangesNonInheritedPermissions': COPY_NONINHERITED_PERMISSIONS_NEVER,
|
|
'copySubFiles': True,
|
|
'copySubFolders': True,
|
|
'copySubShortcuts': True,
|
|
'fileNameMatchPattern': None,
|
|
'folderNameMatchPattern': None,
|
|
'shortcutNameMatchPattern': None,
|
|
'fileMimeTypes': set(),
|
|
'notMimeTypes': False,
|
|
'copySubFilesOwnedBy': None,
|
|
'copyPermissionRoles': set(DRIVEFILE_ACL_ROLES_MAP.values()),
|
|
'copyPermissionTypes': set(DRIVEFILE_ACL_PERMISSION_TYPES),
|
|
}
|
|
|
|
DUPLICATE_FILE_CHOICES = {
|
|
'overwriteall': DUPLICATE_FILE_OVERWRITE_ALL,
|
|
'overwriteolder': DUPLICATE_FILE_OVERWRITE_OLDER,
|
|
'duplicatename': DUPLICATE_FILE_DUPLICATE_NAME,
|
|
'uniquename': DUPLICATE_FILE_UNIQUE_NAME,
|
|
'skip': DUPLICATE_FILE_SKIP,
|
|
}
|
|
DUPLICATE_FOLDER_CHOICES = {
|
|
'merge': DUPLICATE_FOLDER_MERGE,
|
|
'duplicatename': DUPLICATE_FOLDER_DUPLICATE_NAME,
|
|
'uniquename': DUPLICATE_FOLDER_UNIQUE_NAME,
|
|
'skip': DUPLICATE_FOLDER_SKIP,
|
|
}
|
|
|
|
def getCopyMoveOptions(myarg, copyMoveOptions):
|
|
# Copy/Move arguments
|
|
if myarg == 'newfilename':
|
|
copyMoveOptions['newFilename'] = getString(Cmd.OB_DRIVE_FILE_NAME)
|
|
elif myarg =='stripnameprefix':
|
|
copyMoveOptions['stripNamePrefix'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'replacefilename':
|
|
copyMoveOptions['replaceFilename'].append(getREPatternSubstitution(re.IGNORECASE))
|
|
elif myarg == 'showpermissionmessages':
|
|
copyMoveOptions['showPermissionMessages'] = getBoolean()
|
|
elif myarg == 'sendemailifrequired':
|
|
copyMoveOptions['sendEmailIfRequired'] = getBoolean()
|
|
elif myarg == 'summary':
|
|
copyMoveOptions['summary'] = getBoolean()
|
|
elif myarg == 'mergewithparent':
|
|
copyMoveOptions['mergeWithParent'] = getBoolean()
|
|
if copyMoveOptions['mergeWithParent']:
|
|
copyMoveOptions['mergeWithParentRetain'] = False
|
|
elif myarg == 'duplicatefiles':
|
|
copyMoveOptions['duplicateFiles'] = getChoice(DUPLICATE_FILE_CHOICES, mapChoice=True)
|
|
elif myarg == 'duplicatefolders':
|
|
copyMoveOptions['duplicateFolders'] = getChoice(DUPLICATE_FOLDER_CHOICES, mapChoice=True)
|
|
elif myarg == 'copymergewithparentfolderpermissions':
|
|
copyMoveOptions['copyMergeWithParentFolderPermissions'] = getBoolean()
|
|
elif myarg == 'copymergedtopfolderpermissions':
|
|
copyMoveOptions['copyMergedTopFolderPermissions'] = getBoolean()
|
|
elif myarg == 'copytopfolderpermissions':
|
|
copyMoveOptions['copyTopFolderPermissions'] = getBoolean()
|
|
elif myarg == 'copytopfolderinheritedpermissions':
|
|
copyMoveOptions['copyTopFolderInheritedPermissions'] = getBoolean()
|
|
elif myarg == 'copytopfoldernoninheritedpermissions':
|
|
copyMoveOptions['copyTopFolderNonInheritedPermissions'] = getChoice(COPY_NONINHERITED_PERMISSIONS_CHOICES_MAP, mapChoice=True)
|
|
elif myarg == 'copymergedsubfolderpermissions':
|
|
copyMoveOptions['copyMergedSubFolderPermissions'] = getBoolean()
|
|
elif myarg == 'copysubfolderpermissions':
|
|
copyMoveOptions['copySubFolderPermissions'] = getBoolean()
|
|
elif myarg == 'copysubfolderinheritedpermissions':
|
|
copyMoveOptions['copySubFolderInheritedPermissions'] = getBoolean()
|
|
elif myarg == 'copysubfoldernoninheritedpermissions':
|
|
copyMoveOptions['copySubFolderNonInheritedPermissions'] = getChoice(COPY_NONINHERITED_PERMISSIONS_CHOICES_MAP, mapChoice=True)
|
|
elif myarg == 'excludepermissionsfromdomains':
|
|
copyMoveOptions['excludePermissionsFromDomains'] = set(getString(Cmd.OB_DOMAIN_NAME_LIST).lower().replace(',', ' ').split())
|
|
copyMoveOptions['includePermissionsFromDomains'] = set()
|
|
elif myarg == 'includepermissionsfromdomains':
|
|
copyMoveOptions['includePermissionsFromDomains'] = set(getString(Cmd.OB_DOMAIN_NAME_LIST).lower().replace(',', ' ').split())
|
|
copyMoveOptions['excludePermissionsFromDomains'] = set()
|
|
elif myarg == 'mappermissionsdomain':
|
|
oldDomain = getString(Cmd.OB_DOMAIN_NAME).lower()
|
|
copyMoveOptions['mapPermissionsDomains'][oldDomain] = getString(Cmd.OB_DOMAIN_NAME).lower()
|
|
elif myarg == 'enforceexpansiveaccess':
|
|
copyMoveOptions['enforceExpansiveAccess'] = getBoolean()
|
|
else:
|
|
# Move arguments
|
|
if not copyMoveOptions['copyCmd']:
|
|
if myarg == 'retainsourcefolders':
|
|
copyMoveOptions['retainSourceFolders'] = getBoolean()
|
|
elif myarg == 'mergewithparentretain':
|
|
copyMoveOptions['mergeWithParentRetain'] = getBoolean()
|
|
if copyMoveOptions['mergeWithParentRetain']:
|
|
copyMoveOptions['mergeWithParent'] = False
|
|
elif myarg == 'createshortcutsfornonmovablefiles':
|
|
copyMoveOptions['createShortcutsForNonmovableFiles'] = getBoolean()
|
|
else:
|
|
return False
|
|
# Copy arguments
|
|
else:
|
|
if myarg == 'copiedshortcutspointtocopiedfiles':
|
|
copyMoveOptions['copiedShortcutsPointToCopiedFiles'] = getBoolean()
|
|
elif myarg == 'copyfilepermissions':
|
|
copyMoveOptions['copyFilePermissions'] = getBoolean()
|
|
elif myarg == 'copyfileinheritedpermissions':
|
|
copyMoveOptions['copyFileInheritedPermissions'] = getBoolean()
|
|
elif myarg == 'copyfilenoninheritedpermissions':
|
|
copyMoveOptions['copyFileNonInheritedPermissions'] = COPY_NONINHERITED_PERMISSIONS_ALWAYS if getBoolean() else COPY_NONINHERITED_PERMISSIONS_NEVER
|
|
elif myarg == 'copypermissionroles':
|
|
copyMoveOptions['copyPermissionRoles'] = set()
|
|
for prole in getString(Cmd.OB_PERMISSION_ROLE_LIST).lower().replace(',', ' ').split():
|
|
if prole in DRIVEFILE_ACL_ROLES_MAP:
|
|
copyMoveOptions['copyPermissionRoles'].add(DRIVEFILE_ACL_ROLES_MAP[prole])
|
|
else:
|
|
invalidChoiceExit(prole, DRIVEFILE_ACL_ROLES_MAP, True)
|
|
elif myarg == 'copypermissiontypes':
|
|
copyMoveOptions['copyPermissionTypes'] = set()
|
|
for ptype in getString(Cmd.OB_PERMISSION_TYPE_LIST).lower().replace(',', ' ').split():
|
|
if ptype in DRIVEFILE_ACL_PERMISSION_TYPES:
|
|
copyMoveOptions['copyPermissionTypes'].add(ptype)
|
|
else:
|
|
invalidChoiceExit(ptype, DRIVEFILE_ACL_PERMISSION_TYPES, True)
|
|
elif myarg == 'copysheetprotectedranges':
|
|
if getBoolean():
|
|
copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] = True
|
|
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] = COPY_NONINHERITED_PERMISSIONS_ALWAYS
|
|
else:
|
|
copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] = False
|
|
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] = COPY_NONINHERITED_PERMISSIONS_NEVER
|
|
elif myarg == 'copysheetprotectedrangesinheritedpermissions':
|
|
copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] = getBoolean()
|
|
elif myarg == 'copysheetprotectedrangesnoninheritedpermissions':
|
|
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] = COPY_NONINHERITED_PERMISSIONS_ALWAYS if getBoolean() else COPY_NONINHERITED_PERMISSIONS_NEVER
|
|
elif myarg == 'copysubfiles':
|
|
copyMoveOptions['copySubFiles'] = getBoolean()
|
|
elif myarg == 'copysubfolders':
|
|
copyMoveOptions['copySubFolders'] = getBoolean()
|
|
elif myarg == 'copysubshortcuts':
|
|
copyMoveOptions['copySubShortcuts'] = getBoolean()
|
|
elif myarg == 'filenamematchpattern':
|
|
copyMoveOptions['fileNameMatchPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'foldernamematchpattern':
|
|
copyMoveOptions['folderNameMatchPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'shortcutnamematchpattern':
|
|
copyMoveOptions['shortcutNameMatchPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'filemimetype':
|
|
copyMoveOptions['notMimeTypes'] = checkArgumentPresent('not')
|
|
for mimeType in getString(Cmd.OB_MIMETYPE_LIST).lower().replace(',', ' ').split():
|
|
copyMoveOptions['fileMimeTypes'].add(validateMimeType(mimeType))
|
|
elif myarg == 'copysubfilesownedby':
|
|
copyMoveOptions['copySubFilesOwnedBy'] = getChoice(SHOW_OWNED_BY_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _targetFilenameExists(destFilename, mimeType, targetChildren):
|
|
destFilenameLower = destFilename.lower()
|
|
for target in targetChildren:
|
|
if destFilenameLower == target['name'].lower() and mimeType == target['mimeType']:
|
|
return target
|
|
return None
|
|
|
|
UNIQUE_PREFIX_PATTERN = re.compile(r'^(.+)\((\d+)\)$')
|
|
|
|
def _getFilenameParts(destFilename):
|
|
base, ext = os.path.splitext(destFilename)
|
|
if ext and base.endswith('.tar'):
|
|
ext = '.tar'+ext
|
|
base = base[:-4]
|
|
elif len(ext) > 5:
|
|
base = destFilename
|
|
ext = ''
|
|
mg = UNIQUE_PREFIX_PATTERN.match(base)
|
|
if mg:
|
|
return (mg.group(1), int(mg.group(2)), ext)
|
|
return (base, 0, ext)
|
|
|
|
def _getFilenamePrefix(destFilename):
|
|
base, _, _ = _getFilenameParts(destFilename)
|
|
return base
|
|
|
|
def _getUniqueFilename(destFilename, mimeType, targetChildren):
|
|
if _targetFilenameExists(destFilename, mimeType, targetChildren) is None:
|
|
return destFilename
|
|
base, _, ext = _getFilenameParts(destFilename)
|
|
n = 0
|
|
for target in targetChildren:
|
|
tbase, tcnt, text = _getFilenameParts(target['name'])
|
|
if base != tbase or ext != text:
|
|
continue
|
|
if tcnt > n:
|
|
n = tcnt
|
|
if ext:
|
|
return f'{base}({n+1}){ext}'
|
|
return f'{base}({n+1})'
|
|
|
|
def _copyPermissions(drive, user, i, count, j, jcount,
|
|
entityType, fileId, fileTitle, newFileId, newFileTitle,
|
|
statistics, stat, copyMoveOptions, atTop, copyInherited, copyNonInherited,
|
|
updateOwner):
|
|
def getPermissions(fid):
|
|
permissions = {}
|
|
try:
|
|
result = callGAPIpages(drive.permissions(), 'list', 'permissions',
|
|
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
fileId=fid,
|
|
fields='nextPageToken,permissions(allowFileDiscovery,domain,emailAddress,expirationTime,id,role,type,deleted,view,pendingOwner,permissionDetails)',
|
|
useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'], supportsAllDrives=True)
|
|
for permission in result:
|
|
permission.pop('teamDrivePermissionDetails', None)
|
|
permission['inherited'] = permission.pop('permissionDetails', [{'inherited': False}])[0]['inherited']
|
|
permissions[permission['id']] = permission
|
|
return permissions
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
|
|
GAPI.unknownError, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, fileTitle], str(e), j, jcount)
|
|
_incrStatistic(statistics, stat)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, stat)
|
|
return None
|
|
|
|
def permissionKVList(user, entityType, title, permission):
|
|
permstr = f"{['noninherited', 'inherited'][permission['inherited']]}/{permission['role']}/{permission['type']}"
|
|
if permission['type'] in {'group', 'user'}:
|
|
permstr += f"/{permission['emailAddress']}"
|
|
elif permission['type'] == 'domain':
|
|
permstr += f"/{permission['domain']}"
|
|
return [Ent.USER, user, entityType, title, Ent.PERMISSION, permstr]
|
|
|
|
def isPermissionCopyable(kvList, permission):
|
|
role = permission['role']
|
|
emailAddress = permission.get('emailAddress', '')
|
|
permissionType = permission['type']
|
|
domain = ''
|
|
if copyMoveOptions['excludePermissionsFromDomains'] or copyMoveOptions['includePermissionsFromDomains']:
|
|
if permissionType in {'group', 'user'}:
|
|
atLoc = emailAddress.find('@')
|
|
if atLoc > 0:
|
|
domain = emailAddress[atLoc+1:]
|
|
elif permissionType == 'domain':
|
|
domain = permission.get('domain', '')
|
|
if role not in copyMoveOptions['copyPermissionRoles']:
|
|
notCopiedMessage = f'role {role} not selected'
|
|
elif permissionType not in copyMoveOptions['copyPermissionTypes']:
|
|
notCopiedMessage = f'type {permissionType} not selected'
|
|
elif permission['inherited'] and not copyMoveOptions[copyInherited]:
|
|
notCopiedMessage = 'inherited not selected'
|
|
elif not permission['inherited'] and copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_NEVER:
|
|
notCopiedMessage = 'noninherited not selected'
|
|
elif role == 'owner':
|
|
if emailAddress == user or copyMoveOptions['destDriveId'] or not updateOwner:
|
|
notCopiedMessage = f'role {role} copy not required/appropriate'
|
|
else:
|
|
permission['role'] = 'writer'
|
|
return True
|
|
elif updateOwner and emailAddress == user:
|
|
notCopiedMessage = 'user is now owner'
|
|
elif domain and domain in copyMoveOptions['excludePermissionsFromDomains']:
|
|
notCopiedMessage = f'domain {domain} excluded'
|
|
elif domain and copyMoveOptions['includePermissionsFromDomains'] and domain not in copyMoveOptions['includePermissionsFromDomains']:
|
|
notCopiedMessage = f'domain {domain} not included'
|
|
elif permission.pop('deleted', False) or (permissionType in {'group', 'user'} and not emailAddress):
|
|
notCopiedMessage = f"{permissionType} deleted or has blank email address"
|
|
elif ((copyInherited == 'copySheetProtectedRangesInheritedPermissions' and copyMoveOptions[copyInherited]) or
|
|
(copyNonInherited == 'copySheetProtectedRangesNonInheritedPermissions' and
|
|
copyMoveOptions[copyNonInherited] != COPY_NONINHERITED_PERMISSIONS_NEVER)):
|
|
if role in {'organizer', 'fileOrganizer'}:
|
|
permission['role'] = 'writer'
|
|
return True
|
|
elif role == 'organizer' and (not atTop or copyMoveOptions['destParentType'] != DEST_PARENT_SHAREDDRIVE_ROOT):
|
|
notCopiedMessage = f'role {role} not copyable to {Ent.Plural(Ent.DRIVE_FILE_OR_FOLDER)}'
|
|
elif role == 'fileOrganizer' and entityType == Ent.DRIVE_FILE:
|
|
notCopiedMessage = f'role {role} not copyable to {Ent.Plural(entityType)}'
|
|
elif role == 'fileOrganizer' and not copyMoveOptions['destDriveId']:
|
|
notCopiedMessage = f'role {role} only copyable to {Ent.Singular(Ent.SHAREDDRIVE)} {Ent.Plural(Ent.DRIVE_FOLDER)}'
|
|
else:
|
|
return True
|
|
if copyMoveOptions['showPermissionMessages']:
|
|
entityActionNotPerformedWarning(kvList, notCopiedMessage, 0, 0)
|
|
return False
|
|
|
|
def getNonInheritedPermissions(permissions):
|
|
nonInheritedPermIds = set()
|
|
for permissionId, permission in iter(permissions.items()):
|
|
if not permission['inherited']:
|
|
nonInheritedPermIds.add(permissionId)
|
|
return nonInheritedPermIds
|
|
|
|
def mapPermissionsDomains(srcPerm):
|
|
if 'emailAddress' in srcPerm:
|
|
email, domain = srcPerm['emailAddress'].lower().split('@', 1)
|
|
if domain in copyMoveOptions['mapPermissionsDomains']:
|
|
srcPerm['emailAddress'] = f"{email}@{copyMoveOptions['mapPermissionsDomains'][domain]}"
|
|
return True
|
|
elif 'domain' in srcPerm:
|
|
domain = srcPerm['domain'].lower()
|
|
if domain in copyMoveOptions['mapPermissionsDomains']:
|
|
srcPerm['domain'] = copyMoveOptions['mapPermissionsDomains'][domain]
|
|
return True
|
|
return False
|
|
|
|
sourcePerms = getPermissions(fileId)
|
|
if sourcePerms is None:
|
|
return
|
|
copySourcePerms = {}
|
|
deleteTargetPermIds = set()
|
|
updateTargetPerms = {}
|
|
for permissionId, permission in iter(sourcePerms.items()):
|
|
kvList = permissionKVList(user, entityType, newFileTitle, permission)
|
|
if isPermissionCopyable(kvList, permission):
|
|
copySourcePerms[permissionId] = permission
|
|
if copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_ALWAYS:
|
|
if copyMoveOptions['mapPermissionsDomains']:
|
|
for permissionId in getNonInheritedPermissions(copySourcePerms):
|
|
mapPermissionsDomains(copySourcePerms[permissionId])
|
|
elif copyMoveOptions[copyNonInherited] in {COPY_NONINHERITED_PERMISSIONS_SYNC_ALL_FOLDERS,
|
|
COPY_NONINHERITED_PERMISSIONS_SYNC_UPDATED_FOLDERS}:
|
|
targetPerms = getPermissions(newFileId)
|
|
if targetPerms is None:
|
|
return
|
|
sourceNonInheritedPermIDs = getNonInheritedPermissions(copySourcePerms)
|
|
targetNonInheritedPermIDs = getNonInheritedPermissions(targetPerms)
|
|
# Permissions in Source only
|
|
if copyMoveOptions['mapPermissionsDomains']:
|
|
for permissionId in sourceNonInheritedPermIDs-targetNonInheritedPermIDs:
|
|
mapPermissionsDomains(copySourcePerms[permissionId])
|
|
# Permissions in Target only
|
|
deleteTargetPermIds = targetNonInheritedPermIDs-sourceNonInheritedPermIDs
|
|
# Permissions in Source and Target
|
|
for permissionId in targetNonInheritedPermIDs&sourceNonInheritedPermIDs:
|
|
srcPerm = copySourcePerms[permissionId]
|
|
if copyMoveOptions['mapPermissionsDomains'] and mapPermissionsDomains(srcPerm):
|
|
deleteTargetPermIds.add(permissionId)
|
|
continue
|
|
tgtPerm = targetPerms[permissionId]
|
|
updatePerm = {}
|
|
for field in ['expirationTime', 'role', 'view', 'pendingOwner']:
|
|
if field in srcPerm:
|
|
if field not in tgtPerm:
|
|
if field != 'pendingOwner' or srcPerm[field]:
|
|
updatePerm[field] = srcPerm[field]
|
|
elif srcPerm[field] != tgtPerm[field]:
|
|
updatePerm[field] = srcPerm[field]
|
|
elif field in tgtPerm:
|
|
if field == 'expirationTime':
|
|
updatePerm['removeExpiration'] = True
|
|
elif field == 'pendingOwner':
|
|
updatePerm[field] = False
|
|
if updatePerm:
|
|
updateTargetPerms[permissionId] = targetPerms[permissionId]
|
|
updateTargetPerms[permissionId].update(updatePerm)
|
|
updateTargetPerms[permissionId]['updates'] = updatePerm
|
|
copySourcePerms.pop(permissionId)
|
|
deleteUpdateKwargs = {'useDomainAdminAccess': copyMoveOptions['useDomainAdminAccess']}
|
|
if entityType != Ent.SHAREDDRIVE:
|
|
deleteUpdateKwargs['enforceExpansiveAccess'] = copyMoveOptions['enforceExpansiveAccess']
|
|
Ind.Increment()
|
|
action = Act.Get()
|
|
Act.Set(Act.COPY)
|
|
kcount = len(copySourcePerms)
|
|
k = 0
|
|
for permissionId, permission in iter(copySourcePerms.items()):
|
|
k += 1
|
|
kvList = permissionKVList(user, entityType, newFileTitle, permission)
|
|
permission.pop('id')
|
|
permission.pop('inherited')
|
|
if copyMoveOptions['destDriveId']:
|
|
permission.pop('pendingOwner', None)
|
|
sendNotificationEmail = False
|
|
while True:
|
|
try:
|
|
callGAPI(drive.permissions(), 'create',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS,
|
|
# retryReasons=[GAPI.INVALID_SHARING_REQUEST],
|
|
useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'],
|
|
fileId=newFileId, sendNotificationEmail=sendNotificationEmail, emailMessage=None,
|
|
body=permission, fields='', supportsAllDrives=True)
|
|
if copyMoveOptions['showPermissionMessages']:
|
|
entityActionPerformed(kvList, k, kcount)
|
|
break
|
|
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
|
|
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
|
|
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
|
|
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
|
|
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
|
|
GAPI.ownerOnTeamDriveItemNotSupported,
|
|
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
|
|
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
|
|
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
|
|
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
|
|
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility, GAPI.abusiveContentRestriction) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
break
|
|
except GAPI.invalidSharingRequest as e:
|
|
if not copyMoveOptions['sendEmailIfRequired'] or sendNotificationEmail or 'You are trying to invite' not in str(e):
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
break
|
|
sendNotificationEmail = True
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, stat)
|
|
Ind.Decrement()
|
|
Act.Set(action)
|
|
return
|
|
Act.Set(Act.DELETE)
|
|
kcount = len(deleteTargetPermIds)
|
|
k = 0
|
|
for permissionId in deleteTargetPermIds:
|
|
k += 1
|
|
kvList = permissionKVList(user, entityType, newFileTitle, targetPerms[permissionId])
|
|
try:
|
|
callGAPI(drive.permissions(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_DELETE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
**deleteUpdateKwargs,
|
|
fileId=newFileId, permissionId=permissionId, supportsAllDrives=True)
|
|
if copyMoveOptions['showPermissionMessages']:
|
|
entityActionPerformed(kvList, k, kcount)
|
|
except (GAPI.notFound, GAPI.permissionNotFound,
|
|
GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.fileNeverWritable, GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded, GAPI.cannotDeletePermission) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, stat)
|
|
Ind.Decrement()
|
|
Act.Set(action)
|
|
return
|
|
Act.Set(Act.UPDATE)
|
|
kcount = len(updateTargetPerms)
|
|
k = 0
|
|
for permissionId, permission in iter(updateTargetPerms.items()):
|
|
k += 1
|
|
kvList = permissionKVList(user, entityType, newFileTitle, permission)
|
|
removeExpiration = permission['updates'].pop('removeExpiration', False)
|
|
try:
|
|
callGAPI(drive.permissions(), 'update',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_UPDATE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
removeExpiration=removeExpiration,
|
|
**deleteUpdateKwargs,
|
|
fileId=newFileId, permissionId=permissionId, body=permission['updates'], supportsAllDrives=True)
|
|
if copyMoveOptions['showPermissionMessages']:
|
|
entityActionPerformed(kvList, k, kcount)
|
|
except (GAPI.notFound, GAPI.permissionNotFound,
|
|
GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.fileNeverWritable, GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, stat)
|
|
Ind.Decrement()
|
|
Act.Set(action)
|
|
return
|
|
Act.Set(action)
|
|
Ind.Decrement()
|
|
|
|
def _updateSheetProtectedRangesACLchange(sheet, user, i, count, j, jcount, fileId, fileTitle,
|
|
aclAdd, permission):
|
|
ptype = permission['type']
|
|
updList = 'users' if ptype == 'user' else 'groups' if ptype == 'group' else ''
|
|
if updList:
|
|
emailAddress = permission['emailAddress']
|
|
else:
|
|
updDomain = (ptype == 'domain') and (permission['domain'] == GC.Values[GC.DOMAIN])
|
|
if not updDomain:
|
|
return
|
|
addEditor = aclAdd and (permission['role'] not in {'reader', 'commenter'})
|
|
updateProtectedRangeRequests = []
|
|
try:
|
|
result = callGAPI(sheet.spreadsheets(), 'get',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=fileId, fields='sheets(protectedRanges)')
|
|
for rsheet in result.get('sheets', []):
|
|
for protectedRange in rsheet.get('protectedRanges', []):
|
|
editors = protectedRange.get('editors', None)
|
|
if editors is None:
|
|
continue
|
|
updReqd = False
|
|
if updList:
|
|
if addEditor:
|
|
if updList not in editors:
|
|
editors[updList] = [emailAddress]
|
|
updReqd = True
|
|
elif emailAddress not in editors[updList]:
|
|
editors[updList].append(emailAddress)
|
|
updReqd = True
|
|
else:
|
|
if emailAddress in editors.get(updList, []):
|
|
editors[updList].remove(emailAddress)
|
|
updReqd = True
|
|
elif updDomain:
|
|
editors['domainUsersCanEdit'] = addEditor
|
|
updReqd = True
|
|
if updReqd:
|
|
updateProtectedRangeRequests.append({'updateProtectedRange': {'protectedRange': protectedRange, 'fields': 'editors'}})
|
|
if updateProtectedRangeRequests:
|
|
callGAPI(sheet.spreadsheets(), 'batchUpdate',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=fileId, body={'requests': updateProtectedRangeRequests})
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, fileTitle], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
|
|
def _getSheetProtectedRanges(sheet, user, i, count, j, jcount, fileId, fileTitle,
|
|
statistics, stat):
|
|
sheetProtectedRanges = []
|
|
try:
|
|
result = callGAPI(sheet.spreadsheets(), 'get',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=fileId, fields='sheets(protectedRanges)')
|
|
for rsheet in result.get('sheets', []):
|
|
for protectedRange in rsheet.get('protectedRanges', []):
|
|
sheetProtectedRanges.append({'updateProtectedRange': {'protectedRange': protectedRange, 'fields': 'editors'}})
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, fileTitle], str(e), j, jcount)
|
|
_incrStatistic(statistics, stat)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, stat)
|
|
return sheetProtectedRanges
|
|
|
|
def _updateSheetProtectedRanges(sheet, user, i, count, j, jcount, newFileId, newFileTitle, sheetProtectedRanges,
|
|
statistics, stat):
|
|
try:
|
|
callGAPI(sheet.spreadsheets(), 'batchUpdate',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=newFileId, body={'requests': sheetProtectedRanges})
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, newFileTitle], str(e), j, jcount)
|
|
_incrStatistic(statistics, stat)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, stat)
|
|
|
|
def _identicalSourceTarget(fileId, targetChildren):
|
|
for target in targetChildren:
|
|
if fileId == target['id']:
|
|
return True
|
|
return False
|
|
|
|
def _checkForDuplicateTargetFile(drive, user, k, kcount, child, destFilename, targetChildren, copyMoveOptions, statistics):
|
|
destFilenameLower = destFilename.lower()
|
|
for target in targetChildren:
|
|
if not target.get('processed', False) and destFilenameLower == target['name'].lower() and child['mimeType'] == target['mimeType']:
|
|
target['processed'] = True
|
|
if copyMoveOptions['duplicateFiles'] in [DUPLICATE_FILE_OVERWRITE_ALL, DUPLICATE_FILE_OVERWRITE_OLDER]:
|
|
if (copyMoveOptions['duplicateFiles'] == DUPLICATE_FILE_OVERWRITE_OLDER) and (child['modifiedTime'] <= target['modifiedTime']):
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, child['name'], Ent.DRIVE_FILE, target['name']], Msg.DUPLICATE, k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_DUPLICATE)
|
|
return True
|
|
if not target['capabilities']['canDelete']:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, child['name'], Ent.DRIVE_FILE, target['name']], Msg.NOT_DELETABLE, k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
return True
|
|
try:
|
|
callGAPI(drive.files(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
fileId=target['id'], supportsAllDrives=True)
|
|
child['name'] = destFilename
|
|
return False
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.fileNeverWritable) as e:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, child['name'], Ent.DRIVE_FILE, target['name']], f'{Msg.NOT_DELETABLE}: {str(e)}', k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
return True
|
|
if copyMoveOptions['duplicateFiles'] == DUPLICATE_FILE_DUPLICATE_NAME:
|
|
child['name'] = destFilename
|
|
return False
|
|
if copyMoveOptions['duplicateFiles'] == DUPLICATE_FILE_UNIQUE_NAME:
|
|
child['name'] = _getUniqueFilename(destFilename, child['mimeType'], targetChildren)
|
|
return False
|
|
#copyMoveOptions['duplicateFiles'] == DUPLICATE_FILE_SKIP
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, child['name'], Ent.DRIVE_FILE, target['name']], Msg.DUPLICATE, k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_DUPLICATE)
|
|
return True
|
|
child['name'] = destFilename
|
|
return False
|
|
|
|
def _getCopyMoveParentInfo(drive, user, i, count, j, jcount, newParentId, statistics):
|
|
# newParentId is known to be a folder
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=newParentId, fields='name,driveId,parents,modifiedTime', supportsAllDrives=True)
|
|
if 'driveId' not in result:
|
|
result['driveId'] = None
|
|
if result['name'] == MY_DRIVE and not result.get('parents', []):
|
|
result['destParentType'] = DEST_PARENT_MYDRIVE_ROOT
|
|
else:
|
|
result['destParentType'] = DEST_PARENT_MYDRIVE_FOLDER
|
|
else:
|
|
if result['name'] == TEAM_DRIVE and not result.get('parents', []):
|
|
result['name'] = _getSharedDriveNameFromId(result['driveId'])
|
|
result['destParentType'] = DEST_PARENT_SHAREDDRIVE_ROOT
|
|
else:
|
|
result['destParentType'] = DEST_PARENT_SHAREDDRIVE_FOLDER
|
|
return result
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
|
|
GAPI.unknownError, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.fileNeverWritable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, newParentId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
return None
|
|
|
|
def _getCopyMoveTargetInfo(drive, user, i, count, j, jcount, source, destFilename, newParentId, statistics, parentParms):
|
|
try:
|
|
return callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=f"mimeType = '{source['mimeType']}' and name contains '{escapeDriveFileName(_getFilenamePrefix(destFilename))}' and trashed = false and '{newParentId}' in parents",
|
|
orderBy='folder desc,name,modifiedTime desc',
|
|
fields='nextPageToken,files(id,name,capabilities,mimeType,modifiedTime)',
|
|
**parentParms[DFA_SEARCHARGS])
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
|
|
GAPI.unknownError, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.fileNeverWritable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, newParentId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
return None
|
|
|
|
def _verifyUserIsOrganizer(drive, user, i, count, fileId):
|
|
role = UNKNOWN
|
|
try:
|
|
permissionId = getPermissionIdForEmail(user, i, count, user)
|
|
role = callGAPI(drive.permissions(), 'get',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.PERMISSION_NOT_FOUND, GAPI.INSUFFICIENT_ADMINISTRATOR_PRIVILEGES],
|
|
useDomainAdminAccess=False,
|
|
fileId=fileId, permissionId=permissionId, fields='role', supportsAllDrives=True)['role']
|
|
if role == 'organizer':
|
|
return True
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId, Ent.ROLE, role], Msg.ROLE_MUST_BE_ORGANIZER, i, count)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.insufficientAdministratorPrivileges) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId], str(e), i, count)
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId, Ent.PERMISSION_ID, permissionId], i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
entityActionNotPerformedWarning([Ent.USER, user], Msg.UNABLE_TO_GET_PERMISSION_ID.format(user), i, count)
|
|
return False
|
|
|
|
def _getCopyFolderNonInheritedPermissions(copyMoveOptions, copyNonInherited, sourceModifiedTime, targetModifiedTime):
|
|
if ((copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_NEVER) or
|
|
((copyMoveOptions[copyNonInherited] == COPY_NONINHERITED_PERMISSIONS_SYNC_UPDATED_FOLDERS) and
|
|
(sourceModifiedTime <= targetModifiedTime))):
|
|
return 'noCopyNonInheritedPermissions'
|
|
return copyNonInherited
|
|
|
|
def _checkForExistingShortcut(drive, fileId, fileName, parentId):
|
|
try:
|
|
existingShortcuts = callGAPI(drive.files(), 'list',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
supportsAllDrives=True, includeItemsFromAllDrives=True,
|
|
q=f"shortcutDetails.targetId = '{fileId}' and trashed = False", fields='files(id,name,parents)')['files']
|
|
for shortcut in existingShortcuts:
|
|
if parentId in shortcut['parents'] and fileName == shortcut['name']:
|
|
return shortcut['id']
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
pass
|
|
return None
|
|
|
|
copyReturnItemMap = {
|
|
'returnidonly': 'id',
|
|
'returnlinkonly': 'webViewLink',
|
|
}
|
|
|
|
# gam <UserTypeEntity> copy drivefile <DriveFileEntity>
|
|
# [newfilename <DriveFileName>] (replacefilename <REMatchPattern> <RESubstitution>)*
|
|
# [stripnameprefix <String>]
|
|
# [excludetrashed]
|
|
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*)) |
|
|
# (returnidonly|returnlinkonly)]
|
|
# [summary [<Boolean>]] [showpermissionsmessages [<Boolean>]]
|
|
# [<DriveFileParentAttribute>]
|
|
# [mergewithparent [<Boolean>]] [recursive [depth <Number>]]
|
|
# <DriveFileCopyAttribute>*
|
|
# [skipids <DriveFileEntity>]
|
|
# [copysubfiles [<Boolean>]] [filenamematchpattern <REMatchPattern>] [filemimetype [not] <MimeTypeList>]
|
|
# [copysubfolders [<Boolean>]] [foldernamematchpattern <REMatchPattern>]
|
|
# [copysubshortcuts [<Boolean>]] [shortcutnamematchpattern <REMatchPattern>]
|
|
# [duplicatefiles overwriteolder|overwriteall|duplicatename|uniquename|skip]
|
|
# [duplicatefolders merge|duplicatename|uniquename|skip]
|
|
# [copiedshortcutspointtocopiedfiles [<Boolean>]]
|
|
# [copyfilepermissions [<Boolean>]]
|
|
# [copyfileinheritedpermissions [<Boolean>]
|
|
# [copyfilenoninheritedpermissions [<Boolean>]
|
|
# [copymergewithparentfolderpermissions [<Boolean>]]
|
|
# [copymergedtopfolderpermissions [<Boolean>]]
|
|
# [copytopfolderpermissions [<Boolean>]]
|
|
# [copytopfolderiheritedpermissions [<Boolean>]]
|
|
# [copytopfoldernoniheritedpermissions never|always|syncallfolders|syncupdatedfolders]
|
|
# [copymergedsubfolderpermissions [<Boolean>]]
|
|
# [copysubfolderpermissions [<Boolean>]]
|
|
# [copysubfolderinheritedpermissions [<Boolean>]]
|
|
# [copysubfoldernoniheritedpermissions never|always|syncallfolders|syncupdatedfolders]
|
|
# [copypermissionroles <DriveFileACLRoleList>]
|
|
# [copypermissiontypes <DriveFileACLTypeList>]
|
|
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
|
# (mappermissionsdomain <DomainName> <DomainName>)*
|
|
# [copysheetprotectedranges [<Boolean>]]
|
|
# [copysheetprotectedrangesinheritedpermissions [<Boolean>]]
|
|
# [copysheetprotectedrangesnoninheritedpermissions [<Boolean>]]
|
|
# [sendemailifrequired [<Boolean>]]
|
|
# [suppressnotselectedmessages [<Boolean>]]
|
|
# [verifyorganizer [<Boolean>]]
|
|
# [enforceexpansiveaccess [<Boolean>]]
|
|
def copyDriveFile(users):
|
|
def _writeCSVData(user, oldName, oldId, newName, newId, mimeType):
|
|
row = {'User': user, fileNameTitle: oldName, 'id': oldId,
|
|
newFileNameTitle: newName, 'newId': newId, 'mimeType': mimeType}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
|
|
def _cloneFolderCopy(drive, user, i, count, j, jcount,
|
|
source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime,
|
|
statistics, copyMoveOptions, atTop):
|
|
folderId = source.pop('id')
|
|
folderName = source['name']
|
|
folderNameId = f'{folderName}({folderId})'
|
|
kvList = [Ent.USER, user, Ent.DRIVE_FOLDER, folderNameId]
|
|
newParentNameId = f'{newParentName}({newParentId})'
|
|
# Merge top parent folder
|
|
if atTop and copyMoveOptions['mergeWithParent']:
|
|
action = Act.Get()
|
|
Act.Set(Act.COPY_MERGE)
|
|
if not csvPF:
|
|
entityPerformActionModifierItemValueList(kvList, Act.MODIFIER_CONTENTS_WITH, [Ent.DRIVE_FOLDER, newParentNameId], j, jcount)
|
|
else:
|
|
_writeCSVData(user, folderName, folderId, newParentName, newParentId, MIMETYPE_GA_FOLDER)
|
|
Act.Set(action)
|
|
_incrStatistic(statistics, STAT_FOLDER_MERGED)
|
|
if (copyMoveOptions['copyMergeWithParentFolderPermissions'] and
|
|
copyMoveOptions['destParentType'] != DEST_PARENT_MYDRIVE_ROOT):
|
|
copyFolderNonInheritedPermissions =\
|
|
_getCopyFolderNonInheritedPermissions(copyMoveOptions,
|
|
'copyTopFolderNonInheritedPermissions',
|
|
source['modifiedTime'], mergeParentModifiedTime)
|
|
_copyPermissions(drive, user, i, count, j, jcount,
|
|
Ent.DRIVE_FOLDER, folderId, folderName, newParentId, newFolderName,
|
|
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
|
|
copyMoveOptions, True,
|
|
'copyTopFolderInheritedPermissions',
|
|
copyFolderNonInheritedPermissions,
|
|
True)
|
|
return (newParentId, newParentName, True)
|
|
# Merge parent folders
|
|
if copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE:
|
|
newFolderNameLower = newFolderName.lower()
|
|
for target in targetChildren:
|
|
if not target.get('processed', False) and newFolderNameLower == target['name'].lower() and source['mimeType'] == target['mimeType']:
|
|
target['processed'] = True
|
|
if target['capabilities']['canAddChildren']:
|
|
newFolderId = target['id']
|
|
action = Act.Get()
|
|
Act.Set(Act.COPY_MERGE)
|
|
if not csvPF:
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_CONTENTS_WITH, [Ent.DRIVE_FOLDER, f'{newFolderName}({newFolderId})'], j, jcount)
|
|
else:
|
|
_writeCSVData(user, folderName, folderId, newFolderName, newFolderId, MIMETYPE_GA_FOLDER)
|
|
Act.Set(action)
|
|
_incrStatistic(statistics, STAT_FOLDER_MERGED)
|
|
if (copyMoveOptions[['copyMergedSubFolderPermissions', 'copyMergedTopFolderPermissions'][atTop]] and
|
|
(not atTop or copyMoveOptions['destParentType'] != DEST_PARENT_MYDRIVE_ROOT)):
|
|
copyFolderNonInheritedPermissions =\
|
|
_getCopyFolderNonInheritedPermissions(copyMoveOptions,
|
|
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
|
|
source['modifiedTime'], target['modifiedTime'])
|
|
_copyPermissions(drive, user, i, count, j, jcount,
|
|
Ent.DRIVE_FOLDER, folderId, folderName, newFolderId, newFolderName,
|
|
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
|
|
copyMoveOptions, atTop,
|
|
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
|
|
copyFolderNonInheritedPermissions,
|
|
False)
|
|
return (newFolderId, newFolderName, True)
|
|
entityActionFailedWarning(kvList+[Ent.DRIVE_FOLDER, newParentNameId], Msg.NOT_WRITABLE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_NOT_WRITABLE)
|
|
return (None, None, False)
|
|
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_UNIQUE_NAME:
|
|
newFolderName = _getUniqueFilename(newFolderName, source['mimeType'], targetChildren)
|
|
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_SKIP:
|
|
targetChild = _targetFilenameExists(newFolderName, source['mimeType'], targetChildren)
|
|
if targetChild is not None:
|
|
entityModifierItemValueListActionNotPerformedWarning(kvList, Act.MODIFIER_TO,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f"{newFolderName}({targetChild['id']})"],
|
|
Msg.DUPLICATE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_DUPLICATE)
|
|
return (None, None, False)
|
|
# Copy parent folders
|
|
body = source.copy()
|
|
body.pop('capabilities', None)
|
|
if copyMoveOptions['sourceDriveId'] or copyMoveOptions['destDriveId']:
|
|
body.pop('copyRequiresWriterPermission', None)
|
|
body.pop('writersCanShare', None)
|
|
body.pop('trashed', None)
|
|
body.pop('driveId', None)
|
|
body['name'] = newFolderName
|
|
try:
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INTERNAL_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.BAD_REQUEST],
|
|
body=body, fields='id,webViewLink,modifiedTime', supportsAllDrives=True)
|
|
newFolderId = result['id']
|
|
if returnIdLink:
|
|
writeStdout(f'{result[returnIdLink]}\n')
|
|
elif not csvPF:
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f'{newFolderName}({newFolderId})'],
|
|
j, jcount)
|
|
else:
|
|
_writeCSVData(user, folderName, folderId, newFolderName, newFolderId, body['mimeType'])
|
|
_incrStatistic(statistics, STAT_FOLDER_COPIED_MOVED)
|
|
if copyMoveOptions[['copySubFolderPermissions', 'copyTopFolderPermissions'][atTop]]:
|
|
_copyPermissions(drive, user, i, count, j, jcount,
|
|
Ent.DRIVE_FOLDER, folderId, folderName, newFolderId, newFolderName,
|
|
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
|
|
copyMoveOptions, False,
|
|
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
|
|
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
|
|
True)
|
|
return (newFolderId, newFolderName, False)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.internalError,
|
|
GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, newFolderName], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, STAT_FOLDER_FAILED)
|
|
copyMoveOptions['retainSourceFolders'] = True
|
|
return (None, None, False)
|
|
|
|
def _makeCopyShortcut(drive, user, k, kcount, entityType, childId, childName, newChildName, newParentId, newParentName, targetId):
|
|
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
|
|
if entityType == Ent.DRIVE_FILE:
|
|
targetEntityType = Ent.DRIVE_FILE_SHORTCUT
|
|
statShortcutCreated = STAT_FILE_SHORTCUT_CREATED
|
|
statShortcutExists = STAT_FILE_SHORTCUT_EXISTS
|
|
else:
|
|
targetEntityType = Ent.DRIVE_FOLDER_SHORTCUT
|
|
statShortcutCreated = STAT_FOLDER_SHORTCUT_CREATED
|
|
statShortcutExists = STAT_FOLDER_SHORTCUT_EXISTS
|
|
newParentNameId = f'{newParentName}({newParentId})'
|
|
action = Act.Get()
|
|
existingShortcut = _checkForExistingShortcut(drive, targetId, newChildName, newParentId)
|
|
if existingShortcut:
|
|
Act.Set(Act.CREATE_SHORTCUT)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_PREVIOUSLY_IN,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, targetEntityType, f"{childName}({existingShortcut})"],
|
|
k, kcount)
|
|
Act.Set(action)
|
|
_incrStatistic(statistics, statShortcutExists)
|
|
return
|
|
body = {'name': newChildName, 'mimeType': MIMETYPE_GA_SHORTCUT,
|
|
'parents': [newParentId], 'shortcutDetails': {'targetId': targetId}}
|
|
try:
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID],
|
|
body=body, fields='id', supportsAllDrives=True)
|
|
Act.Set(Act.CREATE_SHORTCUT)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_IN,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, targetEntityType, f"{newChildName}({result['id']})"],
|
|
k, kcount)
|
|
Act.Set(action)
|
|
_incrStatistic(statistics, statShortcutCreated)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.invalid, GAPI.badRequest, GAPI.fileNotFound, GAPI.unknownError,
|
|
GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e:
|
|
entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
|
|
def _checkChildCopyAllowed(childMimeType, childName, child):
|
|
if childMimeType == MIMETYPE_GA_FOLDER:
|
|
if not copyMoveOptions['copySubFolders']:
|
|
return False
|
|
nameMatchPattern = copyMoveOptions['folderNameMatchPattern']
|
|
elif childMimeType == MIMETYPE_GA_SHORTCUT:
|
|
if not copyMoveOptions['copySubShortcuts']:
|
|
return False
|
|
nameMatchPattern = copyMoveOptions['shortcutNameMatchPattern']
|
|
else:
|
|
if not copyMoveOptions['copySubFiles']:
|
|
return False
|
|
if copyMoveOptions['copySubFilesOwnedBy'] is not None:
|
|
if child.get('driveId', None) is None and child.get('ownedByMe', False) != copyMoveOptions['copySubFilesOwnedBy']:
|
|
return False
|
|
if copyMoveOptions['fileMimeTypes']:
|
|
if not copyMoveOptions['notMimeTypes']:
|
|
if childMimeType not in copyMoveOptions['fileMimeTypes']:
|
|
return False
|
|
else:
|
|
if childMimeType in copyMoveOptions['fileMimeTypes']:
|
|
return False
|
|
nameMatchPattern = copyMoveOptions['fileNameMatchPattern']
|
|
return not nameMatchPattern or nameMatchPattern.match(childName)
|
|
|
|
def _recursiveFolderCopy(drive, user, i, count, j, jcount,
|
|
source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime, atTop, depth):
|
|
folderId = source['id']
|
|
newFolderId, newFolderName, existingTargetFolder = _cloneFolderCopy(drive, user, i, count, j, jcount,
|
|
source, targetChildren, newFolderName,
|
|
newParentId, newParentName, mergeParentModifiedTime,
|
|
statistics, copyMoveOptions, atTop)
|
|
if newFolderId is None:
|
|
return
|
|
newFolderNameId = f'{newFolderName}({newFolderId})'
|
|
copiedSourceFiles[folderId] = newFolderId
|
|
copiedTargetFiles.add(newFolderId) # Don't recopy folder copied into a sub-folder
|
|
if maxdepth != -1 and depth > maxdepth:
|
|
return
|
|
depth += 1
|
|
sourceChildren = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=WITH_PARENTS.format(folderId),
|
|
orderBy='folder desc,name,modifiedTime desc',
|
|
fields='nextPageToken,files(id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
|
|
'description,folderColorRgb,mimeType,modifiedTime,ownedByMe,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare,'\
|
|
'shortcutDetails(targetId,targetMimeType))',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **sourceSearchArgs)
|
|
kcount = len(sourceChildren)
|
|
if kcount > 0:
|
|
if existingTargetFolder:
|
|
subTargetChildren = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=ANY_NON_TRASHED_WITH_PARENTS.format(newFolderId),
|
|
orderBy='folder desc,name,modifiedTime desc',
|
|
fields='nextPageToken,files(id,name,capabilities,mimeType,modifiedTime)',
|
|
**parentParms[DFA_SEARCHARGS])
|
|
else:
|
|
subTargetChildren = []
|
|
Ind.Increment()
|
|
k = 0
|
|
for child in sourceChildren:
|
|
k += 1
|
|
childId = child['id']
|
|
childName = child['name']
|
|
childNameId = f'{childName}({childId})'
|
|
childMimeType = child['mimeType']
|
|
if childId in copiedTargetFiles: # Don't recopy file/folder copied into a sub-folder
|
|
continue
|
|
kvList = [Ent.USER, user, _getEntityMimeType(child), childNameId]
|
|
if childId in skipFileIdEntity['list']:
|
|
if not suppressNotSelectedMessages:
|
|
entityActionNotPerformedWarning(kvList, Msg.IN_SKIPIDS, k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_IN_SKIPIDS)
|
|
continue
|
|
if not _checkChildCopyAllowed(childMimeType, childName, child):
|
|
if not suppressNotSelectedMessages:
|
|
entityActionNotPerformedWarning(kvList, Msg.NOT_SELECTED, k, kcount)
|
|
continue
|
|
child.pop('ownedByMe', None)
|
|
trashed = child.pop('trashed', False)
|
|
if (childId == newFolderId) or (excludeTrashed and trashed):
|
|
entityActionNotPerformedWarning(kvList,
|
|
[Msg.NOT_COPYABLE, Msg.IN_TRASH_AND_EXCLUDE_TRASHED][trashed], k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
|
|
continue
|
|
if copyMoveOptions['replaceFilename']:
|
|
newChildName = processFilenameReplacements(childName, copyMoveOptions['replaceFilename'])
|
|
else:
|
|
newChildName = childName
|
|
# If source child has already been copied, make shortcut to its copy
|
|
if childId in copiedSourceFiles:
|
|
_makeCopyShortcut(drive, user, k, kcount, _getEntityMimeType(child), childId, childName,
|
|
newChildName, newFolderId, newFolderName, copiedSourceFiles[childId])
|
|
continue
|
|
child.pop('parents', [])
|
|
child['parents'] = [newFolderId]
|
|
if childMimeType == MIMETYPE_GA_FOLDER:
|
|
_recursiveFolderCopy(drive, user, i, count, k, kcount,
|
|
child, subTargetChildren, newChildName, newFolderId, newFolderName, child['modifiedTime'],
|
|
False, depth)
|
|
elif childMimeType == MIMETYPE_GA_SHORTCUT:
|
|
shortcutsToCreate.append({'childName': childName, 'childId': childId, 'newChildName': newChildName,
|
|
'newFolderId': newFolderId, 'newFolderName': newFolderName,
|
|
'targetId': child['shortcutDetails']['targetId'], 'mimeType': child['shortcutDetails']['targetMimeType']})
|
|
else:
|
|
if not child.pop('capabilities')['canCopy']:
|
|
entityActionFailedWarning(kvList, Msg.NOT_COPYABLE, k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
|
|
continue
|
|
if existingTargetFolder:
|
|
if _checkForDuplicateTargetFile(drive, user, k, kcount, child, newChildName, subTargetChildren, copyMoveOptions, statistics):
|
|
continue
|
|
else:
|
|
child['name'] = newChildName
|
|
child.pop('id')
|
|
if copyMoveOptions['destDriveId']:
|
|
child.pop('copyRequiresWriterPermission', None)
|
|
child.pop('writersCanShare', None)
|
|
child.pop('driveId', None)
|
|
if childMimeType == MIMETYPE_GA_SHORTCUT:
|
|
child.pop('folderColorRgb', None)
|
|
child.pop('mimeType')
|
|
try:
|
|
result = callGAPI(drive.files(), 'copy',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_COPY_THROW_REASONS+[GAPI.INTERNAL_ERROR, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED],
|
|
fileId=childId, body=child, fields='id,name', supportsAllDrives=True)
|
|
if not csvPF:
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
|
|
[Ent.DRIVE_FOLDER, newFolderNameId, Ent.DRIVE_FILE, f"{result['name']}({result['id']})"],
|
|
k, kcount)
|
|
else:
|
|
_writeCSVData(user, childName, childId, result['name'], result['id'], childMimeType)
|
|
_incrStatistic(statistics, STAT_FILE_COPIED_MOVED)
|
|
copiedSourceFiles[childId] = result['id']
|
|
copiedTargetFiles.add(result['id']) # Don't recopy file copied into a sub-folder
|
|
if (childMimeType == MIMETYPE_GA_SPREADSHEET and
|
|
(copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] or
|
|
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] != COPY_NONINHERITED_PERMISSIONS_NEVER)):
|
|
protectedSheetRanges = _getSheetProtectedRanges(sheet, user, i, count, k, kcount, childId, childName,
|
|
statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
|
|
_copyPermissions(drive, user, i, count, k, kcount,
|
|
Ent.DRIVE_FILE, childId, childName, result['id'], result['name'],
|
|
statistics, STAT_FILE_PERMISSIONS_FAILED,
|
|
copyMoveOptions, False,
|
|
'copySheetProtectedRangesInheritedPermissions',
|
|
'copySheetProtectedRangesNonInheritedPermissions',
|
|
True)
|
|
_updateSheetProtectedRanges(sheet, user, i, count, k, kcount, result['id'], result['name'], protectedSheetRanges,
|
|
statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
|
|
elif copyMoveOptions['copyFilePermissions']:
|
|
_copyPermissions(drive, user, i, count, k, kcount,
|
|
Ent.DRIVE_FILE, childId, childName, result['id'], result['name'],
|
|
statistics, STAT_FILE_PERMISSIONS_FAILED,
|
|
copyMoveOptions, False,
|
|
'copyFileInheritedPermissions',
|
|
'copyFileNonInheritedPermissions',
|
|
True)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
|
|
GAPI.insufficientParentPermissions, GAPI.unknownError,
|
|
GAPI.invalid, GAPI.cannotCopyFile, GAPI.badRequest, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
|
|
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.rateLimitExceeded, GAPI.userRateLimitExceeded,
|
|
GAPI.internalError, GAPI.teamDrivesShortcutFileNotSupported) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
except (GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
fileIdEntity = getDriveFileEntity()
|
|
csvPF = None
|
|
addCSVData = {}
|
|
returnIdLink = None
|
|
copyBody = {}
|
|
parentBody = {}
|
|
parentParms = initDriveFileAttributes()
|
|
copyParameters = initDriveFileAttributes()
|
|
copyMoveOptions = initCopyMoveOptions(True)
|
|
excludeTrashed = newParentsSpecified = recursive = suppressNotSelectedMessages = False
|
|
maxdepth = -1
|
|
verifyOrganizer = True
|
|
skipFileIdEntity = initDriveFileEntity()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getCopyMoveOptions(myarg, copyMoveOptions):
|
|
pass
|
|
elif getDriveFileParentAttribute(myarg, parentParms):
|
|
newParentsSpecified = True
|
|
elif myarg in copyReturnItemMap:
|
|
returnIdLink = copyReturnItemMap[myarg]
|
|
elif myarg == 'excludetrashed':
|
|
excludeTrashed = True
|
|
elif myarg == 'recursive':
|
|
recursive = getBoolean()
|
|
elif myarg == 'depth':
|
|
maxdepth = getInteger(minVal=-1)
|
|
elif myarg == 'skipids':
|
|
skipFileIdEntity = getDriveFileEntity()
|
|
elif myarg == 'convert':
|
|
deprecatedArgument(myarg)
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile()
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'suppressnotselectedmessages':
|
|
suppressNotSelectedMessages = getBoolean()
|
|
elif getDriveFileCopyAttribute(myarg, copyBody, copyParameters):
|
|
pass
|
|
elif myarg == 'verifyorganizer':
|
|
verifyOrganizer = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
if csvPF:
|
|
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
|
|
newFileNameTitle = f'new{fileNameTitle.capitalize()}'
|
|
csvPF.SetTitles(['User', fileNameTitle, 'id', newFileNameTitle, 'newId', 'mimeType'])
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=Ent.DRIVE_FILE_OR_FOLDER if returnIdLink is None else None)
|
|
if jcount == 0:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms):
|
|
continue
|
|
if (copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] or
|
|
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] != COPY_NONINHERITED_PERMISSIONS_NEVER):
|
|
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
|
|
if not sheet:
|
|
continue
|
|
copiedSourceFiles = {}
|
|
copiedTargetFiles = set()
|
|
shortcutsToCreate = []
|
|
statistics = _initStatistics()
|
|
if skipFileIdEntity['query'] or skipFileIdEntity[ROOT]:
|
|
_validateUserGetFileIDs(origUser, i, count, skipFileIdEntity, drive=drive)
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
source = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId,
|
|
fields='id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
|
|
'description,mimeType,modifiedTime,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare',
|
|
supportsAllDrives=True)
|
|
# Source at root of Shared Drive?
|
|
sourceMimeType = source['mimeType']
|
|
if sourceMimeType == MIMETYPE_GA_FOLDER and source.get('driveId') and source['name'] == TEAM_DRIVE and not source.get('parents', []):
|
|
source['name'] = _getSharedDriveNameFromId(source['driveId'])
|
|
sourceName = source['name']
|
|
sourceNameId = f"{sourceName}({source['id']})"
|
|
copyMoveOptions['sourceDriveId'] = source.get('driveId')
|
|
kvList = [Ent.USER, user, _getEntityMimeType(source), sourceNameId]
|
|
if fileId in copiedSourceFiles:
|
|
entityActionNotPerformedWarning(kvList, Msg.DUPLICATE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_DUPLICATE)
|
|
continue
|
|
if fileId in skipFileIdEntity['list']:
|
|
entityActionNotPerformedWarning(kvList, Msg.IN_SKIPIDS, j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_IN_SKIPIDS)
|
|
continue
|
|
trashed = source.pop('trashed', False)
|
|
if excludeTrashed and trashed:
|
|
entityActionNotPerformedWarning(kvList, Msg.IN_TRASH_AND_EXCLUDE_TRASHED, j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
|
|
continue
|
|
if copyMoveOptions['sourceDriveId']:
|
|
# If copying from a Shared Drive, user has to be an organizer
|
|
if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['sourceDriveId']):
|
|
_incrStatistic(statistics, STAT_USER_NOT_ORGANIZER)
|
|
continue
|
|
sourceSearchArgs = {'driveId': copyMoveOptions['sourceDriveId'], 'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
else:
|
|
sourceSearchArgs = {}
|
|
sourceParents = source.pop('parents', [])
|
|
if newParentsSpecified:
|
|
newParents = parentBody['parents']
|
|
numNewParents = len(newParents)
|
|
if numNewParents > 1:
|
|
entityActionNotPerformedWarning(kvList, Msg.MULTIPLE_PARENTS_SPECIFIED.format(numNewParents), j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
continue
|
|
else:
|
|
newParents = sourceParents if sourceParents else [ROOT]
|
|
source['parents'] = newParents
|
|
newParentId = newParents[0]
|
|
dest = _getCopyMoveParentInfo(drive, user, i, count, j, jcount, newParentId, statistics)
|
|
if dest is None:
|
|
continue
|
|
newParentName = dest['name']
|
|
newParentNameId = f'{newParentName}({newParentId})'
|
|
copyMoveOptions['destDriveId'] = dest['driveId']
|
|
copyMoveOptions['destParentType'] = dest['destParentType']
|
|
if copyMoveOptions['destDriveId']:
|
|
# If copying to a Shared Drive, user has to be an organizer
|
|
if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['destDriveId']):
|
|
_incrStatistic(statistics, STAT_USER_NOT_ORGANIZER)
|
|
continue
|
|
if not parentParms[DFA_SEARCHARGS]:
|
|
parentParms[DFA_SEARCHARGS] = {'driveId': copyMoveOptions['destDriveId'], 'corpora': 'drive',
|
|
'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
if copyMoveOptions['newFilename']:
|
|
destName = copyMoveOptions['newFilename']
|
|
elif (sourceMimeType == MIMETYPE_GA_FOLDER) and copyMoveOptions['mergeWithParent']:
|
|
destName = dest['name']
|
|
elif ((newParentsSpecified and newParentId not in sourceParents) or
|
|
((newParentId in sourceParents and
|
|
(sourceMimeType == MIMETYPE_GA_FOLDER and copyMoveOptions['duplicateFolders'] != DUPLICATE_FOLDER_MERGE) or
|
|
(sourceMimeType != MIMETYPE_GA_FOLDER and copyMoveOptions['duplicateFiles'] not in [DUPLICATE_FILE_OVERWRITE_ALL, DUPLICATE_FILE_OVERWRITE_OLDER])))):
|
|
if copyMoveOptions['replaceFilename']:
|
|
destName = processFilenameReplacements(sourceName, copyMoveOptions['replaceFilename'])
|
|
else:
|
|
destName = sourceName
|
|
elif copyMoveOptions['replaceFilename']:
|
|
destName = processFilenameReplacements(sourceName, copyMoveOptions['replaceFilename'])
|
|
else:
|
|
destName = f'Copy of {sourceName}'
|
|
if copyMoveOptions['stripNamePrefix'] and destName.startswith(copyMoveOptions['stripNamePrefix']):
|
|
destName = destName[len(copyMoveOptions['stripNamePrefix']):]
|
|
targetChildren = _getCopyMoveTargetInfo(drive, user, i, count, j, jcount, source, destName, newParentId, statistics, parentParms)
|
|
if targetChildren is None:
|
|
continue
|
|
# Copy folder
|
|
if sourceMimeType == MIMETYPE_GA_FOLDER:
|
|
copiedTargetFiles.add(newParentId) # Don't recopy folder copied into a sub-folder
|
|
if fileId == newParentId:
|
|
entityActionNotPerformedWarning(kvList, Msg.NOT_COPYABLE_INTO_ITSELF, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_FAILED)
|
|
continue
|
|
if copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE:
|
|
if _identicalSourceTarget(fileId, targetChildren):
|
|
entityActionNotPerformedWarning(kvList, Msg.NOT_COPYABLE_SAME_NAME_CURRENT_FOLDER_MERGE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_FAILED)
|
|
continue
|
|
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_UNIQUE_NAME:
|
|
destName = _getUniqueFilename(destName, sourceMimeType, targetChildren)
|
|
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_SKIP:
|
|
targetChild = _targetFilenameExists(destName, sourceMimeType, targetChildren)
|
|
if targetChild is not None:
|
|
entityModifierItemValueListActionNotPerformedWarning(kvList, Act.MODIFIER_TO,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f"{destName}({targetChild['id']})"],
|
|
Msg.DUPLICATE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_DUPLICATE)
|
|
continue
|
|
if recursive:
|
|
_recursiveFolderCopy(drive, user, i, count, j, jcount,
|
|
source, targetChildren, destName, newParentId, newParentName, dest['modifiedTime'],
|
|
True, 0)
|
|
kcount = len(shortcutsToCreate)
|
|
if kcount > 0:
|
|
entityPerformActionNumItems([Ent.USER, user], kcount, Ent.DRIVE_FILE_SHORTCUT, i, count)
|
|
Ind.Increment()
|
|
k = 0
|
|
for shortcut in shortcutsToCreate:
|
|
k += 1
|
|
targetId = shortcut['targetId']
|
|
if targetId in copiedSourceFiles and copyMoveOptions['copiedShortcutsPointToCopiedFiles']:
|
|
targetId = copiedSourceFiles[targetId]
|
|
_makeCopyShortcut(drive, user, k, kcount, _getEntityMimeType(shortcut), shortcut['childId'], shortcut['childName'],
|
|
shortcut['newChildName'], shortcut['newFolderId'], shortcut['newFolderName'], targetId)
|
|
Ind.Decrement()
|
|
else:
|
|
source.update(copyBody)
|
|
_cloneFolderCopy(drive, user, i, count, j, jcount,
|
|
source, targetChildren, destName, newParentId, newParentName, dest['modifiedTime'],
|
|
statistics, copyMoveOptions, True)
|
|
# Copy file
|
|
else:
|
|
if not source.pop('capabilities')['canCopy']:
|
|
entityActionFailedWarning(kvList, Msg.NOT_COPYABLE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
|
|
continue
|
|
if copyMoveOptions['duplicateFiles'] in [DUPLICATE_FILE_OVERWRITE_ALL, DUPLICATE_FILE_OVERWRITE_OLDER] and _identicalSourceTarget(fileId, targetChildren):
|
|
entityActionNotPerformedWarning(kvList, Msg.NOT_COPYABLE_SAME_NAME_CURRENT_FOLDER_OVERWRITE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
continue
|
|
if _checkForDuplicateTargetFile(drive, user, j, jcount, source, destName, targetChildren, copyMoveOptions, statistics):
|
|
continue
|
|
sourceId = source.pop('id')
|
|
sourceMimeType = source.pop('mimeType')
|
|
if copyMoveOptions['destDriveId']:
|
|
source.pop('copyRequiresWriterPermission', None)
|
|
source.pop('writersCanShare', None)
|
|
source.pop('driveId', None)
|
|
if sourceMimeType == MIMETYPE_GA_SHORTCUT:
|
|
source.pop('folderColorRgb', None)
|
|
source.update(copyBody)
|
|
result = callGAPI(drive.files(), 'copy',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_COPY_THROW_REASONS+[GAPI.INTERNAL_ERROR, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
|
|
fileId=fileId,
|
|
ignoreDefaultVisibility=copyParameters[DFA_IGNORE_DEFAULT_VISIBILITY],
|
|
keepRevisionForever=copyParameters[DFA_KEEP_REVISION_FOREVER],
|
|
body=source, fields='id,name,webViewLink', supportsAllDrives=True)
|
|
if returnIdLink:
|
|
writeStdout(f'{result[returnIdLink]}\n')
|
|
elif not csvPF:
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FILE, f"{result['name']}({result['id']})"],
|
|
j, jcount)
|
|
else:
|
|
_writeCSVData(user, sourceName, sourceId, result['name'], result['id'], sourceMimeType)
|
|
_incrStatistic(statistics, STAT_FILE_COPIED_MOVED)
|
|
if (sourceMimeType == MIMETYPE_GA_SPREADSHEET and
|
|
(copyMoveOptions['copySheetProtectedRangesInheritedPermissions'] or
|
|
copyMoveOptions['copySheetProtectedRangesNonInheritedPermissions'] != COPY_NONINHERITED_PERMISSIONS_NEVER)):
|
|
protectedSheetRanges = _getSheetProtectedRanges(sheet, user, i, count, j, jcount, sourceId, sourceName,
|
|
statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
|
|
_copyPermissions(drive, user, i, count, j, jcount,
|
|
Ent.DRIVE_FILE, sourceId, sourceName, result['id'], result['name'],
|
|
statistics, STAT_FILE_PERMISSIONS_FAILED,
|
|
copyMoveOptions, False,
|
|
'copySheetProtectedRangesInheritedPermissions',
|
|
'copySheetProtectedRangesNonInheritedPermissions',
|
|
True)
|
|
_updateSheetProtectedRanges(sheet, user, i, count, j, jcount, result['id'], result['name'], protectedSheetRanges,
|
|
statistics, STAT_FILE_PROTECTEDRANGES_FAILED)
|
|
elif copyMoveOptions['copyFilePermissions']:
|
|
_copyPermissions(drive, user, i, count, j, jcount,
|
|
Ent.DRIVE_FILE, sourceId, sourceName, result['id'], result['name'],
|
|
statistics, STAT_FILE_PERMISSIONS_FAILED,
|
|
copyMoveOptions, False,
|
|
'copyFileInheritedPermissions',
|
|
'copyFileNonInheritedPermissions',
|
|
True)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions,
|
|
GAPI.insufficientParentPermissions, GAPI.unknownError,
|
|
GAPI.invalid, GAPI.badRequest, GAPI.cannotCopyFile, GAPI.responsePreparationFailure, GAPI.fileNeverWritable, GAPI.fieldNotWritable,
|
|
GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.rateLimitExceeded, GAPI.userRateLimitExceeded,
|
|
GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
break
|
|
Ind.Decrement()
|
|
if copyMoveOptions['summary']:
|
|
_printStatistics(user, statistics, i, count, True)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Copied Files-Folders')
|
|
|
|
def _updateMoveFilePermissions(drive, user, i, count,
|
|
entityType, fileId, fileTitle,
|
|
statistics, stat, copyMoveOptions):
|
|
def getPermissions(fid):
|
|
permissions = {}
|
|
try:
|
|
result = callGAPIpages(drive.permissions(), 'list', 'permissions',
|
|
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
fileId=fid,
|
|
fields='nextPageToken,permissions(allowFileDiscovery,domain,emailAddress,expirationTime,id,role,type,deleted,view,pendingOwner,permissionDetails)',
|
|
useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'], supportsAllDrives=True)
|
|
for permission in result:
|
|
permission.pop('teamDrivePermissionDetails', None)
|
|
if not permission.pop('permissionDetails', [{'inherited': False}])[0]['inherited']:
|
|
permissions[permission['id']] = permission
|
|
return permissions
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
|
|
GAPI.unknownError, GAPI.invalid):
|
|
pass
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, stat)
|
|
return None
|
|
|
|
def permissionKVList(user, entityType, title, permission):
|
|
permstr = f"noninherited/{permission['role']}/{permission['type']}"
|
|
if permission['type'] in {'group', 'user'}:
|
|
permstr += f"/{permission['emailAddress']}"
|
|
elif permission['type'] == 'domain':
|
|
permstr += f"/{permission['domain']}"
|
|
return [Ent.USER, user, entityType, title, Ent.PERMISSION, permstr]
|
|
|
|
def isPermissionDeletable(kvList, permission):
|
|
domain = ''
|
|
if copyMoveOptions['excludePermissionsFromDomains'] or copyMoveOptions['includePermissionsFromDomains']:
|
|
if permission['type'] in {'group', 'user'}:
|
|
atLoc = permission.get('emailAddress', '').find('@')
|
|
if atLoc > 0:
|
|
domain = permission['emailAddress'][atLoc+1:]
|
|
elif permission['type'] == 'domain':
|
|
domain = permission.get('domain', '')
|
|
if domain and domain in copyMoveOptions['excludePermissionsFromDomains']:
|
|
notMovedMessage = f'domain {domain} excluded'
|
|
elif domain and copyMoveOptions['includePermissionsFromDomains'] and domain not in copyMoveOptions['includePermissionsFromDomains']:
|
|
notMovedMessage = f'domain {domain} not included'
|
|
elif permission.pop('deleted', False):
|
|
notMovedMessage = f"{permission['type']} {permission['emailAddress']} deleted"
|
|
else:
|
|
return False
|
|
deleteSourcePerms[permission['id']] = permission.copy()
|
|
if copyMoveOptions['showPermissionMessages']:
|
|
entityActionNotPerformedWarning(kvList, notMovedMessage, 0, 0)
|
|
return True
|
|
|
|
def mapPermissionsDomains(kvList, permission):
|
|
if 'emailAddress' in permission:
|
|
email, domain = permission['emailAddress'].lower().split('@', 1)
|
|
if domain in copyMoveOptions['mapPermissionsDomains']:
|
|
deleteSourcePerms[permission['id']] = permission.copy()
|
|
if copyMoveOptions['showPermissionMessages']:
|
|
notMovedMessage = f"domain {domain} mapped to {copyMoveOptions['mapPermissionsDomains'][domain]}"
|
|
entityActionNotPerformedWarning(kvList, notMovedMessage, 0, 0)
|
|
permission['emailAddress'] = f"{email}@{copyMoveOptions['mapPermissionsDomains'][domain]}"
|
|
addSourcePerms[permission['id']] = permission
|
|
return True
|
|
elif 'domain' in permission:
|
|
domain = permission['domain'].lower()
|
|
if domain in copyMoveOptions['mapPermissionsDomains']:
|
|
deleteSourcePerms[permission['id']] = permission.copy()
|
|
if copyMoveOptions['showPermissionMessages']:
|
|
notMovedMessage = f"domain {domain} mapped to {copyMoveOptions['mapPermissionsDomains'][domain]}"
|
|
entityActionNotPerformedWarning(kvList, notMovedMessage, 0, 0)
|
|
permission['domain'] = copyMoveOptions['mapPermissionsDomains'][domain]
|
|
addSourcePerms[permission['id']] = permission
|
|
return True
|
|
return False
|
|
|
|
sourcePerms = getPermissions(fileId)
|
|
if sourcePerms is None:
|
|
return
|
|
Ind.Increment()
|
|
deleteSourcePerms = {}
|
|
addSourcePerms = {}
|
|
for permissionId, permission in iter(sourcePerms.items()):
|
|
kvList = permissionKVList(user, entityType, fileTitle, permission)
|
|
if isPermissionDeletable(kvList, permission):
|
|
pass
|
|
elif copyMoveOptions['mapPermissionsDomains']:
|
|
mapPermissionsDomains(kvList, permission)
|
|
action = Act.Get()
|
|
kcount = len(deleteSourcePerms)
|
|
if kcount > 0:
|
|
Act.Set(Act.DELETE)
|
|
k = 0
|
|
for permissionId, permission in iter(deleteSourcePerms.items()):
|
|
k += 1
|
|
kvList = permissionKVList(user, entityType, fileTitle, permission)
|
|
try:
|
|
callGAPI(drive.permissions(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_DELETE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'],
|
|
enforceExpansiveAccess=copyMoveOptions['enforceExpansiveAccess'],
|
|
fileId=fileId, permissionId=permissionId, supportsAllDrives=True)
|
|
if copyMoveOptions['showPermissionMessages']:
|
|
entityActionPerformed(kvList, k, kcount)
|
|
except (GAPI.notFound, GAPI.permissionNotFound,
|
|
GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.fileNeverWritable, GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded, GAPI.cannotDeletePermission) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, stat)
|
|
Ind.Decrement()
|
|
Act.Set(action)
|
|
return
|
|
kcount = len(addSourcePerms)
|
|
if kcount > 0:
|
|
Act.Set(Act.CREATE)
|
|
k = 0
|
|
for permissionId, permission in iter(addSourcePerms.items()):
|
|
k += 1
|
|
kvList = permissionKVList(user, entityType, fileTitle, permission)
|
|
permission.pop('id')
|
|
if copyMoveOptions['destDriveId']:
|
|
permission.pop('pendingOwner', None)
|
|
sendNotificationEmail = False
|
|
while True:
|
|
try:
|
|
callGAPI(drive.permissions(), 'create',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS,
|
|
# retryReasons=[GAPI.INVALID_SHARING_REQUEST],
|
|
fileId=fileId, sendNotificationEmail=sendNotificationEmail, emailMessage=None,
|
|
body=permission, fields='', useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'], supportsAllDrives=True)
|
|
if copyMoveOptions['showPermissionMessages']:
|
|
entityActionPerformed(kvList, k, kcount)
|
|
break
|
|
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
|
|
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
|
|
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
|
|
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
|
|
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
|
|
GAPI.ownerOnTeamDriveItemNotSupported,
|
|
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
|
|
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
|
|
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
|
|
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
|
|
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility, GAPI.abusiveContentRestriction) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
break
|
|
except GAPI.invalidSharingRequest as e:
|
|
if not copyMoveOptions['sendEmailIfRequired'] or sendNotificationEmail or 'You are trying to invite' not in str(e):
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
break
|
|
sendNotificationEmail = True
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, stat)
|
|
Ind.Decrement()
|
|
Act.Set(action)
|
|
return
|
|
Ind.Decrement()
|
|
Act.Set(action)
|
|
|
|
# gam <UserTypeEntity> move drivefile <DriveFileEntity> [newfilename <DriveFileName>]
|
|
# [summary [<Boolean>]] [showpermissionsmessages [<Boolean>]]
|
|
# [<DriveFileParentAttribute>]
|
|
# [mergewithparent|mergewithparentretain [<Boolean>]]
|
|
# [createshortcutsfornonmovablefiles [<Boolean>]]
|
|
# [duplicatefiles overwriteolder|overwriteall|duplicatename|uniquename|skip]
|
|
# [duplicatefolders merge|duplicatename|uniquename|skip]
|
|
# [copymergewithparentfolderpermissions [<Boolean>]]
|
|
# [copymergedtopfolderpermissions [<Boolean>]]
|
|
# [copytopfolderpermissions [<Boolean>]]
|
|
# [copytopfolderiheritedpermissions [<Boolean>]]
|
|
# [copytopfoldernoniheritedpermissions never|always|syncallfolders|syncupdatedfolders]
|
|
# [copymergedsubfolderpermissions [<Boolean>]]
|
|
# [copysubfolderpermissions [<Boolean>]]
|
|
# [copysubfolderinheritedpermissions [<Boolean>]]
|
|
# [copysubfoldernoniheritedpermissions never|always|syncallfolders|syncupdatedfolders]
|
|
# [copypermissionroles <DriveFileACLRoleList>]
|
|
# [copypermissiontypes <DriveFileACLTypeList>]
|
|
# [synctopfoldernoniheritedpermissions [<Boolean>]] [syncsubfoldernoninheritedpermissions [<Boolean>]]
|
|
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
|
# (mappermissionsdomain <DomainName> <DomainName>)*
|
|
# [updatefilepermissions [<Boolean>]]
|
|
# [retainsourcefolders [<Boolean>]]
|
|
# [sendemailifrequired [<Boolean>]]
|
|
# [verifyorganizer [<Boolean>]]
|
|
# [enforceexpansiveaccess [<Boolean>]]
|
|
def moveDriveFile(users):
|
|
def _cloneFolderMove(drive, user, i, count, j, jcount,
|
|
source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime,
|
|
statistics, copyMoveOptions, atTop):
|
|
folderId = source.pop('id')
|
|
folderName = source['name']
|
|
sourceMimeType = source['mimeType']
|
|
folderNameId = f'{folderName}({folderId})'
|
|
kvList = [Ent.USER, user, Ent.DRIVE_FOLDER, folderNameId]
|
|
newParentNameId = f'{newParentName}({newParentId})'
|
|
# Merge top parent folder
|
|
if atTop and (copyMoveOptions['mergeWithParent'] or copyMoveOptions['mergeWithParentRetain']):
|
|
action = Act.Get()
|
|
Act.Set(Act.MOVE_MERGE)
|
|
entityPerformActionModifierItemValueList(kvList, Act.MODIFIER_CONTENTS_WITH, [Ent.DRIVE_FOLDER, newParentNameId], j, jcount)
|
|
Act.Set(action)
|
|
_incrStatistic(statistics, STAT_FOLDER_MERGED)
|
|
if (copyMoveOptions['copyMergeWithParentFolderPermissions'] and
|
|
copyMoveOptions['destParentType'] != DEST_PARENT_MYDRIVE_ROOT):
|
|
copyFolderNonInheritedPermissions =\
|
|
_getCopyFolderNonInheritedPermissions(copyMoveOptions,
|
|
'copyTopFolderNonInheritedPermissions',
|
|
source['modifiedTime'], mergeParentModifiedTime)
|
|
_copyPermissions(drive, user, i, count, j, jcount,
|
|
Ent.DRIVE_FOLDER, folderId, folderName, newParentId, newFolderName,
|
|
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
|
|
copyMoveOptions, True,
|
|
'copyTopFolderInheritedPermissions',
|
|
copyFolderNonInheritedPermissions,
|
|
False)
|
|
source.pop('oldparents', None)
|
|
return (newParentId, newParentName, True)
|
|
# Merge parent folders
|
|
if atTop and copyMoveOptions['sourceIsMyDriveSharedDrive']:
|
|
pass
|
|
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE:
|
|
newFolderNameLower = newFolderName.lower()
|
|
for target in targetChildren:
|
|
if not target.get('processed', False) and newFolderNameLower == target['name'].lower() and sourceMimeType == target['mimeType']:
|
|
target['processed'] = True
|
|
if target['capabilities']['canAddChildren']:
|
|
newFolderId = target['id']
|
|
action = Act.Get()
|
|
Act.Set(Act.MOVE_MERGE)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_CONTENTS_WITH, [Ent.DRIVE_FOLDER, f'{newFolderName}({newFolderId})'], j, jcount)
|
|
Act.Set(action)
|
|
_incrStatistic(statistics, STAT_FOLDER_MERGED)
|
|
if (copyMoveOptions[['copyMergedSubFolderPermissions', 'copyMergedTopFolderPermissions'][atTop]] and
|
|
(not atTop or copyMoveOptions['destParentType'] != DEST_PARENT_MYDRIVE_ROOT)):
|
|
copyFolderNonInheritedPermissions =\
|
|
_getCopyFolderNonInheritedPermissions(copyMoveOptions,
|
|
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
|
|
source['modifiedTime'], target['modifiedTime'])
|
|
_copyPermissions(drive, user, i, count, j, jcount,
|
|
Ent.DRIVE_FOLDER, folderId, folderName, newFolderId, newFolderName,
|
|
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
|
|
copyMoveOptions, atTop,
|
|
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
|
|
copyFolderNonInheritedPermissions,
|
|
False)
|
|
return (newFolderId, newFolderName, True)
|
|
entityActionFailedWarning(kvList+[Ent.DRIVE_FOLDER, newParentNameId], Msg.NOT_WRITABLE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_NOT_WRITABLE)
|
|
copyMoveOptions['retainSourceFolders'] = True
|
|
return (None, None, False)
|
|
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_UNIQUE_NAME:
|
|
newFolderName = _getUniqueFilename(newFolderName, sourceMimeType, targetChildren)
|
|
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_SKIP:
|
|
targetChild = _targetFilenameExists(newFolderName, sourceMimeType, targetChildren)
|
|
if targetChild is not None:
|
|
entityModifierItemValueListActionNotPerformedWarning(kvList, Act.MODIFIER_TO,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f"{newFolderName}({targetChild['id']})"],
|
|
Msg.DUPLICATE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_DUPLICATE)
|
|
copyMoveOptions['retainSourceFolders'] = True
|
|
return (None, None, False)
|
|
# Update parents on: not retain and MD->MD, SD->MD, SD->SD
|
|
if atTop and copyMoveOptions['sourceIsMyDriveSharedDrive']:
|
|
pass
|
|
elif (not copyMoveOptions['retainSourceFolders'] and (copyMoveOptions['sourceDriveId'] or not copyMoveOptions['destDriveId'])):
|
|
if newFolderName != folderName:
|
|
body = {'name': newFolderName}
|
|
else:
|
|
body = {}
|
|
removeParents = ','.join([parentId for parentId in source.pop('oldparents', []) if parentId not in source['parents']])
|
|
try:
|
|
callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST,
|
|
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.FILE_OWNER_NOT_MEMBER_OF_TEAMDRIVE,
|
|
GAPI.FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN,
|
|
GAPI.FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED,
|
|
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION,
|
|
GAPI.CANNOT_MOVE_TRASHED_ITEM_INTO_TEAMDRIVE,
|
|
GAPI.CANNOT_MOVE_TRASHED_ITEM_OUT_OF_TEAMDRIVE,
|
|
GAPI.CROSS_DOMAIN_MOVE_RESTRICTION,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED],
|
|
fileId=folderId,
|
|
addParents=newParentId, removeParents=removeParents,
|
|
body=body, fields=None, supportsAllDrives=True)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f'{newFolderName}({folderId})'],
|
|
j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_COPIED_MOVED)
|
|
return (None, None, False)
|
|
except (GAPI.badRequest, GAPI.insufficientParentPermissions, GAPI.fileOwnerNotMemberOfTeamDrive, GAPI.fileOwnerNotMemberOfWriterDomain,
|
|
GAPI.fileWriterTeamDriveMoveInDisabled, GAPI.targetUserRoleLimitedByLicenseRestriction,
|
|
GAPI.cannotMoveTrashedItemIntoTeamDrive, GAPI.cannotMoveTrashedItemOutOfTeamDrive,
|
|
GAPI.crossDomainMoveRestriction, GAPI.storageQuotaExceeded) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, folderName], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
copyMoveOptions['retainSourceFolders'] = True
|
|
return (None, None, False)
|
|
# Create new parent on: retain or MD->SD
|
|
source.pop('oldparents', None)
|
|
body = source.copy()
|
|
body.pop('capabilities', None)
|
|
if copyMoveOptions['sourceDriveId'] or copyMoveOptions['destDriveId']:
|
|
body.pop('copyRequiresWriterPermission', None)
|
|
body.pop('writersCanShare', None)
|
|
body.pop('trashed', None)
|
|
if not copyMoveOptions['destDriveId']:
|
|
body.pop('driveId', None)
|
|
body['name'] = newFolderName
|
|
try:
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INTERNAL_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
|
|
GAPI.BAD_REQUEST, GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION],
|
|
body=body, fields='id', supportsAllDrives=True)
|
|
newFolderId = result['id']
|
|
action = Act.Get()
|
|
Act.Set(Act.RECREATE)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_IN,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f'{newFolderName}({newFolderId})'],
|
|
j, jcount)
|
|
Act.Set(action)
|
|
_incrStatistic(statistics, STAT_FOLDER_COPIED_MOVED)
|
|
if copyMoveOptions[['copySubFolderPermissions', 'copyTopFolderPermissions'][atTop]]:
|
|
_copyPermissions(drive, user, i, count, j, jcount,
|
|
Ent.DRIVE_FOLDER, folderId, folderName, newFolderId, newFolderName,
|
|
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
|
|
copyMoveOptions, False,
|
|
['copySubFolderInheritedPermissions', 'copyTopFolderInheritedPermissions'][atTop],
|
|
['copySubFolderNonInheritedPermissions', 'copyTopFolderNonInheritedPermissions'][atTop],
|
|
True)
|
|
return (newFolderId, newFolderName, False)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.internalError, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,
|
|
GAPI.badRequest, GAPI.targetUserRoleLimitedByLicenseRestriction) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, newFolderName], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, STAT_FOLDER_FAILED)
|
|
copyMoveOptions['retainSourceFolders'] = True
|
|
return (None, None, False)
|
|
|
|
def _makeMoveShortcut(drive, user, k, kcount, entityType, childId, childName, newParentId, newParentName):
|
|
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
|
|
if entityType == Ent.DRIVE_FILE:
|
|
targetEntityType = Ent.DRIVE_FILE_SHORTCUT
|
|
statShortcutCreated = STAT_FILE_SHORTCUT_CREATED
|
|
statShortcutExists = STAT_FILE_SHORTCUT_EXISTS
|
|
else:
|
|
targetEntityType = Ent.DRIVE_FOLDER_SHORTCUT
|
|
statShortcutCreated = STAT_FOLDER_SHORTCUT_CREATED
|
|
statShortcutExists = STAT_FOLDER_SHORTCUT_EXISTS
|
|
newParentNameId = f'{newParentName}({newParentId})'
|
|
action = Act.Get()
|
|
existingShortcut = _checkForExistingShortcut(drive, childId, childName, newParentId)
|
|
if existingShortcut:
|
|
Act.Set(Act.CREATE_SHORTCUT)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_PREVIOUSLY_IN,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, targetEntityType, f"{childName}({existingShortcut})"],
|
|
k, kcount)
|
|
Act.Set(action)
|
|
_incrStatistic(statistics, statShortcutExists)
|
|
return
|
|
body = {'name': childName, 'mimeType': MIMETYPE_GA_SHORTCUT,
|
|
'parents': [newParentId], 'shortcutDetails': {'targetId': childId}}
|
|
try:
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID,
|
|
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION],
|
|
body=body, fields='id', supportsAllDrives=True)
|
|
Act.Set(Act.CREATE_SHORTCUT)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_IN,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, targetEntityType, f"{childName}({result['id']})"],
|
|
k, kcount)
|
|
Act.Set(action)
|
|
_incrStatistic(statistics, statShortcutCreated)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.invalid, GAPI.badRequest, GAPI.fileNotFound, GAPI.unknownError,
|
|
GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid,
|
|
GAPI.targetUserRoleLimitedByLicenseRestriction) as e:
|
|
entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
|
|
def _moveFile(drive, user, i, count, k, kcount, entityType, childId, childName, newChildName, newParentId, newParentName, removeParents, body):
|
|
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
|
|
newParentNameId = f'{newParentName}({newParentId})'
|
|
if updateFilePermissions:
|
|
_updateMoveFilePermissions(drive, user, i, count,
|
|
entityType, childId, childName,
|
|
statistics, STAT_FILE_PERMISSIONS_FAILED, copyMoveOptions)
|
|
try:
|
|
callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST,
|
|
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.FILE_OWNER_NOT_MEMBER_OF_TEAMDRIVE,
|
|
GAPI.FILE_OWNER_NOT_MEMBER_OF_WRITER_DOMAIN,
|
|
GAPI.FILE_WRITER_TEAMDRIVE_MOVE_IN_DISABLED,
|
|
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION,
|
|
GAPI.CANNOT_MOVE_TRASHED_ITEM_INTO_TEAMDRIVE,
|
|
GAPI.CANNOT_MOVE_TRASHED_ITEM_OUT_OF_TEAMDRIVE,
|
|
GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED,
|
|
GAPI.CROSS_DOMAIN_MOVE_RESTRICTION,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED],
|
|
fileId=childId, addParents=newParentId, removeParents=removeParents,
|
|
body=body, fields='', supportsAllDrives=True)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_TO,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, entityType, f'{newChildName}({childId})'],
|
|
k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_COPIED_MOVED)
|
|
return
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.unknownError, GAPI.badRequest,
|
|
GAPI.targetUserRoleLimitedByLicenseRestriction,
|
|
GAPI.cannotMoveTrashedItemIntoTeamDrive, GAPI.cannotMoveTrashedItemOutOfTeamDrive,
|
|
GAPI.teamDrivesShortcutFileNotSupported, GAPI.storageQuotaExceeded) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
except (GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.fileOwnerNotMemberOfTeamDrive, GAPI.fileOwnerNotMemberOfWriterDomain,
|
|
GAPI.fileWriterTeamDriveMoveInDisabled,
|
|
GAPI.crossDomainMoveRestriction) as e:
|
|
if not copyMoveOptions['createShortcutsForNonmovableFiles']:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
else:
|
|
_makeMoveShortcut(drive, user, k, kcount, entityType, childId, childName, newParentId, newParentName)
|
|
copyMoveOptions['retainSourceFolders'] = True
|
|
|
|
def _recursiveFolderMove(drive, user, i, count, j, jcount,
|
|
source, targetChildren, newFolderName, newParentId, newParentName, mergeParentModifiedTime, atTop):
|
|
folderId = source['id']
|
|
newFolderId, newFolderName, existingTargetFolder = _cloneFolderMove(drive, user, i, count, j, jcount,
|
|
source, targetChildren, newFolderName,
|
|
newParentId, newParentName, mergeParentModifiedTime,
|
|
statistics, copyMoveOptions, atTop)
|
|
if newFolderId is None:
|
|
return
|
|
movedFiles[newFolderId] = 1
|
|
sourceChildren = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=WITH_PARENTS.format(folderId),
|
|
orderBy='folder desc,name,modifiedTime desc',
|
|
fields='nextPageToken,files(id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
|
|
'description,folderColorRgb,mimeType,modifiedTime,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare)',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **sourceSearchArgs)
|
|
kcount = len(sourceChildren)
|
|
if kcount > 0:
|
|
if existingTargetFolder:
|
|
subTargetChildren = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=ANY_NON_TRASHED_WITH_PARENTS.format(newFolderId),
|
|
orderBy='folder desc,name,modifiedTime desc',
|
|
fields='nextPageToken,files(id,name,capabilities,mimeType,modifiedTime)',
|
|
**parentParms[DFA_SEARCHARGS])
|
|
else:
|
|
subTargetChildren = []
|
|
Ind.Increment()
|
|
k = 0
|
|
for child in sourceChildren:
|
|
k += 1
|
|
childId = child['id']
|
|
childName = child['name']
|
|
childNameId = f'{childName}({childId})'
|
|
if movedFiles.get(childId):
|
|
continue
|
|
trashed = child.pop('trashed', False)
|
|
if (childId == newFolderId) or (copyMoveOptions['destDriveId'] and trashed):
|
|
entityActionNotPerformedWarning([Ent.USER, user, _getEntityMimeType(child), childNameId],
|
|
[Msg.NOT_MOVABLE, Msg.NOT_MOVABLE_IN_TRASH][trashed], k, kcount)
|
|
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
|
|
continue
|
|
childParents = child.pop('parents', [])
|
|
child['parents'] = [newFolderId]
|
|
if child['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
child['oldparents'] = childParents
|
|
_recursiveFolderMove(drive, user, i, count, k, kcount,
|
|
child, subTargetChildren, childName, newFolderId, newFolderName, child['modifiedTime'], False)
|
|
else:
|
|
if existingTargetFolder and _checkForDuplicateTargetFile(drive, user, k, kcount, child, childName, subTargetChildren, copyMoveOptions, statistics):
|
|
copyMoveOptions['retainSourceFolders'] = True
|
|
continue
|
|
if childName != child['name']:
|
|
body = {'name': child['name']}
|
|
else:
|
|
body = {}
|
|
removeParents = ','.join(childParents)
|
|
_moveFile(drive, user, i, count, k, kcount, Ent.DRIVE_FILE, childId, childName, childName, newFolderId, newFolderName, removeParents, body)
|
|
Ind.Decrement()
|
|
sourceName = source['name']
|
|
kvList = [Ent.USER, user, Ent.DRIVE_FOLDER, f"{sourceName}({folderId})"]
|
|
if (atTop and (copyMoveOptions['mergeWithParentRetain'] or copyMoveOptions['sourceIsMyDriveSharedDrive'])) or copyMoveOptions['retainSourceFolders']:
|
|
Act.Set(Act.RETAIN)
|
|
entityActionPerformed(kvList, i, count)
|
|
else:
|
|
Act.Set(Act.DELETE)
|
|
try:
|
|
callGAPI(drive.files(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
fileId=folderId, supportsAllDrives=True)
|
|
entityActionPerformed(kvList, i, count)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.fileNeverWritable) as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
Act.Set(Act.MOVE)
|
|
return
|
|
|
|
def anyFolderPermissionOperations():
|
|
for field in ['copyMergeWithParentFolderPermissions',
|
|
'copyMergedTopFolderPermissions', 'copyMergedSubFolderPermissions',
|
|
'copyTopFolderPermissions', 'copySubFolderPermissions']:
|
|
if copyMoveOptions[field]:
|
|
return True
|
|
return False
|
|
|
|
fileIdEntity = getDriveFileEntity()
|
|
parentBody = {}
|
|
parentParms = initDriveFileAttributes()
|
|
copyMoveOptions = initCopyMoveOptions(False)
|
|
newParentsSpecified = updateFilePermissions = False
|
|
movedFiles = {}
|
|
verifyOrganizer = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getCopyMoveOptions(myarg, copyMoveOptions):
|
|
pass
|
|
elif getDriveFileParentAttribute(myarg, parentParms):
|
|
newParentsSpecified = True
|
|
elif myarg == 'updatefilepermissions':
|
|
updateFilePermissions = getBoolean()
|
|
elif myarg == 'verifyorganizer':
|
|
verifyOrganizer = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE_OR_FOLDER)
|
|
if jcount == 0:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms):
|
|
continue
|
|
statistics = _initStatistics()
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
source = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId,
|
|
fields='id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
|
|
'description,mimeType,modifiedTime,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare',
|
|
supportsAllDrives=True)
|
|
# Source at root of My Drive or Shared Drive?
|
|
sourceMimeType = source['mimeType']
|
|
if sourceMimeType == MIMETYPE_GA_FOLDER and source['name'] in [MY_DRIVE, TEAM_DRIVE] and not source.get('parents', []):
|
|
copyMoveOptions['sourceIsMyDriveSharedDrive'] = True
|
|
if source.get('driveId'):
|
|
source['name'] = _getSharedDriveNameFromId(source['driveId'])
|
|
sourceName = source['name']
|
|
sourceNameId = f"{sourceName}({source['id']})"
|
|
copyMoveOptions['sourceDriveId'] = source.get('driveId')
|
|
kvList = [Ent.USER, user, _getEntityMimeType(source), sourceNameId]
|
|
if fileId in movedFiles:
|
|
entityActionNotPerformedWarning(kvList, Msg.DUPLICATE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_DUPLICATE)
|
|
continue
|
|
if copyMoveOptions['sourceDriveId']:
|
|
# If moving from a Shared Drive, user has to be an organizer
|
|
if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['sourceDriveId']):
|
|
_incrStatistic(statistics, STAT_USER_NOT_ORGANIZER)
|
|
continue
|
|
if source['trashed']:
|
|
entityActionNotPerformedWarning(kvList, Msg.NOT_MOVABLE_IN_TRASH, j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
|
|
continue
|
|
sourceSearchArgs = {'driveId': copyMoveOptions['sourceDriveId'], 'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
else:
|
|
sourceSearchArgs = {}
|
|
sourceParents = source.pop('parents', [])
|
|
if newParentsSpecified:
|
|
newParents = parentBody['parents']
|
|
numNewParents = len(newParents)
|
|
if numNewParents > 1:
|
|
entityActionNotPerformedWarning(kvList, Msg.MULTIPLE_PARENTS_SPECIFIED.format(numNewParents), j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
continue
|
|
else:
|
|
newParents = sourceParents if sourceParents else [ROOT]
|
|
source['parents'] = newParents
|
|
newParentId = newParents[0]
|
|
dest = _getCopyMoveParentInfo(drive, user, i, count, j, jcount, newParentId, statistics)
|
|
if dest is None:
|
|
continue
|
|
newParentName = dest['name']
|
|
newParentNameId = f'{newParentName}({newParentId})'
|
|
copyMoveOptions['destDriveId'] = dest['driveId']
|
|
copyMoveOptions['destParentType'] = dest['destParentType']
|
|
if copyMoveOptions['destDriveId'] and not parentParms[DFA_SEARCHARGS]:
|
|
parentParms[DFA_SEARCHARGS] = {'driveId': copyMoveOptions['destDriveId'], 'corpora': 'drive',
|
|
'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
if copyMoveOptions['newFilename']:
|
|
destName = copyMoveOptions['newFilename']
|
|
elif (sourceMimeType == MIMETYPE_GA_FOLDER) and (copyMoveOptions['mergeWithParent'] or copyMoveOptions['mergeWithParentRetain']):
|
|
destName = dest['name']
|
|
else:
|
|
destName = sourceName
|
|
targetChildren = _getCopyMoveTargetInfo(drive, user, i, count, j, jcount, source, destName, newParentId, statistics, parentParms)
|
|
if targetChildren is None:
|
|
continue
|
|
if copyMoveOptions['destDriveId']:
|
|
# If moving to a Shared Drive, user has to be an organizer
|
|
if verifyOrganizer and not _verifyUserIsOrganizer(drive, user, i, count, copyMoveOptions['destDriveId']):
|
|
_incrStatistic(statistics, STAT_USER_NOT_ORGANIZER)
|
|
continue
|
|
# 3rd party shortcuts can't be moved to Shared Drives
|
|
if sourceMimeType.startswith(MIMETYPE_GA_3P_SHORTCUT):
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_3PSHORTCUT, sourceNameId], Msg.NOT_MOVABLE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_NOT_COPYABLE_MOVABLE)
|
|
continue
|
|
# Move folder
|
|
if sourceMimeType == MIMETYPE_GA_FOLDER:
|
|
if fileId == newParentId:
|
|
entityActionNotPerformedWarning(kvList, Msg.NOT_MOVABLE_INTO_ITSELF, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_FAILED)
|
|
continue
|
|
if copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE:
|
|
if _identicalSourceTarget(fileId, targetChildren):
|
|
entityActionNotPerformedWarning(kvList, Msg.NOT_MOVABLE_SAME_NAME_CURRENT_FOLDER_MERGE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_FAILED)
|
|
continue
|
|
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_UNIQUE_NAME:
|
|
destName = _getUniqueFilename(destName, sourceMimeType, targetChildren)
|
|
elif copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_SKIP:
|
|
targetChild = _targetFilenameExists(destName, sourceMimeType, targetChildren)
|
|
if targetChild is not None:
|
|
entityModifierItemValueListActionNotPerformedWarning(kvList, Act.MODIFIER_TO,
|
|
[Ent.DRIVE_FOLDER, newParentNameId, Ent.DRIVE_FOLDER, f"{destName}({targetChild['id']})"],
|
|
Msg.DUPLICATE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FOLDER_DUPLICATE)
|
|
continue
|
|
if ((not copyMoveOptions['sourceDriveId'] and copyMoveOptions['destDriveId']) or
|
|
(copyMoveOptions['mergeWithParent'] or copyMoveOptions['mergeWithParentRetain'] or copyMoveOptions['retainSourceFolders']) or
|
|
(copyMoveOptions['duplicateFolders'] == DUPLICATE_FOLDER_MERGE and _targetFilenameExists(destName, sourceMimeType, targetChildren) is not None) or
|
|
anyFolderPermissionOperations()):
|
|
source['oldparents'] = sourceParents
|
|
_recursiveFolderMove(drive, user, i, count, j, jcount,
|
|
source, targetChildren, destName, newParentId, newParentName, dest['modifiedTime'], True)
|
|
continue
|
|
entityType = Ent.DRIVE_FOLDER
|
|
# Move file
|
|
else:
|
|
if copyMoveOptions['duplicateFiles'] in [DUPLICATE_FILE_OVERWRITE_ALL, DUPLICATE_FILE_OVERWRITE_OLDER] and _identicalSourceTarget(fileId, targetChildren):
|
|
entityActionNotPerformedWarning(kvList, Msg.NOT_MOVABLE_SAME_NAME_CURRENT_FOLDER_OVERWRITE, j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
continue
|
|
if _checkForDuplicateTargetFile(drive, user, j, jcount, source, destName, targetChildren, copyMoveOptions, statistics):
|
|
continue
|
|
destName = source['name'] # duplicatefiles uniquename may cause rename
|
|
entityType = Ent.DRIVE_FILE
|
|
# Simple move file/folder
|
|
if destName != sourceName:
|
|
body = {'name': destName}
|
|
else:
|
|
body = {}
|
|
# All parents removed from top level moved item as non-path parents can't be determined
|
|
removeParents = ','.join(sourceParents)
|
|
_moveFile(drive, user, i, count, j, jcount, entityType, fileId, sourceName, destName, newParentId, newParentName, removeParents, body)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.invalid, GAPI.badRequest, GAPI.cannotCopyFile,
|
|
GAPI.fileOwnerNotMemberOfTeamDrive, GAPI.fileOwnerNotMemberOfWriterDomain,
|
|
GAPI.fileWriterTeamDriveMoveInDisabled,
|
|
GAPI.cannotMoveTrashedItemIntoTeamDrive, GAPI.cannotMoveTrashedItemOutOfTeamDrive,
|
|
GAPI.teamDrivesShortcutFileNotSupported, GAPI.crossDomainMoveRestriction) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], str(e), j, jcount)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
_incrStatistic(statistics, STAT_FILE_FAILED)
|
|
break
|
|
Ind.Decrement()
|
|
if copyMoveOptions['summary']:
|
|
_printStatistics(user, statistics, i, count, False)
|
|
|
|
DELETE_DRIVEFILE_CHOICE_MAP = {'purge': 'delete', 'trash': 'trash', 'untrash': 'untrash'}
|
|
DELETE_DRIVEFILE_FUNCTION_TO_ACTION_MAP = {'delete': Act.PURGE, 'trash': Act.TRASH, 'untrash': Act.UNTRASH}
|
|
DELETE_DRIVEFILE_FUNCTION_TO_CAPABILITY_MAP = {'delete': 'canDelete', 'trash': 'canTrash', 'untrash': 'canUntrash'}
|
|
|
|
# gam <UserTypeEntity> delete drivefile <DriveFileEntity> [purge|trash|untrash] [shortcutandtarget [<Boolean>]]
|
|
def deleteDriveFile(users, function=None):
|
|
fileIdEntity = getDriveFileEntity()
|
|
if not function:
|
|
function = getChoice(DELETE_DRIVEFILE_CHOICE_MAP, defaultChoice='trash', mapChoice=True)
|
|
if checkArgumentPresent('shortcutandtarget'):
|
|
shortcutAndTarget = getBoolean()
|
|
else:
|
|
shortcutAndTarget = False
|
|
checkForExtraneousArguments()
|
|
Act.Set(DELETE_DRIVEFILE_FUNCTION_TO_ACTION_MAP[function])
|
|
if function != 'delete':
|
|
trash_body = {'trashed': function == 'trash'}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE_OR_FOLDER)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
fileInfoList = []
|
|
if shortcutAndTarget:
|
|
capability = DELETE_DRIVEFILE_FUNCTION_TO_CAPABILITY_MAP[function]
|
|
fileInfo = (fileId, Ent.DRIVE_FILE_OR_FOLDER, Ent.DRIVE_FILE_OR_FOLDER_ID)
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields=f'name,mimeType,shortcutDetails,capabilities({capability})', supportsAllDrives=True)
|
|
if result['mimeType'] == MIMETYPE_GA_SHORTCUT:
|
|
if not result['capabilities'][capability]:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_SHORTCUT, result['name']],
|
|
Msg.SHORTCUT_TARGET_CAPABILITY_IS_FALSE.format('Shortcut', capability), j, jcount)
|
|
continue
|
|
fileInfoList.append((fileId, Ent.DRIVE_SHORTCUT, Ent.DRIVE_SHORTCUT_ID))
|
|
fileId = result['shortcutDetails']['targetId']
|
|
fileInfo = (fileId, Ent.DRIVE_FILE_OR_FOLDER, Ent.DRIVE_FILE_OR_FOLDER_ID)
|
|
tresult = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields=f'name,capabilities({capability})', supportsAllDrives=True)
|
|
if not tresult['capabilities'][capability]:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_SHORTCUT, result['name'], Ent.DRIVE_FILE_OR_FOLDER, tresult['name']],
|
|
Msg.SHORTCUT_TARGET_CAPABILITY_IS_FALSE.format('Target', capability), j, jcount)
|
|
continue
|
|
fileInfoList.append((fileId, Ent.DRIVE_FILE_OR_FOLDER, Ent.DRIVE_FILE_OR_FOLDER_ID))
|
|
for fileInfo in fileInfoList:
|
|
fileId = fileInfo[0]
|
|
if function != 'delete':
|
|
result = callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
fileId=fileId, body=trash_body, fields='name', supportsAllDrives=True)
|
|
if result and 'name' in result:
|
|
fileName = result['name']
|
|
else:
|
|
fileName = fileId
|
|
else:
|
|
callGAPI(drive.files(), function,
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
fileId=fileId, supportsAllDrives=True)
|
|
fileName = fileId
|
|
entityActionPerformed([Ent.USER, user, fileInfo[1], fileName], j, jcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.fileNeverWritable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, fileInfo[2], fileId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> purge drivefile <DriveFileEntity> [shortcutandtarget [<Boolean>]]
|
|
def purgeDriveFile(users):
|
|
deleteDriveFile(users, 'delete')
|
|
|
|
# gam <UserTypeEntity> trash drivefile <DriveFileEntity> [shortcutandtarget [<Boolean>]]
|
|
def trashDriveFile(users):
|
|
deleteDriveFile(users, 'trash')
|
|
|
|
# gam <UserTypeEntity> untrash drivefile <DriveFileEntity> [shortcutandtarget [<Boolean>]]
|
|
def untrashDriveFile(users):
|
|
deleteDriveFile(users, 'untrash')
|
|
|
|
NON_DOWNLOADABLE_MIMETYPES = [MIMETYPE_GA_FORM, MIMETYPE_GA_FUSIONTABLE, MIMETYPE_GA_MAP, MIMETYPE_GA_FOLDER, MIMETYPE_GA_SHORTCUT]
|
|
|
|
GOOGLEDOC_VALID_EXTENSIONS_MAP = {
|
|
MIMETYPE_GA_DRAWING: ['.jpeg', '.jpg', '.pdf', '.png', '.svg'],
|
|
MIMETYPE_GA_DOCUMENT: ['.docx', '.epub', '.html', '.odt', '.pdf', '.rtf', '.txt', '.zip'],
|
|
MIMETYPE_GA_JAM: ['.pdf'],
|
|
MIMETYPE_GA_PRESENTATION: ['.pdf', '.pptx', '.odp', '.txt'],
|
|
MIMETYPE_GA_SCRIPT: ['.json'],
|
|
MIMETYPE_GA_SPREADSHEET: ['.csv', '.ods', '.pdf', '.tsv', '.xlsx', '.zip'],
|
|
}
|
|
|
|
MICROSOFT_FORMATS_LIST = [
|
|
{'mime': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'ext': '.docx'},
|
|
{'mime': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'ext': '.dotx'},
|
|
{'mime': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'ext': '.pptx'},
|
|
{'mime': 'application/vnd.openxmlformats-officedocument.presentationml.template', 'ext': '.potx'},
|
|
{'mime': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ext': '.xlsx'},
|
|
{'mime': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'ext': '.xltx'},
|
|
{'mime': 'application/msword', 'ext': '.doc'},
|
|
{'mime': 'application/msword', 'ext': '.dot'},
|
|
{'mime': 'application/vnd.ms-powerpoint', 'ext': '.ppt'},
|
|
{'mime': 'application/vnd.ms-powerpoint', 'ext': '.pot'},
|
|
{'mime': 'application/vnd.ms-excel', 'ext': '.xls'},
|
|
{'mime': 'application/vnd.ms-excel', 'ext': '.xlt'},
|
|
]
|
|
|
|
OPENOFFICE_FORMATS_LIST = [
|
|
{'mime': 'application/vnd.oasis.opendocument.presentation', 'ext': '.odp'},
|
|
{'mime': 'application/x-vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
|
|
{'mime': 'application/vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
|
|
{'mime': 'application/vnd.oasis.opendocument.text', 'ext': '.odt'},
|
|
]
|
|
|
|
DOCUMENT_FORMATS_MAP = {
|
|
'csv': [{'mime': 'text/csv', 'ext': '.csv'}],
|
|
'doc': [{'mime': 'application/msword', 'ext': '.doc'}],
|
|
'dot': [{'mime': 'application/msword', 'ext': '.dot'}],
|
|
'docx': [{'mime': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'ext': '.docx'}],
|
|
'dotx': [{'mime': 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'ext': '.dotx'}],
|
|
'epub': [{'mime': 'application/epub+zip', 'ext': '.epub'}],
|
|
'html': [{'mime': 'text/html', 'ext': '.html'}],
|
|
'jpeg': [{'mime': 'image/jpeg', 'ext': '.jpeg'}],
|
|
'jpg': [{'mime': 'image/jpeg', 'ext': '.jpg'}],
|
|
'json': [{'mime': MIMETYPE_GA_SCRIPT_JSON, 'ext': '.json'}],
|
|
'mht': [{'mime': 'message/rfc822', 'ext': 'mht'}],
|
|
'odp': [{'mime': 'application/vnd.oasis.opendocument.presentation', 'ext': '.odp'}],
|
|
'ods': [{'mime': 'application/x-vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'},
|
|
{'mime': 'application/vnd.oasis.opendocument.spreadsheet', 'ext': '.ods'}],
|
|
'odt': [{'mime': 'application/vnd.oasis.opendocument.text', 'ext': '.odt'}],
|
|
'pdf': [{'mime': 'application/pdf', 'ext': '.pdf'}],
|
|
'png': [{'mime': 'image/png', 'ext': '.png'}],
|
|
'ppt': [{'mime': 'application/vnd.ms-powerpoint', 'ext': '.ppt'}],
|
|
'pot': [{'mime': 'application/vnd.ms-powerpoint', 'ext': '.pot'}],
|
|
'potx': [{'mime': 'application/vnd.openxmlformats-officedocument.presentationml.template', 'ext': '.potx'}],
|
|
'pptx': [{'mime': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'ext': '.pptx'}],
|
|
'rtf': [{'mime': 'application/rtf', 'ext': '.rtf'}],
|
|
'svg': [{'mime': 'image/svg+xml', 'ext': '.svg'}],
|
|
'tsv': [{'mime': 'text/tab-separated-values', 'ext': '.tsv'},
|
|
{'mime': 'text/tsv', 'ext': '.tsv'}],
|
|
'txt': [{'mime': 'text/plain', 'ext': '.txt'}],
|
|
'xls': [{'mime': 'application/vnd.ms-excel', 'ext': '.xls'}],
|
|
'xlt': [{'mime': 'application/vnd.ms-excel', 'ext': '.xlt'}],
|
|
'xlsx': [{'mime': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ext': '.xlsx'}],
|
|
'xltx': [{'mime': 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', 'ext': '.xltx'}],
|
|
'zip': [{'mime': 'application/zip', 'ext': '.zip'}],
|
|
}
|
|
|
|
MIMETYPE_EXTENSION_MAP = {
|
|
'application/epub+zip': '.epub',
|
|
'application/msword': '.doc',
|
|
'application/octet-stream': '',
|
|
'application/pdf': '.pdf',
|
|
'application/rtf': '.rtf',
|
|
'application/vnd.ms-excel': '.xls',
|
|
'application/vnd.ms-powerpoint': '.ppt',
|
|
'application/vnd.oasis.opendocument.presentation': '.odp',
|
|
'application/vnd.oasis.opendocument.spreadsheet': '.ods',
|
|
'application/vnd.oasis.opendocument.text': '.odt',
|
|
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx',
|
|
'application/vnd.openxmlformats-officedocument.presentationml.template': '.potx',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.template': '.xltx',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.template': '.dotx',
|
|
'application/x-vnd.oasis.opendocument.spreadsheet': '.ods',
|
|
'application/zip': '.zip',
|
|
'image/gif': '.gif',
|
|
'image/jpeg': '.jpg',
|
|
'image/jpg': '.jpg',
|
|
'image/png': '.png',
|
|
'image/svg+xml': '.svg',
|
|
'image/webp': '.webp',
|
|
'message/rfc822': 'mht',
|
|
'text/csv': '.csv',
|
|
'text/html': '.html',
|
|
'text/plain': '.txt',
|
|
'text/rtf': '.rtf',
|
|
'text/tab-separated-values': '.tsv',
|
|
'text/tsv': '.tsv',
|
|
}
|
|
|
|
HTTP_ERROR_PATTERN = re.compile(r'^.*returned "(.*)">$')
|
|
|
|
# gam <UserTypeEntity> get drivefile <DriveFileEntity> [revision <DriveFileRevisionID>]
|
|
# [(format <FileFormatList>)|(gsheet|csvsheet <SheetEntity>)] [exportsheetaspdf <String>]
|
|
# [targetfolder <FilePath>] [targetname -|<FileName>]
|
|
# [donotfollowshortcuts [<Boolean>]] [overwrite [<Boolean>]] [showprogress [<Boolean>]]
|
|
# [acknowledgeabuse [<Boolean>]]
|
|
def getDriveFile(users):
|
|
def closeRemoveTargetFile(f):
|
|
if f and not targetStdout:
|
|
closeFile(f)
|
|
os.remove(filename)
|
|
|
|
fileIdEntity = getDriveFileEntity()
|
|
sheetEntity = None
|
|
exportSheetAsPDF = revisionId = ''
|
|
exportFormatName = 'openoffice'
|
|
exportFormatChoices = [exportFormatName]
|
|
exportFormats = OPENOFFICE_FORMATS_LIST
|
|
defaultFormats = True
|
|
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
|
|
targetNamePattern = None
|
|
acknowledgeAbuse = donotFollowShortcuts = overwrite = showProgress = suppressStdoutMsgs = targetStdout = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'format':
|
|
exportFormatChoices = getString(Cmd.OB_FORMAT_LIST).replace(',', ' ').lower().split()
|
|
exportFormats = []
|
|
for exportFormat in exportFormatChoices:
|
|
if exportFormat in {'ms', 'microsoft', 'micro$oft'}:
|
|
exportFormats.extend(MICROSOFT_FORMATS_LIST)
|
|
elif exportFormat == 'openoffice':
|
|
exportFormats.extend(OPENOFFICE_FORMATS_LIST)
|
|
elif exportFormat in DOCUMENT_FORMATS_MAP:
|
|
exportFormats.extend(DOCUMENT_FORMATS_MAP[exportFormat])
|
|
else:
|
|
invalidChoiceExit(exportFormat, DOCUMENT_FORMATS_MAP, True)
|
|
defaultFormats = False
|
|
elif myarg == 'targetfolder':
|
|
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
elif myarg == 'targetname':
|
|
targetNamePattern = getString(Cmd.OB_FILE_NAME)
|
|
targetStdout = targetNamePattern == '-'
|
|
suppressStdoutMsgs = False if not targetStdout else GM.Globals[GM.STDOUT][GM.REDIRECT_STD]
|
|
elif myarg == 'donotfollowshortcuts':
|
|
donotFollowShortcuts = getBoolean()
|
|
elif myarg == 'overwrite':
|
|
overwrite = getBoolean()
|
|
elif myarg == 'revision':
|
|
revisionId = getString(Cmd.OB_DRIVE_FILE_REVISION_ID)
|
|
elif myarg in {'gsheet', 'csvsheet'}:
|
|
sheetEntity = getSheetEntity(False)
|
|
elif myarg == 'exportsheetaspdf':
|
|
exportSheetAsPDF = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'nocache':
|
|
pass
|
|
elif myarg == 'showprogress':
|
|
showProgress = getBoolean()
|
|
elif myarg == 'acknowledgeabuse':
|
|
acknowledgeAbuse = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
if exportSheetAsPDF:
|
|
exportFormatName = 'pdf'
|
|
exportFormatChoices = [exportFormatName]
|
|
exportFormats = DOCUMENT_FORMATS_MAP[exportFormatName]
|
|
elif sheetEntity:
|
|
if defaultFormats:
|
|
exportFormatName = 'csv'
|
|
else:
|
|
exportFormatName = exportFormats[0]['ext'][1:]
|
|
exportFormatChoices = [exportFormatName]
|
|
exportFormats = DOCUMENT_FORMATS_MAP[exportFormatName]
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE if not suppressStdoutMsgs else None)
|
|
if jcount == 0:
|
|
continue
|
|
_, userName, _ = splitEmailAddressOrUID(user)
|
|
if exportSheetAsPDF or sheetEntity:
|
|
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
|
|
if not sheet:
|
|
continue
|
|
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
targetName = _substituteForUser(targetNamePattern, user, userName) if targetNamePattern else None
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
fileExtension = None
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='name,fullFileExtension,mimeType,quotaBytesUsed,shortcutDetails', supportsAllDrives=True)
|
|
mimeType = result['mimeType']
|
|
if (mimeType == MIMETYPE_GA_SHORTCUT) and not donotFollowShortcuts:
|
|
fileId = result['shortcutDetails']['targetId']
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='name,fullFileExtension,mimeType,size', supportsAllDrives=True)
|
|
mimeType = result['mimeType']
|
|
entityValueList = [Ent.USER, user, _getEntityMimeType(result), result['name']]
|
|
if mimeType in NON_DOWNLOADABLE_MIMETYPES:
|
|
entityActionNotPerformedWarning(entityValueList, Msg.FORMAT_NOT_DOWNLOADABLE, j, jcount)
|
|
continue
|
|
if revisionId:
|
|
callGAPI(drive.revisions(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS+[GAPI.REVISION_NOT_FOUND],
|
|
fileId=fileId, revisionId=revisionId, fields='id')
|
|
fileExtension = result.get('fullFileExtension')
|
|
googleDocExtensions = GOOGLEDOC_VALID_EXTENSIONS_MAP.get(mimeType)
|
|
if googleDocExtensions:
|
|
my_line = ['Type', 'Google Doc']
|
|
googleDoc = True
|
|
for exportFormat in exportFormats:
|
|
if exportFormat['ext'] in googleDocExtensions:
|
|
exportMimeType = exportFormat['mime']
|
|
if fileExtension:
|
|
extension = '.'+fileExtension
|
|
else:
|
|
extension = exportFormat['ext']
|
|
break
|
|
else:
|
|
entityActionNotPerformedWarning(entityValueList, Msg.FORMAT_NOT_AVAILABLE.format(','.join(exportFormatChoices)), j, jcount)
|
|
continue
|
|
else:
|
|
if 'quotaBytesUsed' in result:
|
|
my_line = ['Size', formatFileSize(int(result['quotaBytesUsed']))]
|
|
else:
|
|
my_line = ['Size', UNKNOWN]
|
|
googleDoc = False
|
|
if fileExtension:
|
|
extension = '.'+fileExtension
|
|
else:
|
|
extension = MIMETYPE_EXTENSION_MAP.get(mimeType, '')
|
|
while True:
|
|
if targetStdout:
|
|
filename = 'stdout'
|
|
else:
|
|
filename, _ = uniqueFilename(targetFolder, targetName or cleanFilename(result['name']), overwrite, extension)
|
|
spreadsheetUrl = None
|
|
f = None
|
|
try:
|
|
if googleDoc:
|
|
if (not exportSheetAsPDF and not sheetEntity) or mimeType != MIMETYPE_GA_SPREADSHEET:
|
|
request = drive.files().export_media(fileId=fileId, mimeType=exportMimeType)
|
|
if revisionId:
|
|
request.uri = f'{request.uri}&revision={revisionId}'
|
|
else:
|
|
spreadsheet = callGAPI(sheet.spreadsheets(), 'get',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=fileId, fields='spreadsheetUrl,sheets(properties(sheetId,title))')
|
|
# spreadsheetUrl = f'{re.sub("/edit.*$", "/export", spreadsheet["spreadsheetUrl"])}?exportFormat={exportFormatName}&format={exportFormatName}&id={fileId}'
|
|
spreadsheetUrl = f'{re.sub("/edit.*$", "/export", spreadsheet["spreadsheetUrl"])}?format={exportFormatName}&id={fileId}'
|
|
if sheetEntity:
|
|
entityValueList.extend([sheetEntity['sheetType'], sheetEntity['sheetValue']])
|
|
sheetId = getSheetIdFromSheetEntity(spreadsheet, sheetEntity)
|
|
if sheetId is None:
|
|
entityActionNotPerformedWarning(entityValueList, Msg.NOT_FOUND, j, jcount)
|
|
break
|
|
spreadsheetUrl += f'&gid={sheetId}'
|
|
spreadsheetUrl += exportSheetAsPDF
|
|
else:
|
|
if revisionId:
|
|
entityValueList.extend([Ent.DRIVE_FILE_REVISION, revisionId])
|
|
request = drive.revisions().get_media(fileId=fileId, revisionId=revisionId)
|
|
else:
|
|
request = drive.files().get_media(fileId=fileId, acknowledgeAbuse=acknowledgeAbuse)
|
|
if not targetStdout:
|
|
f = open(filename, 'wb')
|
|
else:
|
|
f = os.fdopen(os.dup(sys.stdout.fileno()), 'wb')
|
|
if not spreadsheetUrl:
|
|
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
|
|
done = False
|
|
while not done:
|
|
status, done = downloader.next_chunk()
|
|
if showProgress and not suppressStdoutMsgs and status.progress() < 1.0:
|
|
entityActionPerformedMessage(entityValueList, f'{status.progress():>7.2%}', j, jcount)
|
|
else:
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
sys.stderr.write(f'Debug: spreadsheetUrl: {spreadsheetUrl}\n')
|
|
maxRetries = 10
|
|
sleepTime = 5
|
|
for retry in range(1, maxRetries+1):
|
|
status, content = drive._http.request(uri=spreadsheetUrl, method='GET')
|
|
if status['status'] != '429':
|
|
break
|
|
writeStderr(Msg.RETRYING_GOOGLE_SHEET_EXPORT_SLEEPING.format(retry, maxRetries, sleepTime))
|
|
time.sleep(sleepTime)
|
|
if status['status'] == '200':
|
|
f.write(content)
|
|
if targetStdout and content[-1] != '\n':
|
|
f.write(bytes('\n', UTF8))
|
|
else:
|
|
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, filename, f'HTTP Error: {status["status"]}', j, jcount)
|
|
closeRemoveTargetFile(f)
|
|
break
|
|
if not targetStdout:
|
|
closeFile(f)
|
|
if not suppressStdoutMsgs:
|
|
entityModifierNewValueKeyValueActionPerformed(entityValueList, Act.MODIFIER_TO, filename, my_line[0], my_line[1], j, jcount)
|
|
break
|
|
except (IOError, httplib2.HttpLib2Error) as e:
|
|
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, filename, str(e), j, jcount)
|
|
except googleapiclient.http.HttpError as e:
|
|
mg = HTTP_ERROR_PATTERN.match(str(e))
|
|
if mg:
|
|
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, filename, mg.group(1), j, jcount)
|
|
else:
|
|
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, filename, str(e), j, jcount)
|
|
closeRemoveTargetFile(f)
|
|
break
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], Msg.DOES_NOT_EXIST, j, jcount)
|
|
except GAPI.revisionNotFound:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId, Ent.DRIVE_FILE_REVISION, revisionId], Msg.DOES_NOT_EXIST, j, jcount)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, fileId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
SUGGESTIONS_VIEW_MODE_CHOICE_MAP = {
|
|
'default': 'DEFAULT_FOR_CURRENT_ACCESS',
|
|
'suggestionsinline': 'SUGGESTIONS_INLINE',
|
|
'previewsuggestionsaccepted': 'PREVIEW_SUGGESTIONS_ACCEPTED',
|
|
'previewwithoutsuggestions': 'PREVIEW_WITHOUT_SUGGESTIONS'
|
|
}
|
|
|
|
# gam <UserTypeEntity> get document <DriveFileEntity>
|
|
# [viewmode default|suggestions_inline|preview_suggestions_accepted|preview_without_suggestions]
|
|
# [targetfolder <FilePath>] [targetname <FileName>]
|
|
# [donotfollowshortcuts [<Boolean>]] [overwrite [<Boolean>]]
|
|
def getGoogleDocument(users):
|
|
fileIdEntity = getDriveFileEntity()
|
|
suggestionsViewMode = SUGGESTIONS_VIEW_MODE_CHOICE_MAP['default']
|
|
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
|
|
targetNamePattern = None
|
|
donotFollowShortcuts = overwrite = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'viewmode':
|
|
suggestionsViewMode = getChoice(SUGGESTIONS_VIEW_MODE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'targetfolder':
|
|
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
elif myarg == 'targetname':
|
|
targetNamePattern = getString(Cmd.OB_FILE_NAME)
|
|
elif myarg == 'donotfollowshortcuts':
|
|
donotFollowShortcuts = getBoolean()
|
|
elif myarg == 'overwrite':
|
|
overwrite = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DOCUMENT)
|
|
if jcount == 0:
|
|
continue
|
|
_, docs = buildGAPIServiceObject(API.DOCS, user, i, count)
|
|
if not docs:
|
|
continue
|
|
_, userName, _ = splitEmailAddressOrUID(user)
|
|
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
targetName = _substituteForUser(targetNamePattern, user, userName) if targetNamePattern else None
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='name,mimeType,shortcutDetails', supportsAllDrives=True)
|
|
mimeType = result['mimeType']
|
|
if (mimeType == MIMETYPE_GA_SHORTCUT) and not donotFollowShortcuts:
|
|
fileId = result['shortcutDetails']['targetId']
|
|
result = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='name,mimeType', supportsAllDrives=True)
|
|
mimeType = result['mimeType']
|
|
docName = result['name']
|
|
if mimeType != MIMETYPE_GA_DOCUMENT:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.DRIVE_FILE, docName],
|
|
Msg.INVALID_MIMETYPE.format(mimeType, MIMETYPE_GA_DOCUMENT), j, jcount)
|
|
continue
|
|
filename, _ = uniqueFilename(targetFolder, targetName or cleanFilename(docName), overwrite)
|
|
result = callGAPI(docs.documents(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
documentId=fileId, suggestionsViewMode=suggestionsViewMode)
|
|
if writeFile(filename, json.dumps(result, indent=2, sort_keys=True)+'\n', continueOnError=True):
|
|
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DOCUMENT, f'{docName}({fileId})'], Act.MODIFIER_TO, filename, j, jcount)
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DOCUMENT, fileId], Msg.DOES_NOT_EXIST, j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> update docuument <DriveFileEntity>
|
|
# ((json [charset <Charset>] <SpreadsheetJSONUpdateRequest>) |
|
|
# (json file <FileName> [charset <Charset>]))
|
|
# [formatjson]
|
|
def updateGoogleDocument(users):
|
|
fileIdEntity = getDriveFileEntity()
|
|
body = {}
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'json':
|
|
body = getJSON([])
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DOCUMENT if not FJQC.formatJSON else None)
|
|
if jcount == 0:
|
|
continue
|
|
_, docs = buildGAPIServiceObject(API.DOCS, user, i, count)
|
|
if not docs:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for documentId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(docs.documents(), 'batchUpdate',
|
|
throwReasons=GAPI.DOCS_ACCESS_THROW_REASONS,
|
|
documentId=documentId, body=body)
|
|
if FJQC.formatJSON:
|
|
printLine('{'+f'"User": "{user}", "documentId": "{documentId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
|
|
continue
|
|
entityActionPerformed([Ent.USER, user, Ent.DOCUMENT, documentId], j, jcount)
|
|
Ind.Increment()
|
|
for field in ['replies', 'writeControl']:
|
|
if field in result:
|
|
showJSON(field, result[field])
|
|
Ind.Decrement()
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DOCUMENT, documentId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> collect orphans
|
|
# [(targetuserfoldername <DriveFolderName>)(targetuserfolderid <DriveFolderID>)]
|
|
# [useshortcuts [<Boolean>]]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [preview] [todrive <ToDriveAttribute>*]
|
|
def collectOrphans(users):
|
|
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
|
|
csvPF = None
|
|
targetParms = initDriveFileAttributes()
|
|
targetUserFolderId = None
|
|
targetUserFolderPattern = '#user# orphaned files'
|
|
targetParentBody = {}
|
|
query = ME_IN_OWNERS_AND+'trashed = false'
|
|
useShortcuts = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg == 'targetuserfoldername':
|
|
targetUserFolderPattern = getString(Cmd.OB_DRIVE_FOLDER_NAME)
|
|
targetUserFolderId = None
|
|
elif myarg == 'targetuserfolderid':
|
|
targetUserFolderId = getString(Cmd.OB_DRIVE_FOLDER_ID)
|
|
targetUserFolderPattern = None
|
|
elif myarg == 'useshortcuts':
|
|
useShortcuts = getBoolean()
|
|
elif myarg == 'preview':
|
|
csvPF = CSVPrintFile(['Owner', 'type', 'id', 'name', 'action'])
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
userName, _ = splitEmailAddress(user)
|
|
try:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, Ent.TypeName(Ent.USER, user), i, count, query=query)
|
|
feed = callGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=query, orderBy=OBY.orderBy,
|
|
fields='nextPageToken,files(id,name,parents,mimeType,sharedWithMeTime,capabilities(canMoveItemWithinDrive))',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
if targetUserFolderPattern:
|
|
trgtUserFolderName = _substituteForUser(targetUserFolderPattern, user, userName)
|
|
targetParms[DFA_PARENTQUERY] = MY_NON_TRASHED_FOLDER_NAME.format(escapeDriveFileName(trgtUserFolderName))
|
|
else:
|
|
targetParms[DFA_PARENTID] = targetUserFolderId
|
|
trgtUserFolderName = targetUserFolderId
|
|
if not _getDriveFileParentInfo(drive, user, i, count, targetParentBody, targetParms, True, False):
|
|
continue
|
|
orphanDriveFiles = []
|
|
for fileEntry in feed:
|
|
if not fileEntry.get('parents') and 'sharedWithMeTime' not in fileEntry:
|
|
orphanDriveFiles.append(fileEntry)
|
|
jcount = len(orphanDriveFiles)
|
|
entityPerformActionNumItemsModifier([Ent.USER, user], jcount, Ent.DRIVE_ORPHAN_FILE_OR_FOLDER,
|
|
f'{Act.MODIFIER_INTO} {Ent.Singular(Ent.DRIVE_FOLDER)}: {trgtUserFolderName}', i, count)
|
|
if jcount == 0:
|
|
continue
|
|
if not csvPF:
|
|
if 'parents' not in targetParentBody or not targetParentBody['parents']:
|
|
try:
|
|
newParentId = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.UNKNOWN_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
|
|
GAPI. TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
|
|
body={'name': trgtUserFolderName, 'mimeType': MIMETYPE_GA_FOLDER}, fields='id')['id']
|
|
except (GAPI.forbidden, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, trgtUserFolderName], str(e), i, count)
|
|
continue
|
|
else:
|
|
newParentId = targetParentBody['parents'][0]
|
|
setSysExitRC(ORPHANS_COLLECTED_RC)
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileEntry in orphanDriveFiles:
|
|
j += 1
|
|
fileId = fileEntry['id']
|
|
fileName = fileEntry['name']
|
|
fileType = _getEntityMimeType(fileEntry)
|
|
# Deleted 6.26.16
|
|
# if fileType == Ent.DRIVE_FOLDER and not fileEntry['capabilities']['canAddMyDriveParent']:
|
|
# # Typically Google Backup & Sync images of laptops
|
|
# continue
|
|
if not useShortcuts and fileEntry['capabilities']['canMoveItemWithinDrive']:
|
|
if csvPF:
|
|
csvPF.WriteRow({'Owner': user, 'type': Ent.Singular(fileType), 'id': fileId, 'name': fileName, 'action': 'changeParent'})
|
|
continue
|
|
try:
|
|
callGAPI(drive.files(), 'update',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND,
|
|
GAPI.INTERNAL_ERROR, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
|
|
retryReasons=[GAPI.FILE_NOT_FOUND],
|
|
fileId=fileId, body={}, addParents=newParentId, fields='')
|
|
entityModifierNewValueItemValueListActionPerformed([Ent.USER, user, fileType, fileName],
|
|
Act.MODIFIER_INTO, None, [Ent.DRIVE_FOLDER, trgtUserFolderName], j, jcount)
|
|
except (GAPI.badRequest, GAPI.fileNotFound, GAPI.internalError, GAPI.insufficientParentPermissions,) as e:
|
|
entityActionFailedWarning([Ent.USER, user, fileType, fileName], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
else:
|
|
if csvPF:
|
|
csvPF.WriteRow({'Owner': user, 'type': Ent.Singular(fileType), 'id': fileId, 'name': fileName, 'action': 'createShortcut'})
|
|
continue
|
|
try:
|
|
# Check for existing shortcut, do not duplicate
|
|
files = callGAPIitems(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID],
|
|
q=f"'me' in owners and name = '{escapeDriveFileName(fileName)}' and mimeType = '{MIMETYPE_GA_SHORTCUT}' and '{newParentId}' in parents and trashed = false",
|
|
fields='files(id,shortcutDetails(targetId))')
|
|
existingShortcut = False
|
|
for f_file in files:
|
|
if f_file['shortcutDetails']['targetId'] == fileId:
|
|
entityActionNotPerformedWarning([Ent.USER, user, fileType, fileName, Ent.DRIVE_FILE_SHORTCUT, f"{fileName}({f_file['id']})"],
|
|
Msg.ALREADY_EXISTS_IN_TARGET_FOLDER.format(Ent.Singular(Ent.DRIVE_FOLDER), trgtUserFolderName), j, jcount)
|
|
existingShortcut = True
|
|
break
|
|
if existingShortcut:
|
|
continue
|
|
body = {'name': fileName, 'mimeType': MIMETYPE_GA_SHORTCUT, 'parents': [newParentId], 'shortcutDetails': {'targetId': fileId}}
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
|
|
GAPI.SHORTCUT_TARGET_INVALID],
|
|
body=body, fields='id,name', supportsAllDrives=True)
|
|
entityModifierNewValueItemValueListActionPerformed([Ent.USER, user, fileType, fileName, Ent.DRIVE_FILE_SHORTCUT, f'{result["name"]}({result["id"]})'],
|
|
Act.MODIFIER_INTO, None, [Ent.DRIVE_FOLDER, trgtUserFolderName], j, jcount)
|
|
|
|
except GAPI.invalidQuery:
|
|
entityActionFailedWarning([Ent.USER, user, fileType, fileName], invalidQuery(query), j, jcount)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep,
|
|
GAPI.shortcutTargetInvalid) as e:
|
|
entityActionFailedWarning([Ent.USER, user, fileType, fileName, Ent.DRIVE_FILE_SHORTCUT, body['name']], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Orphans to Collect')
|
|
|
|
TRANSFER_DRIVEFILE_ACL_ROLES_MAP = {
|
|
'commenter': 'commenter',
|
|
'contentmanager': 'fileOrganizer',
|
|
'contributor': 'writer',
|
|
'editor': 'writer',
|
|
'fileorganizer': 'fileOrganizer',
|
|
'fileorganiser': 'fileOrganizer',
|
|
'manager': 'organizer',
|
|
'organizer': 'organizer',
|
|
'organiser': 'organizer',
|
|
'owner': 'organizer',
|
|
'read': 'reader',
|
|
'reader': 'reader',
|
|
'viewer': 'reader',
|
|
'writer': 'writer',
|
|
'current': 'current',
|
|
'none': 'none',
|
|
'source': 'source',
|
|
}
|
|
|
|
# gam <UserTypeEntity> transfer drive <UserItem> [select <DriveFileEntity>]
|
|
# [(targetfolderid <DriveFolderID>)|(targetfoldername <DriveFolderName>)]
|
|
# [targetuserfoldername <DriveFolderName>] [targetuserorphansfoldername <DriveFolderName>]
|
|
# [mergewithtarget [<Boolean>]]
|
|
# [createshortcutsfornonmovablefiles [<Boolean>]]
|
|
# [skipids <DriveFileEntity>]
|
|
# [keepuser | (retainrole reader|commenter|writer|editor|fileorganizer|none)] [noretentionmessages]
|
|
# [nonowner_retainrole reader|commenter|writer|editor|fileorganizer|current|none]
|
|
# [nonowner_targetrole reader|commenter|writer|editor|fileorganizer|current|none|source]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [enforceexpansiveaccess [<Boolean>]]
|
|
# [preview] [todrive <ToDriveAttribute>*]
|
|
def transferDrive(users):
|
|
|
|
def _getOwnerUser(childEntryInfo):
|
|
if 'owners' not in childEntryInfo or not childEntryInfo['owners']:
|
|
return (UNKNOWN, None)
|
|
ownerUser = childEntryInfo['owners'][0]['emailAddress']
|
|
if ownerUser not in thirdPartyOwners:
|
|
_, ownerDrive = buildGAPIServiceObject(API.DRIVE3, ownerUser, displayError=False)
|
|
thirdPartyOwners[ownerUser] = ownerDrive
|
|
else:
|
|
ownerDrive = thirdPartyOwners[ownerUser]
|
|
return (ownerUser, ownerDrive)
|
|
|
|
TARGET_PARENT_ID = 0
|
|
TARGET_ORPHANS_PARENT_ID = 1
|
|
|
|
def _buildTargetFile(folderName, folderParentId):
|
|
try:
|
|
op = 'Find Target Folder'
|
|
result = callGAPIpages(targetDrive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.BAD_REQUEST],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy,
|
|
q=MY_NON_TRASHED_FOLDER_NAME_WITH_PARENTS.format(escapeDriveFileName(folderName), folderParentId),
|
|
fields='nextPageToken,files(id)')
|
|
if result:
|
|
return result[0]['id']
|
|
op = 'Create Target Folder'
|
|
return callGAPI(targetDrive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.UNKNOWN_ERROR, GAPI.BAD_REQUEST, GAPI.STORAGE_QUOTA_EXCEEDED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
|
|
body={'parents': [folderParentId], 'name': folderName, 'mimeType': MIMETYPE_GA_FOLDER}, fields='id')['id']
|
|
except (GAPI.forbidden, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions, GAPI.unknownError, GAPI.badRequest,
|
|
GAPI.storageQuotaExceeded, GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
|
|
entityActionFailedWarning([Ent.USER, targetUser, Ent.DRIVE_FOLDER, folderName], f'{op}: {str(e)}')
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(targetUser, str(e))
|
|
return None
|
|
|
|
def _buildTargetUserFolder():
|
|
folderName = _substituteForUser(targetUserFolderPattern, sourceUser, sourceUserName)
|
|
if not folderName:
|
|
return targetFolderId
|
|
return _buildTargetFile(folderName, targetFolderId)
|
|
|
|
def _buildTargetUserOrphansFolder():
|
|
folderName = _substituteForUser(targetUserOrphansFolderPattern, sourceUser, sourceUserName)
|
|
if folderName:
|
|
targetIds[TARGET_ORPHANS_PARENT_ID] = _buildTargetFile(folderName, targetIds[TARGET_PARENT_ID])
|
|
if targetIds[TARGET_ORPHANS_PARENT_ID] is None:
|
|
targetIds[TARGET_ORPHANS_PARENT_ID] = targetIds[TARGET_PARENT_ID]
|
|
else:
|
|
targetIds[TARGET_ORPHANS_PARENT_ID] = targetIds[TARGET_PARENT_ID]
|
|
|
|
def _getMappedParentForRootParentOrOrphan(childEntryInfo, atSelectTop):
|
|
if 'parents' not in childEntryInfo or not childEntryInfo['parents']:
|
|
return (None, targetIds[TARGET_ORPHANS_PARENT_ID])
|
|
if atSelectTop:
|
|
return (childEntryInfo['parents'], targetIds[TARGET_PARENT_ID])
|
|
for parentId in childEntryInfo['parents']:
|
|
if parentId == sourceRootId:
|
|
return ([sourceRootId], targetIds[TARGET_PARENT_ID])
|
|
return (None, None)
|
|
|
|
def _setUpdateRole(permission):
|
|
return {'role': permission['role']}
|
|
|
|
def _makeXferShortcut(drive, user, j, jcount, entityType, childId, childName, newParentId):
|
|
kvList = [Ent.USER, user, entityType, f'{childName}({childId})']
|
|
targetEntityType = Ent.DRIVE_FILE_SHORTCUT if entityType == Ent.DRIVE_FILE else Ent.DRIVE_FOLDER_SHORTCUT
|
|
action = Act.Get()
|
|
existingShortcut = _checkForExistingShortcut(drive, childId, childName, newParentId)
|
|
if existingShortcut:
|
|
Act.Set(Act.CREATE_SHORTCUT)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_PREVIOUSLY_IN,
|
|
[Ent.DRIVE_FOLDER, newParentId, targetEntityType, f"{childName}({existingShortcut})"],
|
|
j, jcount)
|
|
Act.Set(action)
|
|
return
|
|
body = {'name': childName, 'mimeType': MIMETYPE_GA_SHORTCUT,
|
|
'parents': [newParentId], 'shortcutDetails': {'targetId': childId}}
|
|
try:
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID],
|
|
body=body, fields='id', supportsAllDrives=True)
|
|
Act.Set(Act.CREATE_SHORTCUT)
|
|
entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_IN,
|
|
[Ent.DRIVE_FOLDER, newParentId, targetEntityType, f"{childName}({result['id']})"],
|
|
j, jcount)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e:
|
|
entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), j, jcount)
|
|
Act.Set(action)
|
|
|
|
# Recreate source user shortcut in target user
|
|
def _transferShortcut(j, jcount, childEntryInfo, childId, childName, newParentId):
|
|
entityType = Ent.DRIVE_FOLDER_SHORTCUT if childEntryInfo['shortcutDetails']['targetMimeType'] == MIMETYPE_GA_FOLDER else Ent.DRIVE_FILE_SHORTCUT
|
|
kvList = [Ent.USER, sourceUser, entityType, f'{childName}({childId})']
|
|
action = Act.Get()
|
|
body = {'name': childName, 'mimeType': MIMETYPE_GA_SHORTCUT,
|
|
'parents': [newParentId], 'shortcutDetails': {'targetId': childEntryInfo['shortcutDetails']['targetId']}}
|
|
Act.Set(Act.RECREATE)
|
|
try:
|
|
result = callGAPI(targetDrive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID],
|
|
body=body, fields='id', supportsAllDrives=True)
|
|
shortcutId = result['id']
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_IN, None, [Ent.USER, targetUser,
|
|
Ent.DRIVE_FOLDER, newParentId, entityType, f"{shortcutId})"],
|
|
j, jcount)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e:
|
|
entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), j, jcount)
|
|
Act.Set(action)
|
|
return
|
|
if ownerRetainRoleBody['role'] == 'none':
|
|
Act.Set(Act.DELETE_SHORTCUT)
|
|
kvList = [Ent.USER, sourceUser, entityType, f'{childName}({childId})']
|
|
try:
|
|
callGAPI(sourceDrive.files(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
fileId=childId, supportsAllDrives=True)
|
|
entityActionPerformed(kvList, j, jcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.fileNeverWritable) as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
Act.Set(action)
|
|
|
|
def _transferFile(childEntry, i, count, j, jcount, atSelectTop):
|
|
childEntryInfo = childEntry['info']
|
|
childFileId = childEntryInfo['id']
|
|
childFileName = childEntryInfo['name']
|
|
childFileType = _getEntityMimeType(childEntryInfo)
|
|
# Owned files
|
|
if childEntryInfo['ownedByMe']:
|
|
childEntryInfo['sourcePermission'] = {'role': 'owner'}
|
|
for permission in childEntryInfo.get('permissions', []):
|
|
if targetPermissionId == permission['id']:
|
|
childEntryInfo['targetPermission'] = _setUpdateRole(permission)
|
|
updateTargetPermission = True
|
|
break
|
|
else:
|
|
childEntryInfo['targetPermission'] = {'role': 'none'}
|
|
updateTargetPermission = False
|
|
if csvPF:
|
|
csvPF.WriteRow({'OldOwner': sourceUser, 'NewOwner': targetUser, 'type': Ent.Singular(childFileType), 'id': childFileId, 'name': childFileName, 'role': 'owner'})
|
|
return
|
|
Act.Set(Act.TRANSFER_OWNERSHIP)
|
|
addTargetParent = None
|
|
removeSourceParents = set()
|
|
removeTargetParents = set()
|
|
childParents = childEntryInfo.get('parents', [])
|
|
if childParents:
|
|
for parentId in childParents:
|
|
if parentId in parentIdMap:
|
|
addTargetParent = parentIdMap[parentId]
|
|
if parentId != sourceRootId:
|
|
removeSourceParents.add(parentId)
|
|
elif not mergeWithTarget and targetFolderId != targetRootId:
|
|
removeTargetParents.add(targetRootId)
|
|
else:
|
|
if targetIds[TARGET_ORPHANS_PARENT_ID] is None:
|
|
_buildTargetUserOrphansFolder()
|
|
addTargetParent = targetIds[TARGET_ORPHANS_PARENT_ID]
|
|
removeTargetParents.add(targetRootId)
|
|
try:
|
|
actionUser = sourceUser
|
|
if childEntryInfo['mimeType'] != MIMETYPE_GA_SHORTCUT:
|
|
if not updateTargetPermission:
|
|
op = 'Create Source ACL'
|
|
callGAPI(sourceDrive.permissions(), 'create',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INVALID_SHARING_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
|
|
fileId=childFileId, sendNotificationEmail=False, body=targetWriterPermissionsBody, fields='')
|
|
op = 'Update Source ACL'
|
|
callGAPI(sourceDrive.permissions(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.INVALID_OWNERSHIP_TRANSFER,
|
|
GAPI.PERMISSION_NOT_FOUND, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
|
|
enforceExpansiveAccess=enforceExpansiveAccess,
|
|
fileId=childFileId, permissionId=targetPermissionId,
|
|
transferOwnership=True, body={'role': 'owner'}, fields='')
|
|
if removeSourceParents:
|
|
op = 'Remove Source Parents'
|
|
callGAPI(sourceDrive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS, retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3,
|
|
fileId=childFileId, removeParents=','.join(removeSourceParents), fields='')
|
|
actionUser = targetUser
|
|
if addTargetParent or removeTargetParents:
|
|
op = 'Add/Remove Target Parents'
|
|
callGAPI(targetDrive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
|
|
retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3,
|
|
fileId=childFileId,
|
|
addParents=addTargetParent, removeParents=','.join(removeTargetParents), fields='')
|
|
entityModifierNewValueItemValueListActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount)
|
|
else:
|
|
if topSourceId in childParents:
|
|
_transferShortcut(j, jcount, childEntryInfo, childFileId, childFileName, addTargetParent)
|
|
else:
|
|
entityModifierNewValueItemValueListActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.sharingRateLimitExceeded, GAPI.cannotAddParent, GAPI.insufficientParentPermissions) as e:
|
|
entityActionFailedWarning([Ent.USER, actionUser, childFileType, childFileName], f'{op}: {str(e)}', j, jcount)
|
|
except (GAPI.insufficientFilePermissions, GAPI.fileOwnerNotMemberOfWriterDomain, GAPI.crossDomainMoveRestriction) as e:
|
|
if not createShortcutsForNonmovableFiles:
|
|
entityActionFailedWarning([Ent.USER, actionUser, childFileType, childFileName], f'{op}: {str(e)}', j, jcount)
|
|
else:
|
|
_makeXferShortcut(targetDrive, targetUser, j, jcount, childFileType, childFileId, childFileName, addTargetParent)
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, actionUser, childFileType, childFileName, Ent.PERMISSION_ID, targetPermissionId], j, jcount)
|
|
except GAPI.invalidSharingRequest as e:
|
|
entityActionFailedWarning([Ent.USER, actionUser, childFileType, childFileName], Ent.TypeNameMessage(Ent.PERMISSION_ID, targetPermissionId, str(e)), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(actionUser, str(e), i, count)
|
|
# Non-owned files
|
|
else:
|
|
Act.Set(Act.PROCESS)
|
|
for permission in childEntryInfo.get('permissions', []):
|
|
if sourcePermissionId == permission['id']:
|
|
childEntryInfo['sourcePermission'] = _setUpdateRole(permission)
|
|
getSourcePermissionFromOwner = False
|
|
break
|
|
else:
|
|
childEntryInfo['sourcePermission'] = nonOwnerRetainRoleBody
|
|
getSourcePermissionFromOwner = True
|
|
for permission in childEntryInfo.get('permissions', []):
|
|
if targetPermissionId == permission['id']:
|
|
childEntryInfo['targetPermission'] = _setUpdateRole(permission)
|
|
getTargetPermissionFromOwner = False
|
|
break
|
|
else:
|
|
childEntryInfo['targetPermission'] = {'role': 'none'}
|
|
getTargetPermissionFromOwner = True
|
|
ownerUser, ownerDrive = _getOwnerUser(childEntryInfo)
|
|
if not ownerDrive:
|
|
entityActionNotPerformedWarning([Ent.USER, sourceUser, childFileType, childFileName],
|
|
Msg.SERVICE_NOT_APPLICABLE_THIS_ADDRESS.format(ownerUser), j, jcount)
|
|
return
|
|
if getSourcePermissionFromOwner or getTargetPermissionFromOwner:
|
|
try:
|
|
permissions = callGAPIpages(ownerDrive.permissions(), 'list', 'permissions',
|
|
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS+[GAPI.BAD_REQUEST],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
fileId=childFileId, fields='nextPageToken,permissions')
|
|
if getSourcePermissionFromOwner:
|
|
for permission in permissions:
|
|
if sourcePermissionId == permission['id']:
|
|
childEntryInfo['sourcePermission'] = _setUpdateRole(permission)
|
|
break
|
|
else:
|
|
childEntryInfo['sourcePermission'] = nonOwnerRetainRoleBody
|
|
if getTargetPermissionFromOwner:
|
|
for permission in permissions:
|
|
if targetPermissionId == permission['id']:
|
|
childEntryInfo['targetPermission'] = _setUpdateRole(permission)
|
|
break
|
|
else:
|
|
childEntryInfo['targetPermission'] = {'role': 'none'}
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
|
|
GAPI.unknownError, GAPI.invalid, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], str(e), j, jcount)
|
|
return
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(ownerUser, str(e), i, count)
|
|
return
|
|
if csvPF:
|
|
csvPF.WriteRow({'OldOwner': sourceUser, 'NewOwner': targetUser, 'type': Ent.Singular(childFileType),
|
|
'id': childFileId, 'name': childFileName, 'role': childEntryInfo['sourcePermission']['role']})
|
|
return
|
|
if (childFileType == Ent.DRIVE_FOLDER) and (childEntryInfo['targetPermission']['role'] == 'none') and (ownerRetainRoleBody['role'] == 'none'):
|
|
if targetIds[TARGET_ORPHANS_PARENT_ID] is None:
|
|
_buildTargetUserOrphansFolder()
|
|
parentIdMap[childFileId] = _buildTargetFile(childFileName, targetIds[TARGET_ORPHANS_PARENT_ID])
|
|
entityActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], j, jcount)
|
|
return
|
|
existingParentIds, mappedParentId = _getMappedParentForRootParentOrOrphan(childEntryInfo, atSelectTop)
|
|
if mappedParentId is not None:
|
|
# Give temporary writer access to target user so other actions can be performed
|
|
if childEntryInfo['targetPermission']['role'] in {'none', 'reader'}:
|
|
try:
|
|
callGAPI(ownerDrive.permissions(), 'create',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INVALID_SHARING_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
|
|
fileId=childFileId, sendNotificationEmail=False, body=targetWriterPermissionsBody, fields='')
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.sharingRateLimitExceeded) as e:
|
|
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], str(e), j, jcount)
|
|
return
|
|
except GAPI.invalidSharingRequest as e:
|
|
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName],
|
|
Ent.TypeNameMessage(Ent.PERMISSION_ID, sourcePermissionId, str(e)), j, jcount)
|
|
return
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(ownerUser, str(e), i, count)
|
|
return
|
|
if existingParentIds is not None:
|
|
# We have to make a shortcut to a non-owned non-orphan as we can't change the parents
|
|
try:
|
|
body = {'name': childFileName, 'mimeType': MIMETYPE_GA_SHORTCUT, 'parents': [mappedParentId], 'shortcutDetails': {'targetId': childFileId}}
|
|
callGAPI(targetDrive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
|
|
body=body, fields='', supportsAllDrives=True)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
|
|
entityActionFailedWarning([Ent.USER, targetUser, childFileType, childFileName, Ent.DRIVE_FILE_SHORTCUT, body['name']], str(e), j, jcount)
|
|
return
|
|
# Delete existing parents
|
|
try:
|
|
callGAPI(sourceDrive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST],
|
|
retryReasons=[GAPI.FILE_NOT_FOUND], triesLimit=3,
|
|
fileId=childFileId,
|
|
removeParents=','.join(existingParentIds), body={}, fields='')
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.USER, sourceUser, childFileType, childFileName], str(e), j, jcount)
|
|
return
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
|
|
return
|
|
# 5.31.03 - Non-owned files without parents are SharedWithMe, parents can not be changed
|
|
# else:
|
|
## We can add a parent when transferring an orphan
|
|
# try:
|
|
# callGAPI(targetDrive.files(), 'update',
|
|
# throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
|
|
# retryReasons=[GAPI.FILE_NOT_FOUND], triesLimit=3,
|
|
# fileId=childFileId,
|
|
# addParents=mappedParentId, body={}, fields='')
|
|
# except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.unknownError,
|
|
# GAPI.badRequest, GAPI.cannotAddParent) as e:
|
|
# entityActionFailedWarning([Ent.USER, targetUser, childFileType, childFileName], str(e), j, jcount)
|
|
# return
|
|
# except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
# userDriveServiceNotEnabledWarning(targetUser, str(e), i, count)
|
|
# return
|
|
entityActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], j, jcount)
|
|
|
|
def _manageRoleRetention(childEntry, i, count, j, jcount, atSelectTop):
|
|
def _setTargetInsertBody(permission):
|
|
return {'role': permission['role'], 'type': 'user', 'emailAddress': targetUser}
|
|
|
|
def _checkForDiminishedTargetRole(currentPermission, newPermission):
|
|
if currentPermission['role'] in {'owner', 'organizer', 'fileOrganizer', 'writer'}:
|
|
return False
|
|
if (currentPermission['role'] == 'commenter') and (newPermission['role'] == 'reader'):
|
|
return False
|
|
return True
|
|
|
|
childEntryInfo = childEntry['info']
|
|
childFileId = childEntryInfo['id']
|
|
childFileName = childEntryInfo['name']
|
|
childFileType = _getEntityMimeType(childEntryInfo)
|
|
if childEntryInfo['mimeType'] == MIMETYPE_GA_SHORTCUT:
|
|
if showRetentionMessages:
|
|
entityActionNotPerformedWarning([Ent.USER, sourceUser, childFileType, childFileName, Ent.ROLE, ownerRetainRoleBody['role']], Msg.NOT_APPROPRIATE, j, jcount)
|
|
return
|
|
if childEntryInfo['ownedByMe']:
|
|
try:
|
|
if ownerRetainRoleBody['role'] != 'none':
|
|
if ownerRetainRoleBody['role'] != 'writer':
|
|
callGAPI(targetDrive.permissions(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
|
|
enforceExpansiveAccess=enforceExpansiveAccess,
|
|
fileId=childFileId, permissionId=sourcePermissionId, body=ownerRetainRoleBody, fields='')
|
|
else:
|
|
callGAPI(targetDrive.permissions(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED, GAPI.CANNOT_REMOVE_OWNER],
|
|
fileId=childFileId, permissionId=sourcePermissionId)
|
|
if showRetentionMessages:
|
|
entityActionPerformed([Ent.USER, sourceUser, childFileType, childFileName, Ent.ROLE, ownerRetainRoleBody['role']], j, jcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.sharingRateLimitExceeded, GAPI.cannotRemoveOwner, GAPI.cannotDeletePermission) as e:
|
|
entityActionFailedWarning([Ent.USER, sourceUser, childFileType, childFileName], str(e), j, jcount)
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, sourceUser, childFileType, childFileName, Ent.PERMISSION_ID, sourcePermissionId], j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
|
|
else:
|
|
ownerUser, ownerDrive = _getOwnerUser(childEntryInfo)
|
|
if not ownerDrive:
|
|
return
|
|
if nonOwnerRetainRoleBody['role'] == 'current':
|
|
sourceUpdateRole = childEntryInfo['sourcePermission']
|
|
else:
|
|
sourceUpdateRole = nonOwnerRetainRoleBody
|
|
if 'targetPermission' not in childEntryInfo:
|
|
childEntryInfo['targetPermission'] = {'role': 'current'}
|
|
errorTargetRole = True
|
|
else:
|
|
errorTargetRole = False
|
|
if nonOwnerTargetRoleBody['role'] == 'current':
|
|
targetInsertBody = _setTargetInsertBody(childEntryInfo['targetPermission'])
|
|
resetTargetRole = False
|
|
elif nonOwnerTargetRoleBody['role'] == 'source':
|
|
targetInsertBody = _setTargetInsertBody(childEntryInfo['sourcePermission'])
|
|
resetTargetRole = True
|
|
else:
|
|
targetInsertBody = _setTargetInsertBody(nonOwnerTargetRoleBody)
|
|
resetTargetRole = True
|
|
if not errorTargetRole:
|
|
if resetTargetRole:
|
|
resetTargetRole = _checkForDiminishedTargetRole(childEntryInfo['targetPermission'], targetInsertBody)
|
|
else:
|
|
_, mappedParentId = _getMappedParentForRootParentOrOrphan(childEntryInfo, atSelectTop)
|
|
if mappedParentId is not None and childEntryInfo['targetPermission']['role'] in {'none', 'reader'}:
|
|
resetTargetRole = True
|
|
# Update owner permissions
|
|
try:
|
|
if nonOwnerRetainRoleBody['role'] != 'none':
|
|
if nonOwnerRetainRoleBody['role'] != 'current':
|
|
callGAPI(ownerDrive.permissions(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
|
|
enforceExpansiveAccess=enforceExpansiveAccess,
|
|
fileId=childFileId, permissionId=sourcePermissionId, body=sourceUpdateRole, fields='')
|
|
else:
|
|
try:
|
|
callGAPI(ownerDrive.permissions(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED, GAPI.CANNOT_REMOVE_OWNER],
|
|
fileId=childFileId, permissionId=sourcePermissionId)
|
|
except GAPI.permissionNotFound:
|
|
pass
|
|
if showRetentionMessages:
|
|
entityActionPerformed([Ent.USER, sourceUser, childFileType, childFileName, Ent.ROLE, sourceUpdateRole['role']], j, jcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.sharingRateLimitExceeded, GAPI.cannotRemoveOwner, GAPI.cannotDeletePermission) as e:
|
|
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], str(e), j, jcount)
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, ownerUser, childFileType, childFileName, Ent.PERMISSION_ID, sourcePermissionId], j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(ownerUser, str(e), i, count)
|
|
# Update target permissions
|
|
if resetTargetRole and targetUser != ownerUser:
|
|
try:
|
|
if nonOwnerTargetRoleBody['role'] != 'none':
|
|
if nonOwnerTargetRoleBody['role'] != 'current' and targetInsertBody['role'] not in {'current', 'none'}:
|
|
callGAPI(ownerDrive.permissions(), 'create',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INVALID_SHARING_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
|
|
fileId=childFileId, sendNotificationEmail=False, body=targetInsertBody, fields='')
|
|
else:
|
|
try:
|
|
callGAPI(ownerDrive.permissions(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.SHARING_RATE_LIMIT_EXCEEDED],
|
|
fileId=childFileId, permissionId=targetPermissionId)
|
|
except GAPI.permissionNotFound:
|
|
pass
|
|
if showRetentionMessages:
|
|
entityActionPerformed([Ent.USER, targetUser, childFileType, childFileName, Ent.ROLE, targetInsertBody['role']], j, jcount)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.sharingRateLimitExceeded, GAPI.cannotDeletePermission) as e:
|
|
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], str(e), j, jcount)
|
|
except GAPI.invalidSharingRequest as e:
|
|
entityActionFailedWarning([Ent.USER, ownerUser, childFileType, childFileName], Ent.TypeNameMessage(Ent.PERMISSION_ID, targetPermissionId, str(e)), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
|
|
elif showRetentionMessages:
|
|
entityActionPerformed([Ent.USER, targetUser, childFileType, childFileName, Ent.ROLE, childEntryInfo['targetPermission']['role']], j, jcount)
|
|
|
|
def _transferDriveFilesFromTree(fileEntry, i, count):
|
|
jcount = len(fileEntry['children'])
|
|
if jcount == 0:
|
|
return
|
|
j = 0
|
|
for childFileId in fileEntry['children']:
|
|
j += 1
|
|
childEntry = fileTree.get(childFileId)
|
|
if not childEntry or childFileId in filesTransferred:
|
|
continue
|
|
if childFileId in skipFileIdEntity['list']:
|
|
entityActionNotPerformedWarning([Ent.USER, sourceUser, _getEntityMimeType(childEntry['info']), f'{childEntry["info"]["name"]} ({childFileId})'],
|
|
Msg.IN_SKIPIDS, j, jcount)
|
|
continue
|
|
filesTransferred.add(childFileId)
|
|
_transferFile(childEntry, i, count, j, jcount, False)
|
|
if childEntry['info']['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
Ind.Increment()
|
|
_transferDriveFilesFromTree(childEntry, i, count)
|
|
Ind.Decrement()
|
|
|
|
def _manageRoleRetentionDriveFilesFromTree(fileEntry, i, count):
|
|
jcount = len(fileEntry['children'])
|
|
if jcount == 0:
|
|
return
|
|
j = 0
|
|
for childFileId in fileEntry['children']:
|
|
j += 1
|
|
childEntry = fileTree.get(childFileId)
|
|
if not childEntry or childFileId in filesTransferred or childFileId in skipFileIdEntity['list']:
|
|
continue
|
|
filesTransferred.add(childFileId)
|
|
_manageRoleRetention(childEntry, i, count, j, jcount, False)
|
|
if childEntry['info']['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
Ind.Increment()
|
|
_manageRoleRetentionDriveFilesFromTree(childEntry, i, count)
|
|
Ind.Decrement()
|
|
|
|
def _identifyDriveFileAndChildren(fileEntry, i, count):
|
|
fileId = fileEntry['id']
|
|
if fileId not in fileTree:
|
|
fileTree[fileId] = {'info': fileEntry, 'children': []}
|
|
if fileEntry['mimeType'] != MIMETYPE_GA_FOLDER:
|
|
return
|
|
try:
|
|
children = callGAPIpages(sourceDrive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy, q=WITH_PARENTS.format(fileId),
|
|
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId),permissions(id,role),shortcutDetails)',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
|
|
return
|
|
for childEntry in children:
|
|
if not childEntry['trashed']:
|
|
childId = childEntry['id']
|
|
if childId in skipFileIdEntity['list']:
|
|
entityActionNotPerformedWarning([Ent.USER, sourceUser, _getEntityMimeType(childEntry), f'{childEntry["name"]} ({childId})'],
|
|
Msg.IN_SKIPIDS)
|
|
continue
|
|
fileTree[fileId]['children'].append(childId)
|
|
_identifyDriveFileAndChildren(childEntry, i, count)
|
|
|
|
def _transferDriveFileAndChildren(fileEntry, i, count, j, jcount, atSelectTop):
|
|
fileId = fileEntry['info']['id']
|
|
if fileId in filesTransferred:
|
|
return
|
|
if fileEntry['info']['name'] != MY_DRIVE:
|
|
filesTransferred.add(fileId)
|
|
if not atSelectTop or not mergeWithTarget:
|
|
_transferFile(fileEntry, i, count, j, jcount, atSelectTop)
|
|
kcount = len(fileEntry['children'])
|
|
if kcount == 0:
|
|
return
|
|
k = 0
|
|
for childFileId in fileEntry['children']:
|
|
k += 1
|
|
childEntry = fileTree.get(childFileId)
|
|
if childEntry:
|
|
Ind.Increment()
|
|
_transferDriveFileAndChildren(childEntry, i, count, k, kcount, False)
|
|
Ind.Decrement()
|
|
|
|
def _manageRoleRetentionDriveFileAndChildren(fileEntry, i, count, j, jcount, atSelectTop):
|
|
fileId = fileEntry['info']['id']
|
|
if fileId in filesTransferred:
|
|
return
|
|
if fileEntry['info']['name'] != MY_DRIVE:
|
|
filesTransferred.add(fileId)
|
|
if not atSelectTop or not mergeWithTarget:
|
|
_manageRoleRetention(fileEntry, i, count, j, jcount, atSelectTop)
|
|
kcount = len(fileEntry['children'])
|
|
if kcount == 0:
|
|
return
|
|
k = 0
|
|
for childFileId in fileEntry['children']:
|
|
k += 1
|
|
childEntry = fileTree.get(childFileId)
|
|
if childEntry:
|
|
Ind.Increment()
|
|
_manageRoleRetentionDriveFileAndChildren(childEntry, i, count, k, kcount, False)
|
|
Ind.Decrement()
|
|
|
|
targetUser = getEmailAddress()
|
|
buildTree = True
|
|
csvPF = None
|
|
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
|
|
ownerRetainRoleBody = {'role': 'none'}
|
|
nonOwnerRetainRoleBody = {}
|
|
nonOwnerTargetRoleBody = {'role': 'source'}
|
|
showRetentionMessages = True
|
|
targetFolderId = targetFolderName = None
|
|
targetUserFolderPattern = '#user# old files'
|
|
targetUserOrphansFolderPattern = '#user# orphaned files'
|
|
targetIds = [None, None]
|
|
createShortcutsForNonmovableFiles = enforceExpansiveAccess = False
|
|
mergeWithTarget = False
|
|
thirdPartyOwners = {}
|
|
skipFileIdEntity = initDriveFileEntity()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'keepuser':
|
|
ownerRetainRoleBody['role'] = 'writer'
|
|
elif myarg == 'retainrole':
|
|
ownerRetainRoleBody['role'] = getChoice(TRANSFER_DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
|
|
if ownerRetainRoleBody['role'] in {'source', 'current'}:
|
|
ownerRetainRoleBody['role'] = 'writer'
|
|
elif myarg == 'nonownerretainrole':
|
|
nonOwnerRetainRoleBody['role'] = getChoice(TRANSFER_DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
|
|
if nonOwnerRetainRoleBody['role'] == 'source':
|
|
nonOwnerRetainRoleBody['role'] = 'current'
|
|
elif myarg == 'nonownertargetrole':
|
|
nonOwnerTargetRoleBody['role'] = getChoice(TRANSFER_DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
|
|
elif myarg == 'enforceexpansiveaccess':
|
|
enforceExpansiveAccess = getBoolean()
|
|
elif myarg == 'noretentionmessages':
|
|
showRetentionMessages = False
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg == 'targetfolderid':
|
|
targetFolderIdLocation = Cmd.Location()
|
|
targetFolderId = getString(Cmd.OB_DRIVE_FILE_ID, minLen=0)
|
|
elif myarg == 'targetfoldername':
|
|
targetFolderNameLocation = Cmd.Location()
|
|
targetFolderName = getString(Cmd.OB_DRIVE_FILE_NAME, minLen=0)
|
|
elif myarg == 'targetuserfoldername':
|
|
targetUserFolderPattern = getString(Cmd.OB_DRIVE_FILE_NAME, minLen=0)
|
|
elif myarg == 'targetuserorphansfoldername':
|
|
targetUserOrphansFolderPattern = getString(Cmd.OB_DRIVE_FILE_NAME, minLen=0)
|
|
elif myarg == 'select':
|
|
fileIdEntity = getDriveFileEntity()
|
|
buildTree = False
|
|
elif myarg == 'mergewithtarget':
|
|
mergeWithTarget = getBoolean()
|
|
elif myarg == 'createshortcutsfornonmovablefiles':
|
|
createShortcutsForNonmovableFiles = getBoolean()
|
|
elif myarg == 'skipids':
|
|
skipFileIdEntity = getDriveFileEntity()
|
|
elif myarg == 'preview':
|
|
csvPF = CSVPrintFile(['OldOwner', 'NewOwner', 'type', 'id', 'name', 'role'])
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
unknownArgumentExit()
|
|
if not nonOwnerRetainRoleBody:
|
|
nonOwnerRetainRoleBody = ownerRetainRoleBody
|
|
if not OBY.orderBy:
|
|
OBY.SetItems('folder,createdTime')
|
|
targetUser, targetDrive = buildGAPIServiceObject(API.DRIVE3, targetUser)
|
|
if not targetDrive:
|
|
return
|
|
try:
|
|
result = callGAPI(targetDrive.about(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='storageQuota,user(permissionId)')
|
|
if result['storageQuota'].get('limit'):
|
|
targetDriveFree = int(result['storageQuota']['limit'])-int(result['storageQuota']['usageInDrive'])
|
|
else:
|
|
targetDriveFree = None
|
|
targetPermissionId = result['user']['permissionId']
|
|
result = callGAPI(targetDrive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=ROOT, fields='id,name')
|
|
targetRootId = result['id']
|
|
if not targetFolderId and not targetFolderName:
|
|
targetFolderId = targetRootId
|
|
targetFolderName = result['name']
|
|
else:
|
|
if targetFolderId:
|
|
targetFolder = callGAPI(targetDrive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=targetFolderId, fields='id,name,mimeType,ownedByMe')
|
|
if targetFolder['mimeType'] != MIMETYPE_GA_FOLDER:
|
|
Cmd.SetLocation(targetFolderIdLocation)
|
|
usageErrorExit(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(Ent.USER), targetUser,
|
|
Ent.Singular(Ent.DRIVE_FOLDER_ID), targetFolderId,
|
|
Msg.NOT_AN_ENTITY.format(Ent.Singular(Ent.DRIVE_FOLDER))],
|
|
'\n'))
|
|
if not targetFolder['ownedByMe']:
|
|
Cmd.SetLocation(targetFolderIdLocation)
|
|
usageErrorExit(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(Ent.USER), targetUser,
|
|
Ent.Singular(Ent.DRIVE_FOLDER_ID), targetFolderId,
|
|
Msg.NOT_OWNED_BY.format(targetUser)],
|
|
'\n'))
|
|
targetFolderId = targetFolder['id']
|
|
targetFolderName = targetFolder['name']
|
|
elif targetFolderName:
|
|
result = callGAPIpages(targetDrive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=MY_NON_TRASHED_FOLDER_NAME.format(escapeDriveFileName(targetFolderName)),
|
|
fields='nextPageToken,files(id)')
|
|
if not result:
|
|
Cmd.SetLocation(targetFolderNameLocation)
|
|
usageErrorExit(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(Ent.USER), targetUser,
|
|
Ent.Singular(Ent.DRIVE_FOLDER), targetFolderName,
|
|
Msg.DOES_NOT_EXIST],
|
|
'\n'))
|
|
targetFolderId = result[0]['id']
|
|
except GAPI.fileNotFound:
|
|
Cmd.SetLocation(targetFolderIdLocation)
|
|
usageErrorExit(formatKeyValueList(Ind.Spaces(),
|
|
[Ent.Singular(Ent.USER), targetUser,
|
|
Ent.Singular(Ent.DRIVE_FOLDER_ID), targetFolderId,
|
|
Msg.DOES_NOT_EXIST],
|
|
'\n'))
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(targetUser, str(e))
|
|
return
|
|
targetWriterPermissionsBody = {'role': 'writer', 'type': 'user', 'emailAddress': targetUser}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
if buildTree:
|
|
sourceUser, sourceDrive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not sourceDrive:
|
|
continue
|
|
else:
|
|
sourceUser, sourceDrive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FOLDER)
|
|
if jcount == 0:
|
|
continue
|
|
sourceUserName, _ = splitEmailAddress(sourceUser)
|
|
try:
|
|
result = callGAPI(sourceDrive.about(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='storageQuota,user(permissionId)')
|
|
sourceDriveSize = int(result['storageQuota']['usageInDrive'])
|
|
sourcePermissionId = result['user']['permissionId']
|
|
sourceRootId = callGAPI(sourceDrive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fileId=ROOT, fields='id')['id']
|
|
if (targetDriveFree is not None) and (targetDriveFree < sourceDriveSize):
|
|
printWarningMessage(TARGET_DRIVE_SPACE_ERROR_RC,
|
|
(f'{Msg.NO_TRANSFER_LACK_OF_DISK_SPACE} '
|
|
f'{formatKeyValueList("", ["Source drive size", formatFileSize(sourceDriveSize), "Target drive free", formatFileSize(targetDriveFree)], "")}'))
|
|
continue
|
|
printKeyValueList(['Source drive size', formatFileSize(sourceDriveSize),
|
|
'Target drive free', formatFileSize(targetDriveFree) if targetDriveFree is not None else 'UNLIMITED'])
|
|
if targetDriveFree is not None:
|
|
targetDriveFree = targetDriveFree-sourceDriveSize # prep targetDriveFree for next user
|
|
if not csvPF:
|
|
targetIds[TARGET_PARENT_ID] = _buildTargetUserFolder()
|
|
if targetIds[TARGET_PARENT_ID] is None:
|
|
return
|
|
Ind.Increment()
|
|
if skipFileIdEntity['query'] or skipFileIdEntity[ROOT]:
|
|
_validateUserGetFileIDs(origUser, i, count, skipFileIdEntity, drive=sourceDrive)
|
|
if buildTree:
|
|
topSourceId = sourceRootId
|
|
parentIdMap = {sourceRootId: targetIds[TARGET_PARENT_ID]}
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, Ent.TypeName(Ent.SOURCE_USER, user), i, count)
|
|
feed = callGAPIpages(sourceDrive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy, q=NON_TRASHED,
|
|
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,owners(emailAddress,permissionId),permissions(id,role),shortcutDetails)',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
fileTree = buildFileTree(feed, sourceDrive)
|
|
del feed
|
|
filesTransferred = set()
|
|
_transferDriveFilesFromTree(fileTree[sourceRootId], i, count)
|
|
if fileTree[ORPHANS]['children']:
|
|
if not csvPF:
|
|
_buildTargetUserOrphansFolder()
|
|
_transferDriveFilesFromTree(fileTree[ORPHANS], i, count)
|
|
if not csvPF:
|
|
Act.Set(Act.RETAIN)
|
|
filesTransferred = set()
|
|
_manageRoleRetentionDriveFilesFromTree(fileTree[sourceRootId], i, count)
|
|
if fileTree[ORPHANS]['children']:
|
|
_manageRoleRetentionDriveFilesFromTree(fileTree[ORPHANS], i, count)
|
|
else:
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
fileTree = {}
|
|
parentIdMap = {sourceRootId: targetIds[TARGET_PARENT_ID]}
|
|
Act.Set(Act.TRANSFER_OWNERSHIP)
|
|
try:
|
|
fileEntry = callGAPI(sourceDrive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId,
|
|
fields='id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId),permissions(id,role),shortcutDetails')
|
|
entityType = _getEntityMimeType(fileEntry)
|
|
if fileId in skipFileIdEntity['list']:
|
|
entityActionNotPerformedWarning([Ent.USER, sourceUser, entityType, f'{fileEntry["name"]} ({fileId})'],
|
|
Msg.IN_SKIPIDS, j, jcount)
|
|
continue
|
|
entityPerformActionItemValue([Ent.USER, sourceUser], entityType, f'{fileEntry["name"]} ({fileId})', j, jcount)
|
|
if not mergeWithTarget:
|
|
topSourceId = None
|
|
for parentId in fileEntry.get('parents', []):
|
|
parentIdMap[parentId] = targetIds[TARGET_PARENT_ID]
|
|
else:
|
|
topSourceId = fileId
|
|
parentIdMap[fileId] = targetIds[TARGET_PARENT_ID]
|
|
_identifyDriveFileAndChildren(fileEntry, i, count)
|
|
filesTransferred = set()
|
|
_transferDriveFileAndChildren(fileTree[fileId], i, count, j, jcount, True)
|
|
if not csvPF:
|
|
Act.Set(Act.RETAIN)
|
|
filesTransferred = set()
|
|
_manageRoleRetentionDriveFileAndChildren(fileTree[fileId], i, count, j, jcount, True)
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning([Ent.USER, sourceUser, Ent.DRIVE_FILE_OR_FOLDER, fileId], Msg.NOT_FOUND, j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(sourceUser, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Files to Transfer')
|
|
|
|
def validateUserGetPermissionId(user, i=0, count=0, drive=None):
|
|
if drive is None:
|
|
_, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if drive:
|
|
try:
|
|
result = callGAPI(drive.about(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='user(permissionId)')
|
|
return (drive, result['user']['permissionId'])
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return (None, None)
|
|
|
|
def getPermissionIdForEmail(user, i, count, email):
|
|
currentSvcAcctAPI = GM.Globals[GM.CURRENT_SVCACCT_API]
|
|
currentSvcAcctAPIScopes = GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES]
|
|
_, drive = buildGAPIServiceObject(API.DRIVE2, user, i, count)
|
|
GM.Globals[GM.CURRENT_SVCACCT_API] = currentSvcAcctAPI
|
|
GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] = currentSvcAcctAPIScopes
|
|
if drive:
|
|
try:
|
|
return callGAPI(drive.permissions(), 'getIdForEmail',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
email=email, fields='id')['id']
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
entityActionNotPerformedWarning([Ent.USER, user], Msg.UNABLE_TO_GET_PERMISSION_ID.format(email), i, count)
|
|
systemErrorExit(GM.Globals[GM.SYSEXITRC], None)
|
|
return None
|
|
|
|
# gam <UserTypeEntity> transfer ownership <DriveFileEntity> <UserItem>
|
|
# [<DriveFileParentAttribute>] [includetrashed] [norecursion [<Boolean>]]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [preview] [filepath] [pathdelimiter <Character>] [buildtree]
|
|
# [enforceexpansiveaccess [<Boolean>]]
|
|
# [todrive <ToDriveAttribute>*]
|
|
def transferOwnership(users):
|
|
def _identifyFilesToTransfer(fileEntry):
|
|
for childFileId in fileEntry['children']:
|
|
if childFileId in filesTransferred:
|
|
continue
|
|
filesTransferred.add(childFileId)
|
|
childEntry = fileTree.get(childFileId)
|
|
if childEntry:
|
|
childEntryInfo = childEntry['info']
|
|
if includeTrashed or not childEntryInfo['trashed']:
|
|
if childEntryInfo['ownedByMe']:
|
|
filesToTransfer[childFileId] = {'name': childEntryInfo['name'], 'type': _getEntityMimeType(childEntryInfo)}
|
|
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
_identifyFilesToTransfer(childEntry)
|
|
|
|
def _identifyChildrenToTransfer(fileEntry, user, i, count):
|
|
q = WITH_PARENTS.format(fileEntry['id'])
|
|
setGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, query=q)
|
|
pageMessage = getPageMessageForWhom(clearLastGotMsgLen=False)
|
|
try:
|
|
children = callGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=pageMessage, noFinalize=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy, q=q,
|
|
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed)',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return
|
|
for childEntryInfo in children:
|
|
childFileId = childEntryInfo['id']
|
|
if filepath:
|
|
fileTree[childFileId] = {'info': childEntryInfo}
|
|
if childFileId in filesTransferred:
|
|
continue
|
|
filesTransferred.add(childFileId)
|
|
if includeTrashed or not childEntryInfo['trashed']:
|
|
if childEntryInfo['ownedByMe']:
|
|
filesToTransfer[childFileId] = {'name': childEntryInfo['name'], 'type': _getEntityMimeType(childEntryInfo)}
|
|
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
_identifyChildrenToTransfer(childEntryInfo, user, i, count)
|
|
|
|
fileIdEntity = getDriveFileEntity()
|
|
body = {}
|
|
newOwner = getEmailAddress()
|
|
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
|
|
changeParents = enforceExpansiveAccess = filepath = includeTrashed = noRecursion = False
|
|
pathDelimiter = '/'
|
|
csvPF = fileTree = None
|
|
addParents = ''
|
|
parentBody = {}
|
|
parentParms = initDriveFileAttributes()
|
|
buildTree = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'includetrashed':
|
|
includeTrashed = True
|
|
elif myarg == 'norecursion':
|
|
noRecursion = getBoolean()
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg == 'filepath':
|
|
filepath = True
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
elif myarg == 'buildtree':
|
|
buildTree = True
|
|
elif myarg == 'preview':
|
|
csvPF = CSVPrintFile(['OldOwner', 'NewOwner', 'type', 'id', 'name'])
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getDriveFileParentAttribute(myarg, parentParms):
|
|
changeParents = True
|
|
elif myarg == 'enforceexpansiveaccess':
|
|
enforceExpansiveAccess = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
Act.Set(Act.TRANSFER_OWNERSHIP)
|
|
targetDrive, permissionId = validateUserGetPermissionId(newOwner)
|
|
if not permissionId:
|
|
return
|
|
if changeParents:
|
|
if not _getDriveFileParentInfo(targetDrive, newOwner, 0, 0, parentBody, parentParms):
|
|
return
|
|
addParents = ','.join(parentBody['parents'])
|
|
if csvPF:
|
|
if filepath:
|
|
csvPF.AddTitles('paths')
|
|
else:
|
|
filepath = False
|
|
body = {'role': 'owner'}
|
|
bodyAdd = {'role': 'writer', 'type': 'user', 'emailAddress': newOwner}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE_OR_FOLDER)
|
|
if jcount == 0:
|
|
continue
|
|
if filepath:
|
|
filePathInfo = initFilePathInfo(pathDelimiter)
|
|
filesTransferred = set()
|
|
if buildTree:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count)
|
|
try:
|
|
feed = callGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy,
|
|
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed)',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
fileTree = buildFileTree(feed, drive)
|
|
del feed
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
else:
|
|
fileTree = {}
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
kvList = [Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId]
|
|
if buildTree:
|
|
fileEntry = fileTree.get(fileId)
|
|
if not fileEntry:
|
|
entityActionFailedWarning(kvList, Msg.NOT_FOUND, j, jcount)
|
|
continue
|
|
fileEntryInfo = fileEntry['info']
|
|
else:
|
|
try:
|
|
fileEntryInfo = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId, fields='id,name,parents,mimeType,ownedByMe,trashed,shortcutDetails')
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning(kvList, Msg.NOT_FOUND, j, jcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
if filepath:
|
|
fileTree[fileId] = {'info': fileEntryInfo}
|
|
entityType = _getEntityMimeType(fileEntryInfo)
|
|
entityPerformActionItemValue([Ent.USER, user], entityType, f'{fileEntryInfo["name"]} ({fileId})', j, jcount)
|
|
if fileId in filesTransferred:
|
|
continue
|
|
filesTransferred.add(fileId)
|
|
filesToTransfer = {}
|
|
if includeTrashed or not fileEntryInfo['trashed']:
|
|
if fileEntryInfo['ownedByMe'] and fileEntryInfo['name'] != MY_DRIVE:
|
|
filesToTransfer[fileId] = {'name': fileEntryInfo['name'], 'type': entityType}
|
|
if changeParents:
|
|
filesToTransfer[fileId]['addParents'] = addParents
|
|
filesToTransfer[fileId]['removeParents'] = ','.join(fileEntryInfo.get('parents', []))
|
|
if fileEntryInfo['mimeType'] == MIMETYPE_GA_SHORTCUT and entityType != Ent.DRIVE_SHORTCUT:
|
|
filesToTransfer[fileId]['shortcutDetails'] = fileEntryInfo['shortcutDetails']
|
|
if fileEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER and not noRecursion:
|
|
if buildTree:
|
|
_identifyFilesToTransfer(fileEntry)
|
|
else:
|
|
_identifyChildrenToTransfer(fileEntryInfo, user, i, count)
|
|
if csvPF:
|
|
for xferFileId, fileInfo in iter(filesToTransfer.items()):
|
|
row = {'OldOwner': user, 'NewOwner': newOwner, 'type': Ent.Singular(fileInfo['type']), 'id': xferFileId, 'name': fileInfo['name']}
|
|
if filepath:
|
|
addFilePathsToRow(drive, fileTree, fileTree[xferFileId]['info'], filePathInfo, csvPF, row)
|
|
csvPF.WriteRow(row)
|
|
continue
|
|
Ind.Increment()
|
|
kcount = len(filesToTransfer)
|
|
entityPerformActionNumItemsModifier([Ent.USER, user], kcount, Ent.DRIVE_FILE_OR_FOLDER, f'{Act.MODIFIER_TO} {Ent.Singular(Ent.USER)}: {newOwner}', i, count)
|
|
Ind.Increment()
|
|
k = 0
|
|
for xferFileId, fileInfo in iter(filesToTransfer.items()):
|
|
k += 1
|
|
entityType = fileInfo['type']
|
|
fileDesc = f'{fileInfo["name"]} ({xferFileId})'
|
|
kvList = [Ent.USER, user, entityType, fileDesc]
|
|
try:
|
|
if entityType not in {Ent.DRIVE_SHORTCUT, Ent.DRIVE_FILE_SHORTCUT, Ent.DRIVE_FOLDER_SHORTCUT}:
|
|
if changeParents:
|
|
removeParents = fileInfo.get('removeParents', '')
|
|
if removeParents:
|
|
callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
|
|
fileId=xferFileId, removeParents=removeParents, fields='', supportsAllDrives=True)
|
|
action = Act.Get()
|
|
Act.Set(Act.REMOVE)
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None, [Ent.DRIVE_FOLDER, fileInfo['removeParents']], k, kcount)
|
|
Act.Set(action)
|
|
callGAPI(drive.permissions(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND],
|
|
enforceExpansiveAccess=enforceExpansiveAccess,
|
|
fileId=xferFileId, permissionId=permissionId, transferOwnership=True, body=body, fields='')
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.USER, newOwner], k, kcount)
|
|
else:
|
|
if changeParents and entityType != Ent.DRIVE_SHORTCUT:
|
|
callGAPI(drive.files(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
|
|
fileId=xferFileId, supportsAllDrives=True)
|
|
action = Act.Get()
|
|
Act.Set(Act.DELETE_SHORTCUT)
|
|
entityActionPerformed(kvList, k, kcount)
|
|
Act.Set(action)
|
|
else:
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.USER, newOwner], k, kcount)
|
|
except GAPI.permissionNotFound:
|
|
# this might happen if target user isn't explicitly in ACL (i.e. shared with anyone)
|
|
try:
|
|
callGAPI(drive.permissions(), 'create',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INVALID_SHARING_REQUEST],
|
|
fileId=xferFileId, sendNotificationEmail=False, body=bodyAdd, fields='')
|
|
callGAPI(drive.permissions(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND],
|
|
enforceExpansiveAccess=enforceExpansiveAccess,
|
|
fileId=xferFileId, permissionId=permissionId, transferOwnership=True, body=body, fields='')
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.USER, newOwner], k, kcount)
|
|
except GAPI.invalidSharingRequest as e:
|
|
entityActionFailedWarning(kvList, Ent.TypeNameMessage(Ent.PERMISSION_ID, permissionId, str(e)), k, kcount)
|
|
continue
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning(kvList+[Ent.PERMISSION_ID, permissionId], k, kcount)
|
|
continue
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, k, kcount)
|
|
continue
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, k, kcount)
|
|
continue
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
kvList = [Ent.USER, newOwner, entityType, fileDesc]
|
|
try:
|
|
if entityType not in {Ent.DRIVE_SHORTCUT, Ent.DRIVE_FILE_SHORTCUT, Ent.DRIVE_FOLDER_SHORTCUT}:
|
|
if changeParents and 'addParents' in fileInfo:
|
|
if entityType != Ent.DRIVE_FILE_SHORTCUT:
|
|
callGAPI(targetDrive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
|
|
fileId=xferFileId, addParents=fileInfo['addParents'], fields='', supportsAllDrives=True)
|
|
action = Act.Get()
|
|
Act.Set(Act.ADD)
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.DRIVE_FOLDER, fileInfo['addParents']], k, kcount)
|
|
Act.Set(action)
|
|
else:
|
|
if changeParents and 'addParents' in fileInfo and entityType != Ent.DRIVE_SHORTCUT:
|
|
body = {'name': fileInfo['name'], 'mimeType': MIMETYPE_GA_SHORTCUT,
|
|
'parents': [fileInfo['addParents']], 'shortcutDetails': {'targetId': fileInfo['shortcutDetails']['targetId']}}
|
|
callGAPI(targetDrive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID,
|
|
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION],
|
|
body=body, fields='id', supportsAllDrives=True)
|
|
Act.Set(Act.CREATE_SHORTCUT)
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_IN, None, [Ent.DRIVE_FOLDER, fileInfo['addParents']], k, kcount)
|
|
Act.Set(action)
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, k, kcount)
|
|
except (GAPI.forbidden, GAPI.cannotAddParent, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.invalid, GAPI.badRequest, GAPI.unknownError) as e:
|
|
entityActionFailedWarning(kvList, str(e), k, kcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(newOwner, str(e), 0, 0)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Files to Transfer Ownership')
|
|
|
|
# gam <UserTypeEntity> claim ownership <DriveFileEntity>
|
|
# [<DriveFileParentAttribute>] [includetrashed]
|
|
# [skipids <DriveFileEntity>] [onlyUsers|skipusers <UserTypeEntity>] [subdomains <DomainNameEntity>]
|
|
# [restricted [<Boolean>]] [writerscanshare|writerscantshare [<Boolean>]]
|
|
# [keepuser | (retainrole reader|commenter|writer|editor|none)] [noretentionmessages]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [preview] [filepath] [pathdelimiter <Character>] [buildtree]
|
|
# [enforceexpansiveaccess [<Boolean>]]
|
|
# [todrive <ToDriveAttribute>*]
|
|
def claimOwnership(users):
|
|
def _identifyFilesToClaim(fileEntry):
|
|
for childFileId in fileEntry['children']:
|
|
if childFileId in filesTransferred or childFileId in skipFileIdEntity['list']:
|
|
continue
|
|
filesTransferred.add(childFileId)
|
|
childEntry = fileTree.get(childFileId)
|
|
if childEntry:
|
|
childEntryInfo = childEntry['info']
|
|
if includeTrashed or not childEntryInfo['trashed']:
|
|
owner = childEntryInfo['owners'][0]['emailAddress']
|
|
if (not childEntryInfo['ownedByMe']) and ((not checkOwner) or (checkOnly and owner in onlyOwners) or (checkSkip and owner not in skipOwners)):
|
|
oldOwnerPermissionIds[owner] = childEntryInfo['owners'][0]['permissionId']
|
|
filesToClaim.setdefault(owner, {})
|
|
if childFileId not in filesToClaim[owner]:
|
|
filesToClaim[owner][childFileId] = {'name': childEntryInfo['name'], 'type': _getEntityMimeType(childEntryInfo)}
|
|
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
_identifyFilesToClaim(childEntry)
|
|
|
|
def _identifyChildrenToClaim(fileEntry, user, i, count):
|
|
q = WITH_PARENTS.format(fileEntry['id'])
|
|
setGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, query=q)
|
|
pageMessage = getPageMessageForWhom(clearLastGotMsgLen=False)
|
|
try:
|
|
children = callGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=pageMessage, noFinalize=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy, q=q,
|
|
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId))',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return
|
|
for childEntryInfo in children:
|
|
childFileId = childEntryInfo['id']
|
|
if childFileId in filesTransferred or childFileId in skipFileIdEntity['list']:
|
|
continue
|
|
filesTransferred.add(childFileId)
|
|
if includeTrashed or not childEntryInfo['trashed']:
|
|
if filepath:
|
|
fileTree[childFileId] = {'info': childEntryInfo}
|
|
owner = childEntryInfo['owners'][0]['emailAddress']
|
|
if (not childEntryInfo['ownedByMe']) and ((not checkOwner) or (checkOnly and owner in onlyOwners) or (checkSkip and owner not in skipOwners)):
|
|
oldOwnerPermissionIds[owner] = childEntryInfo['owners'][0]['permissionId']
|
|
filesToClaim.setdefault(owner, {})
|
|
if childFileId not in filesToClaim[owner]:
|
|
filesToClaim[owner][childFileId] = {'name': childEntryInfo['name'], 'type': _getEntityMimeType(childEntryInfo)}
|
|
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
_identifyChildrenToClaim(childEntryInfo, user, i, count)
|
|
|
|
def _processRetainedRole(user, i, count, oldOwner, entityType, ofileId, fileDesc, l, lcount):
|
|
oldOwnerPermissionId = oldOwnerPermissionIds[oldOwner]
|
|
Act.Set(Act.RETAIN)
|
|
try:
|
|
if sourceRetainRoleBody['role'] != 'none':
|
|
if sourceRetainRoleBody['role'] != 'writer':
|
|
callGAPI(sourceDrive.permissions(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST],
|
|
enforceExpansiveAccess=enforceExpansiveAccess,
|
|
fileId=ofileId, permissionId=oldOwnerPermissionId, body=sourceRetainRoleBody, fields='')
|
|
else:
|
|
callGAPI(sourceDrive.permissions(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND, GAPI.BAD_REQUEST],
|
|
fileId=ofileId, permissionId=oldOwnerPermissionId)
|
|
if showRetentionMessages:
|
|
entityActionPerformed([Ent.USER, oldOwner, entityType, fileDesc, Ent.ROLE, sourceRetainRoleBody['role']], l, lcount)
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, oldOwner, entityType, fileDesc, Ent.PERMISSION_ID, oldOwnerPermissionId], l, lcount)
|
|
except (GAPI.badRequest, GAPI.insufficientFilePermissions, GAPI.cannotDeletePermission) as e:
|
|
entityActionFailedWarning([Ent.USER, oldOwner, entityType, fileDesc], str(e), l, lcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
Act.Set(Act.CLAIM_OWNERSHIP)
|
|
|
|
fileIdEntity = getDriveFileEntity()
|
|
skipFileIdEntity = initDriveFileEntity()
|
|
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
|
|
body = {}
|
|
checkOnly = checkSkip = False
|
|
onlyOwners = set()
|
|
skipOwners = set()
|
|
subdomains = []
|
|
enforceExpansiveAccess = filepath = includeTrashed = False
|
|
pathDelimiter = '/'
|
|
addParents = ''
|
|
parentBody = {}
|
|
parentParms = initDriveFileAttributes()
|
|
sourceRetainRoleBody = {'role': 'writer'}
|
|
showRetentionMessages = True
|
|
oldOwnerPermissionIds = {}
|
|
csvPF = fileTree = None
|
|
buildTree = changeParents = False
|
|
bodyShare = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'keepuser':
|
|
sourceRetainRoleBody['role'] = 'writer'
|
|
elif myarg == 'retainrole':
|
|
sourceRetainRoleBody['role'] = getChoice(TRANSFER_DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
|
|
elif myarg == 'noretentionmessages':
|
|
showRetentionMessages = False
|
|
elif myarg == 'skipids':
|
|
skipFileIdEntity = getDriveFileEntity()
|
|
elif myarg == 'onlyusers':
|
|
_, userList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
checkOnly = True
|
|
onlyOwners = set(userList)
|
|
elif myarg == 'skipusers':
|
|
_, userList = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
checkSkip = len(userList) > 0
|
|
skipOwners = set(userList)
|
|
elif myarg == 'subdomains':
|
|
subdomains = getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY)
|
|
elif myarg == 'includetrashed':
|
|
includeTrashed = True
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg == 'enforceexpansiveaccess':
|
|
enforceExpansiveAccess = getBoolean()
|
|
elif myarg == 'restricted':
|
|
bodyShare['copyRequiresWriterPermission'] = getBoolean()
|
|
elif myarg == 'writerscanshare':
|
|
bodyShare['writersCanShare'] = getBoolean()
|
|
elif myarg == 'writerscantshare':
|
|
bodyShare['writersCanShare'] = not getBoolean()
|
|
elif myarg == 'filepath':
|
|
filepath = True
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
elif myarg == 'buildtree':
|
|
buildTree = True
|
|
elif myarg == 'preview':
|
|
csvPF = CSVPrintFile(['OldOwner', 'NewOwner', 'type', 'id', 'name'])
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getDriveFileParentAttribute(myarg, parentParms):
|
|
changeParents = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if checkOnly and checkSkip:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('onlyusers', 'skipusers'))
|
|
checkOwner = checkOnly or checkSkip
|
|
Act.Set(Act.CLAIM_OWNERSHIP)
|
|
if csvPF:
|
|
if filepath:
|
|
csvPF.AddTitles('paths')
|
|
else:
|
|
filepath = False
|
|
body = {'role': 'owner'}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
_, permissionId = validateUserGetPermissionId(user, i, count, drive)
|
|
if not permissionId:
|
|
continue
|
|
if changeParents:
|
|
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parentParms):
|
|
return
|
|
addParents = ','.join(parentBody['parents'])
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.DRIVE_FILE_OR_FOLDER, i, count)
|
|
if jcount == 0:
|
|
continue
|
|
if filepath:
|
|
filePathInfo = initFilePathInfo(pathDelimiter)
|
|
filesTransferred = set()
|
|
bodyAdd = {'role': 'writer', 'type': 'user', 'emailAddress': user}
|
|
if skipFileIdEntity['query'] or skipFileIdEntity[ROOT]:
|
|
_validateUserGetFileIDs(origUser, i, count, skipFileIdEntity, drive=drive)
|
|
if buildTree:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, i, count)
|
|
try:
|
|
feed = callGAPIpages(drive.files(), 'list', 'files',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy,
|
|
fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId))',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS])
|
|
fileTree = buildFileTree(feed, drive)
|
|
del feed
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
else:
|
|
fileTree = {}
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
filesToClaim = {}
|
|
kvList = [Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER, fileId]
|
|
if buildTree:
|
|
fileEntry = fileTree.get(fileId)
|
|
if not fileEntry:
|
|
entityActionFailedWarning(kvList, Msg.NOT_FOUND, j, jcount)
|
|
continue
|
|
fileEntryInfo = fileEntry['info']
|
|
else:
|
|
try:
|
|
fileEntryInfo = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=fileId,
|
|
fields='id,name,parents,mimeType,ownedByMe,trashed,shortcutDetails,owners(emailAddress,permissionId)')
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning(kvList, Msg.NOT_FOUND, j, jcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
if filepath:
|
|
fileTree[fileId] = {'info': fileEntryInfo}
|
|
entityType = _getEntityMimeType(fileEntryInfo)
|
|
if fileId in skipFileIdEntity['list']:
|
|
entityActionNotPerformedWarning([Ent.USER, user, entityType, f'{fileEntryInfo["name"]} ({fileId})'],
|
|
Msg.IN_SKIPIDS, j, jcount)
|
|
continue
|
|
entityPerformActionItemValue([Ent.USER, user], entityType, f'{fileEntryInfo["name"]} ({fileId})', j, jcount)
|
|
if fileId in filesTransferred:
|
|
continue
|
|
filesTransferred.add(fileId)
|
|
if fileId not in skipFileIdEntity['list'] and (includeTrashed or not fileEntryInfo['trashed']):
|
|
owner = fileEntryInfo['owners'][0]['emailAddress']
|
|
if (not fileEntryInfo['ownedByMe']) and ((not checkOwner) or (checkOnly and owner in onlyOwners) or (checkSkip and owner not in skipOwners)):
|
|
oldOwnerPermissionIds[owner] = fileEntryInfo['owners'][0]['permissionId']
|
|
filesToClaim.setdefault(owner, {})
|
|
if fileId not in filesToClaim[owner]:
|
|
filesToClaim[owner][fileId] = {'name': fileEntryInfo['name'], 'type': entityType}
|
|
if changeParents:
|
|
filesToClaim[owner][fileId]['addParents'] = addParents
|
|
filesToClaim[owner][fileId]['removeParents'] = ','.join(fileEntryInfo.get('parents', []))
|
|
if fileEntryInfo['mimeType'] == MIMETYPE_GA_SHORTCUT and entityType != Ent.DRIVE_SHORTCUT:
|
|
filesToClaim[owner][fileId]['shortcutDetails'] = fileEntryInfo['shortcutDetails']
|
|
if fileEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
if buildTree:
|
|
_identifyFilesToClaim(fileEntry)
|
|
else:
|
|
_identifyChildrenToClaim(fileEntryInfo, user, i, count)
|
|
if csvPF:
|
|
for oldOwner in filesToClaim:
|
|
for claimFileId, fileInfo in iter(filesToClaim[oldOwner].items()):
|
|
row = {'NewOwner': user, 'OldOwner': oldOwner, 'type': Ent.Singular(fileInfo['type']), 'id': claimFileId, 'name': fileInfo['name']}
|
|
if filepath:
|
|
addFilePathsToRow(drive, fileTree, fileTree[claimFileId]['info'], filePathInfo, csvPF, row)
|
|
csvPF.WriteRow(row)
|
|
continue
|
|
Ind.Increment()
|
|
kcount = len(filesToClaim)
|
|
entityPerformActionNumItems([Ent.USER, user], kcount, Ent.USER, i, count)
|
|
Ind.Increment()
|
|
k = 0
|
|
for oldOwner in filesToClaim:
|
|
k += 1
|
|
_, userDomain = splitEmailAddress(oldOwner)
|
|
lcount = len(filesToClaim[oldOwner])
|
|
if userDomain == GC.Values[GC.DOMAIN] or userDomain in subdomains:
|
|
_, sourceDrive = buildGAPIServiceObject(API.DRIVE3, oldOwner, k, kcount)
|
|
if not sourceDrive:
|
|
continue
|
|
entityPerformActionNumItemsModifier([Ent.USER, user], lcount, Ent.DRIVE_FILE_OR_FOLDER,
|
|
f'{Act.MODIFIER_FROM} {Ent.Singular(Ent.USER)}: {oldOwner}', k, kcount)
|
|
Ind.Increment()
|
|
l = 0
|
|
for xferFileId, fileInfo in iter(filesToClaim[oldOwner].items()):
|
|
l += 1
|
|
entityType = fileInfo['type']
|
|
fileDesc = f'{fileInfo["name"]} ({xferFileId})'
|
|
kvList = [Ent.USER, oldOwner, entityType, fileDesc]
|
|
try:
|
|
if entityType not in {Ent.DRIVE_SHORTCUT, Ent.DRIVE_FILE_SHORTCUT, Ent.DRIVE_FOLDER_SHORTCUT}:
|
|
if bodyShare:
|
|
callGAPI(sourceDrive.files(), 'update',
|
|
fileId=xferFileId, body=bodyShare, fields='')
|
|
if changeParents:
|
|
removeParents = fileInfo.get('removeParents', '')
|
|
if removeParents:
|
|
callGAPI(sourceDrive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
|
|
fileId=xferFileId, removeParents=removeParents, fields='', supportsAllDrives=True)
|
|
action = Act.Get()
|
|
Act.Set(Act.REMOVE)
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None,
|
|
[Ent.DRIVE_FOLDER, removeParents], l, lcount)
|
|
Act.Set(action)
|
|
callGAPI(sourceDrive.permissions(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND],
|
|
enforceExpansiveAccess=enforceExpansiveAccess,
|
|
fileId=xferFileId, permissionId=permissionId, transferOwnership=True, body=body, fields='')
|
|
kvList = [Ent.USER, user, entityType, fileDesc]
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None, [Ent.USER, oldOwner], l, lcount)
|
|
_processRetainedRole(user, i, count, oldOwner, entityType, xferFileId, fileDesc, l, lcount)
|
|
else:
|
|
if changeParents and entityType != Ent.DRIVE_SHORTCUT:
|
|
callGAPI(sourceDrive.files(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
|
|
fileId=xferFileId, supportsAllDrives=True)
|
|
action = Act.Get()
|
|
Act.Set(Act.DELETE_SHORTCUT)
|
|
entityActionPerformed(kvList, l, lcount)
|
|
Act.Set(action)
|
|
else:
|
|
kvList = [Ent.USER, user, entityType, fileDesc]
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None, [Ent.USER, oldOwner], l, lcount)
|
|
except GAPI.permissionNotFound:
|
|
# if claimer not in ACL (file might be visible for all with link)
|
|
try:
|
|
callGAPI(sourceDrive.permissions(), 'create',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INVALID_SHARING_REQUEST],
|
|
fileId=xferFileId, sendNotificationEmail=False, body=bodyAdd, fields='')
|
|
callGAPI(sourceDrive.permissions(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.PERMISSION_NOT_FOUND],
|
|
enforceExpansiveAccess=enforceExpansiveAccess,
|
|
fileId=xferFileId, permissionId=permissionId, transferOwnership=True, body=body, fields='')
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_FROM, None, [Ent.USER, oldOwner], l, lcount)
|
|
_processRetainedRole(user, i, count, oldOwner, entityType, xferFileId, fileDesc, l, lcount)
|
|
except GAPI.invalidSharingRequest as e:
|
|
entityActionFailedWarning(kvList, Ent.TypeNameMessage(Ent.PERMISSION_ID, permissionId, str(e)), l, lcount)
|
|
continue
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning(kvList+[Ent.PERMISSION_ID, permissionId], l, lcount)
|
|
continue
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, l, lcount)
|
|
continue
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions) as e:
|
|
entityActionFailedWarning(kvList, str(e), l, lcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, l, lcount)
|
|
continue
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions) as e:
|
|
entityActionFailedWarning(kvList, str(e), l, lcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
kvList = [Ent.USER, user, entityType, fileDesc]
|
|
try:
|
|
if entityType not in {Ent.DRIVE_SHORTCUT, Ent.DRIVE_FILE_SHORTCUT, Ent.DRIVE_FOLDER_SHORTCUT}:
|
|
if changeParents and 'addParents' in fileInfo:
|
|
callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
|
|
fileId=xferFileId, addParents=fileInfo['addParents'], fields='', supportsAllDrives=True)
|
|
action = Act.Get()
|
|
Act.Set(Act.ADD)
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_TO, None, [Ent.DRIVE_FOLDER, fileInfo['addParents']], l, lcount)
|
|
Act.Set(action)
|
|
else:
|
|
if changeParents and 'addParents' in fileInfo and entityType != Ent.DRIVE_SHORTCUT:
|
|
body = {'name': fileInfo['name'], 'mimeType': MIMETYPE_GA_SHORTCUT,
|
|
'parents': [fileInfo['addParents']], 'shortcutDetails': {'targetId': fileInfo['shortcutDetails']['targetId']}}
|
|
callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID,
|
|
GAPI.TARGET_USER_ROLE_LIMITED_BY_LICENSE_RESTRICTION],
|
|
body=body, fields='id', supportsAllDrives=True)
|
|
Act.Set(Act.CREATE_SHORTCUT)
|
|
entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_IN, None, [Ent.DRIVE_FOLDER, fileInfo['addParents']], l, lcount)
|
|
Act.Set(action)
|
|
except GAPI.fileNotFound:
|
|
entityActionFailedWarning(kvList, Msg.DOES_NOT_EXIST, l, lcount)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.cannotAddParent) as e:
|
|
entityActionFailedWarning(kvList, str(e), l, lcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
Ind.Decrement()
|
|
else:
|
|
entityPerformActionModifierNumItemsModifier([Ent.USER, user], 'Not Performed', kcount, Ent.DRIVE_FILE_OR_FOLDER,
|
|
f'{Act.MODIFIER_FROM} {Ent.Singular(Ent.USER)}: {oldOwner}', j, jcount)
|
|
Ind.Increment()
|
|
l = 0
|
|
for xferFileId, fileInfo in iter(filesToClaim[oldOwner].items()):
|
|
l += 1
|
|
entityActionNotPerformedWarning([Ent.USER, user, fileInfo['type'], f'{fileInfo["name"]} ({xferFileId})'],
|
|
Msg.USER_IN_OTHER_DOMAIN.format(Ent.Singular(Ent.USER), oldOwner), l, lcount)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Files to Claim Ownership')
|
|
|
|
# gam <UserTypeEntity> print emptydrivefolders [todrive <ToDriveAttribute>*]
|
|
# [select <DriveFileEntity>]
|
|
# [pathdelimiter <Character>]
|
|
def printEmptyDriveFolders(users):
|
|
def _checkChildDriveFolderContents(drive, fileEntry, user, i, count, pathList):
|
|
query = WITH_PARENTS.format(fileEntry ['id'])
|
|
try:
|
|
children = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.BAD_REQUEST],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=query, fields='nextPageToken,files(id,name,mimeType,ownedByMe)',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **fileIdEntity['shareddrive'])
|
|
if not children:
|
|
if sharedDriveId or fileEntry.get('ownedByMe', False):
|
|
row = {'User': user, 'id': fileEntry['id'], 'name': pathDelimiter.join(pathList)}
|
|
if sharedDriveId:
|
|
row['driveId'] = sharedDriveId
|
|
csvPF.WriteRow(row)
|
|
return
|
|
for childEntryInfo in children:
|
|
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
_checkChildDriveFolderContents(drive, childEntryInfo, user, i, count, pathList+[childEntryInfo['name']])
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(query), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
|
|
csvPF = CSVPrintFile(['User', 'id', 'name']) if Act.csvFormat() else None
|
|
fileIdEntity = {}
|
|
pathDelimiter = '/'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'select':
|
|
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': False, 'allowQuery': False, 'mimeTypeInQuery': True})
|
|
fileIdEntity = getDriveFileEntity(DLP=DLP)
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
else:
|
|
unknownArgumentExit()
|
|
if not fileIdEntity:
|
|
fileIdEntity = initDriveFileEntity()
|
|
cleanFileIDsList(fileIdEntity, [ROOT])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
fileId = sharedDriveId = fileIdEntity.get('shareddrive', {}).get('driveId', '')
|
|
if not sharedDriveId:
|
|
fileId = fileIdEntity['list'][0]
|
|
try:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FOLDER, user, i, count)
|
|
fileEntryInfo = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND, GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
fileId=fileId, fields='id,name,mimeType,ownedByMe,driveId', supportsAllDrives=True)
|
|
if 'driveId' in fileEntryInfo:
|
|
sharedDriveId = fileEntryInfo['driveId']
|
|
fileIdEntity['shareddrive'] = {'driveId': sharedDriveId, 'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
csvPF.AddTitles(['driveId'])
|
|
csvPF.MoveTitlesToEnd(['name'])
|
|
pathList = [f'{SHARED_DRIVES}/{_getSharedDriveNameFromId(sharedDriveId)}']
|
|
else:
|
|
pathList = [fileEntryInfo['name']]
|
|
mimeType = fileEntryInfo['mimeType']
|
|
if mimeType != MIMETYPE_GA_FOLDER:
|
|
entityValueList = [Ent.USER, user, _getEntityMimeType(fileEntryInfo), fileEntryInfo['name']]
|
|
entityActionNotPerformedWarning(entityValueList, Msg.INVALID_MIMETYPE.format(mimeType, MIMETYPE_GA_FOLDER), i, count)
|
|
continue
|
|
_checkChildDriveFolderContents(drive, fileEntryInfo, user, i, count, pathList)
|
|
except (GAPI.fileNotFound, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, fileId], str(e), i, count)
|
|
except GAPI.teamDriveMembershipRequired as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Empty Folders')
|
|
|
|
# gam <UserTypeEntity> delete emptydrivefolders
|
|
# [select <DriveFileEntity>]
|
|
# [<SharedDriveEntity>]
|
|
# [pathdelimiter <Character>]
|
|
def deleteEmptyDriveFolders(users):
|
|
def _deleteEmptyChildDriveFolders(drive, fileEntry, user, i, count, pathList, atTop):
|
|
query = WITH_PARENTS.format(fileEntry ['id'])
|
|
try:
|
|
children = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.BAD_REQUEST],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
q=query, fields='nextPageToken,files(id,name,mimeType,ownedByMe)',
|
|
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **fileIdEntity['shareddrive'])
|
|
numChildren = len(children)
|
|
for childEntryInfo in children:
|
|
if childEntryInfo['mimeType'] == MIMETYPE_GA_FOLDER:
|
|
numChildren -= _deleteEmptyChildDriveFolders(drive, childEntryInfo, user, i, count, pathList+[childEntryInfo['name']], False)
|
|
if numChildren == 0 and not atTop:
|
|
try:
|
|
callGAPI(drive.files(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS,
|
|
fileId=fileEntry['id'], supportsAllDrives=True)
|
|
entityActionPerformed([Ent.USER, user, Ent.DRIVE_FOLDER_ID, fileEntry['id'],
|
|
Ent.DRIVE_FOLDER, pathDelimiter.join(pathList)], i, count)
|
|
return 1
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, fileEntry['name']], str(e), i, count)
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.badRequest):
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, None], invalidQuery(query), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
return 0
|
|
|
|
Act.Set(Act.DELETE_EMPTY)
|
|
fileIdEntity = {}
|
|
pathDelimiter = '/'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'select':
|
|
DLP = DriveListParameters({'allowChoose': False, 'allowCorpora': False, 'allowQuery': False, 'mimeTypeInQuery': True})
|
|
fileIdEntity = getDriveFileEntity(DLP=DLP)
|
|
elif myarg == 'pathdelimiter':
|
|
pathDelimiter = getCharacter()
|
|
else:
|
|
fileIdEntity = getSharedDriveEntity()
|
|
if not fileIdEntity:
|
|
fileIdEntity = initDriveFileEntity()
|
|
cleanFileIDsList(fileIdEntity, [ROOT])
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
printEntityKVList([Ent.USER, user],
|
|
[f'{Act.ToPerform()} {Ent.Plural(Ent.DRIVE_FILE_OR_FOLDER)}'],
|
|
i, count)
|
|
Ind.Increment()
|
|
fileId = sharedDriveId = fileIdEntity.get('shareddrive', {}).get('driveId', '')
|
|
if not sharedDriveId:
|
|
fileId = fileIdEntity['list'][0]
|
|
try:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FOLDER, user, i, count)
|
|
fileEntryInfo = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FILE_NOT_FOUND, GAPI.NOT_FOUND, GAPI.TEAMDRIVE_MEMBERSHIP_REQUIRED],
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
fileId=fileId, fields='id,name,mimeType,ownedByMe,driveId', supportsAllDrives=True)
|
|
if 'driveId' in fileEntryInfo:
|
|
sharedDriveId = fileEntryInfo['driveId']
|
|
fileIdEntity['shareddrive'] = {'driveId': sharedDriveId, 'corpora': 'drive', 'includeItemsFromAllDrives': True, 'supportsAllDrives': True}
|
|
pathList = [f'{SHARED_DRIVES}/{_getSharedDriveNameFromId(sharedDriveId)}']
|
|
else:
|
|
pathList = [fileEntryInfo['name']]
|
|
mimeType = fileEntryInfo['mimeType']
|
|
if mimeType != MIMETYPE_GA_FOLDER:
|
|
entityValueList = [Ent.USER, user, _getEntityMimeType(fileEntryInfo), fileEntryInfo['name']]
|
|
entityActionNotPerformedWarning(entityValueList, Msg.INVALID_MIMETYPE.format(mimeType, MIMETYPE_GA_FOLDER), i, count)
|
|
continue
|
|
_deleteEmptyChildDriveFolders(drive, fileEntryInfo, user, i, count, pathList, True)
|
|
except (GAPI.fileNotFound, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FOLDER, fileId], str(e), i, count)
|
|
except GAPI.teamDriveMembershipRequired as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, fileId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> empty drivetrash [<SharedDriveEntity>]
|
|
def emptyDriveTrash(users):
|
|
if Cmd.ArgumentsRemaining():
|
|
fileIdEntity = getSharedDriveEntity()
|
|
else:
|
|
fileIdEntity = {}
|
|
checkForExtraneousArguments()
|
|
kwargs = {'driveId': None}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if not fileIdEntity:
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
else:
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
kwargs['driveId'] = fileIdEntity['shareddrive']['driveId']
|
|
try:
|
|
callGAPI(drive.files(), 'emptyTrash',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INSUFFICIENT_FILE_PERMISSIONS],
|
|
**kwargs)
|
|
entityActionPerformed([Ent.USER, user, Ent.DRIVE_TRASH, kwargs['driveId']], i, count)
|
|
except (GAPI.notFound, GAPI.insufficientFilePermissions) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, kwargs['driveId']], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
|
|
def _getDriveFileACLPrintKeysTimeObjects():
|
|
printKeys = ['id', 'type', 'emailAddress', 'domain', 'role', 'permissionDetails',
|
|
'expirationTime', 'photoLink', 'allowFileDiscovery', 'deleted',
|
|
'pendingOwner', 'view']
|
|
timeObjects = ['expirationTime']
|
|
if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES]:
|
|
_mapDrive3TitlesToDrive2(printKeys, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
|
|
_mapDrive3TitlesToDrive2(timeObjects, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
|
|
return (printKeys, set(timeObjects))
|
|
|
|
# DriveFileACL commands utilities
|
|
def _showDriveFilePermissionJSON(user, fileId, fileName, createdTime, permission, timeObjects):
|
|
_mapDrivePermissionNames(permission)
|
|
row = {'Owner': user, 'id': fileId, 'permission': permission}
|
|
if createdTime is not None:
|
|
row['createdTime'] = createdTime
|
|
if fileId != fileName:
|
|
row['name'] = fileName
|
|
printLine(json.dumps(cleanJSON(row, timeObjects=timeObjects), ensure_ascii=False, sort_keys=True))
|
|
|
|
def _showDriveFilePermissionsJSON(user, fileId, fileName, createdTime, permissions, timeObjects):
|
|
for permission in permissions:
|
|
_mapDrivePermissionNames(permission)
|
|
row = {'Owner': user, 'id': fileId, 'permissions': permissions}
|
|
if createdTime is not None:
|
|
row['createdTime'] = createdTime
|
|
if fileId != fileName:
|
|
row['name'] = fileName
|
|
printLine(json.dumps(cleanJSON(row, timeObjects=timeObjects), ensure_ascii=False, sort_keys=True))
|
|
|
|
def _showDriveFilePermission(permission, printKeys, timeObjects, i=0, count=0):
|
|
if permission.get('displayName'):
|
|
name = permission['displayName']
|
|
elif 'id' in permission:
|
|
if permission['id'] == 'anyone':
|
|
name = 'Anyone'
|
|
elif permission['id'] == 'anyoneWithLink':
|
|
name = 'Anyone with Link'
|
|
else:
|
|
name = permission['id']
|
|
else:
|
|
name = 'Permission'
|
|
_mapDrivePermissionNames(permission)
|
|
printKeyValueListWithCount([name], i, count)
|
|
Ind.Increment()
|
|
for key in printKeys:
|
|
value = permission.get(key)
|
|
if value is None:
|
|
continue
|
|
if key == 'permissionDetails':
|
|
printKeyValueList([key, ''])
|
|
Ind.Increment()
|
|
for detail in value:
|
|
printKeyValueList(['role', detail['role']])
|
|
Ind.Increment()
|
|
printKeyValueList(['type', detail['permissionType']])
|
|
if 'additionalRoles' in detail:
|
|
printKeyValueList(['additionalRoles', ','.join(detail['additionalRoles'])])
|
|
printKeyValueList(['inherited', detail['inherited']])
|
|
if detail['inherited']:
|
|
printKeyValueList(['inheritedFrom', detail.get('inheritedFrom', UNKNOWN)])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
elif key not in timeObjects:
|
|
printKeyValueList([key, value])
|
|
else:
|
|
printKeyValueList([key, formatLocalTime(value)])
|
|
Ind.Decrement()
|
|
|
|
def _showDriveFilePermissions(entityType, fileName, permissions, printKeys, timeObjects, j, jcount):
|
|
kcount = len(permissions)
|
|
entityPerformActionNumItems([entityType, fileName], kcount, Ent.PERMITTEE, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for permission in permissions:
|
|
k += 1
|
|
_showDriveFilePermission(permission, printKeys, timeObjects, k, kcount)
|
|
Ind.Decrement()
|
|
|
|
def _checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess):
|
|
if useDomainAdminAccess and fileIdEntity['nonDomainAdminAccess']:
|
|
Cmd.SetLocation(fileIdEntity['location'])
|
|
usageErrorExit(Msg.INVALID_FILE_SELECTION_WITH_ADMIN_ACCESS)
|
|
|
|
# gam [<UserTypeEntity>] create drivefileacl <DriveFileEntity> [asadmin]
|
|
# anyone|(user <UserItem>)|(group <GroupItem>)|(domain <DomainName>) (role <DriveFileACLRole>)]
|
|
# [withlink|(allowfilediscovery|discoverable [<Boolean>])] [expiration <Time>]
|
|
# (mappermissionsdomain <DomainName> <DomainName>)*
|
|
# [moveToNewOwnersRoot [<Boolean>]]
|
|
# [updatesheetprotectedranges [<Boolean>]]
|
|
# [sendemail] [emailmessage <String>]
|
|
# [showtitles] [nodetails|(csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]])]
|
|
def createDriveFileACL(users, useDomainAdminAccess=False):
|
|
moveToNewOwnersRoot = False
|
|
sendNotificationEmail = showTitles = _transferOwnership = updateSheetProtectedRanges = False
|
|
roleLocation = withLinkLocation = expirationLocation = None
|
|
emailMessage = None
|
|
showDetails = True
|
|
csvPF = None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
|
|
fileIdEntity = getDriveFileEntity()
|
|
body = {}
|
|
body['type'] = permType = getChoice(DRIVEFILE_ACL_PERMISSION_TYPES)
|
|
if permType != 'anyone':
|
|
if permType != 'domain':
|
|
body['emailAddress'] = permissionId = getEmailAddress()
|
|
else:
|
|
body['domain'] = permissionId = getString(Cmd.OB_DOMAIN_NAME)
|
|
else:
|
|
permissionId = 'anyone'
|
|
mapPermissionsDomains = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'withlink':
|
|
withLinkLocation = Cmd.Location()
|
|
body['allowFileDiscovery'] = False
|
|
elif myarg in {'allowfilediscovery', 'discoverable'}:
|
|
withLinkLocation = Cmd.Location()
|
|
body['allowFileDiscovery'] = getBoolean()
|
|
elif myarg == 'role':
|
|
roleLocation = Cmd.Location()
|
|
body['role'] = getChoice(DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
|
|
if body['role'] == 'owner':
|
|
sendNotificationEmail = _transferOwnership = True
|
|
elif myarg == 'mappermissionsdomain':
|
|
oldDomain = getString(Cmd.OB_DOMAIN_NAME).lower()
|
|
mapPermissionsDomains[oldDomain] = getString(Cmd.OB_DOMAIN_NAME).lower()
|
|
elif myarg == 'enforcesingleparent':
|
|
deprecatedArgument(myarg)
|
|
elif myarg == 'movetonewownersroot':
|
|
moveToNewOwnersRoot = getBoolean()
|
|
elif myarg in {'expiration', 'expires'}:
|
|
expirationLocation = Cmd.Location()
|
|
body['expirationTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg == 'sendemail':
|
|
sendNotificationEmail = True
|
|
elif myarg == 'emailmessage':
|
|
sendNotificationEmail = True
|
|
emailMessage = getString(Cmd.OB_STRING)
|
|
elif myarg == 'showtitles':
|
|
showTitles = True
|
|
elif myarg == 'updatesheetprotectedranges':
|
|
updateSheetProtectedRanges = getBoolean()
|
|
elif myarg == 'nodetails':
|
|
showDetails = False
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile(['Owner', 'id'], 'sortall')
|
|
FJQC.SetCsvPF(csvPF)
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg)
|
|
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
|
|
if 'role' not in body:
|
|
missingArgumentExit(f'role {formatChoiceList(DRIVEFILE_ACL_ROLES_MAP)}')
|
|
if mapPermissionsDomains:
|
|
if permType != 'anyone':
|
|
if permType != 'domain':
|
|
atLoc = permissionId.find('@')
|
|
if atLoc != -1:
|
|
mappedDomain = mapPermissionsDomains.get(permissionId[atLoc+1:], None)
|
|
if mappedDomain:
|
|
body['emailAddress'] = permissionId = f"{permissionId[:atLoc]}@{mappedDomain}"
|
|
else:
|
|
mappedDomain = mapPermissionsDomains.get(permissionId, None)
|
|
if mappedDomain:
|
|
body['domain'] = permissionId = mappedDomain
|
|
_validateACLOwnerType(roleLocation, body)
|
|
_validateACLAttributes('allowfilediscovery/withlink', withLinkLocation, body, 'allowFileDiscovery', ['anyone', 'domain'])
|
|
_validateACLAttributes('expiration', expirationLocation, body, 'expirationTime', ['user', 'group'])
|
|
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
|
|
if csvPF:
|
|
if showTitles:
|
|
csvPF.AddTitles(fileNameTitle)
|
|
csvPF.SetSortAllTitles()
|
|
if FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(csvPF.titlesList+['JSON'])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=Ent.DRIVE_FILE_OR_FOLDER_ACL if not csvPF else None,
|
|
useDomainAdminAccess=useDomainAdminAccess)
|
|
if jcount == 0:
|
|
continue
|
|
sheet = None
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
fileName = fileId
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
|
|
if showTitles or updateSheetProtectedRanges:
|
|
fileName, entityType, mimeType = _getDriveFileNameFromId(drive, fileId, combineTitleId=not csvPF)
|
|
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
|
|
if not sheet:
|
|
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
|
|
if not sheet:
|
|
break
|
|
permission = callGAPI(drive.permissions(), 'create',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
moveToNewOwnersRoot=moveToNewOwnersRoot,
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
fileId=fileId, sendNotificationEmail=sendNotificationEmail, emailMessage=emailMessage,
|
|
transferOwnership=_transferOwnership, body=body, fields='*', supportsAllDrives=True)
|
|
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
|
|
_updateSheetProtectedRangesACLchange(sheet, user, i, count, j, jcount, fileId, fileName, True, permission)
|
|
if csvPF:
|
|
baserow = {'Owner': user, 'id': fileId}
|
|
if showTitles:
|
|
baserow[fileNameTitle] = fileName
|
|
row = baserow.copy()
|
|
_mapDrivePermissionNames(permission)
|
|
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = baserow.copy()
|
|
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
else:
|
|
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
|
|
if showDetails:
|
|
_showDriveFilePermission(permission, printKeys, timeObjects)
|
|
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.cannotSetExpiration, GAPI.cannotSetExpirationOnAnyoneOrDomain,
|
|
GAPI.expirationDateNotAllowedForSharedDriveMembers, GAPI.expirationDatesMustBeInTheFuture,
|
|
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
|
|
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
|
|
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
|
|
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
|
|
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
|
|
GAPI.ownerOnTeamDriveItemNotSupported,
|
|
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
|
|
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
|
|
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
|
|
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
|
|
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility,
|
|
GAPI.invalidSharingRequest, GAPI.fileNeverWritable, GAPI.abusiveContentRestriction) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], str(e), j, jcount)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileName], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive File ACLs')
|
|
|
|
def doCreateDriveFileACL():
|
|
createDriveFileACL([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] update drivefileacl <DriveFileEntity> <DriveFilePermissionIDorEmail> [asadmin]
|
|
# (role <DriveFileACLRole>) [expiration <Time>] [removeexpiration [<Boolean>]]
|
|
# [updatesheetprotectedranges [<Boolean>]] [enforceexpansiveaccess [<Boolean>]]
|
|
# [showtitles] [nodetails|(csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]])]
|
|
def updateDriveFileACLs(users, useDomainAdminAccess=False):
|
|
fileIdEntity = getDriveFileEntity()
|
|
isEmail, permissionId = getPermissionId()
|
|
enforceExpansiveAccess = None
|
|
removeExpiration = showTitles = updateSheetProtectedRanges = False
|
|
showDetails = True
|
|
csvPF = None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'role':
|
|
body['role'] = getChoice(DRIVEFILE_ACL_ROLES_MAP, mapChoice=True)
|
|
elif myarg in {'expiration', 'expires'}:
|
|
body['expirationTime'] = getTimeOrDeltaFromNow()
|
|
elif myarg == 'removeexpiration':
|
|
removeExpiration = getBoolean()
|
|
elif myarg == 'showtitles':
|
|
showTitles = True
|
|
elif myarg == 'updatesheetprotectedranges':
|
|
updateSheetProtectedRanges = getBoolean()
|
|
elif myarg == 'enforceexpansiveaccess':
|
|
enforceExpansiveAccess = getBoolean()
|
|
elif myarg == 'nodetails':
|
|
showDetails = False
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile(['Owner', 'id'], 'sortall')
|
|
FJQC.SetCsvPF(csvPF)
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'transferownership':
|
|
deprecatedArgument(myarg)
|
|
getBoolean()
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg)
|
|
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
|
|
if 'role' not in body:
|
|
missingArgumentExit(f'role {formatChoiceList(DRIVEFILE_ACL_ROLES_MAP)}')
|
|
updateKwargs = {'useDomainAdminAccess': useDomainAdminAccess}
|
|
if enforceExpansiveAccess is not None:
|
|
updateKwargs['enforceExpansiveAccess'] = enforceExpansiveAccess
|
|
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
|
|
if csvPF and showTitles:
|
|
csvPF.AddTitles(fileNameTitle)
|
|
csvPF.SetSortAllTitles()
|
|
if FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(csvPF.titlesList+['JSON'])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=Ent.DRIVE_FILE_OR_FOLDER_ACL if not csvPF else None,
|
|
useDomainAdminAccess=useDomainAdminAccess)
|
|
if jcount == 0:
|
|
continue
|
|
if isEmail:
|
|
permissionId = getPermissionIdForEmail(user, i, count, permissionId)
|
|
if not permissionId:
|
|
return
|
|
isEmail = False
|
|
sheet = None
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
fileName = fileId
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
|
|
if showTitles or updateSheetProtectedRanges:
|
|
fileName, entityType, mimeType = _getDriveFileNameFromId(drive, fileId, combineTitleId=not csvPF)
|
|
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
|
|
if not sheet:
|
|
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
|
|
if not sheet:
|
|
break
|
|
permission = callGAPI(drive.permissions(), 'update',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_UPDATE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
**updateKwargs,
|
|
fileId=fileId, permissionId=permissionId, removeExpiration=removeExpiration,
|
|
transferOwnership=body.get('role', '') == 'owner', body=body, fields='*', supportsAllDrives=True)
|
|
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
|
|
_updateSheetProtectedRangesACLchange(sheet, user, i, count, j, jcount, fileId, fileName, True, permission)
|
|
if csvPF:
|
|
baserow = {'Owner': user, 'id': fileId}
|
|
if showTitles:
|
|
baserow[fileNameTitle] = fileName
|
|
row = baserow.copy()
|
|
_mapDrivePermissionNames(permission)
|
|
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = baserow.copy()
|
|
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
else:
|
|
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
|
|
if showDetails:
|
|
_showDriveFilePermission(permission, printKeys, timeObjects)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.cannotSetExpiration, GAPI.cannotSetExpirationOnAnyoneOrDomain,
|
|
GAPI.expirationDateNotAllowedForSharedDriveMembers, GAPI.expirationDatesMustBeInTheFuture,
|
|
GAPI.badRequest, GAPI.invalidOwnershipTransfer, GAPI.cannotRemoveOwner,
|
|
GAPI.fileNeverWritable, GAPI.ownershipChangeAcrossDomainNotPermitted, GAPI.sharingRateLimitExceeded,
|
|
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges,
|
|
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
|
|
GAPI.organizerOnNonTeamDriveItemNotSupported, GAPI.fileOrganizerOnNonTeamDriveNotSupported,
|
|
GAPI.cannotUpdatePermission, GAPI.cannotModifyInheritedTeamDrivePermission, GAPI.fieldNotWritable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
|
|
except (GAPI.notFound, GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
|
|
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.ownerOnTeamDriveItemNotSupported,
|
|
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive, GAPI.fileOrganizerOnFoldersInSharedDriveOnly) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileName], str(e), j, jcount)
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive File ACLs')
|
|
|
|
def doUpdateDriveFileACLs():
|
|
updateDriveFileACLs([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] create permissions <DriveFileEntity> <DriveFilePermissionsEntity> [asadmin]
|
|
# [expiration <Time>] [sendmail] [emailmessage <String>]
|
|
# [moveToNewOwnersRoot [<Boolean>]]
|
|
# <PermissionMatch>* [<PermissionMatchAction>]
|
|
def createDriveFilePermissions(users, useDomainAdminAccess=False):
|
|
def convertJSONPermissions(jsonPermissions):
|
|
permissions = []
|
|
for permission in PM.GetMatchingPermissions(jsonPermissions):
|
|
if permission.get('deleted', False):
|
|
continue
|
|
if permission['type'] in {'anyone', 'domain'}:
|
|
permItem = permission['type'] if permission['allowFileDiscovery'] else permission['type']+'withlink'
|
|
if permission['type'] == 'domain':
|
|
permItem += ':'+permission['domain']
|
|
else:
|
|
permItem = permission['type']+':'+permission['emailAddress']
|
|
permissions.append(permItem+';'+permission['role'].lower())
|
|
return permissions
|
|
|
|
def _makePermissionBody(permission):
|
|
body = {}
|
|
try:
|
|
scope, role = permission.split(';', 1)
|
|
scope = scope.lower()
|
|
role = role.lower()
|
|
if scope in {'anyone', 'anyonewithlink'}:
|
|
body['type'] = 'anyone'
|
|
body['allowFileDiscovery'] = scope != 'anyonewithlink'
|
|
else:
|
|
body['type'], value = scope.split(':', 1)
|
|
if body['type'] == 'domainwithlink':
|
|
body['allowFileDiscovery'] = False
|
|
body['type'] = 'domain'
|
|
if body['type'] not in DRIVEFILE_ACL_PERMISSION_TYPES[1:]:
|
|
return None
|
|
if body['type'] != 'domain':
|
|
body['emailAddress'] = value
|
|
else:
|
|
body['domain'] = value
|
|
body['role'] = DRIVEFILE_ACL_ROLES_MAP.get(role)
|
|
if not body['role']:
|
|
return None
|
|
if expiration:
|
|
body['expirationTime'] = expiration
|
|
return body
|
|
except ValueError:
|
|
return None
|
|
|
|
def _callbackCreatePermission(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if int(ri[RI_J]) == 1:
|
|
entityPerformActionNumItems([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY]], int(ri[RI_JCOUNT]), Ent.PERMITTEE, int(ri[RI_I]), int(ri[RI_COUNT]))
|
|
Ind.Increment()
|
|
if exception is None:
|
|
entityActionPerformed([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason not in GAPI.DEFAULT_RETRY_REASONS+[GAPI.SERVICE_LIMIT]:
|
|
if reason in GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS:
|
|
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], message, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
errMsg = getHTTPError({}, http_status, reason, message)
|
|
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
if int(ri[RI_J]) == int(ri[RI_JCOUNT]):
|
|
Ind.Decrement()
|
|
return
|
|
waitOnFailure(1, 10, reason, message)
|
|
try:
|
|
callGAPI(drive.permissions(), 'create',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_CREATE_ACL_THROW_REASONS, retryReasons=[GAPI.SERVICE_LIMIT],
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
fileId=ri[RI_ENTITY], sendNotificationEmail=sendNotificationEmail, emailMessage=emailMessage,
|
|
body=_makePermissionBody(ri[RI_ITEM]), fields='', supportsAllDrives=True)
|
|
entityActionPerformed([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except (GAPI.badRequest, GAPI.invalid, GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.cannotSetExpiration, GAPI.cannotSetExpirationOnAnyoneOrDomain,
|
|
GAPI.expirationDateNotAllowedForSharedDriveMembers, GAPI.expirationDatesMustBeInTheFuture,
|
|
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.ownershipChangeAcrossDomainNotPermitted,
|
|
GAPI.teamDriveDomainUsersOnlyRestriction, GAPI.teamDriveTeamMembersOnlyRestriction,
|
|
GAPI.targetUserRoleLimitedByLicenseRestriction, GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded,
|
|
GAPI.publishOutNotPermitted, GAPI.shareInNotPermitted, GAPI.shareOutNotPermitted, GAPI.shareOutNotPermittedToUser,
|
|
GAPI.cannotShareTeamDriveTopFolderWithAnyoneOrDomains, GAPI.cannotShareTeamDriveWithNonGoogleAccounts,
|
|
GAPI.ownerOnTeamDriveItemNotSupported,
|
|
GAPI.organizerOnNonTeamDriveNotSupported, GAPI.organizerOnNonTeamDriveItemNotSupported,
|
|
GAPI.fileOrganizerNotYetEnabledForThisTeamDrive,
|
|
GAPI.fileOrganizerOnFoldersInSharedDriveOnly,
|
|
GAPI.fileOrganizerOnNonTeamDriveNotSupported,
|
|
GAPI.teamDrivesFolderSharingNotSupported, GAPI.invalidLinkVisibility,
|
|
GAPI.invalidSharingRequest, GAPI.fileNeverWritable, GAPI.abusiveContentRestriction,
|
|
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.SHAREDDRIVE, ri[RI_ENTITY], Ent.PERMITTEE, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
if int(ri[RI_J]) == int(ri[RI_JCOUNT]):
|
|
Ind.Decrement()
|
|
|
|
moveToNewOwnersRoot = False
|
|
sendNotificationEmail = False
|
|
emailMessage = expiration = None
|
|
fileIdEntity = getDriveFileEntity()
|
|
if not checkArgumentPresent('json'):
|
|
permissions = getEntityList(Cmd.OB_DRIVE_FILE_PERMISSION_ENTITY)
|
|
jsonData = None
|
|
PM = None
|
|
else:
|
|
permissions = []
|
|
jsonData = getJSON([])
|
|
PM = PermissionMatch()
|
|
PM.SetDefaultMatch(False, {'role': 'owner'})
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'enforcesingleparent':
|
|
deprecatedArgument(myarg)
|
|
elif myarg == 'movetonewownersroot':
|
|
moveToNewOwnersRoot = getBoolean()
|
|
elif myarg in {'expiration', 'expires'}:
|
|
expiration = getTimeOrDeltaFromNow()
|
|
elif myarg == 'sendemail':
|
|
sendNotificationEmail = True
|
|
elif myarg == 'emailmessage':
|
|
sendNotificationEmail = True
|
|
emailMessage = getString(Cmd.OB_STRING)
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
elif PM and PM.ProcessArgument(myarg):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
|
|
if jsonData:
|
|
if 'permission' in jsonData:
|
|
permissions = convertJSONPermissions([jsonData['permission']])
|
|
elif 'permissions' in jsonData:
|
|
permissions = convertJSONPermissions(jsonData['permissions'])
|
|
permissionsLists = permissions if isinstance(permissions, dict) else None
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, useDomainAdminAccess=useDomainAdminAccess)
|
|
if not drive:
|
|
continue
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.DRIVE_FILE_OR_FOLDER_ACL, Act.MODIFIER_TO, jcount, Ent.DRIVE_FILE_OR_FOLDER, i, count)
|
|
if jcount == 0:
|
|
continue
|
|
try:
|
|
callGAPI(drive.about(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='kind')
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
Ind.Increment()
|
|
svcargs = dict([('fileId', None), ('sendNotificationEmail', sendNotificationEmail), ('emailMessage', emailMessage),
|
|
('useDomainAdminAccess', useDomainAdminAccess),
|
|
('body', None), ('fields', ''), ('supportsAllDrives', True)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(drive.permissions(), 'create')
|
|
dbatch = drive.new_batch_http_request(callback=_callbackCreatePermission)
|
|
bcount = 0
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
if permissionsLists:
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
permissions = permissionsLists[fileId]
|
|
else:
|
|
permissions = permissionsLists[origUser][fileId]
|
|
kcount = len(permissions)
|
|
if kcount == 0:
|
|
continue
|
|
k = 0
|
|
for permission in permissions:
|
|
k += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['fileId'] = fileId
|
|
svcparms['body'] = _makePermissionBody(permission)
|
|
if not svcparms['body']:
|
|
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, fileId, Ent.PERMITTEE, permission], Msg.INVALID, k, kcount)
|
|
continue
|
|
if svcparms['body']['role'] == 'owner':
|
|
svcparms['moveToNewOwnersRoot'] = moveToNewOwnersRoot
|
|
svcparms['transferOwnership'] = svcparms['sendNotificationEmail'] = True
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(fileId, j, jcount, k, kcount, permission))
|
|
bcount += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = drive.new_batch_http_request(callback=_callbackCreatePermission)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
Ind.Decrement()
|
|
|
|
def doCreatePermissions():
|
|
createDriveFilePermissions([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] delete drivefileacl <DriveFileEntity> <DriveFilePermissionIDorEmail> [asadmin]
|
|
# [updatesheetprotectedranges [<Boolean>]] [enforceexpansiveaccess [<Boolean>]]
|
|
# [showtitles]
|
|
def deleteDriveFileACLs(users, useDomainAdminAccess=False):
|
|
fileIdEntity = getDriveFileEntity()
|
|
isEmail, permissionId = getPermissionId()
|
|
enforceExpansiveAccess = None
|
|
showTitles = updateSheetProtectedRanges = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'showtitles':
|
|
showTitles = getBoolean()
|
|
elif myarg == 'updatesheetprotectedranges':
|
|
updateSheetProtectedRanges = getBoolean()
|
|
elif myarg == 'enforceexpansiveaccess':
|
|
enforceExpansiveAccess = getBoolean()
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
else:
|
|
unknownArgumentExit()
|
|
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
|
|
deleteKwargs = {'useDomainAdminAccess': useDomainAdminAccess}
|
|
if enforceExpansiveAccess is not None:
|
|
deleteKwargs['enforceExpansiveAccess'] = enforceExpansiveAccess
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.DRIVE_FILE_OR_FOLDER_ACL, useDomainAdminAccess=useDomainAdminAccess)
|
|
if jcount == 0:
|
|
continue
|
|
if isEmail:
|
|
permissionId = getPermissionIdForEmail(user, i, count, permissionId)
|
|
if not permissionId:
|
|
return
|
|
isEmail = False
|
|
sheet = None
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
fileName = fileId
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
|
|
if showTitles or updateSheetProtectedRanges:
|
|
fileName, entityType, mimeType = _getDriveFileNameFromId(drive, fileId)
|
|
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
|
|
permission = callGAPI(drive.permissions(), 'get',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.PERMISSION_NOT_FOUND, GAPI.INSUFFICIENT_ADMINISTRATOR_PRIVILEGES],
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
fileId=fileId, permissionId=permissionId, fields='type,emailAddress,domain,role', supportsAllDrives=True)
|
|
if not sheet:
|
|
_, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
|
|
if not sheet:
|
|
break
|
|
callGAPI(drive.permissions(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_DELETE_ACL_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE],
|
|
**deleteKwargs,
|
|
fileId=fileId, permissionId=permissionId, supportsAllDrives=True)
|
|
entityActionPerformed([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
|
|
if updateSheetProtectedRanges and mimeType == MIMETYPE_GA_SPREADSHEET:
|
|
_updateSheetProtectedRangesACLchange(sheet, user, i, count, j, jcount, fileId, fileName, False, permission)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.fileNeverWritable, GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded, GAPI.cannotDeletePermission) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileName], str(e), j, jcount)
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def doDeleteDriveFileACLs():
|
|
deleteDriveFileACLs([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] delete permissions <DriveFileEntity> <DriveFilePermissionIDEntity> [asadmin]
|
|
# <PermissionMatch>* [<PermissionMatchAction>]
|
|
# [enforceexpansiveaccess [<Boolean>]]
|
|
def deletePermissions(users, useDomainAdminAccess=False):
|
|
def convertJSONPermissions(jsonPermissions):
|
|
permissionIds = []
|
|
for permission in PM.GetMatchingPermissions(jsonPermissions):
|
|
if permission.get('role', '') == 'owner':
|
|
continue
|
|
permissionIds.append(permission['id'])
|
|
return permissionIds
|
|
|
|
def _callbackDeletePermissionId(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if int(ri[RI_J]) == 1:
|
|
entityPerformActionNumItems([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY]], int(ri[RI_JCOUNT]), Ent.PERMISSION_ID, int(ri[RI_I]), int(ri[RI_COUNT]))
|
|
Ind.Increment()
|
|
if exception is None:
|
|
entityActionPerformed([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
if reason not in GAPI.DEFAULT_RETRY_REASONS+[GAPI.SERVICE_LIMIT]:
|
|
if reason == GAPI.PERMISSION_NOT_FOUND:
|
|
entityDoesNotHaveItemWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
elif reason in GAPI.DRIVE3_DELETE_ACL_THROW_REASONS:
|
|
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], message, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
errMsg = getHTTPError({}, http_status, reason, message)
|
|
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
if int(ri[RI_J]) == int(ri[RI_JCOUNT]):
|
|
Ind.Decrement()
|
|
return
|
|
waitOnFailure(1, 10, reason, message)
|
|
try:
|
|
callGAPI(drive.permissions(), 'delete',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+GAPI.DRIVE3_DELETE_ACL_THROW_REASONS,
|
|
retryReasons=[GAPI.SERVICE_LIMIT],
|
|
useDomainAdminAccess=useDomainAdminAccess, enforceExpansiveAccess=enforceExpansiveAccess,
|
|
fileId=ri[RI_ENTITY], permissionId=ri[RI_ITEM], supportsAllDrives=True)
|
|
entityActionPerformed([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.cannotRemoveOwner, GAPI.cannotModifyInheritedTeamDrivePermission,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.sharingRateLimitExceeded, GAPI.permissionNotFound, GAPI.cannotDeletePermission,
|
|
GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
entityActionFailedWarning([Ent.DRIVE_FILE_OR_FOLDER_ID, ri[RI_ENTITY], Ent.PERMISSION_ID, ri[RI_ITEM]], str(e), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
if int(ri[RI_J]) == int(ri[RI_JCOUNT]):
|
|
Ind.Decrement()
|
|
|
|
fileIdEntity = getDriveFileEntity()
|
|
if not checkArgumentPresent('json'):
|
|
permissionIds = getEntityList(Cmd.OB_DRIVE_FILE_PERMISSION_ID_ENTITY)
|
|
jsonData = None
|
|
PM = None
|
|
else:
|
|
permissionIds = []
|
|
jsonData = getJSON([])
|
|
PM = PermissionMatch()
|
|
PM.SetDefaultMatch(False, {'role': 'owner'})
|
|
enforceExpansiveAccess = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
elif PM and PM.ProcessArgument(myarg):
|
|
pass
|
|
elif myarg == 'enforceexpansiveaccess':
|
|
enforceExpansiveAccess = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
|
|
if jsonData:
|
|
if 'permission' in jsonData:
|
|
permissionIds = convertJSONPermissions([jsonData['permission']])
|
|
elif 'permissions' in jsonData:
|
|
permissionIds = convertJSONPermissions(jsonData['permissions'])
|
|
permissionIdsLists = permissionIds if isinstance(permissionIds, dict) else None
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, useDomainAdminAccess=useDomainAdminAccess)
|
|
if not drive:
|
|
continue
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.DRIVE_FILE_OR_FOLDER_ACL, Act.MODIFIER_FROM, jcount, Ent.DRIVE_FILE_OR_FOLDER, i, count)
|
|
if jcount == 0:
|
|
continue
|
|
try:
|
|
callGAPI(drive.about(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='kind')
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
Ind.Increment()
|
|
svcargs = dict([('fileId', None), ('permissionId', None), ('useDomainAdminAccess', useDomainAdminAccess), ('supportsAllDrives', True)]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(drive.permissions(), 'delete')
|
|
dbatch = drive.new_batch_http_request(callback=_callbackDeletePermissionId)
|
|
bcount = 0
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
if permissionIdsLists:
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
permissionIds = permissionIdsLists[fileId]
|
|
else:
|
|
permissionIds = permissionIdsLists[origUser][fileId]
|
|
kcount = len(permissionIds)
|
|
if kcount == 0:
|
|
continue
|
|
k = 0
|
|
for permissionId in permissionIds:
|
|
k += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['fileId'] = fileId
|
|
svcparms['permissionId'] = permissionId
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(fileId, j, jcount, k, kcount, permissionId))
|
|
bcount += 1
|
|
if bcount >= GC.Values[GC.BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = drive.new_batch_http_request(callback=_callbackDeletePermissionId)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
Ind.Decrement()
|
|
|
|
def doDeletePermissions():
|
|
deletePermissions([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] info drivefileacl <DriveFileEntity> <DriveFilePermissionIDorEmail> [asadmin]
|
|
# [showtitles] [formatjson]
|
|
def infoDriveFileACLs(users, useDomainAdminAccess=False):
|
|
fileIdEntity = getDriveFileEntity()
|
|
isEmail, permissionId = getPermissionId()
|
|
showTitles = False
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'showtitles':
|
|
showTitles = getBoolean()
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
|
|
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=[Ent.DRIVE_FILE_OR_FOLDER_ACL, None][FJQC.formatJSON],
|
|
useDomainAdminAccess=useDomainAdminAccess)
|
|
if jcount == 0:
|
|
continue
|
|
if isEmail:
|
|
permissionId = getPermissionIdForEmail(user, i, count, permissionId)
|
|
if not permissionId:
|
|
return
|
|
isEmail = False
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
fileName = fileId
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
|
|
if showTitles:
|
|
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId, not FJQC.formatJSON, useDomainAdminAccess)
|
|
permission = callGAPI(drive.permissions(), 'get',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.BAD_REQUEST, GAPI.PERMISSION_NOT_FOUND, GAPI.INSUFFICIENT_ADMINISTRATOR_PRIVILEGES],
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
fileId=fileId, permissionId=permissionId, fields='*', supportsAllDrives=True)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([entityType, fileName], jcount, Ent.PERMITTEE)
|
|
Ind.Increment()
|
|
_showDriveFilePermission(permission, printKeys, timeObjects, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
_showDriveFilePermissionJSON(user, fileId, fileName, None, permission, timeObjects)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError,
|
|
GAPI.badRequest, GAPI.insufficientAdministratorPrivileges) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
|
|
except GAPI.permissionNotFound:
|
|
entityDoesNotHaveItemWarning([Ent.USER, user, entityType, fileName, Ent.PERMISSION_ID, permissionId], j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def doInfoDriveFileACLs():
|
|
infoDriveFileACLs([_getAdminEmail()], True)
|
|
|
|
DRIVEFILE_PERMISSIONS_FOR_VIEW_CHOICES = ['published']
|
|
|
|
def getDriveFilePermissionsFields(myarg, fieldsList):
|
|
if myarg in DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP:
|
|
fieldsList.append(DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP[myarg])
|
|
elif myarg == 'basicpermissions':
|
|
fieldsList.extend(DRIVEFILE_BASIC_PERMISSION_FIELDS[:-1])
|
|
elif myarg == 'fields':
|
|
for field in _getFieldsList():
|
|
if field in DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP:
|
|
fieldsList.append(DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP[field])
|
|
elif field == 'basicpermissions':
|
|
fieldsList.extend(DRIVEFILE_BASIC_PERMISSION_FIELDS[:-1])
|
|
else:
|
|
invalidChoiceExit(field, DRIVE_PERMISSIONS_SUBFIELDS_CHOICE_MAP, True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
# gam [<UserTypeEntity>] print drivefileacls <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# (role|roles <DriveFileACLRoleList>)*
|
|
# <PermissionMatch>* [<PermissionMatchAction>] [pmselect]
|
|
# [includepermissionsforview published]
|
|
# [oneitemperrow] [<DrivePermissionsFieldName>*|(fields <DrivePermissionsFieldNameList>)]
|
|
# [showtitles|(addtitle <String>)]]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [formatjson [quotechar <Character>]] [asadmin]
|
|
# gam [<UserTypeEntity>] show drivefileacls <DriveFileEntity>
|
|
# (role|roles <DriveFileACLRoleList>)*
|
|
# <PermissionMatch>* [<PermissionMatchAction>] [pmselect]
|
|
# [includepermissionsforview published]
|
|
# [oneitemperrow] [<DrivePermissionsFieldName>*|(fields <DrivePermissionsFieldNameList>)]
|
|
# [showtitles|(addtitle <String>)]]
|
|
# (orderby <DriveFileOrderByFieldName> [ascending|descending])*
|
|
# [formatjson] [asadmin]
|
|
def printShowDriveFileACLs(users, useDomainAdminAccess=False):
|
|
def _printPermissionRow(baserow, permission):
|
|
row = baserow.copy()
|
|
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = baserow.copy()
|
|
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
csvPF = CSVPrintFile(['Owner', 'id'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fileIdEntity = getDriveFileEntity()
|
|
aclRolesMap = DRIVEFILE_ACL_ROLES_MAP.copy()
|
|
if fileIdEntity['shareddrivename'] or fileIdEntity['shareddriveadminquery'] or fileIdEntity['shareddrivefilequery'] or fileIdEntity['shareddrive']:
|
|
aclRolesMap['owner'] = 'organizer'
|
|
addTitle = None
|
|
roles = set()
|
|
oneItemPerRow = pmselect = showTitles = False
|
|
includePermissionsForView = None
|
|
fieldsList = []
|
|
OBY = OrderBy(DRIVEFILE_ORDERBY_CHOICE_MAP)
|
|
PM = PermissionMatch()
|
|
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'role', 'roles'}:
|
|
roles |= getACLRoles(aclRolesMap)
|
|
elif myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
elif myarg in {'showtitles', 'addtitle'}:
|
|
if myarg == 'showtitles':
|
|
showTitles = True
|
|
addTitle = None
|
|
else:
|
|
addTitle = getString(Cmd.OB_STRING)
|
|
showTitles = False
|
|
if csvPF:
|
|
csvPF.AddTitles(fileNameTitle)
|
|
csvPF.SetSortAllTitles()
|
|
elif getDriveFilePermissionsFields(myarg, fieldsList):
|
|
pass
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
elif myarg == 'pmselect':
|
|
pmselect = True
|
|
elif myarg == 'pmfilter': # Ignore, this is the default behavior
|
|
pass
|
|
elif PM.ProcessArgument(myarg):
|
|
pass
|
|
elif myarg == 'includepermissionsforview':
|
|
includePermissionsForView = getChoice(DRIVEFILE_PERMISSIONS_FOR_VIEW_CHOICES)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
|
|
if fieldsList:
|
|
if roles:
|
|
fieldsList.append('role')
|
|
fields = getItemFieldsFromFieldsList('permissions', fieldsList, True)
|
|
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=[Ent.DRIVE_FILE_OR_FOLDER, None][csvPF is not None or FJQC.formatJSON],
|
|
orderBy=OBY.orderBy, useDomainAdminAccess=useDomainAdminAccess)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
fileName = fileId
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
|
|
if showTitles:
|
|
fileName, entityType, _ = _getDriveFileNameFromId(drive, fileId, not (csvPF or FJQC.formatJSON), useDomainAdminAccess)
|
|
elif addTitle:
|
|
fileName = f'{addTitle} ({fileId})'
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER
|
|
try:
|
|
permissions = callGAPIpages(drive.permissions(), 'list', 'permissions',
|
|
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS+[GAPI.NOT_FOUND],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
includePermissionsForView=includePermissionsForView,
|
|
fileId=fileId, fields=fields, supportsAllDrives=True)
|
|
for permission in permissions:
|
|
permission.pop('teamDrivePermissionDetails', None)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
|
|
GAPI.unknownError, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, fileName], str(e), j, jcount)
|
|
continue
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, fileName], str(e), j, jcount)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
if pmselect:
|
|
if not PM.CheckPermissionMatches(permissions):
|
|
continue
|
|
else:
|
|
permissions = PM.GetMatchingPermissions(permissions)
|
|
if roles:
|
|
matchingPermissions = []
|
|
for permission in permissions:
|
|
if roles and permission['role'] not in roles:
|
|
continue
|
|
matchingPermissions.append(permission)
|
|
permissions = matchingPermissions
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
_showDriveFilePermissions(entityType, fileName, permissions, printKeys, timeObjects, j, jcount)
|
|
else:
|
|
if oneItemPerRow:
|
|
for permission in permissions:
|
|
_showDriveFilePermissionJSON(user, fileId, fileName, None, permission, timeObjects)
|
|
else:
|
|
_showDriveFilePermissionsJSON(user, fileId, fileName, None, permissions, timeObjects)
|
|
else:
|
|
baserow = {'Owner': user, 'id': fileId}
|
|
if showTitles or addTitle:
|
|
baserow[fileNameTitle] = fileName
|
|
if oneItemPerRow:
|
|
for permission in permissions:
|
|
_mapDrivePermissionNames(permission)
|
|
pdetails = permission.pop('permissionDetails', [])
|
|
if not pdetails:
|
|
_printPermissionRow(baserow, permission)
|
|
else:
|
|
for pdetail in pdetails:
|
|
permission['permissionDetails'] = pdetail
|
|
_printPermissionRow(baserow, permission)
|
|
else:
|
|
row = baserow.copy()
|
|
for permission in permissions:
|
|
_mapDrivePermissionNames(permission)
|
|
flattenJSON({'permissions': permissions}, flattened=row, timeObjects=timeObjects)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
baserow['JSON'] = json.dumps(cleanJSON({'permissions': permissions}, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(baserow)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
if not oneItemPerRow:
|
|
csvPF.SetIndexedTitles(['permissions'])
|
|
csvPF.writeCSVfile('Drive File ACLs')
|
|
|
|
def doPrintShowDriveFileACLs():
|
|
printShowDriveFileACLs([_getAdminEmail()], True)
|
|
|
|
DRIVELABELS_PROJECTION_CHOICE_MAP = {'basic': 'LABEL_VIEW_BASIC', 'full': 'LABEL_VIEW_FULL'}
|
|
DRIVELABELS_PERMISSION_ROLE_MAP = {
|
|
'applier': 'APPLIER',
|
|
'editor': 'EDITOR',
|
|
'organizer': 'ORGANIZER',
|
|
'organiser': 'ORGANIZER',
|
|
'reader': 'READER',
|
|
}
|
|
DRIVELABELS_TIME_OBJECTS = {'createTime', 'publishTime', 'disableTime', 'revisionCreateTime'}
|
|
|
|
def _getDisplayDriveLabelsParameters(myarg, parameters):
|
|
if myarg in DRIVELABELS_PROJECTION_CHOICE_MAP:
|
|
parameters['view'] = DRIVELABELS_PROJECTION_CHOICE_MAP[myarg]
|
|
elif myarg == 'language':
|
|
parameters['languageCode'] = getLanguageCode(BCP47_LANGUAGE_CODES_MAP)
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
parameters['useAdminAccess'] = True
|
|
elif myarg == 'publishedonly':
|
|
parameters['publishedOnly'] = getBoolean()
|
|
elif myarg == 'minimumrole':
|
|
parameters['minimumRole'] = getChoice(DRIVELABELS_PERMISSION_ROLE_MAP, mapChoice=True)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def normalizeDriveLabelID(driveLabelID):
|
|
atLoc = driveLabelID.find('@')
|
|
if atLoc != -1:
|
|
driveLabelID = driveLabelID[:atLoc]
|
|
if driveLabelID.startswith('labels/'):
|
|
return driveLabelID[7:]
|
|
return driveLabelID
|
|
|
|
def normalizeDriveLabelName(driveLabelName):
|
|
if driveLabelName.startswith('labels/'):
|
|
return driveLabelName
|
|
return f'labels/{driveLabelName}'
|
|
|
|
def validateDriveLabelName(name, kvList, j, jcount, permName=False):
|
|
name = normalizeDriveLabelName(name)
|
|
# Label name
|
|
if not permName:
|
|
mg = re.match(r'^(labels/[^/]+)$', name)
|
|
if not mg:
|
|
entityActionNotPerformedWarning(kvList, 'Expected labels/<String>', j, jcount)
|
|
return None
|
|
return name
|
|
# Label permission name
|
|
mg = re.match(r'^(labels/[^/]+)/permissions/(?:audiences|groups|people)/.+$', name)
|
|
if not mg:
|
|
entityActionNotPerformedWarning(kvList, 'Expected labels/<String>/permissions/(audiences|groups|people)/<String>', j, jcount)
|
|
return (None, None)
|
|
return (name, mg.group(1))
|
|
|
|
def _showDriveLabel(label, j, jcount, FJQC):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(label, timeObjects=DRIVELABELS_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CLASSIFICATION_LABEL_NAME, f'{label["name"]}'], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, label, timeObjects=DRIVELABELS_TIME_OBJECTS, dictObjectsKey={'fields': 'id', 'choices': 'id'})
|
|
Ind.Decrement()
|
|
|
|
# gam [<UserTypeEntity>] info classificationlabels <ClassificationLabelNameEntity>
|
|
# [[basic|full] [languagecode <BCP47LanguageCode>]
|
|
# [formatjson] [asadmin]
|
|
def infoDriveLabels(users, useAdminAccess=False):
|
|
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_NAME, Ent.CLASSIFICATION_LABEL, shlexSplit=True)
|
|
FJQC = FormatJSONQuoteChar()
|
|
parameters = {'useAdminAccess': useAdminAccess}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getDisplayDriveLabelsParameters(myarg, parameters):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, labelNames, jcount = _validateUserGetObjectList(user, i, count, driveLabelNameEntity,
|
|
api=api, showAction=not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for name in labelNames:
|
|
j += 1
|
|
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_NAME, name]
|
|
name = validateDriveLabelName(name, kvList, j, jcount, False)
|
|
if name is None:
|
|
continue
|
|
try:
|
|
label = callGAPI(drive.labels(), 'get',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED,
|
|
GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR],
|
|
name=name, **parameters)
|
|
_showDriveLabel(label, j, jcount, FJQC)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.internalError) as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def doInfoDriveLabels():
|
|
infoDriveLabels([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] print classificationlabels> [todrive <ToDriveAttribute>*]
|
|
# [basic|full] [languagecode <BCP47LanguageCode>]
|
|
# [publishedonly [<Boolean>]] [minimumrole applier|editor|organizer|reader]
|
|
# [formatjson [quotechar <Character>]] [asadmin]
|
|
# gam [<UserTypeEntity>] show classificationlabels
|
|
# [basic|full] [languagecode <BCP47LanguageCode>]
|
|
# [publishedonly [<Boolean>]] [minimumrole applier|editor|organizer|reader]
|
|
# [formatjson] [asadmin]
|
|
def printShowDriveLabels(users, useAdminAccess=False):
|
|
csvPF = CSVPrintFile(['User', 'name', 'description', 'id'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
parameters = {'useAdminAccess': useAdminAccess}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getDisplayDriveLabelsParameters(myarg, parameters):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(['User', 'name', 'JSON'])
|
|
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(api, user, i, count)
|
|
if not drive:
|
|
continue
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(Ent.CLASSIFICATION_LABEL, user, i, count)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
try:
|
|
labels = callGAPIpages(drive.labels(), 'list', 'labels',
|
|
pageMessage=pageMessage,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.PERMISSION_DENIED],
|
|
**parameters, fields='nextPageToken,labels', pageSize=200)
|
|
if not csvPF:
|
|
jcount = len(labels)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.CLASSIFICATION_LABEL, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for label in labels:
|
|
j += 1
|
|
_showDriveLabel(label, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
for label in labels:
|
|
row = flattenJSON(label, flattened={'User': user}, timeObjects=DRIVELABELS_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'User': user, 'name': label['name']}
|
|
row['JSON'] = json.dumps(cleanJSON(label, timeObjects=DRIVELABELS_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CLASSIFICATION_LABEL, None], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Classification Labels')
|
|
|
|
def doPrintShowDriveLabels():
|
|
printShowDriveLabels([_getAdminEmail()], True)
|
|
|
|
def _showDriveLabelPermission(labelperm, j, jcount, FJQC):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(labelperm), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.CLASSIFICATION_LABEL_PERMISSION_NAME, f'{labelperm["name"]}'], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, labelperm)
|
|
Ind.Decrement()
|
|
|
|
# gam [<UserTypeEntity>] create classificationlabelpermission <ClassificationLabelNameEntity>
|
|
# (user <UserItem>) | (group <GroupItem) | (audience <String>)
|
|
# role applier|editor|organizer|reader
|
|
# [nodetails|formatjson] [asadmin]
|
|
def createDriveLabelPermissions(users, useAdminAccess=False):
|
|
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_NAME, Ent.CLASSIFICATION_LABEL_PERMISSION, shlexSplit=True)
|
|
FJQC = FormatJSONQuoteChar()
|
|
parameters = {'useAdminAccess': useAdminAccess}
|
|
body = {}
|
|
showDetails = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in ADMIN_ACCESS_OPTIONS:
|
|
parameters['useAdminAccess'] = True
|
|
elif myarg == 'role':
|
|
body['role'] = getChoice(DRIVELABELS_PERMISSION_ROLE_MAP, mapChoice=True)
|
|
elif myarg in {'user', 'group'}:
|
|
email = getEmailAddress(returnUIDprefix='id:')
|
|
body['email'], status = convertUIDtoEmailAddressWithType(email, emailTypes=[myarg])
|
|
if status == 'unknown':
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.ENTITY_DOES_NOT_EXIST.format(email))
|
|
elif myarg == 'audience':
|
|
audience = getString(Cmd.OB_STRING)
|
|
if not audience.startswith('audiences/'):
|
|
audience = 'audiences/'+audience
|
|
body['audience'] = audience
|
|
elif myarg == 'nodetails':
|
|
showDetails = False
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if 'role' not in body:
|
|
missingArgumentExit(f'role {"|".join(DRIVELABELS_PERMISSION_ROLE_MAP.keys())}')
|
|
if 'email' not in body and 'audience' not in body:
|
|
missingArgumentExit('(user <UserItem>) | (group <GroupItem) | (audience <String>)')
|
|
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, labelNames, jcount = _validateUserGetObjectList(user, i, count, driveLabelNameEntity,
|
|
api=api, showAction=not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for name in labelNames:
|
|
j += 1
|
|
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_NAME, name, Ent.CLASSIFICATION_LABEL_PERMISSION, None]
|
|
name = validateDriveLabelName(name, kvList, j, jcount, False)
|
|
if name is None:
|
|
continue
|
|
try:
|
|
labelperm = callGAPI(drive.labels().permissions(), 'create',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.NOT_FOUND,
|
|
GAPI.INVALID, GAPI.INTERNAL_ERROR],
|
|
parent=name, body=body, **parameters)
|
|
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_PERMISSION, labelperm['name']]
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, j, jcount)
|
|
if showDetails:
|
|
Ind.Increment()
|
|
_showDriveLabelPermission(labelperm, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.permissionDenied, GAPI.notFound, GAPI.invalid, GAPI.internalError) as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def doCreateDriveLabelPermissions():
|
|
createDriveLabelPermissions([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] delete classificationlabelpermission <ClassificationLabelNameEntity>
|
|
# (user <UserItem>) | (group <GroupItem) | (audience <String>)
|
|
# [asadmin]
|
|
# gam [<UserTypeEntity>] remove classificationlabelpermission <ClassificationLabelPermissionNameEntity>
|
|
# [asadmin]
|
|
def deleteDriveLabelPermissions(users, useAdminAccess=False):
|
|
doDelete = Act.Get() == Act.DELETE
|
|
if doDelete:
|
|
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_NAME, Ent.CLASSIFICATION_LABEL, shlexSplit=True)
|
|
else:
|
|
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_PERMISSION_NAME, Ent.CLASSIFICATION_LABEL_PERMISSION, shlexSplit=True)
|
|
parameters = {'useAdminAccess': useAdminAccess, 'requests': [None]}
|
|
labelperm = ''
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in ADMIN_ACCESS_OPTIONS:
|
|
parameters['useAdminAccess'] = True
|
|
elif doDelete and myarg in {'user', 'group'}:
|
|
labelperm = ['people/', 'groups/'][myarg == 'group']+convertEmailAddressToUID(getEmailAddress(), cd=None, emailType=myarg, savedLocation=None)
|
|
elif doDelete and myarg == 'audience':
|
|
audience = getString(Cmd.OB_STRING)
|
|
if not audience.startswith('audiences/'):
|
|
audience = 'audiences/'+audience
|
|
labelperm = audience
|
|
else:
|
|
unknownArgumentExit()
|
|
if doDelete and not labelperm:
|
|
missingArgumentExit('(user <UserItem>) | (group <GroupItem) | (audience <String>)')
|
|
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, labelPermNames, jcount = _validateUserGetObjectList(user, i, count, driveLabelNameEntity,
|
|
api=api, showAction=True)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for name in labelPermNames:
|
|
j += 1
|
|
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_PERMISSION_NAME, name]
|
|
if doDelete:
|
|
parent = validateDriveLabelName(name, kvList, j, jcount, False)
|
|
if parent is None:
|
|
continue
|
|
name = parent+'/permissions/'+labelperm
|
|
else:
|
|
name, parent = validateDriveLabelName(name, kvList, j, jcount, True)
|
|
if name is None:
|
|
continue
|
|
kvList[-1] = name
|
|
parameters['requests'][0] = {'name': name}
|
|
try:
|
|
callGAPI(drive.labels().permissions(), 'batchDelete',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.INVALID, GAPI.NOT_FOUND],
|
|
parent=parent, body=parameters)
|
|
entityActionPerformed(kvList, j, jcount)
|
|
except (GAPI.permissionDenied, GAPI.invalid, GAPI.notFound) as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def doDeleteDriveLabelPermissions():
|
|
deleteDriveLabelPermissions([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] print classificationlabelpermissions <ClassificationLabelNameEntity> [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]] [asadmin]
|
|
# gam [<UserTypeEntity>] show classificationlabelpermissions <ClassificationLabelNameEntity>
|
|
# [formatjson] [asadmin]
|
|
def printShowDriveLabelPermissions(users, useAdminAccess=False):
|
|
csvPF = CSVPrintFile(['User', 'name', 'email', 'role', 'person', 'group', 'audience'], 'sortall') if Act.csvFormat() else None
|
|
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_CLASSIFICATION_LABEL_NAME, Ent.CLASSIFICATION_LABEL_PERMISSION, shlexSplit=True)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
parameters = {'useAdminAccess': useAdminAccess}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
parameters['useAdminAccess'] = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(['User', 'name', 'JSON'])
|
|
api = API.DRIVELABELS_ADMIN if parameters['useAdminAccess'] else API.DRIVELABELS_USER
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, labelNames, jcount = _validateUserGetObjectList(user, i, count, driveLabelNameEntity,
|
|
api=api, showAction=FJQC is None or not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for name in labelNames:
|
|
j += 1
|
|
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_NAME, name, Ent.CLASSIFICATION_LABEL_PERMISSION, None]
|
|
name = validateDriveLabelName(name, kvList, j, jcount, False)
|
|
if name is None:
|
|
continue
|
|
kvList = [Ent.USER, user, Ent.CLASSIFICATION_LABEL_NAME, name]
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(Ent.CLASSIFICATION_LABEL_PERMISSION, name, j, jcount)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
try:
|
|
labelperms = callGAPIpages(drive.labels().permissions(), 'list', 'labelPermissions',
|
|
pageMessage=pageMessage,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.NOT_FOUND],
|
|
parent=name, **parameters, fields='nextPageToken,labelPermissions', pageSize=200)
|
|
if not csvPF:
|
|
jcount = len(labelperms)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems(kvList, jcount, Ent.CLASSIFICATION_LABEL_PERMISSION, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for labelperm in labelperms:
|
|
j += 1
|
|
_showDriveLabelPermission(labelperm, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
for labelperm in labelperms:
|
|
row = flattenJSON(labelperm, flattened={'User': user})
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'User': user, 'name': labelperm['name']}
|
|
row['JSON'] = json.dumps(cleanJSON(labelperm),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
except (GAPI.permissionDenied, GAPI.notFound) as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Classification Label Permissions')
|
|
|
|
def doPrintShowDriveLabelPermissions():
|
|
printShowDriveLabelPermissions([_getAdminEmail()], True)
|
|
|
|
DRIVELABEL_FIELD_TYPE_MAP = {
|
|
'text': 'setTextValues',
|
|
'selection': 'setSelectionValues',
|
|
'integer': 'setIntegerValues',
|
|
'date': 'setDateValues',
|
|
'user': 'setUserValues',
|
|
}
|
|
|
|
# gam <UserTypeEntity> process filedrivelabels <DriveFileEntity>
|
|
# (addlabel <ClassificationLabelIDList>)*
|
|
# (deletelabel <ClassificationLabelIDList>)*
|
|
# (addlabelfield <ClassificationLabelID> <ClassificationLabelFieldID>
|
|
# (text <String>)|selection <ClassificationLabelSelectionIDList>)|
|
|
# (integer <Number>)|(date <Date>)|(user <EmailAddressList>))*
|
|
# (deletelabelfield <ClassificationLabelID> <ClassificationLabelFieldID>)*
|
|
# [nodetails]
|
|
def processFileDriveLabels(users):
|
|
fileIdEntity = getDriveFileEntity()
|
|
actionList = {'addlabel': {'action': Act.CREATE, 'list': []},
|
|
'deletelabel': {'action': Act.DELETE, 'list': []},
|
|
'addlabelfield': {'action': Act.CREATE, 'list': []},
|
|
'deletelabelfield': {'action': Act.DELETE, 'list': []},
|
|
}
|
|
showDetails = True
|
|
kcount = 0
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'addlabel':
|
|
labelIds = getEntityList(Cmd.OB_CLASSIFICATION_LABEL_ID, shlexSplit=True)
|
|
for labelId in labelIds:
|
|
actionList[myarg]['list'].append({'labelModifications': [{'labelId': normalizeDriveLabelID(labelId)}]})
|
|
kcount += 1
|
|
elif myarg == 'deletelabel':
|
|
labelIds = getEntityList(Cmd.OB_CLASSIFICATION_LABEL_ID, shlexSplit=True)
|
|
for labelId in labelIds:
|
|
actionList[myarg]['list'].append({'labelModifications': [{'labelId': normalizeDriveLabelID(labelId), 'removeLabel': True}]})
|
|
kcount += 1
|
|
elif myarg == 'addlabelfield':
|
|
labelId = normalizeDriveLabelID(getString(Cmd.OB_CLASSIFICATION_LABEL_ID))
|
|
fieldId = getString(Cmd.OB_CLASSIFICATION_LABEL_FIELD_ID)
|
|
fieldType = getChoice(DRIVELABEL_FIELD_TYPE_MAP, mapChoice=True)
|
|
if fieldType == 'setTextValues':
|
|
valueList = [getString(Cmd.OB_STRING, minLen=0)]
|
|
elif fieldType == 'setSelectionValues':
|
|
valueList = convertEntityToList(getString(Cmd.OB_CLASSIFICATION_LABEL_SELECTION_ID_LIST, minLen=0), shlexSplit=True)
|
|
elif fieldType == 'setIntegerValues':
|
|
valueList = [getInteger()]
|
|
elif fieldType == 'setDateValues':
|
|
valueList = [getYYYYMMDD()]
|
|
else: #elif fieldType == 'setUserValues':
|
|
valueList = convertEntityToList(getString(Cmd.OB_EMAIL_ADDRESS_LIST, minLen=0))
|
|
actionList[myarg]['list'].append({'labelModifications': [{'labelId': labelId,
|
|
'fieldModifications': [{'fieldId': fieldId, fieldType: valueList}]}]})
|
|
kcount += 1
|
|
elif myarg == 'deletelabelfield':
|
|
labelId = normalizeDriveLabelID(getString(Cmd.OB_CLASSIFICATION_LABEL_ID))
|
|
fieldId = getString(Cmd.OB_CLASSIFICATION_LABEL_FIELD_ID)
|
|
actionList[myarg]['list'].append({'labelModifications': [{'labelId': labelId,
|
|
'fieldModifications': [{'fieldId': fieldId, 'unsetValues': True}]}]})
|
|
kcount += 1
|
|
elif myarg == 'nodetails':
|
|
showDetails = False
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=None)
|
|
if jcount == 0:
|
|
continue
|
|
Act.Set(Act.PROCESS)
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.CLASSIFICATION_LABEL, Act.MODIFIER_FOR, jcount, Ent.DRIVE_FILE_OR_FOLDER)
|
|
Ind.Increment()
|
|
j = 0
|
|
userError = False
|
|
for fileId in fileIdEntity['list']:
|
|
j += 1
|
|
k = 0
|
|
kvList = [Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId]
|
|
Act.Set(Act.PROCESS)
|
|
entityPerformActionNumItems(kvList, kcount, Ent.CLASSIFICATION_LABEL)
|
|
Ind.Increment()
|
|
for operation in ['deletelabelfield', 'deletelabel', 'addlabel', 'addlabelfield']:
|
|
Act.Set(actionList[operation]['action'])
|
|
for action in actionList[operation]['list']:
|
|
k += 1
|
|
xkvList = kvList.copy()
|
|
xkvList.extend([Ent.CLASSIFICATION_LABEL_ID, action['labelModifications'][0]['labelId']])
|
|
if 'fieldModifications' in action['labelModifications'][0]:
|
|
xkvList.extend([Ent.CLASSIFICATION_LABEL_FIELD_ID, action['labelModifications'][0]['fieldModifications'][0]['fieldId']])
|
|
try:
|
|
label = callGAPI(drive.files(), 'modifyLabels',
|
|
throwReasons=GAPI.DRIVE3_MODIFY_LABEL_THROW_REASONS,
|
|
fileId=fileId, body=action)
|
|
entityActionPerformed(xkvList, k, kcount)
|
|
if showDetails:
|
|
Ind.Increment()
|
|
showJSON(None, label, timeObjects=DRIVELABELS_TIME_OBJECTS, dictObjectsKey={'fields': 'id', 'modifiedLabels': 'id'})
|
|
Ind.Decrement()
|
|
except GAPI.fileNotFound as e:
|
|
entityActionFailedWarning(kvList, str(e), j, jcount)
|
|
break
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.fileNeverWritable, GAPI.applyLabelForbidden,
|
|
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.invalidInput, GAPI.badRequest,
|
|
GAPI.labelMutationUnknownField, GAPI.labelMutationIllegalSelection, GAPI.labelMutationForbidden,
|
|
GAPI.labelMultipleValuesForSingularField) as e:
|
|
entityActionFailedWarning(xkvList, str(e), k, kcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
userError = True
|
|
break
|
|
Ind.Decrement()
|
|
if userError:
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam print ownership <DriveFileID>|(drivefilename <DriveFileName>) [todrive <ToDriveAttribute>*]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show ownership <DriveFileID>|(drivefilename <DriveFileName>)
|
|
# [formatjson]
|
|
def doPrintShowOwnership():
|
|
rep = buildGAPIObject(API.REPORTS)
|
|
customerId = GC.Values[GC.CUSTOMER_ID]
|
|
if customerId == GC.MY_CUSTOMER:
|
|
customerId = None
|
|
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
|
|
csvPF = CSVPrintFile('Owner') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
addCSVData = {}
|
|
showComplete = False
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER_ID
|
|
myarg = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
|
|
mycmd = myarg.lower().replace('_', '').replace('-', '')
|
|
if mycmd == 'id':
|
|
fileId = getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True)
|
|
elif mycmd == 'drivefilename':
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER
|
|
fileId = getString(Cmd.OB_DRIVE_FILE_NAME, checkBlank=True)
|
|
elif mycmd.find(':') != -1:
|
|
kw, fileId = myarg.split(':', 1)
|
|
kw = kw.lower().replace('_', '').replace('-', '')
|
|
if fileId.isspace():
|
|
Cmd.Backup()
|
|
blankArgumentExit(Cmd.OB_DRIVE_FILE_ID)
|
|
if kw == 'id':
|
|
pass
|
|
elif kw == 'drivefilename':
|
|
entityType = Ent.DRIVE_FILE_OR_FOLDER
|
|
else:
|
|
Cmd.Backup()
|
|
invalidArgumentExit(Cmd.OB_DRIVE_FILE_ID)
|
|
else:
|
|
fileId = myarg
|
|
if not fileId:
|
|
Cmd.Backup()
|
|
invalidArgumentExit(Cmd.OB_DRIVE_FILE_ID)
|
|
if entityType == Ent.DRIVE_FILE_OR_FOLDER_ID:
|
|
filters = f'doc_id=={fileId}'
|
|
else:
|
|
filters = f'doc_title=={fileId}'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and not FJQC.formatJSON:
|
|
csvPF.AddTitles(['id', fileNameTitle, 'type', 'ownerIsSharedDrive', 'driveId', 'event'])
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
csvPF.SetSortAllTitles()
|
|
foundIds = {}
|
|
try:
|
|
feed = callGAPIpages(rep.activities(), 'list', 'items',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.AUTH_ERROR],
|
|
applicationName='drive', userKey='all', customerId=customerId,
|
|
filters=filters, fields='nextPageToken,items(events(name,parameters))')
|
|
except GAPI.badRequest:
|
|
systemErrorExit(BAD_REQUEST_RC, Msg.BAD_REQUEST)
|
|
except GAPI.invalid as e:
|
|
systemErrorExit(GOOGLE_API_ERROR_RC, str(e))
|
|
except GAPI.authError:
|
|
accessErrorExit(None)
|
|
for activity in feed:
|
|
events = activity.pop('events')
|
|
for event in events:
|
|
fileInfo = {'event': event['name']}
|
|
for item in event.get('parameters', []):
|
|
if item['name'] == 'primary_event':
|
|
if not item['boolValue']:
|
|
break
|
|
elif item['name'] == 'doc_id':
|
|
if item['value'] in foundIds:
|
|
break
|
|
fileInfo['id'] = item['value']
|
|
elif event['name'] == 'change_owner' and item['name'] == 'new_owner':
|
|
fileInfo['Owner'] = item['value']
|
|
elif event['name'] != 'change_owner' and item['name'] == 'owner':
|
|
fileInfo['Owner'] = item['value']
|
|
elif item['name'] == 'doc_title':
|
|
fileInfo[fileNameTitle] = item['value']
|
|
elif item['name'] == 'doc_type':
|
|
fileInfo['type'] = item['value']
|
|
elif item['name'] == 'owner_is_shared_drive':
|
|
fileInfo['ownerIsSharedDrive'] = item['boolValue']
|
|
elif item['name'] == 'shared_drive_id':
|
|
fileInfo['driveId'] = item['value']
|
|
else:
|
|
if 'Owner' in fileInfo and 'id' in fileInfo:
|
|
foundIds[fileInfo['id']] = True
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
printEntityKVList([Ent.OWNER, fileInfo['Owner']],
|
|
['id', fileInfo['id'], fileNameTitle, fileInfo.get(fileNameTitle, ''),
|
|
'type', fileInfo.get('type', ''),
|
|
'ownerIsSharedDrive', fileInfo.get('ownerIsSharedDrive', False),
|
|
'driveId', fileInfo.get('driveId', ''),
|
|
'event', fileInfo['event']])
|
|
else:
|
|
printLine(json.dumps(cleanJSON(fileInfo), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
if addCSVData:
|
|
fileInfo.update(addCSVData)
|
|
row = flattenJSON(fileInfo)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'JSON': json.dumps(cleanJSON(fileInfo), ensure_ascii=False, sort_keys=True)})
|
|
if entityType == Ent.DRIVE_FILE_OR_FOLDER_ID:
|
|
showComplete = True
|
|
break
|
|
if showComplete:
|
|
break
|
|
if showComplete:
|
|
break
|
|
if not foundIds:
|
|
entityActionFailedWarning([entityType, fileId], Msg.NOT_FOUND)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive File Ownership')
|
|
|
|
def _getSharedDriveTheme(myarg, body):
|
|
if myarg in {'theme', 'themeid'}:
|
|
body.pop('backgroundImageFile', None)
|
|
body.pop('colorRgb', None)
|
|
body['themeId'] = getString(Cmd.OB_STRING, checkBlank=True)
|
|
elif myarg == 'customtheme':
|
|
body.pop('themeId', None)
|
|
body['backgroundImageFile'] = {
|
|
'id': getString(Cmd.OB_DRIVE_FILE_ID, checkBlank=True),
|
|
'xCoordinate': getFloat(minVal=0.0, maxVal=1.0),
|
|
'yCoordinate': getFloat(minVal=0.0, maxVal=1.0),
|
|
'width': getFloat(minVal=0.0, maxVal=1.0)
|
|
}
|
|
elif myarg in {'color', 'colour'}:
|
|
body.pop('themeId', None)
|
|
body['colorRgb'] = getColor()
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
SHAREDDRIVE_RESTRICTIONS_MAP = {
|
|
'adminmanagedrestrictions': 'adminManagedRestrictions',
|
|
'allowcontentmanagerstosharefolders': 'sharingFoldersRequiresOrganizerPermission',
|
|
'copyrequireswriterpermission': 'copyRequiresWriterPermission',
|
|
'domainusersonly': 'domainUsersOnly',
|
|
'drivemembersonly': 'driveMembersOnly',
|
|
'sharingfoldersrequiresorganizerpermission': 'sharingFoldersRequiresOrganizerPermission',
|
|
'teammembersonly': 'driveMembersOnly',
|
|
}
|
|
|
|
def _getSharedDriveRestrictions(myarg, body):
|
|
def _setRestriction(restriction):
|
|
body.setdefault('restrictions', {})
|
|
if restriction != 'allowcontentmanagerstosharefolders':
|
|
body['restrictions'][SHAREDDRIVE_RESTRICTIONS_MAP[restriction]] = getBoolean()
|
|
else:
|
|
body['restrictions'][SHAREDDRIVE_RESTRICTIONS_MAP[restriction]] = not getBoolean()
|
|
|
|
if myarg.startswith('restrictions.'):
|
|
_, subField = myarg.split('.', 1)
|
|
if subField in SHAREDDRIVE_RESTRICTIONS_MAP:
|
|
_setRestriction(subField)
|
|
return True
|
|
invalidChoiceExit(subField, SHAREDDRIVE_RESTRICTIONS_MAP, True)
|
|
if myarg in SHAREDDRIVE_RESTRICTIONS_MAP:
|
|
_setRestriction(myarg)
|
|
return True
|
|
return False
|
|
|
|
def _moveSharedDriveToOU(orgUnit, orgUnitId, driveId, user, i, count, ci, returnIdOnly):
|
|
action = Act.Get()
|
|
name = f'orgUnits/-/memberships/shared_drive;{driveId}'
|
|
if ci is None:
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_ORGUNITS_BETA)
|
|
cibody = {'customer': _getCustomersCustomerIdWithC(),
|
|
'destinationOrgUnit': f'orgUnits/{orgUnitId[3:]}'}
|
|
try:
|
|
callGAPI(ci.orgUnits().memberships(), 'move',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN,
|
|
GAPI.INVALID_ARGUMENT, GAPI.ABORTED],
|
|
name=name, body=cibody)
|
|
if not returnIdOnly:
|
|
Act.Set(Act.MOVE)
|
|
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE, driveId], Act.MODIFIER_TO,
|
|
f'{Ent.Singular(Ent.ORGANIZATIONAL_UNIT)}: {orgUnit}', i, count)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.aborted, GAPI.badRequest, GAPI.internalError,
|
|
GAPI.noManageTeamDriveAdministratorPrivilege, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
Act.Set(action)
|
|
return ci
|
|
|
|
# gam <UserTypeEntity> create shareddrive <Name> [asadmin]
|
|
# [(theme|themeid <String>) | ([customtheme <DriveFileID> <Float> <Float> <Float>] [color <ColorValue>])]
|
|
# (<SharedDriveRestrictionsFieldName> <Boolean>)*
|
|
# [hide|hidden <Boolean>] [ou|org|orgunit <OrgUnitItem>]
|
|
# [errorretries <Integer>] [updateinitialdelay <Integer>] [updateretrydelay <Integer>]
|
|
# [movetoorgunitdelay <Integer>]
|
|
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*) | returnidonly]
|
|
def createSharedDrive(users, useDomainAdminAccess=False):
|
|
def waitingForCreationToComplete(sleep_time):
|
|
writeStderr(Ind.Spaces()+Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.SHAREDDRIVE), sleep_time))
|
|
time.sleep(sleep_time)
|
|
|
|
requestId = str(uuid.uuid4())
|
|
body = {'name': getString(Cmd.OB_NAME, checkBlank=True)}
|
|
updateBody = {}
|
|
csvPF = None
|
|
addCSVData = {}
|
|
hide = returnIdOnly = False
|
|
orgUnit = orgUnitId = ci = None
|
|
errorRetries = 5
|
|
updateInitialDelay = 10
|
|
updateRetryDelay = 10
|
|
moveToOrgUnitDelay = 20
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getSharedDriveTheme(myarg, body):
|
|
pass
|
|
elif _getSharedDriveRestrictions(myarg, updateBody):
|
|
pass
|
|
elif myarg in {'hide', 'hidden'}:
|
|
hide = getBoolean()
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
orgUnit, orgUnitId = getOrgUnitId()
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile()
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
elif myarg == 'errorretries':
|
|
errorRetries = getInteger(minVal=0, maxVal=10)
|
|
elif myarg == 'updateinitialdelay':
|
|
updateInitialDelay = getInteger(minVal=0, maxVal=60)
|
|
elif myarg == 'updateretrydelay':
|
|
updateRetryDelay = getInteger(minVal=0, maxVal=60)
|
|
elif myarg == 'movetoorgunitdelay':
|
|
moveToOrgUnitDelay = getInteger(minVal=0, maxVal=60)
|
|
else:
|
|
unknownArgumentExit()
|
|
if csvPF:
|
|
csvPF.SetTitles(['User', 'name', 'id'])
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
for field in ['backgroundImageFile', 'colorRgb']:
|
|
if field in body:
|
|
updateBody[field] = body.pop(field)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
doUpdate = False
|
|
Act.Set(Act.CREATE)
|
|
retry = 0
|
|
while True:
|
|
try:
|
|
shareddrive = callGAPI(drive.drives(), 'create',
|
|
bailOnTransientError=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.TRANSIENT_ERROR, GAPI.TEAMDRIVE_ALREADY_EXISTS,
|
|
GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_FILE_PERMISSIONS,
|
|
GAPI.DUPLICATE, GAPI.BAD_REQUEST, GAPI.USER_CANNOT_CREATE_TEAMDRIVES],
|
|
requestId=requestId, body=body, fields='id')
|
|
driveId = shareddrive['id']
|
|
if returnIdOnly:
|
|
writeStdout(f'{driveId}\n')
|
|
elif not csvPF:
|
|
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_NAME, body['name'], Ent.SHAREDDRIVE_ID, driveId], i, count)
|
|
else:
|
|
row = {'User': user, 'name': body['name'], 'id': driveId}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
doUpdate = True
|
|
break
|
|
except (GAPI.transientError, GAPI.teamDriveAlreadyExists) as e:
|
|
retry += 1
|
|
if retry > errorRetries:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.REQUEST_ID, requestId], str(e), i, count)
|
|
break
|
|
requestId = str(uuid.uuid4())
|
|
except GAPI.duplicate:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.REQUEST_ID, requestId], Msg.DUPLICATE, i, count)
|
|
break
|
|
except (GAPI.insufficientPermissions, GAPI.insufficientFilePermissions, GAPI.badRequest, GAPI.userCannotCreateTeamDrives) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.REQUEST_ID, requestId], str(e), i, count)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
if not (doUpdate or updateBody or hide or orgUnit):
|
|
continue
|
|
waitingForCreationToComplete(updateInitialDelay)
|
|
created = False
|
|
retry = 0
|
|
while not created:
|
|
try:
|
|
callGAPI(drive.drives(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
driveId=driveId, fields='id')
|
|
created = True
|
|
break
|
|
except GAPI.notFound as e:
|
|
retry += 1
|
|
if retry > errorRetries:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
|
|
break
|
|
waitingForCreationToComplete(updateRetryDelay)
|
|
if not created:
|
|
continue
|
|
try:
|
|
if updateBody:
|
|
Act.Set(Act.UPDATE)
|
|
try:
|
|
callGAPI(drive.drives(), 'update',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN,
|
|
GAPI.NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE,
|
|
GAPI.BAD_REQUEST, GAPI.INTERNAL_ERROR, GAPI.PERMISSION_DENIED,
|
|
GAPI.FILE_NOT_FOUND],
|
|
useDomainAdminAccess=useDomainAdminAccess, driveId=driveId, body=updateBody)
|
|
if not returnIdOnly and not csvPF:
|
|
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
|
|
except GAPI.fileNotFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId,
|
|
Ent.DRIVE_FILE, body.get('backgroundImageFile', {}).get('id', UNKNOWN)],
|
|
str(e), i, count)
|
|
if hide:
|
|
Act.Set(Act.HIDE)
|
|
callGAPI(drive.drives(), 'hide',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
driveId=driveId)
|
|
if not returnIdOnly and not csvPF:
|
|
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
|
|
if orgUnit:
|
|
waitingForCreationToComplete(moveToOrgUnitDelay)
|
|
ci = _moveSharedDriveToOU(orgUnit, orgUnitId, driveId, user, i, count, ci, returnIdOnly or csvPF)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.badRequest, GAPI.noManageTeamDriveAdministratorPrivilege) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('SharedDrives')
|
|
|
|
# gam create shareddrive <Name>
|
|
# [(theme|themeid <String>) | ([customtheme <DriveFileID> <Float> <Float> <Float>] [color <ColorValue>])]
|
|
# (<SharedDriveRestrictionsFieldName> <Boolean>)*
|
|
# [hide|hidden <Boolean>]
|
|
# [errorretries <Integer>] [updateinitialdelay <Integer>] [updateretrydelay <Integer>]
|
|
# [movetoorgunitdelay <Integer>]
|
|
# [(csv [todrive <ToDriveAttribute>*] (addcsvdata <FieldName> <String>)*) | returnidonly]
|
|
def doCreateSharedDrive():
|
|
createSharedDrive([_getAdminEmail()], True)
|
|
|
|
# gam <UserTypeEntity> update shareddrive <SharedDriveEntity> [asadmin] [name <Name>]
|
|
# [(theme|themeid <String>) | ([customtheme <DriveFileID> <Float> <Float> <Float>] [color <ColorValue>])]
|
|
# (<SharedDriveRestrictionsFieldName> <Boolean>)*
|
|
# [hide|hidden <Boolean>] [ou|org|orgunit <OrgUnitItem>]
|
|
def updateSharedDrive(users, useDomainAdminAccess=False):
|
|
fileIdEntity = getSharedDriveEntity()
|
|
body = {}
|
|
hide = None
|
|
orgUnit = orgUnitId = ci = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
body['name'] = getString(Cmd.OB_NAME, checkBlank=True)
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
orgUnit, orgUnitId = getOrgUnitId()
|
|
elif _getSharedDriveTheme(myarg, body):
|
|
pass
|
|
elif _getSharedDriveRestrictions(myarg, body):
|
|
pass
|
|
elif myarg in {'hide', 'hidden'}:
|
|
hide = getBoolean()
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity, useDomainAdminAccess=useDomainAdminAccess)
|
|
if not drive:
|
|
continue
|
|
try:
|
|
driveId = fileIdEntity['shareddrive']['driveId']
|
|
if body:
|
|
result = callGAPI(drive.drives(), 'update',
|
|
bailOnInternalError=True,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
|
|
GAPI.NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE,
|
|
GAPI.INTERNAL_ERROR, GAPI.FILE_NOT_FOUND],
|
|
useDomainAdminAccess=useDomainAdminAccess, driveId=driveId, body=body, fields='name')
|
|
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_NAME, result['name'], Ent.SHAREDDRIVE_ID, driveId], i, count)
|
|
if hide is not None:
|
|
if hide:
|
|
Act.Set(Act.HIDE)
|
|
function = 'hide'
|
|
else:
|
|
Act.Set(Act.UNHIDE)
|
|
function = 'unhide'
|
|
callGAPI(drive.drives(), function,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
driveId=driveId)
|
|
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
|
|
if orgUnit:
|
|
ci = _moveSharedDriveToOU(orgUnit, orgUnitId, driveId, user, i, count, ci, False)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.badRequest, GAPI.internalError,
|
|
GAPI.noManageTeamDriveAdministratorPrivilege) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
|
|
except GAPI.fileNotFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId,
|
|
Ent.DRIVE_FILE, body.get('backgroundImageFile', {}).get('id', UNKNOWN)],
|
|
str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
Act.Set(Act.UPDATE)
|
|
|
|
def doUpdateSharedDrive():
|
|
updateSharedDrive([_getAdminEmail()], True)
|
|
|
|
# gam <UserTypeEntity> delete shareddrive <SharedDriveEntity>
|
|
# [asadmin [allowitemdeletion]
|
|
def deleteSharedDrive(users):
|
|
fileIdEntity = getSharedDriveEntity()
|
|
allowItemDeletion = useDomainAdminAccess = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'nukefromorbit', 'allowitemdeletion'}:
|
|
allowItemDeletion = useDomainAdminAccess = True
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
try:
|
|
driveId = fileIdEntity['shareddrive']['driveId']
|
|
callGAPI(drive.drives(), 'delete',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN,
|
|
GAPI.CANNOT_DELETE_RESOURCE_WITH_CHILDREN, GAPI.INSUFFICIENT_FILE_PERMISSIONS,
|
|
GAPI.NO_MANAGE_TEAMDRIVE_ADMINISTRATOR_PRIVILEGE],
|
|
driveId=driveId, allowItemDeletion=allowItemDeletion, useDomainAdminAccess=useDomainAdminAccess)
|
|
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
|
|
except (GAPI.notFound, GAPI.forbidden,
|
|
GAPI.cannotDeleteResourceWithChildren, GAPI.insufficientFilePermissions,
|
|
GAPI.noManageTeamDriveAdministratorPrivilege) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
|
|
# gam delete shareddrive <SharedDriveEntity> [allowitemdeletion]
|
|
def doDeleteSharedDrive():
|
|
deleteSharedDrive([_getAdminEmail()])
|
|
|
|
# gam <UserTypeEntity> hide/unhide shareddrive <SharedDriveEntity>
|
|
def hideUnhideSharedDrive(users):
|
|
fileIdEntity = getSharedDriveEntity()
|
|
checkForExtraneousArguments()
|
|
function = 'hide' if Act.Get() == Act.HIDE else 'unhide'
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity)
|
|
if not drive:
|
|
continue
|
|
try:
|
|
driveId = fileIdEntity['shareddrive']['driveId']
|
|
callGAPI(drive.drives(), function,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN],
|
|
driveId=driveId)
|
|
entityActionPerformed([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], i, count)
|
|
except (GAPI.notFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
|
|
# gam hide/unhide shareddrive <SharedDriveEntity>
|
|
def doHideUnhideSharedDrive():
|
|
hideUnhideSharedDrive([_getAdminEmail()])
|
|
|
|
SHAREDDRIVE_FIELDS_CHOICE_MAP = {
|
|
'backgroundimagefile': 'backgroundImageFile',
|
|
'backgroundimagelink': 'backgroundImageLink',
|
|
'capabilities': 'capabilities',
|
|
'colorrgb': 'colorRgb',
|
|
'createddate': 'createdTime',
|
|
'createdtime': 'createdTime',
|
|
'id': 'id',
|
|
'hidden': 'hidden',
|
|
'name': 'name',
|
|
'restrictions': 'restrictions',
|
|
'themeid': 'themeId',
|
|
}
|
|
SHAREDDRIVE_LIST_FIELDS_CHOICE_MAP = {
|
|
'org': 'orgUnitId',
|
|
'orgunit': 'orgUnitId',
|
|
'orgunitid': 'orgUnitId',
|
|
'ou': 'orgUnitId',
|
|
}
|
|
SHAREDDRIVE_TIME_OBJECTS = {'createdTime'}
|
|
SHAREDDRIVE_ROLES_CAPABILITIES_MAP = {
|
|
'commenter': {'canComment': True, 'canEdit': False},
|
|
'reader': {'canComment': False, 'canEdit': False},
|
|
'writer': {'canEdit': True, 'canTrashChildren': False},
|
|
'fileOrganizer': {'canTrashChildren': True, 'canManageMembers': False},
|
|
'organizer': {'canManageMembers': True},
|
|
}
|
|
SHAREDDRIVE_API_GUI_ROLES_MAP = {
|
|
'commenter': 'Commenter',
|
|
'fileOrganizer': 'Content manager',
|
|
'organizer': 'Manager',
|
|
'reader': 'Viewer',
|
|
'writer': 'Contributor',
|
|
'unknown': 'Unknown'
|
|
}
|
|
|
|
def _getSharedDriveRole(shareddrive):
|
|
if 'capabilities' not in shareddrive:
|
|
return None
|
|
for role, capabilities in iter(SHAREDDRIVE_ROLES_CAPABILITIES_MAP.items()):
|
|
match = True
|
|
for capability in capabilities:
|
|
if capabilities[capability] != shareddrive['capabilities'].get(capability, ''):
|
|
match = False
|
|
break
|
|
if match:
|
|
break
|
|
else:
|
|
role = 'unknown'
|
|
return role
|
|
|
|
def _showSharedDrive(user, shareddrive, j, jcount, FJQC):
|
|
def _showCapabilitiesRestrictions(field):
|
|
if field in shareddrive:
|
|
printKeyValueList([field, ''])
|
|
Ind.Increment()
|
|
for capability in sorted(shareddrive[field]):
|
|
printKeyValueList([capability, shareddrive[field][capability]])
|
|
Ind.Decrement()
|
|
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(shareddrive, timeObjects=SHAREDDRIVE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.USER, user, Ent.SHAREDDRIVE, f'{shareddrive["name"]} ({shareddrive["id"]})'], j, jcount)
|
|
Ind.Increment()
|
|
printEntity([Ent.SHAREDDRIVE_ID, shareddrive['id']])
|
|
printEntity([Ent.SHAREDDRIVE_NAME, shareddrive['name']])
|
|
if 'hidden' in shareddrive:
|
|
printKeyValueList(['hidden', shareddrive['hidden']])
|
|
if 'createdTime' in shareddrive:
|
|
printKeyValueList(['createdTime', formatLocalTime(shareddrive['createdTime'])])
|
|
for setting in ['backgroundImageLink', 'colorRgb', 'themeId', 'orgUnit', 'orgUnitId']:
|
|
if setting in shareddrive:
|
|
printKeyValueList([setting, shareddrive[setting]])
|
|
if 'role' in shareddrive:
|
|
printKeyValueList(['role', shareddrive['role']])
|
|
_showCapabilitiesRestrictions('capabilities')
|
|
_showCapabilitiesRestrictions('restrictions')
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> info shareddrive <SharedDriveEntity>
|
|
# [asadmin]
|
|
# [fields <SharedDriveFieldNameList>]
|
|
# [guiroles [<Boolean>]] [formatjson]
|
|
def infoSharedDrive(users, useDomainAdminAccess=False):
|
|
fileIdEntity = getSharedDriveEntity()
|
|
fieldsList = []
|
|
FJQC = FormatJSONQuoteChar()
|
|
guiRoles = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
elif getFieldsList(myarg, SHAREDDRIVE_FIELDS_CHOICE_MAP, fieldsList, initialField=['id', 'name']):
|
|
pass
|
|
elif myarg == 'guiroles':
|
|
guiRoles = getBoolean()
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList) if fieldsList else '*'
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, fileIdEntity, useDomainAdminAccess=useDomainAdminAccess)
|
|
if not drive:
|
|
continue
|
|
try:
|
|
driveId = fileIdEntity['shareddrive']['driveId']
|
|
shareddrive = callGAPI(drive.drives(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
driveId=driveId, fields=fields)
|
|
role = _getSharedDriveRole(shareddrive)
|
|
if role:
|
|
shareddrive['role'] = role if not guiRoles else SHAREDDRIVE_API_GUI_ROLES_MAP[role]
|
|
_showSharedDrive(user, shareddrive, i, count, FJQC)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
|
|
def doInfoSharedDrive():
|
|
infoSharedDrive([_getAdminEmail()], True)
|
|
|
|
SHAREDDRIVE_ACL_ROLES_MAP = {
|
|
'commenter': 'commenter',
|
|
'contentmanager': 'fileOrganizer',
|
|
'contributor': 'writer',
|
|
'editor': 'writer',
|
|
'fileorganizer': 'fileOrganizer',
|
|
'fileorganiser': 'fileOrganizer',
|
|
'manager': 'organizer',
|
|
'organizer': 'organizer',
|
|
'organiser': 'organizer',
|
|
'owner': 'organizer',
|
|
'read': 'reader',
|
|
'reader': 'reader',
|
|
'viewer': 'reader',
|
|
'writer': 'writer',
|
|
}
|
|
|
|
# gam <UserTypeEntity> print shareddrives [todrive <ToDriveAttribute>*]
|
|
# [asadmin [shareddriveadminquery|query <QuerySharedDrive>]]
|
|
# [matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
|
|
# (role|roles <SharedDriveACLRoleList>)*
|
|
# [fields <SharedDriveFieldNameList>] [noorgunits [<Boolean>]]
|
|
# [guiroles [<Boolean>]] [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show shareddrives
|
|
# [asadmin [shareddriveadminquery|query <QuerySharedDrive>]]
|
|
# [matchname <REMatchPattrn>] [orgunit|org|ou <OrgUnitPath>]
|
|
# (role|roles <SharedDriveACLRoleLIst>)*
|
|
# [fields <SharedDriveFieldNameList>] [noorgunits [<Boolean>]]
|
|
# [guiroles [<Boolean>]] [formatjson]
|
|
def printShowSharedDrives(users, useDomainAdminAccess=False):
|
|
def stripNonShowFields(shareddrive):
|
|
if orgUnitIdToPathMap:
|
|
td_ouid = shareddrive.get('orgUnitId')
|
|
if td_ouid:
|
|
shareddrive['orgUnit'] = orgUnitIdToPathMap.get(f'id:{td_ouid}', UNKNOWN)
|
|
if not showFields:
|
|
return shareddrive
|
|
sshareddrive = {}
|
|
for field in showFields:
|
|
if field in shareddrive:
|
|
sshareddrive[field] = shareddrive[field]
|
|
return sshareddrive
|
|
|
|
csvPF = CSVPrintFile(['User', 'id', 'name'], ['User', 'id', 'name', 'role']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
roles = set()
|
|
cd = orgUnitId = query = matchPattern = None
|
|
showFields = set()
|
|
fieldsList = []
|
|
SHAREDDRIVE_FIELDS_CHOICE_MAP.update(SHAREDDRIVE_LIST_FIELDS_CHOICE_MAP)
|
|
showOrgUnitPaths = True
|
|
orgUnitIdToPathMap = None
|
|
guiRoles = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'teamdriveadminquery', 'shareddriveadminquery', 'query'}:
|
|
queryLocation = Cmd.Location()
|
|
query = getString(Cmd.OB_QUERY, minLen=0) or None
|
|
if query:
|
|
query = mapQueryRelativeTimes(query, ['createdTime'])
|
|
elif myarg == 'matchname':
|
|
matchPattern = getREPattern(re.IGNORECASE)
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
orgLocation = Cmd.Location()
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
_, orgUnitId = getOrgUnitId(cd)
|
|
orgUnitId = orgUnitId[3:]
|
|
elif myarg in {'role', 'roles'}:
|
|
roles |= getACLRoles(SHAREDDRIVE_ACL_ROLES_MAP)
|
|
elif myarg == 'checkgroups':
|
|
pass
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
elif getFieldsList(myarg, SHAREDDRIVE_FIELDS_CHOICE_MAP, fieldsList, initialField=['id', 'name']):
|
|
pass
|
|
elif myarg == 'noorgunits':
|
|
showOrgUnitPaths = not getBoolean()
|
|
elif myarg == 'guiroles':
|
|
guiRoles = getBoolean()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if query and not useDomainAdminAccess:
|
|
Cmd.SetLocation(queryLocation-1)
|
|
usageErrorExit(Msg.ONLY_ADMINISTRATORS_CAN_PERFORM_SHARED_DRIVE_QUERIES)
|
|
if orgUnitId is not None and not useDomainAdminAccess:
|
|
Cmd.SetLocation(orgLocation-1)
|
|
usageErrorExit(Msg.ONLY_ADMINISTRATORS_CAN_SPECIFY_SHARED_DRIVE_ORGUNIT)
|
|
if fieldsList:
|
|
showFields = set(fieldsList)
|
|
if csvPF and not useDomainAdminAccess:
|
|
if fieldsList:
|
|
showFields.add('role')
|
|
csvPF.AddTitle('role')
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles(['role'])
|
|
csvPF.MoveJSONTitlesToEnd(['JSON'])
|
|
if showOrgUnitPaths and useDomainAdminAccess and ((not showFields) or ('orgUnitId' in showFields)):
|
|
orgUnitIdToPathMap = getOrgUnitIdToPathMap(cd)
|
|
if showFields:
|
|
showFields.add('orgUnit')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
if useDomainAdminAccess:
|
|
printGettingAllAccountEntities(Ent.SHAREDDRIVE, query)
|
|
pageMessage = getPageMessage()
|
|
else:
|
|
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, user, i, count, query)
|
|
pageMessage = getPageMessageForWhom()
|
|
try:
|
|
feed = callGAPIpages(drive.drives(), 'list', 'drives',
|
|
pageMessage=pageMessage,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.QUERY_REQUIRES_ADMIN_CREDENTIALS,
|
|
GAPI.NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE,
|
|
GAPI.FILE_NOT_FOUND],
|
|
q=query, useDomainAdminAccess=useDomainAdminAccess,
|
|
fields='*', pageSize=100)
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.queryRequiresAdminCredentials,
|
|
GAPI.noListTeamDrivesAdministratorPrivilege, GAPI.fileNotFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, None], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
matchedFeed = []
|
|
if not useDomainAdminAccess:
|
|
for shareddrive in feed:
|
|
if matchPattern is not None and matchPattern.match(shareddrive['name']) is None:
|
|
continue
|
|
role = _getSharedDriveRole(shareddrive)
|
|
if not roles or role in roles:
|
|
shareddrive['role'] = role if not guiRoles else SHAREDDRIVE_API_GUI_ROLES_MAP[role]
|
|
matchedFeed.append(shareddrive)
|
|
elif matchPattern is not None or orgUnitId is not None:
|
|
for shareddrive in feed:
|
|
if ((matchPattern is not None and matchPattern.match(shareddrive['name']) is None) or
|
|
(orgUnitId is not None and orgUnitId != shareddrive.get('orgUnitId'))):
|
|
continue
|
|
matchedFeed.append(shareddrive)
|
|
else:
|
|
matchedFeed = feed
|
|
jcount = len(matchedFeed)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.SHAREDDRIVE, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for shareddrive in matchedFeed:
|
|
j += 1
|
|
shareddrive = stripNonShowFields(shareddrive)
|
|
_showSharedDrive(user, shareddrive, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
for shareddrive in matchedFeed:
|
|
shareddrive = stripNonShowFields(shareddrive)
|
|
if FJQC.formatJSON:
|
|
row = {'User': user, 'id': shareddrive['id'], 'name': shareddrive['name']}
|
|
if not useDomainAdminAccess:
|
|
row['role'] = shareddrive['role'] if not guiRoles else SHAREDDRIVE_API_GUI_ROLES_MAP[shareddrive['role']]
|
|
row['JSON'] = json.dumps(cleanJSON(shareddrive, timeObjects=SHAREDDRIVE_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRow(row)
|
|
else:
|
|
csvPF.WriteRowTitles(flattenJSON(shareddrive, flattened={'User': user}, timeObjects=SHAREDDRIVE_TIME_OBJECTS))
|
|
if csvPF:
|
|
csvPF.writeCSVfile('SharedDrives')
|
|
|
|
def doPrintShowSharedDrives():
|
|
printShowSharedDrives([_getAdminEmail()], True)
|
|
|
|
# gam print oushareddrives [todrive <ToDriveAttribute>*]
|
|
# [ou|org|orgunit <OrgUnitPath>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show oushareddrives
|
|
# [ou|org|orgunit <OrgUnitPath>]
|
|
# [formatjson]
|
|
def doPrintShowOrgunitSharedDrives():
|
|
def _getOrgUnitSharedDriveInfo(shareddrive):
|
|
shareddrive['driveId'] = shareddrive['name'].rsplit(';')[1]
|
|
shareddrive['driveName'] = _getSharedDriveNameFromId(shareddrive['driveId'])
|
|
shareddrive['orgUnitPath'] = orgUnitPath
|
|
|
|
def _showOrgUnitSharedDrive(shareddrive, j, jcount, FJQC):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(shareddrive), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.NAME, f'{shareddrive["name"]}'], j, jcount)
|
|
Ind.Increment()
|
|
printEntity([Ent.TYPE, shareddrive['type']])
|
|
printEntity([Ent.MEMBER, shareddrive['member']])
|
|
printEntity([Ent.MEMBER_URI, shareddrive['memberUri']])
|
|
printEntity([Ent.SHAREDDRIVE_ID, shareddrive['driveId']])
|
|
printEntity([Ent.SHAREDDRIVE_NAME, shareddrive['driveName']])
|
|
printEntity([Ent.ORGANIZATIONAL_UNIT, shareddrive['orgUnit']])
|
|
Ind.Decrement()
|
|
|
|
ci = buildGAPIObject(API.CLOUDIDENTITY_ORGUNITS_BETA)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
_, GM.Globals[GM.ADMIN_DRIVE] = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
|
|
if not GM.Globals[GM.ADMIN_DRIVE]:
|
|
return
|
|
csvPF = CSVPrintFile(['name', 'type', 'member', 'memberUri', 'driveId', 'driveName', 'orgUnitPath']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
orgUnitPath = '/'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
orgUnitPath = getString(Cmd.OB_ORGUNIT_ITEM)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(['name', 'JSON'])
|
|
orgUnitPath, orgUnitId = getOrgUnitId(cd, orgUnitPath)
|
|
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, orgUnitPath, entityType=Ent.ORGANIZATIONAL_UNIT)
|
|
sds = callGAPIpages(ci.orgUnits().memberships(), 'list', 'orgMemberships',
|
|
pageMessage=getPageMessageForWhom(),
|
|
parent=f'orgUnits/{orgUnitId[3:]}',
|
|
customer=_getCustomersCustomerIdWithC(),
|
|
filter="type == 'shared_drive'")
|
|
jcount = len(sds)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.SHAREDDRIVE)
|
|
Ind.Increment()
|
|
j = 0
|
|
for shareddrive in sds:
|
|
j += 1
|
|
_getOrgUnitSharedDriveInfo(shareddrive)
|
|
_showOrgUnitSharedDrive(shareddrive, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
for shareddrive in sds:
|
|
_getOrgUnitSharedDriveInfo(shareddrive)
|
|
if FJQC.formatJSON:
|
|
row = {'name': shareddrive['name']}
|
|
row['JSON'] = json.dumps(cleanJSON(shareddrive), ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRow(row)
|
|
else:
|
|
csvPF.WriteRowTitles(flattenJSON(shareddrive))
|
|
if csvPF:
|
|
csvPF.writeCSVfile('OrgUnit {orgUnitPath} SharedDrives')
|
|
|
|
# gam [<UserTypeEntity>] copy shareddriveacls <SharedDriveEntity> to <SharedDriveEntity>
|
|
# gam [<UserTypeEntity>] sync shareddriveacls <SharedDriveEntity> with <SharedDriveEntity>
|
|
# [asadmin]
|
|
# [showpermissionsmessages [<Boolean>]]
|
|
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
|
# (mappermissionsdomain <DomainName> <DomainName>)*
|
|
def copySyncSharedDriveACLs(users, useDomainAdminAccess=False):
|
|
copyMoveOptions = initCopyMoveOptions(True)
|
|
srcFileIdEntity = getSharedDriveEntity()
|
|
checkArgumentPresent(['to', 'with'], True)
|
|
tgtFileIdEntity = getSharedDriveEntity()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getCopyMoveOptions(myarg, copyMoveOptions):
|
|
pass
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
else:
|
|
unknownArgumentExit()
|
|
copyMoveOptions['useDomainAdminAccess'] = useDomainAdminAccess
|
|
copyMoveOptions['copyTopFolderNonInheritedPermissions'] =\
|
|
COPY_NONINHERITED_PERMISSIONS_ALWAYS if Act.Get() == Act.COPY else COPY_NONINHERITED_PERMISSIONS_SYNC_ALL_FOLDERS
|
|
copyMoveOptions['copyMergeWithParentFolderPermissions'] = True
|
|
copyMoveOptions['copyTopFolderInheritedPermissions'] = False
|
|
copyMoveOptions['destParentType'] = DEST_PARENT_SHAREDDRIVE_ROOT
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = _validateUserSharedDrive(user, i, count, srcFileIdEntity)
|
|
if not drive:
|
|
continue
|
|
if not srcFileIdEntity.get('shareddrivename'):
|
|
srcFileIdEntity['shareddrivename'] = _getSharedDriveNameFromId(srcFileIdEntity['shareddrive']['driveId'])
|
|
if tgtFileIdEntity.get('shareddrivename'):
|
|
if not _convertSharedDriveNameToId(drive, user, i, count, tgtFileIdEntity, useDomainAdminAccess):
|
|
continue
|
|
tgtFileIdEntity['shareddrive']['corpora'] = 'drive'
|
|
else:
|
|
tgtFileIdEntity['shareddrivename'] = _getSharedDriveNameFromId(tgtFileIdEntity['shareddrive']['driveId'])
|
|
statistics = _initStatistics()
|
|
copyMoveOptions['sourceDriveId'] = srcFileIdEntity['shareddrive']['driveId']
|
|
copyMoveOptions['destDriveId'] = tgtFileIdEntity['shareddrive']['driveId']
|
|
entityPerformActionModifierItemValueList([Ent.USER, user, Ent.SHAREDDRIVE, srcFileIdEntity['shareddrivename']],
|
|
f"{Ent.Plural(Ent.PERMISSION)} {Act.MODIFIER_TO}",
|
|
[Ent.SHAREDDRIVE, tgtFileIdEntity['shareddrivename']], i, count)
|
|
_copyPermissions(drive, user, i, count, 0, 0,
|
|
Ent.SHAREDDRIVE, srcFileIdEntity['shareddrive']['driveId'], srcFileIdEntity['shareddrivename'],
|
|
tgtFileIdEntity['shareddrive']['driveId'], tgtFileIdEntity['shareddrivename'],
|
|
statistics, STAT_FOLDER_PERMISSIONS_FAILED,
|
|
copyMoveOptions, True,
|
|
'copyTopFolderInheritedPermissions',
|
|
'copyTopFolderNonInheritedPermissions',
|
|
False)
|
|
|
|
def doCopySyncSharedDriveACLs():
|
|
copySyncSharedDriveACLs([_getAdminEmail()], True)
|
|
|
|
SHOW_NO_PERMISSIONS_DRIVES_CHOICE_MAP = {
|
|
'true': 1,
|
|
'false': 0,
|
|
'only': -1,
|
|
}
|
|
|
|
# gam [<UserTypeEntity>] print shareddriveacls [todrive <ToDriveAttribute>*]
|
|
# [asadmin] [shareddriveadminquery|query <QuerySharedDrive>]
|
|
# [matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
|
|
# [user|group <EmailAddress> [checkgroups]] (role|roles <SharedDriveACLRoleList>)*
|
|
# <PermissionMatch>* [<PermissionMatchAction>] [pmselect]
|
|
# [oneitemperrow] [maxitems <Integer>]
|
|
# [shownopermissionsdrives false|true|only]
|
|
# [<DrivePermissionsFieldName>*|(fields <DrivePermissionsFieldNameList>)]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam [<UserTypeEntity>] show shareddriveacls
|
|
# [asadmin] [shareddriveadminquery|query <QuerySharedDrive>]
|
|
# [matchname <REMatchPattern>] [orgunit|org|ou <OrgUnitPath>]
|
|
# [user|group <EmailAddress> [checkgroups]] (role|roles <SharedDriveACLRoleList>)*
|
|
# <PermissionMatch>* [<PermissionMatchAction>] [pmselect]
|
|
# [oneitemperrow] [maxitems <Integer>]
|
|
# [shownopermissionsdrives false|true|only]
|
|
# [<DrivePermissionsFieldName>*|(fields <DrivePermissionsFieldNameList>)]
|
|
# [formatjson]
|
|
def printShowSharedDriveACLs(users, useDomainAdminAccess=False):
|
|
def _printPermissionRow(baserow, permission):
|
|
row = baserow.copy()
|
|
flattenJSON({'permission': permission}, flattened=row, timeObjects=timeObjects)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = baserow.copy()
|
|
row['JSON'] = json.dumps(cleanJSON({'permission': permission}, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
|
|
csvPF = CSVPrintFile(['User', 'id', 'name', 'createdTime'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
roles = set()
|
|
checkGroups = oneItemPerRow = pmselect = False
|
|
showNoPermissionsDrives = SHOW_NO_PERMISSIONS_DRIVES_CHOICE_MAP['false']
|
|
fieldsList = []
|
|
cd = emailAddress = orgUnitId = query = matchPattern = permtype = None
|
|
PM = PermissionMatch()
|
|
maxItems = 0
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'teamdriveadminquery', 'shareddriveadminquery', 'query'}:
|
|
queryLocation = Cmd.Location()
|
|
query = getString(Cmd.OB_QUERY, minLen=0) or None
|
|
if query:
|
|
query = mapQueryRelativeTimes(query, ['createdTime'])
|
|
elif myarg == 'matchname':
|
|
matchPattern = getREPattern(re.IGNORECASE)
|
|
elif myarg in {'ou', 'org', 'orgunit'}:
|
|
orgLocation = Cmd.Location()
|
|
if cd is None:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
_, orgUnitId = getOrgUnitId(cd)
|
|
orgUnitId = orgUnitId[3:]
|
|
elif myarg in {'user', 'group'}:
|
|
permtype = myarg
|
|
emailAddress = getEmailAddress(noUid=True)
|
|
elif myarg in {'role', 'roles'}:
|
|
roles |= getACLRoles(SHAREDDRIVE_ACL_ROLES_MAP)
|
|
elif myarg == 'checkgroups':
|
|
checkGroups = True
|
|
elif myarg == 'oneitemperrow':
|
|
oneItemPerRow = True
|
|
elif myarg == 'maxitems':
|
|
maxItems = getInteger(minVal=0)
|
|
elif getDriveFilePermissionsFields(myarg, fieldsList):
|
|
pass
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
elif PM.ProcessArgument(myarg):
|
|
pass
|
|
elif myarg == 'pmselect':
|
|
pmselect = True
|
|
elif myarg == 'shownopermissionsdrives':
|
|
showNoPermissionsDrives = getChoice(SHOW_NO_PERMISSIONS_DRIVES_CHOICE_MAP, mapChoice=True)
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if query and not useDomainAdminAccess:
|
|
Cmd.SetLocation(queryLocation-1)
|
|
usageErrorExit(Msg.ONLY_ADMINISTRATORS_CAN_PERFORM_SHARED_DRIVE_QUERIES)
|
|
if orgUnitId is not None and not useDomainAdminAccess:
|
|
Cmd.SetLocation(orgLocation-1)
|
|
usageErrorExit(Msg.ONLY_ADMINISTRATORS_CAN_SPECIFY_SHARED_DRIVE_ORGUNIT)
|
|
if fieldsList:
|
|
if permtype is not None:
|
|
fieldsList.extend(['type', 'emailAddress'])
|
|
if roles:
|
|
fieldsList.append('role')
|
|
fields = getItemFieldsFromFieldsList('permissions', fieldsList, True)
|
|
printKeys, timeObjects = _getDriveFileACLPrintKeysTimeObjects()
|
|
if checkGroups:
|
|
if emailAddress:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
try:
|
|
groups = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
userKey=emailAddress, orderBy='email', fields='nextPageToken,groups(email)')
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
badRequestWarning(Ent.GROUP, Ent.MEMBER, emailAddress)
|
|
return
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
groupsSet = {group['email'] for group in groups}
|
|
else:
|
|
checkGroups = False
|
|
if csvPF and addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles(sorted(addCSVData.keys()))
|
|
csvPF.MoveJSONTitlesToEnd(['JSON'])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
feed = None
|
|
if permtype == 'user':
|
|
_, userdrive = buildGAPIServiceObject(API.DRIVE3, emailAddress, displayError=False)
|
|
if userdrive is not None:
|
|
try:
|
|
feed = callGAPIpages(userdrive.drives(), 'list', 'drives',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
fields='nextPageToken,drives(id,name,createdTime,orgUnitId)', pageSize=100)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
pass
|
|
if feed is None:
|
|
if useDomainAdminAccess:
|
|
printGettingAllAccountEntities(Ent.SHAREDDRIVE, query)
|
|
pageMessage = getPageMessage()
|
|
else:
|
|
printGettingAllEntityItemsForWhom(Ent.SHAREDDRIVE, user, i, count, query)
|
|
pageMessage = getPageMessageForWhom()
|
|
try:
|
|
feed = callGAPIpages(drive.drives(), 'list', 'drives',
|
|
pageMessage=pageMessage,
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.INVALID_QUERY, GAPI.INVALID,
|
|
GAPI.QUERY_REQUIRES_ADMIN_CREDENTIALS,
|
|
GAPI.NO_LIST_TEAMDRIVES_ADMINISTRATOR_PRIVILEGE],
|
|
q=query, useDomainAdminAccess=useDomainAdminAccess,
|
|
fields='nextPageToken,drives(id,name,createdTime,orgUnitId)', pageSize=100)
|
|
except (GAPI.invalidQuery, GAPI.invalid, GAPI.queryRequiresAdminCredentials, GAPI.noListTeamDrivesAdministratorPrivilege) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE, None], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
continue
|
|
matchFeed = []
|
|
jcount = len(feed)
|
|
j = 0
|
|
for shareddrive in feed:
|
|
j += 1
|
|
if ((matchPattern is not None and matchPattern.match(shareddrive['name']) is None) or
|
|
(orgUnitId is not None and orgUnitId != shareddrive.get('orgUnitId'))):
|
|
continue
|
|
printGettingAllEntityItemsForWhom(Ent.PERMISSION, shareddrive['name'], j, jcount)
|
|
shareddrive['createdTime'] = formatLocalTime(shareddrive['createdTime'])
|
|
shareddrive['permissions'] = []
|
|
try:
|
|
permissions = callGAPIpages(drive.permissions(), 'list', 'permissions',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.DRIVE3_GET_ACL_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
fileId=shareddrive['id'], fields=fields, supportsAllDrives=True)
|
|
if not permissions:
|
|
if showNoPermissionsDrives == 0: # no permissions and showNoPermissionDrives False - ignore
|
|
continue
|
|
matchFeed.append(shareddrive) # no permissions and showNoPermissionDrives Only/True - keep
|
|
continue
|
|
if showNoPermissionsDrives < 0: # permissions and showNoPermissionDrives Only/True - ignore
|
|
continue
|
|
if pmselect:
|
|
if not PM.CheckPermissionMatches(permissions):
|
|
continue
|
|
else:
|
|
permissions = PM.GetMatchingPermissions(permissions)
|
|
for permission in permissions:
|
|
if roles and permission['role'] not in roles:
|
|
continue
|
|
permission.pop('teamDrivePermissionDetails', None)
|
|
if permtype is None:
|
|
shareddrive['permissions'].append(permission)
|
|
elif permission['type'] == permtype and permission['emailAddress'] == emailAddress:
|
|
shareddrive['permissions'].append(permission)
|
|
elif checkGroups and permission['emailAddress'] in groupsSet:
|
|
shareddrive['permissions'].append(permission)
|
|
if shareddrive['permissions']:
|
|
numItems = len(shareddrive['permissions'])
|
|
if numItems > maxItems > 0:
|
|
shareddrive['permissions'] = shareddrive['permissions'][0:maxItems]
|
|
matchFeed.append(shareddrive)
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientAdministratorPrivileges, GAPI.insufficientFilePermissions,
|
|
GAPI.unknownError, GAPI.invalid):
|
|
pass
|
|
jcount = len(matchFeed)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.SHAREDDRIVE, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for shareddrive in matchFeed:
|
|
j += 1
|
|
if not FJQC.formatJSON:
|
|
_showDriveFilePermissions(Ent.SHAREDDRIVE, f'{shareddrive["name"]} ({shareddrive["id"]}) - {shareddrive["createdTime"]}',
|
|
shareddrive['permissions'], printKeys, timeObjects, j, jcount)
|
|
else:
|
|
if oneItemPerRow and shareddrive['permissions']:
|
|
for permission in shareddrive['permissions']:
|
|
_showDriveFilePermissionJSON(user, shareddrive['id'], shareddrive['name'], shareddrive['createdTime'], permission, timeObjects)
|
|
else:
|
|
_showDriveFilePermissionsJSON(user, shareddrive['id'], shareddrive['name'], shareddrive['createdTime'], shareddrive['permissions'], timeObjects)
|
|
Ind.Decrement()
|
|
elif matchFeed:
|
|
if oneItemPerRow:
|
|
for shareddrive in matchFeed:
|
|
baserow = {'User': user, 'id': shareddrive['id'], 'name': shareddrive['name'], 'createdTime': shareddrive['createdTime']}
|
|
if addCSVData:
|
|
baserow.update(addCSVData)
|
|
if shareddrive['permissions']:
|
|
for permission in shareddrive['permissions']:
|
|
_mapDrivePermissionNames(permission)
|
|
pdetails = permission.pop('permissionDetails', [])
|
|
if not pdetails:
|
|
_printPermissionRow(baserow, permission)
|
|
else:
|
|
for pdetail in pdetails:
|
|
permission['permissionDetails'] = pdetail
|
|
_printPermissionRow(baserow, permission)
|
|
else:
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(baserow)
|
|
elif csvPF.CheckRowTitles(baserow):
|
|
baserow['JSON'] = json.dumps({})
|
|
csvPF.WriteRowNoFilter(baserow)
|
|
else:
|
|
for shareddrive in matchFeed:
|
|
baserow = {'User': user, 'id': shareddrive['id'], 'name': shareddrive['name'], 'createdTime': shareddrive['createdTime']}
|
|
if addCSVData:
|
|
baserow.update(addCSVData)
|
|
row = baserow.copy()
|
|
if shareddrive['permissions']:
|
|
for permission in shareddrive['permissions']:
|
|
_mapDrivePermissionNames(permission)
|
|
flattenJSON({'permissions': shareddrive['permissions']}, flattened=row, timeObjects=timeObjects)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
baserow['JSON'] = json.dumps(cleanJSON({'permissions': shareddrive['permissions']}, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(baserow)
|
|
else:
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(baserow)
|
|
elif csvPF.CheckRowTitles(baserow):
|
|
baserow['JSON'] = json.dumps({})
|
|
csvPF.WriteRowNoFilter(baserow)
|
|
if csvPF:
|
|
if not oneItemPerRow:
|
|
csvPF.SetIndexedTitles(['permissions'])
|
|
csvPF.writeCSVfile('SharedDrive ACLs')
|
|
|
|
def doPrintShowSharedDriveACLs():
|
|
printShowSharedDriveACLs([_getAdminEmail()], True)
|
|
|
|
LOOKERSTUDIO_ASSETTYPE_CHOICE_MAP = {
|
|
'report': ['REPORT'],
|
|
'datasource': ['DATA_SOURCE'],
|
|
'all': ['REPORT', 'DATA_SOURCE'],
|
|
}
|
|
|
|
def initLookerStudioAssetSelectionParameters():
|
|
return ({'owner': None, 'title': None, 'includeTrashed': False}, {'assetTypes': ['REPORT']})
|
|
|
|
def getLookerStudioAssetSelectionParameters(myarg, parameters, assetTypes):
|
|
if myarg in {'assettype', 'assettypes'}:
|
|
assetTypes['assetTypes'] = getChoice(LOOKERSTUDIO_ASSETTYPE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'title':
|
|
parameters['title'] = getString(Cmd.OB_STRING)
|
|
elif myarg == 'owner':
|
|
parameters['owner'] = getEmailAddress(noUid=True)
|
|
elif myarg == 'includetrashed':
|
|
parameters['includeTrashed'] = True
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _validateUserGetLookerStudioAssetIds(user, i, count, entity):
|
|
if entity:
|
|
if entity['dict']:
|
|
entityList = [{'name': item, 'title': item} for item in entity['dict'][user]]
|
|
else:
|
|
entityList = [{'name': item, 'title': item} for item in entity['list']]
|
|
else:
|
|
entityList = []
|
|
user, ds = buildGAPIServiceObject(API.LOOKERSTUDIO, user, i, count)
|
|
if not ds:
|
|
return (user, None, None, 0)
|
|
return (user, ds, entityList, len(entityList))
|
|
|
|
def _getLookerStudioAssetByID(ds, user, i, count, assetId):
|
|
printGettingAllEntityItemsForWhom(Ent.LOOKERSTUDIO_ASSET, user, i, count)
|
|
try:
|
|
return callGAPI(ds.assets(), 'get',
|
|
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
|
|
name=f'assets/{assetId}')
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userLookerStudioServiceNotEnabledWarning(user, i, count)
|
|
return None
|
|
|
|
def _getLookerStudioAssets(ds, user, i, count, parameters, assetTypes, fields, orderBy=None):
|
|
assets = []
|
|
for assetType in assetTypes['assetTypes']:
|
|
entityType = Ent.LOOKERSTUDIO_ASSET_REPORT if assetType == 'REPORT' else Ent.LOOKERSTUDIO_ASSET_DATASOURCE
|
|
printGettingAllEntityItemsForWhom(entityType, user, i, count)
|
|
parameters['assetTypes'] = assetType
|
|
try:
|
|
assets.extend(callGAPIpages(ds.assets(), 'search', 'assets',
|
|
pageMessage=getPageMessage(),
|
|
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
|
|
**parameters, orderBy=orderBy, fields=fields))
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
return (None, 0)
|
|
except GAPI.serviceNotAvailable:
|
|
userLookerStudioServiceNotEnabledWarning(user, i, count)
|
|
return (None, 0)
|
|
return (assets, len(assets))
|
|
|
|
LOOKERSTUDIO_ASSETS_ORDERBY_CHOICE_MAP = {
|
|
'title': 'title'
|
|
}
|
|
LOOKERSTUDIO_ASSETS_TIME_OBJECTS = {'updateTime', 'updateByMeTime', 'createTime', 'lastViewByMeTime'}
|
|
|
|
# gam <UserTypeEntity> print lookerstudioassets [todrive <ToDriveAttribute>*]
|
|
# [([assettype report|datasource|all] [title <String>]
|
|
# [owner <Emailddress>] [includetrashed]
|
|
# [orderby title [ascending|descending]]) |
|
|
# (assetids <LookerStudioAssetIDEntity>)]
|
|
# [stripcrsfromtitle]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show lookerstudioassets
|
|
# [([assettype report|datasource|all] [title <String>]
|
|
# [owner <Emailddress>] [includetrashed]
|
|
# [orderby title [ascending|descending]]) |
|
|
# (assetids <LookerStudioAssetIDEntity>)]
|
|
# [stripcrsfromtitle]
|
|
# [formatjson]
|
|
def printShowLookerStudioAssets(users):
|
|
def _printAsset(asset, user):
|
|
if stripCRsFromTitle:
|
|
asset['title'] = _stripControlCharsFromName(asset['title'])
|
|
row = flattenJSON(asset, flattened={'User': user})
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'User': user, 'title': asset['title'],
|
|
'JSON': json.dumps(cleanJSON(asset, timeObjects=LOOKERSTUDIO_ASSETS_TIME_OBJECTS), ensure_ascii=False, sort_keys=True)})
|
|
|
|
def _showAsset(asset):
|
|
if stripCRsFromTitle:
|
|
asset['title'] = _stripControlCharsFromName(asset['title'])
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(asset, timeObjects=LOOKERSTUDIO_ASSETS_TIME_OBJECTS), ensure_ascii=False, sort_keys=False))
|
|
return
|
|
printEntity([Ent.LOOKERSTUDIO_ASSET, asset['title']], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, asset, timeObjects=LOOKERSTUDIO_ASSETS_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
csvPF = CSVPrintFile(['User', 'title']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
OBY = OrderBy(LOOKERSTUDIO_ASSETS_ORDERBY_CHOICE_MAP, ascendingKeyword='ascending', descendingKeyword='')
|
|
parameters, assetTypes = initLookerStudioAssetSelectionParameters()
|
|
assetIdEntity = None
|
|
stripCRsFromTitle = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getLookerStudioAssetSelectionParameters(myarg, parameters, assetTypes):
|
|
pass
|
|
elif myarg in {'assetid', 'assetids'}:
|
|
assetIdEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.LOOKERSTUDIO_ASSETID)
|
|
elif myarg == 'stripcrsfromtitle':
|
|
stripCRsFromTitle = True
|
|
elif myarg == 'orderby':
|
|
OBY.GetChoice()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, ds, assets, jcount = _validateUserGetLookerStudioAssetIds(user, i, count, assetIdEntity)
|
|
if not ds:
|
|
continue
|
|
if assetIdEntity is None:
|
|
assets, jcount = _getLookerStudioAssets(ds, user, i, count, parameters, assetTypes, 'nextPageToken,assets', OBY.orderBy)
|
|
if assets is None:
|
|
continue
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LOOKERSTUDIO_ASSET, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for asset in assets:
|
|
j += 1
|
|
if assetIdEntity:
|
|
asset = _getLookerStudioAssetByID(ds, user, i, count, asset['name'])
|
|
if asset:
|
|
_showAsset(asset)
|
|
Ind.Decrement()
|
|
else:
|
|
for asset in assets:
|
|
if assetIdEntity:
|
|
asset = _getLookerStudioAssetByID(ds, user, i, count, asset['name'])
|
|
if asset:
|
|
_printAsset(asset, user)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Looker Studio Assets')
|
|
|
|
def _showLookerStudioPermissions(user, asset, permissions, j, jcount, FJQC):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
permissions['User'] = user
|
|
permissions['assetId'] = asset['name']
|
|
printLine(json.dumps(cleanJSON(permissions), ensure_ascii=False, sort_keys=False))
|
|
return
|
|
permissions = permissions['permissions']
|
|
if permissions:
|
|
printEntity([Ent.LOOKERSTUDIO_ASSET, asset['title'], Ent.LOOKERSTUDIO_PERMISSION, ''], j, jcount)
|
|
for role in ['OWNER', 'EDITOR', 'VIEWER']:
|
|
members = permissions.get(role, {}).get('members', [])
|
|
if members:
|
|
lrole = role.lower()
|
|
Ind.Increment()
|
|
for member in members:
|
|
printKeyValueList([lrole, member])
|
|
Ind.Decrement()
|
|
|
|
LOOKERSTUDIO_VIEW_PERMISSION_ROLE_CHOICE_MAP = {
|
|
'editor': 'EDITOR',
|
|
'owner': 'OWNER',
|
|
'viewer': 'VIEWER',
|
|
}
|
|
|
|
LOOKERSTUDIO_ADD_UPDATE_PERMISSION_ROLE_CHOICE_MAP = {
|
|
'editor': 'EDITOR',
|
|
'viewer': 'VIEWER',
|
|
}
|
|
|
|
LOOKERSTUDIO_DELETE_PERMISSION_ROLE_CHOICE_MAP = {
|
|
'any': None,
|
|
'editor': None,
|
|
'owner': None,
|
|
'viewer': None,
|
|
}
|
|
|
|
LOOKERSTUDIO_PERMISSION_MODIFIER_MAP = {
|
|
Act.ADD: Act.MODIFIER_TO,
|
|
Act.DELETE: Act.MODIFIER_FROM,
|
|
Act.UPDATE: Act.MODIFIER_FOR
|
|
}
|
|
|
|
# gam <UserTypeEntity> add lookerstudiopermissions
|
|
# [([assettype report|datasource|all] [title <String>]
|
|
# [owner <Emailddress>] [includetrashed]
|
|
# [orderby title [ascending|descending]]) |
|
|
# (assetids <LookerStudioAssetIDEntity>)]
|
|
# (role editor|viewer <LookerStudioPermissionEntity>)+
|
|
# [nodetails]
|
|
# gam <UserTypeEntity> delete lookerstudiopermissions
|
|
# ([[assettype report|datasource|all] [title <String>]
|
|
# [owner <Emailddress>] [includetrashed]
|
|
# [orderby title [ascending|descending]]) |
|
|
# (assetids <LookerStudioAssetIDEntity>)]
|
|
# (role any <LookerStudioPermissionEntity>)+
|
|
# [nodetails]
|
|
# gam <UserTypeEntity> update lookerstudiopermissions
|
|
# [([assettype report|datasource|all] [title <String>]
|
|
# [owner <Emailddress>] [includetrashed]
|
|
# [orderby title [ascending|descending]]) |
|
|
# (assetids <LookerStudioAssetIDEntity>)]
|
|
# (role editor|viewer <LookerStudioPermissionEntity>)+
|
|
# [nodetails]
|
|
def processLookerStudioPermissions(users):
|
|
action = Act.Get()
|
|
if action == Act.CREATE:
|
|
action = Act.ADD
|
|
modifier = LOOKERSTUDIO_PERMISSION_MODIFIER_MAP[action]
|
|
parameters, assetTypes = initLookerStudioAssetSelectionParameters()
|
|
permissions = {}
|
|
assetIdEntity = None
|
|
showDetails = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getLookerStudioAssetSelectionParameters(myarg, parameters, assetTypes):
|
|
pass
|
|
elif myarg in {'assetid', 'assetids'}:
|
|
assetIdEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.LOOKERSTUDIO_ASSETID)
|
|
elif myarg == 'role':
|
|
permissions.setdefault('permissions', {})
|
|
if action in {Act.ADD, Act.UPDATE}:
|
|
role = getChoice(LOOKERSTUDIO_ADD_UPDATE_PERMISSION_ROLE_CHOICE_MAP, mapChoice=True)
|
|
else:
|
|
role = getChoice(LOOKERSTUDIO_DELETE_PERMISSION_ROLE_CHOICE_MAP, mapChoice=True)
|
|
permissions['permissions'].setdefault(role, {'members': []})
|
|
permissions['permissions'][role]['members'].extend(getEntityList(Cmd.OB_LOOKERSTUDIO_PERMISSION_ENTITY))
|
|
elif myarg == 'nodetails':
|
|
showDetails = False
|
|
else:
|
|
unknownArgumentExit()
|
|
if not permissions:
|
|
if action in {Act.ADD, Act.UPDATE}:
|
|
missingArgumentExit('role editor|owner|viewer members')
|
|
else:
|
|
missingArgumentExit('members')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, ds, assets, jcount = _validateUserGetLookerStudioAssetIds(user, i, count, assetIdEntity)
|
|
if not ds:
|
|
continue
|
|
if assetIdEntity is None:
|
|
assets, jcount = _getLookerStudioAssets(ds, user, i, count, parameters, assetTypes, 'nextPageToken,assets(name,title)', None)
|
|
if assets is None:
|
|
continue
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.LOOKERSTUDIO_PERMISSION, modifier, jcount, Ent.LOOKERSTUDIO_ASSET, i, count)
|
|
j = 0
|
|
for asset in assets:
|
|
j += 1
|
|
for role in permissions['permissions']:
|
|
try:
|
|
body = {'name': asset['name'], 'members': permissions['permissions'][role]['members']}
|
|
if action in {Act.DELETE, Act.UPDATE}:
|
|
results = callGAPI(ds.assets().permissions(), 'revokeAllPermissions',
|
|
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
|
|
name=asset['name'], body=body)
|
|
if action in {Act.ADD, Act.UPDATE}:
|
|
body['role'] = role
|
|
results = callGAPI(ds.assets().permissions(), 'addMembers',
|
|
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
|
|
name=asset['name'], body=body)
|
|
entityActionPerformed([Ent.USER, user, Ent.LOOKERSTUDIO_ASSET, asset['title'], Ent.LOOKERSTUDIO_PERMISSION, ''], j, jcount)
|
|
if showDetails:
|
|
_showLookerStudioPermissions(user, asset, results, j, jcount, None)
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LOOKERSTUDIO_ASSET, asset['title']], str(e), j, jcount)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userLookerStudioServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
|
|
# gam <UserTypeEntity> print lookerstudiopermissions [todrive <ToDriveAttribute>*]
|
|
# [([assettype report|datasource|all] [title <String>]
|
|
# [owner <Emailddress>] [includetrashed]
|
|
# [orderby title [ascending|descending]]) |
|
|
# (assetids <LookerStudioAssetIDEntity>)]
|
|
# [role editor|owner|viewer]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show lookerstudiopermissions
|
|
# [([assettype report|datasource|all] [title <String>]
|
|
# [owner <Emailddress>] [includetrashed]
|
|
# [orderby title [ascending|descending]]) |
|
|
# (assetids <LookerStudioAssetIDEntity>)[
|
|
# [role editor|owner|viewer]
|
|
# [formatjson]
|
|
def printShowLookerStudioPermissions(users):
|
|
def _printLookerStudioPermissions(user, asset, permissions):
|
|
row = flattenJSON(permissions, flattened={'User': user, 'assetId': asset['name']},
|
|
simpleLists=['members'], delimiter=delimiter)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'User': user, 'assetId': asset['name'],
|
|
'JSON': json.dumps(cleanJSON(permissions), ensure_ascii=False, sort_keys=True)})
|
|
|
|
csvPF = CSVPrintFile(['User', 'assetId']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
parameters, assetTypes = initLookerStudioAssetSelectionParameters()
|
|
assetIdEntity = None
|
|
role = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getLookerStudioAssetSelectionParameters(myarg, parameters, assetTypes):
|
|
pass
|
|
elif myarg in {'assetid', 'assetids'}:
|
|
assetIdEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.LOOKERSTUDIO_ASSETID)
|
|
elif myarg == 'role':
|
|
role = getChoice(LOOKERSTUDIO_VIEW_PERMISSION_ROLE_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, ds, assets, jcount = _validateUserGetLookerStudioAssetIds(user, i, count, assetIdEntity)
|
|
if not ds:
|
|
continue
|
|
if assetIdEntity is None:
|
|
assets, jcount = _getLookerStudioAssets(ds, user, i, count, parameters, assetTypes, 'nextPageToken,assets(name,title)', None)
|
|
if assets is None:
|
|
continue
|
|
if not csvPF and not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LOOKERSTUDIO_ASSET, i, count)
|
|
j = 0
|
|
for asset in assets:
|
|
j += 1
|
|
try:
|
|
permissions = callGAPI(ds.assets(), 'getPermissions',
|
|
throwReasons=GAPI.LOOKERSTUDIO_THROW_REASONS,
|
|
name=asset['name'], role=role)
|
|
except (GAPI.invalidArgument, GAPI.badRequest, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LOOKERSTUDIO_ASSET, asset['title']], str(e), j, jcount)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userLookerStudioServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
if not csvPF:
|
|
Ind.Increment()
|
|
_showLookerStudioPermissions(user, asset, permissions, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
_printLookerStudioPermissions(user, asset, permissions)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Looker Studio Permissions')
|
|
|
|
def _validateSubkeyRoleGetGroups(user, role, origUser, userGroupLists, i, count):
|
|
roleLower = role.lower()
|
|
if roleLower in GROUP_ROLES_MAP:
|
|
return (GROUP_ROLES_MAP[roleLower], userGroupLists[origUser][role])
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.ROLE, role], Msg.INVALID_ROLE.format(','.join(sorted(GROUP_ROLES_MAP))), i, count)
|
|
return (None, None)
|
|
|
|
def _addUserToGroups(cd, user, addGroupsSet, addGroups, i, count):
|
|
jcount = len(addGroupsSet)
|
|
entityPerformActionModifierNumItems([Ent.USER, user], Act.MODIFIER_TO, jcount, Ent.GROUP, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for group in sorted(addGroupsSet):
|
|
j += 1
|
|
role = addGroups[group]['role']
|
|
body = {'email': user, 'role': role}
|
|
if addGroups[group]['delivery_settings'] != DELIVERY_SETTINGS_UNDEFINED:
|
|
body['delivery_settings'] = addGroups[group]['delivery_settings']
|
|
try:
|
|
callGAPI(cd.members(), 'insert',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.DUPLICATE, GAPI.MEMBER_NOT_FOUND, GAPI.RESOURCE_NOT_FOUND,
|
|
GAPI.INVALID_MEMBER, GAPI.CYCLIC_MEMBERSHIPS_NOT_ALLOWED,
|
|
GAPI.CONDITION_NOT_MET, GAPI.CONFLICT],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=group, body=body, fields='')
|
|
entityActionPerformed([Ent.GROUP, group, role, user], j, jcount)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.GROUP, group, j, jcount)
|
|
except (GAPI.duplicate, GAPI.cyclicMembershipsNotAllowed, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.GROUP, group, role, user], str(e), j, jcount)
|
|
except GAPI.conflict:
|
|
entityActionPerformedMessage([Ent.GROUP, group, role, user], Msg.ACTION_MAY_BE_DELAYED, j, jcount)
|
|
except (GAPI.memberNotFound, GAPI.resourceNotFound, GAPI.invalidMember) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> add group|groups
|
|
# ([<GroupRole>] [[delivery] <DeliverySetting>] <GroupEntity>)+
|
|
def addUserToGroups(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
baseRole = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
|
|
baseDeliverySettings = getDeliverySettings()
|
|
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
if not isinstance(groupKeys, dict):
|
|
userGroupLists = None
|
|
addGroups = {}
|
|
for group in groupKeys:
|
|
addGroups[normalizeEmailAddressOrUID(group)] = {'role': baseRole, 'delivery_settings': baseDeliverySettings}
|
|
while Cmd.ArgumentsRemaining():
|
|
role = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
|
|
deliverySettings = getDeliverySettings()
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
addGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': deliverySettings}
|
|
else:
|
|
userGroupLists = groupKeys
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if userGroupLists:
|
|
roleList = [baseRole]
|
|
if not subkeyRoleField:
|
|
groupKeys = userGroupLists[origUser]
|
|
else:
|
|
roleList = userGroupLists[origUser]
|
|
addGroups = {}
|
|
for role in roleList:
|
|
if subkeyRoleField:
|
|
role, groupKeys = _validateSubkeyRoleGetGroups(user, role, origUser, userGroupLists, i, count)
|
|
if role is None:
|
|
continue
|
|
for group in groupKeys:
|
|
addGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': DELIVERY_SETTINGS_UNDEFINED}
|
|
_addUserToGroups(cd, user, set(addGroups), addGroups, i, count)
|
|
|
|
def _deleteUserFromGroups(cd, user, deleteGroupsSet, deleteGroups, i, count):
|
|
jcount = len(deleteGroupsSet)
|
|
entityPerformActionModifierNumItems([Ent.USER, user], Act.MODIFIER_FROM, jcount, Ent.GROUP, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for group in sorted(deleteGroupsSet):
|
|
j += 1
|
|
role = deleteGroups[group]['role']
|
|
try:
|
|
callGAPI(cd.members(), 'delete',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER,
|
|
GAPI.CONDITION_NOT_MET, GAPI.CONFLICT],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=group, memberKey=user)
|
|
entityActionPerformed([Ent.GROUP, group, role, user], j, jcount)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.GROUP, group, j, jcount)
|
|
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, group], str(e), j, jcount)
|
|
except GAPI.conflict:
|
|
entityActionPerformedMessage([Ent.GROUP, group, role, user], Msg.ACTION_MAY_BE_DELAYED, j, jcount)
|
|
Ind.Decrement()
|
|
|
|
def _getUserGroupOptionalDomainCustomerId():
|
|
if checkArgumentPresent('domain'):
|
|
return {'domain': getString(Cmd.OB_DOMAIN_NAME).lower()}
|
|
if checkArgumentPresent('customerid'):
|
|
return {'customer': getString(Cmd.OB_CUSTOMER_ID)}
|
|
return {}
|
|
|
|
def _setUserGroupArgs(user, kwargs):
|
|
if 'customer' in kwargs:
|
|
if "'" not in user:
|
|
kwargs['query'] = f'memberKey={user}'
|
|
else:
|
|
quser = user.replace("'", "\\'")
|
|
kwargs['query'] = f'memberKey={quser}'
|
|
else:
|
|
kwargs['userKey'] = user
|
|
|
|
def checkUserGroupMatchPattern(groupEmail, matchPattern):
|
|
if not matchPattern['not']:
|
|
if not matchPattern['pattern'].match(groupEmail):
|
|
return False
|
|
else:
|
|
if matchPattern['pattern'].match(groupEmail):
|
|
return False
|
|
return True
|
|
|
|
# gam <UserTypeEntity> delete group|groups
|
|
# [(domain <DomainName>)|(customerid <CustomerID>)|
|
|
# (emailmatchpattern [not] <REMatchPattern>)|<GroupEntity>]
|
|
def deleteUserFromGroups(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
groupKeys = None
|
|
matchPattern = {}
|
|
kwargs = _getUserGroupOptionalDomainCustomerId()
|
|
if not kwargs:
|
|
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
|
|
deleteGroups = {}
|
|
if Cmd.ArgumentsRemaining():
|
|
if not checkArgumentPresent('emailmatchpattern'):
|
|
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
userGroupLists = groupKeys if isinstance(groupKeys, dict) else None
|
|
for group in groupKeys:
|
|
deleteGroups[normalizeEmailAddressOrUID(group)] = {'role': Ent.MEMBER}
|
|
else:
|
|
matchPattern = {'not': checkArgumentPresent('not'), 'pattern': getREPattern(re.IGNORECASE)}
|
|
checkForExtraneousArguments()
|
|
else:
|
|
checkForExtraneousArguments()
|
|
role = Ent.MEMBER
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if groupKeys is None:
|
|
_setUserGroupArgs(user, kwargs)
|
|
try:
|
|
result = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
|
|
continue
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
deleteGroups = {}
|
|
for group in result:
|
|
if not matchPattern or checkUserGroupMatchPattern(group['email'], matchPattern):
|
|
deleteGroups[group['email']] = {'role': role}
|
|
elif userGroupLists:
|
|
userGroupKeys = userGroupLists[origUser]
|
|
deleteGroups = {}
|
|
for group in userGroupKeys:
|
|
deleteGroups[normalizeEmailAddressOrUID(group)] = {'role': role}
|
|
_deleteUserFromGroups(cd, user, set(deleteGroups), deleteGroups, i, count)
|
|
|
|
def _updateUserGroups(cd, user, updateGroupsSet, updateGroups, i, count):
|
|
jcount = len(updateGroupsSet)
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.GROUP, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for group in sorted(updateGroupsSet):
|
|
j += 1
|
|
role = updateGroups[group]['role']
|
|
body = {'email': user, 'role': role}
|
|
if updateGroups[group]['delivery_settings'] != DELIVERY_SETTINGS_UNDEFINED:
|
|
body['delivery_settings'] = updateGroups[group]['delivery_settings']
|
|
try:
|
|
callGAPI(cd.members(), 'patch',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=group, memberKey=user, body=body, fields='')
|
|
entityActionPerformed([Ent.GROUP, group, role, user], j, jcount)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.GROUP, group, j, jcount)
|
|
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, group], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> update group|groups
|
|
# [(domain <DomainName>)|(customerid <CustomerID>)]) [<GroupRole>] [[delivery] <DeliverySetting>]
|
|
# ([<GroupRole>] [[delivery] <DeliverySetting>] [<GroupEntity>])*
|
|
def updateUserGroups(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
groupKeys = None
|
|
kwargs = _getUserGroupOptionalDomainCustomerId()
|
|
baseRole = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
|
|
baseDeliverySettings = getDeliverySettings()
|
|
if not kwargs:
|
|
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
|
|
if Cmd.ArgumentsRemaining():
|
|
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
if not isinstance(groupKeys, dict):
|
|
userGroupLists = None
|
|
updateGroups = {}
|
|
for group in groupKeys:
|
|
updateGroups[normalizeEmailAddressOrUID(group)] = {'role': baseRole, 'delivery_settings': baseDeliverySettings}
|
|
while Cmd.ArgumentsRemaining():
|
|
role = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
|
|
deliverySettings = getDeliverySettings()
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
updateGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': deliverySettings}
|
|
else:
|
|
userGroupLists = groupKeys
|
|
checkForExtraneousArguments()
|
|
else:
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if groupKeys is None:
|
|
_setUserGroupArgs(user, kwargs)
|
|
try:
|
|
result = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
|
|
continue
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
updateGroups = {}
|
|
for group in result:
|
|
updateGroups[group['email']] = {'role': baseRole, 'delivery_settings': baseDeliverySettings}
|
|
elif userGroupLists:
|
|
roleList = [baseRole]
|
|
if not subkeyRoleField:
|
|
groupKeys = userGroupLists[origUser]
|
|
else:
|
|
roleList = userGroupLists[origUser]
|
|
updateGroups = {}
|
|
for role in roleList:
|
|
if subkeyRoleField:
|
|
role, groupKeys = _validateSubkeyRoleGetGroups(user, role, origUser, userGroupLists, i, count)
|
|
if role is None:
|
|
continue
|
|
for group in groupKeys:
|
|
updateGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': DELIVERY_SETTINGS_UNDEFINED}
|
|
_updateUserGroups(cd, user, set(updateGroups), updateGroups, i, count)
|
|
|
|
# gam <UserTypeEntity> sync group|groups
|
|
# [(domain <DomainName>)|(customerid <CustomerID>)]
|
|
# [<GroupRole>] [[delivery] <DeliverySetting>] (<GroupEntity>)*
|
|
def syncUserWithGroups(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
kwargs = _getUserGroupOptionalDomainCustomerId()
|
|
if not kwargs:
|
|
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
|
|
baseRole = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
|
|
baseDeliverySettings = getDeliverySettings()
|
|
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
subkeyRoleField = GM.Globals[GM.CSV_SUBKEY_FIELD]
|
|
if not isinstance(groupKeys, dict):
|
|
userGroupLists = None
|
|
syncGroups = {}
|
|
for group in groupKeys:
|
|
syncGroups[normalizeEmailAddressOrUID(group)] = {'role': baseRole, 'delivery_settings': baseDeliverySettings}
|
|
while Cmd.ArgumentsRemaining():
|
|
role = getChoice(GROUP_ROLES_MAP, defaultChoice=Ent.ROLE_MEMBER, mapChoice=True)
|
|
deliverySettings = getDeliverySettings()
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
syncGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': deliverySettings}
|
|
else:
|
|
userGroupLists = groupKeys
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if userGroupLists:
|
|
roleList = [baseRole]
|
|
if not subkeyRoleField:
|
|
groupKeys = userGroupLists[origUser]
|
|
else:
|
|
roleList = userGroupLists[origUser]
|
|
syncGroups = {}
|
|
for role in roleList:
|
|
if subkeyRoleField:
|
|
role, groupKeys = _validateSubkeyRoleGetGroups(user, role, origUser, userGroupLists, i, count)
|
|
if role is None:
|
|
continue
|
|
for group in groupKeys:
|
|
syncGroups[normalizeEmailAddressOrUID(group)] = {'role': role, 'delivery_settings': DELIVERY_SETTINGS_UNDEFINED}
|
|
currGroups = {}
|
|
_setUserGroupArgs(user, kwargs)
|
|
try:
|
|
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
|
|
continue
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
accessErrorExit(cd)
|
|
for groupEntity in entityList:
|
|
groupEmail = groupEntity['email']
|
|
try:
|
|
result = callGAPI(cd.members(), 'get',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=groupEmail, memberKey=user, fields='role,delivery_settings')
|
|
currGroups[groupEmail] = {'role': result.get('role', Ent.MEMBER),
|
|
'delivery_settings': result.get('delivery_settings', DELIVERY_SETTINGS_UNDEFINED)}
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.GROUP, groupEmail, i, count)
|
|
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), i, count)
|
|
currGroupsSet = set(currGroups)
|
|
syncGroupsSet = set(syncGroups)
|
|
removeGroupsSet = currGroupsSet-syncGroupsSet
|
|
addGroupsSet = syncGroupsSet-currGroupsSet
|
|
updateGroupsSet = set()
|
|
for group in currGroupsSet.intersection(syncGroupsSet):
|
|
if (syncGroups[group]['role'] != currGroups[group]['role'] or
|
|
(syncGroups[group]['delivery_settings'] != currGroups[group]['delivery_settings'] and
|
|
syncGroups[group]['delivery_settings'] != DELIVERY_SETTINGS_UNDEFINED)):
|
|
updateGroupsSet.add(group)
|
|
if removeGroupsSet or addGroupsSet or updateGroupsSet:
|
|
if removeGroupsSet:
|
|
Act.Set(Act.REMOVE)
|
|
_deleteUserFromGroups(cd, user, removeGroupsSet, currGroups, i, count)
|
|
if addGroupsSet:
|
|
Act.Set(Act.ADD)
|
|
_addUserToGroups(cd, user, addGroupsSet, syncGroups, i, count)
|
|
if updateGroupsSet:
|
|
Act.Set(Act.UPDATE)
|
|
_updateUserGroups(cd, user, updateGroupsSet, syncGroups, i, count)
|
|
else:
|
|
printEntityKVList([Ent.USER, user], [Msg.NO_CHANGES], i, count)
|
|
|
|
def _getUserGroupDomainCustomerId(myarg, kwargs):
|
|
if myarg == 'domain':
|
|
kwargs['domain'] = getString(Cmd.OB_DOMAIN_NAME).lower()
|
|
kwargs.pop('customer', None)
|
|
elif myarg == 'customerid':
|
|
kwargs['customer'] = getString(Cmd.OB_CUSTOMER_ID)
|
|
kwargs.pop('domain', None)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
# gam <UserTypeEntity> check group|groups
|
|
# [roles <GroupRoleList>] [includederivedmembership] [csv] <GroupEntity>
|
|
def checkUserInGroups(users):
|
|
def _setCheckError():
|
|
sysRC['sysRC'] = CHECK_USER_GROUPS_ERROR_RC
|
|
|
|
def _checkMember(result):
|
|
role = result.get('role', Ent.MEMBER)
|
|
if role in rolesSet:
|
|
if not csvPF:
|
|
printEntity([Ent.USER, user, Ent.GROUP, groupEmail, Ent.ROLE, role], j, jcount)
|
|
else:
|
|
csvPF.WriteRow({'user': user, 'group': groupEmail, 'role': role})
|
|
else:
|
|
if not csvPF:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail, Ent.ROLE, role], Msg.ROLE_NOT_IN_SET.format(rolesSet), j, jcount)
|
|
else:
|
|
csvPF.WriteRow({'user': user, 'group': groupEmail, 'role': notMemberOrRole})
|
|
_setCheckError()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = None
|
|
groupKeys = []
|
|
checkGroupsSet = set()
|
|
rolesSet = set()
|
|
includeDerivedMembership = False
|
|
sysRC = {'sysRC' : 0}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'role', 'roles'}:
|
|
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
|
|
if role in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[role])
|
|
else:
|
|
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
|
|
elif myarg == 'includederivedmembership':
|
|
includeDerivedMembership = True
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile(['user', 'group', 'role'])
|
|
else:
|
|
Cmd.Backup()
|
|
groupKeys = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
if not rolesSet:
|
|
rolesSet = ALL_GROUP_ROLES
|
|
notMemberOrRole = Msg.NOT_AN_ENTITY.format('|'.join(rolesSet))
|
|
userGroupLists = groupKeys if isinstance(groupKeys, dict) else None
|
|
for group in groupKeys:
|
|
checkGroupsSet.add(normalizeEmailAddressOrUID(group))
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if userGroupLists:
|
|
userGroupKeys = userGroupLists[origUser]
|
|
checkGroupsSet = set()
|
|
for group in userGroupKeys:
|
|
checkGroupsSet.add(normalizeEmailAddressOrUID(group))
|
|
jcount = len(checkGroupsSet)
|
|
entityPerformActionModifierNumItems([Ent.USER, user], Act.MODIFIER_IN, jcount, Ent.GROUP, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for groupEmail in sorted(checkGroupsSet):
|
|
j += 1
|
|
if not includeDerivedMembership:
|
|
try:
|
|
result = callGAPI(cd.members(), 'get',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=groupEmail, memberKey=user, fields='role')
|
|
_checkMember(result)
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.GROUP, groupEmail, j, jcount)
|
|
_setCheckError()
|
|
except GAPI.memberNotFound:
|
|
if not csvPF:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], Msg.NOT_A_MEMBER, j, jcount)
|
|
else:
|
|
csvPF.WriteRow({'user': user, 'group': groupEmail, 'role': notMemberOrRole})
|
|
_setCheckError()
|
|
except (GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), j, jcount)
|
|
_setCheckError()
|
|
else:
|
|
try:
|
|
result = callGAPIpages(cd.members(), 'list', 'members',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS, retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
includeDerivedMembership=includeDerivedMembership,
|
|
groupKey=groupEmail, fields='nextPageToken,members(email,role,type)', maxResults=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
for member in result:
|
|
if member['type'] != Ent.TYPE_USER or member['email'].lower() != user:
|
|
continue
|
|
_checkMember(member)
|
|
break
|
|
else:
|
|
if not csvPF:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], Msg.NOT_A_MEMBER, j, jcount)
|
|
else:
|
|
csvPF.WriteRow({'user': user, 'group': groupEmail, 'role': notMemberOrRole})
|
|
_setCheckError()
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.GROUP, groupEmail, j, jcount)
|
|
_setCheckError()
|
|
except (GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), j, jcount)
|
|
_setCheckError()
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('User Check Groups')
|
|
setSysExitRC(sysRC['sysRC'])
|
|
|
|
# gam <UserTypeEntity> print groups [todrive <ToDriveAttribute>*]
|
|
# [(domain <DomainName>)|(customerid <CustomerID>)]
|
|
# [roles <GroupRoleList>] [countsonly|totalonly|nodetails]
|
|
# gam <UserTypeEntity> show groups
|
|
# [(domain <DomainName>)|(customerid <CustomerID>)]
|
|
# [roles <GroupRoleList>] [countsonly|totalonly|nodetails]
|
|
def printShowUserGroups(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
|
|
csvPF = CSVPrintFile(['User', 'Group', 'Role', 'Status', 'Delivery'], 'sortall') if Act.csvFormat() else None
|
|
rolesSet = set()
|
|
countsOnly = noDetails = totalOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getUserGroupDomainCustomerId(myarg, kwargs):
|
|
pass
|
|
elif myarg in {'role', 'roles'}:
|
|
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
|
|
if role in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[role])
|
|
else:
|
|
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
elif myarg == 'totalonly':
|
|
countsOnly = totalOnly = True
|
|
elif myarg == 'nodetails':
|
|
noDetails = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not rolesSet:
|
|
rolesSet = ALL_GROUP_ROLES
|
|
allRoles = rolesSet == ALL_GROUP_ROLES
|
|
if noDetails:
|
|
if csvPF:
|
|
titles = ['User', 'Group']
|
|
csvPF.SetTitles(titles)
|
|
csvPF.SetSortTitles([])
|
|
elif countsOnly:
|
|
zeroCounts = {'User': None, 'Total': 0}
|
|
if not totalOnly:
|
|
for role in [Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER]:
|
|
if role in rolesSet:
|
|
zeroCounts[role] = 0
|
|
if csvPF:
|
|
titles = ['User', 'Total']
|
|
if not totalOnly:
|
|
for role in [Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER]:
|
|
if role in rolesSet:
|
|
titles.append(role)
|
|
csvPF.SetTitles(titles)
|
|
csvPF.SetSortTitles([])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(Ent.GROUP, user, i, count)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
_setUserGroupArgs(user, kwargs)
|
|
try:
|
|
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
pageMessage=pageMessage,
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
|
|
continue
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
if kwargs.get('domain'):
|
|
badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain'])
|
|
return
|
|
accessErrorExit(cd)
|
|
jcount = len(entityList)
|
|
if totalOnly:
|
|
if not csvPF:
|
|
printEntityKVList([Ent.USER, user], ['Total', jcount])
|
|
else:
|
|
csvPF.WriteRow({'User': user, 'Total': jcount})
|
|
continue
|
|
if not csvPF:
|
|
if allRoles:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.GROUP, i, count)
|
|
else:
|
|
entityPerformActionModifierNumItems([Ent.USER, user], Msg.MAXIMUM_OF, jcount, Ent.GROUP, i, count)
|
|
if noDetails:
|
|
Ind.Increment()
|
|
j = 0
|
|
for groupEntity in entityList:
|
|
j += 1
|
|
groupEmail = groupEntity['email']
|
|
if not csvPF:
|
|
printEntity([Ent.GROUP, groupEmail], j, jcount)
|
|
else:
|
|
csvPF.WriteRow({'User': user, 'Group': groupEmail})
|
|
Ind.Decrement()
|
|
continue
|
|
if countsOnly:
|
|
userCounts = zeroCounts.copy()
|
|
userCounts['User'] = user
|
|
Ind.Increment()
|
|
j = 0
|
|
for groupEntity in entityList:
|
|
j += 1
|
|
groupEmail = groupEntity['email']
|
|
try:
|
|
result = callGAPI(cd.members(), 'get',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=groupEmail, memberKey=user, fields='role,status,delivery_settings')
|
|
role = result.get('role', Ent.MEMBER)
|
|
status = result.get('status', 'UNKNOWN')
|
|
delivery_settings = result.get('delivery_settings', '')
|
|
if role in rolesSet:
|
|
if countsOnly:
|
|
userCounts[role] += 1
|
|
elif not csvPF:
|
|
printEntity([Ent.GROUP, groupEmail, Ent.ROLE, role, Ent.STATUS, status, Ent.DELIVERY, delivery_settings], j, jcount)
|
|
else:
|
|
csvPF.WriteRow({'User': user, 'Group': groupEmail, 'Role': role, 'Status': status, 'Delivery': delivery_settings})
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.GROUP, groupEmail, j, jcount)
|
|
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), j, jcount)
|
|
if countsOnly:
|
|
for role in [Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER]:
|
|
if role in rolesSet:
|
|
userCounts['Total'] += userCounts[role]
|
|
if not csvPF:
|
|
kvList = ['Total', userCounts['Total']]
|
|
for role in [Ent.ROLE_MEMBER, Ent.ROLE_MANAGER, Ent.ROLE_OWNER]:
|
|
if role in rolesSet:
|
|
kvList.extend([role, userCounts[role]])
|
|
printEntityKVList([Ent.USER, user], kvList)
|
|
else:
|
|
csvPF.WriteRow(userCounts)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('User Groups')
|
|
|
|
# gam <UserTypeEntity> print grouptree [todrive <ToDriveAttribute>*]
|
|
# [(domain <DomainName>)|(customerid <CustomerID>)]
|
|
# [roles <GroupRoleList>]
|
|
# [showparentsaslist [<Boolean>]] [delimiter <Character>]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show grouptree
|
|
# [(domain <DomainName>)|(customerid <CustomerID>)]
|
|
# [roles <GroupRoleList>]
|
|
# [formatjson]
|
|
def printShowGroupTree(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
|
|
csvPF = CSVPrintFile(['User', 'Group', 'Name']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
showParentsAsList = False
|
|
rolesSet = set()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getUserGroupDomainCustomerId(myarg, kwargs):
|
|
pass
|
|
elif myarg in {'role', 'roles'}:
|
|
for role in getString(Cmd.OB_GROUP_ROLE_LIST).lower().replace(',', ' ').split():
|
|
if role in GROUP_ROLES_MAP:
|
|
rolesSet.add(GROUP_ROLES_MAP[role])
|
|
else:
|
|
invalidChoiceExit(role, GROUP_ROLES_MAP, True)
|
|
elif csvPF and myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif csvPF and myarg == 'showparentsaslist':
|
|
showParentsAsList = getBoolean()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if csvPF:
|
|
if rolesSet:
|
|
csvPF.AddTitles('Role')
|
|
if not FJQC.formatJSON:
|
|
if not showParentsAsList:
|
|
csvPF.SetIndexedTitles(['parents'])
|
|
else:
|
|
csvPF.AddTitles(['ParentsCount', 'Parents', 'ParentsName'])
|
|
else:
|
|
if rolesSet:
|
|
csvPF.AddJSONTitles('Role')
|
|
csvPF.AddJSONTitles('JSON')
|
|
allRoles = rolesSet == ALL_GROUP_ROLES
|
|
groupParents = {}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
_setUserGroupArgs(user, kwargs)
|
|
try:
|
|
groups = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', fields='nextPageToken,groups(email,name)', **kwargs)
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
continue
|
|
j = 0
|
|
jcount = len(groups)
|
|
if not csvPF and not FJQC.formatJSON:
|
|
if allRoles:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.GROUP_TREE, i, count)
|
|
else:
|
|
entityPerformActionModifierNumItems([Ent.USER, user], Msg.MAXIMUM_OF, jcount, Ent.GROUP_TREE, i, count)
|
|
Ind.Increment()
|
|
for group in groups:
|
|
j += 1
|
|
groupEmail = group['email']
|
|
if groupEmail not in groupParents:
|
|
getGroupParents(cd, groupParents, groupEmail, group['name'], kwargs)
|
|
if rolesSet:
|
|
try:
|
|
result = callGAPI(cd.members(), 'get',
|
|
throwReasons=GAPI.MEMBERS_THROW_REASONS+[GAPI.MEMBER_NOT_FOUND, GAPI.INVALID_MEMBER, GAPI.CONDITION_NOT_MET],
|
|
retryReasons=GAPI.MEMBERS_RETRY_REASONS,
|
|
groupKey=groupEmail, memberKey=user, fields='role')
|
|
role = result.get('role', Ent.MEMBER)
|
|
if role not in rolesSet:
|
|
continue
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.invalid, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.GROUP, groupEmail, j, jcount)
|
|
continue
|
|
except (GAPI.memberNotFound, GAPI.invalidMember, GAPI.conditionNotMet, GAPI.serviceNotAvailable) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.GROUP, groupEmail], str(e), j, jcount)
|
|
continue
|
|
else:
|
|
role = None
|
|
if not FJQC.formatJSON:
|
|
if not csvPF:
|
|
showGroupParents(groupParents, groupEmail, role, j, jcount)
|
|
else:
|
|
row = {'User': user, 'Group': groupEmail, 'Name': group['name'], 'parents': []}
|
|
if role is not None:
|
|
row['Role'] = role
|
|
printGroupParents(groupParents, groupEmail, row, csvPF, delimiter, showParentsAsList)
|
|
else:
|
|
groupInfo = {'email': groupEmail, 'name': group['name'], 'parents': []}
|
|
if role is not None:
|
|
groupInfo['role'] = role
|
|
addJsonGroupParents(groupParents, groupInfo, groupEmail)
|
|
if not csvPF:
|
|
printLine(json.dumps(cleanJSON(groupInfo), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
row = flattenJSON(groupInfo)
|
|
if csvPF.CheckRowTitles(row):
|
|
row = {'User': user, 'Group': groupEmail, 'Name': group['name']}
|
|
if rolesSet:
|
|
row['Role'] = role
|
|
row['JSON'] = json.dumps(cleanJSON(groupInfo),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('User Group Trees')
|
|
|
|
# gam <UserTypeEntity> print groupslist [todrive <ToDriveAttribute>*]
|
|
# [(domain <DomainName>)|(customerid <CustomerID>)]
|
|
# [delimiter <Character>] [quotechar <Character>]
|
|
def printUserGroupsList(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
kwargs = {'customer': GC.Values[GC.CUSTOMER_ID]}
|
|
csvPF = CSVPrintFile(['User', 'Groups', 'GroupsList'])
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getUserGroupDomainCustomerId(myarg, kwargs):
|
|
pass
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
else:
|
|
FJQC.GetQuoteChar(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
printGettingAllEntityItemsForWhom(Ent.GROUP, user, i, count)
|
|
_setUserGroupArgs(user, kwargs)
|
|
try:
|
|
entityList = callGAPIpages(cd.groups(), 'list', 'groups',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.GROUP_LIST_USERKEY_THROW_REASONS,
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
orderBy='email', fields='nextPageToken,groups(email)', **kwargs)
|
|
except (GAPI.invalidMember, GAPI.invalidInput):
|
|
badRequestWarning(Ent.GROUP, Ent.MEMBER, user)
|
|
continue
|
|
except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest):
|
|
if kwargs.get('domain'):
|
|
badRequestWarning(Ent.GROUP, Ent.DOMAIN, kwargs['domain'])
|
|
return
|
|
accessErrorExit(cd)
|
|
csvPF.WriteRow({'User': user, 'Groups': len(entityList), 'GroupsList': delimiter.join([group['email'] for group in entityList])})
|
|
csvPF.writeCSVfile('User GroupsList')
|
|
|
|
# License command utilities
|
|
LICENSE_PRODUCT_SKUIDS = 'productSkuIds'
|
|
LICENSE_PREVIEW_TITLES = ['user', 'productId', 'skuId', 'action', 'message']
|
|
|
|
def getLicenseParameters(operation):
|
|
lic = buildGAPIObject(API.LICENSING)
|
|
parameters = {LICENSE_PRODUCT_SKUIDS: [], 'csvPF': None, 'preview': False, 'syncOperation': 'addremove', 'syncACLsMode': None, 'archive': False}
|
|
skuLocation = Cmd.Location()
|
|
if operation != 'patch':
|
|
parameters[LICENSE_PRODUCT_SKUIDS] = getGoogleSKUList(allowUnknownProduct=True)
|
|
else:
|
|
parameters[LICENSE_PRODUCT_SKUIDS] = [getGoogleSKU()]
|
|
if checkArgumentPresent(['product', 'productid']):
|
|
productId = getGoogleProduct()
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
productSku = (productId, productSku[1])
|
|
if operation == 'patch':
|
|
checkArgumentPresent('from')
|
|
productId, oldSkuId = getGoogleSKU()
|
|
if not productId:
|
|
invalidChoiceExit(oldSkuId, SKU.getSortedSKUList(), True)
|
|
skuId = parameters[LICENSE_PRODUCT_SKUIDS][0][1]
|
|
parameters[LICENSE_PRODUCT_SKUIDS] = [(productId, skuId, oldSkuId)]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if operation == 'sync' and myarg in {'addonly', 'removeonly'}:
|
|
parameters['syncOperation'] = myarg
|
|
elif operation == 'sync' and myarg in {'allskus', 'onesku'}:
|
|
parameters['syncACLsMode'] = myarg
|
|
elif myarg == 'preview':
|
|
parameters['preview'] = True
|
|
elif myarg == 'actioncsv':
|
|
titles = LICENSE_PREVIEW_TITLES
|
|
if operation == 'patch':
|
|
titles.insert(2, 'oldskuId')
|
|
parameters['csvPF'] = CSVPrintFile(titles)
|
|
elif operation == 'patch' and myarg == 'archive':
|
|
if skuId not in SKU.ARCHIVABLE_SKUS:
|
|
usageErrorExit(Msg.SKU_HAS_NO_MATCHING_ARCHIVED_USER_SKU.format(skuId))
|
|
parameters['archive'] = True
|
|
else:
|
|
unknownArgumentExit()
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
if not productSku[0]:
|
|
Cmd.SetLocation(skuLocation)
|
|
invalidChoiceExit(productSku[1], SKU.getSortedSKUList(), False)
|
|
if operation == 'sync':
|
|
if len(parameters[LICENSE_PRODUCT_SKUIDS]) > 1:
|
|
if parameters['syncACLsMode'] is None:
|
|
missingArgumentExit('allskus|onesku')
|
|
if parameters['syncACLsMode'] == 'onesku':
|
|
# With onesku, all SKU productIds must be the same
|
|
baseProductId = parameters[LICENSE_PRODUCT_SKUIDS][0][0]
|
|
baseSkuId = parameters[LICENSE_PRODUCT_SKUIDS][0][1]
|
|
for i in range(1, len(parameters[LICENSE_PRODUCT_SKUIDS])):
|
|
productId = parameters[LICENSE_PRODUCT_SKUIDS][i][0]
|
|
if baseProductId != productId:
|
|
skuId = parameters[LICENSE_PRODUCT_SKUIDS][i][1]
|
|
Cmd.SetLocation(skuLocation)
|
|
usageErrorExit(Msg.ALL_SKU_PRODUCTIDS_MUST_MATCH.format(f'{baseProductId}:{baseSkuId}', f'{productId}:{skuId}'))
|
|
else:
|
|
parameters['syncACLsMode'] = 'allskus'
|
|
return (lic, parameters)
|
|
|
|
def _writeLicenseAction(productId, skuId, oldSkuId, parameters, user, action, message):
|
|
actionName = Act.PerformedName(action) if message == Act.SUCCESS else Act.ToPerformName(action)
|
|
if action != Act.UPDATE:
|
|
parameters['csvPF'].WriteRow({'user': user,
|
|
'productId': productId,
|
|
'skuId': skuId,
|
|
'action': actionName,
|
|
'message': message})
|
|
else:
|
|
parameters['csvPF'].WriteRow({'user': user,
|
|
'productId': productId,
|
|
'oldskuId': oldSkuId,
|
|
'skuId': skuId,
|
|
'action': actionName,
|
|
'message': message})
|
|
|
|
def _createLicenses(lic, productId, skuId, parameters, jcount, users, i, count, returnDoneSet=False):
|
|
Act.Set([Act.ADD, Act.ADD_PREVIEW][parameters['preview']])
|
|
if parameters['preview']:
|
|
message = Act.PREVIEW
|
|
noAvailableLicenses = False
|
|
doneSet = set()
|
|
if not returnDoneSet:
|
|
entityPerformActionModifierNumItems([Ent.LICENSE, SKU.skuIdToDisplayName(skuId)], Msg.TO_LC, jcount, Ent.USER, i, count)
|
|
else:
|
|
entityPerformActionModifierNumItems([Ent.LICENSE, SKU.skuIdToDisplayName(skuId)], Msg.TO_MAXIMUM_OF, jcount, Ent.USER, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for user in users:
|
|
j += 1
|
|
if returnDoneSet:
|
|
origUser = user
|
|
doneSet.add(user)
|
|
user = normalizeEmailAddressOrUID(user)
|
|
if not noAvailableLicenses:
|
|
try:
|
|
if not parameters['preview']:
|
|
callGAPI(lic.licenseAssignments(), 'insert',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.DUPLICATE, GAPI.CONDITION_NOT_MET, GAPI.INVALID,
|
|
GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
productId=productId, skuId=skuId, body={'userId': user}, fields='')
|
|
message = Act.SUCCESS
|
|
entityActionPerformed([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], j, jcount)
|
|
except (GAPI.internalError, GAPI.duplicate, GAPI.invalid,
|
|
GAPI.forbidden, GAPI.serviceNotAvailable) as e:
|
|
message = str(e)
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], message, j, jcount)
|
|
except (GAPI.conditionNotMet, GAPI.backendError) as e:
|
|
message = str(e)
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], message, j, jcount)
|
|
if ("there aren't enough available licenses" in message.lower() or
|
|
"backend error" in message.lower()):
|
|
noAvailableLicenses = True
|
|
if returnDoneSet:
|
|
doneSet.remove(origUser)
|
|
break
|
|
except GAPI.userNotFound as e:
|
|
message = str(e)
|
|
entityUnknownWarning(Ent.USER, user, j, jcount)
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], message, j, jcount)
|
|
if parameters['csvPF']:
|
|
_writeLicenseAction(productId, skuId, None, parameters, user, Act.ADD, message)
|
|
Ind.Decrement()
|
|
if returnDoneSet:
|
|
return doneSet
|
|
|
|
# gam <UserTypeEntity> create license <SKUIDList> [product|productid <ProductID>] [preview] [actioncsv]
|
|
def createLicense(users):
|
|
lic, parameters = getLicenseParameters('insert')
|
|
_, jcount, users = getEntityArgument(users)
|
|
count = len(parameters[LICENSE_PRODUCT_SKUIDS])
|
|
i = 0
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
i += 1
|
|
_createLicenses(lic, productSku[0], productSku[1], parameters, jcount, users, i, count)
|
|
if parameters['csvPF']:
|
|
parameters['csvPF'].writeCSVfile('Create Licenses')
|
|
|
|
# gam <UserTypeEntity> update license <SKUID> [product|productid <ProductID>] [from] <SKUID>
|
|
# [preview] [actioncsv] [archive]
|
|
def updateLicense(users):
|
|
lic, parameters = getLicenseParameters('patch')
|
|
j, jcount, users = getEntityArgument(users)
|
|
Act.Set([Act.UPDATE, Act.UPDATE_PREVIEW][parameters['preview']])
|
|
cd = None
|
|
if parameters['preview']:
|
|
message = Act.PREVIEW
|
|
elif parameters['archive']:
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
productId, skuId, oldSkuId = parameters[LICENSE_PRODUCT_SKUIDS][0]
|
|
body = {'skuId': skuId}
|
|
entityPerformActionModifierNumItems([Ent.LICENSE, SKU.skuIdToDisplayName(skuId)], Msg.FOR, jcount, Ent.USER)
|
|
Ind.Increment()
|
|
for user in users:
|
|
j += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
if not parameters['preview']:
|
|
callGAPI(lic.licenseAssignments(), 'patch',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.NOT_FOUND, GAPI.CONDITION_NOT_MET, GAPI.INVALID,
|
|
GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
productId=productId, skuId=oldSkuId, userId=user, body=body, fields='')
|
|
message = Act.SUCCESS
|
|
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.LICENSE, SKU.skuIdToDisplayName(skuId)],
|
|
Act.MODIFIER_FROM, SKU.skuIdToDisplayName(oldSkuId), j, jcount)
|
|
except (GAPI.internalError, GAPI.notFound, GAPI.conditionNotMet, GAPI.invalid,
|
|
GAPI.forbidden, GAPI.backendError, GAPI.serviceNotAvailable) as e:
|
|
message = str(e)
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(oldSkuId)], message, j, jcount)
|
|
except GAPI.userNotFound as e:
|
|
message = str(e)
|
|
entityUnknownWarning(Ent.USER, user, j, jcount)
|
|
if parameters['archive'] and message == Act.SUCCESS:
|
|
Act.Set(Act.ARCHIVE)
|
|
try:
|
|
callGAPI(cd.users(), 'update',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
|
|
GAPI.INSUFFICIENT_ARCHIVED_USER_LICENSES],
|
|
retryReasons=[GAPI.INSUFFICIENT_ARCHIVED_USER_LICENSES],
|
|
userKey=user, body={'archived': True})
|
|
entityActionPerformed([Ent.USER, user], j, jcount)
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, j, jcount)
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest,
|
|
GAPI.insufficientArchivedUserLicenses) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), j, jcount)
|
|
Act.Set(Act.UPDATE)
|
|
if parameters['csvPF']:
|
|
_writeLicenseAction(productId, skuId, oldSkuId, parameters, user, Act.UPDATE, message)
|
|
Ind.Decrement()
|
|
if parameters['csvPF']:
|
|
parameters['csvPF'].writeCSVfile('Update Licenses')
|
|
|
|
def _deleteLicenses(lic, productId, skuId, parameters, jcount, users, i, count):
|
|
Act.Set([Act.DELETE, Act.DELETE_PREVIEW][parameters['preview']])
|
|
if parameters['preview']:
|
|
message = Act.PREVIEW
|
|
entityPerformActionModifierNumItems([Ent.LICENSE, SKU.skuIdToDisplayName(skuId)], Msg.FROM_LC, jcount, Ent.USER, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for user in users:
|
|
j += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
if not parameters['preview']:
|
|
callGAPI(lic.licenseAssignments(), 'delete',
|
|
throwReasons=[GAPI.INTERNAL_ERROR, GAPI.NOT_FOUND, GAPI.CONDITION_NOT_MET, GAPI.INVALID,
|
|
GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.BACKEND_ERROR, GAPI.SERVICE_NOT_AVAILABLE],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
productId=productId, skuId=skuId, userId=user)
|
|
message = Act.SUCCESS
|
|
entityActionPerformed([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], j, jcount)
|
|
except (GAPI.internalError, GAPI.notFound, GAPI.conditionNotMet, GAPI.invalid,
|
|
GAPI.forbidden, GAPI.backendError, GAPI.serviceNotAvailable) as e:
|
|
message = str(e)
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, SKU.formatSKUIdDisplayName(skuId)], message, j, jcount)
|
|
except GAPI.userNotFound as e:
|
|
message = str(e)
|
|
entityUnknownWarning(Ent.USER, user, j, jcount)
|
|
if parameters['csvPF']:
|
|
_writeLicenseAction(productId, skuId, None, parameters, user, Act.DELETE, message)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delete license <SKUIDList> [product|productid <ProductID>] [preview] [actioncsv]
|
|
def deleteLicense(users):
|
|
lic, parameters = getLicenseParameters('delete')
|
|
_, jcount, users = getEntityArgument(users)
|
|
count = len(parameters[LICENSE_PRODUCT_SKUIDS])
|
|
i = 0
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
i += 1
|
|
_deleteLicenses(lic, productSku[0], productSku[1], parameters, jcount, users, i, count)
|
|
if parameters['csvPF']:
|
|
parameters['csvPF'].writeCSVfile('Delete Licenses')
|
|
|
|
# gam <UserTypeEntity> sync license <SKUIDList> [product|productid <ProductID>]
|
|
# [addonly|removeonly] [allskus|onesku] [preview] [actioncsv]
|
|
def syncLicense(users):
|
|
lic, parameters = getLicenseParameters('sync')
|
|
_, _, users = getEntityArgument(users)
|
|
usersSet = set()
|
|
currentLicenses = {}
|
|
for user in users:
|
|
usersSet.add(normalizeEmailAddressOrUID(user))
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
skuId = productSku[1]
|
|
currentLicenses[skuId] = set(getItemsToModify(Cmd.ENTITY_LICENSES, skuId))
|
|
count = len(parameters[LICENSE_PRODUCT_SKUIDS])
|
|
if parameters['syncACLsMode'] != 'onesku':
|
|
i = 0
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
i += 1
|
|
if parameters['syncOperation'] != 'addonly':
|
|
deleteSet = currentLicenses[skuId]-usersSet
|
|
_deleteLicenses(lic, productSku[0], productSku[1], parameters, len(deleteSet), deleteSet, i, count)
|
|
if parameters['syncOperation'] != 'removeonly':
|
|
addSet = usersSet-currentLicenses[skuId]
|
|
_createLicenses(lic, productSku[0], productSku[1], parameters, len(addSet), addSet, i, count)
|
|
else: #parameters['syncACLsMode'] == 'onesku':
|
|
if parameters['syncOperation'] != 'addonly':
|
|
i = 0
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
i += 1
|
|
deleteSet = currentLicenses[productSku[1]]-usersSet
|
|
_deleteLicenses(lic, productSku[0], productSku[1], parameters, len(deleteSet), deleteSet, i, count)
|
|
if parameters['syncOperation'] != 'removeonly':
|
|
addSet = usersSet.copy()
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
addSet = addSet-currentLicenses[productSku[1]]
|
|
i = 0
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
if not addSet:
|
|
break
|
|
i += 1
|
|
addSet -= _createLicenses(lic, productSku[0], productSku[1], parameters, len(addSet), addSet, i, count, returnDoneSet=True)
|
|
if addSet:
|
|
productId = productSku[0]
|
|
skuIds = []
|
|
for productSku in parameters[LICENSE_PRODUCT_SKUIDS]:
|
|
skuIds.append(productSku[1])
|
|
skuIdsList = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER].join(skuIds)
|
|
message = Msg.NO_AVAILABLE_LICENSES
|
|
for user in addSet:
|
|
user = normalizeEmailAddressOrUID(user)
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LICENSE, skuIdsList], message)
|
|
if parameters['csvPF']:
|
|
_writeLicenseAction(productId, skuIdsList, None, parameters, user, Act.ADD, message)
|
|
if parameters['csvPF']:
|
|
parameters['csvPF'].writeCSVfile('Sync Licenses')
|
|
|
|
# gam <UserTypeEntity> update photo
|
|
# ([<FileNamePattern>] |
|
|
# ([drivedir|(sourcefolder <FilePath>)] [filename <FileNamePattern>]) |
|
|
# (gphoto <EmailAddress> <DriveFileIDEntity>|<DriveFileNameEntity>))
|
|
# #user# and #email" will be replaced with user email address #username# will be replaced by portion of email address in front of @
|
|
# in <FileNamePattern> and <DriveFileNameEntity>
|
|
def updatePhoto(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
baseFileIdEntity = drive = owner = None
|
|
sourceFolder = os.getcwd()
|
|
if Cmd.NumArgumentsRemaining() == 1:
|
|
filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN)
|
|
else:
|
|
filenamePattern = '#email#.jpg'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'drivedir':
|
|
sourceFolder = GC.Values[GC.DRIVE_DIR]
|
|
elif myarg == 'sourcefolder':
|
|
sourceFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
if not os.path.isdir(sourceFolder):
|
|
entityDoesNotExistExit(Ent.DIRECTORY, sourceFolder)
|
|
elif myarg == 'filename':
|
|
filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN)
|
|
elif myarg == 'gphoto':
|
|
owner, drive = buildGAPIServiceObject(API.DRIVE3, getEmailAddress())
|
|
if not drive:
|
|
return
|
|
baseFileIdEntity = getDriveFileEntity(queryShortcutsOK=False)
|
|
else:
|
|
unknownArgumentExit()
|
|
p = re.compile('^(ht|f)tps?://.*$')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, userName, _ = splitEmailAddressOrUID(user)
|
|
filename = _substituteForUser(filenamePattern, user, userName)
|
|
if baseFileIdEntity is not None:
|
|
fileIdEntity = baseFileIdEntity.copy()
|
|
if fileIdEntity['query'] is not None:
|
|
fileIdEntity['query'] = _substituteForUser(fileIdEntity['query'], user, userName)
|
|
_, _, jcount = _validateUserGetFileIDs(owner, 0, 0, fileIdEntity, drive=drive, entityType=None)
|
|
if jcount == 0:
|
|
entityItemValueListActionNotPerformedWarning([Ent.USER, user], [Ent.OWNER, owner],
|
|
Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)), i, count)
|
|
continue
|
|
if jcount > 1:
|
|
entityItemValueListActionNotPerformedWarning([Ent.USER, user], [Ent.OWNER, owner],
|
|
Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list'])), i, count)
|
|
continue
|
|
fb = TemporaryFile(mode='wb+')
|
|
filename = fileIdEntity['list'][0]
|
|
request = drive.files().get_media(fileId=filename)
|
|
downloader = googleapiclient.http.MediaIoBaseDownload(fb, request)
|
|
done = False
|
|
while not done:
|
|
_, done = downloader.next_chunk()
|
|
fb.seek(0)
|
|
image_data = fb.read()
|
|
fb.close()
|
|
elif p.match(filename):
|
|
try:
|
|
status, image_data = getHttpObj().request(filename, 'GET')
|
|
if status['status'] != '200':
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], Msg.NOT_ALLOWED, i, count)
|
|
continue
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
|
|
continue
|
|
else:
|
|
if sourceFolder is not None:
|
|
filename = os.path.join(sourceFolder, filename)
|
|
try:
|
|
with open(os.path.expanduser(filename), 'rb') as f:
|
|
image_data = f.read()
|
|
except (OSError, IOError) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
|
|
continue
|
|
body = {'photoData': base64.urlsafe_b64encode(image_data).decode(UTF8)}
|
|
try:
|
|
callGAPI(cd.users().photos(), 'update',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID_INPUT, GAPI.CONDITION_NOT_MET],
|
|
userKey=user, body=body, fields='')
|
|
entityActionPerformed([Ent.USER, user, Ent.PHOTO, filename], i, count)
|
|
except (GAPI.invalidInput, GAPI.conditionNotMet) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
|
|
except (GAPI.userNotFound, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
|
|
# gam <UserTypeEntity> delete photo
|
|
def deletePhoto(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
callGAPI(cd.users().photos(), 'delete',
|
|
bailOnInternalError=True,
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.PHOTO_NOT_FOUND, GAPI.INTERNAL_ERROR],
|
|
userKey=user)
|
|
entityActionPerformed([Ent.USER, user, Ent.PHOTO, ''], i, count)
|
|
except (GAPI.photoNotFound, GAPI.internalError) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, ''], str(e), i, count)
|
|
except (GAPI.userNotFound, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
|
|
def getPhoto(users, profileMode):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
targetFolder = os.getcwd()
|
|
filenamePattern = '#email#.#ext#'
|
|
noDefault = returnURLonly = False
|
|
writeFileData = showPhotoData = True
|
|
size = ''
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'drivedir':
|
|
targetFolder = GC.Values[GC.DRIVE_DIR]
|
|
elif myarg == 'targetfolder':
|
|
targetFolder = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
elif myarg == 'filename':
|
|
filenamePattern = getString(Cmd.OB_PHOTO_FILENAME_PATTERN)
|
|
elif myarg == 'nofile':
|
|
writeFileData = False
|
|
elif myarg == 'noshow':
|
|
showPhotoData = False
|
|
elif profileMode and myarg == 'returnurlonly':
|
|
returnURLonly = True
|
|
elif myarg == 'nodefault':
|
|
noDefault = True
|
|
elif profileMode and myarg == 'size':
|
|
size = getInteger(minVal=50)
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if profileMode:
|
|
user, people = buildGAPIServiceObject(API.PEOPLE, user, i, count)
|
|
if not people:
|
|
continue
|
|
else:
|
|
user = normalizeEmailAddressOrUID(user)
|
|
_, userName, _ = splitEmailAddressOrUID(user)
|
|
filename = os.path.join(targetFolder, _substituteForUser(filenamePattern, user, userName))
|
|
try:
|
|
if not showPhotoData:
|
|
entityPerformActionNumItems([Ent.USER, user], 1, Ent.PHOTO, i, count)
|
|
if not profileMode:
|
|
photo = callGAPI(cd.users().photos(), 'get',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN, GAPI.PHOTO_NOT_FOUND],
|
|
userKey=user)
|
|
if showPhotoData:
|
|
writeStdout(photo['photoData']+'\n')
|
|
photo_data = base64.urlsafe_b64decode(photo['photoData'])
|
|
else:
|
|
result = callGAPI(people.people(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND],
|
|
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
|
resourceName='people/me', personFields='photos')
|
|
default = False
|
|
url = None
|
|
for photo in result.get('photos', []):
|
|
if photo['metadata']['source']['type'] == 'PROFILE':
|
|
default = photo.get('default', False)
|
|
url = photo['url']
|
|
break
|
|
if not url:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, None], Msg.PROFILE_PHOTO_NOT_FOUND, i, count)
|
|
continue
|
|
if noDefault and default:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, None], Msg.PROFILE_PHOTO_IS_DEFAULT, i, count)
|
|
continue
|
|
if returnURLonly:
|
|
writeStdout(f'{url}\n')
|
|
continue
|
|
if size:
|
|
url = re.sub(r"=s\d+$", f"=s{size}", url)
|
|
try:
|
|
status, photo_data = getHttpObj().request(url, 'GET')
|
|
if status['status'] != '200':
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], Msg.NOT_ALLOWED, i, count)
|
|
continue
|
|
if showPhotoData:
|
|
writeStdout(base64.encodebytes(photo_data).decode(UTF8))
|
|
except (httplib2.HttpLib2Error, google.auth.exceptions.TransportError, RuntimeError) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filename], str(e), i, count)
|
|
continue
|
|
if writeFileData:
|
|
if photo_data[:3] == b'\xff\xd8\xff':
|
|
extension = 'jpg'
|
|
elif photo_data[:8] == b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a':
|
|
extension = 'png'
|
|
elif photo_data[:6] == b'\x47\x49\x46\x38\x37\x61' or photo_data[:6] == b'\x47\x49\x46\x38\x39\x61':
|
|
extension = 'gif'
|
|
elif photo_data[:2] == b'\x42\x4d':
|
|
extension= 'bmp'
|
|
elif photo_data[:4] == b'\x49\x49\x2A\x00' or photo_data[:4] == b'\x4D\x4D\x00\x2A':
|
|
extension= 'tif'
|
|
else:
|
|
extension = 'img'
|
|
filenameExt = filename.replace('#ext#', extension)
|
|
status, e = writeFileReturnError(filenameExt, photo_data, mode='wb')
|
|
if status:
|
|
if not showPhotoData:
|
|
entityActionPerformed([Ent.USER, user, Ent.PHOTO, filenameExt], i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, filenameExt], str(e), i, count)
|
|
except (GAPI.notFound, GAPI.photoNotFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.PHOTO, None], str(e), i, count)
|
|
except (GAPI.userNotFound, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
|
|
# gam <UserTypeEntity> get photo [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]]
|
|
# [noshow] [nofile]
|
|
def getUserPhoto(users):
|
|
getPhoto(users, False)
|
|
|
|
# gam <UserTypeEntity> get profilephoto [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]
|
|
# [noshow] [nofile] [returnurlonly] [nodefault] [size <Integer>]
|
|
def getProfilePhoto(users):
|
|
getPhoto(users, True)
|
|
|
|
PROFILE_SHARING_CHOICE_MAP = {
|
|
'share': True,
|
|
'shared': True,
|
|
'unshare': False,
|
|
'unshared': False,
|
|
}
|
|
|
|
def _setShowProfile(users, function, **kwargs):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
result = callGAPI(cd.users(), function,
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.FORBIDDEN],
|
|
userKey=user, fields='includeInGlobalAddressList', **kwargs)
|
|
printEntity([Ent.USER, user, Ent.PROFILE_SHARING_ENABLED, result.get('includeInGlobalAddressList', UNKNOWN)], i, count)
|
|
except (GAPI.userNotFound, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
|
|
# gam <UserTypeEntity> profile share|shared|unshare|unshared
|
|
def setProfile(users):
|
|
body = {'includeInGlobalAddressList': getChoice(PROFILE_SHARING_CHOICE_MAP, mapChoice=True)}
|
|
_setShowProfile(users, 'update', body=body)
|
|
|
|
# gam <UserTypeEntity> show profile
|
|
def showProfile(users):
|
|
_setShowProfile(users, 'get')
|
|
|
|
# gam <UserTypeEntity> create sheet
|
|
# ((json [charset <Charset>] <SpreadsheetJSONCreateRequest>) |
|
|
# (json file <FileName> [charset <Charset>]))
|
|
# [<DriveFileParentAttribute>]
|
|
# [formatjson] [returnidonly]
|
|
def createSheet(users):
|
|
parameters = initDriveFileAttributes()
|
|
parentBody = {}
|
|
changeParents = returnIdOnly = False
|
|
addParents = ''
|
|
removeParents = ROOT
|
|
body = {}
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'json':
|
|
body = getJSON([])
|
|
elif getDriveFileParentAttribute(myarg, parameters):
|
|
changeParents = True
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
|
|
if not sheet:
|
|
continue
|
|
if changeParents:
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, parentBody, parameters):
|
|
continue
|
|
addParents = ','.join(parentBody['parents'])
|
|
try:
|
|
result = callGAPI(sheet.spreadsheets(), 'create',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
body=body)
|
|
spreadsheetId = result['spreadsheetId']
|
|
if not returnIdOnly and not FJQC.formatJSON:
|
|
entityActionPerformed([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], i, count)
|
|
parentId = ROOT
|
|
parentMsg = Act.SUCCESS
|
|
if changeParents:
|
|
try:
|
|
callGAPI(drive.files(), 'update',
|
|
throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.CANNOT_ADD_PARENT, GAPI.INSUFFICIENT_PARENT_PERMISSIONS],
|
|
fileId=result['spreadsheetId'],
|
|
addParents=addParents, removeParents=removeParents, fields='', supportsAllDrives=True)
|
|
parentId = addParents
|
|
except (GAPI.fileNotFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition,
|
|
GAPI.cannotAddParent) as e:
|
|
parentMsg = f'{ERROR_PREFIX}{addParents}: {str(e)}'
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
parentMsg = f'{ERROR_PREFIX}{addParents}: {str(e)}'
|
|
if returnIdOnly:
|
|
writeStdout(f'{spreadsheetId}\n')
|
|
continue
|
|
if FJQC.formatJSON:
|
|
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "parentId": "{parentId}", '\
|
|
'"parentAssignment": "{parentMsg}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
|
|
continue
|
|
Ind.Increment()
|
|
for field in ['spreadsheetId', 'spreadsheetUrl']:
|
|
printKeyValueList([field, result[field]])
|
|
printKeyValueList(['parentId', parentId])
|
|
printKeyValueList(['parentAssignment', parentMsg])
|
|
for field in ['properties', 'sheets', 'namedRanges', 'developerMetadata']:
|
|
if field in result:
|
|
showJSON(field, result[field])
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.internalError,
|
|
GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, ''], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
|
|
def _validateUserGetSpreadsheetIDs(user, i, count, fileIdEntity, showEntityType):
|
|
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.SPREADSHEET if showEntityType else None)
|
|
if jcount == 0:
|
|
return (user, None, 0)
|
|
user, sheet = buildGAPIServiceObject(API.SHEETS, user, i, count)
|
|
if not sheet:
|
|
return (user, None, 0)
|
|
return (user, sheet, jcount)
|
|
|
|
# gam <UserTypeEntity> update sheet <DriveFileEntity>
|
|
# ((json [charset <Charset>] <SpreadsheetJSONUpdateRequest>) |
|
|
# (json file <FileName> [charset <Charset>]))
|
|
# [formatjson]
|
|
def updateSheets(users):
|
|
spreadsheetIdEntity = getDriveFileEntity()
|
|
body = {}
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'json':
|
|
body = getJSON([])
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for spreadsheetId in spreadsheetIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(sheet.spreadsheets(), 'batchUpdate',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=spreadsheetId, body=body)
|
|
if FJQC.formatJSON:
|
|
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
|
|
continue
|
|
entityActionPerformed([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], j, jcount)
|
|
Ind.Increment()
|
|
for field in ['replies', 'updatedSpreadsheet']:
|
|
if field in result:
|
|
showJSON(field, result[field])
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
SPREADSHEET_FIELDS_CHOICE_MAP = {
|
|
'developermetadata': 'developerMetadata',
|
|
'namedranges': 'namedRanges',
|
|
'properties': 'properties',
|
|
'sheets': 'sheets',
|
|
'spreadsheetid': 'spreadsheetId',
|
|
'spreadsheeturl': 'spreadsheetUrl',
|
|
}
|
|
|
|
SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP = {
|
|
'properties': 'sheets.properties',
|
|
'data': 'sheets.data',
|
|
'merges': 'sheets.merges',
|
|
'conditionalformats': 'sheets.conditionalFormats',
|
|
'filterviews': 'sheets.filterViews',
|
|
'protectedranges': 'sheets.protectedRanges',
|
|
'basicfilter': 'sheets.basicFilter',
|
|
'charts': 'sheets.charts',
|
|
'bandedranges': 'sheets.bandedRanges',
|
|
'developermetadata': 'sheets.developerMetadata',
|
|
'rowgroups': 'sheets.rowGroups',
|
|
'columngroups': 'sheets.columnGroups',
|
|
'slicers': 'sheets.slicers',
|
|
}
|
|
|
|
# gam <UserTypeEntity> info|show sheet <DriveFileEntity>
|
|
# [fields <SpreadsheetFieldList>] [sheetsfields <SpreadsheetSheetsFieldList>]
|
|
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
|
|
# [includegriddata [<Boolean>]] [shownames]
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> print sheet <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# [fields <SpreadsheetFieldList>] [sheetsfields <SpreadsheetSheetsFieldList>]
|
|
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
|
|
# [includegriddata [<Boolean>]] [shownames]
|
|
# [formatjson [quotechar <Character>]]
|
|
def infoPrintShowSheets(users):
|
|
csvPF = CSVPrintFile(['User', 'spreadsheetId'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
spreadsheetIdEntity = getDriveFileEntity()
|
|
fieldsList = []
|
|
ranges = []
|
|
includeGridData = showSheetNames = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'range':
|
|
ranges.append(getString(Cmd.OB_SPREADSHEET_RANGE))
|
|
elif myarg == 'rangelist':
|
|
ranges.extend(convertEntityToList(getString(Cmd.OB_SPREADSHEET_RANGE_LIST), shlexSplit=True))
|
|
elif myarg == 'includegriddata':
|
|
includeGridData = getBoolean()
|
|
elif getFieldsList(myarg, SPREADSHEET_FIELDS_CHOICE_MAP, fieldsList, initialField='spreadsheetId'):
|
|
pass
|
|
elif myarg == 'sheetsfields':
|
|
for field in _getFieldsList():
|
|
if field in SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP:
|
|
fieldsList.append(SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP[field])
|
|
else:
|
|
invalidChoiceExit(field, SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP, True)
|
|
elif myarg == 'shownames':
|
|
showSheetNames = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and showSheetNames:
|
|
csvPF.AddTitles('spreadsheetName')
|
|
csvPF.SetSortAllTitles()
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles('spreadsheetName')
|
|
csvPF.MoveJSONTitlesToEnd(['JSON'])
|
|
if includeGridData and fieldsList:
|
|
fieldsList.append(SPREADSHEET_SHEETS_SUBFIELDS_CHOICE_MAP['data'])
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
if showSheetNames:
|
|
_, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for spreadsheetId in spreadsheetIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(sheet.spreadsheets(), 'get',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=spreadsheetId, ranges=ranges, includeGridData=includeGridData, fields=fields)
|
|
if not includeGridData and 'sheets' in result:
|
|
for usheet in result['sheets']:
|
|
usheet.pop('data', None)
|
|
if showSheetNames:
|
|
try:
|
|
spreadsheetName = callGAPI(drive.files(), 'get',
|
|
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
|
|
fileId=spreadsheetId, fields='name', supportsAllDrives=True)['name']
|
|
except (GAPI.fileNotFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
spreadsheetName = spreadsheetId
|
|
if not csvPF:
|
|
if FJQC.formatJSON:
|
|
baserow = {'User': user, 'spreadsheetId': spreadsheetId}
|
|
if showSheetNames:
|
|
baserow['spreadsheetName'] = spreadsheetName
|
|
baserow['JSON'] = result
|
|
printLine(json.dumps(baserow, ensure_ascii=False, sort_keys=False)+'\n')
|
|
continue
|
|
if showSheetNames:
|
|
printEntity([Ent.SPREADSHEET, f'{spreadsheetName}({spreadsheetId})'], j, jcount)
|
|
else:
|
|
printEntity([Ent.SPREADSHEET, spreadsheetId], j, jcount)
|
|
Ind.Increment()
|
|
if 'spreadsheetUrl' in result:
|
|
printKeyValueList(['spreadsheetUrl', result['spreadsheetUrl']])
|
|
for field in ['properties', 'sheets', 'namedRanges', 'developerMetadata', 'dataSources', 'dataSourceSchedules']:
|
|
if field in result:
|
|
if field != 'sheets':
|
|
showJSON(field, result[field])
|
|
else:
|
|
jcount = len(result[field])
|
|
j = 0
|
|
for usheet in result[field]:
|
|
j += 1
|
|
printEntity([Ent.SHEET, usheet.get('properties', {}).get('title', '')], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, usheet)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
baserow = {'User': user, 'spreadsheetId': spreadsheetId}
|
|
if showSheetNames:
|
|
baserow['spreadsheetName'] = spreadsheetName
|
|
row = flattenJSON(result, flattened=baserow.copy())
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
baserow['JSON'] = json.dumps(cleanJSON(result), ensure_ascii=False, sort_keys=False)
|
|
csvPF.WriteRowNoFilter(baserow)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Spreadsheet')
|
|
|
|
SHEET_VALUE_INPUT_OPTIONS_MAP = {
|
|
'raw': 'RAW',
|
|
'userentered': 'USER_ENTERED',
|
|
}
|
|
SHEET_DIMENSIONS_MAP = {
|
|
'rows': 'ROWS',
|
|
'columns': 'COLUMNS',
|
|
}
|
|
SHEET_VALUE_RENDER_OPTIONS_MAP = {
|
|
'formula': 'FORMULA',
|
|
'formattedvalue': 'FORMATTED_VALUE',
|
|
'unformattedvalue': 'UNFORMATTED_VALUE',
|
|
}
|
|
SHEET_DATETIME_RENDER_OPTIONS_MAP = {
|
|
'serialnumber': 'SERIAL_NUMBER',
|
|
'formattedstring': 'FORMATTED_STRING',
|
|
}
|
|
SHEET_INSERT_DATA_OPTIONS_MAP = {
|
|
'overwrite': 'OVERWRITE',
|
|
'insertrows': 'INSERT_ROWS',
|
|
}
|
|
|
|
def _getSpreadsheetRangesValues(append):
|
|
spreadsheetRangesValues = []
|
|
kwargs = {
|
|
'valueInputOption': 'USER_ENTERED',
|
|
'includeValuesInResponse': False,
|
|
'responseValueRenderOption': 'FORMATTED_VALUE',
|
|
'responseDateTimeRenderOption': 'FORMATTED_STRING',
|
|
}
|
|
if append:
|
|
kwargs['insertDataOption'] = 'INSERT_ROWS'
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'json':
|
|
if append and spreadsheetRangesValues:
|
|
usageErrorExit(Msg.ONLY_ONE_JSON_RANGE_ALLOWED)
|
|
spreadsheetRangeValue = getJSON([])
|
|
if isinstance(spreadsheetRangeValue, dict) and 'valueRanges' in spreadsheetRangeValue:
|
|
spreadsheetRangesValues.extend(spreadsheetRangeValue['valueRanges'])
|
|
elif isinstance(spreadsheetRangeValue, list):
|
|
spreadsheetRangesValues.extend(spreadsheetRangeValue)
|
|
else:
|
|
spreadsheetRangesValues.append(spreadsheetRangeValue)
|
|
if append and len(spreadsheetRangesValues) > 1:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.ONLY_ONE_JSON_RANGE_ALLOWED)
|
|
elif myarg in SHEET_VALUE_INPUT_OPTIONS_MAP:
|
|
kwargs['valueInputOption'] = SHEET_VALUE_INPUT_OPTIONS_MAP[myarg]
|
|
elif myarg == 'includevaluesinresponse':
|
|
kwargs['includeValuesInResponse'] = getBoolean()
|
|
elif myarg in SHEET_VALUE_RENDER_OPTIONS_MAP:
|
|
kwargs['responseValueRenderOption'] = SHEET_VALUE_RENDER_OPTIONS_MAP[myarg]
|
|
elif myarg in SHEET_DATETIME_RENDER_OPTIONS_MAP:
|
|
kwargs['responseDateTimeRenderOption'] = SHEET_DATETIME_RENDER_OPTIONS_MAP[myarg]
|
|
elif append and myarg in SHEET_INSERT_DATA_OPTIONS_MAP:
|
|
kwargs['insertDataOption'] = SHEET_INSERT_DATA_OPTIONS_MAP[myarg]
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
return (kwargs, spreadsheetRangesValues, FJQC)
|
|
|
|
def _showValueRange(valueRange):
|
|
Ind.Increment()
|
|
printKeyValueList(['majorDimension', valueRange['majorDimension']])
|
|
printKeyValueList(['range', valueRange['range']])
|
|
printKeyValueList(['value', '{'+f'"values": {json.dumps(valueRange.get("values", []), ensure_ascii=False, sort_keys=False)}'+'}'])
|
|
Ind.Decrement()
|
|
|
|
def _showUpdateValuesResponse(result, k, kcount):
|
|
printKeyValueListWithCount(['updatedRange', result['updatedRange']], k, kcount)
|
|
Ind.Increment()
|
|
for field in ['updatedRows', 'updatedColumns', 'updatedCells']:
|
|
printKeyValueList([field, result[field]])
|
|
if 'updatedData' in result:
|
|
printKeyValueList(['updatedData', ''])
|
|
_showValueRange(result['updatedData'])
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> append sheetrange <DriveFileEntity>
|
|
# ((json [charset <Charset>] <SpreadsheetJSONRangeValues>|<SpreadsheetJSONRangeValuesList>) |
|
|
# (json file <FileName> [charset <Charset>]))
|
|
# [overwrite|insertrows]
|
|
# [raw|userentered] [serialnumber|formattedstring] [formula|formattedvalue|unformattedvalue]
|
|
# [includevaluesinresponse [<Boolean>]] [formatjson]
|
|
def appendSheetRanges(users):
|
|
spreadsheetIdEntity = getDriveFileEntity()
|
|
kwargs, spreadsheetRangesValues, FJQC = _getSpreadsheetRangesValues(True)
|
|
kcount = len(spreadsheetRangesValues)
|
|
body = spreadsheetRangesValues[0] if kcount > 0 else {}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for spreadsheetId in spreadsheetIdEntity['list']:
|
|
j += 1
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], kcount, Ent.SPREADSHEET_RANGE, j, jcount)
|
|
Ind.Increment()
|
|
k = 1
|
|
try:
|
|
result = callGAPI(sheet.spreadsheets().values(), 'append',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=spreadsheetId, range=body['range'], body=body, **kwargs)
|
|
if FJQC.formatJSON:
|
|
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
|
|
continue
|
|
for field in ['tableRange']:
|
|
if field in result:
|
|
printKeyValueList([field, result[field]])
|
|
_showUpdateValuesResponse(result['updates'], k, kcount)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> update sheetrange <DriveFileEntity>
|
|
# ((json [charset <Charset>] <SpreadsheetJSONRangeValues>|<SpreadsheetJSONRangeValuesList>)+
|
|
# (json file <FileName> [charset <Charset>]))+
|
|
# [raw|userentered] [serialnumber|formattedstring] [formula|formattedvalue|unformattedvalue]
|
|
# [includevaluesinresponse [<Boolean>]] [formatjson]
|
|
def updateSheetRanges(users):
|
|
spreadsheetIdEntity = getDriveFileEntity()
|
|
body, spreadsheetRangesValues, FJQC = _getSpreadsheetRangesValues(False)
|
|
body['data'] = spreadsheetRangesValues
|
|
kcount = len(spreadsheetRangesValues)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for spreadsheetId in spreadsheetIdEntity['list']:
|
|
j += 1
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], kcount, Ent.SPREADSHEET_RANGE, j, jcount)
|
|
Ind.Increment()
|
|
try:
|
|
result = callGAPI(sheet.spreadsheets().values(), 'batchUpdate',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=spreadsheetId, body=body)
|
|
if FJQC.formatJSON:
|
|
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
|
|
continue
|
|
for field in ['totalUpdatedRows', 'totalUpdatedColumns', 'totalUpdatedCells', 'totalUpdatedSheets']:
|
|
printKeyValueList([field, result[field]])
|
|
k = 0
|
|
for response in result.get('responses', []):
|
|
k += 1
|
|
_showUpdateValuesResponse(response, k, kcount)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> clear sheetrange <DriveFileEntity>
|
|
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
|
|
# [formatjson]
|
|
def clearSheetRanges(users):
|
|
spreadsheetIdEntity = getDriveFileEntity()
|
|
body = {'ranges': []}
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'range':
|
|
body['ranges'].append(getString(Cmd.OB_SPREADSHEET_RANGE))
|
|
elif myarg == 'rangelist':
|
|
body['ranges'].extend(convertEntityToList(getString(Cmd.OB_SPREADSHEET_RANGE_LIST), shlexSplit=True))
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
kcount = len(body['ranges'])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for spreadsheetId in spreadsheetIdEntity['list']:
|
|
j += 1
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], kcount, Ent.SPREADSHEET_RANGE, j, jcount)
|
|
Ind.Increment()
|
|
try:
|
|
result = callGAPIitems(sheet.spreadsheets().values(), 'batchClear', 'clearedRanges',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=spreadsheetId, body=body)
|
|
if FJQC.formatJSON:
|
|
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps({"clearedRanges": result}, ensure_ascii=False, sort_keys=False)}'+'}')
|
|
continue
|
|
k = 0
|
|
for clearedRange in result:
|
|
k += 1
|
|
printKeyValueListWithCount(['range', clearedRange], k, kcount)
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print sheetrange <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
|
|
# [rows|columns] [serialnumber|formattedstring] [formula|formattedvalue|unformattedvalue]
|
|
# [formatjson [quotechar <Character>] [valuerangesonly [<Boolean>]]]
|
|
# gam <UserTypeEntity> show sheetrange <DriveFileEntity>
|
|
# (range <SpreadsheetRange>)* (rangelist <SpreadsheetRangeList>)*
|
|
# [rows|columns] [serialnumber|formattedstring] [formula|formattedvalue|unformattedvalue]
|
|
# [formatjson [valuerangesonly [<Boolean>]]]
|
|
def printShowSheetRanges(users):
|
|
csvPF = CSVPrintFile(['User', 'spreadsheetId'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
spreadsheetIdEntity = getDriveFileEntity()
|
|
spreadsheetRanges = []
|
|
kwargs = {
|
|
'majorDimension': 'ROWS',
|
|
'valueRenderOption': 'FORMATTED_VALUE',
|
|
'dateTimeRenderOption': 'FORMATTED_STRING',
|
|
}
|
|
valueRangesOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'range':
|
|
spreadsheetRanges.append(getString(Cmd.OB_SPREADSHEET_RANGE))
|
|
elif myarg == 'rangelist':
|
|
spreadsheetRanges.extend(convertEntityToList(getString(Cmd.OB_SPREADSHEET_RANGE_LIST), shlexSplit=True))
|
|
elif myarg == 'valuerangesonly':
|
|
valueRangesOnly = getBoolean()
|
|
elif myarg in SHEET_DIMENSIONS_MAP:
|
|
kwargs['majorDimension'] = SHEET_DIMENSIONS_MAP[myarg]
|
|
elif myarg in SHEET_VALUE_RENDER_OPTIONS_MAP:
|
|
kwargs['valueRenderOption'] = SHEET_VALUE_RENDER_OPTIONS_MAP[myarg]
|
|
elif myarg in SHEET_DATETIME_RENDER_OPTIONS_MAP:
|
|
kwargs['dateTimeRenderOption'] = SHEET_DATETIME_RENDER_OPTIONS_MAP[myarg]
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and FJQC.formatJSON and valueRangesOnly:
|
|
csvPF.SetJSONTitles(['JSON'])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sheet, jcount = _validateUserGetSpreadsheetIDs(user, i, count, spreadsheetIdEntity, not csvPF and not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for spreadsheetId in spreadsheetIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(sheet.spreadsheets().values(), 'batchGet',
|
|
throwReasons=GAPI.SHEETS_ACCESS_THROW_REASONS,
|
|
spreadsheetId=spreadsheetId, ranges=spreadsheetRanges, fields='valueRanges', **kwargs)
|
|
valueRanges = result.get('valueRanges', [])
|
|
kcount = len(valueRanges)
|
|
if not csvPF:
|
|
if FJQC.formatJSON:
|
|
if not valueRangesOnly:
|
|
printLine('{'+f'"User": "{user}", "spreadsheetId": "{spreadsheetId}", "JSON": {json.dumps(result, ensure_ascii=False, sort_keys=False)}'+'}')
|
|
else:
|
|
printLine(json.dumps(result.get('valueRanges', []), ensure_ascii=False, sort_keys=False))
|
|
continue
|
|
entityPerformActionNumItems([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], kcount, Ent.SPREADSHEET_RANGE, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for valueRange in valueRanges:
|
|
k += 1
|
|
printKeyValueListWithCount(['range', valueRange['range']], k, kcount)
|
|
_showValueRange(valueRange)
|
|
Ind.Decrement()
|
|
else:
|
|
if kcount:
|
|
row = flattenJSON(result, flattened={'User': user, 'spreadsheetId': spreadsheetId})
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
if not valueRangesOnly:
|
|
csvPF.WriteRowNoFilter({'User': user, 'spreadsheetId': spreadsheetId,
|
|
'JSON': json.dumps(result, ensure_ascii=False, sort_keys=False)})
|
|
else:
|
|
csvPF.WriteRowNoFilter({'JSON': json.dumps(result.get('valueRanges', []), ensure_ascii=False, sort_keys=False)})
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'User': user})
|
|
except (GAPI.notFound, GAPI.forbidden, GAPI.permissionDenied,
|
|
GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.badRequest,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SPREADSHEET, spreadsheetId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Spreadsheet')
|
|
|
|
# Token commands utilities
|
|
def commonClientIds(clientId):
|
|
if clientId == 'gasmo':
|
|
return '1095133494869.apps.googleusercontent.com'
|
|
return clientId
|
|
|
|
# gam <UserTypeEntity> delete token|tokens|3lo|oauth clientid <ClientID>
|
|
def deleteTokens(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
checkArgumentPresent('clientid', required=True)
|
|
clientId = commonClientIds(getString(Cmd.OB_CLIENT_ID))
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
callGAPI(cd.tokens(), 'get',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN,
|
|
GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
|
|
userKey=user, clientId=clientId, fields='')
|
|
callGAPI(cd.tokens(), 'delete',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN,
|
|
GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
|
|
userKey=user, clientId=clientId)
|
|
entityActionPerformed([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], i, count)
|
|
except (GAPI.notFound, GAPI.resourceNotFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], str(e), i, count)
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
|
|
TOKENS_FIELDS_TITLES = ['clientId', 'displayText', 'anonymous', 'nativeApp', 'userKey', 'scopes']
|
|
TOKENS_AGGREGATE_FIELDS_TITLES = ['clientId', 'displayText', 'anonymous', 'nativeApp', 'users', 'scopes']
|
|
TOKENS_AGGREGATE_ORDERBY_CHOICE_MAP = {
|
|
'clientid': 'clientId',
|
|
'id': 'clientId',
|
|
'displaytext': 'displayText',
|
|
'appname': 'displayText',
|
|
}
|
|
TOKENS_TITLE_MAP = {
|
|
'clientId': 'Client ID',
|
|
'displayText': 'App Name',
|
|
'user': 'user',
|
|
}
|
|
|
|
def _printShowTokens(entityType, users):
|
|
def _printToken(token):
|
|
row = {}
|
|
for item in token:
|
|
if item != 'scopes':
|
|
row[item] = token.get(item, '')
|
|
else:
|
|
row[item] = delimiter.join(token.get('scopes', []))
|
|
csvPF.WriteRow(row)
|
|
|
|
def _showToken(token, keyTitle, keyField, j, jcount):
|
|
printKeyValueListWithCount([keyTitle, token[keyField]], j, jcount)
|
|
Ind.Increment()
|
|
for item in sorted(token):
|
|
if item not in {keyField, 'scopes'}:
|
|
printKeyValueList([item, token.get(item, '')])
|
|
item = 'scopes'
|
|
printKeyValueList([item, None])
|
|
Ind.Increment()
|
|
for it in sorted(token.get(item, [])):
|
|
printKeyValueList([it])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
clientId = None
|
|
aggregateUsersBy = ''
|
|
orderBy = 'clientId'
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
aggregateTokensById = {}
|
|
tokenNameIdMap = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'clientid':
|
|
clientId = commonClientIds(getString(Cmd.OB_CLIENT_ID))
|
|
elif myarg == 'orderby':
|
|
orderBy = getChoice(TOKENS_AGGREGATE_ORDERBY_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'aggregateusersby':
|
|
aggregateUsersBy = getChoice(TOKENS_AGGREGATE_ORDERBY_CHOICE_MAP, mapChoice=True)
|
|
if aggregateUsersBy == 'displayText':
|
|
tokenNameIdMap = {}
|
|
elif myarg == 'usertokencounts':
|
|
aggregateUsersBy = 'user'
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif not entityType:
|
|
Cmd.Backup()
|
|
entityType, users = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not entityType:
|
|
users = getItemsToModify(Cmd.ENTITY_ALL_USERS_NS, None)
|
|
if csvPF:
|
|
if not aggregateUsersBy:
|
|
csvPF.SetTitles(['user']+TOKENS_FIELDS_TITLES)
|
|
elif aggregateUsersBy != 'user':
|
|
csvPF.SetTitles(TOKENS_AGGREGATE_FIELDS_TITLES)
|
|
else:
|
|
csvPF.SetTitles(['user', 'tokenCount'])
|
|
else:
|
|
if not aggregateUsersBy:
|
|
tokenTitle = TOKENS_TITLE_MAP[orderBy]
|
|
else:
|
|
tokenTitle = TOKENS_TITLE_MAP[aggregateUsersBy]
|
|
fields = ','.join(TOKENS_FIELDS_TITLES)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
try:
|
|
if csvPF or aggregateUsersBy:
|
|
printGettingEntityItemForWhom(Ent.ACCESS_TOKEN, user, i, count)
|
|
if clientId:
|
|
results = [callGAPI(cd.tokens(), 'get',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN, GAPI.BAD_REQUEST,
|
|
GAPI.NOT_FOUND, GAPI.RESOURCE_NOT_FOUND],
|
|
userKey=user, clientId=clientId, fields=fields)]
|
|
else:
|
|
results = callGAPIitems(cd.tokens(), 'list', 'items',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN, GAPI.BAD_REQUEST],
|
|
userKey=user, fields=f'items({fields})')
|
|
jcount = len(results)
|
|
if not aggregateUsersBy:
|
|
if not csvPF:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.ACCESS_TOKEN, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for token in sorted(results, key=lambda k: k[orderBy]):
|
|
j += 1
|
|
_showToken(token, tokenTitle, orderBy, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
if results:
|
|
for token in sorted(results, key=lambda k: k[orderBy]):
|
|
row = {'user': user, 'scopes': delimiter.join(token.get('scopes', []))}
|
|
for item in token:
|
|
if item != 'scopes':
|
|
row[item] = token.get(item, '')
|
|
csvPF.WriteRow(row)
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'user': user})
|
|
elif aggregateUsersBy != 'user':
|
|
if results:
|
|
for token in results:
|
|
tokcid = token['clientId']
|
|
if tokcid not in aggregateTokensById:
|
|
token.pop('userKey', None)
|
|
token['users'] = 0
|
|
aggregateTokensById[tokcid] = token
|
|
aggregateTokensById[tokcid]['users'] += 1
|
|
if tokenNameIdMap is not None:
|
|
tokname = token['displayText']
|
|
if tokname not in tokenNameIdMap:
|
|
tokenNameIdMap[tokname] = set()
|
|
tokenNameIdMap[tokname].add(tokcid)
|
|
else: # aggregateUsersBy == 'user':
|
|
aggregateTokensById[user] = jcount
|
|
except (GAPI.notFound, GAPI.resourceNotFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], str(e), i, count)
|
|
except (GAPI.userNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
if aggregateUsersBy == 'clientId':
|
|
if not csvPF:
|
|
jcount = len(aggregateTokensById)
|
|
performActionNumItems(jcount, Ent.ACCESS_TOKEN)
|
|
Ind.Increment()
|
|
j = 0
|
|
for _, token in sorted(iter(aggregateTokensById.items())):
|
|
j += 1
|
|
_showToken(token, tokenTitle, aggregateUsersBy, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for _, token in sorted(iter(aggregateTokensById.items())):
|
|
_printToken(token)
|
|
elif aggregateUsersBy == 'displayText':
|
|
if not csvPF:
|
|
jcount = len(aggregateTokensById)
|
|
performActionNumItems(jcount, Ent.ACCESS_TOKEN)
|
|
Ind.Increment()
|
|
j = 0
|
|
for _, tokenIds in sorted(iter(tokenNameIdMap.items())):
|
|
for tokcid in sorted(tokenIds):
|
|
j += 1
|
|
_showToken(aggregateTokensById[tokcid], tokenTitle, aggregateUsersBy, j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for _, tokenIds in sorted(iter(tokenNameIdMap.items())):
|
|
for tokcid in sorted(tokenIds):
|
|
_printToken(aggregateTokensById[tokcid])
|
|
else: # aggregateUsersBy == 'user':
|
|
if not csvPF:
|
|
jcount = len(aggregateTokensById)
|
|
j = 0
|
|
for user, count in sorted(iter(aggregateTokensById.items())):
|
|
j += 1
|
|
printEntityKVList([Ent.USER, user], [Ent.Plural(Ent.ACCESS_TOKEN), count], j, jcount)
|
|
else:
|
|
for user, count in sorted(iter(aggregateTokensById.items())):
|
|
csvPF.WriteRow({'user': user, 'tokenCount': count})
|
|
if csvPF:
|
|
csvPF.writeCSVfile('OAuth Tokens')
|
|
|
|
# gam <UserTypeEntity> print tokens|token [todrive <ToDriveAttribute>*] [clientid <ClientID>]
|
|
# [usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)] [delimiter <Character>]
|
|
# gam <UserTypeEntity> show tokens|token|3lo|oauth [clientid <ClientID>]
|
|
# [usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)] [delimiter <Character>]
|
|
def printShowTokens(users):
|
|
_printShowTokens(Cmd.ENTITY_USERS, users)
|
|
|
|
# gam print tokens|token [todrive <ToDriveAttribute>*] [clientid <ClientID>]
|
|
# [usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)] [delimiter <Character>]
|
|
# [<UserTypeEntity>]
|
|
# gam show tokens|token [clientid <ClientID>]
|
|
# [usertokencounts|(aggregateusersby|orderby clientid|id|appname|displaytext)] [delimiter <Character>]
|
|
# [<UserTypeEntity>]
|
|
def doPrintShowTokens():
|
|
_printShowTokens(None, None)
|
|
|
|
# gam <UserTypeEntity> deprovision|deprov [popimap] [signout] [turnoff2sv]
|
|
def deprovisionUser(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
disablePopImap = signout = turnoff2sv = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'popimap':
|
|
disablePopImap = True
|
|
elif myarg == 'signout':
|
|
signout = True
|
|
elif myarg == 'turnoff2sv':
|
|
turnoff2sv = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if disablePopImap:
|
|
imapBody = _imapDefaults(False)
|
|
popBody = _popDefaults(False)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user = normalizeEmailAddressOrUID(user)
|
|
userSuspended = checkUserSuspended(cd, user, Ent.USER, i, count)
|
|
if userSuspended is None:
|
|
continue
|
|
try:
|
|
printGettingEntityItemForWhom(Ent.APPLICATION_SPECIFIC_PASSWORD, user, i, count)
|
|
asps = callGAPIitems(cd.asps(), 'list', 'items',
|
|
throwReasons=[GAPI.USER_NOT_FOUND],
|
|
userKey=user, fields='items(codeId)')
|
|
codeIds = [asp['codeId'] for asp in asps]
|
|
jcount = len(codeIds)
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.APPLICATION_SPECIFIC_PASSWORD, i, count)
|
|
if jcount > 0:
|
|
Ind.Increment()
|
|
j = 0
|
|
for codeId in codeIds:
|
|
j += 1
|
|
try:
|
|
callGAPI(cd.asps(), 'delete',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_PARAMETER, GAPI.FORBIDDEN],
|
|
userKey=user, codeId=codeId)
|
|
entityActionPerformed([Ent.USER, user, Ent.APPLICATION_SPECIFIC_PASSWORD, codeId], j, jcount)
|
|
except (GAPI.invalid, GAPI.invalidParameter, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.APPLICATION_SPECIFIC_PASSWORD, codeId], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
#
|
|
printGettingEntityItemForWhom(Ent.BACKUP_VERIFICATION_CODES, user, i, count)
|
|
try:
|
|
codes = callGAPIitems(cd.verificationCodes(), 'list', 'items',
|
|
throwReasons=[GAPI.USER_NOT_FOUND],
|
|
userKey=user, fields='items(verificationCode)')
|
|
jcount = len(codes)
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.BACKUP_VERIFICATION_CODES, i, count)
|
|
if jcount > 0:
|
|
if not userSuspended:
|
|
callGAPI(cd.verificationCodes(), 'invalidate',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.INVALID_INPUT],
|
|
userKey=user)
|
|
entityActionPerformed([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None], i, count)
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None],
|
|
Msg.IS_SUSPENDED_NO_BACKUPCODES, i, count)
|
|
except (GAPI.invalid, GAPI.invalidInput) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.BACKUP_VERIFICATION_CODES, None], str(e), i, count)
|
|
#
|
|
printGettingEntityItemForWhom(Ent.ACCESS_TOKEN, user, i, count)
|
|
tokens = callGAPIitems(cd.tokens(), 'list', 'items',
|
|
throwReasons=[GAPI.USER_NOT_FOUND],
|
|
userKey=user, fields='items(clientId)')
|
|
jcount = len(tokens)
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.ACCESS_TOKEN, i, count)
|
|
if jcount > 0:
|
|
Ind.Increment()
|
|
j = 0
|
|
for token in tokens:
|
|
j += 1
|
|
clientId = token['clientId']
|
|
try:
|
|
callGAPI(cd.tokens(), 'delete',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.NOT_FOUND],
|
|
userKey=user, clientId=clientId)
|
|
entityActionPerformed([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], j, jcount)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.ACCESS_TOKEN, clientId], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
#
|
|
if turnoff2sv:
|
|
Act.Set(Act.TURNOFF2SV)
|
|
try:
|
|
callGAPI(cd.twoStepVerification(), 'turnOff',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN],
|
|
userKey=user)
|
|
entityActionPerformed([Ent.USER, user], i, count)
|
|
except GAPI.invalid as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
#
|
|
if signout:
|
|
Act.Set(Act.SIGNOUT)
|
|
callGAPI(cd.users(), 'signOut',
|
|
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.INVALID, GAPI.DOMAIN_NOT_FOUND,
|
|
GAPI.DOMAIN_CANNOT_USE_APIS, GAPI.FORBIDDEN],
|
|
userKey=user)
|
|
entityActionPerformed([Ent.USER, user], i, count)
|
|
#
|
|
Act.Set(Act.DEPROVISION)
|
|
if disablePopImap:
|
|
_setImap(user, imapBody, i, count)
|
|
_setPop(user, popBody, i, count)
|
|
#
|
|
entityActionPerformed([Ent.USER, user], i, count)
|
|
except GAPI.userNotFound:
|
|
entityUnknownWarning(Ent.USER, user, i, count)
|
|
except (GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
|
|
# gam <UserTypeEntity> watch gmail [maxmessages <Integer>]
|
|
def watchGmail(users):
|
|
maxMessages = 100
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'maxmessages':
|
|
maxMessages = getInteger(minVal=1)
|
|
else:
|
|
unknownArgumentExit()
|
|
project = f'projects/{_getCurrentProjectId()}'
|
|
gamTopics = project+'/topics/gam-pubsub-gmail-'
|
|
gamSubscriptions = project+'/subscriptions/gam-pubsub-gmail-'
|
|
pubsub = buildGAPIObject(API.PUBSUB)
|
|
topics = callGAPIpages(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())
|
|
callGAPI(pubsub.projects().topics(), 'create',
|
|
name=topic)
|
|
body = {'policy': {'bindings': [{'members': ['serviceAccount:gmail-api-push@system.gserviceaccount.com'], 'role': 'roles/pubsub.editor'}]}}
|
|
callGAPI(pubsub.projects().topics(), 'setIamPolicy',
|
|
resource=topic, body=body)
|
|
subscriptions = callGAPIpages(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())
|
|
callGAPI(pubsub.projects().subscriptions(), 'create',
|
|
name=subscription, body={'topic': topic})
|
|
gmails = {}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
gmails[user] = {'g': gmail}
|
|
callGAPI(gmails[user]['g'].users(), 'watch',
|
|
userId='me', body={'topicName': topic})
|
|
gmails[user]['seen_historyId'] = callGAPI(gmails[user]['g'].users(), 'getProfile',
|
|
userId='me', fields='historyId')['historyId']
|
|
entityPerformActionNumItems([Ent.EVENT, 'gmail'], count, Ent.USER)
|
|
while True:
|
|
results = callGAPI(pubsub.projects().subscriptions(), 'pull',
|
|
subscription=subscription, body={'maxMessages': maxMessages})
|
|
if 'receivedMessages' in results:
|
|
ackIds = []
|
|
update_history = []
|
|
for message in results['receivedMessages']:
|
|
if 'data' in message['message']:
|
|
try:
|
|
decoded_message = json.loads(base64.b64decode(message['message']['data']))
|
|
if 'historyId' in decoded_message:
|
|
update_history.append(decoded_message['emailAddress'])
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError):
|
|
pass
|
|
if 'ackId' in message:
|
|
ackIds.append(message['ackId'])
|
|
if ackIds:
|
|
callGAPI(pubsub.projects().subscriptions(), 'acknowledge',
|
|
subscription=subscription, body={'ackIds': ackIds})
|
|
if update_history:
|
|
for a_user in update_history:
|
|
results = callGAPI(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:
|
|
Act.Set(Act.ADD)
|
|
for labelling in history['labelsAdded']:
|
|
entityActionPerformed([Ent.USER, a_user, Ent.MESSAGE, labelling['message']['id'],
|
|
Ent.LABEL, ', '.join(labelling['labelIds'])])
|
|
if 'labelsRemoved' in history:
|
|
Act.Set(Act.REMOVE)
|
|
for labelling in history['labelsRemoved']:
|
|
entityActionPerformed([Ent.USER, a_user, Ent.MESSAGE, labelling['message']['id'],
|
|
Ent.LABEL, ', '.join(labelling['labelIds'])])
|
|
if 'messagesAdded' in history:
|
|
Act.Set(Act.CREATE)
|
|
for adding in history['messagesAdded']:
|
|
entityActionPerformed([Ent.USER, a_user, Ent.MESSAGE, adding['message']['id'],
|
|
Ent.LABEL, ', '.join(adding['message']['labelIds'])])
|
|
if 'messagesDeleted' in history:
|
|
Act.Set(Act.DELETE)
|
|
for deleting in history['messagesDeleted']:
|
|
entityActionPerformed([Ent.USER, a_user, Ent.MESSAGE, deleting['message']['id']])
|
|
gmails[a_user]['seen_historyId'] = results['historyId']
|
|
|
|
# gam <UserTypeEntity> print gmailprofile [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show gmailprofile
|
|
def printShowGmailProfile(users):
|
|
csvPF = CSVPrintFile(['emailAddress'], 'sortall') if Act.csvFormat() else None
|
|
getTodriveOnly(csvPF)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.GMAIL_PROFILE, user, i, count)
|
|
try:
|
|
results = callGAPI(gmail.users(), 'getProfile',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
if not csvPF:
|
|
kvList = []
|
|
for item in ['historyId', 'messagesTotal', 'threadsTotal']:
|
|
kvList += [item, results[item]]
|
|
printEntityKVList([Ent.USER, user], kvList, i, count)
|
|
else:
|
|
csvPF.WriteRowTitles(results)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Gmail Profiles')
|
|
|
|
def _getUserGmailLabels(gmail, user, i, count, fields):
|
|
try:
|
|
labels = callGAPI(gmail.users().labels(), 'list',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me', fields=fields)
|
|
if not labels:
|
|
labels = {'labels': []}
|
|
return labels
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
return None
|
|
|
|
def _getLabelId(labels, labelName):
|
|
for label in labels['labels']:
|
|
if (labelName.upper() == label['id']) or (labelName.lower() in {label['id'].lower(), label['name'].lower()}):
|
|
return label['id']
|
|
return None
|
|
|
|
def _getLabelName(labels, labelId):
|
|
for label in labels['labels']:
|
|
if label['id'] == labelId:
|
|
return label['name']
|
|
return labelId
|
|
|
|
def _getLabelSet(labels):
|
|
labelSet = {}
|
|
for ulabel in labels['labels']:
|
|
if ulabel['type'] != LABEL_TYPE_SYSTEM:
|
|
labelSet[ulabel['name']] = ulabel['id']
|
|
return labelSet
|
|
|
|
LABEL_LABEL_LIST_VISIBILITY_CHOICE_MAP = {
|
|
'hide': 'labelHide',
|
|
'show': 'labelShow',
|
|
'showifunread': 'labelShowIfUnread',
|
|
}
|
|
LABEL_MESSAGE_LIST_VISIBILITY_CHOICES = ['hide', 'show']
|
|
LABEL_TYPE_SYSTEM = 'system'
|
|
LABEL_TYPE_USER = 'user'
|
|
|
|
def getLabelAttributes(myarg, body):
|
|
if myarg == 'labellistvisibility':
|
|
body['labelListVisibility'] = getChoice(LABEL_LABEL_LIST_VISIBILITY_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'messagelistvisibility':
|
|
body['messageListVisibility'] = getChoice(LABEL_MESSAGE_LIST_VISIBILITY_CHOICES)
|
|
elif myarg in {'backgroundcolor', 'backgroundcolour'}:
|
|
body.setdefault('color', {})
|
|
body['color']['backgroundColor'] = getLabelColor(LABEL_BACKGROUND_COLORS)
|
|
elif myarg in {'textcolor', 'textcolour'}:
|
|
body.setdefault('color', {})
|
|
body['color']['textColor'] = getLabelColor(LABEL_TEXT_COLORS)
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
def checkLabelColor(body):
|
|
if 'color' not in body:
|
|
return
|
|
if 'backgroundColor' in body['color']:
|
|
if 'textColor' in body['color']:
|
|
return
|
|
missingArgumentExit('textcolor <LabelColorHex>')
|
|
missingArgumentExit('backgroundcolor <LabelColorHex>')
|
|
|
|
def buildLabelPath(gmail, user, i, count, body, label, labelSet, l=0, lcount=0):
|
|
label = label.strip('/')
|
|
if label in labelSet:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], Msg.DUPLICATE, l, lcount)
|
|
return
|
|
labelParts = label.split('/')
|
|
invalid = False
|
|
for j, labelPart in enumerate(labelParts):
|
|
labelParts[j] = labelPart.strip()
|
|
if not labelParts[j]:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], Msg.INVALID, l, lcount)
|
|
invalid = True
|
|
break
|
|
if invalid:
|
|
return
|
|
decrement = False
|
|
duplicate = True
|
|
labelPath = ''
|
|
j = 0
|
|
for k, labelPart in enumerate(labelParts):
|
|
if labelPath != '':
|
|
labelPath += '/'
|
|
labelPath += labelPart
|
|
if labelPath not in labelSet:
|
|
if duplicate:
|
|
jcount = len(labelParts)-k
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LABEL, i, count)
|
|
Ind.Increment()
|
|
decrement = True
|
|
duplicate = False
|
|
j += 1
|
|
body['name'] = labelPath
|
|
try:
|
|
newLabel = callGAPI(gmail.users().labels(), 'create',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.DUPLICATE],
|
|
userId='me', body=body, fields='id,name')
|
|
labelSet[newLabel['name']] = newLabel['id']
|
|
entityActionPerformed([Ent.USER, user, Ent.LABEL, labelPath], j, jcount)
|
|
except GAPI.duplicate:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, labelPath], Msg.DUPLICATE, j, jcount)
|
|
break
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
if duplicate:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, labelPath], Msg.DUPLICATE, l, lcount)
|
|
if decrement:
|
|
Ind.Decrement()
|
|
|
|
def createLabels(users, labelEntity):
|
|
body = {}
|
|
buildPath = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'buildpath':
|
|
buildPath = getBoolean()
|
|
else:
|
|
getLabelAttributes(myarg, body)
|
|
checkLabelColor(body)
|
|
if not isinstance(labelEntity, dict):
|
|
userLabelList = None
|
|
labelList = labelEntity
|
|
else:
|
|
userLabelList = labelEntity
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if userLabelList:
|
|
labelList = userLabelList[origUser]
|
|
lcount = len(labelList)
|
|
if buildPath:
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
|
|
if not labels:
|
|
continue
|
|
labelSet = _getLabelSet(labels)
|
|
entityPerformActionNumItems([Ent.USER, user], lcount, Ent.LABEL, i, count)
|
|
Ind.Increment()
|
|
l = 0
|
|
for label in labelList:
|
|
l += 1
|
|
body['name'] = label
|
|
if not buildPath:
|
|
try:
|
|
callGAPI(gmail.users().labels(), 'create',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.DUPLICATE, GAPI.INVALID, GAPI.PERMISSION_DENIED],
|
|
userId='me', body=body, fields='')
|
|
entityActionPerformed([Ent.USER, user, Ent.LABEL, label], l, lcount)
|
|
except GAPI.duplicate:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], Msg.DUPLICATE, l, lcount)
|
|
except (GAPI.invalid, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], str(e), l, lcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
else:
|
|
buildLabelPath(gmail, user, i, count, body, label, labelSet, l, lcount)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> [create] label|labels <String>
|
|
# [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
|
|
# [backgroundcolor <LabelColorHex>] [textcolor <LabelColorHex>]
|
|
# [buildpath [<Boolean>]]
|
|
def createLabel(users):
|
|
createLabels(users, getStringReturnInList(Cmd.OB_LABEL_NAME))
|
|
|
|
# gam <UserTypeEntity> create labellist <LabelNameEntity>
|
|
# [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
|
|
# [backgroundcolor <LabelColorHex>] [textcolor <LabelColorHex>]
|
|
# [buildpath [<Boolean>]]
|
|
def createLabelList(users):
|
|
createLabels(users, getEntityList(Cmd.OB_LABEL_NAME_LIST, shlexSplit=True))
|
|
|
|
# gam <UserTypeEntity> update labelsettings <LabelName> [name <String>]
|
|
# [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
|
|
# [backgroundcolor <LabelColorHex>] [textcolor <LabelColorHex>]
|
|
def updateLabelSettings(users):
|
|
label_name = getString(Cmd.OB_LABEL_NAME)
|
|
label_name_lower = label_name.lower()
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
body['name'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
getLabelAttributes(myarg, body)
|
|
checkLabelColor(body)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name)')
|
|
if not labels:
|
|
continue
|
|
try:
|
|
for label in labels['labels']:
|
|
if label['name'].lower() == label_name_lower:
|
|
result = callGAPI(gmail.users().labels(), 'patch',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
|
|
userId='me', id=label['id'], body=body, fields='name')
|
|
entityActionPerformed([Ent.USER, user, Ent.LABEL, result['name']], i, count)
|
|
break
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label_name], Msg.DOES_NOT_EXIST, i, count)
|
|
except (GAPI.notFound, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label_name], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> update labelid <LabelID> [name <String>]
|
|
# [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
|
|
# [backgroundcolor <LabelColorHex>] [textcolor <LabelColorHex>]
|
|
def updateLabelSettingsById(users):
|
|
labelId = getString(Cmd.OB_LABEL_ID)
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'name':
|
|
body['name'] = getString(Cmd.OB_STRING)
|
|
else:
|
|
getLabelAttributes(myarg, body)
|
|
checkLabelColor(body)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
try:
|
|
result = callGAPI(gmail.users().labels(), 'patch',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
|
|
userId='me', id=labelId, body=body, fields='name')
|
|
entityActionPerformed([Ent.USER, user, Ent.LABEL_ID, labelId, Ent.LABEL, result['name']], i, count)
|
|
except (GAPI.notFound, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL_ID, labelId], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
#
|
|
def cleanLabelQuery(labelQuery):
|
|
for ch in '/ (){}':
|
|
labelQuery = labelQuery.replace(ch, '-')
|
|
return labelQuery.lower()
|
|
|
|
# gam <UserTypeEntity> update label|labels
|
|
# [search <REMatchPattern>] [replace <RESubstitution>] [merge [keepoldlabel]]
|
|
# search defaults to '^Inbox/(.*)$' which will find all labels in the Inbox
|
|
# replace defaults to '%s'
|
|
def updateLabels(users):
|
|
search = '^Inbox/(.*)$'
|
|
pattern = re.compile(search, re.IGNORECASE)
|
|
replace = '%s'
|
|
keepOldLabel = merge = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'search':
|
|
search = getString(Cmd.OB_RE_PATTERN)
|
|
pattern = validateREPattern(search, re.IGNORECASE)
|
|
elif myarg == 'replace':
|
|
replaceLocation = Cmd.Location()
|
|
replace = getString(Cmd.OB_RE_SUBSTITUTION)
|
|
elif myarg == 'merge':
|
|
merge = True
|
|
elif myarg == 'keepoldlabel':
|
|
keepOldLabel = True
|
|
else:
|
|
unknownArgumentExit()
|
|
# Validate that number of substitions in replace matches the number of groups in pattern
|
|
useRegexSub = replace.find('%s') == -1
|
|
if useRegexSub:
|
|
Cmd.SetLocation(replaceLocation)
|
|
validateREPatternSubstitution(pattern, replace)
|
|
else:
|
|
if pattern.groups != replace.count('%s'):
|
|
Cmd.SetLocation(replaceLocation)
|
|
usageErrorExit(Msg.MISMATCH_SEARCH_REPLACE_SUBFIELDS.format(pattern.groups, search, replace.count('%s'), replace))
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
|
|
if not labels:
|
|
continue
|
|
try:
|
|
labelMatches = 0
|
|
for label in sorted(labels['labels'], key=lambda k: k['name'], reverse=True):
|
|
if label['type'] == LABEL_TYPE_SYSTEM:
|
|
continue
|
|
match_result = pattern.match(label['name'])
|
|
if match_result is not None:
|
|
labelMatches += 1
|
|
if useRegexSub:
|
|
newLabelName = pattern.sub(replace, label['name'])
|
|
else:
|
|
newLabelName = replace % match_result.groups()
|
|
newLabelNameLower = newLabelName.lower()
|
|
try:
|
|
Act.Set(Act.RENAME)
|
|
callGAPI(gmail.users().labels(), 'patch',
|
|
throwReasons=[GAPI.ABORTED, GAPI.DUPLICATE],
|
|
userId='me', id=label['id'], body={'name': newLabelName}, fields='')
|
|
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.LABEL, label['name']], Act.MODIFIER_TO, newLabelName, i, count)
|
|
except (GAPI.aborted, GAPI.duplicate):
|
|
if merge:
|
|
Act.Set(Act.MERGE)
|
|
entityPerformActionModifierNewValue([Ent.USER, user, Ent.LABEL, label['name']], Act.MODIFIER_WITH, newLabelName, i, count)
|
|
messagesToRelabel = callGAPIpages(gmail.users().messages(), 'list', 'messages',
|
|
userId='me', q=f'label:{cleanLabelQuery(label["name"])}')
|
|
Act.Set(Act.RELABEL)
|
|
jcount = len(messagesToRelabel)
|
|
Ind.Increment()
|
|
if jcount > 0:
|
|
for new_label in labels['labels']:
|
|
if new_label['name'].lower() == newLabelNameLower:
|
|
body = {'addLabelIds': [new_label['id']]}
|
|
break
|
|
j = 0
|
|
for message in messagesToRelabel:
|
|
j += 1
|
|
callGAPI(gmail.users().messages(), 'modify',
|
|
userId='me', id=message['id'], body=body, fields='')
|
|
entityActionPerformed([Ent.USER, user, Ent.MESSAGE, message['id']], j, jcount)
|
|
else:
|
|
printEntityKVList([Ent.USER, user],
|
|
[Msg.NO_MESSAGES_WITH_LABEL, label['name']],
|
|
i, count)
|
|
Ind.Decrement()
|
|
if not keepOldLabel:
|
|
callGAPI(gmail.users().labels(), 'delete',
|
|
userId='me', id=label['id'])
|
|
Act.Set(Act.DELETE)
|
|
entityActionPerformed([Ent.USER, user, Ent.LABEL, label['name']], i, count)
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.LABEL, newLabelName], Msg.ALREADY_EXISTS_USE_MERGE_ARGUMENT, i, count)
|
|
if labels and (labelMatches == 0):
|
|
printEntityKVList([Ent.USER, user],
|
|
[Msg.NO_LABELS_MATCH, search],
|
|
i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.badRequest):
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
def _validateLabelList(user, i, count, labels, labelList, userOnly):
|
|
validLabels = []
|
|
for label in labelList:
|
|
label_name_lower = label.lower()
|
|
if label_name_lower[:6] == 'regex:':
|
|
labelPattern = validateREPattern(label[6:])
|
|
else:
|
|
labelPattern = None
|
|
if label.upper() == '--ALL_LABELS--':
|
|
count = len(labels['labels'])
|
|
for delLabel in sorted(labels['labels'], key=lambda k: k['name'], reverse=True):
|
|
if (not userOnly or delLabel['type'] != LABEL_TYPE_SYSTEM):
|
|
validLabels.append(delLabel)
|
|
elif labelPattern:
|
|
for delLabel in sorted(labels['labels'], key=lambda k: k['name'], reverse=True):
|
|
if (not userOnly or delLabel['type'] != LABEL_TYPE_SYSTEM) and labelPattern.match(delLabel['name']):
|
|
validLabels.append(delLabel)
|
|
else:
|
|
for delLabel in sorted(labels['labels'], key=lambda k: k['name'], reverse=True):
|
|
if (not userOnly or delLabel['type'] != LABEL_TYPE_SYSTEM) and label_name_lower == delLabel['name'].lower():
|
|
validLabels.append(delLabel)
|
|
break
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], Msg.DOES_NOT_EXIST, i, count)
|
|
return (validLabels, len(validLabels))
|
|
|
|
def deleteLabels(users, labelEntity):
|
|
def _handleProcessGmailError(exception, ri):
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
entityActionFailedWarning([Ent.USER, ri[RI_ENTITY], Ent.LABEL, labelIdToNameMap[ri[RI_ITEM]]], formatHTTPError(http_status, reason, message), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
def _callbackDeleteLabel(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
entityActionPerformed([Ent.USER, ri[RI_ENTITY], Ent.LABEL, labelIdToNameMap[ri[RI_ITEM]]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
_handleProcessGmailError(exception, ri)
|
|
|
|
if not isinstance(labelEntity, dict):
|
|
userLabelList = None
|
|
labelList = labelEntity
|
|
else:
|
|
userLabelList = labelEntity
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if userLabelList:
|
|
labelList = userLabelList[origUser]
|
|
try:
|
|
printGettingAllEntityItemsForWhom(Ent.LABEL, user, i, count)
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
|
|
if not labels:
|
|
continue
|
|
delLabels, jcount = _validateLabelList(user, i, count, labels, labelList, True)
|
|
labelIdToNameMap = {}
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LABEL, i, count)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
svcargs = dict([('userId', 'me'), ('id', None), ('fields', '')]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(gmail.users().labels(), 'delete')
|
|
dbatch = gmail.new_batch_http_request(callback=_callbackDeleteLabel)
|
|
bcount = 0
|
|
j = 0
|
|
for del_me in delLabels:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['id'] = del_me['id']
|
|
labelIdToNameMap[del_me['id']] = del_me['name']
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(user, i, count, j, jcount, del_me['id']))
|
|
bcount += 1
|
|
if bcount == 10:
|
|
executeBatch(dbatch)
|
|
dbatch = gmail.new_batch_http_request(callback=_callbackDeleteLabel)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
Ind.Decrement()
|
|
except (GAPI.serviceNotAvailable, GAPI.badRequest):
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> delete label|labels <LabelName>|regex:<REMatchPattern>
|
|
def deleteLabel(users):
|
|
deleteLabels(users, getStringReturnInList(Cmd.OB_LABEL_NAME))
|
|
|
|
# gam <UserTypeEntity> delete labellist <LabelNameEntity>
|
|
def deleteLabelList(users):
|
|
deleteLabels(users, getEntityList(Cmd.OB_LABEL_NAME_LIST, shlexSplit=True))
|
|
|
|
def deleteLabelIds(users, labelEntity):
|
|
if not isinstance(labelEntity, dict):
|
|
userLabelList = None
|
|
labelList = labelEntity
|
|
else:
|
|
userLabelList = labelEntity
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if userLabelList:
|
|
labelList = userLabelList[origUser]
|
|
lcount = len(labelList)
|
|
entityPerformActionNumItems([Ent.USER, user], lcount, Ent.LABEL, i, count)
|
|
Ind.Increment()
|
|
l = 0
|
|
for labelId in labelList:
|
|
l += 1
|
|
try:
|
|
callGAPI(gmail.users().labels(), 'delete',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID,
|
|
GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
userId='me', id=labelId)
|
|
entityActionPerformed([Ent.USER, user, Ent.LABEL_ID, labelId], l, lcount)
|
|
except (GAPI.notFound, GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL_ID, labelId], str(e), l, lcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delete labelid <LabelID>
|
|
def deleteLabelId(users):
|
|
deleteLabelIds(users, getStringReturnInList(Cmd.OB_LABEL_ID))
|
|
|
|
# gam <UserTypeEntity> delete labelidlist <LabelIDEntity>
|
|
def deleteLabelIdList(users):
|
|
deleteLabelIds(users, getEntityList(Cmd.OB_LABEL_ID_LIST))
|
|
|
|
PRINT_LABELS_TITLES = ['User', 'type', 'name', 'id']
|
|
SHOW_LABELS_DISPLAY_CHOICES = ['allfields', 'basename', 'fullname']
|
|
LABEL_DISPLAY_FIELDS_LIST = ['type', 'id', 'labelListVisibility', 'messageListVisibility', 'color']
|
|
LABEL_COUNTS_FIELDS_LIST = ['messagesTotal', 'messagesUnread', 'threadsTotal', 'threadsUnread']
|
|
LABEL_COUNTS_FIELDS = ','.join(LABEL_COUNTS_FIELDS_LIST)
|
|
|
|
# gam <UserTypeEntity> print labels|label [todrive <ToDriveAttribute>*]
|
|
# [onlyuser|useronly [<Boolean>]] [showcounts [<Boolean>]]
|
|
# [labellist <LabelNameEntity>]
|
|
# gam <UserTypeEntity> show labels|label
|
|
# [onlyuser|useronly [<Boolean>]] [showcounts [<Boolean>]]
|
|
# [nested [<Boolean>]] [display allfields|basename|fullname]
|
|
# [labellist <LabelNameEntity>]
|
|
def printShowLabels(users):
|
|
def _buildLabelTree(labels):
|
|
def _checkChildLabel(label):
|
|
labelItemList = label.split('/')
|
|
i = len(labelItemList)-1
|
|
while i > 0:
|
|
parent = '/'.join(labelItemList[:i])
|
|
base = '/'.join(labelItemList[i:])
|
|
if parent in labelTree:
|
|
if label in labelTree:
|
|
labelTree[label]['info']['base'] = base
|
|
labelTree[parent]['children'].append(labelTree.pop(label))
|
|
_checkChildLabel(parent)
|
|
return
|
|
i -= 1
|
|
|
|
labelTree = {}
|
|
for label in labels['labels']:
|
|
if not onlyUser or (label['type'] != LABEL_TYPE_SYSTEM):
|
|
label['base'] = label['name']
|
|
labelTree[label['name']] = {'info': label, 'children': []}
|
|
labelList = sorted(list(labelTree), reverse=True)
|
|
for label in labelList:
|
|
_checkChildLabel(label)
|
|
return labelTree
|
|
|
|
def _printLabel(label):
|
|
if not displayAllFields:
|
|
if not showCounts:
|
|
printKeyValueList([label[nameField]])
|
|
else:
|
|
counts = callGAPI(gmail.users().labels(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me', id=label['id'],
|
|
fields=LABEL_COUNTS_FIELDS)
|
|
kvlist = [label[nameField], 'Counts']
|
|
for a_key in LABEL_COUNTS_FIELDS_LIST:
|
|
kvlist.extend([a_key, counts[a_key]])
|
|
printKeyValueList(kvlist)
|
|
else:
|
|
printKeyValueList([label[nameField]])
|
|
Ind.Increment()
|
|
for a_key in LABEL_DISPLAY_FIELDS_LIST:
|
|
if a_key in label:
|
|
if a_key != 'color':
|
|
printKeyValueList([a_key, label[a_key]])
|
|
else:
|
|
printKeyValueList(['backgroundColor', label[a_key]['backgroundColor']])
|
|
printKeyValueList(['textColor', label[a_key]['textColor']])
|
|
if showCounts:
|
|
counts = callGAPI(gmail.users().labels(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me', id=label['id'],
|
|
fields=LABEL_COUNTS_FIELDS)
|
|
for a_key in LABEL_COUNTS_FIELDS_LIST:
|
|
printKeyValueList([a_key, counts[a_key]])
|
|
Ind.Decrement()
|
|
|
|
def _printFlatLabel(label):
|
|
_printLabel(label['info'])
|
|
if label['children']:
|
|
for child in sorted(label['children'], key=lambda k: k['info']['name']):
|
|
_printFlatLabel(child)
|
|
|
|
def _printNestedLabel(label):
|
|
_printLabel(label['info'])
|
|
if label['children']:
|
|
Ind.Increment()
|
|
if displayAllFields:
|
|
printKeyValueList(['nested', len(label['children'])])
|
|
Ind.Increment()
|
|
for child in sorted(label['children'], key=lambda k: k['info']['name']):
|
|
_printNestedLabel(child)
|
|
Ind.Decrement()
|
|
else:
|
|
for child in sorted(label['children'], key=lambda k: k['info']['name']):
|
|
_printNestedLabel(child)
|
|
Ind.Decrement()
|
|
|
|
csvPF = CSVPrintFile(PRINT_LABELS_TITLES, 'sortall') if Act.csvFormat() else None
|
|
onlyUser = showCounts = showNested = False
|
|
displayAllFields = True
|
|
nameField = 'name'
|
|
labelEntity = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'onlyuser', 'useronly'}:
|
|
onlyUser = getBoolean()
|
|
elif myarg == 'showcounts':
|
|
showCounts = getBoolean()
|
|
elif not csvPF and myarg == 'nested':
|
|
showNested = getBoolean()
|
|
elif not csvPF and myarg == 'display':
|
|
fields = getChoice(SHOW_LABELS_DISPLAY_CHOICES)
|
|
nameField = 'name' if fields != 'basename' else 'base'
|
|
displayAllFields = fields == 'allfields'
|
|
elif myarg == 'labellist':
|
|
labelEntity = getEntityList(Cmd.OB_LABEL_NAME_LIST, shlexSplit=True)
|
|
else:
|
|
unknownArgumentExit()
|
|
if not isinstance(labelEntity, dict):
|
|
userLabelList = None
|
|
labelList = labelEntity
|
|
else:
|
|
userLabelList = labelEntity
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if userLabelList:
|
|
labelList = userLabelList[origUser]
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.LABEL, user, i, count)
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels')
|
|
if not labels:
|
|
continue
|
|
if not labelList:
|
|
jcount = len(labels['labels'])
|
|
if (jcount > 0) and onlyUser:
|
|
for label in labels['labels']:
|
|
if label['type'] == LABEL_TYPE_SYSTEM:
|
|
jcount -= 1
|
|
else:
|
|
labels['labels'], jcount = _validateLabelList(user, i, count, labels, labelList, onlyUser)
|
|
try:
|
|
if not csvPF:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.LABEL, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
if not csvPF:
|
|
labelTree = _buildLabelTree(labels)
|
|
Ind.Increment()
|
|
if not showNested:
|
|
for label, _ in sorted(iter(labelTree.items()), key=lambda k: (k[1]['info']['type'], k[1]['info']['name'])):
|
|
_printFlatLabel(labelTree[label])
|
|
else:
|
|
for label, _ in sorted(iter(labelTree.items()), key=lambda k: (k[1]['info']['type'], k[1]['info']['name'])):
|
|
_printNestedLabel(labelTree[label])
|
|
Ind.Decrement()
|
|
else:
|
|
for label in sorted(labels['labels'], key=lambda k: (k['type'], k['name'])):
|
|
if not onlyUser or label['type'] != LABEL_TYPE_SYSTEM:
|
|
if showCounts:
|
|
counts = callGAPI(gmail.users().labels(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me', id=label['id'],
|
|
fields=LABEL_COUNTS_FIELDS)
|
|
for a_key in LABEL_COUNTS_FIELDS_LIST:
|
|
label[a_key] = counts[a_key]
|
|
csvPF.WriteRowTitles(flattenJSON(label, flattened={'User': user}))
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Labels')
|
|
|
|
GMAIL_SYSTEM_LABELS = {
|
|
'CHAT': 'CHAT',
|
|
'DRAFT': 'DRAFT',
|
|
'IMPORTANT': 'IMPORTANT',
|
|
'INBOX': 'INBOX',
|
|
'SENT': 'SENT',
|
|
'SPAM': 'SPAM',
|
|
'STARRED': 'STARRED',
|
|
'TRASH': 'TRASH',
|
|
'UNREAD': 'UNREAD',
|
|
}
|
|
GMAIL_CATEGORY_LABELS = {
|
|
'CATEGORY_PERSONAL': 'CATEGORY_PERSONAL',
|
|
'CATEGORY_SOCIAL': 'CATEGORY_SOCIAL',
|
|
'CATEGORY_PROMOTIONS': 'CATEGORY_PROMOTIONS',
|
|
'CATEGORY_UPDATES': 'CATEGORY_UPDATES',
|
|
'CATEGORY_FORUMS': 'CATEGORY_FORUMS',
|
|
}
|
|
|
|
def _initLabelNameMap(userGmailLabels):
|
|
labelNameMap = {}
|
|
labelNameMap.update(GMAIL_SYSTEM_LABELS)
|
|
labelNameMap.update(GMAIL_CATEGORY_LABELS)
|
|
for label in userGmailLabels['labels']:
|
|
if label['type'] == 'system':
|
|
labelNameMap[label['id']] = label['id']
|
|
else:
|
|
labelNameMap[label['name']] = labelNameMap[label['name'].upper()] = label['id']
|
|
return labelNameMap
|
|
|
|
def _convertLabelNamesToIds(gmail, user, i, count, bodyLabels, labelNameMap, addLabel):
|
|
labelIds = []
|
|
for label in bodyLabels:
|
|
if label in labelNameMap:
|
|
labelIds.append(labelNameMap[label])
|
|
continue
|
|
if label.upper() in labelNameMap:
|
|
labelIds.append(labelNameMap[label.upper()])
|
|
continue
|
|
if not addLabel:
|
|
entityListDoesNotExistWarning([Ent.USER, user, Ent.LABEL, label], i, count)
|
|
continue
|
|
try:
|
|
results = callGAPI(gmail.users().labels(), 'create',
|
|
throwReasons=[GAPI.INVALID],
|
|
userId='me', body={'labelListVisibility': 'labelShow', 'messageListVisibility': 'show', 'name': label}, fields='id')
|
|
except GAPI.invalid as e:
|
|
action = Act.Get()
|
|
Act.Set(Act.CREATE)
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, label], str(e), i, count)
|
|
Act.Set(action)
|
|
continue
|
|
labelNameMap[label] = labelNameMap[label.upper()] = results['id']
|
|
labelIds.append(results['id'])
|
|
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 labelNameMap) and (not parent_label.upper() in labelNameMap):
|
|
result = callGAPI(gmail.users().labels(), 'create',
|
|
userId='me', body={'name': parent_label}, fields='id')
|
|
labelNameMap[parent_label] = labelNameMap[parent_label.upper()] = result['id']
|
|
if parent_label.find('/') == -1:
|
|
break
|
|
parent_label = parent_label[:parent_label.rfind('/')]
|
|
return labelIds
|
|
|
|
MESSAGES_MAX_TO_KEYWORDS = {
|
|
Act.ARCHIVE: 'maxtoarchive',
|
|
Act.DELETE: 'maxtodelete',
|
|
Act.EXPORT: 'maxtoexport',
|
|
Act.FORWARD: 'maxtoforward',
|
|
Act.MODIFY: 'maxtomodify',
|
|
Act.PRINT: 'maxtoprint',
|
|
Act.SHOW: 'maxtoshow',
|
|
Act.SPAM: 'maxtospam',
|
|
Act.TRASH: 'maxtotrash',
|
|
Act.UNTRASH: 'maxtountrash',
|
|
}
|
|
|
|
def _initMessageThreadParameters(entityType, doIt, maxToProcess):
|
|
listType = 'messages' if entityType == Ent.MESSAGE else 'threads'
|
|
return {'currLabelOp': 'and', 'prevLabelOp': 'and', 'labelGroupOpen': False,
|
|
'query': '', 'queryTimes': {},
|
|
'entityType': entityType, 'messageEntity': None, 'doIt': doIt, 'quick': True,
|
|
'labelMatchPattern': None, 'senderMatchPattern': None,
|
|
'maxToProcess': maxToProcess, 'maxItems': 0, 'maxMessagesPerThread': 0,
|
|
'maxToKeywords': [MESSAGES_MAX_TO_KEYWORDS[Act.Get()], 'maxtoprocess'],
|
|
'listType': listType, 'fields': f'nextPageToken,{listType}(id)'}
|
|
|
|
LABEL_QUERY_REPLACEMENT_CHARACTERS = ' &()"|{}/'
|
|
|
|
def _getMessageSelectParameters(myarg, parameters):
|
|
if myarg == 'query':
|
|
parameters['query'] += f' ({getString(Cmd.OB_QUERY)})'
|
|
elif myarg.startswith('querytime'):
|
|
parameters['queryTimes'][myarg] = getDateOrDeltaFromNow().replace('-', '/')
|
|
elif myarg == 'matchlabel':
|
|
labelTemp = getString(Cmd.OB_LABEL_NAME).lower()
|
|
labelName = ''
|
|
for c in labelTemp:
|
|
labelName += c if c not in LABEL_QUERY_REPLACEMENT_CHARACTERS else '-'
|
|
if not parameters['labelGroupOpen']:
|
|
parameters['query'] += '('
|
|
parameters['labelGroupOpen'] = True
|
|
parameters['query'] += f' label:{labelName}'
|
|
elif myarg in {'or', 'and'}:
|
|
parameters['prevLabelOp'] = parameters['currLabelOp']
|
|
parameters['currLabelOp'] = myarg
|
|
if parameters['labelGroupOpen'] and parameters['currLabelOp'] != parameters['prevLabelOp']:
|
|
parameters['query'] += ')'
|
|
parameters['labelGroupOpen'] = False
|
|
if parameters['currLabelOp'] == 'or':
|
|
parameters['query'] += ' OR '
|
|
elif myarg == 'labelmatchpattern':
|
|
parameters['labelMatchPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'sendermatchpattern':
|
|
parameters['senderMatchPattern'] = getREPattern(re.IGNORECASE)
|
|
elif myarg == 'ids':
|
|
parameters['messageEntity'] = getUserObjectEntity(Cmd.OB_MESSAGE_ID, parameters['entityType'])
|
|
elif myarg == 'quick':
|
|
parameters['quick'] = True
|
|
elif myarg == 'notquick':
|
|
parameters['quick'] = False
|
|
elif myarg == 'doit':
|
|
parameters['doIt'] = True
|
|
elif myarg in parameters['maxToKeywords']:
|
|
parameters['maxToProcess'] = getInteger(minVal=0)
|
|
elif myarg == 'maxmessagesperthread':
|
|
parameters['maxMessagesPerThread'] = getInteger(minVal=0)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
MESSAGE_TIME_QUERY_PATTERN = re.compile(r'(after:|before:|older:|newer:)(\d{4})/(\d{2})/(\d{2})')
|
|
|
|
def _mapMessageQueryDates(parameters):
|
|
query = parameters['query']
|
|
pos = 0
|
|
while True:
|
|
mg = MESSAGE_TIME_QUERY_PATTERN.search(query, pos)
|
|
if not mg:
|
|
break
|
|
try:
|
|
dt = datetime.datetime(int(mg.groups()[1]), int(mg.groups()[2]), int(mg.groups()[3]), tzinfo=GC.Values[GC.TIMEZONE])
|
|
query = query[:mg.start(2)]+str(int(datetime.datetime.timestamp(dt)))+query[mg.end(4):]
|
|
except ValueError:
|
|
pass
|
|
pos = mg.end()
|
|
parameters['query'] = query
|
|
|
|
def _finalizeMessageSelectParameters(parameters, queryOrIdsRequired):
|
|
if parameters['query']:
|
|
if parameters['labelGroupOpen']:
|
|
parameters['query'] += ')'
|
|
if parameters['queryTimes']:
|
|
for queryTimeName, queryTimeValue in iter(parameters['queryTimes'].items()):
|
|
parameters['query'] = parameters['query'].replace(f'#{queryTimeName}#', queryTimeValue)
|
|
_mapMessageQueryDates(parameters)
|
|
elif queryOrIdsRequired and parameters['messageEntity'] is None:
|
|
missingArgumentExit('query|matchlabel|ids')
|
|
else:
|
|
parameters['query'] = None
|
|
parameters['maxItems'] = parameters['maxToProcess'] if parameters['quick'] and not parameters['labelMatchPattern'] else 0
|
|
|
|
# gam <UserTypeEntity> archive messages <GroupItem>
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_archive <Number>])|(ids <MessageIDEntity>)
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
def archiveMessages(users):
|
|
def _processMessageFailed(user, idsList, errMsg, j=0, jcount=0):
|
|
if not csvPF:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, idsList], errMsg, j, jcount)
|
|
else:
|
|
csvPF.WriteRow({'User': user, entityHeader: idsList, 'action': Act.Failed(), 'error': errMsg})
|
|
|
|
entityType = Ent.MESSAGE
|
|
entityHeader = 'id'
|
|
parameters = _initMessageThreadParameters(entityType, False, 0)
|
|
group = getEmailAddress()
|
|
csvPF = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getMessageSelectParameters(myarg, parameters):
|
|
pass
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile(['User', entityHeader, 'action', 'error'])
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
unknownArgumentExit()
|
|
_finalizeMessageSelectParameters(parameters, False)
|
|
if not GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
|
|
gm = buildGAPIObject(API.GROUPSMIGRATION)
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
try:
|
|
group = callGAPI(cd.groups(), 'get',
|
|
throwReasons=GAPI.GROUP_GET_THROW_REASONS,
|
|
groupKey=group, fields='email')['email']
|
|
except (GAPI.groupNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest):
|
|
entityDoesNotExistExit(Ent.GROUP, group)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, messageIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
|
|
if not gmail:
|
|
continue
|
|
if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]:
|
|
_, gm = buildGAPIServiceObject(API.GROUPSMIGRATION, user, i, count)
|
|
if not gm:
|
|
continue
|
|
service = gmail.users().messages()
|
|
try:
|
|
if parameters['messageEntity'] is None:
|
|
printGettingAllEntityItemsForWhom(entityType, user, i, count)
|
|
listResult = callGAPIpages(service, 'list', parameters['listType'],
|
|
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
|
|
userId='me', q=parameters['query'], fields=parameters['fields'],
|
|
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
|
|
messageIds = [message['id'] for message in listResult]
|
|
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(messageIds)
|
|
if jcount == 0:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count)
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
if parameters['messageEntity'] is None:
|
|
if parameters['maxToProcess'] and jcount > parameters['maxToProcess']:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(jcount, Act.ToPerform(), parameters['maxToProcess']), i, count)
|
|
continue
|
|
if not parameters['doIt']:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
|
|
continue
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for messageId in messageIds:
|
|
j += 1
|
|
try:
|
|
message = callGAPI(service, 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
|
|
userId='me', id=messageId, format='raw')
|
|
stream = io.BytesIO()
|
|
stream.write(base64.urlsafe_b64decode(str(message['raw'])))
|
|
try:
|
|
callGAPI(gm.archive(), 'insert',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID,
|
|
GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN],
|
|
retryReasons=[GAPI.NOT_FOUND],
|
|
groupId=group, media_body=googleapiclient.http.MediaIoBaseUpload(stream, mimetype='message/rfc822', resumable=True))
|
|
if not csvPF:
|
|
entityActionPerformed([Ent.USER, user, entityType, messageId], j, jcount)
|
|
else:
|
|
csvPF.WriteRow({'User': user, entityHeader: messageId, 'action': Act.Performed()})
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
except GAPI.notFound as e:
|
|
_processMessageFailed(user, messageId, str(e), j, jcount)
|
|
break
|
|
except (GAPI.badRequest, GAPI.invalid, GAPI.failedPrecondition, GAPI.forbidden,
|
|
googleapiclient.errors.MediaUploadSizeError) as e:
|
|
_processMessageFailed(user, messageId, str(e), j, jcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
except (GAPI.notFound, GAPI.invalidMessageId) as e:
|
|
_processMessageFailed(user, messageId, str(e), j, jcount)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile(f'{Act.ToPerform()} Messages')
|
|
|
|
def _processMessagesThreads(users, entityType):
|
|
def _processMessageFailed(user, idsList, errMsg, j=0, jcount=0):
|
|
if not csvPF:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, idsList], errMsg, j, jcount)
|
|
else:
|
|
csvPF.WriteRow({'User': user, entityHeader: idsList, 'action': Act.Failed(), 'error': errMsg})
|
|
|
|
def _batchDeleteModifyMessages(gmail, function, user, jcount, messageIds, body):
|
|
mcount = 0
|
|
bcount = min(jcount-mcount, GC.Values[GC.MESSAGE_BATCH_SIZE])
|
|
while bcount > 0:
|
|
body['ids'] = messageIds[mcount:mcount+bcount]
|
|
idsCount = min(5, bcount)
|
|
idsList = ','.join(body['ids'][0:idsCount])
|
|
if bcount > 5:
|
|
idsList += ',...'
|
|
try:
|
|
callGAPI(gmail.users().messages(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID_MESSAGE_ID, GAPI.INVALID, GAPI.INVALID_ARGUMENT,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
|
|
userId='me', body=body)
|
|
for messageId in body['ids']:
|
|
mcount += 1
|
|
if not csvPF:
|
|
entityActionPerformed([Ent.USER, user, entityType, messageId], mcount, jcount)
|
|
else:
|
|
csvPF.WriteRow({'User': user, entityHeader: messageId, 'action': Act.Performed()})
|
|
except GAPI.serviceNotAvailable:
|
|
mcount += bcount
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
_processMessageFailed(user, idsList, f'{str(e)} ({mcount+1}-{mcount+bcount}/{jcount})')
|
|
mcount += bcount
|
|
except GAPI.invalidMessageId:
|
|
_processMessageFailed(user, idsList, f'{Msg.INVALID_MESSAGE_ID} ({mcount+1}-{mcount+bcount}/{jcount})')
|
|
mcount += bcount
|
|
except GAPI.failedPrecondition:
|
|
_processMessageFailed(user, idsList, f'{Msg.FAILED_PRECONDITION} ({mcount+1}-{mcount+bcount}/{jcount})')
|
|
mcount += bcount
|
|
bcount = min(jcount-mcount, GC.Values[GC.MESSAGE_BATCH_SIZE])
|
|
|
|
_GMAIL_ERROR_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST,
|
|
GAPI.INVALID_MESSAGE_ID: Msg.INVALID_MESSAGE_ID,
|
|
GAPI.FAILED_PRECONDITION: Msg.FAILED_PRECONDITION}
|
|
|
|
def _callbackProcessMessage(request_id, _, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
if not csvPF:
|
|
entityActionPerformed([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
csvPF.WriteRow({'User': ri[RI_ENTITY], entityHeader: ri[RI_ITEM], 'action': Act.Performed()})
|
|
else:
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
_processMessageFailed(ri[RI_ENTITY], ri[RI_ITEM], getHTTPError(_GMAIL_ERROR_REASON_TO_MESSAGE_MAP, http_status, reason, message), int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
|
|
def _batchProcessMessagesThreads(service, function, user, jcount, messageIds, **kwargs):
|
|
svcargs = dict([('userId', 'me'), ('id', None), ('fields', '')]+list(kwargs.items())+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
method = getattr(service, function)
|
|
dbatch = gmail.new_batch_http_request(callback=_callbackProcessMessage)
|
|
bcount = 0
|
|
j = 0
|
|
for messageId in messageIds:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['id'] = messageId
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(user, 0, 0, j, jcount, svcparms['id']))
|
|
bcount += 1
|
|
if bcount == GC.Values[GC.EMAIL_BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = gmail.new_batch_http_request(callback=_callbackProcessMessage)
|
|
bcount = 0
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
|
|
parameters = _initMessageThreadParameters(entityType, False, 1)
|
|
includeSpamTrash = False
|
|
function = {Act.DELETE: 'delete', Act.MODIFY: 'modify', Act.SPAM: 'spam', Act.TRASH: 'trash', Act.UNTRASH: 'untrash'}[Act.Get()]
|
|
labelNameMap = {}
|
|
addLabelNames = []
|
|
addLabelIds = []
|
|
removeLabelNames = []
|
|
removeLabelIds = []
|
|
csvPF = None
|
|
entityHeader = 'id' if entityType == Ent.MESSAGE else 'threadId'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getMessageSelectParameters(myarg, parameters):
|
|
pass
|
|
elif (function == 'modify') and (myarg == 'addlabel'):
|
|
addLabelNames.append(getString(Cmd.OB_LABEL_NAME))
|
|
elif (function == 'modify') and (myarg == 'removelabel'):
|
|
removeLabelNames.append(getString(Cmd.OB_LABEL_NAME))
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile(['User', entityHeader, 'action', 'error'])
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
unknownArgumentExit()
|
|
if function == 'modify' and not addLabelNames and not removeLabelNames:
|
|
missingArgumentExit('(addlabel <LabelName>)|(removelabel <LabelName>)')
|
|
_finalizeMessageSelectParameters(parameters, True)
|
|
includeSpamTrash = Act.Get() in [Act.DELETE, Act.MODIFY, Act.UNTRASH]
|
|
if function == 'spam':
|
|
function = 'modify'
|
|
addLabelIds = ['SPAM']
|
|
removeLabelIds = ['INBOX']
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, messageIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
|
|
if not gmail:
|
|
continue
|
|
service = gmail.users().messages() if entityType == Ent.MESSAGE else gmail.users().threads()
|
|
if addLabelNames or removeLabelNames:
|
|
userGmailLabels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
|
|
if not userGmailLabels:
|
|
continue
|
|
labelNameMap = _initLabelNameMap(userGmailLabels)
|
|
addLabelIds = _convertLabelNamesToIds(gmail, user, i, count, addLabelNames, labelNameMap, True)
|
|
removeLabelIds = _convertLabelNamesToIds(gmail, user, i, count, removeLabelNames, labelNameMap, False)
|
|
if not addLabelIds and not removeLabelIds:
|
|
entityActionNotPerformedWarning([Ent.USER, user], Msg.NO_LABELS_TO_PROCESS, i, count)
|
|
continue
|
|
try:
|
|
if parameters['messageEntity'] is None:
|
|
printGettingAllEntityItemsForWhom(Ent.MESSAGE, user, i, count)
|
|
listResult = callGAPIpages(service, 'list', parameters['listType'],
|
|
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
|
|
userId='me', q=parameters['query'], fields=parameters['fields'], includeSpamTrash=includeSpamTrash,
|
|
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
|
|
messageIds = [message['id'] for message in listResult]
|
|
else:
|
|
# Need to get authorization set up for batch
|
|
callGAPI(gmail.users(), 'getProfile',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
|
|
userId='me', fields='')
|
|
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(messageIds)
|
|
if jcount == 0:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count)
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
if parameters['messageEntity'] is None:
|
|
if parameters['maxToProcess'] and jcount > parameters['maxToProcess']:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(jcount, Act.ToPerform(), parameters['maxToProcess']), i, count)
|
|
continue
|
|
if not parameters['doIt']:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
|
|
continue
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
|
|
Ind.Increment()
|
|
if function == 'delete' and entityType == Ent.MESSAGE:
|
|
_batchDeleteModifyMessages(gmail, 'batchDelete', user, jcount, messageIds, {'ids': []})
|
|
elif function == 'modify' and entityType == Ent.MESSAGE:
|
|
_batchDeleteModifyMessages(gmail, 'batchModify', user, jcount, messageIds, {'ids': [], 'addLabelIds': addLabelIds, 'removeLabelIds': removeLabelIds})
|
|
else:
|
|
if addLabelIds or removeLabelIds:
|
|
kwargs = {'body': {'addLabelIds': addLabelIds, 'removeLabelIds': removeLabelIds}}
|
|
else:
|
|
kwargs = {}
|
|
_batchProcessMessagesThreads(service, function, user, jcount, messageIds, **kwargs)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile(f'{Act.ToPerform()} Messages')
|
|
|
|
# gam <UserTypeEntity> delete message|messages
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_delete <Number>])|(ids <MessageIDEntity>)
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
# gam <UserTypeEntity> modify message|messages
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_modify <Number>])|(ids <MessageIDEntity>)
|
|
# (addlabel <LabelName>)* (removelabel <LabelName>)*
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
# gam <UserTypeEntity> spam message|messages
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_spam <Number>])|(ids <MessageIDEntity>)
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
# gam <UserTypeEntity> trash message|messages
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>)
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
# gam <UserTypeEntity> untrash message|messages
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <MessageIDEntity>)
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
def processMessages(users):
|
|
_processMessagesThreads(users, Ent.MESSAGE)
|
|
|
|
# gam <UserTypeEntity> delete thread|threads
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_delete <Number>])|(ids <ThreadIDEntity>)
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
# gam <UserTypeEntity> modify thread|threads
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_modify <Number>])|(ids <ThreadIDEntity>)
|
|
# (addlabel <LabelName>)* (removelabel <LabelName>)*
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
# gam <UserTypeEntity> spam thread|threads
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_spam <Number>])|(ids <ThreadIDEntity>)
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
# gam <UserTypeEntity> trash thread|threads
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_trash <Number>])|(ids <MessageIDEntity>)
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
# gam <UserTypeEntity> untrash thread|threads
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_untrash <Number>])|(ids <ThreadIDEntity>)
|
|
# [csv [todrive <ToDriveAttribute>*]]
|
|
def processThreads(users):
|
|
_processMessagesThreads(users, Ent.THREAD)
|
|
|
|
def exportMessagesThreads(users, entityType):
|
|
parameters = _initMessageThreadParameters(entityType, False, 1)
|
|
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
|
|
targetNamePattern = None
|
|
includeSpamTrash = overwrite = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getMessageSelectParameters(myarg, parameters):
|
|
pass
|
|
elif myarg == 'targetfolder':
|
|
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
elif myarg == 'targetname':
|
|
targetNamePattern = getString(Cmd.OB_FILE_NAME)
|
|
elif myarg == 'overwrite':
|
|
overwrite = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
_finalizeMessageSelectParameters(parameters, True)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, entityIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
|
|
if not gmail:
|
|
continue
|
|
_, userName, _ = splitEmailAddressOrUID(user)
|
|
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
targetName = _substituteForUser(targetNamePattern, user, userName) if targetNamePattern else None
|
|
service = gmail.users().messages() if entityType == Ent.MESSAGE else gmail.users().threads()
|
|
try:
|
|
if parameters['messageEntity'] is None:
|
|
printGettingAllEntityItemsForWhom(entityType, user, i, count)
|
|
listResult = callGAPIpages(service, 'list', parameters['listType'],
|
|
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
|
|
userId='me', q=parameters['query'], fields=parameters['fields'], includeSpamTrash=includeSpamTrash,
|
|
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
|
|
entityIds = [entity['id'] for entity in listResult]
|
|
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(entityIds)
|
|
if jcount == 0:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count)
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
if parameters['messageEntity'] is None:
|
|
if parameters['maxToProcess'] and jcount > parameters['maxToProcess']:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(jcount, Act.ToPerform(), parameters['maxToProcess']), i, count)
|
|
continue
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for entityId in entityIds:
|
|
j += 1
|
|
if entityType == Ent.THREAD:
|
|
try:
|
|
result = callGAPI(gmail.users().threads(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT],
|
|
userId='me', id=entityId, fields='messages(id)')
|
|
messageIds = [message['id'] for message in result['messages']]
|
|
kcount = len(messageIds)
|
|
entityPerformActionNumItems([Ent.USER, user, Ent.THREAD, entityId], kcount, Ent.MESSAGE, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
except (GAPI.notFound, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.THREAD, entityId], str(e), j, jcount)
|
|
continue
|
|
else:
|
|
messageIds = [entityId]
|
|
kcount = jcount
|
|
k = j-1
|
|
for messageId in messageIds:
|
|
k += 1
|
|
try:
|
|
result = callGAPI(gmail.users().messages(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
|
|
userId='me', id=messageId, format='raw')
|
|
if targetName:
|
|
msgName = targetName.replace('#id#', messageId)
|
|
else:
|
|
msgName = f'Msg-{messageId}.eml'
|
|
filename, _ = uniqueFilename(targetFolder, msgName, overwrite)
|
|
status, e = writeFileReturnError(filename, base64.urlsafe_b64decode(str(result['raw'])), mode='wb')
|
|
if status:
|
|
entityActionPerformed([Ent.MESSAGE, filename])
|
|
else:
|
|
entityActionFailedWarning([Ent.MESSAGE, filename], str(e))
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
except (GAPI.notFound, GAPI.invalidMessageId) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], str(e), k, kcount)
|
|
continue
|
|
if entityType == Ent.THREAD:
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> export message|messages
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [max_to_export <Number>])|(ids <MessageIDEntity>)
|
|
# [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
|
|
def exportMessages(users):
|
|
exportMessagesThreads(users, Ent.MESSAGE)
|
|
|
|
# gam <UserTypeEntity> export thread|threads
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [max_to_export <Number>])|(ids <ThreadIDEntity>)
|
|
# [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
|
|
def exportThreads(users):
|
|
exportMessagesThreads(users, Ent.THREAD)
|
|
|
|
HEADER_ENCODE_PATTERN = re.compile(r'=\?([^?]*?)\?[qQbB]\?(.*?)\?=', re.VERBOSE | re.MULTILINE)
|
|
|
|
def _decodeHeader(header):
|
|
header = header.encode(UTF8, 'replace').decode(UTF8)
|
|
while True:
|
|
mg = HEADER_ENCODE_PATTERN.search(header)
|
|
if not mg:
|
|
return header
|
|
try:
|
|
header = header[:mg.start()]+decode_header(mg.group())[0][0].decode(mg.group(1))+header[mg.end():]
|
|
except LookupError:
|
|
stderrWarningMsg(Msg.INVALID_CHARSET.format(mg.group(1)))
|
|
return header
|
|
|
|
# gam <UserTypeEntity> forward message|messages recipient|to <RecipientEntity>
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <MessageIDEntity>)
|
|
# [subject <String>] [addorigfieldstosubject [<Boolean>]] [altcharset <String>]
|
|
# gam <UserTypeEntity> forward thread|threads recipient|to <RecipientEntity>
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])+ [quick|notquick] [doit] [max_to_forward <Number>])|(ids <ThreadIDEntity>)
|
|
# [subject <String>] [addorigfieldstosubject [<Boolean>]] [altcharset <String>]
|
|
def forwardMessagesThreads(users, entityType):
|
|
checkArgumentPresent({'recipient', 'recipients', 'to'})
|
|
recipients = getRecipients()
|
|
parameters = _initMessageThreadParameters(entityType, False, 1)
|
|
addOriginalFieldsToSubject = includeSpamTrash = False
|
|
subject = ''
|
|
encodings = [UTF8]
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getMessageSelectParameters(myarg, parameters):
|
|
pass
|
|
elif myarg == 'subject':
|
|
subject = getString(Cmd.OB_STRING)
|
|
elif myarg == 'addorigfieldstosubject':
|
|
addOriginalFieldsToSubject = getBoolean()
|
|
elif myarg == 'altcharset':
|
|
encodings.append(getString(Cmd.OB_CHAR_SET))
|
|
else:
|
|
unknownArgumentExit()
|
|
_finalizeMessageSelectParameters(parameters, True)
|
|
msgTo = ','.join(recipients)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, entityIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
|
|
if not gmail:
|
|
continue
|
|
service = gmail.users().messages() if entityType == Ent.MESSAGE else gmail.users().threads()
|
|
if parameters['messageEntity'] is None:
|
|
try:
|
|
printGettingAllEntityItemsForWhom(entityType, user, i, count)
|
|
listResult = callGAPIpages(service, 'list', parameters['listType'],
|
|
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
|
|
userId='me', q=parameters['query'], fields=parameters['fields'], includeSpamTrash=includeSpamTrash,
|
|
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
|
|
entityIds = [entity['id'] for entity in listResult]
|
|
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(entityIds)
|
|
if jcount == 0:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.NO_ENTITIES_MATCHED.format(Ent.Plural(entityType)), i, count)
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
if parameters['messageEntity'] is None:
|
|
if parameters['maxToProcess'] and jcount > parameters['maxToProcess']:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.COUNT_N_EXCEEDS_MAX_TO_PROCESS_M.format(jcount, Act.ToPerform(), parameters['maxToProcess']), i, count)
|
|
continue
|
|
if not parameters['doIt']:
|
|
entityNumEntitiesActionNotPerformedWarning([Ent.USER, user], entityType, jcount, Msg.USE_DOIT_ARGUMENT_TO_PERFORM_ACTION, i, count)
|
|
continue
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for entityId in entityIds:
|
|
j += 1
|
|
if entityType == Ent.THREAD:
|
|
try:
|
|
result = callGAPI(gmail.users().threads(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
|
|
userId='me', id=entityId, fields='messages(id)')
|
|
messageIds = [message['id'] for message in result['messages']]
|
|
kcount = len(messageIds)
|
|
entityPerformActionNumItems([Ent.USER, user, Ent.THREAD, entityId], kcount, Ent.MESSAGE, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
except (GAPI.notFound, GAPI.invalidMessageId) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.THREAD, entityId], str(e), j, jcount)
|
|
continue
|
|
else:
|
|
messageIds = [entityId]
|
|
kcount = jcount
|
|
k = j-1
|
|
for messageId in messageIds:
|
|
k += 1
|
|
try:
|
|
result = callGAPI(gmail.users().messages(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
|
|
userId='me', id=messageId, format='raw')
|
|
for encoding in encodings:
|
|
try:
|
|
message = message_from_string(base64.urlsafe_b64decode(str(result['raw'])).decode(encoding), policy=policySMTP)
|
|
break
|
|
except UnicodeDecodeError as e:
|
|
errMsg = str(e)
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.RECIPIENT, msgTo, entityType, messageId], errMsg, k, kcount)
|
|
continue
|
|
if not subject:
|
|
msgSubject = f"Fwd: {_decodeHeader(message['Subject'])}"
|
|
else:
|
|
msgSubject = f"Subject: {subject}"
|
|
for header in ['To', 'Cc', 'Subject']:
|
|
if header in message:
|
|
del message[header]
|
|
message['To'] = msgTo
|
|
if addOriginalFieldsToSubject:
|
|
msgSubject += ' (Original'
|
|
for header in ['From', 'To', 'Date']:
|
|
if header in message:
|
|
msgSubject += f' {header}: {message[header]}'
|
|
msgSubject += ')'
|
|
message['Subject'] = msgSubject
|
|
try:
|
|
result = callGAPI(gmail.users().messages(), 'send',
|
|
throwReasons=[GAPI.SERVICE_NOT_AVAILABLE, GAPI.AUTH_ERROR, GAPI.DOMAIN_POLICY,
|
|
GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.FORBIDDEN, GAPI.PERMISSION_DENIED],
|
|
userId='me', body={'raw': base64.urlsafe_b64encode(message.as_bytes()).decode(encoding)}, fields='id')
|
|
entityActionPerformedMessage([Ent.RECIPIENT, msgTo], f"{result['id']}", k, kcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy,
|
|
GAPI.invalid, GAPI.invalidArgument, GAPI.forbidden, GAPI.permissionDenied, UnicodeEncodeError) as e:
|
|
entityActionFailedWarning([Ent.RECIPIENT, msgTo], str(e), k, kcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
except (GAPI.notFound, GAPI.invalidMessageId) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.MESSAGE, messageId], str(e), k, kcount)
|
|
continue
|
|
if entityType == Ent.THREAD:
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
SMTP_HEADERS_MAP = {
|
|
'accept-language': 'Accept-Language',
|
|
'alternate-recipient': 'Alternate-Recipient',
|
|
'autoforwarded': 'Autoforwarded',
|
|
'autosubmitted': 'Autosubmitted',
|
|
'bcc': 'Bcc',
|
|
'cc': 'Cc',
|
|
'comments': 'Comments',
|
|
'content-alternative': 'Content-Alternative',
|
|
'content-base': 'Content-Base',
|
|
'content-description': 'Content-Description',
|
|
'content-disposition': 'Content-Disposition',
|
|
'content-duration': 'Content-Duration',
|
|
'content-id': 'Content-ID',
|
|
'content-identifier': 'Content-Identifier',
|
|
'content-language': 'Content-Language',
|
|
'content-location': 'Content-Location',
|
|
'content-md5': 'Content-MD5',
|
|
'content-return': 'Content-Return',
|
|
'content-transfer-encoding': 'Content-Transfer-Encoding',
|
|
'content-type': 'Content-Type',
|
|
'content-features': 'Content-features',
|
|
'conversion': 'Conversion',
|
|
'conversion-with-loss': 'Conversion-With-Loss',
|
|
'dl-expansion-history': 'DL-Expansion-History',
|
|
'date': 'Date',
|
|
'deferred-delivery': 'Deferred-Delivery',
|
|
'delivered-to': 'Delivered-To',
|
|
'delivery-date': 'Delivery-Date',
|
|
'discarded-x400-ipms-extensions': 'Discarded-X400-IPMS-Extensions',
|
|
'discarded-x400-mts-extensions': 'Discarded-X400-MTS-Extensions',
|
|
'disclose-recipients': 'Disclose-Recipients',
|
|
'disposition-notification-options': 'Disposition-Notification-Options',
|
|
'disposition-notification-to': 'Disposition-Notification-To',
|
|
'encoding': 'Encoding',
|
|
'encrypted': 'Encrypted',
|
|
'expires': 'Expires',
|
|
'expiry-date': 'Expiry-Date',
|
|
'from': 'From',
|
|
'generate-delivery-report': 'Generate-Delivery-Report',
|
|
'importance': 'Importance',
|
|
'in-reply-to': 'In-Reply-To',
|
|
'incomplete-copy': 'Incomplete-Copy',
|
|
'keywords': 'Keywords',
|
|
'language': 'Language',
|
|
'latest-delivery-time': 'Latest-Delivery-Time',
|
|
'list-archive': 'List-Archive',
|
|
'list-help': 'List-Help',
|
|
'list-id': 'List-ID',
|
|
'list-owner': 'List-Owner',
|
|
'list-post': 'List-Post',
|
|
'list-subscribe': 'List-Subscribe',
|
|
'list-unsubscribe': 'List-Unsubscribe',
|
|
'mime-version': 'MIME-Version',
|
|
'message-context': 'Message-Context',
|
|
'message-id': 'Message-ID',
|
|
'message-type': 'Message-Type',
|
|
'obsoletes': 'Obsoletes',
|
|
'original-encoded-information-types': 'Original-Encoded-Information-Types',
|
|
'original-message-id': 'Original-Message-ID',
|
|
'originator-return-address': 'Originator-Return-Address',
|
|
'pics-label': 'PICS-Label',
|
|
'prevent-nondelivery-report': 'Prevent-NonDelivery-Report',
|
|
'priority': 'Priority',
|
|
'received': 'Received',
|
|
'recipient': 'To',
|
|
'references': 'References',
|
|
'reply-by': 'Reply-By',
|
|
'reply-to': 'Reply-To',
|
|
'resent-bcc': 'Resent-Bcc',
|
|
'resent-cc': 'Resent-Cc',
|
|
'resent-date': 'Resent-Date',
|
|
'resent-from': 'Resent-From',
|
|
'resent-message-id': 'Resent-Message-ID',
|
|
'resent-reply-to': 'Resent-Reply-To',
|
|
'resent-sender': 'Resent-Sender',
|
|
'resent-to': 'Resent-To',
|
|
'return-path': 'Return-Path',
|
|
'sender': 'Sender',
|
|
'sensitivity': 'Sensitivity',
|
|
'subject': 'Subject',
|
|
'supersedes': 'Supersedes',
|
|
'to': 'To',
|
|
'x400-content-identifier': 'X400-Content-Identifier',
|
|
'x400-content-return': 'X400-Content-Return',
|
|
'x400-content-type': 'X400-Content-Type',
|
|
'x400-mts-identifier': 'X400-MTS-Identifier',
|
|
'x400-originator': 'X400-Originator',
|
|
'x400-received': 'X400-Received',
|
|
'x400-recipients': 'X400-Recipients',
|
|
'x400-trace': 'X400-Trace',
|
|
}
|
|
|
|
SMTP_ADDRESS_HEADERS = {
|
|
'Bcc',
|
|
'Cc',
|
|
'Delivered-To',
|
|
'From',
|
|
'Reply-To',
|
|
'Resent-Bcc',
|
|
'Resent-Cc',
|
|
'Resent-Reply-To',
|
|
'Resent-Sender',
|
|
'Resent-To',
|
|
'Sender',
|
|
'To',
|
|
}
|
|
|
|
SMTP_DATE_HEADERS = {
|
|
'date',
|
|
'delivery-date',
|
|
'expires',
|
|
'expiry-date',
|
|
'latest-delivery-time',
|
|
'reply-by',
|
|
'resent-date',
|
|
}
|
|
|
|
SMTP_NAME_ADDRESS_PATTERN = re.compile(r'^(.+?)\s*<(.+)>$')
|
|
|
|
IMPORT_INSERT = {'import', 'insert'}
|
|
|
|
def _draftImportInsertMessage(users, operation):
|
|
def _appendToHeader(header, value):
|
|
try:
|
|
header.append(value)
|
|
except UnicodeDecodeError:
|
|
header.append(value, UTF8)
|
|
|
|
labelNameMap = {}
|
|
addLabelNames = []
|
|
msgHTML = msgText = ''
|
|
msgHeaders = {}
|
|
tagReplacements = _initTagReplacements()
|
|
attachments = []
|
|
embeddedImages = []
|
|
internalDateSource = 'receivedTime'
|
|
deleted = processForCalendar = substituteForUserInHeaders = False
|
|
neverMarkSpam = True
|
|
emlFile = False
|
|
emlEncoding = 'ascii'
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in SMTP_HEADERS_MAP:
|
|
if myarg == 'content-type':
|
|
unknownArgumentExit()
|
|
if myarg in SMTP_DATE_HEADERS:
|
|
msgDate, _, _ = getTimeOrDeltaFromNow(True)
|
|
msgHeaders[SMTP_HEADERS_MAP[myarg]] = formatdate(time.mktime(msgDate.timetuple()) + msgDate.microsecond/1E6, True)
|
|
if myarg == 'date':
|
|
internalDateSource = 'dateHeader'
|
|
else:
|
|
value = getString(Cmd.OB_STRING)
|
|
if (value.find('#user#') >= 0) or (value.find('#email#') >= 0) or (value.find('#username#') >= 0):
|
|
substituteForUserInHeaders = True
|
|
msgHeaders[SMTP_HEADERS_MAP[myarg]] = value
|
|
elif myarg == 'header':
|
|
header = getString(Cmd.OB_STRING, minLen=1)
|
|
if header.lower() == 'content-type':
|
|
unknownArgumentExit()
|
|
value = getString(Cmd.OB_STRING)
|
|
if (value.find('#user#') >= 0) or (value.find('#email#') >= 0) or (value.find('#username#') >= 0):
|
|
substituteForUserInHeaders = True
|
|
msgHeaders[SMTP_HEADERS_MAP.get(header.lower(), header)] = value
|
|
elif myarg in SORF_MSG_FILE_ARGUMENTS:
|
|
if 'html' in myarg:
|
|
msgHTML, _, _ = getStringOrFile(myarg)
|
|
else:
|
|
msgText, _, _ = getStringOrFile(myarg)
|
|
emlFile = False
|
|
elif myarg == 'emlfile':
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
if checkArgumentPresent('charset'):
|
|
emlEncoding = getString(Cmd.OB_CHAR_SET)
|
|
msgText = readFile(filename, encoding=emlEncoding)
|
|
emlFile = True
|
|
internalDateSource = 'dateHeader'
|
|
if checkArgumentPresent('emlutf8'):
|
|
emlEncoding = UTF8
|
|
elif _getTagReplacement(myarg, tagReplacements, True):
|
|
pass
|
|
elif operation in IMPORT_INSERT and myarg == 'addlabel':
|
|
addLabelNames.append(getString(Cmd.OB_LABEL_NAME, minLen=1))
|
|
elif operation in IMPORT_INSERT and myarg == 'labels':
|
|
addLabelNames.extend(shlexSplitList(getString(Cmd.OB_LABEL_NAME_LIST)))
|
|
elif operation in IMPORT_INSERT and myarg == 'deleted':
|
|
deleted = getBoolean()
|
|
elif myarg == 'attach':
|
|
attachments.append((getFilename(), getCharSet()))
|
|
elif myarg == 'embedimage':
|
|
embeddedImages.append((getFilename(), getString(Cmd.OB_STRING)))
|
|
elif operation == 'import' and myarg == 'nevermarkspam':
|
|
neverMarkSpam = getBoolean()
|
|
elif operation == 'import' and myarg == 'checkspam':
|
|
neverMarkSpam = not getBoolean()
|
|
elif operation == 'import' and myarg == 'processforcalendar':
|
|
processForCalendar = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
if not msgText and not msgHTML:
|
|
missingArgumentExit('textmessage|textfile|htmlmessage|htmlfile|empfile')
|
|
if not emlFile:
|
|
msgText = msgText.replace('\r', '').replace('\\n', '\n')
|
|
msgHTML = msgHTML.replace('\r', '').replace('\\n', '<br/>')
|
|
if not tagReplacements['tags']:
|
|
tmpText = msgText
|
|
tmpHTML = msgHTML
|
|
if operation != 'draft':
|
|
if not emlFile:
|
|
if 'To' not in msgHeaders:
|
|
msgHeaders['To'] = '#user#'
|
|
substituteForUserInHeaders = True
|
|
if 'From' not in msgHeaders:
|
|
msgHeaders['From'] = _getAdminEmail()
|
|
kwargs = {'internalDateSource': internalDateSource, 'deleted': deleted}
|
|
if operation == 'import':
|
|
function = 'import_'
|
|
kwargs.update({'neverMarkSpam': neverMarkSpam, 'processForCalendar': processForCalendar})
|
|
else: #'insert':
|
|
function = 'insert'
|
|
else:
|
|
function = 'create'
|
|
add_charset(UTF8, QP, QP, UTF8)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
userName, _ = splitEmailAddress(user)
|
|
if not emlFile:
|
|
if tagReplacements['tags']:
|
|
if tagReplacements['subs']:
|
|
_getTagReplacementFieldValues(user, i, count, tagReplacements)
|
|
tmpText = _processTagReplacements(tagReplacements, msgText)
|
|
tmpHTML = _processTagReplacements(tagReplacements, msgHTML)
|
|
if attachments or embeddedImages:
|
|
if tmpText and tmpHTML:
|
|
message = MIMEMultipart('alternative')
|
|
textpart = MIMEText(tmpText, 'plain', UTF8)
|
|
message.attach(textpart)
|
|
htmlpart = MIMEText(tmpHTML, 'html', UTF8)
|
|
message.attach(htmlpart)
|
|
elif tmpHTML:
|
|
message = MIMEMultipart()
|
|
htmlpart = MIMEText(tmpHTML, 'html', UTF8)
|
|
message.attach(htmlpart)
|
|
else:
|
|
message = MIMEMultipart()
|
|
textpart = MIMEText(tmpText, 'plain', UTF8)
|
|
message.attach(textpart)
|
|
_addAttachmentsToMessage(message, attachments)
|
|
_addEmbeddedImagesToMessage(message, embeddedImages)
|
|
else:
|
|
if tmpText and tmpHTML:
|
|
message = MIMEMultipart('alternative')
|
|
textpart = MIMEText(tmpText, 'plain', UTF8)
|
|
message.attach(textpart)
|
|
htmlpart = MIMEText(tmpHTML, 'html', UTF8)
|
|
message.attach(htmlpart)
|
|
elif tmpHTML:
|
|
message = MIMEText(tmpHTML, 'html', UTF8)
|
|
else:
|
|
message = MIMEText(tmpText, 'plain', UTF8)
|
|
for header, value in iter(msgHeaders.items()):
|
|
if substituteForUserInHeaders:
|
|
value = _substituteForUser(value, user, userName)
|
|
message[header] = Header()
|
|
if header in SMTP_ADDRESS_HEADERS:
|
|
match = SMTP_NAME_ADDRESS_PATTERN.match(value.strip())
|
|
if match:
|
|
_appendToHeader(message[header], match.group(1))
|
|
_appendToHeader(message[header], match.group(2))
|
|
else:
|
|
_appendToHeader(message[header], value)
|
|
else:
|
|
_appendToHeader(message[header], value)
|
|
tmpFile = TemporaryFile(mode='w+', encoding=UTF8)
|
|
g = Generator(tmpFile, False)
|
|
g.flatten(message)
|
|
tmpFile.seek(0)
|
|
body = {'raw': base64.urlsafe_b64encode(bytes(tmpFile.read(), UTF8)).decode()}
|
|
tmpFile.close()
|
|
else:
|
|
for header, value in iter(msgHeaders.items()):
|
|
if substituteForUserInHeaders:
|
|
value = _substituteForUser(value, user, userName)
|
|
msgText = re.sub(fr'(?sm)\n{header}:.+?(?=[\r\n]+[a-zA-Z0-9-]+:)', f'\n{header}: {value}', msgText, 1)
|
|
message_bytes = msgText.encode(emlEncoding)
|
|
base64_bytes = base64.b64encode(message_bytes)
|
|
body = {'raw': base64_bytes.decode(emlEncoding)}
|
|
try:
|
|
if operation != 'draft':
|
|
if addLabelNames:
|
|
userGmailLabels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
|
|
if not userGmailLabels:
|
|
continue
|
|
labelNameMap = _initLabelNameMap(userGmailLabels)
|
|
body['labelIds'] = _convertLabelNamesToIds(gmail, user, i, count, addLabelNames, labelNameMap, True)
|
|
else:
|
|
body['labelIds'] = ['INBOX']
|
|
result = callGAPI(gmail.users().messages(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
|
|
userId='me', body=body, fields='id', **kwargs)
|
|
else:
|
|
result = callGAPI(gmail.users().drafts(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
userId='me', body={'message': body}, fields='id')
|
|
entityActionPerformed([Ent.USER, user, Ent.MESSAGE, result['id']], i, count)
|
|
except (GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> draft message
|
|
# <MessageContent>
|
|
# (replace <Tag> <UserReplacement>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*
|
|
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
|
# (attach <FileName> [charset <CharSet>])*
|
|
# (embedimage <FileName> <String>)*
|
|
def draftMessage(users):
|
|
_draftImportInsertMessage(users, 'draft')
|
|
|
|
# gam <UserTypeEntity> import message
|
|
# <MessageContent>
|
|
# (replace <Tag> <UserReplacement>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*
|
|
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
|
# (addlabel <LabelName>)* [labels <LabelNameList>]
|
|
# (attach <FileName> [charset <CharSet>])*
|
|
# (embedimage <FileName> <String>)*
|
|
# [deleted [<Boolean>]] [nevermarkspam [<Boolean>]] [processforcalendar [<Boolean>]]
|
|
def importMessage(users):
|
|
_draftImportInsertMessage(users, 'import')
|
|
|
|
# gam <UserTypeEntity> insert message
|
|
# <MessageContent>
|
|
# (replace <Tag> <UserReplacement>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*
|
|
# (<SMTPDateHeader> <Time>)* (<SMTPHeader> <String>)* (header <String> <String>)*
|
|
# (addlabel <LabelName>)* [labels <LabelNameList>]
|
|
# (attach <FileName> [charset <CharSet>])*
|
|
# (embedimage <FileName> <String>)*
|
|
# [deleted [<Boolean>]]
|
|
def insertMessage(users):
|
|
_draftImportInsertMessage(users, 'insert')
|
|
|
|
def printShowMessagesThreads(users, entityType):
|
|
|
|
def _getBodyData(payload, getOrigMsg):
|
|
data = headers = ''
|
|
for part in payload.get('parts', []):
|
|
if getOrigMsg:
|
|
if show_all_headers:
|
|
if not headers:
|
|
headers = '---------- Original message ----------\n'
|
|
for header in part.get('headers', []):
|
|
headers += header['name']+': '+_decodeHeader(header['value'])+'\n'
|
|
elif 'headers' in part:
|
|
for name in headersToShow:
|
|
for header in part['headers']:
|
|
if name == header['name'].lower():
|
|
if not headers:
|
|
headers = '---------- Original message ----------\n'
|
|
headers += SMTP_HEADERS_MAP.get(name, header['name'])+': '+_decodeHeader(header['value'])+'\n'
|
|
if headers:
|
|
headers += 'Body:\n'
|
|
data = Ind.INDENT_SPACES_PER_LEVEL
|
|
if part['mimeType'] == 'text/plain':
|
|
if 'data' in part['body']:
|
|
data += base64.urlsafe_b64decode(str(part['body']['data'])).decode(UTF8)+'\n'
|
|
elif show_html and part['mimeType'] == 'text/html':
|
|
if 'data' in part['body']:
|
|
data += base64.urlsafe_b64decode(str(part['body']['data'])).decode(UTF8)+'\n'
|
|
elif part['mimeType'] == 'text/rfc822-headers':
|
|
if 'data' in part['body']:
|
|
data += _decodeHeader(base64.urlsafe_b64decode(str(part['body']['data'])).decode(UTF8)+'\n')
|
|
else:
|
|
data += _getBodyData(part, part['mimeType'] == 'message/rfc822')
|
|
if getOrigMsg:
|
|
data = data.replace('\n', f'\n{Ind.INDENT_SPACES_PER_LEVEL}').rstrip()
|
|
return headers+data
|
|
|
|
def _getMessageBody(payload):
|
|
if 'attachmentId' not in payload.get('body', {}) and 'data' in payload.get('body', {}):
|
|
return base64.urlsafe_b64decode(str(payload['body']['data'])).decode(UTF8)
|
|
data = _getBodyData(payload, False)
|
|
if data:
|
|
return data
|
|
return 'Body not available'
|
|
|
|
ATTACHMENT_NAME_PATTERN = re.compile(r'^.*name="?(.*?)(?:"|;|$)')
|
|
CHARSET_NAME_PATTERN = re.compile(r'^.*charset="?(.*?)(?:"|;|$)')
|
|
|
|
def _showAttachmentMimeTypeSizeCharset(part, charset):
|
|
printKeyValueList(['mimeType', part['mimeType']])
|
|
printKeyValueList(['size', part['body']['size']])
|
|
if charset:
|
|
printKeyValueList(['charset', charset])
|
|
|
|
def _showSaveAttachments(messageId, payload, attachmentNamePattern, j, jcount):
|
|
for part in payload.get('parts', []):
|
|
if 'attachmentId' in part['body']:
|
|
for header in part['headers']:
|
|
if header['name'] in {'Content-Type', 'Content-Disposition'}:
|
|
mg = ATTACHMENT_NAME_PATTERN.match(header['value'])
|
|
if not mg:
|
|
continue
|
|
attachmentName = mg.group(1)
|
|
if (not attachmentNamePattern) or attachmentNamePattern.match(attachmentName):
|
|
charset = ''
|
|
if part['mimeType'] == 'text/plain':
|
|
mg = CHARSET_NAME_PATTERN.match(header['value'])
|
|
if mg:
|
|
charset = mg.group(1)
|
|
if (part['mimeType'] == 'text/plain' and not noshow_text_plain) or save_attachments or upload_attachments:
|
|
try:
|
|
result = callGAPI(gmail.users().messages().attachments(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
messageId=messageId, id=part['body']['attachmentId'], userId='me')
|
|
if 'data' in result:
|
|
if show_attachments:
|
|
printKeyValueList(['Attachment', attachmentName])
|
|
Ind.Increment()
|
|
if part['mimeType'] == 'text/plain':
|
|
try:
|
|
printKeyValueList([Ind.MultiLineText(base64.urlsafe_b64decode(str(result['data'])).decode(charset)+'\n')])
|
|
except (LookupError, UnicodeDecodeError, UnicodeError):
|
|
_showAttachmentMimeTypeSizeCharset(part, charset)
|
|
else:
|
|
_showAttachmentMimeTypeSizeCharset(part, charset)
|
|
Ind.Decrement()
|
|
if save_attachments:
|
|
filename, _ = uniqueFilename(targetFolder, cleanFilename(attachmentName), overwrite)
|
|
action = Act.Get()
|
|
Act.Set(Act.DOWNLOAD)
|
|
status, e = writeFileReturnError(filename, base64.urlsafe_b64decode(str(result['data'])), mode='wb')
|
|
if status:
|
|
entityActionPerformed([Ent.ATTACHMENT, filename])
|
|
else:
|
|
entityActionFailedWarning([Ent.ATTACHMENT, filename], str(e))
|
|
Act.Set(action)
|
|
if upload_attachments:
|
|
filename = cleanFilename(attachmentName)
|
|
uploadAttachmentBody.update({'name': filename, 'mimeType': part['mimeType']})
|
|
action = Act.Get()
|
|
Act.Set(Act.CREATE)
|
|
media_body = googleapiclient.http.MediaIoBaseUpload(io.BytesIO(base64.urlsafe_b64decode(str(result['data']))), mimetype=part['mimeType'], resumable=True)
|
|
try:
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
|
|
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.INTERNAL_ERROR,
|
|
GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP,
|
|
GAPI.UPLOAD_TOO_LARGE, GAPI.TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED],
|
|
media_body=media_body, body=uploadAttachmentBody, fields='id,name', supportsAllDrives=True)
|
|
entityModifierItemValueListActionPerformed([Ent.DRIVE_FILE, f"{result['name']}({result['id']})"],
|
|
Act.MODIFIER_WITH_CONTENT_FROM, [Ent.ATTACHMENT, filename], j, jcount)
|
|
except (GAPI.forbidden, GAPI.insufficientPermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.internalError,
|
|
GAPI.storageQuotaExceeded, GAPI.teamdrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamdrivefileLimitExceeded, GAPI.teamdriveHierarchyTooDeep,
|
|
GAPI.uploadTooLarge, GAPI.teamdrivesShortcutFileNotSupported) as e:
|
|
entityModifierItemValueListActionFailedWarning([Ent.DRIVE_FILE, None],
|
|
Act.MODIFIER_WITH_CONTENT_FROM, [Ent.ATTACHMENT, filename], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
Act.Set(action)
|
|
except (GAPI.serviceNotAvailable, GAPI.notFound):
|
|
pass
|
|
elif show_attachments:
|
|
printKeyValueList(['Attachment', attachmentName])
|
|
Ind.Increment()
|
|
_showAttachmentMimeTypeSizeCharset(part, charset)
|
|
Ind.Decrement()
|
|
break
|
|
else:
|
|
_showSaveAttachments(messageId, part, attachmentNamePattern, j, jcount)
|
|
|
|
def _initSenderLabelsMap(sender):
|
|
if sender not in senderLabelsMaps:
|
|
senderLabelsMaps[sender] = {'*None*': {'name': '*None*', 'count': 0, 'size': 0, 'type': LABEL_TYPE_USER, 'match': labelMatchPattern is None}}
|
|
for label in labels['labels']:
|
|
senderLabelsMaps[sender][label['id']] = {'name': label['name'], 'count': 0, 'size': 0, 'type': label['type'],
|
|
'match': True if not labelMatchPattern else labelMatchPattern.match(label['name']) is not None}
|
|
return senderLabelsMaps[sender]
|
|
|
|
def _getMatchMessageLabels(result, sender):
|
|
labelsMap = _initSenderLabelsMap(sender)
|
|
messageLabels = []
|
|
match = False
|
|
for labelId in result.get('labelIds', []):
|
|
if labelId in labelsMap:
|
|
match |= labelsMap[labelId]['match']
|
|
if not onlyUser or labelsMap[labelId]['type'] != LABEL_TYPE_SYSTEM:
|
|
messageLabels.append(labelsMap[labelId]['name'])
|
|
else:
|
|
messageLabels.append(labelId)
|
|
if labelMatchPattern and not match:
|
|
return None
|
|
return messageLabels
|
|
|
|
def _checkSenderMatch(result):
|
|
for header in result['payload'].get('headers', []):
|
|
sender = _decodeHeader(header['value'])
|
|
if header['name'] == 'Sender' and senderMatchPattern.match(sender):
|
|
return sender
|
|
return None
|
|
|
|
def _checkSenderMatchCount(result):
|
|
for header in result['payload'].get('headers', []):
|
|
sender = _decodeHeader(header['value'])
|
|
if header['name'] == 'Sender' and senderMatchPattern.match(sender):
|
|
senderCounts.setdefault(sender, {'count': 0, 'size': 0})
|
|
senderCounts[sender]['count'] += 1
|
|
senderCounts[sender]['size'] += result['sizeEstimate']
|
|
return sender
|
|
return None
|
|
|
|
def _qualifyMessage(user, result):
|
|
if senderMatchPattern:
|
|
sender = _checkSenderMatchCount(result)
|
|
if not sender:
|
|
return (False, None)
|
|
else:
|
|
sender = user
|
|
if show_labels or labelMatchPattern:
|
|
messageLabels = _getMatchMessageLabels(result, sender)
|
|
if messageLabels is None:
|
|
return (False, None)
|
|
return (True, messageLabels)
|
|
return (True, None)
|
|
|
|
def _convertDateTime(headerValue):
|
|
dateTimeValue = headerValue.replace('GMT', '+0000')
|
|
dateTimeValue = dateTimeValue.replace('UT', '+0000')
|
|
pLoc = dateTimeValue.find(' (')
|
|
if pLoc > 0:
|
|
dateTimeValue = dateTimeValue[:pLoc]
|
|
try:
|
|
dateTimeValue = datetime.datetime.strptime(dateTimeValue, RFC2822_TIME_FORMAT)
|
|
if dateHeaderConvertTimezone:
|
|
dateTimeValue = dateTimeValue.astimezone(GC.Values[GC.TIMEZONE])
|
|
return dateTimeValue.strftime(dateHeaderFormat)
|
|
except ValueError:
|
|
return headerValue
|
|
|
|
def _showMessage(user, result, j, jcount, checkMax=True):
|
|
if checkMax and parameters['maxToProcess'] and parameters['messagesProcessed'] == parameters['maxToProcess']:
|
|
return
|
|
status, messageLabels = _qualifyMessage(user, result)
|
|
if not status:
|
|
return
|
|
printEntity([Ent.MESSAGE, result['id']], j, jcount)
|
|
Ind.Increment()
|
|
if show_snippet:
|
|
printKeyValueList(['Snippet', dehtml(result['snippet']).replace('\n', ' ')])
|
|
if show_all_headers:
|
|
for header in result['payload'].get('headers', []):
|
|
headerValue = _decodeHeader(header['value'])
|
|
if dateHeaderFormat and header['name'].lower() in SMTP_DATE_HEADERS:
|
|
headerValue = _convertDateTime(headerValue)
|
|
printKeyValueList([header['name'], headerValue])
|
|
else:
|
|
for name in headersToShow:
|
|
for header in result['payload'].get('headers', []):
|
|
if name == header['name'].lower():
|
|
headerValue = _decodeHeader(header['value'])
|
|
if dateHeaderFormat and name in SMTP_DATE_HEADERS:
|
|
headerValue = _convertDateTime(headerValue)
|
|
printKeyValueList([SMTP_HEADERS_MAP.get(name, header['name']), headerValue])
|
|
if show_date:
|
|
printKeyValueList(['Date', formatLocalTimestamp(result['internalDate'])])
|
|
if show_size:
|
|
printKeyValueList(['SizeEstimate', result['sizeEstimate']])
|
|
if show_labels:
|
|
printKeyValueList(['Labels', delimiter.join(messageLabels)])
|
|
if show_body:
|
|
printKeyValueList(['Body', None])
|
|
Ind.Increment()
|
|
printKeyValueList([Ind.MultiLineText(_getMessageBody(result['payload']))])
|
|
Ind.Decrement()
|
|
if show_attachments or save_attachments or upload_attachments:
|
|
_showSaveAttachments(result['id'], result['payload'], attachmentNamePattern, j, jcount)
|
|
Ind.Decrement()
|
|
if checkMax:
|
|
parameters['messagesProcessed'] += 1
|
|
|
|
def _getAttachments(messageId, payload, attachmentNamePattern, attachments):
|
|
for part in payload.get('parts', []):
|
|
if 'attachmentId' in part['body']:
|
|
for header in part['headers']:
|
|
if header['name'] in {'Content-Type', 'Content-Disposition'}:
|
|
mg = ATTACHMENT_NAME_PATTERN.match(header['value'])
|
|
if not mg:
|
|
continue
|
|
attachmentName = mg.group(1)
|
|
charset = ''
|
|
if part['mimeType'] == 'text/plain':
|
|
mg = CHARSET_NAME_PATTERN.match(header['value'])
|
|
if mg:
|
|
charset = mg.group(1)
|
|
if (not attachmentNamePattern) or attachmentNamePattern.match(attachmentName):
|
|
attachments.append((attachmentName, part['mimeType'], part['body']['size'], charset))
|
|
break
|
|
else:
|
|
_getAttachments(messageId, part, attachmentNamePattern, attachments)
|
|
|
|
def _printMessage(user, result, checkMax=True):
|
|
if checkMax and parameters['maxToProcess'] and parameters['messagesProcessed'] == parameters['maxToProcess']:
|
|
return
|
|
status, messageLabels = _qualifyMessage(user, result)
|
|
if not status:
|
|
return
|
|
row = {'User': user, 'threadId': result['threadId'], 'id': result['id']}
|
|
if show_snippet:
|
|
row['Snippet'] = dehtml(result['snippet']).replace('\n', ' ')
|
|
if show_all_headers:
|
|
headerCounts = {}
|
|
for header in result['payload'].get('headers', []):
|
|
headerCounts.setdefault(header['name'], 0)
|
|
headerValue = _decodeHeader(header['value'])
|
|
if dateHeaderFormat and header['name'].lower() in SMTP_DATE_HEADERS:
|
|
headerValue = _convertDateTime(headerValue)
|
|
if headerCounts[header['name']] == 0:
|
|
row[header['name']] = headerValue
|
|
else:
|
|
row[f'{header["name"]} {headerCounts[header["name"]]}'] = headerValue
|
|
headerCounts[header['name']] += 1
|
|
else:
|
|
for name in headersToShow:
|
|
j = 0
|
|
for header in result['payload'].get('headers', []):
|
|
if name == header['name'].lower():
|
|
headerValue = _decodeHeader(header['value'])
|
|
if dateHeaderFormat and name in SMTP_DATE_HEADERS:
|
|
headerValue = _convertDateTime(headerValue)
|
|
if j == 0:
|
|
row[SMTP_HEADERS_MAP.get(name, header['name'])] = headerValue
|
|
else:
|
|
row[f'{SMTP_HEADERS_MAP.get(name, header["name"])} {j}'] = headerValue
|
|
j += 1
|
|
if show_date:
|
|
row['Date'] = formatLocalTimestamp(result['internalDate'])
|
|
if show_size:
|
|
row['SizeEstimate'] = result['sizeEstimate']
|
|
if show_labels:
|
|
row['LabelsCount'] = len(messageLabels)
|
|
row['Labels'] = delimiter.join(messageLabels)
|
|
if show_body:
|
|
if not convertCRNL:
|
|
row['Body'] = _getMessageBody(result['payload'])
|
|
else:
|
|
row['Body'] = escapeCRsNLs(_getMessageBody(result['payload']))
|
|
if show_attachments:
|
|
attachments = []
|
|
_getAttachments(result['id'], result['payload'], attachmentNamePattern, attachments)
|
|
row['Attachments'] = len(attachments)
|
|
for i, attachment in enumerate(attachments):
|
|
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}name'] = attachment[0]
|
|
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}mimeType'] = attachment[1]
|
|
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}size'] = attachment[2]
|
|
row[f'Attachments{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{i}{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}charset'] = attachment[3]
|
|
csvPF.WriteRowTitles(row)
|
|
if checkMax:
|
|
parameters['messagesProcessed'] += 1
|
|
|
|
def _countMessageLabels(user, result):
|
|
if senderMatchPattern:
|
|
sender = _checkSenderMatch(result)
|
|
if not sender:
|
|
return False
|
|
else:
|
|
sender = user
|
|
labelsMap = _initSenderLabelsMap(sender)
|
|
labelIds = result.get('labelIds', [])
|
|
if labelIds:
|
|
for labelId in labelIds:
|
|
if labelId in labelsMap:
|
|
labelsMap[labelId]['count'] += 1
|
|
labelsMap[labelId]['size'] += result['sizeEstimate']
|
|
else:
|
|
labelsMap[labelId] = {'name': labelId, 'count': 1, 'size': result['sizeEstimate'], 'type': LABEL_TYPE_USER,
|
|
'match': True if not labelMatchPattern else labelMatchPattern.match(labelId) is not None}
|
|
elif not labelMatchPattern:
|
|
labelsMap['*None*']['count'] += 1
|
|
labelsMap['*None*']['size'] += result['sizeEstimate']
|
|
|
|
def _countMessages(_, result):
|
|
if senderMatchPattern and not _checkSenderMatchCount(result):
|
|
return
|
|
messageThreadCounts['messages'] += 1
|
|
messageThreadCounts['size'] += result['sizeEstimate']
|
|
|
|
def _showThread(user, result, j, jcount):
|
|
if parameters['maxToProcess'] and parameters['messagesProcessed'] == parameters['maxToProcess']:
|
|
return
|
|
if senderMatchPattern:
|
|
for message in result['messages']:
|
|
if _checkSenderMatch(message):
|
|
break
|
|
else:
|
|
return
|
|
messageThreadCounts['threads'] += 1
|
|
printEntity([Ent.THREAD, result['id']], j, jcount)
|
|
Ind.Increment()
|
|
if show_snippet and 'snippet' in result:
|
|
printKeyValueList(['Snippet', dehtml(result['snippet']).replace('\n', ' ')])
|
|
kcount = len(result['messages'])
|
|
k = 0
|
|
for message in result['messages']:
|
|
k += 1
|
|
_showMessage(user, message, k, kcount, False)
|
|
if k == parameters['maxMessagesPerThread']:
|
|
break
|
|
Ind.Decrement()
|
|
parameters['messagesProcessed'] += 1
|
|
|
|
def _printThread(user, result):
|
|
if parameters['maxToProcess'] and parameters['messagesProcessed'] == parameters['maxToProcess']:
|
|
return
|
|
if senderMatchPattern:
|
|
for message in result['messages']:
|
|
if _checkSenderMatch(message):
|
|
break
|
|
else:
|
|
return
|
|
k = 0
|
|
for message in result['messages']:
|
|
k += 1
|
|
_printMessage(user, message, False)
|
|
if k == parameters['maxMessagesPerThread']:
|
|
break
|
|
messageThreadCounts['threads'] += 1
|
|
parameters['messagesProcessed'] += 1
|
|
|
|
def _countThreadLabels(user, result):
|
|
for message in result['messages']:
|
|
_countMessageLabels(user, message)
|
|
|
|
def _countThreads(_, result):
|
|
if senderMatchPattern:
|
|
for message in result['messages']:
|
|
if _checkSenderMatchCount(message):
|
|
messageThreadCounts['size'] += message['sizeEstimate']
|
|
break
|
|
else:
|
|
return
|
|
else:
|
|
k = 0
|
|
for message in result['messages']:
|
|
k += 1
|
|
messageThreadCounts['size'] += message['sizeEstimate']
|
|
if k == parameters['maxMessagesPerThread']:
|
|
break
|
|
messageThreadCounts['threads'] += 1
|
|
|
|
_GMAIL_ERROR_REASON_TO_MESSAGE_MAP = {GAPI.NOT_FOUND: Msg.DOES_NOT_EXIST, GAPI.INVALID_MESSAGE_ID: Msg.INVALID_MESSAGE_ID}
|
|
|
|
def _handleGmailError(exception, ri):
|
|
http_status, reason, message = checkGAPIError(exception)
|
|
errMsg = getHTTPError(_GMAIL_ERROR_REASON_TO_MESSAGE_MAP, http_status, reason, message)
|
|
if reason not in GAPI.DEFAULT_RETRY_REASONS:
|
|
if not csvPF:
|
|
printKeyValueListWithCount([Ent.Singular(entityType), ri[RI_ITEM], errMsg], int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
setSysExitRC(ACTION_FAILED_RC)
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], errMsg, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
return
|
|
try:
|
|
response = callGAPI(service, 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_MESSAGE_ID],
|
|
userId='me', id=ri[RI_ITEM], format=['metadata', 'full'][show_size or show_body or show_attachments or save_attachments or upload_attachments])
|
|
if countsOnly:
|
|
_callbacks['process'](ri[RI_ENTITY], response)
|
|
else:
|
|
if not csvPF:
|
|
_callbacks['process'](ri[RI_ENTITY], response, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
_callbacks['process'](ri[RI_ENTITY], response)
|
|
except GAPI.notFound:
|
|
entityActionFailedWarning([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], Msg.DOES_NOT_EXIST, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except GAPI.invalidMessageId:
|
|
entityActionFailedWarning([Ent.USER, ri[RI_ENTITY], entityType, ri[RI_ITEM]], Msg.INVALID_MESSAGE_ID, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(ri[RI_ENTITY], int(ri[RI_I]), int(ri[RI_COUNT]))
|
|
|
|
def _callbackShow(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
_callbacks['process'](ri[RI_ENTITY], response, int(ri[RI_J]), int(ri[RI_JCOUNT]))
|
|
else:
|
|
_handleGmailError(exception, ri)
|
|
|
|
def _callbackPrint(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
_callbacks['process'](ri[RI_ENTITY], response)
|
|
else:
|
|
_handleGmailError(exception, ri)
|
|
|
|
def _callbackCountLabels(request_id, response, exception):
|
|
ri = request_id.splitlines()
|
|
if exception is None:
|
|
_callbacks['process'](ri[RI_ENTITY], response)
|
|
else:
|
|
_handleGmailError(exception, ri)
|
|
|
|
def _batchPrintShowMessagesThreads(service, user, jcount, messageIds):
|
|
svcargs = dict([('userId', 'me'), ('id', None), ('format', ['metadata', 'full'][show_body or show_attachments or save_attachments or upload_attachments])]+GM.Globals[GM.EXTRA_ARGS_LIST])
|
|
if countsOnly:
|
|
if show_labels:
|
|
if not senderMatchPattern:
|
|
svcargs['fields'] = 'labelIds,sizeEstimate' if entityType == Ent.MESSAGE else 'messages(labelIds,sizeEstimate)'
|
|
else:
|
|
svcargs['fields'] = 'labelIds,sizeEstimate,payload' if entityType == Ent.MESSAGE else 'messages(labelIds,sizeEstimate,payload)'
|
|
else:
|
|
if not senderMatchPattern:
|
|
svcargs['fields'] = 'sizeEstimate' if entityType == Ent.MESSAGE else 'messages(sizeEstimate)'
|
|
else:
|
|
svcargs['fields'] = 'sizeEstimate,payload' if entityType == Ent.MESSAGE else 'messages(sizeEstimate,payload)'
|
|
method = getattr(service, 'get')
|
|
dbatch = gmail.new_batch_http_request(callback=_callbacks['batch'])
|
|
bcount = 0
|
|
j = 0
|
|
for messageId in messageIds:
|
|
j += 1
|
|
svcparms = svcargs.copy()
|
|
svcparms['id'] = messageId
|
|
dbatch.add(method(**svcparms), request_id=batchRequestID(user, 0, 0, j, jcount, svcparms['id']))
|
|
bcount += 1
|
|
if not labelMatchPattern and parameters['maxToProcess'] and j == parameters['maxToProcess']:
|
|
break
|
|
if bcount == GC.Values[GC.EMAIL_BATCH_SIZE]:
|
|
executeBatch(dbatch)
|
|
dbatch = gmail.new_batch_http_request(callback=_callbacks['batch'])
|
|
bcount = 0
|
|
if labelMatchPattern and parameters['messagesProcessed'] == parameters['maxToProcess']:
|
|
break
|
|
if bcount > 0:
|
|
dbatch.execute()
|
|
|
|
parameters = _initMessageThreadParameters(entityType, True, 0)
|
|
convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
countsOnly = positiveCountsOnly = includeSpamTrash = onlyUser = overwrite = save_attachments = upload_attachments = False
|
|
show_all_headers = show_attachments = show_body = show_date = show_html = show_labels = show_size = show_snippet = False
|
|
noshow_text_plain = False
|
|
attachmentNamePattern = None
|
|
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
|
|
defaultHeaders = ['Date', 'Subject', 'From', 'Reply-To', 'To', 'Delivered-To', 'Content-Type', 'Message-ID']
|
|
headersToShow = [header.lower() for header in defaultHeaders]
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
showMode = Act.Get() == Act.SHOW
|
|
dateHeaderFormat = ''
|
|
dateHeaderConvertTimezone = False
|
|
uploadAttachmentBody = {}
|
|
addCSVData = {}
|
|
parentParms = initDriveFileAttributes()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif _getMessageSelectParameters(myarg, parameters):
|
|
pass
|
|
elif myarg == 'headers':
|
|
headersToShow = getString(Cmd.OB_STRING, minLen=0).lower().replace(',', ' ').split()
|
|
show_all_headers = headersToShow and headersToShow[0] == 'all'
|
|
elif not showMode and myarg in {'convertcrnl', 'converttextnl', 'convertbodynl'}:
|
|
convertCRNL = True
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
elif myarg == 'showdate':
|
|
show_date = True
|
|
elif myarg == 'showbody':
|
|
show_body = True
|
|
elif myarg == 'showhtml':
|
|
show_html = True
|
|
elif myarg == 'showlabels':
|
|
show_labels = True
|
|
elif myarg == 'showsize':
|
|
show_size = True
|
|
elif myarg == 'showsnippet':
|
|
show_snippet = True
|
|
elif myarg == 'showattachments':
|
|
show_attachments = True
|
|
elif myarg == 'noshowtextplain':
|
|
noshow_text_plain = True
|
|
elif myarg == 'attachmentnamepattern':
|
|
attachmentNamePattern = getREPattern(re.IGNORECASE)
|
|
elif showMode and myarg == 'saveattachments':
|
|
save_attachments = True
|
|
elif showMode and myarg == 'targetfolder':
|
|
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
elif showMode and myarg == 'overwrite':
|
|
overwrite = getBoolean()
|
|
elif showMode and myarg == 'uploadattachments':
|
|
upload_attachments = True
|
|
elif showMode and getDriveFileParentAttribute(myarg, parentParms):
|
|
pass
|
|
elif myarg == 'includespamtrash':
|
|
includeSpamTrash = True
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
elif myarg == 'positivecountsonly':
|
|
countsOnly = positiveCountsOnly = True
|
|
elif myarg in {'onlyuser', 'useronly'}:
|
|
onlyUser = getBoolean()
|
|
elif myarg == 'dateheaderformat':
|
|
dateHeaderFormat = getString(Cmd.OB_STRING, minLen=0)
|
|
if dateHeaderFormat == 'iso':
|
|
dateHeaderFormat = IS08601_TIME_FORMAT
|
|
elif dateHeaderFormat == 'rfc2822':
|
|
dateHeaderFormat = RFC2822_TIME_FORMAT
|
|
elif myarg == 'dateheaderconverttimezone':
|
|
dateHeaderConvertTimezone = getBoolean()
|
|
if not dateHeaderFormat:
|
|
dateHeaderFormat = RFC2822_TIME_FORMAT
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
unknownArgumentExit()
|
|
labelMatchPattern = parameters['labelMatchPattern']
|
|
senderMatchPattern = parameters['senderMatchPattern']
|
|
if senderMatchPattern and not show_all_headers and 'sender' not in headersToShow:
|
|
headersToShow.append('sender')
|
|
_finalizeMessageSelectParameters(parameters, False)
|
|
if csvPF:
|
|
if countsOnly:
|
|
if show_labels:
|
|
if not senderMatchPattern:
|
|
sortTitles = ['User']
|
|
else:
|
|
sortTitles = ['User', 'Sender']
|
|
csvPF.SetIndexedTitles(['Labels'])
|
|
_callbacks = {'batch': _callbackCountLabels, 'process': _countMessageLabels if entityType == Ent.MESSAGE else _countThreadLabels}
|
|
else:
|
|
if not senderMatchPattern:
|
|
sortTitles = ['User', parameters['listType']]
|
|
else:
|
|
sortTitles = ['User', 'Sender', parameters['listType']]
|
|
_callbacks = {'batch': _callbackCountLabels, 'process': _countMessages if entityType == Ent.MESSAGE else _countThreads}
|
|
if show_size:
|
|
sortTitles.append('size')
|
|
if addCSVData:
|
|
sortTitles.extend(sorted(addCSVData.keys()))
|
|
csvPF.SetTitles(sortTitles)
|
|
else:
|
|
sortTitles = ['User', 'threadId', 'id']
|
|
sortTitles.extend(defaultHeaders)
|
|
if show_size:
|
|
sortTitles.append('SizeEstimate')
|
|
if show_labels:
|
|
sortTitles.extend(['LabelsCount', 'Labels'])
|
|
if show_snippet:
|
|
sortTitles.append('Snippet')
|
|
if show_body:
|
|
sortTitles.append('Body')
|
|
if addCSVData:
|
|
sortTitles.extend(sorted(addCSVData.keys()))
|
|
_callbacks = {'batch': _callbackPrint, 'process': _printMessage if entityType == Ent.MESSAGE else _printThread}
|
|
csvPF.SetTitles(sortTitles)
|
|
csvPF.SetSortTitles(sortTitles)
|
|
else:
|
|
if countsOnly:
|
|
if show_labels:
|
|
_callbacks = {'batch': _callbackCountLabels, 'process': _countMessageLabels if entityType == Ent.MESSAGE else _countThreadLabels}
|
|
else:
|
|
_callbacks = {'batch': _callbackCountLabels, 'process': _countMessages if entityType == Ent.MESSAGE else _countThreads}
|
|
else:
|
|
_callbacks = {'batch': _callbackShow, 'process': _showMessage if entityType == Ent.MESSAGE else _showThread}
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, messageIds = _validateUserGetMessageIds(user, i, count, parameters['messageEntity'])
|
|
if not gmail:
|
|
continue
|
|
service = gmail.users().messages() if entityType == Ent.MESSAGE else gmail.users().threads()
|
|
if upload_attachments:
|
|
_, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, uploadAttachmentBody, parentParms):
|
|
continue
|
|
if show_labels or labelMatchPattern:
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
|
|
if not labels:
|
|
continue
|
|
senderLabelsMaps = {}
|
|
if not senderMatchPattern:
|
|
_initSenderLabelsMap(user)
|
|
messageThreadCounts = {'User': user, parameters['listType']: 0, 'size': 0}
|
|
if addCSVData:
|
|
messageThreadCounts.update(addCSVData)
|
|
senderCounts = {}
|
|
if save_attachments:
|
|
_, userName, _ = splitEmailAddressOrUID(user)
|
|
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
try:
|
|
if parameters['messageEntity'] is None:
|
|
printGettingAllEntityItemsForWhom(entityType, user, i, count)
|
|
listResult = callGAPIpages(service, 'list', parameters['listType'],
|
|
pageMessage=getPageMessageForWhom(), maxItems=parameters['maxItems'],
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
|
|
userId='me', q=parameters['query'], fields=parameters['fields'], includeSpamTrash=includeSpamTrash,
|
|
maxResults=GC.Values[GC.MESSAGE_MAX_RESULTS])
|
|
messageIds = [message['id'] for message in listResult]
|
|
else:
|
|
# Need to get authorization set up for batch
|
|
callGAPI(gmail.users(), 'getProfile',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+GAPI.GMAIL_LIST_THROW_REASONS,
|
|
userId='me', fields='')
|
|
except (GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.invalid, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(messageIds)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
if countsOnly and not show_labels and not senderMatchPattern and not show_size:
|
|
if not positiveCountsOnly or jcount > 0:
|
|
if not csvPF:
|
|
printEntityKVList([Ent.USER, user], [parameters['listType'], jcount], i, count)
|
|
else:
|
|
row = {'User': user, parameters['listType']: jcount}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
continue
|
|
if not csvPF and not countsOnly:
|
|
if (parameters['messageEntity'] is not None or
|
|
((parameters['maxToProcess'] == 0 or jcount <= parameters['maxToProcess']) and
|
|
(not labelMatchPattern and not senderMatchPattern))):
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
|
|
elif not labelMatchPattern and not senderMatchPattern:
|
|
entityPerformActionNumItemsModifier([Ent.USER, user], parameters['maxToProcess'], entityType, f'of {jcount} Total {Ent.Plural(entityType)}', i, count)
|
|
else:
|
|
entityPerformActionModifierNumItemsModifier([Ent.USER, user], Msg.MAXIMUM_OF, parameters['maxToProcess'] or jcount, entityType,
|
|
f'of {jcount} Total {Ent.Plural(entityType)}', i, count)
|
|
if parameters['messageEntity'] is None and not labelMatchPattern and parameters['maxToProcess'] and (jcount > parameters['maxToProcess']):
|
|
jcount = parameters['maxToProcess']
|
|
parameters['messagesProcessed'] = 0
|
|
if not csvPF:
|
|
Ind.Increment()
|
|
_batchPrintShowMessagesThreads(service, user, jcount, messageIds)
|
|
Ind.Decrement()
|
|
else:
|
|
_batchPrintShowMessagesThreads(service, user, jcount, messageIds)
|
|
if countsOnly:
|
|
if show_labels:
|
|
if onlyUser or positiveCountsOnly or labelMatchPattern:
|
|
for sender in senderLabelsMaps:
|
|
userLabelsMap = {}
|
|
for labelId, label in iter(senderLabelsMaps[sender].items()):
|
|
if (label['match'] and
|
|
(not onlyUser or label['type'] != LABEL_TYPE_SYSTEM) and
|
|
(not positiveCountsOnly or label['count'] > 0)):
|
|
userLabelsMap[labelId] = label
|
|
senderLabelsMaps[sender] = userLabelsMap
|
|
if not csvPF:
|
|
for sender, labelsMap in sorted(iter(senderLabelsMaps.items())):
|
|
jcount = len(labelsMap)
|
|
kvlist = [Ent.USER, user]
|
|
if senderMatchPattern:
|
|
kvlist.extend([Ent.SENDER, sender])
|
|
entityPerformActionNumItems(kvlist, jcount, Ent.LABEL, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for label in sorted(iter(labelsMap.values()), key=lambda k: k['name']):
|
|
j += 1
|
|
if not show_size:
|
|
printEntityKVList([Ent.LABEL, label['name']], ['Count', label['count'], 'Type', label['type']], j, jcount)
|
|
else:
|
|
printEntityKVList([Ent.LABEL, label['name']], ['Count', label['count'], 'Size', label['size'], 'Type', label['type']], j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for sender, labelsMap in sorted(iter(senderLabelsMaps.items())):
|
|
row = {'User': user}
|
|
if senderMatchPattern:
|
|
row['Sender'] = sender
|
|
if not show_size:
|
|
for label in labelsMap.values():
|
|
label.pop('size', None)
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRowTitles(flattenJSON({'Labels': sorted(iter(labelsMap.values()), key=lambda k: k['name'])}, flattened=row))
|
|
elif not senderMatchPattern:
|
|
v = messageThreadCounts[parameters['listType']]
|
|
if not positiveCountsOnly or v > 0:
|
|
if not csvPF:
|
|
if not show_size:
|
|
printEntityKVList([Ent.USER, user], [parameters['listType'], v], i, count)
|
|
else:
|
|
printEntityKVList([Ent.USER, user], [parameters['listType'], v, 'size', messageThreadCounts['size']], i, count)
|
|
else:
|
|
if not show_size:
|
|
messageThreadCounts.pop('size', None)
|
|
csvPF.WriteRow(messageThreadCounts)
|
|
else:
|
|
if not show_size:
|
|
if not csvPF:
|
|
for k, v in sorted(iter(senderCounts.items())):
|
|
if not positiveCountsOnly or v['count'] > 0:
|
|
printEntityKVList([Ent.USER, user, Ent.SENDER, k], [parameters['listType'], v['count']], i, count)
|
|
else:
|
|
for k, v in sorted(iter(senderCounts.items())):
|
|
if not positiveCountsOnly or v['count'] > 0:
|
|
row = {'User': user, 'Sender': k, parameters['listType']: v['count']}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
else:
|
|
if not csvPF:
|
|
for k, v in sorted(iter(senderCounts.items())):
|
|
if not positiveCountsOnly or v['count'] > 0:
|
|
printEntityKVList([Ent.USER, user, Ent.SENDER, k], [parameters['listType'], v['count'], 'size', v['size']], i, count)
|
|
else:
|
|
for k, v in sorted(iter(senderCounts.items())):
|
|
if not positiveCountsOnly or v['count'] > 0:
|
|
row = {'User': user, 'Sender': k, parameters['listType']: v['count'], 'size': v['size']}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRow(row)
|
|
if csvPF:
|
|
if not countsOnly:
|
|
csvPF.RemoveTitles(['SizeEstimate', 'LabelsCount', 'Labels', 'Snippet', 'Body'])
|
|
if show_size:
|
|
csvPF.AddTitle('SizeEstimate')
|
|
if show_labels:
|
|
csvPF.AddTitles(['LabelsCount', 'Labels'])
|
|
if show_snippet:
|
|
csvPF.AddTitle('Snippet')
|
|
if show_body:
|
|
csvPF.AddTitle('Body')
|
|
csvPF.SetSortAllTitles()
|
|
csvPF.writeCSVfile('Messages')
|
|
else:
|
|
csvPF.writeCSVfile('Message Counts' if not show_labels else 'Message Label Counts')
|
|
|
|
# gam <UserTypeEntity> print message|messages [todrive <ToDriveAttribute>*]
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <MessageIDEntity>)
|
|
# [labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
|
|
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
|
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
|
# [convertcrnl] [delimiter <Character>]
|
|
# [countsonly|positivecountsonly] [useronly]
|
|
# [[attachmentnamepattern <REMatchPattern>]
|
|
# [showattachments [noshowtextplain]]]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# gam <UserTypeEntity> show message|messages
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <MessageIDEntity>)
|
|
# [labelmatchpattern <REMatchPattern>] [sendermatchpattern <REMatchPattern>]
|
|
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
|
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
|
# [countsonly|positivecountsonly] [useronly]
|
|
# [[attachmentnamepattern <REMatchPattern>]
|
|
# [showattachments [noshowtextplain]]
|
|
# [saveattachments [targetfolder <FilePath>] [overwrite [<Boolean>]]]
|
|
# [uploadattachments [<DriveFileParentAttribute>]]]
|
|
def printShowMessages(users):
|
|
printShowMessagesThreads(users, Ent.MESSAGE)
|
|
|
|
# gam <UserTypeEntity> print thread|threads [todrive <ToDriveAttribute>*]
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_print <Number>] [includespamtrash])|(ids <ThreadIDEntity>)
|
|
# [labelmatchpattern <REMatchPattern>]
|
|
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
|
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
|
# [convertcrnl] [delimiter <Character>]
|
|
# [countsonly|positivecountsonly] [useronly]
|
|
# [[attachmentnamepattern <REMatchPattern>]
|
|
# [showattachments [noshowtextplain]]]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# gam <UserTypeEntity> show thread|threads
|
|
# (((query <QueryGmail> [querytime<String> <Date>]*) (matchlabel <LabelName>) [or|and])* [quick|notquick] [max_to_show <Number>] [includespamtrash])|(ids <ThreadIDEntity>)
|
|
# [labelmatchpattern <REMatchPattern>]
|
|
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
|
# [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet]
|
|
# [countsonly|positivecountsonly] [useronly]
|
|
# [[attachmentnamepattern <REMatchPattern>]
|
|
# [showattachments [noshowtextplain]]
|
|
# [saveattachments [targetfolder <FilePath>] [overwrite [<Boolean>]]]
|
|
# [uploadattachments [<DriveFileParentAttribute>]]]
|
|
def printShowThreads(users):
|
|
printShowMessagesThreads(users, Ent.THREAD)
|
|
|
|
# gam <UserTypeEntity> create delegate|delegates [convertalias] <UserEntity>
|
|
# gam <UserTypeEntity> delete delegate|delegates [convertalias] <UserEntity>
|
|
def processDelegates(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
function = 'delete' if Act.Get() == Act.DELETE else 'create'
|
|
aliasAllowed = not checkArgumentPresent(['convertalias'])
|
|
delegateEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.DELEGATE)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, delegates, jcount = _validateUserGetObjectList(user, i, count, delegateEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for delegate in delegates:
|
|
j += 1
|
|
delegateEmail = convertUIDtoEmailAddress(delegate, cd=cd, emailTypes=['user', 'group'], aliasAllowed=aliasAllowed)
|
|
try:
|
|
if function == 'create':
|
|
callGAPI(gmail.users().settings().delegates(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION, GAPI.INVALID,
|
|
GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
userId='me', body={'delegateEmail': delegateEmail})
|
|
else:
|
|
callGAPI(gmail.users().settings().delegates(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.INVALID_INPUT, GAPI.PERMISSION_DENIED],
|
|
userId='me', delegateEmail=delegateEmail)
|
|
entityActionPerformed([Ent.USER, user, Ent.DELEGATE, delegateEmail], j, jcount)
|
|
except (GAPI.alreadyExists, GAPI.failedPrecondition, GAPI.invalid,
|
|
GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DELEGATE, delegateEmail], str(e), j, jcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delegate to [convertalias] <UserEntity>
|
|
def delegateTo(users):
|
|
checkArgumentPresent('to', required=True)
|
|
processDelegates(users)
|
|
|
|
# gam <UserTypeEntity> update delegate|delegates [convertalias] [<UserEntity>]
|
|
def updateDelegates(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
aliasAllowed = not checkArgumentPresent(['convertalias'])
|
|
if Cmd.ArgumentsRemaining():
|
|
delegateEntity = getUserObjectEntity(Cmd.OB_USER_ENTITY, Ent.DELEGATE)
|
|
checkForExtraneousArguments()
|
|
else:
|
|
delegateEntity = None
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if delegateEntity is None:
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
try:
|
|
result = callGAPI(gmail.users().settings().delegates(), 'list',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
|
|
userId='me')
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DELEGATE, None], str(e), i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
delegates = result.get('delegates', []) if result is not None else []
|
|
jcount = len(delegates)
|
|
entityPerformActionModifierNumItems([Ent.USER, user], Msg.MAXIMUM_OF, jcount, Ent.DELEGATE, i, count)
|
|
else:
|
|
user, gmail, delegates, jcount = _validateUserGetObjectList(user, i, count, delegateEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for delegate in delegates:
|
|
j += 1
|
|
if delegateEntity is not None or delegate['verificationStatus'] == 'accepted':
|
|
delegateEmail = delegate['delegateEmail'] if delegateEntity is None else convertUIDtoEmailAddress(delegate, cd=cd, aliasAllowed=aliasAllowed)
|
|
try:
|
|
callGAPI(gmail.users().settings().delegates(), 'create',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION,
|
|
GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
userId='me', body={'delegateEmail': delegateEmail, 'verificationStatus': 'accepted'})
|
|
entityActionPerformed([Ent.USER, user, Ent.DELEGATE, delegateEmail], j, jcount)
|
|
except GAPI.alreadyExists:
|
|
entityActionPerformed([Ent.USER, user, Ent.DELEGATE, delegateEmail], j, jcount)
|
|
except (GAPI.failedPrecondition, GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DELEGATE, delegateEmail], str(e), j, jcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print delegates|delegate [todrive <ToDriveAttribute>*] [shownames]
|
|
# gam <UserTypeEntity> show delegates|delegate [shownames] [csv]
|
|
def printShowDelegates(users):
|
|
titlesList = ['User', 'delegateAddress', 'delegationStatus']
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
cd = None
|
|
csvStyle = showNames = False
|
|
delegateNames = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif not csvPF and myarg == 'csv':
|
|
csvStyle = True
|
|
elif myarg == 'shownames':
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
titlesList = ['User', 'delegateName', 'delegateAddress', 'delegationStatus']
|
|
showNames = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if csvPF:
|
|
csvPF.AddTitles(titlesList)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(Ent.DELEGATE, user, i, count)
|
|
try:
|
|
result = callGAPI(gmail.users().settings().delegates(), 'list',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.FAILED_PRECONDITION],
|
|
userId='me')
|
|
delegates = result.get('delegates', []) if result is not None else []
|
|
jcount = len(delegates)
|
|
if not csvPF:
|
|
if not csvStyle:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.DELEGATE, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for delegate in delegates:
|
|
j += 1
|
|
status = delegate['verificationStatus']
|
|
delegateEmail = delegate['delegateEmail']
|
|
if cd:
|
|
printEntity([Ent.DELEGATE, _getDelegateName(cd, delegateEmail, delegateNames)], j, jcount)
|
|
Ind.Increment()
|
|
printKeyValueList(['Status', status])
|
|
printKeyValueList(['Delegate Email', delegateEmail])
|
|
Ind.Decrement()
|
|
else:
|
|
printEntity([Ent.DELEGATE, delegateEmail], j, jcount)
|
|
Ind.Increment()
|
|
printKeyValueList(['Status', status])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
else:
|
|
j = 0
|
|
for delegate in delegates:
|
|
j += 1
|
|
status = delegate['verificationStatus']
|
|
delegateEmail = delegate['delegateEmail']
|
|
if cd:
|
|
writeStdout(f'{user},{_getDelegateName(cd, delegateEmail, delegateNames)},{status},{delegateEmail}\n')
|
|
else:
|
|
writeStdout(f'{user},{status},{delegateEmail}\n')
|
|
else:
|
|
if delegates:
|
|
if showNames:
|
|
for delegate in delegates:
|
|
csvPF.WriteRow({'User': user, 'delegateName': _getDelegateName(cd, delegate['delegateEmail'], delegateNames),
|
|
'delegateAddress': delegate['delegateEmail'], 'delegationStatus': delegate['verificationStatus']})
|
|
else:
|
|
for delegate in delegates:
|
|
csvPF.WriteRow({'User': user, 'delegateAddress': delegate['delegateEmail'],
|
|
'delegationStatus': delegate['verificationStatus']})
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'User': user})
|
|
except (GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DELEGATE, None], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Delegates')
|
|
|
|
FILTER_ADD_LABEL_TO_ARGUMENT_MAP = {
|
|
'IMPORTANT': 'important',
|
|
'STARRED': 'star',
|
|
'TRASH': 'trash',
|
|
}
|
|
|
|
FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP = {
|
|
'IMPORTANT': 'notimportant',
|
|
'INBOX': 'archive',
|
|
'SPAM': 'neverspam',
|
|
'UNREAD': 'markread',
|
|
}
|
|
|
|
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"]} {formatMaxMessageBytes(userFilter["criteria"][item], ONE_KILO_10_BYTES, ONE_MEGA_10_BYTES)}'
|
|
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 _mapFilterLabelIdsToNames(userFilter, labels):
|
|
# Map user label IDs to label names
|
|
if 'action' in userFilter:
|
|
for field in ['addLabelIds', 'removeLabelIds']:
|
|
if field in userFilter['action']:
|
|
for i, labelId in enumerate(userFilter['action'][field]):
|
|
if labelId not in GMAIL_SYSTEM_LABELS and labelId not in GMAIL_CATEGORY_LABELS:
|
|
userFilter['action'][field][i] = _getLabelName(labels, labelId)
|
|
|
|
def _showFilter(userFilter, j, jcount, labels, FJQC=None):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
if labels['labels']:
|
|
_mapFilterLabelIdsToNames(userFilter, labels)
|
|
printLine(json.dumps(cleanJSON(userFilter), ensure_ascii=False, sort_keys=False))
|
|
return
|
|
printEntity([Ent.FILTER, userFilter['id']], j, jcount)
|
|
Ind.Increment()
|
|
printEntitiesCount(Ent.CRITERIA, None)
|
|
Ind.Increment()
|
|
if 'criteria' in userFilter:
|
|
for item in sorted(userFilter['criteria']):
|
|
if item in {'hasAttachment', 'excludeChats'}:
|
|
printKeyValueList([item])
|
|
elif item == 'size':
|
|
printKeyValueList([f'{item} {userFilter["criteria"]["sizeComparison"]} {formatMaxMessageBytes(userFilter["criteria"][item], ONE_KILO_10_BYTES, ONE_MEGA_10_BYTES)}'])
|
|
elif item == 'sizeComparison':
|
|
pass
|
|
else:
|
|
printKeyValueList([f'{item} "{userFilter["criteria"][item]}"'])
|
|
else:
|
|
printKeyValueList([ERROR, Msg.NO_FILTER_CRITERIA.format(Ent.Singular(Ent.FILTER))])
|
|
Ind.Decrement()
|
|
printEntitiesCount(Ent.ACTION, None)
|
|
Ind.Increment()
|
|
if 'action' in userFilter:
|
|
for labelId in sorted(userFilter['action'].get('addLabelIds', [])):
|
|
if labelId in FILTER_ADD_LABEL_TO_ARGUMENT_MAP:
|
|
printKeyValueList([FILTER_ADD_LABEL_TO_ARGUMENT_MAP[labelId]])
|
|
else:
|
|
printKeyValueList([f'label "{_getLabelName(labels, labelId)}"'])
|
|
for labelId in sorted(userFilter['action'].get('removeLabelIds', [])):
|
|
if labelId in FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP:
|
|
printKeyValueList([FILTER_REMOVE_LABEL_TO_ARGUMENT_MAP[labelId]])
|
|
Ind.Decrement()
|
|
if userFilter['action'].get('forward'):
|
|
printEntity([Ent.FORWARDING_ADDRESS, userFilter['action']['forward']])
|
|
else:
|
|
printKeyValueList([ERROR, Msg.NO_FILTER_ACTIONS.format(Ent.Singular(Ent.FILTER))])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
#
|
|
FILTER_CATEGORY_CHOICE_MAP = {
|
|
'personal': 'CATEGORY_PERSONAL',
|
|
'social': 'CATEGORY_SOCIAL',
|
|
'promotions': 'CATEGORY_PROMOTIONS',
|
|
'updates': 'CATEGORY_UPDATES',
|
|
'forums': 'CATEGORY_FORUMS',
|
|
}
|
|
FILTER_CRITERIA_CHOICE_MAP = {
|
|
'excludechats': 'excludeChats',
|
|
'from': 'from',
|
|
'hasattachment': 'hasAttachment',
|
|
'haswords': 'query',
|
|
'musthaveattachment': 'hasAttachment',
|
|
'negatedquery': 'negatedQuery',
|
|
'nowords': 'negatedQuery',
|
|
'query': 'query',
|
|
'size': 'size',
|
|
'subject': 'subject',
|
|
'to': 'to',
|
|
}
|
|
FILTER_ADD_LABEL_ACTIONS = ['important', 'star', 'trash']
|
|
FILTER_REMOVE_LABEL_ACTIONS = ['markread', 'notimportant', 'archive', 'neverspam']
|
|
FILTER_ACTION_CHOICES = FILTER_ADD_LABEL_ACTIONS+FILTER_REMOVE_LABEL_ACTIONS+['category', 'forward', 'label']
|
|
FILTER_ACTION_LABEL_MAP = {
|
|
'archive': 'INBOX',
|
|
'important': 'IMPORTANT',
|
|
'markread': 'UNREAD',
|
|
'neverspam': 'SPAM',
|
|
'notimportant': 'IMPORTANT',
|
|
'star': 'STARRED',
|
|
'trash': 'TRASH',
|
|
}
|
|
|
|
# gam <UserTypeEntity> [create]
|
|
# (filter <FilterCriteria>+ <FilterAction>+) |
|
|
# ((json [charset <Charset>] <String>) |
|
|
# (json file <FileName> [charset <Charset>]))
|
|
# [buildpath [<Boolean>]]
|
|
def createFilter(users):
|
|
body = {'criteria': {}, 'action': {'addLabelIds': [], 'removeLabelIds': []}}
|
|
buildPath = False
|
|
jsonData = None
|
|
categorySpecified = labelSpecified = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if jsonData is None and myarg in FILTER_CRITERIA_CHOICE_MAP:
|
|
myarg = FILTER_CRITERIA_CHOICE_MAP[myarg]
|
|
if myarg in {'from', 'to', 'subject', 'query', 'negatedQuery'}:
|
|
body['criteria'][myarg] = getString(Cmd.OB_STRING)
|
|
elif myarg in {'hasAttachment', 'excludeChats'}:
|
|
body['criteria'][myarg] = True
|
|
elif myarg == 'size':
|
|
body['criteria']['sizeComparison'] = getChoice(['larger', 'smaller'])
|
|
body['criteria'][myarg] = getMaxMessageBytes(ONE_KILO_10_BYTES, ONE_MEGA_10_BYTES)
|
|
elif jsonData is None and myarg in FILTER_ACTION_CHOICES:
|
|
if myarg in FILTER_ADD_LABEL_ACTIONS:
|
|
myarg = FILTER_ACTION_LABEL_MAP[myarg]
|
|
body['action']['addLabelIds'].append(myarg)
|
|
if (myarg == 'IMPORTANT') and (myarg in body['action']['removeLabelIds']):
|
|
body['action']['removeLabelIds'].remove(myarg)
|
|
elif myarg in FILTER_REMOVE_LABEL_ACTIONS:
|
|
myarg = FILTER_ACTION_LABEL_MAP[myarg]
|
|
body['action']['removeLabelIds'].append(myarg)
|
|
if (myarg == 'IMPORTANT') and (myarg in body['action']['addLabelIds']):
|
|
body['action']['addLabelIds'].remove(myarg)
|
|
elif myarg == 'forward':
|
|
body['action']['forward'] = getEmailAddress(noUid=True)
|
|
elif myarg == 'label':
|
|
label = getString(Cmd.OB_LABEL_NAME)
|
|
labelUpper = label.upper()
|
|
if labelUpper not in GMAIL_SYSTEM_LABELS:
|
|
if labelUpper not in GMAIL_CATEGORY_LABELS:
|
|
if not labelSpecified:
|
|
body['action']['addLabelIds'].append(label)
|
|
labelSpecified = True
|
|
else:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.FILTER_CAN_ONLY_CONTAIN_ONE_USER_LABEL)
|
|
elif not categorySpecified:
|
|
body['action']['addLabelIds'].append(labelUpper)
|
|
categorySpecified = True
|
|
else:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.FILTER_CAN_ONLY_CONTAIN_ONE_CATEGORY_LABEL)
|
|
else:
|
|
body['action']['addLabelIds'].append(labelUpper)
|
|
if (labelUpper == 'IMPORTANT') and (labelUpper in body['action']['removeLabelIds']):
|
|
body['action']['removeLabelIds'].remove(labelUpper)
|
|
elif myarg == 'category':
|
|
if not categorySpecified:
|
|
body['action']['addLabelIds'].append(getChoice(FILTER_CATEGORY_CHOICE_MAP, mapChoice=True))
|
|
categorySpecified = True
|
|
else:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.FILTER_CAN_ONLY_CONTAIN_ONE_CATEGORY_LABEL)
|
|
else:
|
|
unknownArgumentExit()
|
|
elif myarg == 'json':
|
|
jsonData = getJSON([])
|
|
body['criteria'] = jsonData['criteria']
|
|
body['action'] = jsonData['action']
|
|
elif myarg == 'buildpath':
|
|
buildPath = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
if not body['criteria']:
|
|
missingChoiceExit(FILTER_CRITERIA_CHOICE_MAP)
|
|
if not body['action'].get('addLabelIds') and not body['action'].get('removeLabelIds') and 'forward' not in body['action']:
|
|
missingChoiceExit(FILTER_ACTION_CHOICES)
|
|
addLabelIndicies = {}
|
|
for field in ['addLabelIds', 'removeLabelIds']:
|
|
for i, labelId in enumerate(body['action'].get(field, [])):
|
|
if labelId not in GMAIL_SYSTEM_LABELS and labelId not in GMAIL_CATEGORY_LABELS:
|
|
addLabelIndicies.setdefault(labelId, {})
|
|
addLabelIndicies[labelId][field] = i
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if addLabelIndicies:
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
|
|
if not labels:
|
|
continue
|
|
labelSet = _getLabelSet(labels)
|
|
try:
|
|
lcount = len(addLabelIndicies)
|
|
l = 0
|
|
for addLabelName, addLabelData in iter(addLabelIndicies.items()):
|
|
l += 1
|
|
retries = 3
|
|
for _ in range(1, retries+1):
|
|
addLabelId = _getLabelId(labels, addLabelName)
|
|
if addLabelId:
|
|
retries = 0
|
|
break
|
|
lbody = {'name': addLabelName}
|
|
if not buildPath:
|
|
try:
|
|
result = callGAPI(gmail.users().labels(), 'create',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.DUPLICATE, GAPI.PERMISSION_DENIED],
|
|
userId='me', body=lbody, fields='id')
|
|
entityActionPerformed([Ent.USER, user, Ent.LABEL, addLabelName], l, lcount)
|
|
addLabelId = result['id']
|
|
labels['labels'].append({'id': result['id'], 'name': addLabelName})
|
|
retries = 0
|
|
break
|
|
except GAPI.duplicate:
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name,type)')
|
|
labelSet = _getLabelSet(labels)
|
|
else:
|
|
buildLabelPath(gmail, user, i, count, lbody, addLabelName, labelSet, l, lcount)
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name, type)')
|
|
labelSet = _getLabelSet(labels)
|
|
if retries:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.LABEL, addLabelName], Msg.DUPLICATE, i, count)
|
|
continue
|
|
for field in ['addLabelIds', 'removeLabelIds']:
|
|
if field in addLabelData:
|
|
body['action'][field][addLabelData[field]] = addLabelId
|
|
result = callGAPI(gmail.users().settings().filters(), 'create',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID, GAPI.INVALID_ARGUMENT,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
|
|
userId='me', body=body, fields='id')
|
|
if result:
|
|
entityActionPerformed([Ent.USER, user, Ent.FILTER, result['id']], i, count)
|
|
except (GAPI.invalid, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.FILTER, ''], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> delete filter <FilterIDEntity>
|
|
def deleteFilters(users):
|
|
filterIdEntity = getUserObjectEntity(Cmd.OB_FILTER_ID_ENTITY, Ent.FILTER)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, filterIds, jcount = _validateUserGetObjectList(user, i, count, filterIdEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for filterId in filterIds:
|
|
j += 1
|
|
try:
|
|
callGAPI(gmail.users().settings().filters(), 'delete',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
|
|
userId='me', id=filterId)
|
|
entityActionPerformed([Ent.USER, user, Ent.FILTER, filterId], j, jcount)
|
|
except (GAPI.notFound, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.FILTER, filterId], str(e), j, jcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> info filters <FilterIDEntity> [labelidsonly] [formatjson]
|
|
def infoFilters(users):
|
|
labelIdsOnly = False
|
|
filterIdEntity = getUserObjectEntity(Cmd.OB_FILTER_ID_ENTITY, Ent.FILTER)
|
|
FJQC = FormatJSONQuoteChar()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'labelidsonly':
|
|
labelIdsOnly = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, filterIds, jcount = _validateUserGetObjectList(user, i, count, filterIdEntity,
|
|
showAction=FJQC is None or not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
if not labelIdsOnly:
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name)')
|
|
if not labels:
|
|
continue
|
|
else:
|
|
labels = {'labels': []}
|
|
Ind.Increment()
|
|
j = 0
|
|
for filterId in filterIds:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(gmail.users().settings().filters(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
userId='me', id=filterId)
|
|
if not FJQC.formatJSON:
|
|
printEntityKVList([Ent.USER, user],
|
|
[Ent.Singular(Ent.FILTER), result['id']],
|
|
i, count)
|
|
Ind.Increment()
|
|
_showFilter(result, j, jcount, labels, FJQC)
|
|
Ind.Decrement()
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.FILTER, filterId], str(e), j, jcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print filters [labelidsonly] [todrive <ToDriveAttribute>*]
|
|
# [formatjson] [quotechar <Character>]
|
|
# gam <UserTypeEntity> show filters [labelidsonly] [formatjson]
|
|
def printShowFilters(users):
|
|
labelIdsOnly = False
|
|
csvPF = CSVPrintFile() if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'labelidsonly':
|
|
labelIdsOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg)
|
|
if csvPF:
|
|
csvPF.SetFormatJSON(False)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if not labelIdsOnly:
|
|
labels = _getUserGmailLabels(gmail, user, i, count, 'labels(id,name)')
|
|
if not labels:
|
|
continue
|
|
else:
|
|
labels = {'labels': []}
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.FILTER, user, i, count)
|
|
try:
|
|
results = callGAPIitems(gmail.users().settings().filters(), 'list', 'filter',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
jcount = len(results)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.FILTER, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for userFilter in results:
|
|
j += 1
|
|
_showFilter(userFilter, j, jcount, labels, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
if results:
|
|
for userFilter in results:
|
|
row = _printFilter(user, userFilter, labels)
|
|
if FJQC.formatJSON:
|
|
if labels['labels']:
|
|
_mapFilterLabelIdsToNames(userFilter, labels)
|
|
row['JSON'] = json.dumps(userFilter, ensure_ascii=False, sort_keys=False)
|
|
csvPF.WriteRowTitles(row)
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'User': user})
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.SetFormatJSON(False)
|
|
csvPF.SetSortTitles(['User', 'id', 'from', 'to', 'subject', 'query', 'negatedQuery', 'hasAttachment', 'excludeChats', 'size', 'forward',
|
|
'archive', 'important', 'label', 'markread', 'star'])
|
|
csvPF.SortTitles()
|
|
if FJQC.formatJSON:
|
|
csvPF.MoveTitlesToEnd(['JSON'])
|
|
csvPF.SetSortTitles([])
|
|
csvPF.writeCSVfile('Filters')
|
|
|
|
def findFormRequest(field, newRequest, ubody):
|
|
for request in ubody['requests']:
|
|
if field in request:
|
|
return request
|
|
ubody['requests'].append(newRequest)
|
|
return ubody['requests'][-1]
|
|
|
|
def updateFormInfoRequest(key, value, ubody):
|
|
request = findFormRequest('updateFormInfo',
|
|
{'updateFormInfo': {'info': {}, 'updateMask': []}},
|
|
ubody)
|
|
request['updateFormInfo']['info'][key] = value
|
|
request['updateFormInfo']['updateMask'].append(key)
|
|
|
|
def updateFormSettingsRequest(key, value, ubody):
|
|
request = findFormRequest('updateSettings',
|
|
{'updateSettings': {'settings': {'quizSettings': {}}, 'updateMask': []}},
|
|
ubody)
|
|
request['updateSettings']['settings']['quizSettings'][key] = value
|
|
request['updateSettings']['updateMask'].append(f'quizSettings.{key}')
|
|
|
|
def updateFormRequestUpdateMasks(ubody):
|
|
for request in ubody['requests']:
|
|
for k, v in request.items():
|
|
if k in {'updateFormInfo', 'updateSettings'}:
|
|
v['updateMask'] = ','.join(v['updateMask'])
|
|
break
|
|
|
|
# gam <UserTypeEntity> create form
|
|
# title <String> [description <String>] [isquiz [<Boolean>]] [<JSONData>]
|
|
# [drivefilename <DriveFileName>] [<DriveFileParentAttribute>]
|
|
# [(csv [todrive <ToDriveAttribute>*]) | returnidonly]
|
|
def createForm(users):
|
|
csvPF = None
|
|
returnIdOnly = False
|
|
title = ''
|
|
body = {'mimeType': MIMETYPE_GA_FORM}
|
|
ubody = {'includeFormInResponse': True, 'requests': []}
|
|
parentParms = initDriveFileAttributes()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'title':
|
|
title = getString(Cmd.OB_STRING)
|
|
updateFormInfoRequest(myarg, title, ubody)
|
|
elif myarg == 'description':
|
|
updateFormInfoRequest(myarg, getString(Cmd.OB_STRING, minLen=0), ubody)
|
|
elif myarg == 'isquiz':
|
|
updateFormSettingsRequest('isQuiz', getBoolean(), ubody)
|
|
elif myarg == 'json':
|
|
jsonData = getJSON([])
|
|
ubody['requests'].extend(jsonData.get('requests', []))
|
|
elif myarg == 'drivefilename':
|
|
body['name'] = getString(Cmd.OB_DRIVE_FILE_NAME)
|
|
elif getDriveFileParentAttribute(myarg, parentParms):
|
|
pass
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
elif myarg == 'csv':
|
|
csvPF = CSVPrintFile(['User', 'formId', 'name', 'title', 'responderUri'])
|
|
elif csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
unknownArgumentExit()
|
|
if not title:
|
|
missingArgumentExit('title')
|
|
updateFormRequestUpdateMasks(ubody)
|
|
if 'name' not in body:
|
|
body['name'] = title
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, body, parentParms):
|
|
continue
|
|
_, gform = buildGAPIServiceObject(API.FORMS, user, i, count)
|
|
if not gform:
|
|
continue
|
|
try:
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
|
|
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
|
|
GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
|
|
body=body, fields='id,name', supportsAllDrives=True)
|
|
formId = result['id']
|
|
form = callGAPI(gform.forms(), 'batchUpdate',
|
|
throwReasons=[GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
formId=formId, body=ubody)
|
|
if returnIdOnly:
|
|
writeStdout(f'{formId}\n')
|
|
elif not csvPF:
|
|
entityActionPerformed([Ent.USER, user, Ent.FORM, title,
|
|
Ent.DRIVE_FILE, f"{result['name']}({formId})"])
|
|
else:
|
|
csvPF.WriteRow({'User': user, 'formId': formId,
|
|
'name': form['form']['info']['documentTitle'],
|
|
'title': form['form']['info']['title'],
|
|
'responderUri': form['form']['responderUri']})
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.FORM, title, Ent.DRIVE_FILE, body['name']], str(e), i, count)
|
|
except GAPI.permissionDenied:
|
|
SvcAcctAPIDisabledExit()
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Forms')
|
|
|
|
# gam <UserTypeEntity> update form <DriveFileEntity>
|
|
# [title <String>] [description <String>] [isquiz [Boolean>]] [<JSONData>]
|
|
def updateForm(users):
|
|
ubody = {'includeFormInResponse': False, 'requests': []}
|
|
fileIdEntity = getDriveFileEntity()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'title':
|
|
updateFormInfoRequest(myarg, getString(Cmd.OB_STRING), ubody)
|
|
elif myarg == 'description':
|
|
updateFormInfoRequest(myarg, getString(Cmd.OB_STRING, minLen=0), ubody)
|
|
elif myarg == 'isquiz':
|
|
updateFormSettingsRequest('isQuiz', getBoolean(), ubody)
|
|
elif myarg == 'json':
|
|
jsonData = getJSON([])
|
|
ubody['requests'].extend(jsonData.get('requests', []))
|
|
else:
|
|
unknownArgumentExit()
|
|
updateFormRequestUpdateMasks(ubody)
|
|
if not ubody['requests']:
|
|
return
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.FORM)
|
|
if jcount == 0:
|
|
continue
|
|
_, gform = buildGAPIServiceObject(API.FORMS, user, i, count)
|
|
if not gform:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for formId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
callGAPI(gform.forms(), 'batchUpdate',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
formId=formId, body=ubody)
|
|
entityActionPerformed([Ent.USER, user, Ent.FORM, formId], j, jcount)
|
|
except (GAPI.notFound, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.FORM, formId], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print forms <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam <UserTypeEntity> show forms <DriveFileEntity>
|
|
# [formatjson]
|
|
def printShowForms(users):
|
|
csvPF = CSVPrintFile(['User', 'formId', 'name', 'title', 'description'], 'sortall') if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
fileIdEntity = getDriveFileEntity()
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if csvPF and addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles(sorted(addCSVData.keys()))
|
|
csvPF.MoveJSONTitlesToEnd(['JSON'])
|
|
csvPF.SetSortAllTitles()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=[Ent.FORM, None][csvPF is not None or FJQC.formatJSON])
|
|
if jcount == 0:
|
|
continue
|
|
_, gform = buildGAPIServiceObject(API.FORMS, user, i, count)
|
|
if not gform:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for formId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
result = callGAPI(gform.forms(), 'get',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
|
|
formId=formId)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
printEntity([Ent.FORM, result['formId']], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, result)
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(result), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
info = result.pop('info')
|
|
baserow = {'User': user, 'formId': formId, 'name': info['documentTitle'],
|
|
'title': info.get('title', ''), 'description': info.get('description', '')}
|
|
if addCSVData:
|
|
baserow.update(addCSVData)
|
|
row = flattenJSON(result, flattened=baserow.copy())
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
result['info'] = info
|
|
baserow['JSON'] = json.dumps(cleanJSON(result), ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(baserow)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.FORM, formId], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Forms')
|
|
|
|
FORM_RESPONSE_TIME_OBJECTS = {'createTime', 'lastSubmittedTime'}
|
|
|
|
# gam <UserTypeEntity> print formresponses <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# [filtertime.* <Time>] [filter <String>]
|
|
# (addcsvdata <FieldName> <String>)*
|
|
# [countsonly|(formatjson [quotechar <Character>])]
|
|
# gam <UserTypeEntity> show formresponses <DriveFileEntity>
|
|
# [filtertime.* <Time>] [filter <String>]
|
|
# [countsonly|formatjson]
|
|
def printShowFormResponses(users):
|
|
csvPF = CSVPrintFile(['User', 'formId', 'responseId', 'createTime', 'lastSubmittedTime', 'respondentEmail', 'totalScore'],
|
|
'sortall', indexedTitles=['answers']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
frfilter = None
|
|
filterTimes = {}
|
|
fileIdEntity = getDriveFileEntity()
|
|
countsOnly = False
|
|
addCSVData = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg.startswith('filtertime'):
|
|
filterTimes[myarg] = getTimeOrDeltaFromNow()
|
|
elif myarg in {'filter', 'filters'}:
|
|
frfilter = getString(Cmd.OB_STRING)
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
elif csvPF and myarg == 'addcsvdata':
|
|
k = getString(Cmd.OB_STRING)
|
|
addCSVData[k] = getString(Cmd.OB_STRING, minLen=0)
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if filterTimes and filter is not None:
|
|
for filterTimeName, filterTimeValue in iter(filterTimes.items()):
|
|
frfilter = frfilter.replace(f'#{filterTimeName}#', filterTimeValue)
|
|
if csvPF:
|
|
if countsOnly:
|
|
csvPF.SetTitles(['User', 'formId', 'responses'])
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if FJQC.formatJSON:
|
|
csvPF.AddJSONTitles(sorted(addCSVData.keys()))
|
|
csvPF.MoveJSONTitlesToEnd(['JSON'])
|
|
csvPF.SetSortAllTitles()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, _, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity,
|
|
entityType=[Ent.FORM, None][csvPF is not None or FJQC.formatJSON])
|
|
if jcount == 0:
|
|
continue
|
|
_, gform = buildGAPIServiceObject(API.FORMS, user, i, count)
|
|
if not gform:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for formId in fileIdEntity['list']:
|
|
j += 1
|
|
try:
|
|
results = callGAPIpages(gform.forms().responses(), 'list', 'responses',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED],
|
|
filter=frfilter, formId=formId)
|
|
kcount = len(results)
|
|
if countsOnly:
|
|
if not csvPF:
|
|
printEntityKVList([Ent.FORM, formId], [Ent.Plural(Ent.FORM_RESPONSE), kcount], j, jcount)
|
|
else:
|
|
row = {'User': user, 'formId': formId, 'responses': kcount}
|
|
if addCSVData:
|
|
row.update(addCSVData)
|
|
csvPF.WriteRowTitles(row)
|
|
continue
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.FORM, formId], kcount, Ent.FORM_RESPONSE, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for response in results:
|
|
k += 1
|
|
if not FJQC.formatJSON:
|
|
printEntity([Ent.FORM_RESPONSE, response['responseId']], k, kcount)
|
|
Ind.Increment()
|
|
showJSON(None, response, timeObjects=FORM_RESPONSE_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
else:
|
|
printLine(json.dumps(cleanJSON(response, timeObjects=FORM_RESPONSE_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True))
|
|
Ind.Decrement()
|
|
else:
|
|
baserow = {'User': user, 'formId': formId}
|
|
if addCSVData:
|
|
baserow.update(addCSVData)
|
|
for response in results:
|
|
row = flattenJSON(response, flattened=baserow.copy())
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = baserow.copy()
|
|
row.update({'responseId': response['responseId'],
|
|
'createTime': response['createTime'],
|
|
'lastSubmittedTime': response['lastSubmittedTime'],
|
|
'respondentEmail': response.get('respondentEmail', ''),
|
|
'totalScore': response.get('totalScore', ''),
|
|
'JSON': json.dumps(cleanJSON(response, timeObjects=FORM_RESPONSE_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
csvPF.WriteRowNoFilter(row)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.FORM, formId], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Form Responses')
|
|
|
|
EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP = {
|
|
'ARCHIVE': 'archive',
|
|
'DELETE': 'trash',
|
|
'KEEP': 'leaveInInBox',
|
|
'MARK_READ': 'markRead',
|
|
}
|
|
|
|
def _showForward(user, i, count, result):
|
|
if 'enabled' in result:
|
|
enabled = result['enabled']
|
|
kvList = [Ent.Singular(Ent.FORWARD_ENABLED), enabled]
|
|
if enabled:
|
|
kvList += [Ent.Singular(Ent.FORWARDING_ADDRESS), result['emailAddress']]
|
|
kvList += [Ent.Singular(Ent.ACTION), result['disposition']]
|
|
else:
|
|
enabled = result['enable'] == 'true'
|
|
kvList = [Ent.Singular(Ent.FORWARD_ENABLED), enabled]
|
|
if enabled:
|
|
kvList += [Ent.Singular(Ent.FORWARDING_ADDRESS), result['forwardTo']]
|
|
kvList += [Ent.Singular(Ent.ACTION), EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP[result['action']]]
|
|
printEntityKVList([Ent.USER, user], kvList, i, count)
|
|
|
|
EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP = {
|
|
'archive': 'archive',
|
|
'delete': 'trash',
|
|
'keep': 'leaveInInbox',
|
|
'leaveininbox': 'leaveInInbox',
|
|
'markread': 'markRead',
|
|
'trash': 'trash',
|
|
}
|
|
|
|
# gam <UserTypeEntity> forward <FalseValues>
|
|
# gam <UserTypeEntity> forward <TrueValues> keep|leaveininbox|archive|delete|trash|markread <EmailAddress>
|
|
def setForward(users):
|
|
if checkArgumentPresent([Cmd.ARG_MESSAGE, Cmd.ARG_MESSAGES]):
|
|
Act.Set(Act.FORWARD)
|
|
forwardMessagesThreads(users, Ent.MESSAGE)
|
|
return
|
|
if checkArgumentPresent([Cmd.ARG_THREAD, Cmd.ARG_THREADS]):
|
|
Act.Set(Act.FORWARD)
|
|
forwardMessagesThreads(users, Ent.THREAD)
|
|
return
|
|
enable = getBoolean(None)
|
|
body = {'enabled': enable}
|
|
if enable:
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP:
|
|
body['disposition'] = EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP[myarg]
|
|
elif myarg == 'confirm':
|
|
pass
|
|
elif myarg.find('@') != -1 or (not Cmd.ArgumentsRemaining() and 'emailAddress' not in body):
|
|
body['emailAddress'] = normalizeEmailAddressOrUID(Cmd.Previous())
|
|
else:
|
|
unknownArgumentExit()
|
|
if not body.get('disposition'):
|
|
missingChoiceExit(EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP)
|
|
if not body.get('emailAddress'):
|
|
missingArgumentExit(Cmd.OB_EMAIL_ADDRESS)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
try:
|
|
result = callGAPI(gmail.users().settings(), 'updateAutoForwarding',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
|
|
userId='me', body=body)
|
|
_showForward(user, i, count, result)
|
|
except (GAPI.invalid, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
|
|
if enable:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.FORWARDING_ADDRESS, body['emailAddress']], str(e), i, count)
|
|
else:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> print forward [enabledonly] [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show forward
|
|
def printShowForward(users):
|
|
def _printForward(user, result, showDisabled):
|
|
if 'enabled' in result:
|
|
enabled = result['enabled']
|
|
if not enabled and not showDisabled:
|
|
return
|
|
row = {'User': user, 'forwardEnabled': enabled}
|
|
if enabled:
|
|
row['forwardTo'] = result['emailAddress']
|
|
row['disposition'] = result['disposition']
|
|
else:
|
|
enabled = result['enable'] == 'true'
|
|
if not enabled and not showDisabled:
|
|
return
|
|
row = {'User': user, 'forwardEnabled': enabled}
|
|
if enabled:
|
|
row['forwardTo'] = result['forwardTo']
|
|
row['disposition'] = EMAILSETTINGS_OLD_NEW_OLD_FORWARD_ACTION_MAP[result['action']]
|
|
csvPF.WriteRow(row)
|
|
|
|
csvPF = CSVPrintFile(['User', 'forwardEnabled', 'forwardTo', 'disposition']) if Act.csvFormat() else None
|
|
showDisabled = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif csvPF and myarg == 'enabledonly':
|
|
showDisabled = False
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.FORWARD_ENABLED, user, i, count)
|
|
try:
|
|
result = callGAPI(gmail.users().settings(), 'getAutoForwarding',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
|
|
userId='me')
|
|
if not csvPF:
|
|
_showForward(user, i, count, result)
|
|
else:
|
|
_printForward(user, result, showDisabled)
|
|
except GAPI.failedPrecondition as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Forward')
|
|
|
|
# Process ForwardingAddresses functions
|
|
def _showForwardingAddress(j, jcount, result):
|
|
printEntityKVList([Ent.FORWARDING_ADDRESS, result['forwardingEmail']], ['Verification Status', result['verificationStatus']], j, jcount)
|
|
|
|
def _processForwardingAddress(user, i, count, emailAddress, j, jcount, gmail, function, **kwargs):
|
|
userDefined = True
|
|
try:
|
|
result = callGAPI(gmail.users().settings().forwardingAddresses(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS, GAPI.DUPLICATE,
|
|
GAPI.INVALID_ARGUMENT, GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
|
|
userId='me', **kwargs)
|
|
if function == 'get':
|
|
_showForwardingAddress(j, count, result)
|
|
else:
|
|
entityActionPerformed([Ent.USER, user, Ent.FORWARDING_ADDRESS, emailAddress], j, jcount)
|
|
except (GAPI.notFound, GAPI.alreadyExists, GAPI.duplicate, GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.FORWARDING_ADDRESS, emailAddress], str(e), j, jcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
userDefined = False
|
|
return userDefined
|
|
|
|
# gam <UserTypeEntity> create forwardingaddresses <EmailAddressEntity>
|
|
def createForwardingAddresses(users):
|
|
emailAddressEntity = getUserObjectEntity(Cmd.OB_EMAIL_ADDRESS_ENTITY, Ent.FORWARDING_ADDRESS)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, emailAddresses, jcount = _validateUserGetObjectList(user, i, count, emailAddressEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for emailAddress in emailAddresses:
|
|
j += 1
|
|
emailAddress = normalizeEmailAddressOrUID(emailAddress, noUid=True)
|
|
body = {'forwardingEmail': emailAddress}
|
|
if not _processForwardingAddress(user, i, count, emailAddress, j, jcount, gmail, 'create', body=body, fields=''):
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def _deleteInfoForwardingAddreses(users, function):
|
|
emailAddressEntity = getUserObjectEntity(Cmd.OB_EMAIL_ADDRESS_ENTITY, Ent.FORWARDING_ADDRESS)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, emailAddresses, jcount = _validateUserGetObjectList(user, i, count, emailAddressEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for emailAddress in emailAddresses:
|
|
j += 1
|
|
emailAddress = normalizeEmailAddressOrUID(emailAddress, noUid=True)
|
|
if not _processForwardingAddress(user, i, count, emailAddress, j, jcount, gmail, function, forwardingEmail=emailAddress):
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> delete forwardingaddresses <EmailAddressEntity>
|
|
def deleteForwardingAddresses(users):
|
|
_deleteInfoForwardingAddreses(users, 'delete')
|
|
|
|
# gam <UserTypeEntity> info forwardingaddresses <EmailAddressEntity>
|
|
def infoForwardingAddresses(users):
|
|
_deleteInfoForwardingAddreses(users, 'get')
|
|
|
|
# gam <UserTypeEntity> print forwardingaddresses [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show forwardingaddresses
|
|
def printShowForwardingAddresses(users):
|
|
csvPF = CSVPrintFile(['User', 'forwardingEmail', 'verificationStatus']) if Act.csvFormat() else None
|
|
getTodriveOnly(csvPF)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.FORWARDING_ADDRESS, user, i, count)
|
|
try:
|
|
results = callGAPIitems(gmail.users().settings().forwardingAddresses(), 'list', 'forwardingAddresses',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
|
|
userId='me')
|
|
jcount = len(results)
|
|
if not csvPF:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.FORWARDING_ADDRESS, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for forward in results:
|
|
j += 1
|
|
_showForwardingAddress(j, jcount, forward)
|
|
Ind.Decrement()
|
|
else:
|
|
if results:
|
|
for forward in results:
|
|
csvPF.WriteRow({'User': user, 'forwardingEmail': forward['forwardingEmail'], 'verificationStatus': forward['verificationStatus']})
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'User': user})
|
|
except GAPI.failedPrecondition as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Forwarding Addresses')
|
|
|
|
def _showImap(user, i, count, result):
|
|
enabled = result['enabled']
|
|
kvList = [Ent.Singular(Ent.IMAP_ENABLED), enabled]
|
|
for item in result:
|
|
if item != 'enabled':
|
|
kvList += [item, result[item]]
|
|
printEntityKVList([Ent.USER, user], kvList, i, count)
|
|
#
|
|
EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICE_MAP = {
|
|
'archive': 'archive',
|
|
'deleteforever': 'deleteForever',
|
|
'trash': 'trash',
|
|
}
|
|
|
|
EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES = ['0', '1000', '2000', '5000', '10000']
|
|
|
|
def _imapDefaults(enable):
|
|
return {'enabled': enable, 'autoExpunge': True, 'expungeBehavior': 'archive', 'maxFolderSize': 0}
|
|
|
|
def _setImap(user, body, i, count):
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if gmail:
|
|
try:
|
|
result = callGAPI(gmail.users().settings(), 'updateImap',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
|
|
userId='me', body=body)
|
|
_showImap(user, i, count, result)
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> imap|imap4 <Boolean> [noautoexpunge] [expungebehavior archive|deleteforever|trash] [maxfoldersize 0|1000|2000|5000|10000]
|
|
def setImap(users):
|
|
body = _imapDefaults(getBoolean(None))
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'noautoexpunge':
|
|
body['autoExpunge'] = False
|
|
elif myarg == 'expungebehavior':
|
|
body['expungeBehavior'] = getChoice(EMAILSETTINGS_IMAP_EXPUNGE_BEHAVIOR_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'maxfoldersize':
|
|
body['maxFolderSize'] = int(getChoice(EMAILSETTINGS_IMAP_MAX_FOLDER_SIZE_CHOICES))
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
_setImap(user, body, i, count)
|
|
|
|
# gam <UserTypeEntity> print imap|imap4 [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show imap|imap4
|
|
def printShowImap(users):
|
|
csvPF = CSVPrintFile(['User', 'enabled']) if Act.csvFormat() else None
|
|
getTodriveOnly(csvPF)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
try:
|
|
result = callGAPI(gmail.users().settings(), 'getImap',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
if not csvPF:
|
|
_showImap(user, i, count, result)
|
|
else:
|
|
csvPF.WriteRowTitles(flattenJSON(result, flattened={'User': user}))
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('IMAP')
|
|
|
|
def _showPop(user, i, count, result):
|
|
enabled = result['accessWindow'] != 'disabled'
|
|
kvList = [Ent.Singular(Ent.POP_ENABLED), enabled]
|
|
if enabled:
|
|
kvList += ['For', result['accessWindow'], Ent.Singular(Ent.ACTION), result['disposition']]
|
|
printEntityKVList([Ent.USER, user], kvList, i, count)
|
|
#
|
|
EMAILSETTINGS_POP_ENABLE_FOR_CHOICE_MAP = {
|
|
'allmail': 'allMail',
|
|
'fromnowon': 'fromNowOn',
|
|
'mailfromnowon': 'fromNowOn',
|
|
'newmail': 'fromNowOn',
|
|
}
|
|
|
|
def _popDefaults(enable):
|
|
return {'accessWindow': ['disabled', 'allMail'][enable], 'disposition': 'leaveInInbox'}
|
|
|
|
def _setPop(user, body, i, count):
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if gmail:
|
|
try:
|
|
result = callGAPI(gmail.users().settings(), 'updatePop',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
|
|
userId='me', body=body)
|
|
_showPop(user, i, count, result)
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> pop|pop3 <Boolean> [for allmail|newmail|mailfromnowon|fromnowown] [action keep|leaveininbox|archive|delete|trash|markread]
|
|
def setPop(users):
|
|
body = _popDefaults(getBoolean(None))
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'for':
|
|
body['accessWindow'] = getChoice(EMAILSETTINGS_POP_ENABLE_FOR_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'action':
|
|
body['disposition'] = getChoice(EMAILSETTINGS_FORWARD_POP_ACTION_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'confirm':
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
_setPop(user, body, i, count)
|
|
|
|
# gam <UserTypeEntity> print pop|pop3 [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show pop|pop3
|
|
def printShowPop(users):
|
|
csvPF = CSVPrintFile(['User', 'enabled']) if Act.csvFormat() else None
|
|
getTodriveOnly(csvPF)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
try:
|
|
result = callGAPI(gmail.users().settings(), 'getPop',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
if not csvPF:
|
|
_showPop(user, i, count, result)
|
|
else:
|
|
csvPF.WriteRowTitles(flattenJSON(result, flattened={'User': user, 'enabled': result['accessWindow'] != 'disabled'}))
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('POP')
|
|
|
|
# gam <UserTypeEntity> language <Language>
|
|
def setLanguage(users):
|
|
language = getLanguageCode(LANGUAGE_CODES_MAP)
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
try:
|
|
result = callGAPI(gmail.users().settings(), 'updateLanguage',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
|
|
userId='me', body={'displayLanguage': language})
|
|
entityActionPerformed([Ent.USER, user, Ent.LANGUAGE, result['displayLanguage']], i, count)
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> print language [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show language
|
|
def printShowLanguage(users):
|
|
csvPF = CSVPrintFile(['User', 'displayLanguage']) if Act.csvFormat() else None
|
|
getTodriveOnly(csvPF)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
try:
|
|
result = callGAPI(gmail.users().settings(), 'getLanguage',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
if not csvPF:
|
|
printEntity([Ent.USER, user, Ent.LANGUAGE, result['displayLanguage']], i, count)
|
|
else:
|
|
csvPF.WriteRowTitles(flattenJSON(result, flattened={'User': user}))
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Language')
|
|
|
|
SIG_REPLY_HTML = 0
|
|
SIG_REPLY_COMPACT = 1
|
|
SIG_REPLY_FORMAT = 2
|
|
SIG_REPLY_OPTIONS_MAP = {'html': SIG_REPLY_HTML, 'compact': SIG_REPLY_COMPACT, 'format': SIG_REPLY_FORMAT}
|
|
SMTPMSA_DISPLAY_FIELDS = ['host', 'port', 'securityMode']
|
|
|
|
def _showSendAs(result, j, jcount, sigReplyFormat, verifyOnly=False):
|
|
if result['displayName']:
|
|
printEntity([Ent.SENDAS_ADDRESS, f'{result["displayName"]} <{result["sendAsEmail"]}>'], j, jcount)
|
|
else:
|
|
printEntity([Ent.SENDAS_ADDRESS, f'<{result["sendAsEmail"]}>'], j, jcount)
|
|
Ind.Increment()
|
|
if result.get('replyToAddress'):
|
|
printKeyValueList(['ReplyTo', result['replyToAddress']])
|
|
printKeyValueList(['IsPrimary', result.get('isPrimary', False)])
|
|
printKeyValueList(['Default', result.get('isDefault', False)])
|
|
if not result.get('isPrimary', False):
|
|
printKeyValueList(['TreatAsAlias', result.get('treatAsAlias', False)])
|
|
if 'smtpMsa' in result:
|
|
for field in SMTPMSA_DISPLAY_FIELDS:
|
|
if field in result['smtpMsa']:
|
|
printKeyValueList([f'smtpMsa.{field}', result['smtpMsa'][field]])
|
|
if 'verificationStatus' in result:
|
|
printKeyValueList(['Verification Status', result['verificationStatus']])
|
|
signature = result.get('signature')
|
|
if verifyOnly:
|
|
printKeyValueList(['Signature', bool(signature)])
|
|
else:
|
|
if not signature:
|
|
signature = 'None'
|
|
if sigReplyFormat == SIG_REPLY_HTML:
|
|
printKeyValueList(['Signature', None])
|
|
Ind.Increment()
|
|
printKeyValueList([Ind.MultiLineText(signature)])
|
|
Ind.Decrement()
|
|
elif sigReplyFormat == SIG_REPLY_FORMAT:
|
|
printKeyValueList(['Signature', None])
|
|
Ind.Increment()
|
|
printKeyValueList([Ind.MultiLineText(dehtml(signature))])
|
|
Ind.Decrement()
|
|
else: # SIG_REPLY_COMPACT
|
|
printKeyValueList(['Signature', escapeCRsNLs(signature)])
|
|
Ind.Decrement()
|
|
|
|
def _processSignature(tagReplacements, signature, html):
|
|
if signature:
|
|
signature = signature.replace('\r', '').replace('\\n', '<br/>')
|
|
if tagReplacements['tags']:
|
|
signature = _processTagReplacements(tagReplacements, signature)
|
|
if not html:
|
|
signature = signature.replace('\n', '<br/>')
|
|
return signature
|
|
|
|
# Process SendAs functions
|
|
def _processSendAs(user, i, count, entityType, emailAddress, j, jcount, gmail, function,
|
|
sigReplyFormat, verifyOnly=False, **kwargs):
|
|
userDefined = True
|
|
try:
|
|
result = callGAPI(gmail.users().settings().sendAs(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS, GAPI.DUPLICATE,
|
|
GAPI.CANNOT_DELETE_PRIMARY_SENDAS, GAPI.INVALID_ARGUMENT,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED,
|
|
GAPI.INSUFFICIENT_PERMISSIONS],
|
|
userId='me', **kwargs)
|
|
if function == 'get':
|
|
_showSendAs(result, j, jcount, sigReplyFormat, verifyOnly)
|
|
else:
|
|
entityActionPerformed([Ent.USER, user, entityType, emailAddress], j, jcount)
|
|
except (GAPI.notFound, GAPI.alreadyExists, GAPI.duplicate,
|
|
GAPI.cannotDeletePrimarySendAs, GAPI.invalidArgument,
|
|
GAPI.failedPrecondition, GAPI.permissionDenied, GAPI.insufficientPermissions) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, emailAddress], str(e), j, jcount)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
userDefined = False
|
|
return userDefined
|
|
|
|
def getSendAsAttributes(myarg, body, tagReplacements):
|
|
if _getTagReplacement(myarg, tagReplacements, True):
|
|
pass
|
|
elif myarg == 'name':
|
|
body['displayName'] = getString(Cmd.OB_NAME, minLen=0)
|
|
elif myarg == 'replyto':
|
|
body['replyToAddress'] = getString(Cmd.OB_EMAIL_ADDRESS, minLen=0)
|
|
if len(body['replyToAddress']) > 0:
|
|
body['replyToAddress'] = normalizeEmailAddressOrUID(body['replyToAddress'], noUid=True, noLower=True)
|
|
elif myarg == 'default':
|
|
body['isDefault'] = True
|
|
elif myarg == 'treatasalias':
|
|
body['treatAsAlias'] = getBoolean()
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
SMTPMSA_PORTS = ['25', '465', '587']
|
|
SMTPMSA_SECURITY_MODES = ['none', 'ssl', 'starttls']
|
|
SMTPMSA_REQUIRED_FIELDS = ['host', 'port', 'username', 'password']
|
|
|
|
# gam <UserTypeEntity> [create] sendas <EmailAddress> [name] <String>
|
|
# [<SendAsContent>
|
|
# (replace <Tag> <UserReplacement>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
|
|
# [html [<Boolean>]] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
|
|
# [smtpmsa.host <SMTPHostName> smtpmsa.port 25|465|587
|
|
# smtpmsa.username <UserName> smtpmsa.password <Password>
|
|
# [smtpmsa.securitymode none|ssl|starttls]]
|
|
# gam <UserTypeEntity> update sendas <EmailAddress> [name <String>]
|
|
# [<SendAsContent>
|
|
# (replace <Tag> <UserReplacement>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
|
|
# [html [<Boolean>]] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
|
|
def createUpdateSendAs(users):
|
|
updateCmd = Act.Get() == Act.UPDATE
|
|
emailAddress = normalizeEmailAddressOrUID(getString(Cmd.OB_EMAIL_ADDRESS), noUid=True, noLower=True)
|
|
if not updateCmd:
|
|
body = {'sendAsEmail': emailAddress}
|
|
checkArgumentPresent(['name'])
|
|
body['displayName'] = getString(Cmd.OB_NAME)
|
|
else:
|
|
body = {}
|
|
signature = None
|
|
smtpMsa = {}
|
|
tagReplacements = _initTagReplacements()
|
|
html = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in SORF_SIG_FILE_ARGUMENTS:
|
|
signature, _, html = getStringOrFile(myarg)
|
|
elif myarg == 'html':
|
|
html = getBoolean()
|
|
elif not updateCmd and myarg.startswith('smtpmsa.'):
|
|
if myarg == 'smtpmsa.host':
|
|
smtpMsa['host'] = getString(Cmd.OB_SMTP_HOST_NAME)
|
|
elif myarg == 'smtpmsa.port':
|
|
smtpMsa['port'] = int(getChoice(SMTPMSA_PORTS))
|
|
elif myarg == 'smtpmsa.username':
|
|
smtpMsa['username'] = getString(Cmd.OB_USER_NAME)
|
|
elif myarg == 'smtpmsa.password':
|
|
smtpMsa['password'] = getString(Cmd.OB_PASSWORD)
|
|
elif myarg == 'smtpmsa.securitymode':
|
|
smtpMsa['securityMode'] = getChoice(SMTPMSA_SECURITY_MODES)
|
|
else:
|
|
unknownArgumentExit()
|
|
else:
|
|
getSendAsAttributes(myarg, body, tagReplacements)
|
|
if signature is not None and not tagReplacements['subs']:
|
|
body['signature'] = _processSignature(tagReplacements, signature, html)
|
|
if smtpMsa:
|
|
for field in SMTPMSA_REQUIRED_FIELDS:
|
|
if field not in smtpMsa:
|
|
missingArgumentExit(f'smtpmsa.{field}')
|
|
body['smtpMsa'] = smtpMsa
|
|
kwargs = {'body': body, 'fields': ''}
|
|
if updateCmd:
|
|
kwargs['sendAsEmail'] = emailAddress
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if signature is not None and tagReplacements['subs']:
|
|
_getTagReplacementFieldValues(user, i, count, tagReplacements)
|
|
kwargs['body']['signature'] = _processSignature(tagReplacements, signature, html)
|
|
_processSendAs(user, i, count, Ent.SENDAS_ADDRESS, emailAddress, i, count, gmail, ['create', 'patch'][updateCmd], False, **kwargs)
|
|
|
|
# gam <UserTypeEntity> delete sendas <EmailAddressEntity>
|
|
# gam <UserTypeEntity> info sendas <EmailAddressEntity> [compact|format|html]
|
|
def deleteInfoSendAs(users):
|
|
function = 'delete' if Act.Get() == Act.DELETE else 'get'
|
|
emailAddressEntity = getUserObjectEntity(Cmd.OB_EMAIL_ADDRESS_ENTITY, Ent.SENDAS_ADDRESS)
|
|
sigReplyFormat = SIG_REPLY_HTML
|
|
if function == 'get':
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in SIG_REPLY_OPTIONS_MAP:
|
|
sigReplyFormat = SIG_REPLY_OPTIONS_MAP[myarg]
|
|
else:
|
|
unknownArgumentExit()
|
|
else:
|
|
checkForExtraneousArguments()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail, emailAddresses, jcount = _validateUserGetObjectList(user, i, count, emailAddressEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for emailAddress in emailAddresses:
|
|
j += 1
|
|
emailAddress = normalizeEmailAddressOrUID(emailAddress, noUid=True)
|
|
if not _processSendAs(user, i, count, Ent.SENDAS_ADDRESS, emailAddress, j, jcount, gmail, function, sigReplyFormat, sendAsEmail=emailAddress):
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> print sendas [compact]
|
|
# [primary|default] [verifyonly] [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show sendas [compact|format|html]
|
|
# [primary|default] [verifyonly]
|
|
def printShowSendAs(users):
|
|
csvPF = CSVPrintFile(['User', 'displayName', 'sendAsEmail', 'replyToAddress',
|
|
'isPrimary', 'isDefault', 'treatAsAlias', 'verificationStatus'],
|
|
'sortall') if Act.csvFormat() else None
|
|
sigReplyFormat = SIG_REPLY_HTML
|
|
selection=None
|
|
verifyOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'primary':
|
|
selection = 'isPrimary'
|
|
elif myarg == 'default':
|
|
selection = 'isDefault'
|
|
elif myarg == 'verifyonly':
|
|
verifyOnly = True
|
|
elif (not csvPF and myarg in SIG_REPLY_OPTIONS_MAP) or (csvPF and myarg == 'compact'):
|
|
sigReplyFormat = SIG_REPLY_OPTIONS_MAP[myarg]
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.SENDAS_ADDRESS, user, i, count)
|
|
try:
|
|
results = callGAPIitems(gmail.users().settings().sendAs(), 'list', 'sendAs',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
jcount = len(results)
|
|
if not csvPF:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount if selection is None else 1, Ent.SENDAS_ADDRESS, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for sendas in results:
|
|
j += 1
|
|
if (selection is None) or (sendas.get(selection, False)):
|
|
_showSendAs(sendas, j, jcount, sigReplyFormat, verifyOnly)
|
|
Ind.Decrement()
|
|
else:
|
|
if results:
|
|
for sendas in results:
|
|
if (selection is None) or (sendas.get(selection, False)):
|
|
row = {'User': user, 'isPrimary': False}
|
|
for item in sendas:
|
|
if item != 'smtpMsa':
|
|
if item == 'signature':
|
|
if verifyOnly:
|
|
row[item] = bool(sendas[item])
|
|
elif sigReplyFormat != SIG_REPLY_COMPACT:
|
|
row[item] = sendas[item]
|
|
else:
|
|
row[item] = sendas[item].replace('\r', '').replace('\n', '')
|
|
else:
|
|
row[item] = sendas[item]
|
|
else:
|
|
for field in SMTPMSA_DISPLAY_FIELDS:
|
|
if field in sendas[item]:
|
|
row[f'smtpMsa{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{field}'] = sendas[item][field]
|
|
csvPF.WriteRowTitles(row)
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'User': user})
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('SendAs')
|
|
|
|
# gam <UserTypeEntity> print signature [compact]
|
|
# [primary] [verifyonly] [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show signature|sig [compact|format|html]
|
|
# [primary] [verifyonly]
|
|
def printShowSignature(users):
|
|
csvPF = CSVPrintFile(['User', 'displayName', 'sendAsEmail', 'replyToAddress',
|
|
'isPrimary', 'isDefault', 'treatAsAlias', 'verificationStatus'],
|
|
'sortall') if Act.csvFormat() else None
|
|
sigReplyFormat = SIG_REPLY_HTML
|
|
selection = None
|
|
verifyOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'primary':
|
|
selection = 'isPrimary'
|
|
elif myarg == 'default':
|
|
selection = 'isDefault'
|
|
elif myarg == 'verifyonly':
|
|
verifyOnly = True
|
|
elif (not csvPF and myarg in SIG_REPLY_OPTIONS_MAP) or (csvPF and myarg == 'compact'):
|
|
sigReplyFormat = SIG_REPLY_OPTIONS_MAP[myarg]
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.SIGNATURE, user, i, count)
|
|
try:
|
|
if selection is None:
|
|
sendas = callGAPI(gmail.users().settings().sendAs(), 'get',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me', sendAsEmail=user)
|
|
else:
|
|
results = callGAPIitems(gmail.users().settings().sendAs(), 'list', 'sendAs',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
for sendas in results:
|
|
if sendas.get(selection, False):
|
|
break
|
|
if not csvPF:
|
|
_showSendAs(sendas, 0, 0, sigReplyFormat, verifyOnly)
|
|
else:
|
|
row = {'User': user, 'isPrimary': False}
|
|
for item in sendas:
|
|
if item != 'smtpMsa':
|
|
if item == 'signature':
|
|
if verifyOnly:
|
|
row[item] = bool(sendas[item])
|
|
elif sigReplyFormat != SIG_REPLY_COMPACT:
|
|
row[item] = sendas[item]
|
|
else:
|
|
row[item] = sendas[item].replace('\r', '').replace('\n', '')
|
|
else:
|
|
row[item] = sendas[item]
|
|
else:
|
|
for field in SMTPMSA_DISPLAY_FIELDS:
|
|
if field in sendas[item]:
|
|
row[f'smtpMsa{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{field}'] = sendas[item][field]
|
|
csvPF.WriteRowTitles(row)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Signature')
|
|
|
|
# gam <UserTypeEntity> create smime file <FileName> [password <Password>]
|
|
# [sendas|sendasemail <EmailAddress>] [default]
|
|
def createSmime(users):
|
|
sendAsEmailBase = None
|
|
setDefault = False
|
|
body = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'file':
|
|
smimefile = getString(Cmd.OB_FILE_NAME)
|
|
smimeData = readFile(smimefile, mode='rb')
|
|
body['pkcs12'] = base64.urlsafe_b64encode(smimeData).decode(UTF8)
|
|
elif myarg == 'password':
|
|
body['encryptedKeyPassword'] = getString(Cmd.OB_PASSWORD)
|
|
elif myarg == 'default':
|
|
setDefault = True
|
|
elif myarg in {'sendas', 'sendasemail'}:
|
|
sendAsEmailBase = getEmailAddress(noUid=True)
|
|
else:
|
|
unknownArgumentExit()
|
|
if 'pkcs12' not in body:
|
|
missingArgumentExit('file')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
sendAsEmail = sendAsEmailBase if sendAsEmailBase else user
|
|
try:
|
|
Act.Set(Act.CREATE)
|
|
result = callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'insert',
|
|
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
|
|
userId='me', sendAsEmail=sendAsEmail, body=body, fields='id,issuerCn')
|
|
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, result['id']],
|
|
Act.MODIFIER_FROM, f'{Ent.Singular(Ent.ISSUER_CN)}: {result["issuerCn"]}', i, count)
|
|
if setDefault:
|
|
Act.Set(Act.UPDATE)
|
|
smimeId = result['id']
|
|
callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'setDefault',
|
|
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
|
|
userId='me', sendAsEmail=sendAsEmail, id=smimeId)
|
|
entityActionPerformedMessage([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, smimeId], Msg.DEFAULT_SMIME, i, count)
|
|
except (GAPI.forbidden, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
def _getSmimeIds(gmail, user, i, count, sendAsEmail, function):
|
|
try:
|
|
result = callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'list',
|
|
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
|
|
userId='me', sendAsEmail=sendAsEmail, fields='smimeInfo(id)')
|
|
smimes = result.get('smimeInfo', [])
|
|
jcount = len(smimes)
|
|
if jcount == 0:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, None],
|
|
Msg.NO_ENTITIES_FOUND.format(Ent.Plural(Ent.SMIME_ID)), i, count)
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
elif jcount > 1:
|
|
entityActionNotPerformedWarning([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, None],
|
|
Msg.PLEASE_SELECT_ENTITY_TO_PROCESS.format(jcount, Ent.Plural(Ent.SMIME_ID), function, 'id <S/MIMEID>'),
|
|
i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for smime in smimes:
|
|
j += 1
|
|
printEntityKVList([Ent.SMIME_ID, smime['id'], Ent.ISSUER_CN, smime['issuerCn']], ['Default', smime.get('isDefault', False)], j, jcount)
|
|
Ind.Decrement()
|
|
else:
|
|
return smimes[0]['id']
|
|
except GAPI.forbidden as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
return None
|
|
|
|
# gam <UserTypeEntity> update smime default
|
|
# [id <SmimeID>] [sendas|sendasemail <EmailAddress>]
|
|
def updateSmime(users):
|
|
smimeIdBase = None
|
|
sendAsEmailBase = None
|
|
setDefault = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'id':
|
|
smimeIdBase = getString(Cmd.OB_SMIME_ID)
|
|
elif myarg in {'sendas', 'sendasemail'}:
|
|
sendAsEmailBase = getEmailAddress(noUid=True)
|
|
elif myarg == 'default':
|
|
setDefault = True
|
|
else:
|
|
unknownArgumentExit()
|
|
if not setDefault:
|
|
missingArgumentExit('default')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
sendAsEmail = sendAsEmailBase if sendAsEmailBase else user
|
|
if not smimeIdBase:
|
|
smimeId = _getSmimeIds(gmail, user, i, count, sendAsEmail, 'update')
|
|
if not smimeId:
|
|
continue
|
|
else:
|
|
smimeId = smimeIdBase
|
|
try:
|
|
callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'setDefault',
|
|
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
|
|
userId='me', sendAsEmail=sendAsEmail, id=smimeId)
|
|
entityActionPerformedMessage([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, smimeId], Msg.DEFAULT_SMIME, i, count)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SMIME_ID, smimeId], str(e), i, count)
|
|
except (GAPI.forbidden, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> delete smime
|
|
# [id <SmimeID>] [sendas|sendasemail <EmailAddress>]
|
|
def deleteSmime(users):
|
|
smimeIdBase = None
|
|
sendAsEmailBase = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'id':
|
|
smimeIdBase = getString(Cmd.OB_SMIME_ID)
|
|
elif myarg in {'sendas', 'sendasemail'}:
|
|
sendAsEmailBase = getEmailAddress(noUid=True)
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
sendAsEmail = sendAsEmailBase if sendAsEmailBase else user
|
|
if not smimeIdBase:
|
|
smimeId = _getSmimeIds(gmail, user, i, count, sendAsEmail, 'delete')
|
|
if not smimeId:
|
|
continue
|
|
else:
|
|
smimeId = smimeIdBase
|
|
try:
|
|
callGAPI(gmail.users().settings().sendAs().smimeInfo(), 'delete',
|
|
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
|
|
userId='me', sendAsEmail=sendAsEmail, id=smimeId)
|
|
entityActionPerformed([Ent.USER, user, Ent.SENDAS_ADDRESS, sendAsEmail, Ent.SMIME_ID, smimeId], i, count)
|
|
except GAPI.notFound as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SMIME_ID, smimeId], str(e), i, count)
|
|
except (GAPI.forbidden, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> print smimes [todrive <ToDriveAttribute>*]
|
|
# [primary|default|(sendas|sendasemail <EmailAddress>)]
|
|
# gam <UserTypeEntity> show smimes
|
|
# [primary|default|(sendas|sendasemail <EmailAddress>)]
|
|
def printShowSmimes(users):
|
|
csvPF = CSVPrintFile(['User', 'id', 'isDefault', 'issuerCn', 'expiration', 'encryptedKeyPassword', 'pem']) if Act.csvFormat() else None
|
|
selectField = 'sendAsEmail'
|
|
sendAsEmailBase = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'default', 'defaultonly'}:
|
|
selectField = 'isDefault'
|
|
elif myarg in {'primary', 'primaryonly'}:
|
|
selectField = 'isPrimary'
|
|
elif myarg in {'sendas', 'sendasemail'}:
|
|
sendAsEmailBase = getEmailAddress(noUid=True)
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
try:
|
|
if sendAsEmailBase:
|
|
sendAsEmails = [sendAsEmailBase]
|
|
else:
|
|
results = callGAPIitems(gmail.users().settings().sendAs(), 'list', 'sendAs',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me', fields='sendAs(isDefault,isPrimary,sendAsEmail)')
|
|
sendAsEmails = [sendAs['sendAsEmail'] for sendAs in results if sendAs.get(selectField, False)]
|
|
jcount = len(sendAsEmails)
|
|
if not csvPF:
|
|
entityPerformActionSubItemModifierNumItems([Ent.USER, user], Ent.SMIME_ID, Act.MODIFIER_FROM, jcount, Ent.SENDAS_ADDRESS, i, count)
|
|
else:
|
|
printGettingEntityItemForWhom(Ent.SENDAS_ADDRESS, user, i, count)
|
|
if sendAsEmails:
|
|
j = 0
|
|
for sendAsEmail in sendAsEmails:
|
|
j += 1
|
|
smimes = callGAPIitems(gmail.users().settings().sendAs().smimeInfo(), 'list', 'smimeInfo',
|
|
throwReasons=GAPI.GMAIL_SMIME_THROW_REASONS,
|
|
userId='me', sendAsEmail=sendAsEmail)
|
|
kcount = len(smimes)
|
|
if not csvPF:
|
|
Ind.Increment()
|
|
printEntity([Ent.SENDAS_ADDRESS, sendAsEmail], j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for smime in smimes:
|
|
k += 1
|
|
printEntity([Ent.SMIME_ID, smime['id']], k, kcount)
|
|
Ind.Increment()
|
|
printKeyValueList(['Default', smime.get('isDefault', False)])
|
|
printKeyValueList(['Issuer CN', smime['issuerCn']])
|
|
printKeyValueList(['Expiration', formatLocalTimestamp(smime['expiration'])])
|
|
printKeyValueList(['Password', smime.get('encryptedKeyPassword', '')])
|
|
printKeyValueList(['PEM', None])
|
|
Ind.Increment()
|
|
printKeyValueList([Ind.MultiLineText(smime['pem'])])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
elif smimes:
|
|
for smime in smimes:
|
|
smime['expiration'] = formatLocalTimestamp(smime['expiration'])
|
|
csvPF.WriteRowTitles(flattenJSON(smime, flattened={'User': user}))
|
|
elif GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'User': user})
|
|
elif csvPF and GC.Values[GC.CSV_OUTPUT_USERS_AUDIT]:
|
|
csvPF.WriteRowNoFilter({'User': user})
|
|
except (GAPI.forbidden, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('S/MIME')
|
|
|
|
def _showCSEItem(result, entityType, keyField, timeObjects, i, count, FJQC):
|
|
if FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(result, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True))
|
|
return
|
|
Ind.Increment()
|
|
printEntity([entityType, result[keyField]], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, result, timeObjects=timeObjects)
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
def _initCSEKeyPairSkipObjects():
|
|
return {'pem', 'kaclsData'}
|
|
|
|
def _resetCSEKeyPairSkipObjects(myarg, skipObjects):
|
|
if myarg == 'showpem':
|
|
skipObjects.discard('pem')
|
|
elif myarg == 'showkaclsdata':
|
|
skipObjects.discard('kaclsData')
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def _stripCSEKeyPairSkipObjects(result, skipObjects):
|
|
if 'pem' in skipObjects:
|
|
result.pop('pem', None)
|
|
if 'kaclsData' in skipObjects:
|
|
for privateKeyMetadata in result.get('privateKeyMetadata', []):
|
|
if 'kaclsKeyMetadata' in privateKeyMetadata:
|
|
privateKeyMetadata['kaclsKeyMetadata'].pop('kaclsData', None)
|
|
|
|
CSE_IDENTITY_TIME_OBJECTS = {}
|
|
CSE_KEYPAIR_TIME_OBJECTS = {'disableTime'}
|
|
|
|
def _printShowCSEItems(users, entityType, keyField, timeObjects):
|
|
csvPF = CSVPrintFile(['User', keyField]) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
skipObjects = _initCSEKeyPairSkipObjects() if entityType == Ent.CSE_KEYPAIR else set()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif entityType == Ent.CSE_KEYPAIR and _resetCSEKeyPairSkipObjects(myarg, skipObjects):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
printGettingAllEntityItemsForWhom(entityType, user, i, count)
|
|
kvList = [Ent.USER, user, entityType, None]
|
|
try:
|
|
if entityType == Ent.CSE_IDENTITY:
|
|
results = callGAPIpages(gmail.users().settings().cse().identities(), 'list', 'cseIdentities',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
|
|
userId='me', fields='nextPageToken,cseIdentities')
|
|
else:
|
|
results = callGAPIpages(gmail.users().settings().cse().keypairs(), 'list', 'cseKeyPairs',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED],
|
|
userId='me', fields='nextPageToken,cseKeyPairs')
|
|
|
|
except GAPI.permissionDenied as e:
|
|
entityActionFailedWarning(kvList, str(e), i, count)
|
|
continue
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
continue
|
|
jcount = len(results)
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, entityType, i, count)
|
|
j = 0
|
|
for result in results:
|
|
j += 1
|
|
if entityType == Ent.CSE_KEYPAIR:
|
|
_stripCSEKeyPairSkipObjects(result, skipObjects)
|
|
_showCSEItem(result, entityType, keyField, timeObjects, j, jcount, FJQC)
|
|
else:
|
|
for result in results:
|
|
row = flattenJSON(result, flattened={'User': user})
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
csvPF.WriteRowNoFilter({'User': user, keyField: result[keyField],
|
|
'JSON': json.dumps(cleanJSON(result, skipObjects=skipObjects, timeObjects=timeObjects),
|
|
ensure_ascii=False, sort_keys=True)})
|
|
if csvPF:
|
|
csvPF.writeCSVfile(Ent.Plural(entityType))
|
|
|
|
CSE_IDENTITY_ACTION_FUNCTION_MAP = {
|
|
Act.CREATE: 'create',
|
|
Act.UPDATE: 'patch',
|
|
Act.DELETE: 'delete',
|
|
Act.INFO: 'get',
|
|
}
|
|
|
|
# gam <UserTypeEntity> create cseidentity
|
|
# (primarykeypairid <KeyPairID>) | (signingkeypairid <KeyPairID> encryptionkeypairid <KeyPairID>)
|
|
# [kpemail <EmailAddress>]
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> update cseidentity
|
|
# (primarykeypairid <KeyPairID>) | (signingkeypairid <KeyPairID> encryptionkeypairid <KeyPairID>)
|
|
# [kpemail <EmailAddress>]
|
|
# [formatjson]
|
|
def createUpdateCSEIdentity(users):
|
|
function = CSE_IDENTITY_ACTION_FUNCTION_MAP[Act.Get()]
|
|
primaryKeyPairId = signingKeyPairId = encryptionKeyPairId = None
|
|
FJQC = FormatJSONQuoteChar()
|
|
kpEmail = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'primarykeypairid':
|
|
primaryKeyPairId = getString(Cmd.OB_CSE_KEYPAIR_ID)
|
|
elif myarg == 'signingkeypairid':
|
|
signingKeyPairId = getString(Cmd.OB_CSE_KEYPAIR_ID)
|
|
elif myarg == 'encryptionkeypairid':
|
|
encryptionKeyPairId = getString(Cmd.OB_CSE_KEYPAIR_ID)
|
|
elif myarg == 'kpemail':
|
|
kpEmail = getEmailAddress(noUid=True)
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if primaryKeyPairId:
|
|
if signingKeyPairId or encryptionKeyPairId:
|
|
usageErrorExit(Msg.ARE_MUTUALLY_EXCLUSIVE.format('primarykeypairid', 'signingkeypairid/encryptionkeypairid'))
|
|
identity = {'primaryKeyPairId': primaryKeyPairId, 'emailAddress': None}
|
|
keyPairId = primaryKeyPairId
|
|
elif signingKeyPairId or encryptionKeyPairId:
|
|
if not signingKeyPairId or not encryptionKeyPairId:
|
|
usageErrorExit(Msg.ARE_BOTH_REQUIRED.format('signingkeypairid', 'encryptionkeypairid'))
|
|
identity = {'signAndEncryptKeyPairs': {'signingKeyPairId': signingKeyPairId, 'encryptionKeyPairId': encryptionKeyPairId},
|
|
'emailAddress': None}
|
|
keyPairId = f'{signingKeyPairId}/{encryptionKeyPairId}'
|
|
else:
|
|
missingArgumentExit('primarykeypairid|(signingkeypairid & encryptionkeypairid)')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
identity['emailAddress'] = user if not kpEmail else kpEmail
|
|
kwargs = {'body': identity}
|
|
if function == 'patch':
|
|
kwargs['emailAddress'] = identity['emailAddress']
|
|
kvList = [Ent.USER, user, Ent.CSE_IDENTITY, identity['emailAddress'], Ent.CSE_KEYPAIR, keyPairId]
|
|
try:
|
|
result = callGAPI(gmail.users().settings().cse().identities(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT, GAPI.NOT_FOUND, GAPI.ALREADY_EXISTS],
|
|
userId='me', **kwargs)
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, i, count)
|
|
_showCSEItem(result, Ent.CSE_IDENTITY, 'emailAddress', CSE_IDENTITY_TIME_OBJECTS, i, count, FJQC)
|
|
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.notFound, GAPI.alreadyExists) as e:
|
|
entityActionFailedWarning(kvList, str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> delete cseidentity [kpemail <EmailAddress>]
|
|
# gam <UserTypeEntity> info cseidentity [kpemail <EmailAddress>]
|
|
# [formatjson]
|
|
def processCSEIdentity(users):
|
|
function = CSE_IDENTITY_ACTION_FUNCTION_MAP[Act.Get()]
|
|
FJQC = FormatJSONQuoteChar()
|
|
kpEmail = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'kpemail':
|
|
kpEmail = getEmailAddress(noUid=True)
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
kwargs = {'cseEmailAddress': user if not kpEmail else kpEmail}
|
|
kvList = [Ent.USER, user, Ent.CSE_IDENTITY, kwargs['cseEmailAddress']]
|
|
try:
|
|
result = callGAPI(gmail.users().settings().cse().identities(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.NOT_FOUND],
|
|
userId='me', **kwargs)
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, i, count)
|
|
if function != 'delete':
|
|
_showCSEItem(result, Ent.CSE_IDENTITY, 'emailAddress', CSE_IDENTITY_TIME_OBJECTS, i, count, FJQC)
|
|
except (GAPI.permissionDenied, GAPI.notFound) as e:
|
|
entityActionFailedWarning(kvList, str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> show cseidentities
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> print cseidentities [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowCSEIdentities(users):
|
|
_printShowCSEItems(users, Ent.CSE_IDENTITY, 'emailAddress', CSE_IDENTITY_TIME_OBJECTS)
|
|
|
|
# gam <UserTypeEntity> create csekeypair [incertdir <FilePath>] [inkeydir <FilePath>]
|
|
# [addidentity [<Boolean>]] [kpemail <EmailAddress>]
|
|
# [showpem] [showkaclsdata] [formatjson|returnidonly]
|
|
def createCSEKeyPair(users):
|
|
def _getFolderPath(myarg):
|
|
filepath = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
if not os.path.isdir(filepath):
|
|
entityDoesNotExistExit(Ent.DIRECTORY, f'{myarg} {filepath}')
|
|
return filepath
|
|
|
|
FJQC = FormatJSONQuoteChar()
|
|
incertdir = GC.Values[GC.GMAIL_CSE_INCERT_DIR]
|
|
inkeydir = GC.Values[GC.GMAIL_CSE_INKEY_DIR]
|
|
addIdentity = returnIdOnly = False
|
|
kpEmail = None
|
|
skipObjects = _initCSEKeyPairSkipObjects()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'incertdir':
|
|
incertdir = _getFolderPath(myarg)
|
|
elif myarg == 'inkeydir':
|
|
inkeydir = _getFolderPath(myarg)
|
|
elif myarg == 'addidentity':
|
|
addIdentity = getBoolean()
|
|
elif myarg == 'kpemail':
|
|
kpEmail = getEmailAddress(noUid=True)
|
|
elif myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
elif _resetCSEKeyPairSkipObjects(myarg, skipObjects):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not incertdir:
|
|
missingArgumentExit('incertdir <FilePath>')
|
|
if not inkeydir:
|
|
missingArgumentExit('inkeydir <FilePath>')
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
kvList = [Ent.USER, user, Ent.CSE_KEYPAIR, None]
|
|
smimeFilename = os.path.join(incertdir, user+'.p7pem')
|
|
if not os.path.isfile(smimeFilename):
|
|
entityActionNotPerformedWarning(kvList, Msg.FILE_NOT_FOUND.format(smimeFilename), i, count)
|
|
continue
|
|
smimeData = readFile(smimeFilename, mode='rb', continueOnError=True)
|
|
if smimeData is None:
|
|
continue
|
|
kaclFilename = os.path.join(inkeydir, user+'.wrap')
|
|
if not os.path.isfile(kaclFilename):
|
|
entityActionNotPerformedWarning(kvList, Msg.FILE_NOT_FOUND.format(kaclFilename), i, count)
|
|
continue
|
|
jsonData = readFile(kaclFilename, mode='r', encoding=UTF8, continueOnError=True)
|
|
if jsonData is None:
|
|
continue
|
|
try:
|
|
keyData = json.loads(jsonData)
|
|
key = 'kacls_url'
|
|
kaclsUri = keyData[key]
|
|
key = 'wrapped_private_key'
|
|
kaclsData = keyData[key]
|
|
cseKeyPair = {
|
|
'pkcs7': smimeData.decode(UTF8),
|
|
'privateKeyMetadata': [{'kaclsKeyMetadata': {'kaclsUri': kaclsUri, 'kaclsData': kaclsData}}]
|
|
}
|
|
except KeyError:
|
|
entityActionNotPerformedWarning(kvList, Msg.JSON_KEY_NOT_FOUND.format(key, kaclFilename), i, count)
|
|
continue
|
|
except (IndexError, SyntaxError, TypeError, ValueError) as e:
|
|
entityActionNotPerformedWarning(kvList, Msg.JSON_ERROR.format(str(e), kaclFilename) , i, count)
|
|
continue
|
|
try:
|
|
result = callGAPI(gmail.users().settings().cse().keypairs(), 'create',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
|
|
userId='me', body=cseKeyPair)
|
|
|
|
keyPairId = result['keyPairId']
|
|
if not returnIdOnly:
|
|
kvList[-1] = keyPairId
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, i, count)
|
|
_stripCSEKeyPairSkipObjects(result, skipObjects)
|
|
_showCSEItem(result, Ent.CSE_KEYPAIR, 'keyPairId', CSE_KEYPAIR_TIME_OBJECTS, i, count, FJQC)
|
|
elif not addIdentity:
|
|
writeStdout(f'{keyPairId}\n')
|
|
if addIdentity:
|
|
identity = {'keyPairId': keyPairId, 'emailAddress': user if not kpEmail else kpEmail}
|
|
kvList = [Ent.USER, user, Ent.CSE_KEYPAIR, keyPairId, Ent.CSE_IDENTITY, identity['emailAddress']]
|
|
result = callGAPI(gmail.users().settings().cse().identities(), 'create',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.ALREADY_EXISTS],
|
|
userId='me', body=identity)
|
|
if not returnIdOnly:
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, i, count)
|
|
_showCSEItem(result, Ent.CSE_IDENTITY, 'emailAddress', CSE_IDENTITY_TIME_OBJECTS, i, count, FJQC)
|
|
else:
|
|
writeStdout(f'{keyPairId}-{user}\n')
|
|
except (GAPI.permissionDenied, GAPI.alreadyExists) as e:
|
|
entityActionFailedWarning(kvList, str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
CSE_KEYPAIR_ACTION_FUNCTION_MAP = {
|
|
Act.DISABLE: 'disable',
|
|
Act.ENABLE: 'enable',
|
|
Act.OBLITERATE: 'obliterate',
|
|
Act.INFO: 'get',
|
|
}
|
|
|
|
# gam <UserTypeEntity> disable csekeypair <KeyPairID>
|
|
# [showpem] [showkaclsdata] [formatjson]
|
|
# gam <UserTypeEntity> enable csekeypair <KeyPairID>
|
|
# [showpem] [showkaclsdata] [formatjson]
|
|
# gam <UserTypeEntity> obliterate csekeypair <KeyPairID>
|
|
# gam <UserTypeEntity> info csekey3pair <KeyPairID>
|
|
# [showpem] [showkaclsdata] [formatjson]
|
|
def processCSEKeyPair(users):
|
|
function = CSE_KEYPAIR_ACTION_FUNCTION_MAP[Act.Get()]
|
|
keyPairId = getString(Cmd.OB_CSE_KEYPAIR_ID)
|
|
FJQC = FormatJSONQuoteChar()
|
|
skipObjects = _initCSEKeyPairSkipObjects()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _resetCSEKeyPairSkipObjects(myarg, skipObjects):
|
|
pass
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
kvList = [Ent.USER, user, Ent.CSE_KEYPAIR, keyPairId]
|
|
try:
|
|
result = callGAPI(gmail.users().settings().cse().keypairs(), function,
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.PERMISSION_DENIED, GAPI.INVALID_ARGUMENT,
|
|
GAPI.NOT_FOUND, GAPI.FAILED_PRECONDITION, GAPI.ALREADY_EXISTS],
|
|
userId='me', keyPairId=keyPairId)
|
|
if function != 'obliterate':
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed(kvList, i, count)
|
|
_stripCSEKeyPairSkipObjects(result, skipObjects)
|
|
_showCSEItem(result, Ent.CSE_KEYPAIR, 'keyPairId', CSE_KEYPAIR_TIME_OBJECTS, i, count, FJQC)
|
|
else:
|
|
entityActionPerformed(kvList, i, count)
|
|
except (GAPI.permissionDenied, GAPI.invalidArgument, GAPI.notFound, GAPI.failedPrecondition, GAPI.alreadyExists) as e:
|
|
entityActionFailedWarning(kvList, str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> show csekeypairs
|
|
# [showpem] [showkaclsdata] [formatjson]
|
|
# gam <UserTypeEntity> print csekeypairs [todrive <ToDriveAttribute>*]
|
|
# [showpem] [showkaclsdata] [formatjson [quotechar <Character>]]
|
|
def printShowCSEKeyPairs(users):
|
|
_printShowCSEItems(users, Ent.CSE_KEYPAIR, 'keyPairId', CSE_KEYPAIR_TIME_OBJECTS)
|
|
|
|
# gam <UserTypeEntity> signature|sig
|
|
# <SignatureContent>
|
|
# (replace <Tag> <String>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <String>)*
|
|
# [html [<Boolean>]] [replyto <EmailAddress>] [default] [treatasalias <Boolean>]
|
|
# [name <String>]
|
|
# [primary]
|
|
def setSignature(users):
|
|
tagReplacements = _initTagReplacements()
|
|
signature, _, html = getStringOrFile('sig')
|
|
body = {}
|
|
primary = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'primary':
|
|
primary = True
|
|
elif myarg == 'html':
|
|
html = getBoolean()
|
|
else:
|
|
getSendAsAttributes(myarg, body, tagReplacements)
|
|
if not tagReplacements['subs']:
|
|
body['signature'] = _processSignature(tagReplacements, signature, html)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if tagReplacements['subs']:
|
|
_getTagReplacementFieldValues(user, i, count, tagReplacements)
|
|
body['signature'] = _processSignature(tagReplacements, signature, html)
|
|
if primary:
|
|
try:
|
|
result = callGAPI(gmail.users().settings().sendAs(), 'list',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
for sendas in result['sendAs']:
|
|
if sendas.get('isPrimary', False):
|
|
emailAddress = sendas['sendAsEmail']
|
|
_processSendAs(user, i, count, Ent.SIGNATURE, emailAddress, i, count, gmail, 'patch', False, body=body, sendAsEmail=emailAddress, fields='')
|
|
break
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
else:
|
|
_processSendAs(user, i, count, Ent.SIGNATURE, user, i, count, gmail, 'patch', False, body=body, sendAsEmail=user, fields='')
|
|
|
|
VACATION_START_STARTED = 'Started'
|
|
VACATION_END_NOT_SPECIFIED = 'NotSpecified'
|
|
|
|
def _showVacation(user, i, count, result, showDisabled, sigReplyFormat):
|
|
enabled = result['enableAutoReply']
|
|
if not enabled and not showDisabled:
|
|
return
|
|
printEntity([Ent.USER, user, Ent.VACATION, None], i, count)
|
|
Ind.Increment()
|
|
printKeyValueList(['Enabled', enabled])
|
|
printKeyValueList(['Contacts Only', result['restrictToContacts']])
|
|
printKeyValueList(['Domain Only', result['restrictToDomain']])
|
|
if 'startTime' in result:
|
|
printKeyValueList(['Start Date', formatLocalDatestamp(result['startTime'])])
|
|
elif enabled:
|
|
printKeyValueList(['Start Date', VACATION_START_STARTED])
|
|
if 'endTime' in result:
|
|
printKeyValueList(['End Date', formatLocalDatestamp(result['endTime'])])
|
|
elif enabled:
|
|
printKeyValueList(['End Date', VACATION_END_NOT_SPECIFIED])
|
|
printKeyValueList(['Subject', result.get('responseSubject', 'None')])
|
|
if sigReplyFormat == SIG_REPLY_HTML:
|
|
printKeyValueList(['Message', None])
|
|
Ind.Increment()
|
|
if result.get('responseBodyPlainText'):
|
|
printKeyValueList([Ind.MultiLineText(result['responseBodyPlainText'])])
|
|
elif result.get('responseBodyHtml'):
|
|
printKeyValueList([Ind.MultiLineText(result['responseBodyHtml'])])
|
|
else:
|
|
printKeyValueList(['None'])
|
|
Ind.Decrement()
|
|
elif sigReplyFormat == SIG_REPLY_FORMAT:
|
|
printKeyValueList(['Message', None])
|
|
Ind.Increment()
|
|
if result.get('responseBodyPlainText'):
|
|
printKeyValueList([Ind.MultiLineText(result['responseBodyPlainText'])])
|
|
elif result.get('responseBodyHtml'):
|
|
printKeyValueList([Ind.MultiLineText(dehtml(result['responseBodyHtml']))])
|
|
else:
|
|
printKeyValueList(['None'])
|
|
Ind.Decrement()
|
|
else: # SIG_REPLY_COMPACT
|
|
if result.get('responseBodyPlainText'):
|
|
printKeyValueList(['Message', escapeCRsNLs(result['responseBodyPlainText'])])
|
|
elif result.get('responseBodyHtml'):
|
|
printKeyValueList(['Message', escapeCRsNLs(result['responseBodyHtml'])])
|
|
else:
|
|
printKeyValueList(['Message', 'None'])
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> vacation [<Boolean>] [subject <String>]
|
|
# [<VacationMessageContent>
|
|
# (replace <Tag> <UserReplacement>)*
|
|
# (replaceregex <REMatchPattern> <RESubstitution> <Tag> <UserReplacement>)*]
|
|
# [html [<Boolean>]] [contactsonly [<Boolean>]] [domainonly [<Boolean>]]
|
|
# [start|startdate <Date>|Started] [end|enddate <Date>|NotSpecified]
|
|
def setVacation(users):
|
|
body = {}
|
|
if Cmd.PeekArgumentPresent(TRUE_VALUES) or Cmd.PeekArgumentPresent(FALSE_VALUES):
|
|
body['enableAutoReply'] = getBoolean(None)
|
|
responseBodyType = 'responseBodyPlainText'
|
|
message = subject = None
|
|
tagReplacements = _initTagReplacements()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'subject':
|
|
subject = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg in SORF_MSG_FILE_ARGUMENTS:
|
|
message, _, html = getStringOrFile(myarg)
|
|
if html:
|
|
responseBodyType = 'responseBodyHtml'
|
|
elif _getTagReplacement(myarg, tagReplacements, True):
|
|
pass
|
|
elif myarg == 'html':
|
|
if getBoolean():
|
|
responseBodyType = 'responseBodyHtml'
|
|
elif myarg == 'contactsonly':
|
|
body['restrictToContacts'] = getBoolean()
|
|
elif myarg == 'domainonly':
|
|
body['restrictToDomain'] = getBoolean()
|
|
elif myarg in {'start', 'startdate'}:
|
|
body['startTime'] = getYYYYMMDD(returnTimeStamp=True, alternateValue=VACATION_START_STARTED)
|
|
elif myarg in {'end', 'enddate'}:
|
|
body['endTime'] = getYYYYMMDD(returnTimeStamp=True, alternateValue=VACATION_END_NOT_SPECIFIED)
|
|
else:
|
|
unknownArgumentExit()
|
|
if message:
|
|
if responseBodyType == 'responseBodyHtml':
|
|
message = message.replace('\r', '').replace('\\n', '<br/>')
|
|
else:
|
|
message = message.replace('\r', '').replace('\\n', '\n')
|
|
if tagReplacements['tags'] and not tagReplacements['subs']:
|
|
message = _processTagReplacements(tagReplacements, message)
|
|
body[responseBodyType] = message
|
|
if subject:
|
|
if tagReplacements['tags'] and not tagReplacements['subs']:
|
|
subject = _processTagReplacements(tagReplacements, subject)
|
|
body['responseSubject'] = subject
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if (message or subject) and tagReplacements['subs']:
|
|
_getTagReplacementFieldValues(user, i, count, tagReplacements)
|
|
if message:
|
|
body[responseBodyType] = _processTagReplacements(tagReplacements, message)
|
|
if subject:
|
|
body['responseSubject'] = _processTagReplacements(tagReplacements, subject)
|
|
try:
|
|
oldBody = callGAPI(gmail.users().settings(), 'getVacation',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
if body.get(responseBodyType):
|
|
oldBody.pop('responseBodyPlainText', None)
|
|
oldBody.pop('responseBodyHtml', None)
|
|
oldBody.update(body)
|
|
result = callGAPI(gmail.users().settings(), 'updateVacation',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID_ARGUMENT,
|
|
GAPI.FAILED_PRECONDITION, GAPI.PERMISSION_DENIED],
|
|
userId='me', body=oldBody)
|
|
printEntity([Ent.USER, user, Ent.VACATION_ENABLED, result['enableAutoReply']], i, count)
|
|
except (GAPI.invalidArgument, GAPI.failedPrecondition, GAPI.permissionDenied) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.VACATION_ENABLED, oldBody['enableAutoReply']], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
|
|
# gam <UserTypeEntity> print vacation [compact] [enabledonly] [todrive <ToDriveAttribute>*]
|
|
# gam <UserTypeEntity> show vacation [compact|format|html] [enabledonly]
|
|
def printShowVacation(users):
|
|
def _printVacation(user, result, showDisabled):
|
|
enabled = result['enableAutoReply']
|
|
if not enabled and not showDisabled:
|
|
return
|
|
row = {'User': user, 'enabled': enabled}
|
|
row['contactsonly'] = result['restrictToContacts']
|
|
row['domainonly'] = result['restrictToDomain']
|
|
if 'startTime' in result:
|
|
row['startdate'] = formatLocalDatestamp(result['startTime'])
|
|
elif enabled:
|
|
row['startdate'] = VACATION_START_STARTED
|
|
if 'endTime' in result:
|
|
row['enddate'] = formatLocalDatestamp(result['endTime'])
|
|
elif enabled:
|
|
row['enddate'] = VACATION_END_NOT_SPECIFIED
|
|
row['subject'] = result.get('responseSubject', 'None')
|
|
if result.get('responseBodyPlainText'):
|
|
row['html'] = False
|
|
row['message'] = escapeCRsNLs(result['responseBodyPlainText'])
|
|
elif result.get('responseBodyHtml'):
|
|
row['html'] = True
|
|
if sigReplyFormat == SIG_REPLY_HTML:
|
|
row['message'] = escapeCRsNLs(result['responseBodyHtml'])
|
|
else:
|
|
row['message'] = result['responseBodyHtml'].replace('\r', '').replace('\n', '')
|
|
else:
|
|
row['html'] = False
|
|
row['message'] = 'None'
|
|
csvPF.WriteRow(row)
|
|
|
|
csvPF = CSVPrintFile(['User', 'enabled', 'contactsonly', 'domainonly',
|
|
'startdate', 'enddate', 'subject', 'html', 'message']) if Act.csvFormat() else None
|
|
showDisabled = True
|
|
sigReplyFormat = SIG_REPLY_HTML
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif (not csvPF and myarg in SIG_REPLY_OPTIONS_MAP) or (csvPF and myarg == 'compact'):
|
|
sigReplyFormat = SIG_REPLY_OPTIONS_MAP[myarg]
|
|
elif myarg == 'enabledonly':
|
|
showDisabled = False
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, gmail = buildGAPIServiceObject(API.GMAIL, user, i, count)
|
|
if not gmail:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.VACATION, user, i, count)
|
|
try:
|
|
result = callGAPI(gmail.users().settings(), 'getVacation',
|
|
throwReasons=GAPI.GMAIL_THROW_REASONS,
|
|
userId='me')
|
|
if not csvPF:
|
|
_showVacation(user, i, count, result, showDisabled, sigReplyFormat)
|
|
else:
|
|
_printVacation(user, result, showDisabled)
|
|
except GAPI.serviceNotAvailable:
|
|
userGmailServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Vacation')
|
|
|
|
def normalizeNoteName(noteName):
|
|
if noteName.startswith('notes/'):
|
|
return noteName
|
|
return f'notes/{noteName}'
|
|
|
|
def _assignNoteOwner(note, user):
|
|
for permission in note.get('permissions', []):
|
|
if permission['role'] == 'OWNER':
|
|
noteOwner = permission.get('user', {}).get('email', '').lower()
|
|
note['owner'] = noteOwner
|
|
note['ownedByMe'] = noteOwner == user
|
|
break
|
|
|
|
def _checkNoteUserRole(note, user, role):
|
|
for permission in note['permissions']:
|
|
if permission['role'] == role and permission.get('user', {}).get('email', '').lower() == user:
|
|
return True
|
|
return False
|
|
|
|
def _showNoteListItems(listItems):
|
|
printKeyValueList(['list', ''])
|
|
Ind.Increment()
|
|
kcount = len(listItems)
|
|
k = 0
|
|
for listItem in listItems:
|
|
k += 1
|
|
printKeyValueListWithCount(['item', ''], k, kcount)
|
|
Ind.Increment()
|
|
if 'text' in listItem and 'text' in listItem['text']:
|
|
printKeyValueList(['text', listItem['text']['text']])
|
|
if 'checked' in listItem:
|
|
printKeyValueList(['checked', listItem['checked']])
|
|
if 'childListItems' in listItem:
|
|
_showNoteListItems(listItem['childListItems'])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
def _showNotePermissions(permissions):
|
|
printKeyValueList(['permissions', ''])
|
|
Ind.Increment()
|
|
kcount = len(permissions)
|
|
k = 0
|
|
for permission in permissions:
|
|
k += 1
|
|
printKeyValueListWithCount(['name', permission['name']], k, kcount)
|
|
Ind.Increment()
|
|
for field in ['role', 'deleted']:
|
|
if field in permission:
|
|
printKeyValueList([field, permission[field]])
|
|
for field in ['user', 'group']:
|
|
if field in permission:
|
|
printKeyValueList([field, permission[field]['email']])
|
|
break
|
|
else:
|
|
if 'email' in permission:
|
|
printKeyValueList(['email', permission['email']])
|
|
if 'family' in permission:
|
|
family = permission['family']
|
|
if 'text' in family:
|
|
printKeyValueList(['family', family['text']['text']])
|
|
elif 'list' in family:
|
|
_showNoteListItems(family['list']['listItems'])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
def _showNoteAttachments(attachments):
|
|
printKeyValueList(['attachments', ''])
|
|
Ind.Increment()
|
|
kcount = len(attachments)
|
|
k = 0
|
|
for attachment in attachments:
|
|
k += 1
|
|
printKeyValueListWithCount(['name', attachment['name']], k, kcount)
|
|
if 'mimeType' in attachment:
|
|
Ind.Increment()
|
|
printKeyValueList(['mimeType', ','.join(attachment['mimeType'])])
|
|
Ind.Decrement()
|
|
Ind.Decrement()
|
|
|
|
NOTES_TIME_OBJECTS = {'createTime', 'updateTime', 'trashTime'}
|
|
|
|
def _showNote(note, j=0, jcount=0, FJQC=None, compact=False):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(note, timeObjects=NOTES_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.NOTE, note['name']], j, jcount)
|
|
Ind.Increment()
|
|
printKeyValueList(['title', note.get('title', None)])
|
|
for field in NOTES_TIME_OBJECTS:
|
|
if field in note:
|
|
printKeyValueList([field, formatLocalTime(note[field])])
|
|
if 'trashed' in note:
|
|
printKeyValueList(['trashed', note['trashed']])
|
|
for field in ['owner', 'ownedByMe']:
|
|
if field in note:
|
|
printKeyValueList([field, note[field]])
|
|
if 'permissions' in note:
|
|
_showNotePermissions(note['permissions'])
|
|
if 'attachments' in note:
|
|
_showNoteAttachments(note['attachments'])
|
|
body = note.get('body', {})
|
|
if 'text' in body and 'text' in body['text']:
|
|
if not compact:
|
|
printKeyValueList(['text', None])
|
|
Ind.Increment()
|
|
printKeyValueList([Ind.MultiLineText(body['text']['text'])])
|
|
Ind.Decrement()
|
|
else:
|
|
printKeyValueList(['text', escapeCRsNLs(body['text']['text'])])
|
|
elif 'list' in body and 'listItems' in body['list']:
|
|
_showNoteListItems(body['list']['listItems'])
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> create note [title <String>]
|
|
# <NoteContent>
|
|
# [missingtextvalue <String>]
|
|
# [copyacls [copyowneraswriter]
|
|
# [compact|formatjson|nodetails]
|
|
def createNote(users):
|
|
def fixTextItem(item):
|
|
if 'text' in item:
|
|
if item['text']:
|
|
item['text'] = unescapeCRsNLs(item['text'])
|
|
return
|
|
if missingTextValue:
|
|
item['text'] = missingTextValue
|
|
|
|
def fixListItem(item):
|
|
if 'listItems' in item:
|
|
if item['listItems']:
|
|
for listItem in item['listItems']:
|
|
fixTextItem(listItem['text'])
|
|
return
|
|
if missingTextValue:
|
|
item['listItems'] = [{'checked': False, 'text': {'text': missingTextValue}}]
|
|
|
|
FJQC = FormatJSONQuoteChar()
|
|
compact = False
|
|
showDetails = True
|
|
missingTextValue = ''
|
|
copyACLs = copyOwnerAsWriter = False
|
|
body = {'title': '', 'body': {}}
|
|
copyUsers = []
|
|
copyGroups = []
|
|
noteOwner = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'title':
|
|
body['title'] = getString(Cmd.OB_STRING, minLen=1, maxLen=1000)
|
|
elif myarg in SORF_TEXT_ARGUMENTS:
|
|
msgText, _, _ = getStringOrFile(myarg, minLen=1, unescapeCRLF=True)
|
|
body['body']['text'] = {'text': msgText}
|
|
elif myarg == 'missingtextvalue':
|
|
missingTextValue = getString(Cmd.OB_STRING, minLen=1)
|
|
elif myarg == 'json':
|
|
jsonData = getJSON([])
|
|
if not body['title']:
|
|
body['title'] = jsonData.get('title', missingTextValue)
|
|
body['body'] = jsonData.get('body', {})
|
|
if 'text' in body['body']:
|
|
fixTextItem(body['body']['text'])
|
|
elif 'list' in body['body']:
|
|
fixListItem(body['body']['list'])
|
|
for permission in jsonData.get('permissions', []):
|
|
if permission['role'] == 'WRITER':
|
|
if 'user' in permission:
|
|
copyUsers.append(permission['user']['email'].lower())
|
|
elif 'group' in permission:
|
|
copyGroups.append(permission['group']['email'].lower())
|
|
elif permission['role'] == 'OWNER':
|
|
if 'user' in permission:
|
|
noteOwner = permission['user']['email'].lower()
|
|
elif myarg == 'copyacls':
|
|
copyACLs = True
|
|
elif myarg == 'copyowneraswriter':
|
|
copyOwnerAsWriter = True
|
|
elif myarg == 'compact':
|
|
compact = True
|
|
elif myarg == 'nodetails':
|
|
showDetails = False
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if not body['body']:
|
|
choices = list(SORF_TEXT_ARGUMENTS)
|
|
choices.append('json')
|
|
missingArgumentExit('|'.join(choices))
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, keep = buildGAPIServiceObject(API.KEEP, user, i, count)
|
|
if not keep:
|
|
continue
|
|
if copyACLs:
|
|
rbody = {'requests': []}
|
|
for addr in copyUsers:
|
|
if addr != user:
|
|
rbody['requests'].append({'parent': None,
|
|
'permission': {'role': 'WRITER', 'user': {'email': addr}}})
|
|
for addr in copyGroups:
|
|
rbody['requests'].append({'parent': None,
|
|
'permission': {'role': 'WRITER', 'group': {'email': addr}}})
|
|
if copyOwnerAsWriter and noteOwner and noteOwner != user:
|
|
rbody['requests'].append({'parent': None,
|
|
'permission': {'role': 'WRITER', 'user': {'email': noteOwner}}})
|
|
kcount = len(rbody['requests'])
|
|
try:
|
|
note = callGAPI(keep.notes(), 'create',
|
|
throwReasons=GAPI.KEEP_THROW_REASONS,
|
|
body=body)
|
|
name = note['name']
|
|
entityKVList = [Ent.USER, user, Ent.NOTE, name]
|
|
_assignNoteOwner(note, user)
|
|
if copyACLs and kcount > 0:
|
|
for request in rbody['requests']:
|
|
request['parent'] = name
|
|
try:
|
|
callGAPI(keep.notes().permissions(), 'batchCreate',
|
|
throwReasons=GAPI.KEEP_THROW_REASONS,
|
|
parent=name, body=rbody)
|
|
note = callGAPI(keep.notes(), 'get',
|
|
throwReasons=GAPI.KEEP_THROW_REASONS,
|
|
name=name)
|
|
except (GAPI.badRequest, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.notFound):
|
|
pass
|
|
except GAPI.serviceNotAvailable:
|
|
pass
|
|
if showDetails:
|
|
_showNote(note, FJQC=FJQC, compact=compact)
|
|
else:
|
|
entityActionPerformed(entityKVList, i, count)
|
|
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.NOTE, body['title']], str(e), i, count)
|
|
except GAPI.authError:
|
|
userKeepServiceNotEnabledWarning(user, i, count)
|
|
|
|
NOTES_FIELDS_CHOICE_MAP = {
|
|
'attachments': 'attachments',
|
|
'body': 'body',
|
|
'createtime': 'createTime',
|
|
'name': 'name',
|
|
'permissions': 'permissions',
|
|
'title': 'title',
|
|
'trashed': 'trashed',
|
|
'trashtime': 'trashTime',
|
|
'updatetime': 'updateTime',
|
|
}
|
|
|
|
# gam <UserTypeEntity> info note <NotesNameEntity>
|
|
# [fields <NotesFieldList>]
|
|
# [compact|formatjson]
|
|
# gam <UserTypeEntity> delete note <NotesNameEntity>
|
|
def deleteInfoNotes(users):
|
|
function = 'delete' if Act.Get() == Act.DELETE else 'get'
|
|
fieldsList = []
|
|
noteNameEntity = getUserObjectEntity(Cmd.OB_NAME, Ent.NOTE)
|
|
showPermissions = True
|
|
if function == 'get':
|
|
FJQC = FormatJSONQuoteChar()
|
|
compact = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getFieldsList(myarg, NOTES_FIELDS_CHOICE_MAP, fieldsList, initialField='name'):
|
|
pass
|
|
elif myarg == 'compact':
|
|
compact = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
fields = getFieldsFromFieldsList(fieldsList)
|
|
else:
|
|
FJQC = None
|
|
checkForExtraneousArguments()
|
|
if fieldsList and 'permissions' not in fieldsList:
|
|
showPermissions = False
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, keep, noteNames, jcount = _validateUserGetObjectList(user, i, count, noteNameEntity,
|
|
api=API.KEEP, showAction=FJQC is None or not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for name in noteNames:
|
|
j += 1
|
|
name = normalizeNoteName(name)
|
|
try:
|
|
if function == 'get':
|
|
note = callGAPI(keep.notes(), function,
|
|
throwReasons=GAPI.KEEP_THROW_REASONS,
|
|
name=name, fields=fields)
|
|
if showPermissions:
|
|
_assignNoteOwner(note, user)
|
|
_showNote(note, j, jcount, FJQC, compact)
|
|
else:
|
|
callGAPI(keep.notes(), function,
|
|
throwReasons=GAPI.KEEP_THROW_REASONS,
|
|
name=name)
|
|
entityActionPerformed([Ent.USER, user, Ent.NOTE, name], j, jcount)
|
|
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.NOTE, name], str(e), j, jcount)
|
|
except GAPI.authError:
|
|
userKeepServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
NOTES_ROLE_CHOICE_MAP = {
|
|
'owner': 'OWNER',
|
|
'writer': 'WRITER',
|
|
}
|
|
|
|
NOTES_COUNTS_MAP = {
|
|
'OWNER': 'noteOwner',
|
|
'WRITER': 'noteWriter',
|
|
}
|
|
|
|
# gam <UserTypeEntity> show notes
|
|
# [fields <NotesFieldList>] [filter <String>]
|
|
# [role owner|writer]
|
|
# [countsonly]
|
|
# [compact] [formatjson]
|
|
# gam <UserTypeEntity> print notes [todrive <ToDriveAttribute>*]
|
|
# [fields <NotesFieldList>] [filter <String>]
|
|
# [role owner|writer]
|
|
# [countsonly]
|
|
# [formatjson [quotechar <Character>]]
|
|
def printShowNotes(users):
|
|
csvPF = CSVPrintFile(['User', 'name', 'title', 'owner', 'ownedByMe']) if Act.csvFormat() else None
|
|
if csvPF:
|
|
csvPF.SetNoEscapeChar(True)
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
compact = countsOnly = False
|
|
fieldsList = []
|
|
noteFilter = None
|
|
role = None
|
|
showPermissions = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif getFieldsList(myarg, NOTES_FIELDS_CHOICE_MAP, fieldsList, initialField=['name', 'title']):
|
|
pass
|
|
elif myarg == 'filter':
|
|
noteFilter = getString(Cmd.OB_STRING)
|
|
elif myarg == 'role':
|
|
role = getChoice(NOTES_ROLE_CHOICE_MAP, mapChoice=True)
|
|
elif not csvPF and myarg == 'compact':
|
|
compact = True
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if FJQC.formatJSON:
|
|
GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL] = False
|
|
if fieldsList and 'permissions' not in fieldsList:
|
|
showPermissions = False
|
|
fieldsList.append('permissions')
|
|
if csvPF:
|
|
if not countsOnly:
|
|
if not FJQC.formatJSON:
|
|
csvPF.RemoveTitles(['owner', 'ownedByMe'])
|
|
else:
|
|
csvPF.RemoveJSONTitles(['owner', 'ownedByMe'])
|
|
if countsOnly and csvPF:
|
|
if not FJQC.formatJSON:
|
|
csvPF.SetTitles(['User', 'noteOwner', 'noteWriter', 'totalNotes'])
|
|
else:
|
|
csvPF.SetJSONTitles(['User', 'JSON'])
|
|
fields = getItemFieldsFromFieldsList('notes', fieldsList, returnItemIfNoneList=False)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, keep = buildGAPIServiceObject(API.KEEP, user, i, count)
|
|
if not keep:
|
|
continue
|
|
if csvPF:
|
|
printGettingEntityItemForWhom(Ent.NOTE, user, i, count)
|
|
pageMessage = getPageMessageForWhom()
|
|
else:
|
|
pageMessage = None
|
|
try:
|
|
notes = callGAPIpages(keep.notes(), 'list', 'notes',
|
|
pageMessage=pageMessage,
|
|
throwReasons=GAPI.KEEP_THROW_REASONS,
|
|
filter=noteFilter, fields=fields)
|
|
if countsOnly:
|
|
noteCounts = {'User': user, 'noteOwner': 0, 'noteWriter': 0, 'totalNotes': 0}
|
|
for note in notes:
|
|
noteCounts['totalNotes'] += 1
|
|
for permission in note['permissions']:
|
|
if permission.get('user', {}).get('email', '').lower() == user:
|
|
noteCounts[NOTES_COUNTS_MAP[permission['role']]] += 1
|
|
break
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
printEntityKVList([Ent.USER, user], ['noteOwner', noteCounts['noteOwner'],
|
|
'noteWriter', noteCounts['noteWriter'],
|
|
'totalNotes', noteCounts['totalNotes']], i, count)
|
|
else:
|
|
printLine(json.dumps(cleanJSON(noteCounts), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
row = {'User': user, 'noteOwner': noteCounts['noteOwner'], 'noteWriter': noteCounts['noteWriter'],
|
|
'totalNotes': noteCounts['totalNotes']}
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'User': noteCounts.pop('User')}
|
|
row['JSON'] = json.dumps(cleanJSON(noteCounts), ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
elif not csvPF:
|
|
jcount = len(notes)
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.NOTE, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for note in notes:
|
|
j += 1
|
|
if showPermissions:
|
|
_assignNoteOwner(note, user)
|
|
if role is None or _checkNoteUserRole(note, user, role):
|
|
if not showPermissions:
|
|
note.pop('permissions', None)
|
|
_showNote(note, j, jcount, FJQC, compact)
|
|
Ind.Decrement()
|
|
else:
|
|
for note in notes:
|
|
if showPermissions:
|
|
_assignNoteOwner(note, user)
|
|
if role is None or _checkNoteUserRole(note, user, role):
|
|
if not showPermissions:
|
|
note.pop('permissions', None)
|
|
row = flattenJSON(note, flattened={'User': user}, timeObjects=NOTES_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'User': user, 'name': note['name'], 'title': note.get('title', '')}
|
|
if showPermissions:
|
|
row.update({'owner': note['owner'], 'ownedByMe': note['ownedByMe']})
|
|
row['JSON'] = json.dumps(cleanJSON(note, timeObjects=NOTES_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.NOTE, None], str(e), i, count)
|
|
except GAPI.authError:
|
|
userKeepServiceNotEnabledWarning(user, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Notes')
|
|
|
|
GET_NOTE_HTTP_ERROR_PATTERN = re.compile(r'^.*\'description\': \'(.*)\'')
|
|
|
|
# gam <UserTypeEntity> get noteattachments <NotesNameEntity>
|
|
# [targetfolder <FilePath>] [targetname <FileName>] [overwrite [<Boolean>]]
|
|
# [<DriveFileParentAttribute>]
|
|
def getNoteAttachments(users):
|
|
noteNameEntity = getUserObjectEntity(Cmd.OB_NAME, Ent.NOTE)
|
|
targetFolderPattern = GC.Values[GC.DRIVE_DIR]
|
|
targetNamePattern = None
|
|
overwrite = False
|
|
body = {}
|
|
parentParms = initDriveFileAttributes()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'targetfolder':
|
|
targetFolderPattern = os.path.expanduser(getString(Cmd.OB_FILE_PATH))
|
|
elif myarg == 'targetname':
|
|
targetNamePattern = getString(Cmd.OB_FILE_NAME)
|
|
elif myarg == 'overwrite':
|
|
overwrite = getBoolean()
|
|
elif getDriveFileParentAttribute(myarg, parentParms):
|
|
pass
|
|
else:
|
|
unknownArgumentExit()
|
|
parentSpecified = _driveFileParentSpecified(parentParms)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, keep, noteNames, jcount = _validateUserGetObjectList(user, i, count, noteNameEntity,
|
|
api=API.KEEP, showAction=True)
|
|
if jcount == 0:
|
|
continue
|
|
if parentSpecified:
|
|
_ , drive = buildGAPIServiceObject(API.DRIVE3, user, i, count)
|
|
if not drive:
|
|
continue
|
|
if not _getDriveFileParentInfo(drive, user, i, count, body, parentParms):
|
|
continue
|
|
_, userName, _ = splitEmailAddressOrUID(user)
|
|
targetFolder = _substituteForUser(targetFolderPattern, user, userName)
|
|
if not os.path.isdir(targetFolder):
|
|
os.makedirs(targetFolder)
|
|
targetName = _substituteForUser(targetNamePattern, user, userName) if targetNamePattern else None
|
|
Ind.Increment()
|
|
j = 0
|
|
for name in noteNames:
|
|
j += 1
|
|
name = normalizeNoteName(name)
|
|
try:
|
|
result = callGAPI(keep.notes(), 'get',
|
|
throwReasons=GAPI.KEEP_THROW_REASONS,
|
|
name=name, fields='title,attachments')
|
|
title = result.get('title', 'attachment')
|
|
kcount = len(result['attachments'])
|
|
entityPerformActionNumItems([Ent.NOTE, name], kcount, Ent.ATTACHMENT, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for attachment in result['attachments']:
|
|
k += 1
|
|
attachmentName = attachment['name'][attachment['name'].find('attachments'):]
|
|
entityValueList = [Ent.ATTACHMENT, attachmentName]
|
|
mimeTypes = attachment.get('mimeType', [])
|
|
if not mimeTypes:
|
|
entityActionNotPerformedWarning(entityValueList, Msg.MIMETYPE_NOT_PRESENT_IN_ATTACHMENT, k, kcount)
|
|
continue
|
|
mimeType = mimeTypes[0]
|
|
localFilename, filename = uniqueFilename(targetFolder, f"{targetName or cleanFilename(title)}-{k}{MIMETYPE_EXTENSION_MAP.get(mimeType, '')}", overwrite)
|
|
request = keep.media().download(name=attachment['name'], mimeType=mimeType)
|
|
f = openFile(localFilename, 'wb', continueOnError=True)
|
|
if f is None:
|
|
continue
|
|
downloader = googleapiclient.http.MediaIoBaseDownload(f, request)
|
|
done = False
|
|
downloadOK = False
|
|
try:
|
|
while not done:
|
|
status, done = downloader.next_chunk()
|
|
if status.progress() < 1.0:
|
|
entityActionPerformedMessage(entityValueList, f'{status.progress():>7.2%}', k, kcount)
|
|
entityModifierNewValueActionPerformed(entityValueList, Act.MODIFIER_TO, localFilename, k, kcount)
|
|
downloadOK = True
|
|
except (IOError, httplib2.HttpLib2Error) as e:
|
|
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, localFilename, str(e), k, kcount)
|
|
except googleapiclient.http.HttpError as e:
|
|
mg = GET_NOTE_HTTP_ERROR_PATTERN.match(str(e))
|
|
if mg:
|
|
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, localFilename, mg.group(1), k, kcount)
|
|
else:
|
|
entityModifierNewValueActionFailedWarning(entityValueList, Act.MODIFIER_TO, localFilename, str(e), k, kcount)
|
|
closeFile(f, True)
|
|
if downloadOK and parentSpecified:
|
|
body['name'] = filename
|
|
body['mimeType'] = mimeType
|
|
media_body = googleapiclient.http.MediaFileUpload(filename, mimetype=mimeType, resumable=True)
|
|
Act.Set(Act.CREATE)
|
|
try:
|
|
result = callGAPI(drive.files(), 'create',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS,
|
|
GAPI.INSUFFICIENT_PARENT_PERMISSIONS,
|
|
GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.CANNOT_ADD_PARENT,
|
|
GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, GAPI.STORAGE_QUOTA_EXCEEDED,
|
|
GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED,
|
|
GAPI.TEAMDRIVE_FILE_LIMIT_EXCEEDED, GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP],
|
|
media_body=media_body, body=body, fields='id,name', supportsAllDrives=True)
|
|
entityModifierNewValueActionPerformed([Ent.USER, user, Ent.DRIVE_FILE, f'{result["name"]}({result["id"]})'],
|
|
Act.MODIFIER_WITH_CONTENT_FROM, localFilename, k, kcount)
|
|
except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions,
|
|
GAPI.invalid, GAPI.badRequest, GAPI.cannotAddParent,
|
|
GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed,
|
|
GAPI.teamDriveFileLimitExceeded, GAPI.teamDriveHierarchyTooDeep) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE, body['name']], str(e), k, kcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userDriveServiceNotEnabledWarning(user, str(e), i, count)
|
|
Act.Set(Act.DOWNLOAD)
|
|
Ind.Decrement()
|
|
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.NOTE, name], str(e), j, jcount)
|
|
except GAPI.authError:
|
|
userKeepServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> create noteacl <NotesNameEntity>
|
|
# (user|group <EmailAddress>)+
|
|
# (json [charset <Charset>] <JSONData>)|(json file <FileName> [charset <Charset>])
|
|
# [nodetails]
|
|
def createNotesACLs(users):
|
|
noteNameEntity = getUserObjectEntity(Cmd.OB_NAME, Ent.NOTES_ACLS)
|
|
body = {'requests': []}
|
|
showDetails = True
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'user', 'group'}:
|
|
body['requests'].append({'parent': None,
|
|
'permission': {'role': 'WRITER', myarg: {'email': getEmailAddress(noUid=True)}}})
|
|
elif myarg == 'json':
|
|
jsonData = getJSON([])
|
|
for permission in jsonData.get('permissions', []):
|
|
if permission['role'] == 'WRITER':
|
|
if 'user' in permission:
|
|
body['requests'].append({'parent': None,
|
|
'permission': {'role': 'WRITER', 'user': {'email': permission['user']['email']}}})
|
|
elif 'group' in permission:
|
|
body['requests'].append({'parent': None,
|
|
'permission': {'role': 'WRITER', 'group': {'email': permission['group']['email']}}})
|
|
elif myarg == 'nodetails':
|
|
showDetails = False
|
|
else:
|
|
unknownArgumentExit()
|
|
kcount = len(body['requests'])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, keep, noteNames, jcount = _validateUserGetObjectList(user, i, count, noteNameEntity, api=API.KEEP)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for name in noteNames:
|
|
j += 1
|
|
name = normalizeNoteName(name)
|
|
entityKVList = [Ent.USER, user, Ent.NOTE, name]
|
|
rbody = body.copy()
|
|
for request in rbody['requests']:
|
|
request['parent'] = name
|
|
try:
|
|
permissions = callGAPI(keep.notes().permissions(), 'batchCreate',
|
|
throwReasons=GAPI.KEEP_THROW_REASONS+[GAPI.FAILED_PRECONDITION],
|
|
parent=name, body=rbody)
|
|
entityNumItemsActionPerformed(entityKVList, kcount, Ent.NOTE_ACL, j, jcount)
|
|
if showDetails:
|
|
Ind.Increment()
|
|
_showNotePermissions(permissions['permissions'])
|
|
Ind.Decrement()
|
|
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound, GAPI.failedPrecondition) as e:
|
|
entityActionFailedWarning(entityKVList, str(e), i, count)
|
|
except GAPI.authError:
|
|
userKeepServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
|
|
# gam <UserTypeEntity> delete noteacl <NotesNameEntity>
|
|
# (user|group <EmailAddress>)+
|
|
# (json [charset <Charset>] <JSONData>)|(json file <FileName> [charset <Charset>])
|
|
def deleteNotesACLs(users):
|
|
noteNameEntity = getUserObjectEntity(Cmd.OB_NAME, Ent.NOTES_ACLS)
|
|
emails = {'user': set(), 'group': set()}
|
|
body = {'names': []}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in {'user', 'group'}:
|
|
emails[myarg].add(getEmailAddress(noUid=True).lower())
|
|
elif myarg == 'json':
|
|
jsonData = getJSON([])
|
|
for permission in jsonData.get('permissions', []):
|
|
if permission['role'] == 'WRITER':
|
|
loc = permission['name'].find('/permissions')
|
|
body['names'].append(permission['name'][loc+1:])
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, keep, noteNames, jcount = _validateUserGetObjectList(user, i, count, noteNameEntity, api=API.KEEP)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for name in noteNames:
|
|
j += 1
|
|
name = normalizeNoteName(name)
|
|
rbody = body.copy()
|
|
if emails['user'] or emails['group']:
|
|
try:
|
|
note = callGAPI(keep.notes(), 'get',
|
|
throwReasons=GAPI.KEEP_THROW_REASONS,
|
|
name=name, fields='permissions')
|
|
for permission in note['permissions']:
|
|
if permission['role'] == 'WRITER':
|
|
if (('user' in permission and permission['user']['email'].lower() in emails['user']) or
|
|
('group' in permission and permission['group']['email'].lower() in emails['group'])):
|
|
rbody['names'].append(permission['name'])
|
|
except (GAPI.badRequest, GAPI.permissionDenied, GAPI.invalidArgument, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.NOTE, name], str(e), i, count)
|
|
break
|
|
except GAPI.authError:
|
|
userKeepServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
for k, perm in enumerate(rbody['names']):
|
|
if perm.startswith('notes/'):
|
|
pass
|
|
elif perm.startswith('permissions'):
|
|
rbody['names'][k] = f'{name}/{perm}'
|
|
else:
|
|
rbody['names'][k] = f'{name}/permissions/{perm}'
|
|
kcount = len(rbody['names'])
|
|
entityKVList = [Ent.USER, user, Ent.NOTE, name]
|
|
try:
|
|
callGAPI(keep.notes().permissions(), 'batchDelete',
|
|
throwReasons=GAPI.KEEP_THROW_REASONS,
|
|
parent=name, body=rbody)
|
|
entityNumItemsActionPerformed(entityKVList, kcount, Ent.NOTE_ACL, j, jcount)
|
|
except (GAPI.badRequest, GAPI.invalidArgument, GAPI.notFound) as e:
|
|
entityActionFailedWarning(entityKVList, str(e), i, count)
|
|
except GAPI.authError:
|
|
userKeepServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
|
|
def verifyTasksServiceEnabled(svc, user, i, count):
|
|
try:
|
|
callGAPIpages(svc.tasklists(), 'list', 'items',
|
|
throwReasons=GAPI.TASKLIST_THROW_REASONS,
|
|
maxItems=1)
|
|
return True
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.invalid):
|
|
userTasksServiceNotEnabledWarning(user, i, count)
|
|
return False
|
|
|
|
def getTaskLists(svc, user, i, count):
|
|
try:
|
|
results = callGAPIpages(svc.tasklists(), 'list', 'items',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.TASKLIST_THROW_REASONS,
|
|
maxResults=100)
|
|
except (GAPI.badRequest, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, None], str(e), i, count)
|
|
results = None
|
|
except GAPI.notFound:
|
|
userTasksServiceNotEnabledWarning(user, i, count)
|
|
results = None
|
|
return results
|
|
|
|
def getTaskListIDfromTitle(svc, userTasklists, title, user, i, count):
|
|
if userTasklists is None:
|
|
printGettingEntityItemForWhom(Ent.TASKLIST, user, i, count)
|
|
userTasklists = getTaskLists(svc, user, i, count)
|
|
if userTasklists is None:
|
|
return None, None
|
|
for userTasklist in userTasklists:
|
|
if userTasklist['title'] == title:
|
|
return userTasklists, userTasklist['id']
|
|
return userTasklists, None
|
|
|
|
TASK_SKIP_OBJECTS = ['selfLink']
|
|
TASK_TIME_OBJECTS = ['completed', 'updated']
|
|
|
|
def _showTask(tasklist, task, j=0, jcount=0, FJQC=None, compact=False):
|
|
task['tasklistId'] = tasklist
|
|
task['taskId'] = f"{tasklist}/{task['id']}"
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(task, skipObjects=TASK_SKIP_OBJECTS, timeObjects=TASK_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.TASK, task['taskId']], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, task, skipObjects=TASK_SKIP_OBJECTS+['notes'], timeObjects=TASK_TIME_OBJECTS)
|
|
field = 'notes'
|
|
if field in task:
|
|
if not compact:
|
|
printKeyValueList([field, None])
|
|
Ind.Increment()
|
|
printKeyValueList([Ind.MultiLineText(task[field])])
|
|
Ind.Decrement()
|
|
else:
|
|
printKeyValueList(['notes', escapeCRsNLs(task[field])])
|
|
Ind.Decrement()
|
|
|
|
TASK_STATUS_MAP = {
|
|
'completed': 'completed',
|
|
'needsaction': 'needsAction',
|
|
}
|
|
|
|
def getTaskAttribute(myarg, body):
|
|
if myarg == 'title':
|
|
body[myarg] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'notes':
|
|
body[myarg] = getStringWithCRsNLs()
|
|
elif myarg == 'status':
|
|
body[myarg] = getChoice(TASK_STATUS_MAP, mapChoice=True)
|
|
elif myarg == 'due':
|
|
body[myarg] = getTimeOrDeltaFromNow()
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def getTaskMoveAttribute(myarg, kwargs):
|
|
if myarg == 'parent':
|
|
kwargs[myarg] = getString(Cmd.OB_TASK_ID)
|
|
elif myarg == 'previous':
|
|
kwargs[myarg] = getString(Cmd.OB_TASK_ID)
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
# gam <UserTypeEntity> create task <TasklistEntity>
|
|
# <TaskAttribute>* [parent <TaskID>] [previous <TaskID>]
|
|
# [compact|formatjson|returnidonly]
|
|
# gam <UserTypeEntity> update task <TasklistIDTaskIDEntity>
|
|
# <TaskAttribute>*
|
|
# [compact|formatjson]
|
|
# gam <UserTypeEntity> info task <TasklistIDTaskIDEntity>
|
|
# [compact|formatjson]
|
|
# gam <UserTypeEntity> delete task <TasklistIDTaskIDEntity>
|
|
# gam <UserTypeEntity> move task <TasklistIDTaskIDEntity>
|
|
# [parent <TaskID>] [previous <TaskID>]
|
|
# [compact|formatjson]
|
|
def processTasks(users):
|
|
action = Act.Get()
|
|
if action != Act.CREATE:
|
|
tasklistTaskEntity = getUserObjectEntity(Cmd.OB_TASKLIST_ID_TASK_ID_ENTITY, Ent.TASK, shlexSplit=True)
|
|
else:
|
|
tasklistTaskEntity = getUserObjectEntity(Cmd.OB_TASKLIST_ID_ENTITY, Ent.TASK, shlexSplit=True)
|
|
if action in {Act.DELETE, Act.CLEAR}:
|
|
FJQC = None
|
|
checkForExtraneousArguments()
|
|
else:
|
|
FJQC = FormatJSONQuoteChar()
|
|
body = {}
|
|
kwargs = {}
|
|
compact = returnIdOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if action in {Act.CREATE, Act.UPDATE} and getTaskAttribute(myarg, body):
|
|
pass
|
|
elif action in {Act.CREATE, Act.MOVE} and getTaskMoveAttribute(myarg, kwargs):
|
|
pass
|
|
elif action == Act.CREATE and myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
elif myarg == 'compact':
|
|
compact = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, svc, tasklistTasks, jcount = _validateUserGetObjectList(user, i, count, tasklistTaskEntity,
|
|
api=API.TASKS, showAction=FJQC is None or not FJQC.formatJSON)
|
|
if jcount == 0 or not verifyTasksServiceEnabled(svc, user, i, count):
|
|
continue
|
|
userTasklists = None
|
|
Ind.Increment()
|
|
j = 0
|
|
for tasklistTask in tasklistTasks:
|
|
j += 1
|
|
if action != Act.CREATE:
|
|
if '/' not in tasklistTask:
|
|
continue
|
|
tasklist, task = tasklistTask.split('/', 1)
|
|
else:
|
|
tasklist = tasklistTask
|
|
task = body.get('title', '')
|
|
if tasklist.startswith('tltitle:'):
|
|
tasklistTitle = tasklist[8:]
|
|
userTasklists, tasklist = getTaskListIDfromTitle(svc, userTasklists, tasklistTitle, user, i, count)
|
|
if tasklist is None:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklistTitle, Ent.TASK, task], Msg.TASKLIST_TITLE_NOT_FOUND, j, jcount)
|
|
continue
|
|
try:
|
|
if action == Act.DELETE:
|
|
callGAPI(svc.tasks(), 'delete',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist, task=task)
|
|
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, tasklist, Ent.TASK, task], j, jcount)
|
|
elif action == Act.INFO:
|
|
result = callGAPI(svc.tasks(), 'get',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist, task=task)
|
|
_showTask(tasklist, result, j, jcount, FJQC, compact)
|
|
else:
|
|
if action == Act.CREATE:
|
|
result = callGAPI(svc.tasks(), 'insert',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist, body=body, **kwargs)
|
|
if returnIdOnly:
|
|
writeStdout(f"{result['id']}\n")
|
|
continue
|
|
elif action == Act.UPDATE:
|
|
result = callGAPI(svc.tasks(), 'patch',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist, task=task, body=body)
|
|
else: #elif action == Act.MOVE
|
|
result = callGAPI(svc.tasks(), 'move',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist, task=task, **kwargs)
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, tasklist, Ent.TASK, result['id']], j, jcount)
|
|
Ind.Increment()
|
|
_showTask(tasklist, result, j, jcount, FJQC, compact)
|
|
Ind.Decrement()
|
|
except (GAPI.badRequest, GAPI.permissionDenied, GAPI.invalid, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklist, Ent.TASK, task], str(e), j, jcount)
|
|
except GAPI.serviceNotAvailable:
|
|
Ind.Decrement()
|
|
userTasksServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
TASK_ORDERBY_CHOICE_MAP = {
|
|
'completed': ('completed', 'No date'),
|
|
'due': ('due', 'No date'),
|
|
'updated': ('updated', 'No date'),
|
|
}
|
|
|
|
TASK_QUERY_TIME_MAP = {
|
|
'completedmin': 'completedMin',
|
|
'completedmax': 'completedMax',
|
|
'duemin': 'dueMin',
|
|
'duemax': 'dueMax',
|
|
'updatedmin': 'updatedMin',
|
|
}
|
|
TASK_QUERY_STATE_MAP = {
|
|
'showcompleted': 'showCompleted',
|
|
'showdeleted': 'showDeleted',
|
|
'showhidden': 'showHidden',
|
|
}
|
|
|
|
# gam <UserTypeEntity> show tasks [tasklists <TasklistEntity>]
|
|
# [completedmin <Time>] [completedmax <Time>]
|
|
# [duemin <Time>] [duemax <Time>]
|
|
# [updatedmin <Time>]
|
|
# [showcompleted [<Boolean>]] [showdeleted [<Boolean>]] [showhidden [<Boolean>]] [showall]
|
|
# [orderby completed|due|updated]
|
|
# [countsonly|compact|formatjson]
|
|
# gam <UserTypeEntity> print tasks [tasklists <TasklistEntity>] [todrive <ToDriveAttribute>*]
|
|
# [completedmin <Time>] [completedmax <Time>]
|
|
# [duemin <Time>] [duemax <Time>]
|
|
# [updatedmin <Time>]
|
|
# [showcompleted [<Boolean>]] [showdeleted [<Boolean>]] [showhidden [<Boolean>]] [showall]
|
|
# [orderby completed|due|updated]
|
|
# [countsonly | (formatjson [quotechar <Character>])]
|
|
def printShowTasks(users):
|
|
def _showTaskAndChildren(tasklist, taskId, k, compact):
|
|
if taskId in taskParentsProcessed:
|
|
return k
|
|
taskParentsProcessed.add(taskId)
|
|
if taskId in taskData:
|
|
k += 1
|
|
_showTask(tasklist, taskData[taskId], k, kcount, FJQC, compact)
|
|
Ind.Increment()
|
|
for task in taskParents.get(taskId, []):
|
|
k = _showTaskAndChildren(tasklist, task['taskId'], k, compact)
|
|
if taskId in taskData:
|
|
Ind.Decrement()
|
|
return k
|
|
|
|
def _printTaskAndChildren(tasklist, taskId):
|
|
if taskId in taskParentsProcessed:
|
|
return
|
|
taskParentsProcessed.add(taskId)
|
|
if taskId in taskData:
|
|
task = taskData[taskId]
|
|
task['tasklistId'] = tasklist
|
|
task['taskId'] = f"{tasklist}/{task['id']}"
|
|
row = flattenJSON(task, flattened={'User': user}, skipObjects=TASK_SKIP_OBJECTS, timeObjects=TASK_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'User': user, 'id': task['id'], 'tasklistId': tasklist, 'taskId': task['taskId'], 'title': task.get('title', '')}
|
|
row['JSON'] = json.dumps(cleanJSON(task, skipObjects=TASK_SKIP_OBJECTS, timeObjects=TASK_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
for task in taskParents.get(taskId, []):
|
|
_printTaskAndChildren(tasklist, task['taskId'])
|
|
|
|
csvPF = CSVPrintFile(['User', 'tasklistId', 'id', 'taskId', 'title', 'status', 'due', 'updated', 'completed'], 'sortall') if Act.csvFormat() else None
|
|
if csvPF:
|
|
csvPF.SetNoEscapeChar(True)
|
|
CSVTitle = 'Tasks'
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
tasklistEntity = None
|
|
kwargs = {'maxResults': 100}
|
|
compact = countsOnly = False
|
|
orderBy = orderByNoDataValue = None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'tasklist', 'tasklists'}:
|
|
tasklistEntity = getUserObjectEntity(Cmd.OB_TASKLIST_ID_ENTITY, Ent.TASKLIST, shlexSplit=True)
|
|
elif myarg in TASK_QUERY_TIME_MAP:
|
|
kwargs[TASK_QUERY_TIME_MAP[myarg]] = getTimeOrDeltaFromNow()
|
|
elif myarg in TASK_QUERY_STATE_MAP:
|
|
kwargs[TASK_QUERY_STATE_MAP[myarg]] = getBoolean()
|
|
elif myarg == 'showall':
|
|
for field in TASK_QUERY_STATE_MAP.values():
|
|
kwargs[field] = True
|
|
elif not csvPF and myarg == 'compact':
|
|
compact = True
|
|
elif myarg == 'orderby':
|
|
orderBy, orderByNoDataValue = getChoice(TASK_ORDERBY_CHOICE_MAP, mapChoice=True)
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
if csvPF:
|
|
csvPF.SetTitles(['User', CSVTitle])
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if csvPF and FJQC.formatJSON:
|
|
csvPF.SetJSONTitles(['User', 'tasklistId', 'id', 'taskId', 'title', 'JSON'])
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if tasklistEntity is None:
|
|
user, svc = buildGAPIServiceObject(API.TASKS, user, i, count)
|
|
if not svc:
|
|
continue
|
|
printGettingEntityItemForWhom(Ent.TASKLIST, user, i, count)
|
|
results = getTaskLists(svc, user, i, count)
|
|
if results is None:
|
|
continue
|
|
tasklists = [tasklist['id'] for tasklist in results]
|
|
jcount = len(tasklists)
|
|
else:
|
|
userTasklists = None
|
|
user, svc, tasklists, jcount = _validateUserGetObjectList(user, i, count, tasklistEntity, api=API.TASKS,
|
|
showAction=FJQC is None or not FJQC.formatJSON)
|
|
if jcount == 0:
|
|
continue
|
|
taskCount = 0
|
|
# if not csvPF and not FJQC.formatJSON:
|
|
# entityPerformActionNumItems([Ent.USER, user], jcount, Ent.TASKLIST, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for tasklist in tasklists:
|
|
j += 1
|
|
if tasklist.startswith('tltitle:'):
|
|
tasklistTitle = tasklist[8:]
|
|
userTasklists, tasklist = getTaskListIDfromTitle(svc, userTasklists, tasklistTitle, user, i, count)
|
|
if tasklist is None:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklistTitle], Msg.TASKLIST_TITLE_NOT_FOUND, j, jcount)
|
|
continue
|
|
printGettingEntityItemForWhom(Ent.TASK, tasklist, j, jcount)
|
|
try:
|
|
tasks = callGAPIpages(svc.tasks(), 'list', 'items',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist, **kwargs)
|
|
kcount = len(tasks)
|
|
if countsOnly:
|
|
taskCount += kcount
|
|
continue
|
|
taskParents = {None: []}
|
|
taskData = {}
|
|
taskParentsProcessed = set()
|
|
if orderBy is None:
|
|
for task in tasks:
|
|
taskData[task['id']] = task
|
|
parent = task.get('parent', None)
|
|
taskInfo = {'taskId': task['id'], 'parent': parent, 'position': task['position']}
|
|
taskParents.setdefault(parent, [])
|
|
taskParents[parent].append(taskInfo)
|
|
for parent in taskParents:
|
|
taskParents[parent].sort(key=lambda k: k['position'])
|
|
else:
|
|
for task in tasks:
|
|
taskData[task['id']] = task
|
|
if orderBy not in task:
|
|
task[orderBy] = orderByNoDataValue
|
|
taskInfo = {'taskId': task['id'], orderBy: task[orderBy],
|
|
'parent': task.get('parent', ' '), 'position': task['position']}
|
|
taskParents[None].append(taskInfo)
|
|
taskParents[None].sort(key=lambda k: (k[orderBy], k['parent'], k['position']))
|
|
if not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.TASKLIST, tasklist], kcount, Ent.TASK, j, jcount)
|
|
Ind.Increment()
|
|
k = 0
|
|
for parent in taskParents.values():
|
|
for task in parent:
|
|
k = _showTaskAndChildren(tasklist, task['taskId'], k, compact)
|
|
Ind.Decrement()
|
|
else:
|
|
for parent in taskParents.values():
|
|
for task in parent:
|
|
_printTaskAndChildren(tasklist, task['taskId'])
|
|
except (GAPI.badRequest, GAPI.invalid, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklist, Ent.TASK, None], str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
userTasksServiceNotEnabledWarning(user, i, count)
|
|
Ind.Decrement()
|
|
if countsOnly:
|
|
if csvPF:
|
|
csvPF.WriteRowTitles({'User': user, CSVTitle: taskCount})
|
|
else:
|
|
printEntityKVList([Ent.USER, user], [CSVTitle, taskCount], i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile(CSVTitle)
|
|
|
|
TASKLIST_SKIP_OBJECTS = ['selfLink']
|
|
TASKLIST_TIME_OBJECTS = ['updated']
|
|
|
|
def _showTasklist(tasklist, j=0, jcount=0, FJQC=None):
|
|
if FJQC is not None and FJQC.formatJSON:
|
|
printLine(json.dumps(cleanJSON(tasklist, skipObjects=TASKLIST_SKIP_OBJECTS, timeObjects=TASKLIST_TIME_OBJECTS), ensure_ascii=False, sort_keys=True))
|
|
return
|
|
printEntity([Ent.TASKLIST, tasklist['id']], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, tasklist, skipObjects=TASKLIST_SKIP_OBJECTS, timeObjects=TASKLIST_TIME_OBJECTS)
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> create tasklist
|
|
# [title <String>]
|
|
# [returnidonly] [formatjson]
|
|
# gam <UserTypeEntity> update tasklist <TasklistEntity>
|
|
# [title <String>]
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> info tasklist <TasklistEntity>
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> delete tasklist <TasklistEntity>
|
|
# gam <UserTypeEntity> clear tasklist <TasklistEntity>
|
|
def processTasklists(users):
|
|
action = Act.Get()
|
|
if action != Act.CREATE:
|
|
tasklistEntity = getUserObjectEntity(Cmd.OB_TASKLIST_ID_ENTITY, Ent.TASKLIST, shlexSplit=True)
|
|
else:
|
|
tasklistEntity = {'item': Ent.TASKLIST, 'list': [None], 'dict': None}
|
|
if action in {Act.DELETE, Act.CLEAR}:
|
|
FJQC = None
|
|
checkForExtraneousArguments()
|
|
else:
|
|
FJQC = FormatJSONQuoteChar()
|
|
body = {}
|
|
returnIdOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if action in {Act.CREATE, Act.UPDATE} and myarg == 'title':
|
|
body['title'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif action == Act.CREATE and myarg == 'returnidonly':
|
|
returnIdOnly = True
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
userTasklists = None
|
|
user, svc, tasklists, jcount = _validateUserGetObjectList(user, i, count, tasklistEntity,
|
|
api=API.TASKS,
|
|
showAction=action != Act.CREATE and (FJQC is None or not FJQC.formatJSON))
|
|
if jcount == 0 or not verifyTasksServiceEnabled(svc, user, i, count):
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for tasklist in tasklists:
|
|
j += 1
|
|
if action != Act.CREATE:
|
|
if tasklist.startswith('tltitle:'):
|
|
tasklistTitle = tasklist[8:]
|
|
userTasklists, tasklist = getTaskListIDfromTitle(svc, userTasklists, tasklistTitle, user, i, count)
|
|
if userTasklists is None:
|
|
continue
|
|
if tasklist is None:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklistTitle], Msg.TASKLIST_TITLE_NOT_FOUND, j, jcount)
|
|
continue
|
|
try:
|
|
if action == Act.DELETE:
|
|
callGAPI(svc.tasklists(), 'delete',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist)
|
|
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, tasklist], j, jcount)
|
|
elif action == Act.CLEAR:
|
|
callGAPI(svc.tasks(), 'clear',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist)
|
|
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, tasklist], j, jcount)
|
|
elif action == Act.INFO:
|
|
result = callGAPI(svc.tasklists(), 'get',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist)
|
|
_showTasklist(result, j, jcount, FJQC)
|
|
else:
|
|
if action == Act.CREATE:
|
|
result = callGAPI(svc.tasklists(), 'insert',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
body=body)
|
|
if returnIdOnly:
|
|
writeStdout(f"{result['id']}\n")
|
|
continue
|
|
else: # Act.UPDATE
|
|
result = callGAPI(svc.tasklists(), 'patch',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklist, body=body)
|
|
if not FJQC.formatJSON:
|
|
entityActionPerformed([Ent.USER, user, Ent.TASKLIST, result['id']], i, count)
|
|
Ind.Increment()
|
|
_showTasklist(result, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.badRequest, GAPI.invalid, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, tasklist], str(e), j, jcount)
|
|
except GAPI.serviceNotAvailable:
|
|
Ind.Decrement()
|
|
userTasksServiceNotEnabledWarning(user, i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> show tasklists
|
|
# [countsonly|formatjson]
|
|
# gam <UserTypeEntity> print tasklists [todrive <ToDriveAttribute>*]
|
|
# [countsonly | (formatjson [quotechar <Character>])]
|
|
def printShowTasklists(users):
|
|
csvPF = CSVPrintFile(['User', 'id', 'title']) if Act.csvFormat() else None
|
|
if csvPF:
|
|
csvPF.SetNoEscapeChar(True)
|
|
CSVTitle = 'TaskLists'
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
countsOnly = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'countsonly':
|
|
countsOnly = True
|
|
if csvPF:
|
|
csvPF.SetTitles(['User', CSVTitle])
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, svc = buildGAPIServiceObject(API.TASKS, user, i, count)
|
|
if not svc:
|
|
continue
|
|
printGettingAllEntityItemsForWhom(Ent.TASKLIST, user, i, count)
|
|
tasklists = getTaskLists(svc, user, i, count)
|
|
if tasklists is None:
|
|
continue
|
|
jcount = len(tasklists)
|
|
if countsOnly:
|
|
if csvPF:
|
|
csvPF.WriteRowTitles({'User': user, CSVTitle: jcount})
|
|
else:
|
|
printEntityKVList([Ent.USER, user], [CSVTitle, jcount], i, count)
|
|
elif not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.TASKLIST, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for tasklist in tasklists:
|
|
j += 1
|
|
_showTasklist(tasklist, j, jcount, FJQC)
|
|
Ind.Decrement()
|
|
else:
|
|
for tasklist in tasklists:
|
|
row = flattenJSON(tasklist, flattened={'User': user}, skipObjects=TASKLIST_SKIP_OBJECTS, timeObjects=TASKLIST_TIME_OBJECTS)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'User': user, 'id': tasklist['id'], 'title': tasklist.get('title', '')}
|
|
row['JSON'] = json.dumps(cleanJSON(tasklist, skipObjects=TASKLIST_SKIP_OBJECTS, timeObjects=TASKLIST_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
if csvPF:
|
|
csvPF.writeCSVfile(CSVTitle)
|
|
|
|
# gam <UserTypeEntity> import tasklist <Filename> [charset <Charset>]))
|
|
def importTasklist(users):
|
|
filename = getString(Cmd.OB_FILE_NAME)
|
|
encoding = getCharSet()
|
|
try:
|
|
jsonData = json.loads(readFile(filename, encoding=encoding))
|
|
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.JSON_ERROR.format(str(e), filename))
|
|
if jsonData.get('kind', '') != 'tasks#taskLists':
|
|
Cmd.Backup()
|
|
usageErrorExit(f'{"Not a Tasks takeout JSON file"}: {filename}')
|
|
parentIdMap = {}
|
|
cleanData = {'items': []}
|
|
for tasklist in jsonData.get('items', []):
|
|
cleanTasklist = {'title': tasklist['title'], 'items': []}
|
|
for task in tasklist.get('items', []):
|
|
cleanTask = {}
|
|
for field in ['id', 'parent', 'title', 'notes', 'status', 'due', 'completed', 'deleted']:
|
|
if field in task:
|
|
cleanTask[field] = task[field]
|
|
parentIdMap[task['id']] = None
|
|
cleanTasklist['items'].append(cleanTask.copy())
|
|
cleanData['items'].append(cleanTasklist.copy())
|
|
if not cleanData['items']:
|
|
writeStdout('No tasks to import\n')
|
|
return
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, svc = buildGAPIServiceObject(API.TASKS, user, i, count)
|
|
if not svc:
|
|
continue
|
|
if not verifyTasksServiceEnabled(svc, user, i, count):
|
|
continue
|
|
for tasklist in cleanData['items']:
|
|
body = {'title': tasklist['title']}
|
|
result = callGAPI(svc.tasklists(), 'insert',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
body=body)
|
|
tasklistId = result['id']
|
|
for task in tasklist['items']:
|
|
taskId = task.pop('id')
|
|
if 'parent' in task:
|
|
parent = parentIdMap[task.pop('parent')]
|
|
else:
|
|
parent = None
|
|
result = callGAPI(svc.tasks(), 'insert',
|
|
throwReasons=GAPI.TASK_THROW_REASONS,
|
|
tasklist=tasklistId, parent=parent, body=task)
|
|
parentIdMap[taskId] = result['id']
|
|
|
|
def getCRMOrgId():
|
|
setTrueCustomerId()
|
|
_, crm = buildGAPIServiceObject(API.CLOUDRESOURCEMANAGER, None)
|
|
results = callGAPI(crm.organizations(), 'search',
|
|
query=f'directorycustomerid:{GC.Values[GC.CUSTOMER_ID]}',
|
|
pageSize=1, fields='organizations/name')
|
|
orgs = results.get('organizations')
|
|
if not orgs:
|
|
# return nothing and let calling API deal with it
|
|
# since caller knows what GCP role would serve best
|
|
return None
|
|
return orgs[0].get('name')
|
|
|
|
def CAARoleErrorExit(caa):
|
|
sa_email = caa._http.credentials.signer_email
|
|
systemErrorExit(NO_SA_ACCESS_CONTEXT_MANAGER_EDITOR_ROLE_RC,
|
|
f'Please grant service account {sa_email} the Access Context Manager Editor role in your GCP organization.')
|
|
|
|
def normalizeCAALevelName(caa, name):
|
|
if name.startswith('accessPolicies/'):
|
|
return name
|
|
ap_name = getAccessPolicy(caa)
|
|
return f'{ap_name}/accessLevels/{name}'
|
|
|
|
def buildCAAServiceObject():
|
|
_, caa = buildGAPIServiceObject(API.ACCESSCONTEXTMANAGER, None)
|
|
return caa
|
|
|
|
def getAccessPolicy(caa=None):
|
|
if not caa:
|
|
caa = buildCAAServiceObject()
|
|
parent = getCRMOrgId()
|
|
if not parent:
|
|
CAARoleErrorExit(caa)
|
|
try:
|
|
aps = callGAPIpages(caa.accessPolicies(), 'list', 'accessPolicies',
|
|
throwReasons=[GAPI.PERMISSION_DENIED],
|
|
parent=parent, fields='nextPageToken,accessPolicies(name,title)')
|
|
except GAPI.permissionDenied:
|
|
CAARoleErrorExit(caa)
|
|
if not aps:
|
|
systemErrorExit(ACCESS_POLICY_ERROR_RC, 'You don\'t seem to have any access policies. That is odd.')
|
|
elif len(aps) == 1:
|
|
return aps[0]['name']
|
|
for ap in aps:
|
|
if ap.get('title') == 'Access policy created in Cloud Identity Console':
|
|
return ap['name']
|
|
systemErrorExit(ACCESS_POLICY_ERROR_RC, ' Could not find a org level access policy. That is odd.')
|
|
|
|
CAA_OS_TYPE_MAP = {
|
|
'desktopmac': 'DESKTOP_MAC',
|
|
'desktopwindows': 'DESKTOP_WINDOWS',
|
|
'desktoplinux': 'DESKTOP_LINUX',
|
|
'desktopchromeos': 'DESKTOP_CHROME_OS',
|
|
'verifieddesktopchromeos': 'VERIFIED_DESKTOP_CHROME_OS',
|
|
'android': 'ANDROID',
|
|
'ios': 'IOS',
|
|
}
|
|
|
|
def CAABuildOsConstraints():
|
|
consts_obj = []
|
|
for constraint in getString(Cmd.OB_STRING).split(','):
|
|
new_const = {}
|
|
if ':' in constraint:
|
|
osType, new_const['minimumVersion'] = constraint.split(':')
|
|
else:
|
|
osType = constraint
|
|
osType = osType.lower().replace('_', '')
|
|
if osType not in CAA_OS_TYPE_MAP:
|
|
invalidChoiceExit(osType, CAA_OS_TYPE_MAP, True)
|
|
if osType != 'verifieddesktopchromeos':
|
|
new_const['osType'] = CAA_OS_TYPE_MAP[osType]
|
|
else:
|
|
new_const['osType'] = 'DESKTOP_CHROME_OS'
|
|
new_const['requireVerifiedChromeOs'] = True
|
|
consts_obj.append(new_const)
|
|
return consts_obj
|
|
|
|
CAA_ALLOWED_DEVICE_MANAGEMENT_LEVELS_MAP = {
|
|
'basic': 'BASIC',
|
|
'advanced': 'COMPLETE',
|
|
'complete': 'COMPLETE',
|
|
'none': 'NONE',
|
|
}
|
|
CAA_ALLOWED_ENCRYPTIION_STATUS_MAP = {
|
|
'encryptionunsupported': 'ENCRYPTION_UNSUPPORTED',
|
|
'encrypted': 'ENCRYPTED',
|
|
'unencrypted': 'UNENCRYPTED',
|
|
}
|
|
|
|
def CAABuildDevicePolicy():
|
|
device_policy = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'requirescreenlock':
|
|
device_policy['requireScreenLock'] = getBoolean()
|
|
elif myarg == 'allowedencryptionstatuses':
|
|
device_policy['allowedEncryptionStatuses'] = []
|
|
for status in getString(Cmd.OB_STRING).lower().split(','):
|
|
if status not in CAA_ALLOWED_ENCRYPTIION_STATUS_MAP:
|
|
invalidChoiceExit(status, CAA_ALLOWED_ENCRYPTIION_STATUS_MAP, True)
|
|
device_policy['allowedEncryptionStatuses'].append(CAA_ALLOWED_ENCRYPTIION_STATUS_MAP[status])
|
|
elif myarg == 'osconstraints':
|
|
device_policy['osConstraints'] = CAABuildOsConstraints()
|
|
elif myarg == 'alloweddevicemanagementlevels':
|
|
device_policy['allowedDeviceManagementLevels'] = []
|
|
for level in getString(Cmd.OB_STRING).lower().split(','):
|
|
if level not in CAA_ALLOWED_DEVICE_MANAGEMENT_LEVELS_MAP:
|
|
invalidChoiceExit(level, CAA_ALLOWED_DEVICE_MANAGEMENT_LEVELS_MAP, True)
|
|
device_policy['allowedDeviceManagementLevels'].append(CAA_ALLOWED_DEVICE_MANAGEMENT_LEVELS_MAP[level])
|
|
elif myarg == 'requireadminapproval':
|
|
device_policy['requireAdminApproval'] = getBoolean()
|
|
elif myarg == 'requirecorpowned':
|
|
device_policy['requireCorpOwned'] = getBoolean()
|
|
elif myarg == 'enddevicepolicy':
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
return device_policy
|
|
|
|
ISO3166_1_ALPHA_2_CODES = {
|
|
"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ",
|
|
"BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ",
|
|
"CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ",
|
|
"DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR",
|
|
"GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY",
|
|
"HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP",
|
|
"KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY",
|
|
"MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ",
|
|
"NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ",
|
|
"OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW",
|
|
"SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ",
|
|
"TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ",
|
|
"UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW",
|
|
}
|
|
|
|
def validateISO3166_1_alpha2_code(region):
|
|
if region not in ISO3166_1_ALPHA_2_CODES:
|
|
Cmd.Backup()
|
|
expectedArgumentExit(Cmd.ARGUMENT_ERROR_NAMES[Cmd.ARGUMENT_INVALID_CHOICE][1].format(region), Msg.INVALID_REGION)
|
|
|
|
def CAABuildCondition():
|
|
condition = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'ipsubnetworks':
|
|
condition['ipSubnetworks'] = getString(Cmd.OB_STRING_LIST).split(',')
|
|
elif myarg == 'devicepolicy':
|
|
condition['devicePolicy'] = CAABuildDevicePolicy()
|
|
elif myarg == 'requiredaccesslevels':
|
|
condition['requiredAccessLevels'] = getString(Cmd.OB_STRING_LIST).split(',')
|
|
elif myarg == 'negate':
|
|
condition['negate'] = getBoolean()
|
|
elif myarg == 'members':
|
|
condition['members'] = getString(Cmd.OB_STRING_LIST).split(',')
|
|
elif myarg == 'regions':
|
|
condition['regions'] = getString(Cmd.OB_STRING_LIST).upper().split(',')
|
|
for region in condition['regions']:
|
|
validateISO3166_1_alpha2_code(region)
|
|
elif myarg == 'endcondition':
|
|
break
|
|
else:
|
|
unknownArgumentExit()
|
|
return condition
|
|
|
|
CAA_COMBINING_FUNCTIONS_MAP = {
|
|
'and': 'AND',
|
|
'or': 'OR',
|
|
}
|
|
|
|
def CAABuildBasicLevel():
|
|
basic_level = {'conditions': []}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'combiningfunction':
|
|
basic_level['combiningFunction'] = getChoice(CAA_COMBINING_FUNCTIONS_MAP, mapChoice=True)
|
|
elif myarg == 'condition':
|
|
basic_level['conditions'].append(CAABuildCondition())
|
|
else:
|
|
unknownArgumentExit()
|
|
return basic_level
|
|
|
|
def CAABuildLevel(body):
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'basic':
|
|
body['basic'] = CAABuildBasicLevel()
|
|
elif myarg == 'custom':
|
|
body['custom'] = {'expr': {'expression': getString(Cmd.OB_STRING), 'title': 'expr'}}
|
|
elif myarg == 'description':
|
|
body['description'] = getString(Cmd.OB_STRING, minLen=0)
|
|
elif myarg == 'json':
|
|
body.update(getJSON(['name']))
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
# gam create caalevel <String> [description <String>] (basic <CAABasicAttribute>+)|(custom <String>)|<JSONData>
|
|
def doCreateCAALevel():
|
|
caa = buildCAAServiceObject()
|
|
ap_name = getAccessPolicy(caa)
|
|
title = getString(Cmd.OB_STRING).replace(' ', '_')
|
|
allowed_title_chars = string.ascii_letters + string.digits + '_'
|
|
name = ''.join([c for c in title if c in allowed_title_chars])[:50]
|
|
name = f'{ap_name}/accessLevels/{name}'
|
|
body = {'name': name, 'title': title}
|
|
CAABuildLevel(body)
|
|
try:
|
|
callGAPI(caa.accessPolicies().accessLevels(), 'create',
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.FAILED_PRECONDITION, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
parent=ap_name, body=body)
|
|
entityActionPerformed([Ent.CAA_LEVEL, name])
|
|
except (GAPI.alreadyExists, GAPI.failedPrecondition, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.CAA_LEVEL, name], str(e))
|
|
except GAPI.permissionDenied:
|
|
CAARoleErrorExit(caa)
|
|
|
|
# gam update caalevel <CAALevelName> [description <String>] (basic <CAABasicAttribute>+)|(custom <String>)|<JSONData>
|
|
def doUpdateCAALevel():
|
|
caa = buildCAAServiceObject()
|
|
name = normalizeCAALevelName(caa, getString(Cmd.OB_ACCESS_LEVEL_NAME))
|
|
body = {}
|
|
CAABuildLevel(body)
|
|
updateMask = ','.join(body.keys())
|
|
try:
|
|
callGAPI(caa.accessPolicies().accessLevels(), 'patch',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name, updateMask=updateMask, body=body)
|
|
entityActionPerformed([Ent.CAA_LEVEL, name])
|
|
except (GAPI.notFound, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.CAA_LEVEL, name], str(e))
|
|
except GAPI.permissionDenied:
|
|
CAARoleErrorExit(caa)
|
|
|
|
# gam delete caalevel <CAALevelName>
|
|
def doDeleteCAALevel():
|
|
caa = buildCAAServiceObject()
|
|
name = normalizeCAALevelName(caa, getString(Cmd.OB_ACCESS_LEVEL_NAME))
|
|
checkForExtraneousArguments()
|
|
try:
|
|
callGAPI(caa.accessPolicies().accessLevels(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
|
|
name=name)
|
|
entityActionPerformed([Ent.CAA_LEVEL, name])
|
|
except (GAPI.notFound, GAPI.invalidArgument) as e:
|
|
entityActionFailedWarning([Ent.CAA_LEVEL, name], str(e))
|
|
except GAPI.permissionDenied:
|
|
CAARoleErrorExit(caa)
|
|
|
|
# gam print caalevels [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show caalevels
|
|
# [formatjson]
|
|
def doPrintShowCAALevels():
|
|
caa = buildCAAServiceObject()
|
|
ap_name = getAccessPolicy(caa)
|
|
csvPF = CSVPrintFile(['name', 'title']) if Act.csvFormat() else None
|
|
FJQC = FormatJSONQuoteChar(csvPF)
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
try:
|
|
levels = callGAPIpages(caa.accessPolicies().accessLevels(), 'list', 'accessLevels',
|
|
throwReasons=[GAPI.PERMISSION_DENIED],
|
|
parent=ap_name, accessLevelFormat='CEL', fields='*')
|
|
except GAPI.permissionDenied:
|
|
CAARoleErrorExit(caa)
|
|
if not csvPF:
|
|
count = len(levels)
|
|
if not FJQC.formatJSON:
|
|
performActionNumItems(count, Ent.CAA_LEVEL)
|
|
i = 0
|
|
for level in levels:
|
|
i += 1
|
|
printEntity([Ent.CAA_LEVEL, level['name']], i, count)
|
|
Ind.Increment()
|
|
showJSON(None, level)
|
|
Ind.Decrement()
|
|
else:
|
|
for level in levels:
|
|
printLine(json.dumps(cleanJSON(level), ensure_ascii=False, sort_keys=True))
|
|
else:
|
|
for level in levels:
|
|
row = flattenJSON(level)
|
|
if not FJQC.formatJSON:
|
|
csvPF.WriteRowTitles(row)
|
|
elif csvPF.CheckRowTitles(row):
|
|
row = {'name': level['name'], 'title': level['title']}
|
|
row['JSON'] = json.dumps(cleanJSON(level, timeObjects=NOTES_TIME_OBJECTS),
|
|
ensure_ascii=False, sort_keys=True)
|
|
csvPF.WriteRowNoFilter(row)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Context Aware Access Levels')
|
|
|
|
# Command line processing
|
|
|
|
CMD_ACTION = 0
|
|
CMD_FUNCTION = 1
|
|
|
|
# Batch commands
|
|
BATCH_CSV_COMMANDS = {
|
|
Cmd.BATCH_CMD: (Act.PERFORM, doBatch),
|
|
Cmd.CSV_CMD: (Act.PERFORM, doCSV),
|
|
Cmd.CSVTEST_CMD: (Act.PERFORM, doCSVTest),
|
|
Cmd.TBATCH_CMD: (Act.PERFORM, doThreadBatch),
|
|
}
|
|
|
|
# Main commands
|
|
MAIN_COMMANDS = {
|
|
'checkconn': (Act.CHECK, doCheckConnection),
|
|
'checkconnection': (Act.CHECK, doCheckConnection),
|
|
'comment': (Act.COMMENT, doComment),
|
|
'help': (Act.PERFORM, doUsage),
|
|
'list': (Act.LIST, doListType),
|
|
'report': (Act.REPORT, doReport),
|
|
'sendemail': (Act.SENDEMAIL, doSendEmail),
|
|
'version': (Act.PERFORM, doVersion),
|
|
'whatis': (Act.INFO, doWhatIs),
|
|
}
|
|
|
|
# Main commands with objects
|
|
MAIN_ADD_CREATE_FUNCTIONS = {
|
|
Cmd.ARG_ADMIN: doCreateAdmin,
|
|
Cmd.ARG_ADMINROLE: doCreateUpdateAdminRoles,
|
|
Cmd.ARG_ALERTFEEDBACK: doCreateAlertFeedback,
|
|
Cmd.ARG_ALIAS: doCreateUpdateAliases,
|
|
Cmd.ARG_BROWSERTOKEN: doCreateBrowserToken,
|
|
Cmd.ARG_BUILDING: doCreateBuilding,
|
|
Cmd.ARG_CAALEVEL: doCreateCAALevel,
|
|
Cmd.ARG_CHATMESSAGE: doCreateChatMessage,
|
|
Cmd.ARG_CHROMENETWORK: doCreateChromeNetwork,
|
|
Cmd.ARG_CHROMEPOLICYIMAGE: doCreateChromePolicyImage,
|
|
Cmd.ARG_CIGROUP: doCreateCIGroup,
|
|
Cmd.ARG_CONTACT: doCreateDomainContact,
|
|
Cmd.ARG_COURSE: doCreateCourse,
|
|
Cmd.ARG_DATATRANSFER: doCreateDataTransfer,
|
|
Cmd.ARG_DEVICE: doCreateCIDevice,
|
|
Cmd.ARG_DOMAIN: doCreateDomain,
|
|
Cmd.ARG_DOMAINALIAS: doCreateDomainAlias,
|
|
Cmd.ARG_DRIVEFILEACL: doCreateDriveFileACL,
|
|
Cmd.ARG_DRIVELABELPERMISSION: doCreateDriveLabelPermissions,
|
|
Cmd.ARG_FEATURE: doCreateFeature,
|
|
Cmd.ARG_GCPFOLDER: doCreateGCPFolder,
|
|
Cmd.ARG_GCPSERVICEACCOUNT: doCreateGCPServiceAccount,
|
|
Cmd.ARG_GROUP: doCreateGroup,
|
|
Cmd.ARG_GUARDIAN: doInviteGuardian,
|
|
Cmd.ARG_GUARDIANINVITATION: doInviteGuardian,
|
|
Cmd.ARG_INBOUNDSSOASSIGNMENT: doCreateInboundSSOAssignment,
|
|
Cmd.ARG_INBOUNDSSOCREDENTIAL: doCreateInboundSSOCredential,
|
|
Cmd.ARG_INBOUNDSSOPROFILE: doCreateInboundSSOProfile,
|
|
Cmd.ARG_ORG: doCreateOrg,
|
|
Cmd.ARG_PERMISSION: doCreatePermissions,
|
|
Cmd.ARG_PRINTER: doCreatePrinter,
|
|
Cmd.ARG_PROJECT: doCreateProject,
|
|
Cmd.ARG_RESOLDCUSTOMER: doCreateResoldCustomer,
|
|
Cmd.ARG_RESOLDSUBSCRIPTION: doCreateResoldSubscription,
|
|
Cmd.ARG_RESOURCE: doCreateResourceCalendar,
|
|
Cmd.ARG_SAKEY: doCreateSvcAcctKeys,
|
|
Cmd.ARG_SCHEMA: doCreateUpdateUserSchemas,
|
|
Cmd.ARG_SHAREDDRIVE: doCreateSharedDrive,
|
|
Cmd.ARG_SITE: deprecatedDomainSites,
|
|
Cmd.ARG_SITEACL: deprecatedDomainSites,
|
|
Cmd.ARG_SVCACCT: doCreateSvcAcct,
|
|
Cmd.ARG_USER: doCreateUser,
|
|
Cmd.ARG_VAULTEXPORT: doCreateVaultExport,
|
|
Cmd.ARG_VAULTHOLD: doCreateVaultHold,
|
|
Cmd.ARG_VAULTMATTER: doCreateVaultMatter,
|
|
Cmd.ARG_VERIFY: doCreateSiteVerification,
|
|
}
|
|
|
|
MAIN_COMMANDS_WITH_OBJECTS = {
|
|
'add':
|
|
(Act.ADD,
|
|
MAIN_ADD_CREATE_FUNCTIONS
|
|
),
|
|
'approve':
|
|
(Act.APPROVE,
|
|
{Cmd.ARG_DEVICEUSER: doApproveCIDeviceUser,
|
|
}
|
|
),
|
|
'block':
|
|
(Act.BLOCK,
|
|
{Cmd.ARG_DEVICEUSER: doBlockCIDeviceUser,
|
|
}
|
|
),
|
|
'cancel':
|
|
(Act.CANCEL,
|
|
{Cmd.ARG_GUARDIANINVITATION: doCancelGuardianInvitation,
|
|
Cmd.ARG_USERINVITATION: doCIUserInvitationsAction,
|
|
}
|
|
),
|
|
'cancelwipe':
|
|
(Act.CANCEL_WIPE,
|
|
{Cmd.ARG_DEVICE: doCancelWipeCIDevice,
|
|
Cmd.ARG_DEVICEUSER: doCancelWipeCIDeviceUser,
|
|
}
|
|
),
|
|
'check':
|
|
(Act.CHECK,
|
|
{Cmd.ARG_SVCACCT: doCheckUpdateSvcAcct,
|
|
Cmd.ARG_USERINVITATION: doCheckCIUserInvitations,
|
|
Cmd.ARG_ISINVITABLE: doCheckCIUserInvitations,
|
|
Cmd.ARG_ORG: doCheckOrgUnit,
|
|
}
|
|
),
|
|
'clear':
|
|
(Act.CLEAR,
|
|
{Cmd.ARG_CONTACT: doClearDomainContacts,
|
|
}
|
|
),
|
|
'close':
|
|
(Act.CLOSE,
|
|
{Cmd.ARG_VAULTMATTER: doCloseVaultMatter,
|
|
}
|
|
),
|
|
'copy':
|
|
(Act.COPY,
|
|
{Cmd.ARG_SHAREDDRIVEACLS: doCopySyncSharedDriveACLs,
|
|
Cmd.ARG_STORAGEBUCKET: doCopyCloudStorageBucket,
|
|
Cmd.ARG_VAULTEXPORT: doCopyVaultExport,
|
|
}
|
|
),
|
|
'create':
|
|
(Act.CREATE,
|
|
MAIN_ADD_CREATE_FUNCTIONS
|
|
),
|
|
'dedup':
|
|
(Act.DEDUP,
|
|
{Cmd.ARG_CONTACT: doDedupDomainContacts,
|
|
}
|
|
),
|
|
'delete':
|
|
(Act.DELETE,
|
|
{Cmd.ARG_ADMIN: doDeleteAdmin,
|
|
Cmd.ARG_ADMINROLE: doDeleteAdminRole,
|
|
Cmd.ARG_ALIAS: doDeleteAliases,
|
|
Cmd.ARG_ALERT: doDeleteOrUndeleteAlert,
|
|
Cmd.ARG_BROWSER: doDeleteBrowsers,
|
|
Cmd.ARG_BUILDING: doDeleteBuilding,
|
|
Cmd.ARG_CAALEVEL: doDeleteCAALevel,
|
|
Cmd.ARG_CHATMESSAGE: doDeleteChatMessage,
|
|
Cmd.ARG_CHROMENETWORK: doDeleteChromeNetwork,
|
|
Cmd.ARG_CHROMEPOLICY: doDeleteChromePolicy,
|
|
Cmd.ARG_CHROMEPROFILE: doDeleteChromeProfile,
|
|
Cmd.ARG_CIGROUP: doDeleteCIGroups,
|
|
Cmd.ARG_CLASSROOMINVITATION: doDeleteClassroomInvitations,
|
|
Cmd.ARG_CONTACT: doDeleteDomainContacts,
|
|
Cmd.ARG_CONTACTPHOTO: doDeleteDomainContactPhoto,
|
|
Cmd.ARG_COURSE: doDeleteCourse,
|
|
Cmd.ARG_COURSES: doDeleteCourses,
|
|
Cmd.ARG_DEVICE: doDeleteCIDevice,
|
|
Cmd.ARG_DEVICEUSER: doDeleteCIDeviceUser,
|
|
Cmd.ARG_DOMAIN: doDeleteDomain,
|
|
Cmd.ARG_DOMAINALIAS: doDeleteDomainAlias,
|
|
Cmd.ARG_DOMAINCONTACT: doDeleteDomainContacts,
|
|
Cmd.ARG_DRIVEFILEACL: doDeleteDriveFileACLs,
|
|
Cmd.ARG_DRIVELABELPERMISSION: doDeleteDriveLabelPermissions,
|
|
Cmd.ARG_FEATURE: doDeleteFeature,
|
|
Cmd.ARG_GROUP: doDeleteGroups,
|
|
Cmd.ARG_GUARDIAN: doDeleteGuardian,
|
|
Cmd.ARG_INBOUNDSSOASSIGNMENT: doDeleteInboundSSOAssignment,
|
|
Cmd.ARG_INBOUNDSSOCREDENTIAL: doDeleteInboundSSOCredential,
|
|
Cmd.ARG_INBOUNDSSOPROFILE: doDeleteInboundSSOProfile,
|
|
Cmd.ARG_MOBILE: doDeleteMobileDevices,
|
|
Cmd.ARG_ORG: doDeleteOrg,
|
|
Cmd.ARG_ORGS: doDeleteOrgs,
|
|
Cmd.ARG_PERMISSION: doDeletePermissions,
|
|
Cmd.ARG_PRINTER: doDeletePrinter,
|
|
Cmd.ARG_PROJECT: doDeleteProject,
|
|
Cmd.ARG_RESOLDSUBSCRIPTION: doDeleteResoldSubscription,
|
|
Cmd.ARG_RESOURCE: doDeleteResourceCalendar,
|
|
Cmd.ARG_RESOURCES: doDeleteResourceCalendars,
|
|
Cmd.ARG_SAKEY: doDeleteSvcAcctKeys,
|
|
Cmd.ARG_SCHEMA: doDeleteUserSchemas,
|
|
Cmd.ARG_SHAREDDRIVE: doDeleteSharedDrive,
|
|
Cmd.ARG_SITEACL: deprecatedDomainSites,
|
|
Cmd.ARG_SVCACCT: doDeleteSvcAcct,
|
|
Cmd.ARG_USER: doDeleteUser,
|
|
Cmd.ARG_USERS: doDeleteUsers,
|
|
Cmd.ARG_VAULTEXPORT: doDeleteVaultExport,
|
|
Cmd.ARG_VAULTHOLD: doDeleteVaultHold,
|
|
Cmd.ARG_VAULTMATTER: doDeleteVaultMatter,
|
|
}
|
|
),
|
|
'download':
|
|
(Act.DOWNLOAD,
|
|
{Cmd.ARG_STORAGEBUCKET: doDownloadCloudStorageBucket,
|
|
Cmd.ARG_STORAGEFILE: doDownloadCloudStorageFile,
|
|
Cmd.ARG_VAULTEXPORT: doDownloadVaultExport,
|
|
}
|
|
),
|
|
'enable':
|
|
(Act.ENABLE,
|
|
{Cmd.ARG_API: doEnableAPIs,
|
|
}
|
|
),
|
|
'get':
|
|
(Act.DOWNLOAD,
|
|
{Cmd.ARG_CONTACTPHOTO: doGetDomainContactPhoto,
|
|
Cmd.ARG_DEVICEFILE: doGetCrOSDeviceFiles,
|
|
}
|
|
),
|
|
'getcommand':
|
|
(Act.GET_COMMAND_RESULT,
|
|
{Cmd.ARG_CROS: doGetCommandResultCrOSDevices,
|
|
}
|
|
),
|
|
'hide':
|
|
(Act.HIDE,
|
|
{Cmd.ARG_SHAREDDRIVE: doHideUnhideSharedDrive,
|
|
}
|
|
),
|
|
'info':
|
|
(Act.INFO,
|
|
{Cmd.ARG_ADMINROLE: doInfoAdminRole,
|
|
Cmd.ARG_ALERT: doInfoAlert,
|
|
Cmd.ARG_ALIAS: doInfoAliases,
|
|
Cmd.ARG_BUILDING: doInfoBuilding,
|
|
Cmd.ARG_BROWSER: doInfoBrowsers,
|
|
Cmd.ARG_CHATEVENT: doInfoChatEvent,
|
|
Cmd.ARG_CHATMEMBER: doInfoChatMember,
|
|
Cmd.ARG_CHATMESSAGE: doInfoChatMessage,
|
|
Cmd.ARG_CHATSPACE: doInfoChatSpace,
|
|
Cmd.ARG_CHROMEAPP: doInfoChromeApp,
|
|
Cmd.ARG_CHROMEPROFILE: doInfoChromeProfile,
|
|
Cmd.ARG_CHROMESCHEMA: doInfoChromePolicySchemas,
|
|
Cmd.ARG_CIGROUP: doInfoCIGroups,
|
|
Cmd.ARG_CIGROUPMEMBERS: doInfoCIGroupMembers,
|
|
Cmd.ARG_CIPOLICY: doInfoCIPolicies,
|
|
Cmd.ARG_CONTACT: doInfoDomainContacts,
|
|
Cmd.ARG_COURSE: doInfoCourse,
|
|
Cmd.ARG_COURSES: doInfoCourses,
|
|
Cmd.ARG_CROS: doInfoCrOSDevices,
|
|
Cmd.ARG_CROSTELEMETRY: doInfoPrintShowCrOSTelemetry,
|
|
Cmd.ARG_CURRENTPROJECTID: doInfoCurrentProjectId,
|
|
Cmd.ARG_CUSTOMER: doInfoCustomer,
|
|
Cmd.ARG_DATATRANSFER: doInfoDataTransfer,
|
|
Cmd.ARG_DEVICE: doInfoCIDevice,
|
|
Cmd.ARG_DEVICEUSER: doInfoCIDeviceUser,
|
|
Cmd.ARG_DEVICEUSERSTATE: doInfoCIDeviceUserState,
|
|
Cmd.ARG_DOMAIN: doInfoDomain,
|
|
Cmd.ARG_DOMAINALIAS: doInfoDomainAlias,
|
|
Cmd.ARG_DOMAINCONTACT: doInfoDomainContacts,
|
|
Cmd.ARG_DRIVEFILEACL: doInfoDriveFileACLs,
|
|
Cmd.ARG_DRIVELABEL: doInfoDriveLabels,
|
|
Cmd.ARG_INSTANCE: doInfoInstance,
|
|
Cmd.ARG_GAL: doInfoGAL,
|
|
Cmd.ARG_GROUP: doInfoGroups,
|
|
Cmd.ARG_GROUPMEMBERS: doInfoGroupMembers,
|
|
Cmd.ARG_INBOUNDSSOASSIGNMENT: doInfoInboundSSOAssignment,
|
|
Cmd.ARG_INBOUNDSSOCREDENTIAL: doInfoInboundSSOCredential,
|
|
Cmd.ARG_INBOUNDSSOPROFILE: doInfoInboundSSOProfile,
|
|
Cmd.ARG_MOBILE: doInfoMobileDevices,
|
|
Cmd.ARG_ORG: doInfoOrg,
|
|
Cmd.ARG_ORGS: doInfoOrgs,
|
|
Cmd.ARG_PEOPLEPROFILE: doInfoDomainPeopleProfile,
|
|
Cmd.ARG_PEOPLECONTACT: doInfoDomainPeopleContacts,
|
|
Cmd.ARG_PRINTER: doInfoPrinter,
|
|
Cmd.ARG_RESOLDCUSTOMER: doInfoResoldCustomer,
|
|
Cmd.ARG_RESOLDSUBSCRIPTION: doInfoResoldSubscription,
|
|
Cmd.ARG_RESOURCE: doInfoResourceCalendar,
|
|
Cmd.ARG_RESOURCES: doInfoResourceCalendars,
|
|
Cmd.ARG_SCHEMA: doInfoUserSchemas,
|
|
Cmd.ARG_SHAREDDRIVE: doInfoSharedDrive,
|
|
Cmd.ARG_SITE: deprecatedDomainSites,
|
|
Cmd.ARG_SITEACL: deprecatedDomainSites,
|
|
Cmd.ARG_USER: doInfoUser,
|
|
Cmd.ARG_USERS: doInfoUsers,
|
|
Cmd.ARG_USERINVITATION: doInfoCIUserInvitations,
|
|
Cmd.ARG_VAULTEXPORT: doInfoVaultExport,
|
|
Cmd.ARG_VAULTHOLD: doInfoVaultHold,
|
|
Cmd.ARG_VAULTMATTER: doInfoVaultMatter,
|
|
Cmd.ARG_VAULTQUERY: doInfoVaultQuery,
|
|
Cmd.ARG_VERIFY: doInfoSiteVerification,
|
|
}
|
|
),
|
|
'issuecommand':
|
|
(Act.ISSUE_COMMAND,
|
|
{Cmd.ARG_CROS: doIssueCommandCrOSDevices,
|
|
}
|
|
),
|
|
'move':
|
|
(Act.MOVE,
|
|
{Cmd.ARG_BROWSER: doMoveBrowsers,
|
|
}
|
|
),
|
|
'print':
|
|
(Act.PRINT,
|
|
{Cmd.ARG_ADDRESSES: doPrintAddresses,
|
|
Cmd.ARG_ADMINROLE: doPrintShowAdminRoles,
|
|
Cmd.ARG_ADMIN: doPrintShowAdmins,
|
|
Cmd.ARG_ALERT: doPrintShowAlerts,
|
|
Cmd.ARG_ALERTFEEDBACK: doPrintShowAlertFeedback,
|
|
Cmd.ARG_ALIAS: doPrintAliases,
|
|
Cmd.ARG_BROWSER: doPrintShowBrowsers,
|
|
Cmd.ARG_BROWSERTOKEN: doPrintShowBrowserTokens,
|
|
Cmd.ARG_BUILDING: doPrintShowBuildings,
|
|
Cmd.ARG_CAALEVEL: doPrintShowCAALevels,
|
|
Cmd.ARG_CHANNELCUSTOMER: doPrintShowChannelCustomers,
|
|
Cmd.ARG_CHANNELCUSTOMERENTITLEMENT: doPrintShowChannelCustomerEntitlements,
|
|
Cmd.ARG_CHANNELOFFER: doPrintShowChannelOffers,
|
|
Cmd.ARG_CHANNELPRODUCT: doPrintShowChannelProducts,
|
|
Cmd.ARG_CHANNELSKU: doPrintShowChannelSKUs,
|
|
Cmd.ARG_CHATMEMBER: doPrintShowChatMembers,
|
|
Cmd.ARG_CHATSPACE: doPrintShowChatSpaces,
|
|
Cmd.ARG_CHROMEAPP: doPrintShowChromeApps,
|
|
Cmd.ARG_CHROMEAPPDEVICES: doPrintShowChromeAppDevices,
|
|
Cmd.ARG_CHROMEAUES: doPrintShowChromeAues,
|
|
Cmd.ARG_CHROMEHISTORY: doPrintShowChromeHistory,
|
|
Cmd.ARG_CHROMENEEDSATTN: doPrintShowChromeNeedsAttn,
|
|
Cmd.ARG_CHROMEPOLICY: doPrintShowChromePolicies,
|
|
Cmd.ARG_CHROMEPROFILE: doPrintShowChromeProfiles,
|
|
Cmd.ARG_CHROMESCHEMA: doPrintShowChromeSchemas,
|
|
Cmd.ARG_CHROMESNVALIDITY: doPrintChromeSnValidity,
|
|
Cmd.ARG_CHROMEVERSIONS: doPrintShowChromeVersions,
|
|
Cmd.ARG_CIGROUP: doPrintCIGroups,
|
|
Cmd.ARG_CIGROUPMEMBERS: doPrintCIGroupMembers,
|
|
Cmd.ARG_CIPOLICY: doPrintShowCIPolicies,
|
|
Cmd.ARG_CLASSROOMINVITATION: doPrintShowClassroomInvitations,
|
|
Cmd.ARG_CONTACT: doPrintShowDomainContacts,
|
|
Cmd.ARG_COURSE: doPrintCourses,
|
|
Cmd.ARG_COURSES: doPrintCourses,
|
|
Cmd.ARG_COURSEANNOUNCEMENTS: doPrintCourseAnnouncements,
|
|
Cmd.ARG_COURSEMATERIALS: doPrintCourseMaterials,
|
|
Cmd.ARG_COURSEPARTICIPANTS: doPrintCourseParticipants,
|
|
Cmd.ARG_COURSESUBMISSIONS: doPrintCourseSubmissions,
|
|
Cmd.ARG_COURSETOPICS: doPrintCourseTopics,
|
|
Cmd.ARG_COURSEWORK: doPrintCourseWork,
|
|
Cmd.ARG_CROS: doPrintCrOSDevices,
|
|
Cmd.ARG_CROSACTIVITY: doPrintCrOSActivity,
|
|
Cmd.ARG_CROSTELEMETRY: doInfoPrintShowCrOSTelemetry,
|
|
Cmd.ARG_DATATRANSFER: doPrintShowDataTransfers,
|
|
Cmd.ARG_DEVICE: doPrintCIDevices,
|
|
Cmd.ARG_DEVICEUSER: doPrintCIDeviceUsers,
|
|
Cmd.ARG_DOMAIN: doPrintShowDomains,
|
|
Cmd.ARG_DOMAINALIAS: doPrintShowDomainAliases,
|
|
Cmd.ARG_DOMAINCONTACT: doPrintShowDomainContacts,
|
|
Cmd.ARG_DRIVEFILEACL: doPrintShowDriveFileACLs,
|
|
Cmd.ARG_DRIVELABEL: doPrintShowDriveLabels,
|
|
Cmd.ARG_DRIVELABELPERMISSION: doPrintShowDriveLabelPermissions,
|
|
Cmd.ARG_FEATURE: doPrintShowFeatures,
|
|
Cmd.ARG_GAL: doPrintShowGAL,
|
|
Cmd.ARG_GROUP: doPrintGroups,
|
|
Cmd.ARG_GROUPMEMBERS: doPrintGroupMembers,
|
|
Cmd.ARG_GROUPTREE: doPrintShowGroupTree,
|
|
Cmd.ARG_GUARDIAN: doPrintShowGuardians,
|
|
Cmd.ARG_INBOUNDSSOASSIGNMENT: doPrintShowInboundSSOAssignments,
|
|
Cmd.ARG_INBOUNDSSOCREDENTIAL: doPrintShowInboundSSOCredentials,
|
|
Cmd.ARG_INBOUNDSSOPROFILE: doPrintShowInboundSSOProfiles,
|
|
Cmd.ARG_LICENSE: doPrintLicenses,
|
|
Cmd.ARG_MOBILE: doPrintMobileDevices,
|
|
Cmd.ARG_ORG: doPrintOrgs,
|
|
Cmd.ARG_ORGS: doPrintOrgs,
|
|
Cmd.ARG_ORGUNITSHAREDDRIVE: doPrintShowOrgunitSharedDrives,
|
|
Cmd.ARG_OWNERSHIP: doPrintShowOwnership,
|
|
Cmd.ARG_PEOPLECONTACT: doPrintShowDomainPeopleContacts,
|
|
Cmd.ARG_PEOPLEPROFILE: doPrintShowDomainPeopleProfiles,
|
|
Cmd.ARG_PRINTER: doPrintShowPrinters,
|
|
Cmd.ARG_PRINTERMODEL: doPrintShowPrinterModels,
|
|
Cmd.ARG_PRIVILEGES: doPrintShowPrivileges,
|
|
Cmd.ARG_PROJECT: doPrintShowProjects,
|
|
Cmd.ARG_RESOLDSUBSCRIPTION: doPrintShowResoldSubscriptions,
|
|
Cmd.ARG_RESOURCE: doPrintShowResourceCalendars,
|
|
Cmd.ARG_RESOURCES: doPrintShowResourceCalendars,
|
|
Cmd.ARG_SCHEMA: doPrintShowUserSchemas,
|
|
Cmd.ARG_SHAREDDRIVE: doPrintShowSharedDrives,
|
|
Cmd.ARG_SHAREDDRIVEACLS: doPrintShowSharedDriveACLs,
|
|
Cmd.ARG_SITE: deprecatedDomainSites,
|
|
Cmd.ARG_SITEACL: deprecatedDomainSites,
|
|
Cmd.ARG_SITEACTIVITY: deprecatedDomainSites,
|
|
Cmd.ARG_SVCACCT: doPrintShowSvcAccts,
|
|
Cmd.ARG_TOKEN: doPrintShowTokens,
|
|
Cmd.ARG_TRANSFERAPPS: doShowTransferApps,
|
|
Cmd.ARG_USER: doPrintUsers,
|
|
Cmd.ARG_USERS: doPrintUsers,
|
|
Cmd.ARG_USERCOUNTSBYORGUNIT: doPrintUserCountsByOrgUnit,
|
|
Cmd.ARG_USERINVITATION: doPrintShowCIUserInvitations,
|
|
Cmd.ARG_VAULTCOUNT: doPrintVaultCounts,
|
|
Cmd.ARG_VAULTEXPORT: doPrintShowVaultExports,
|
|
Cmd.ARG_VAULTHOLD: doPrintShowVaultHolds,
|
|
Cmd.ARG_VAULTMATTER: doPrintShowVaultMatters,
|
|
Cmd.ARG_VAULTQUERY: doPrintShowVaultQueries,
|
|
}
|
|
),
|
|
'remove':
|
|
(Act.REMOVE,
|
|
{Cmd.ARG_ALIAS: doRemoveAliases,
|
|
Cmd.ARG_DRIVELABELPERMISSION: doDeleteDriveLabelPermissions,
|
|
}
|
|
),
|
|
'reopen':
|
|
(Act.REOPEN,
|
|
{Cmd.ARG_VAULTMATTER: doReopenVaultMatter,
|
|
}
|
|
),
|
|
'replace':
|
|
(Act.UPDATE,
|
|
{Cmd.ARG_SAKEY: doReplaceSvcAcctKeys,
|
|
}
|
|
),
|
|
'rotate':
|
|
(Act.UPDATE,
|
|
{Cmd.ARG_SAKEY: doProcessSvcAcctKeys,
|
|
}
|
|
),
|
|
'revoke':
|
|
(Act.REVOKE,
|
|
{Cmd.ARG_BROWSERTOKEN: doRevokeBrowserToken,
|
|
}
|
|
),
|
|
'send':
|
|
(Act.SEND,
|
|
{Cmd.ARG_USERINVITATION: doCIUserInvitationsAction,
|
|
}
|
|
),
|
|
'setup':
|
|
(Act.SETUP,
|
|
{Cmd.ARG_CHAT: doSetupChat,
|
|
}
|
|
),
|
|
'show':
|
|
(Act.SHOW,
|
|
{Cmd.ARG_ADMINROLE: doPrintShowAdminRoles,
|
|
Cmd.ARG_ADMIN: doPrintShowAdmins,
|
|
Cmd.ARG_ALERT: doPrintShowAlerts,
|
|
Cmd.ARG_ALERTFEEDBACK: doPrintShowAlertFeedback,
|
|
Cmd.ARG_BROWSER: doPrintShowBrowsers,
|
|
Cmd.ARG_BROWSERTOKEN: doPrintShowBrowserTokens,
|
|
Cmd.ARG_BUILDING: doPrintShowBuildings,
|
|
Cmd.ARG_CAALEVEL: doPrintShowCAALevels,
|
|
Cmd.ARG_CHANNELCUSTOMER: doPrintShowChannelCustomers,
|
|
Cmd.ARG_CHANNELCUSTOMERENTITLEMENT: doPrintShowChannelCustomerEntitlements,
|
|
Cmd.ARG_CHANNELOFFER: doPrintShowChannelOffers,
|
|
Cmd.ARG_CHANNELPRODUCT: doPrintShowChannelProducts,
|
|
Cmd.ARG_CHANNELSKU: doPrintShowChannelSKUs,
|
|
Cmd.ARG_CHATMEMBER: doPrintShowChatMembers,
|
|
Cmd.ARG_CHATSPACE: doPrintShowChatSpaces,
|
|
Cmd.ARG_CHROMEAPP: doPrintShowChromeApps,
|
|
Cmd.ARG_CHROMEAPPDEVICES: doPrintShowChromeAppDevices,
|
|
Cmd.ARG_CHROMEAUES: doPrintShowChromeAues,
|
|
Cmd.ARG_CHROMEHISTORY: doPrintShowChromeHistory,
|
|
Cmd.ARG_CHROMENEEDSATTN: doPrintShowChromeNeedsAttn,
|
|
Cmd.ARG_CHROMEPOLICY: doPrintShowChromePolicies,
|
|
Cmd.ARG_CHROMEPROFILE: doPrintShowChromeProfiles,
|
|
Cmd.ARG_CHROMESCHEMA: doPrintShowChromeSchemas,
|
|
Cmd.ARG_CHROMEVERSIONS: doPrintShowChromeVersions,
|
|
Cmd.ARG_CIGROUPMEMBERS: doShowCIGroupMembers,
|
|
Cmd.ARG_CIPOLICY: doPrintShowCIPolicies,
|
|
Cmd.ARG_CLASSROOMINVITATION: doPrintShowClassroomInvitations,
|
|
Cmd.ARG_CONTACT: doPrintShowDomainContacts,
|
|
Cmd.ARG_CROSTELEMETRY: doInfoPrintShowCrOSTelemetry,
|
|
Cmd.ARG_DATATRANSFER: doPrintShowDataTransfers,
|
|
Cmd.ARG_DOMAIN: doPrintShowDomains,
|
|
Cmd.ARG_DOMAINALIAS: doPrintShowDomainAliases,
|
|
Cmd.ARG_DOMAINCONTACT: doPrintShowDomainContacts,
|
|
Cmd.ARG_DRIVEFILEACL: doPrintShowDriveFileACLs,
|
|
Cmd.ARG_DRIVELABEL: doPrintShowDriveLabels,
|
|
Cmd.ARG_DRIVELABELPERMISSION: doPrintShowDriveLabelPermissions,
|
|
Cmd.ARG_FEATURE: doPrintShowFeatures,
|
|
Cmd.ARG_GAL: doPrintShowGAL,
|
|
Cmd.ARG_GROUPMEMBERS: doShowGroupMembers,
|
|
Cmd.ARG_GROUPTREE: doPrintShowGroupTree,
|
|
Cmd.ARG_GUARDIAN: doPrintShowGuardians,
|
|
Cmd.ARG_INBOUNDSSOASSIGNMENT: doPrintShowInboundSSOAssignments,
|
|
Cmd.ARG_INBOUNDSSOCREDENTIAL: doPrintShowInboundSSOCredentials,
|
|
Cmd.ARG_INBOUNDSSOPROFILE: doPrintShowInboundSSOProfiles,
|
|
Cmd.ARG_ORGUNITSHAREDDRIVE: doPrintShowOrgunitSharedDrives,
|
|
Cmd.ARG_LICENSE: doShowLicenses,
|
|
Cmd.ARG_ORGTREE: doShowOrgTree,
|
|
Cmd.ARG_OWNERSHIP: doPrintShowOwnership,
|
|
Cmd.ARG_PEOPLECONTACT: doPrintShowDomainPeopleContacts,
|
|
Cmd.ARG_PEOPLEPROFILE: doPrintShowDomainPeopleProfiles,
|
|
Cmd.ARG_PRINTER: doPrintShowPrinters,
|
|
Cmd.ARG_PRINTERMODEL: doPrintShowPrinterModels,
|
|
Cmd.ARG_PRIVILEGES: doPrintShowPrivileges,
|
|
Cmd.ARG_PROJECT: doPrintShowProjects,
|
|
Cmd.ARG_RESOLDSUBSCRIPTION: doPrintShowResoldSubscriptions,
|
|
Cmd.ARG_RESOURCE: doPrintShowResourceCalendars,
|
|
Cmd.ARG_RESOURCES: doPrintShowResourceCalendars,
|
|
Cmd.ARG_SAKEY: doShowSvcAcctKeys,
|
|
Cmd.ARG_SCHEMA: doPrintShowUserSchemas,
|
|
Cmd.ARG_SHAREDDRIVE: doPrintShowSharedDrives,
|
|
Cmd.ARG_SHAREDDRIVEACLS: doPrintShowSharedDriveACLs,
|
|
Cmd.ARG_SHAREDDRIVEINFO: doInfoSharedDrive,
|
|
Cmd.ARG_SHAREDDRIVETHEMES: doShowSharedDriveThemes,
|
|
Cmd.ARG_SITE: deprecatedDomainSites,
|
|
Cmd.ARG_SITEACL: deprecatedDomainSites,
|
|
Cmd.ARG_SVCACCT: doPrintShowSvcAccts,
|
|
Cmd.ARG_TOKEN: doPrintShowTokens,
|
|
Cmd.ARG_TRANSFERAPPS: doShowTransferApps,
|
|
Cmd.ARG_USERINVITATION: doPrintShowCIUserInvitations,
|
|
Cmd.ARG_VAULTEXPORT: doPrintShowVaultExports,
|
|
Cmd.ARG_VAULTHOLD: doPrintShowVaultHolds,
|
|
Cmd.ARG_VAULTMATTER: doPrintShowVaultMatters,
|
|
Cmd.ARG_VAULTQUERY: doPrintShowVaultQueries,
|
|
}
|
|
),
|
|
'suspend':
|
|
(Act.SUSPEND,
|
|
{Cmd.ARG_USER: doSuspendUnsuspendUser,
|
|
Cmd.ARG_USERS: doSuspendUnsuspendUsers,
|
|
}
|
|
),
|
|
'sync':
|
|
(Act.SYNC,
|
|
{Cmd.ARG_DEVICE: doSyncCIDevices,
|
|
Cmd.ARG_SHAREDDRIVEACLS: copySyncSharedDriveACLs,
|
|
}
|
|
),
|
|
'unhide':
|
|
(Act.UNHIDE,
|
|
{Cmd.ARG_SHAREDDRIVE: doHideUnhideSharedDrive,
|
|
}
|
|
),
|
|
'update':
|
|
(Act.UPDATE,
|
|
{Cmd.ARG_ADMINROLE: doCreateUpdateAdminRoles,
|
|
Cmd.ARG_ALIAS: doCreateUpdateAliases,
|
|
Cmd.ARG_BROWSER: doUpdateBrowsers,
|
|
Cmd.ARG_BUILDING: doUpdateBuilding,
|
|
Cmd.ARG_CAALEVEL: doUpdateCAALevel,
|
|
Cmd.ARG_CHATMESSAGE: doUpdateChatMessage,
|
|
Cmd.ARG_CHROMEPOLICY: doUpdateChromePolicy,
|
|
Cmd.ARG_CIGROUP: doUpdateCIGroups,
|
|
Cmd.ARG_CONTACT: doUpdateDomainContacts,
|
|
Cmd.ARG_CONTACTPHOTO: doUpdateDomainContactPhoto,
|
|
Cmd.ARG_COURSE: doUpdateCourse,
|
|
Cmd.ARG_COURSES: doUpdateCourses,
|
|
Cmd.ARG_CROS: doUpdateCrOSDevices,
|
|
Cmd.ARG_CUSTOMER: doUpdateCustomer,
|
|
Cmd.ARG_DEVICE: doUpdateCIDevice,
|
|
Cmd.ARG_DEVICEUSER: doUpdateCIDeviceUser,
|
|
Cmd.ARG_DEVICEUSERSTATE: doUpdateCIDeviceUserState,
|
|
Cmd.ARG_DOMAIN: doUpdateDomain,
|
|
Cmd.ARG_DRIVEFILEACL: doUpdateDriveFileACLs,
|
|
Cmd.ARG_FEATURE: doUpdateFeature,
|
|
Cmd.ARG_GROUP: doUpdateGroups,
|
|
Cmd.ARG_INBOUNDSSOASSIGNMENT: doUpdateInboundSSOAssignment,
|
|
Cmd.ARG_INBOUNDSSOPROFILE: doUpdateInboundSSOProfile,
|
|
Cmd.ARG_MOBILE: doUpdateMobileDevices,
|
|
Cmd.ARG_ORG: doUpdateOrg,
|
|
Cmd.ARG_ORGS: doUpdateOrgs,
|
|
Cmd.ARG_PRINTER: doUpdatePrinter,
|
|
Cmd.ARG_PROJECT: doUpdateProject,
|
|
Cmd.ARG_RESOLDCUSTOMER: doUpdateResoldCustomer,
|
|
Cmd.ARG_RESOLDSUBSCRIPTION: doUpdateResoldSubscription,
|
|
Cmd.ARG_RESOURCE: doUpdateResourceCalendar,
|
|
Cmd.ARG_RESOURCES: doUpdateResourceCalendars,
|
|
Cmd.ARG_SAKEY: doUpdateSvcAcctKeys,
|
|
Cmd.ARG_SCHEMA: doCreateUpdateUserSchemas,
|
|
Cmd.ARG_SHAREDDRIVE: doUpdateSharedDrive,
|
|
Cmd.ARG_SITE: deprecatedDomainSites,
|
|
Cmd.ARG_SITEACL: deprecatedDomainSites,
|
|
Cmd.ARG_SVCACCT: doCheckUpdateSvcAcct,
|
|
Cmd.ARG_USER: doUpdateUser,
|
|
Cmd.ARG_USERS: doUpdateUsers,
|
|
Cmd.ARG_VAULTHOLD: doUpdateVaultHold,
|
|
Cmd.ARG_VAULTMATTER: doUpdateVaultMatter,
|
|
Cmd.ARG_VERIFY: doUpdateSiteVerification,
|
|
}
|
|
),
|
|
'undelete':
|
|
(Act.UNDELETE,
|
|
{Cmd.ARG_ALERT: doDeleteOrUndeleteAlert,
|
|
Cmd.ARG_USER: doUndeleteUser,
|
|
Cmd.ARG_USERS: doUndeleteUsers,
|
|
Cmd.ARG_VAULTMATTER: doUndeleteVaultMatter,
|
|
}
|
|
),
|
|
'unsuspend':
|
|
(Act.UNSUSPEND,
|
|
{Cmd.ARG_USER: doSuspendUnsuspendUser,
|
|
Cmd.ARG_USERS: doSuspendUnsuspendUsers,
|
|
}
|
|
),
|
|
'upload':
|
|
(Act.USE,
|
|
{Cmd.ARG_SAKEY: doUploadSvcAcctKeys,
|
|
}
|
|
),
|
|
'use':
|
|
(Act.USE,
|
|
{Cmd.ARG_PROJECT: doUseProject,
|
|
}
|
|
),
|
|
'wipe':
|
|
(Act.WIPE,
|
|
{Cmd.ARG_DEVICE: doWipeCIDevice,
|
|
Cmd.ARG_DEVICEUSER: doWipeCIDeviceUser,
|
|
}
|
|
),
|
|
'yubikey':
|
|
(Act.RESET_YUBIKEY_PIV,
|
|
{Cmd.ARG_RESETPIV: doResetYubiKeyPIV,
|
|
}
|
|
),
|
|
}
|
|
|
|
MAIN_COMMANDS_OBJ_ALIASES = {
|
|
Cmd.ARG_ADMINS: Cmd.ARG_ADMIN,
|
|
Cmd.ARG_ADMINROLES: Cmd.ARG_ADMINROLE,
|
|
Cmd.ARG_ALERTFEEDBACKS: Cmd.ARG_ALERTFEEDBACK,
|
|
Cmd.ARG_ALERTS: Cmd.ARG_ALERT,
|
|
Cmd.ARG_ALERTSFEEDBACK: Cmd.ARG_ALERTFEEDBACK,
|
|
Cmd.ARG_ALIASDOMAIN: Cmd.ARG_DOMAINALIAS,
|
|
Cmd.ARG_ALIASDOMAINS: Cmd.ARG_DOMAINALIAS,
|
|
Cmd.ARG_ALIASES: Cmd.ARG_ALIAS,
|
|
Cmd.ARG_APIS: Cmd.ARG_API,
|
|
Cmd.ARG_APIPROJECT: Cmd.ARG_PROJECT,
|
|
Cmd.ARG_APPDETAILS: Cmd.ARG_CHROMEAPP,
|
|
Cmd.ARG_BROWSERS: Cmd.ARG_BROWSER,
|
|
Cmd.ARG_BROWSERTOKENS: Cmd.ARG_BROWSERTOKEN,
|
|
Cmd.ARG_BUCKET: Cmd.ARG_STORAGEBUCKET,
|
|
Cmd.ARG_BUCKETS: Cmd.ARG_STORAGEBUCKET,
|
|
Cmd.ARG_BUILDINGS: Cmd.ARG_BUILDING,
|
|
Cmd.ARG_CAALEVELS: Cmd.ARG_CAALEVEL,
|
|
Cmd.ARG_CHATMEMBERS: Cmd.ARG_CHATMEMBER,
|
|
Cmd.ARG_CHANNELCUSTOMERS: Cmd.ARG_CHANNELCUSTOMER,
|
|
Cmd.ARG_CHANNELCUSTOMERENTITLEMENTS: Cmd.ARG_CHANNELCUSTOMERENTITLEMENT,
|
|
Cmd.ARG_CHANNELOFFERS: Cmd.ARG_CHANNELOFFER,
|
|
Cmd.ARG_CHANNELPRODUCTS: Cmd.ARG_CHANNELPRODUCT,
|
|
Cmd.ARG_CHANNELSKUS: Cmd.ARG_CHANNELSKU,
|
|
Cmd.ARG_CHATSPACES: Cmd.ARG_CHATSPACE,
|
|
Cmd.ARG_CHROMEAPPS: Cmd.ARG_CHROMEAPP,
|
|
Cmd.ARG_CHROMENETWORKS: Cmd.ARG_CHROMENETWORK,
|
|
Cmd.ARG_CHROMEPOLICIES: Cmd.ARG_CHROMEPOLICY,
|
|
Cmd.ARG_CHROMEPROFILES: Cmd.ARG_CHROMEPROFILE,
|
|
Cmd.ARG_CHROMESCHEMAS: Cmd.ARG_CHROMESCHEMA,
|
|
Cmd.ARG_CIGROUPS: Cmd.ARG_CIGROUP,
|
|
Cmd.ARG_CIGROUPSMEMBERS: Cmd.ARG_CIGROUPMEMBERS,
|
|
Cmd.ARG_CIMEMBER: Cmd.ARG_CIGROUPMEMBERS,
|
|
Cmd.ARG_CIMEMBERS: Cmd.ARG_CIGROUPMEMBERS,
|
|
Cmd.ARG_CIPOLICIES: Cmd.ARG_CIPOLICY,
|
|
Cmd.ARG_CLASSIFICATIONLABEL: Cmd.ARG_DRIVELABEL,
|
|
Cmd.ARG_CLASSIFICATIONLABELS: Cmd.ARG_DRIVELABEL,
|
|
Cmd.ARG_CLASSIFICATIONLABELPERMISSION: Cmd.ARG_DRIVELABELPERMISSION,
|
|
Cmd.ARG_CLASSIFICATIONLABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
|
|
Cmd.ARG_CLASS: Cmd.ARG_COURSE,
|
|
Cmd.ARG_CLASSES: Cmd.ARG_COURSES,
|
|
Cmd.ARG_CLASSPARTICIPANTS: Cmd.ARG_COURSEPARTICIPANTS,
|
|
Cmd.ARG_CLASSROOMINVITATIONS: Cmd.ARG_CLASSROOMINVITATION,
|
|
Cmd.ARG_CONTACTS: Cmd.ARG_CONTACT,
|
|
Cmd.ARG_CONTACTPHOTOS: Cmd.ARG_CONTACTPHOTO,
|
|
Cmd.ARG_CROSES: Cmd.ARG_CROS,
|
|
Cmd.ARG_DATATRANSFERS: Cmd.ARG_DATATRANSFER,
|
|
Cmd.ARG_DEVICES: Cmd.ARG_DEVICE,
|
|
Cmd.ARG_DEVICEFILES: Cmd.ARG_DEVICEFILE,
|
|
Cmd.ARG_DEVICEUSERS: Cmd.ARG_DEVICEUSER,
|
|
Cmd.ARG_DOMAINS: Cmd.ARG_DOMAIN,
|
|
Cmd.ARG_DOMAINALIASES: Cmd.ARG_DOMAINALIAS,
|
|
Cmd.ARG_DOMAINCONTACT: Cmd.ARG_PEOPLECONTACT,
|
|
Cmd.ARG_DOMAINCONTACTS: Cmd.ARG_PEOPLECONTACT,
|
|
Cmd.ARG_DOMAINPROFILES: Cmd.ARG_PEOPLEPROFILE,
|
|
Cmd.ARG_DRIVEFILEACLS: Cmd.ARG_DRIVEFILEACL,
|
|
Cmd.ARG_DRIVELABELS: Cmd.ARG_DRIVELABEL,
|
|
Cmd.ARG_DRIVELABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
|
|
Cmd.ARG_EXPORT: Cmd.ARG_VAULTEXPORT,
|
|
Cmd.ARG_EXPORTS: Cmd.ARG_VAULTEXPORT,
|
|
Cmd.ARG_FEATURES: Cmd.ARG_FEATURE,
|
|
Cmd.ARG_FORMS: Cmd.ARG_FORM,
|
|
Cmd.ARG_GROUPS: Cmd.ARG_GROUP,
|
|
Cmd.ARG_GROUPSMEMBERS: Cmd.ARG_GROUPMEMBERS,
|
|
Cmd.ARG_GUARDIANINVITATIONS: Cmd.ARG_GUARDIANINVITATION,
|
|
Cmd.ARG_GUARDIANINVITE: Cmd.ARG_GUARDIANINVITATION,
|
|
Cmd.ARG_GUARDIANS: Cmd.ARG_GUARDIAN,
|
|
Cmd.ARG_HOLD: Cmd.ARG_VAULTHOLD,
|
|
Cmd.ARG_HOLDS: Cmd.ARG_VAULTHOLD,
|
|
Cmd.ARG_INBOUNDSSOASSIGNMENTS: Cmd.ARG_INBOUNDSSOASSIGNMENT,
|
|
Cmd.ARG_INBOUNDSSOCREDENTIALS: Cmd.ARG_INBOUNDSSOCREDENTIAL,
|
|
Cmd.ARG_INBOUNDSSOPROFILES: Cmd.ARG_INBOUNDSSOPROFILE,
|
|
Cmd.ARG_INVITEGUARDIAN: Cmd.ARG_GUARDIANINVITATION,
|
|
Cmd.ARG_LICENCE: Cmd.ARG_LICENSE,
|
|
Cmd.ARG_LICENCES: Cmd.ARG_LICENSE,
|
|
Cmd.ARG_LICENSES: Cmd.ARG_LICENSE,
|
|
Cmd.ARG_MATTER: Cmd.ARG_VAULTMATTER,
|
|
Cmd.ARG_MATTERS: Cmd.ARG_VAULTMATTER,
|
|
Cmd.ARG_MEMBER: Cmd.ARG_GROUPMEMBERS,
|
|
Cmd.ARG_MEMBERS: Cmd.ARG_GROUPMEMBERS,
|
|
Cmd.ARG_MOBILES: Cmd.ARG_MOBILE,
|
|
Cmd.ARG_NICKNAME: Cmd.ARG_ALIAS,
|
|
Cmd.ARG_NICKNAMES: Cmd.ARG_ALIAS,
|
|
Cmd.ARG_ORGUNIT: Cmd.ARG_ORG,
|
|
Cmd.ARG_ORGUNITS: Cmd.ARG_ORGS,
|
|
Cmd.ARG_ORGUNITSHAREDDRIVES: Cmd.ARG_ORGUNITSHAREDDRIVE,
|
|
Cmd.ARG_OU: Cmd.ARG_ORG,
|
|
Cmd.ARG_OUS: Cmd.ARG_ORGS,
|
|
Cmd.ARG_OUSHAREDDRIVE: Cmd.ARG_ORGUNITSHAREDDRIVE,
|
|
Cmd.ARG_OUSHAREDDRIVES: Cmd.ARG_ORGUNITSHAREDDRIVE,
|
|
Cmd.ARG_OUTREE: Cmd.ARG_ORGTREE,
|
|
Cmd.ARG_PARTICIPANTS: Cmd.ARG_COURSEPARTICIPANTS,
|
|
Cmd.ARG_PEOPLE: Cmd.ARG_PEOPLEPROFILE,
|
|
Cmd.ARG_PEOPLECONTACTS: Cmd.ARG_PEOPLECONTACT,
|
|
Cmd.ARG_PEOPLEPROFILES: Cmd.ARG_PEOPLEPROFILE,
|
|
Cmd.ARG_PERMISSIONS: Cmd.ARG_PERMISSION,
|
|
Cmd.ARG_PRINTERS: Cmd.ARG_PRINTER,
|
|
Cmd.ARG_PRINTERMODELS: Cmd.ARG_PRINTERMODEL,
|
|
Cmd.ARG_PROJECTS: Cmd.ARG_PROJECT,
|
|
Cmd.ARG_RESELLERCUSTOMERS: Cmd.ARG_RESOLDCUSTOMER,
|
|
Cmd.ARG_RESELLERSUBSCRIPTIONS: Cmd.ARG_RESOLDSUBSCRIPTION,
|
|
Cmd.ARG_RESOLDCUSTOMERS: Cmd.ARG_RESOLDCUSTOMER,
|
|
Cmd.ARG_RESOLDSUBSCRIPTIONS: Cmd.ARG_RESOLDSUBSCRIPTION,
|
|
Cmd.ARG_ROLE: Cmd.ARG_ADMINROLE,
|
|
Cmd.ARG_ROLES: Cmd.ARG_ADMINROLE,
|
|
Cmd.ARG_SAKEYS: Cmd.ARG_SAKEY,
|
|
Cmd.ARG_SCHEMAS: Cmd.ARG_SCHEMA,
|
|
Cmd.ARG_SHAREDDRIVES: Cmd.ARG_SHAREDDRIVE,
|
|
Cmd.ARG_SIGNJWTSERVICEACCOUNT: Cmd.ARG_GCPSERVICEACCOUNT,
|
|
Cmd.ARG_SITEACLS: Cmd.ARG_SITEACL,
|
|
Cmd.ARG_SITES: Cmd.ARG_SITE,
|
|
Cmd.ARG_STORAGEBUCKETS: Cmd.ARG_STORAGEBUCKET,
|
|
Cmd.ARG_STORAGEFILES: Cmd.ARG_STORAGEFILE,
|
|
Cmd.ARG_SVCACCTS: Cmd.ARG_SVCACCT,
|
|
Cmd.ARG_TEAMDRIVE: Cmd.ARG_SHAREDDRIVE,
|
|
Cmd.ARG_TEAMDRIVES: Cmd.ARG_SHAREDDRIVE,
|
|
Cmd.ARG_TEAMDRIVEACLS: Cmd.ARG_SHAREDDRIVEACLS,
|
|
Cmd.ARG_TEAMDRIVEINFO: Cmd.ARG_SHAREDDRIVEINFO,
|
|
Cmd.ARG_TEAMDRIVETHEMES: Cmd.ARG_SHAREDDRIVETHEMES,
|
|
Cmd.ARG_TOKENS: Cmd.ARG_TOKEN,
|
|
Cmd.ARG_TRANSFER: Cmd.ARG_DATATRANSFER,
|
|
Cmd.ARG_TRANSFERS: Cmd.ARG_DATATRANSFER,
|
|
Cmd.ARG_USERINVITATIONS: Cmd.ARG_USERINVITATION,
|
|
Cmd.ARG_VAULTCOUNTS: Cmd.ARG_VAULTCOUNT,
|
|
Cmd.ARG_VAULTEXPORTS: Cmd.ARG_VAULTEXPORT,
|
|
Cmd.ARG_VAULTHOLDS: Cmd.ARG_VAULTHOLD,
|
|
Cmd.ARG_VAULTQUERIES: Cmd.ARG_VAULTQUERY,
|
|
Cmd.ARG_VAULTMATTERS: Cmd.ARG_VAULTMATTER,
|
|
Cmd.ARG_VERIFICATION: Cmd.ARG_VERIFY,
|
|
}
|
|
|
|
# Audit command sub-commands with objects
|
|
AUDIT_SUBCOMMANDS_WITH_OBJECTS = {
|
|
'monitor':
|
|
{'create': (Act.CREATE, doCreateMonitor),
|
|
'delete': (Act.DELETE, doDeleteMonitor),
|
|
'list': (Act.LIST, doShowMonitors),
|
|
},
|
|
}
|
|
|
|
def processAuditCommands():
|
|
CL_subCommand = getChoice(list(AUDIT_SUBCOMMANDS_WITH_OBJECTS))
|
|
CL_objectName = getChoice(AUDIT_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand])
|
|
Act.Set(AUDIT_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CL_objectName][CMD_ACTION])
|
|
AUDIT_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CL_objectName][CMD_FUNCTION]()
|
|
|
|
# Oauth command sub-commands
|
|
OAUTH2_SUBCOMMANDS = {
|
|
'create': (Act.CREATE, doOAuthCreate),
|
|
'delete': (Act.DELETE, doOAuthDelete),
|
|
'export': (Act.EXPORT, doOAuthExport),
|
|
'info': (Act.INFO, doOAuthInfo),
|
|
'refresh': (Act.REFRESH, doOAuthRefresh),
|
|
'update': (Act.UPDATE, doOAuthUpdate),
|
|
}
|
|
|
|
# Oauth sub-command aliases
|
|
OAUTH2_SUBCOMMAND_ALIASES = {
|
|
'request': 'create',
|
|
'revoke': 'delete',
|
|
'verify': 'info',
|
|
}
|
|
|
|
def processOauthCommands():
|
|
CL_subCommand = getChoice(OAUTH2_SUBCOMMANDS, choiceAliases=OAUTH2_SUBCOMMAND_ALIASES)
|
|
Act.Set(OAUTH2_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
|
|
if GC.Values[GC.ENABLE_DASA]:
|
|
systemErrorExit(USAGE_ERROR_RC, Msg.COMMAND_NOT_COMPATIBLE_WITH_ENABLE_DASA.format('oauth', CL_subCommand))
|
|
OAUTH2_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION]()
|
|
|
|
# Calendar command sub-commands
|
|
CALENDAR_SUBCOMMANDS = {
|
|
'showacl': (Act.SHOW, doCalendarsPrintShowACLs),
|
|
'printacl': (Act.PRINT, doCalendarsPrintShowACLs),
|
|
'addevent': (Act.ADD, doCalendarsCreateEvent),
|
|
'deleteevent': (Act.DELETE, doCalendarsDeleteEventsOld),
|
|
'moveevent': (Act.MOVE, doCalendarsMoveEventsOld),
|
|
'updateevent': (Act.UPDATE, doCalendarsUpdateEventsOld),
|
|
'printevents': (Act.PRINT, doCalendarsPrintShowEvents),
|
|
'wipe': (Act.WIPE, doCalendarsWipeEvents),
|
|
'modify': (Act.MODIFY, doCalendarsModifySettings),
|
|
}
|
|
|
|
CALENDAR_OLDACL_SUBCOMMANDS = {
|
|
'add': (Act.ADD, doCalendarsCreateACL),
|
|
'create': (Act.CREATE, doCalendarsCreateACL),
|
|
'delete': (Act.DELETE, doCalendarsDeleteACL),
|
|
'update': (Act.UPDATE, doCalendarsUpdateACL),
|
|
}
|
|
|
|
# Calendar sub-command aliases
|
|
CALENDAR_OLDACL_SUBCOMMAND_ALIASES = {
|
|
'del': 'delete',
|
|
}
|
|
|
|
# Calendars command sub-commands with objects
|
|
CALENDARS_SUBCOMMANDS_WITH_OBJECTS = {
|
|
'add':
|
|
(Act.ADD,
|
|
{Cmd.ARG_CALENDARACL: doCalendarsCreateACLs,
|
|
Cmd.ARG_EVENT: doCalendarsCreateEvent,
|
|
}
|
|
),
|
|
'create':
|
|
(Act.CREATE,
|
|
{Cmd.ARG_CALENDARACL: doCalendarsCreateACLs,
|
|
Cmd.ARG_EVENT: doCalendarsCreateEvent,
|
|
}
|
|
),
|
|
'delete':
|
|
(Act.DELETE,
|
|
{Cmd.ARG_CALENDARACL: doCalendarsDeleteACLs,
|
|
Cmd.ARG_EVENT: doCalendarsDeleteEvents,
|
|
}
|
|
),
|
|
'empty':
|
|
(Act.EMPTY,
|
|
{Cmd.ARG_CALENDARTRASH: doCalendarsEmptyTrash,
|
|
}
|
|
),
|
|
'import':
|
|
(Act.IMPORT,
|
|
{Cmd.ARG_EVENT: doCalendarsImportEvent,
|
|
}
|
|
),
|
|
'info':
|
|
(Act.INFO,
|
|
{Cmd.ARG_CALENDARACL: doCalendarsInfoACLs,
|
|
Cmd.ARG_EVENT: doCalendarsInfoEvents,
|
|
}
|
|
),
|
|
'move':
|
|
(Act.MOVE,
|
|
{Cmd.ARG_EVENT: doCalendarsMoveEvents,
|
|
}
|
|
),
|
|
'print':
|
|
(Act.PRINT,
|
|
{Cmd.ARG_CALENDARACL: doCalendarsPrintShowACLs,
|
|
Cmd.ARG_EVENT: doCalendarsPrintShowEvents,
|
|
Cmd.ARG_SETTINGS: doCalendarsPrintShowSettings,
|
|
}
|
|
),
|
|
'purge':
|
|
(Act.PURGE,
|
|
{Cmd.ARG_EVENT: doCalendarsPurgeEvents,
|
|
}
|
|
),
|
|
'show':
|
|
(Act.SHOW,
|
|
{Cmd.ARG_CALENDARACL: doCalendarsPrintShowACLs,
|
|
Cmd.ARG_EVENT: doCalendarsPrintShowEvents,
|
|
Cmd.ARG_SETTINGS: doCalendarsPrintShowSettings,
|
|
}
|
|
),
|
|
'update':
|
|
(Act.UPDATE,
|
|
{Cmd.ARG_CALENDARACL: doCalendarsUpdateACLs,
|
|
Cmd.ARG_EVENT: doCalendarsUpdateEvents,
|
|
}
|
|
),
|
|
'wipe':
|
|
(Act.WIPE,
|
|
{Cmd.ARG_EVENT: doCalendarsWipeEvents,
|
|
}
|
|
),
|
|
}
|
|
|
|
CALENDARS_SUBCOMMANDS_OBJECT_ALIASES = {
|
|
Cmd.ARG_ACL: Cmd.ARG_CALENDARACL,
|
|
Cmd.ARG_ACLS: Cmd.ARG_CALENDARACL,
|
|
Cmd.ARG_CALENDARACLS: Cmd.ARG_CALENDARACL,
|
|
Cmd.ARG_EVENTS: Cmd.ARG_EVENT,
|
|
}
|
|
|
|
def processCalendarsCommands():
|
|
calendarList = getEntityList(Cmd.OB_EMAIL_ADDRESS_ENTITY)
|
|
CL_subCommand = getChoice(CALENDAR_SUBCOMMANDS, defaultChoice=None)
|
|
if CL_subCommand:
|
|
Act.Set(CALENDAR_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
|
|
CALENDAR_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION](calendarList)
|
|
return
|
|
CL_subCommand = getChoice(CALENDAR_OLDACL_SUBCOMMANDS, choiceAliases=CALENDAR_OLDACL_SUBCOMMAND_ALIASES, defaultChoice=None)
|
|
if CL_subCommand:
|
|
Act.Set(CALENDAR_OLDACL_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
|
|
CL_objectName = getChoice([Cmd.ARG_CALENDARACL, Cmd.ARG_EVENT], choiceAliases=CALENDARS_SUBCOMMANDS_OBJECT_ALIASES, defaultChoice=None)
|
|
if not CL_objectName:
|
|
CALENDAR_OLDACL_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION](calendarList)
|
|
else:
|
|
CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION][CL_objectName](calendarList)
|
|
return
|
|
CL_subCommand = getChoice(CALENDARS_SUBCOMMANDS_WITH_OBJECTS)
|
|
Act.Set(CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_ACTION])
|
|
CL_objectName = getChoice(CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION], choiceAliases=CALENDARS_SUBCOMMANDS_OBJECT_ALIASES)
|
|
CALENDARS_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION][CL_objectName](calendarList)
|
|
|
|
# Course command sub-commands
|
|
COURSE_SUBCOMMANDS = {
|
|
'add': (Act.ADD, doCourseAddItems),
|
|
'clear': (Act.REMOVE, doCourseClearParticipants),
|
|
'remove': (Act.REMOVE, doCourseRemoveItems),
|
|
'sync': (Act.SYNC, doCourseSyncParticipants),
|
|
}
|
|
|
|
# Course sub-command aliases
|
|
COURSE_SUBCOMMAND_ALIASES = {
|
|
'create': 'add',
|
|
'del': 'remove',
|
|
'delete': 'remove',
|
|
}
|
|
|
|
def executeCourseCommands(courseIdList, getEntityListArg):
|
|
CL_subCommand = getChoice(COURSE_SUBCOMMANDS, choiceAliases=COURSE_SUBCOMMAND_ALIASES)
|
|
Act.Set(COURSE_SUBCOMMANDS[CL_subCommand][CMD_ACTION])
|
|
COURSE_SUBCOMMANDS[CL_subCommand][CMD_FUNCTION](courseIdList, getEntityListArg)
|
|
|
|
def processCourseCommands():
|
|
executeCourseCommands(getStringReturnInList(Cmd.OB_COURSE_ID), False)
|
|
|
|
def processCoursesCommands():
|
|
executeCourseCommands(getEntityList(Cmd.OB_COURSE_ENTITY, shlexSplit=True), True)
|
|
|
|
# Resource command sub-commands
|
|
RESOURCE_SUBCOMMANDS_WITH_OBJECTS = {
|
|
'add':
|
|
(Act.ADD,
|
|
{Cmd.ARG_CALENDARACL: doResourceCreateCalendarACLs,
|
|
}
|
|
),
|
|
'create':
|
|
(Act.CREATE,
|
|
{Cmd.ARG_CALENDARACL: doResourceCreateCalendarACLs,
|
|
}
|
|
),
|
|
'update':
|
|
(Act.UPDATE,
|
|
{Cmd.ARG_CALENDARACL: doResourceUpdateCalendarACLs,
|
|
}
|
|
),
|
|
'delete':
|
|
(Act.DELETE,
|
|
{Cmd.ARG_CALENDARACL: doResourceDeleteCalendarACLs,
|
|
}
|
|
),
|
|
'info':
|
|
(Act.INFO,
|
|
{Cmd.ARG_CALENDARACL: doResourceInfoCalendarACLs,
|
|
}
|
|
),
|
|
'print':
|
|
(Act.PRINT,
|
|
{Cmd.ARG_CALENDARACL: doResourcePrintShowCalendarACLs,
|
|
}
|
|
),
|
|
'show':
|
|
(Act.SHOW,
|
|
{Cmd.ARG_CALENDARACL: doResourcePrintShowCalendarACLs,
|
|
}
|
|
),
|
|
}
|
|
|
|
# Resource sub-command aliases
|
|
RESOURCE_SUBCOMMAND_ALIASES = {
|
|
'del': 'delete',
|
|
}
|
|
|
|
RESOURCE_SUBCOMMANDS_OBJECT_ALIASES = {
|
|
Cmd.ARG_ACL: Cmd.ARG_CALENDARACL,
|
|
Cmd.ARG_ACLS: Cmd.ARG_CALENDARACL,
|
|
Cmd.ARG_CALENDARACLS: Cmd.ARG_CALENDARACL,
|
|
}
|
|
|
|
def executeResourceCommands(resourceEntity):
|
|
CL_subCommand = getChoice(RESOURCE_SUBCOMMANDS_WITH_OBJECTS, choiceAliases=RESOURCE_SUBCOMMAND_ALIASES)
|
|
Act.Set(RESOURCE_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_ACTION])
|
|
CL_objectName = getChoice(RESOURCE_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION], choiceAliases=RESOURCE_SUBCOMMANDS_OBJECT_ALIASES)
|
|
RESOURCE_SUBCOMMANDS_WITH_OBJECTS[CL_subCommand][CMD_FUNCTION][CL_objectName](resourceEntity)
|
|
|
|
def processResourceCommands():
|
|
executeResourceCommands(getStringReturnInList(Cmd.OB_RESOURCE_ID))
|
|
|
|
def processResourcesCommands():
|
|
executeResourceCommands(getEntityList(Cmd.OB_RESOURCE_ENTITY))
|
|
|
|
# Commands
|
|
COMMANDS_MAP = {
|
|
'oauth': processOauthCommands,
|
|
'audit': processAuditCommands,
|
|
'calendars': processCalendarsCommands,
|
|
'course': processCourseCommands,
|
|
'courses': processCoursesCommands,
|
|
'resource': processResourceCommands,
|
|
'resources': processResourcesCommands,
|
|
}
|
|
|
|
# Commands aliases
|
|
COMMANDS_ALIASES = {
|
|
'oauth2': 'oauth',
|
|
'calendar': 'calendars',
|
|
}
|
|
|
|
# <CrOSTypeEntity> commands
|
|
CROS_COMMANDS = {
|
|
'getcommand': (Act.GET_COMMAND_RESULT, getCommandResultCrOSDevices),
|
|
'info': (Act.INFO, infoCrOSDevices),
|
|
'issuecommand': (Act.ISSUE_COMMAND, issueCommandCrOSDevices),
|
|
'list': (Act.LIST, doListCrOS),
|
|
'print': (Act.PRINT, doPrintCrOSEntity),
|
|
'update': (Act.UPDATE, updateCrOSDevices),
|
|
}
|
|
|
|
CROS_COMMANDS_WITH_OBJECTS = {
|
|
'get':
|
|
(Act.DOWNLOAD,
|
|
{Cmd.ARG_DEVICEFILE: getCrOSDeviceFiles,
|
|
}
|
|
),
|
|
'show':
|
|
(Act.SHOW,
|
|
{Cmd.ARG_COUNT: showCountCrOS,
|
|
}
|
|
),
|
|
}
|
|
|
|
CROS_COMMANDS_OBJ_ALIASES = {
|
|
Cmd.ARG_DEVICEFILES: Cmd.ARG_DEVICEFILE,
|
|
Cmd.ARG_COUNTS: Cmd.ARG_COUNT,
|
|
}
|
|
|
|
# <UserTypeEntity> commands
|
|
USER_COMMANDS = {
|
|
'delegate': (Act.ADD, delegateTo),
|
|
'deprovision': (Act.DEPROVISION, deprovisionUser),
|
|
'draftemail': (Act.DRAFT, draftMessage),
|
|
'filter': (Act.ADD, createFilter),
|
|
'forward': (Act.SET, setForward),
|
|
'imap': (Act.SET, setImap),
|
|
'importemail': (Act.IMPORT, importMessage),
|
|
'insertemail': (Act.INSERT, insertMessage),
|
|
'label': (Act.ADD, createLabel),
|
|
'list': (Act.LIST, doListUser),
|
|
'language': (Act.SET, setLanguage),
|
|
'pop': (Act.SET, setPop),
|
|
'profile': (Act.SET, setProfile),
|
|
'sendas': (Act.ADD, createUpdateSendAs),
|
|
'sendemail': (Act.SENDEMAIL, doSendEmail),
|
|
'signature': (Act.SET, setSignature),
|
|
'signout': (Act.SIGNOUT, signoutTurnoff2SVUsers),
|
|
'turnoff2sv': (Act.TURNOFF2SV, signoutTurnoff2SVUsers),
|
|
'vacation': (Act.SET, setVacation),
|
|
'waitformailbox': (Act.WAITFORMAILBOX, waitForMailbox),
|
|
}
|
|
|
|
# User commands with objects
|
|
#
|
|
USER_ADD_CREATE_FUNCTIONS = {
|
|
Cmd.ARG_CALENDAR: addCreateCalendars,
|
|
Cmd.ARG_GROUP: addUserToGroups,
|
|
Cmd.ARG_CALENDARACL: createCalendarACLs,
|
|
Cmd.ARG_CHATMEMBER: createChatMember,
|
|
Cmd.ARG_CHATMESSAGE: createChatMessage,
|
|
Cmd.ARG_CHATSPACE: createChatSpace,
|
|
Cmd.ARG_CLASSROOMINVITATION: createClassroomInvitations,
|
|
Cmd.ARG_CONTACTDELEGATE: processContactDelegates,
|
|
Cmd.ARG_CSEIDENTITY: createUpdateCSEIdentity,
|
|
Cmd.ARG_CSEKEYPAIR: createCSEKeyPair,
|
|
Cmd.ARG_LOOKERSTUDIOPERMISSION: processLookerStudioPermissions,
|
|
Cmd.ARG_DELEGATE: processDelegates,
|
|
Cmd.ARG_DRIVEFILE: createDriveFile,
|
|
Cmd.ARG_DRIVEFILEACL: createDriveFileACL,
|
|
Cmd.ARG_DRIVEFILESHORTCUT: createDriveFileShortcut,
|
|
Cmd.ARG_DRIVEFOLDERPATH: createDriveFolderPath,
|
|
Cmd.ARG_DRIVELABELPERMISSION: createDriveLabelPermissions,
|
|
Cmd.ARG_EVENT: createCalendarEvent,
|
|
Cmd.ARG_FILTER: createFilter,
|
|
Cmd.ARG_FOCUSTIME: createFocusTime,
|
|
Cmd.ARG_FORM: createForm,
|
|
Cmd.ARG_FORWARDINGADDRESS: createForwardingAddresses,
|
|
Cmd.ARG_GUARDIAN: inviteGuardians,
|
|
Cmd.ARG_GUARDIANINVITATION: inviteGuardians,
|
|
Cmd.ARG_LABEL: createLabel,
|
|
Cmd.ARG_LABELLIST: createLabelList,
|
|
Cmd.ARG_LICENSE: createLicense,
|
|
Cmd.ARG_MEETSPACE: createMeetSpace,
|
|
Cmd.ARG_NOTE: createNote,
|
|
Cmd.ARG_NOTEACL: createNotesACLs,
|
|
Cmd.ARG_OUTOFOFFICE: createOutOfOffice,
|
|
Cmd.ARG_PEOPLECONTACT: createUserPeopleContact,
|
|
Cmd.ARG_PEOPLECONTACTGROUP: createUserPeopleContactGroup,
|
|
Cmd.ARG_PERMISSION: createDriveFilePermissions,
|
|
Cmd.ARG_SENDAS: createUpdateSendAs,
|
|
Cmd.ARG_SHAREDDRIVE: createSharedDrive,
|
|
Cmd.ARG_SHEET: createSheet,
|
|
Cmd.ARG_SITE: deprecatedUserSites,
|
|
Cmd.ARG_SITEACL: deprecatedUserSites,
|
|
Cmd.ARG_SMIME: createSmime,
|
|
Cmd.ARG_TASK: processTasks,
|
|
Cmd.ARG_TASKLIST: processTasklists,
|
|
Cmd.ARG_WORKINGLOCATION: createWorkingLocation,
|
|
}
|
|
|
|
USER_COMMANDS_WITH_OBJECTS = {
|
|
'accept':
|
|
(Act.ACCEPT,
|
|
{Cmd.ARG_CLASSROOMINVITATION: acceptClassroomInvitations,
|
|
}
|
|
),
|
|
'add':
|
|
(Act.ADD,
|
|
USER_ADD_CREATE_FUNCTIONS
|
|
),
|
|
'append':
|
|
(Act.APPEND,
|
|
{Cmd.ARG_SHEETRANGE: appendSheetRanges,
|
|
}
|
|
),
|
|
'archive':
|
|
(Act.ARCHIVE,
|
|
{Cmd.ARG_MESSAGE: archiveMessages,
|
|
}
|
|
),
|
|
'cancel':
|
|
(Act.CANCEL,
|
|
{Cmd.ARG_GUARDIANINVITATION: cancelGuardianInvitations,
|
|
}
|
|
),
|
|
'check':
|
|
(Act.CHECK,
|
|
{Cmd.ARG_DRIVEFILESHORTCUT: checkDriveFileShortcut,
|
|
Cmd.ARG_GROUP: checkUserInGroups,
|
|
Cmd.ARG_ISINVITABLE: checkCIUserIsInvitable,
|
|
Cmd.ARG_SERVICEACCOUNT: checkServiceAccount,
|
|
}
|
|
),
|
|
'claim':
|
|
(Act.CLAIM,
|
|
{Cmd.ARG_OWNERSHIP: claimOwnership,
|
|
}
|
|
),
|
|
'clear':
|
|
(Act.CLEAR,
|
|
{Cmd.ARG_GUARDIAN: clearGuardians,
|
|
Cmd.ARG_PEOPLECONTACT: clearUserPeopleContacts,
|
|
Cmd.ARG_SHEETRANGE: clearSheetRanges,
|
|
Cmd.ARG_TASKLIST: processTasklists,
|
|
}
|
|
),
|
|
'collect':
|
|
(Act.COLLECT,
|
|
{Cmd.ARG_ORPHANS: collectOrphans,
|
|
}
|
|
),
|
|
'copy':
|
|
(Act.COPY,
|
|
{Cmd.ARG_DRIVEFILE: copyDriveFile,
|
|
Cmd.ARG_OTHERCONTACT: copyUserPeopleOtherContacts,
|
|
Cmd.ARG_SHAREDDRIVEACLS: copySyncSharedDriveACLs,
|
|
}
|
|
),
|
|
'create':
|
|
(Act.CREATE,
|
|
USER_ADD_CREATE_FUNCTIONS
|
|
),
|
|
'dedup':
|
|
(Act.DEDUP,
|
|
{Cmd.ARG_PEOPLECONTACT: dedupReplaceDomainUserPeopleContacts,
|
|
}
|
|
),
|
|
'delete':
|
|
(Act.DELETE,
|
|
{Cmd.ARG_ALIAS: deleteUsersAliases,
|
|
Cmd.ARG_ASP: deleteASP,
|
|
Cmd.ARG_BACKUPCODE: deleteBackupCodes,
|
|
Cmd.ARG_CALENDAR: deleteCalendars,
|
|
Cmd.ARG_CALENDARACL: deleteCalendarACLs,
|
|
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
|
|
Cmd.ARG_CHATMESSAGE: deleteChatMessage,
|
|
Cmd.ARG_CHATSPACE: deleteChatSpace,
|
|
Cmd.ARG_CLASSROOMINVITATION: deleteClassroomInvitations,
|
|
Cmd.ARG_CONTACTDELEGATE: processContactDelegates,
|
|
Cmd.ARG_CSEIDENTITY: processCSEIdentity,
|
|
Cmd.ARG_LOOKERSTUDIOPERMISSION: processLookerStudioPermissions,
|
|
Cmd.ARG_DELEGATE: processDelegates,
|
|
Cmd.ARG_DRIVEFILE: deleteDriveFile,
|
|
Cmd.ARG_DRIVEFILEACL: deleteDriveFileACLs,
|
|
Cmd.ARG_DRIVELABELPERMISSION: deleteDriveLabelPermissions,
|
|
Cmd.ARG_EMPTYDRIVEFOLDERS: deleteEmptyDriveFolders,
|
|
Cmd.ARG_EVENT: deleteCalendarEvents,
|
|
Cmd.ARG_FILEREVISION: deleteFileRevisions,
|
|
Cmd.ARG_FILTER: deleteFilters,
|
|
Cmd.ARG_FOCUSTIME: deleteFocusTime,
|
|
Cmd.ARG_FORWARDINGADDRESS: deleteForwardingAddresses,
|
|
Cmd.ARG_GROUP: deleteUserFromGroups,
|
|
Cmd.ARG_GUARDIAN: deleteGuardians,
|
|
Cmd.ARG_LABEL: deleteLabel,
|
|
Cmd.ARG_LABELLIST: deleteLabelList,
|
|
Cmd.ARG_LABELID: deleteLabelId,
|
|
Cmd.ARG_LABELIDLIST: deleteLabelIdList,
|
|
Cmd.ARG_LICENSE: deleteLicense,
|
|
Cmd.ARG_MESSAGE: processMessages,
|
|
Cmd.ARG_NOTE: deleteInfoNotes,
|
|
Cmd.ARG_NOTEACL: deleteNotesACLs,
|
|
Cmd.ARG_OUTOFOFFICE: deleteOutOfOffice,
|
|
Cmd.ARG_OTHERCONTACT: processUserPeopleOtherContacts,
|
|
Cmd.ARG_PEOPLECONTACT: deleteUserPeopleContacts,
|
|
Cmd.ARG_PEOPLECONTACTGROUP: deleteUserPeopleContactGroups,
|
|
Cmd.ARG_PEOPLECONTACTPHOTO: deleteUserPeopleContactPhoto,
|
|
Cmd.ARG_PERMISSION: deletePermissions,
|
|
Cmd.ARG_PHOTO: deletePhoto,
|
|
Cmd.ARG_SENDAS: deleteInfoSendAs,
|
|
Cmd.ARG_SMIME: deleteSmime,
|
|
Cmd.ARG_SHAREDDRIVE: deleteSharedDrive,
|
|
Cmd.ARG_SITEACL: deprecatedUserSites,
|
|
Cmd.ARG_TASK: processTasks,
|
|
Cmd.ARG_TASKLIST: processTasklists,
|
|
Cmd.ARG_THREAD: processThreads,
|
|
Cmd.ARG_TOKEN: deleteTokens,
|
|
Cmd.ARG_USER: deleteUsers,
|
|
Cmd.ARG_WORKINGLOCATION: deleteWorkingLocation,
|
|
}
|
|
),
|
|
'disable':
|
|
(Act.DISABLE,
|
|
{Cmd.ARG_CSEKEYPAIR: processCSEKeyPair,
|
|
}
|
|
),
|
|
'draft':
|
|
(Act.DRAFT,
|
|
{Cmd.ARG_MESSAGE: draftMessage,
|
|
}
|
|
),
|
|
'empty':
|
|
(Act.EMPTY,
|
|
{Cmd.ARG_CALENDARTRASH: emptyCalendarTrash,
|
|
Cmd.ARG_DRIVETRASH: emptyDriveTrash,
|
|
}
|
|
),
|
|
'enable':
|
|
(Act.ENABLE,
|
|
{Cmd.ARG_CSEKEYPAIR: processCSEKeyPair,
|
|
}
|
|
),
|
|
'end':
|
|
(Act.END,
|
|
{Cmd.ARG_MEETCONFERENCE: endMeetConference,
|
|
}
|
|
),
|
|
'export':
|
|
(Act.EXPORT,
|
|
{Cmd.ARG_MESSAGE: exportMessages,
|
|
Cmd.ARG_THREAD: exportThreads,
|
|
}
|
|
),
|
|
'get':
|
|
(Act.DOWNLOAD,
|
|
{Cmd.ARG_DOCUMENT: getGoogleDocument,
|
|
Cmd.ARG_DRIVEFILE: getDriveFile,
|
|
Cmd.ARG_NOTEATTACHMENT: getNoteAttachments,
|
|
Cmd.ARG_PEOPLECONTACTPHOTO: getUserPeopleContactPhoto,
|
|
Cmd.ARG_PHOTO: getUserPhoto,
|
|
Cmd.ARG_PROFILE_PHOTO: getProfilePhoto,
|
|
}
|
|
),
|
|
'hide':
|
|
(Act.HIDE,
|
|
{Cmd.ARG_SHAREDDRIVE: hideUnhideSharedDrive,
|
|
}
|
|
),
|
|
'import':
|
|
(Act.IMPORT,
|
|
{Cmd.ARG_EVENT: importCalendarEvent,
|
|
Cmd.ARG_MESSAGE: importMessage,
|
|
Cmd.ARG_TASKLIST: importTasklist,
|
|
}
|
|
),
|
|
'info':
|
|
(Act.INFO,
|
|
{Cmd.ARG_CALENDAR: infoCalendars,
|
|
Cmd.ARG_CALENDARACL: infoCalendarACLs,
|
|
Cmd.ARG_CHATEVENT: infoChatEvent,
|
|
Cmd.ARG_CHATMEMBER: infoChatMember,
|
|
Cmd.ARG_CHATMESSAGE: infoChatMessage,
|
|
Cmd.ARG_CHATSPACE: infoChatSpace,
|
|
Cmd.ARG_CHATSPACEDM: infoChatSpaceDM,
|
|
Cmd.ARG_CIGROUPMEMBERS: infoCIGroupMembers,
|
|
Cmd.ARG_CSEIDENTITY: processCSEIdentity,
|
|
Cmd.ARG_CSEKEYPAIR: processCSEKeyPair,
|
|
Cmd.ARG_DRIVEFILE: showFileInfo,
|
|
Cmd.ARG_DRIVEFILEACL: infoDriveFileACLs,
|
|
Cmd.ARG_DRIVELABEL: infoDriveLabels,
|
|
Cmd.ARG_EVENT: infoCalendarEvents,
|
|
Cmd.ARG_FILTER: infoFilters,
|
|
Cmd.ARG_FORWARDINGADDRESS: infoForwardingAddresses,
|
|
Cmd.ARG_GROUPMEMBERS: infoGroupMembers,
|
|
Cmd.ARG_MEETSPACE: infoMeetSpace,
|
|
Cmd.ARG_NOTE: deleteInfoNotes,
|
|
Cmd.ARG_PEOPLECONTACT: infoUserPeopleContacts,
|
|
Cmd.ARG_PEOPLECONTACTGROUP: infoUserPeopleContactGroups,
|
|
Cmd.ARG_SENDAS: deleteInfoSendAs,
|
|
Cmd.ARG_SHAREDDRIVE: infoSharedDrive,
|
|
Cmd.ARG_SHEET: infoPrintShowSheets,
|
|
Cmd.ARG_SITE: deprecatedUserSites,
|
|
Cmd.ARG_SITEACL: deprecatedUserSites,
|
|
Cmd.ARG_TASK: processTasks,
|
|
Cmd.ARG_TASKLIST: processTasklists,
|
|
Cmd.ARG_USER: infoUsers,
|
|
}
|
|
),
|
|
'insert':
|
|
(Act.INSERT,
|
|
{Cmd.ARG_MESSAGE: insertMessage,
|
|
}
|
|
),
|
|
'modify':
|
|
(Act.MODIFY,
|
|
{Cmd.ARG_CALENDAR: modifyCalendars,
|
|
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
|
|
Cmd.ARG_MESSAGE: processMessages,
|
|
Cmd.ARG_THREAD: processThreads,
|
|
}
|
|
),
|
|
'move':
|
|
(Act.MOVE,
|
|
{Cmd.ARG_DRIVEFILE: moveDriveFile,
|
|
Cmd.ARG_EVENT: moveCalendarEvents,
|
|
Cmd.ARG_OTHERCONTACT: processUserPeopleOtherContacts,
|
|
Cmd.ARG_TASK: processTasks,
|
|
}
|
|
),
|
|
'obliterate':
|
|
(Act.OBLITERATE,
|
|
{Cmd.ARG_CSEKEYPAIR: processCSEKeyPair,
|
|
}
|
|
),
|
|
'purge':
|
|
(Act.PURGE,
|
|
{Cmd.ARG_DRIVEFILE: purgeDriveFile,
|
|
Cmd.ARG_EVENT: purgeCalendarEvents,
|
|
}
|
|
),
|
|
'print':
|
|
(Act.PRINT,
|
|
{Cmd.ARG_ANALYTICACCOUNT: printShowAnalyticAccounts,
|
|
Cmd.ARG_ANALYTICACCOUNTSUMMARY: printShowAnalyticAccountSummaries,
|
|
Cmd.ARG_ANALYTICDATASTREAM: printShowAnalyticDatastreams,
|
|
Cmd.ARG_ANALYTICPROPERTY: printShowAnalyticProperties,
|
|
Cmd.ARG_ANALYTICUAPROPERTY: printShowAnalyticUAProperties,
|
|
Cmd.ARG_ASP: printShowASPs,
|
|
Cmd.ARG_BACKUPCODE: printShowBackupCodes,
|
|
Cmd.ARG_CALENDAR: printShowCalendars,
|
|
Cmd.ARG_CALENDARACL: printShowCalendarACLs,
|
|
Cmd.ARG_CALSETTINGS: printShowCalSettings,
|
|
Cmd.ARG_CHATEVENT: printShowChatEvents,
|
|
Cmd.ARG_CHATMEMBER: printShowChatMembers,
|
|
Cmd.ARG_CHATMESSAGE: printShowChatMessages,
|
|
Cmd.ARG_CHATSPACE: printShowChatSpaces,
|
|
Cmd.ARG_CLASSROOMINVITATION: printShowClassroomInvitations,
|
|
Cmd.ARG_CLASSROOMPROFILE: printShowClassroomProfile,
|
|
Cmd.ARG_CONTACTDELEGATE: printShowContactDelegates,
|
|
Cmd.ARG_CSEIDENTITY: printShowCSEIdentities,
|
|
Cmd.ARG_CSEKEYPAIR: printShowCSEKeyPairs,
|
|
Cmd.ARG_LOOKERSTUDIOASSET: printShowLookerStudioAssets,
|
|
Cmd.ARG_LOOKERSTUDIOPERMISSION: printShowLookerStudioPermissions,
|
|
Cmd.ARG_DELEGATE: printShowDelegates,
|
|
Cmd.ARG_DISKUSAGE: printDiskUsage,
|
|
Cmd.ARG_DRIVEACTIVITY: printDriveActivity,
|
|
Cmd.ARG_DRIVEFILEACL: printShowDriveFileACLs,
|
|
Cmd.ARG_DRIVELABEL: printShowDriveLabels,
|
|
Cmd.ARG_DRIVELABELPERMISSION: printShowDriveLabelPermissions,
|
|
Cmd.ARG_DRIVELASTMODIFICATION: printShowDrivelastModifications,
|
|
Cmd.ARG_DRIVESETTINGS: printShowDriveSettings,
|
|
Cmd.ARG_EMPTYDRIVEFOLDERS: printEmptyDriveFolders,
|
|
Cmd.ARG_EVENT: printShowCalendarEvents,
|
|
Cmd.ARG_FILECOMMENT: printShowFileComments,
|
|
Cmd.ARG_FILECOUNT: printShowFileCounts,
|
|
Cmd.ARG_FILEINFO: showFileInfo,
|
|
Cmd.ARG_FILELIST: printFileList,
|
|
Cmd.ARG_FILEPATH: printShowFilePaths,
|
|
Cmd.ARG_FILEREVISION: printShowFileRevisions,
|
|
Cmd.ARG_FILESHARECOUNT: printShowFileShareCounts,
|
|
Cmd.ARG_FILETREE: printShowFileTree,
|
|
Cmd.ARG_FILTER: printShowFilters,
|
|
Cmd.ARG_FOCUSTIME: printShowFocusTime,
|
|
Cmd.ARG_FORM: printShowForms,
|
|
Cmd.ARG_FORMRESPONSE: printShowFormResponses,
|
|
Cmd.ARG_FORWARD: printShowForward,
|
|
Cmd.ARG_FORWARDINGADDRESS: printShowForwardingAddresses,
|
|
Cmd.ARG_GMAILPROFILE: printShowGmailProfile,
|
|
Cmd.ARG_GROUP: printShowUserGroups,
|
|
Cmd.ARG_GROUPSLIST: printUserGroupsList,
|
|
Cmd.ARG_GROUPTREE: printShowGroupTree,
|
|
Cmd.ARG_GUARDIAN: printShowGuardians,
|
|
Cmd.ARG_IMAP: printShowImap,
|
|
Cmd.ARG_LABEL: printShowLabels,
|
|
Cmd.ARG_LANGUAGE: printShowLanguage,
|
|
Cmd.ARG_MEETCONFERENCE: printShowMeetConferences,
|
|
Cmd.ARG_MEETPARTICIPANT: printShowMeetParticipants,
|
|
Cmd.ARG_MEETRECORDING: printShowMeetRecordings,
|
|
Cmd.ARG_MEETTRANSCRIPT: printShowMeetTranscripts,
|
|
Cmd.ARG_MESSAGE: printShowMessages,
|
|
Cmd.ARG_NOTE: printShowNotes,
|
|
Cmd.ARG_OTHERCONTACT: printShowUserPeopleOtherContacts,
|
|
Cmd.ARG_OUTOFOFFICE: printShowOutOfOffice,
|
|
Cmd.ARG_FILEPARENTTREE: printFileParentTree,
|
|
Cmd.ARG_PEOPLECONTACT: printShowUserPeopleContacts,
|
|
Cmd.ARG_PEOPLECONTACTGROUP: printShowUserPeopleContactGroups,
|
|
Cmd.ARG_PEOPLEPROFILE: printShowUserPeopleProfiles,
|
|
Cmd.ARG_POP: printShowPop,
|
|
Cmd.ARG_SENDAS: printShowSendAs,
|
|
Cmd.ARG_SHAREDDRIVE: printShowSharedDrives,
|
|
Cmd.ARG_SHAREDDRIVEACLS: printShowSharedDriveACLs,
|
|
Cmd.ARG_SHEET: infoPrintShowSheets,
|
|
Cmd.ARG_SHEETRANGE: printShowSheetRanges,
|
|
Cmd.ARG_SIGNATURE: printShowSignature,
|
|
Cmd.ARG_SMIME: printShowSmimes,
|
|
Cmd.ARG_SITE: deprecatedUserSites,
|
|
Cmd.ARG_SITEACL: deprecatedUserSites,
|
|
Cmd.ARG_SITEACTIVITY: deprecatedUserSites,
|
|
Cmd.ARG_TASK: printShowTasks,
|
|
Cmd.ARG_TASKLIST: printShowTasklists,
|
|
Cmd.ARG_THREAD: printShowThreads,
|
|
Cmd.ARG_TOKEN: printShowTokens,
|
|
Cmd.ARG_USER: doPrintUserEntity,
|
|
Cmd.ARG_USERLIST: doPrintUserList,
|
|
Cmd.ARG_VACATION: printShowVacation,
|
|
Cmd.ARG_VAULTHOLD: printShowUserVaultHolds,
|
|
Cmd.ARG_WORKINGLOCATION: printShowWorkingLocation,
|
|
Cmd.ARG_YOUTUBECHANNEL: printShowYouTubeChannel,
|
|
}
|
|
),
|
|
'process':
|
|
(Act.PROCESS,
|
|
{Cmd.ARG_FILEDRIVELABEL: processFileDriveLabels,
|
|
}
|
|
),
|
|
'remove':
|
|
(Act.REMOVE,
|
|
{Cmd.ARG_CALENDAR: removeCalendars,
|
|
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
|
|
Cmd.ARG_DRIVELABELPERMISSION: deleteDriveLabelPermissions,
|
|
}
|
|
),
|
|
'replacedomain':
|
|
(Act.REPLACE_DOMAIN,
|
|
{Cmd.ARG_PEOPLECONTACT: dedupReplaceDomainUserPeopleContacts,
|
|
}
|
|
),
|
|
'show':
|
|
(Act.SHOW,
|
|
{Cmd.ARG_ANALYTICACCOUNT: printShowAnalyticAccounts,
|
|
Cmd.ARG_ANALYTICACCOUNTSUMMARY: printShowAnalyticAccountSummaries,
|
|
Cmd.ARG_ANALYTICDATASTREAM: printShowAnalyticDatastreams,
|
|
Cmd.ARG_ANALYTICPROPERTY: printShowAnalyticProperties,
|
|
Cmd.ARG_ANALYTICUAPROPERTY: printShowAnalyticUAProperties,
|
|
Cmd.ARG_ASP: printShowASPs,
|
|
Cmd.ARG_BACKUPCODE: printShowBackupCodes,
|
|
Cmd.ARG_CALENDAR: printShowCalendars,
|
|
Cmd.ARG_CALENDARACL: printShowCalendarACLs,
|
|
Cmd.ARG_CALSETTINGS: printShowCalSettings,
|
|
Cmd.ARG_CHATEVENT: printShowChatEvents,
|
|
Cmd.ARG_CHATMEMBER: printShowChatMembers,
|
|
Cmd.ARG_CHATMESSAGE: printShowChatMessages,
|
|
Cmd.ARG_CHATSPACE: printShowChatSpaces,
|
|
Cmd.ARG_CLASSROOMINVITATION: printShowClassroomInvitations,
|
|
Cmd.ARG_CLASSROOMPROFILE: printShowClassroomProfile,
|
|
Cmd.ARG_CONTACTDELEGATE: printShowContactDelegates,
|
|
Cmd.ARG_COUNT: showCountUser,
|
|
Cmd.ARG_CSEIDENTITY: printShowCSEIdentities,
|
|
Cmd.ARG_CSEKEYPAIR: printShowCSEKeyPairs,
|
|
Cmd.ARG_LOOKERSTUDIOASSET: printShowLookerStudioAssets,
|
|
Cmd.ARG_LOOKERSTUDIOPERMISSION: printShowLookerStudioPermissions,
|
|
Cmd.ARG_DELEGATE: printShowDelegates,
|
|
Cmd.ARG_DISKUSAGE: printDiskUsage,
|
|
Cmd.ARG_DRIVEACTIVITY: printDriveActivity,
|
|
Cmd.ARG_DRIVEFILEACL: printShowDriveFileACLs,
|
|
Cmd.ARG_DRIVELABEL: printShowDriveLabels,
|
|
Cmd.ARG_DRIVELABELPERMISSION: printShowDriveLabelPermissions,
|
|
Cmd.ARG_DRIVELASTMODIFICATION: printShowDrivelastModifications,
|
|
Cmd.ARG_DRIVESETTINGS: printShowDriveSettings,
|
|
Cmd.ARG_EVENT: printShowCalendarEvents,
|
|
Cmd.ARG_FILECOMMENT: printShowFileComments,
|
|
Cmd.ARG_FILECOUNT: printShowFileCounts,
|
|
Cmd.ARG_FILEINFO: showFileInfo,
|
|
Cmd.ARG_FILELIST: printFileList,
|
|
Cmd.ARG_FILEPATH: printShowFilePaths,
|
|
Cmd.ARG_FILEREVISION: printShowFileRevisions,
|
|
Cmd.ARG_FILESHARECOUNT: printShowFileShareCounts,
|
|
Cmd.ARG_FILETREE: printShowFileTree,
|
|
Cmd.ARG_FILTER: printShowFilters,
|
|
Cmd.ARG_FOCUSTIME: printShowFocusTime,
|
|
Cmd.ARG_FORM: printShowForms,
|
|
Cmd.ARG_FORMRESPONSE: printShowFormResponses,
|
|
Cmd.ARG_FORWARD: printShowForward,
|
|
Cmd.ARG_FORWARDINGADDRESS: printShowForwardingAddresses,
|
|
Cmd.ARG_GMAILPROFILE: printShowGmailProfile,
|
|
Cmd.ARG_GROUP: printShowUserGroups,
|
|
Cmd.ARG_GROUPTREE: printShowGroupTree,
|
|
Cmd.ARG_GUARDIAN: printShowGuardians,
|
|
Cmd.ARG_IMAP: printShowImap,
|
|
Cmd.ARG_LABEL: printShowLabels,
|
|
Cmd.ARG_LANGUAGE: printShowLanguage,
|
|
Cmd.ARG_MEETCONFERENCE: printShowMeetConferences,
|
|
Cmd.ARG_MEETPARTICIPANT: printShowMeetParticipants,
|
|
Cmd.ARG_MEETRECORDING: printShowMeetRecordings,
|
|
Cmd.ARG_MEETTRANSCRIPT: printShowMeetTranscripts,
|
|
Cmd.ARG_MESSAGE: printShowMessages,
|
|
Cmd.ARG_NOTE: printShowNotes,
|
|
Cmd.ARG_OTHERCONTACT: printShowUserPeopleOtherContacts,
|
|
Cmd.ARG_OUTOFOFFICE: printShowOutOfOffice,
|
|
Cmd.ARG_PEOPLECONTACT: printShowUserPeopleContacts,
|
|
Cmd.ARG_PEOPLECONTACTGROUP: printShowUserPeopleContactGroups,
|
|
Cmd.ARG_PEOPLEPROFILE: printShowUserPeopleProfiles,
|
|
Cmd.ARG_POP: printShowPop,
|
|
Cmd.ARG_PROFILE: showProfile,
|
|
Cmd.ARG_SENDAS: printShowSendAs,
|
|
Cmd.ARG_SHAREDDRIVE: printShowSharedDrives,
|
|
Cmd.ARG_SHAREDDRIVEACLS: printShowSharedDriveACLs,
|
|
Cmd.ARG_SHAREDDRIVEINFO: infoSharedDrive,
|
|
Cmd.ARG_SHAREDDRIVETHEMES: showSharedDriveThemes,
|
|
Cmd.ARG_SHEET: infoPrintShowSheets,
|
|
Cmd.ARG_SHEETRANGE: printShowSheetRanges,
|
|
Cmd.ARG_SIGNATURE: printShowSignature,
|
|
Cmd.ARG_SITE: deprecatedUserSites,
|
|
Cmd.ARG_SITEACL: deprecatedUserSites,
|
|
Cmd.ARG_SMIME: printShowSmimes,
|
|
Cmd.ARG_TASK: printShowTasks,
|
|
Cmd.ARG_TASKLIST: printShowTasklists,
|
|
Cmd.ARG_THREAD: printShowThreads,
|
|
Cmd.ARG_TOKEN: printShowTokens,
|
|
Cmd.ARG_VAULTHOLD: printShowUserVaultHolds,
|
|
Cmd.ARG_VACATION: printShowVacation,
|
|
Cmd.ARG_WORKINGLOCATION: printShowWorkingLocation,
|
|
Cmd.ARG_YOUTUBECHANNEL: printShowYouTubeChannel,
|
|
}
|
|
),
|
|
'spam':
|
|
(Act.SPAM,
|
|
{Cmd.ARG_MESSAGE: processMessages,
|
|
Cmd.ARG_THREAD: processThreads,
|
|
}
|
|
),
|
|
'suspend':
|
|
(Act.SUSPEND,
|
|
{Cmd.ARG_USER: suspendUnsuspendUsers,
|
|
}
|
|
),
|
|
'sync':
|
|
(Act.SYNC,
|
|
{Cmd.ARG_CHATMEMBER: syncChatMembers,
|
|
Cmd.ARG_GROUP: syncUserWithGroups,
|
|
Cmd.ARG_GUARDIAN: syncGuardians,
|
|
Cmd.ARG_LICENSE: syncLicense,
|
|
Cmd.ARG_SHAREDDRIVEACLS: copySyncSharedDriveACLs,
|
|
}
|
|
),
|
|
'transfer':
|
|
(Act.TRANSFER,
|
|
{Cmd.ARG_DRIVE: transferDrive,
|
|
Cmd.ARG_CALENDAR: transferCalendars,
|
|
Cmd.ARG_OWNERSHIP: transferOwnership,
|
|
}
|
|
),
|
|
'trash':
|
|
(Act.TRASH,
|
|
{Cmd.ARG_DRIVEFILE: trashDriveFile,
|
|
Cmd.ARG_MESSAGE: processMessages,
|
|
Cmd.ARG_THREAD: processThreads,
|
|
}
|
|
),
|
|
'undelete':
|
|
(Act.UNDELETE,
|
|
{Cmd.ARG_USER: undeleteUsers,
|
|
}
|
|
),
|
|
'unhide':
|
|
(Act.UNHIDE,
|
|
{Cmd.ARG_SHAREDDRIVE: hideUnhideSharedDrive,
|
|
}
|
|
),
|
|
'unsuspend':
|
|
(Act.UNSUSPEND,
|
|
{Cmd.ARG_USER: suspendUnsuspendUsers,
|
|
}
|
|
),
|
|
'untrash':
|
|
(Act.UNTRASH,
|
|
{Cmd.ARG_DRIVEFILE: untrashDriveFile,
|
|
Cmd.ARG_MESSAGE: processMessages,
|
|
Cmd.ARG_THREAD: processThreads,
|
|
}
|
|
),
|
|
'update':
|
|
(Act.UPDATE,
|
|
{Cmd.ARG_BACKUPCODE: updateBackupCodes,
|
|
Cmd.ARG_CALATTENDEES: updateCalendarAttendees,
|
|
Cmd.ARG_CALENDAR: updateCalendars,
|
|
Cmd.ARG_CALENDARACL: updateCalendarACLs,
|
|
Cmd.ARG_CHATMEMBER: deleteUpdateChatMember,
|
|
Cmd.ARG_CHATMESSAGE: updateChatMessage,
|
|
Cmd.ARG_CHATSPACE: updateChatSpace,
|
|
Cmd.ARG_CSEIDENTITY: createUpdateCSEIdentity,
|
|
Cmd.ARG_LOOKERSTUDIOPERMISSION: processLookerStudioPermissions,
|
|
Cmd.ARG_DELEGATE: updateDelegates,
|
|
Cmd.ARG_DOCUMENT: updateGoogleDocument,
|
|
Cmd.ARG_DRIVEFILE: updateDriveFile,
|
|
Cmd.ARG_DRIVEFILEACL: updateDriveFileACLs,
|
|
Cmd.ARG_EVENT: updateCalendarEvents,
|
|
Cmd.ARG_FILEREVISION: updateFileRevisions,
|
|
Cmd.ARG_FORM: updateForm,
|
|
Cmd.ARG_GROUP: updateUserGroups,
|
|
Cmd.ARG_LABEL: updateLabels,
|
|
Cmd.ARG_LABELID: updateLabelSettingsById,
|
|
Cmd.ARG_LABELSETTINGS: updateLabelSettings,
|
|
Cmd.ARG_LICENSE: updateLicense,
|
|
Cmd.ARG_MEETSPACE: updateMeetSpace,
|
|
Cmd.ARG_OTHERCONTACT: processUserPeopleOtherContacts,
|
|
Cmd.ARG_PEOPLECONTACT: updateUserPeopleContacts,
|
|
Cmd.ARG_PEOPLECONTACTGROUP: updateUserPeopleContactGroup,
|
|
Cmd.ARG_PEOPLECONTACTPHOTO: updateUserPeopleContactPhoto,
|
|
Cmd.ARG_PHOTO: updatePhoto,
|
|
Cmd.ARG_SENDAS: createUpdateSendAs,
|
|
Cmd.ARG_SERVICEACCOUNT: checkServiceAccount,
|
|
Cmd.ARG_SHAREDDRIVE: updateSharedDrive,
|
|
Cmd.ARG_SHEET: updateSheets,
|
|
Cmd.ARG_SHEETRANGE: updateSheetRanges,
|
|
Cmd.ARG_SMIME: updateSmime,
|
|
Cmd.ARG_SITE: deprecatedUserSites,
|
|
Cmd.ARG_SITEACL: deprecatedUserSites,
|
|
Cmd.ARG_TASK: processTasks,
|
|
Cmd.ARG_TASKLIST: processTasklists,
|
|
Cmd.ARG_USER: updateUsers,
|
|
}
|
|
),
|
|
'watch':
|
|
(Act.WATCH,
|
|
{Cmd.ARG_GMAIL: watchGmail,
|
|
}
|
|
),
|
|
'wipe':
|
|
(Act.WIPE,
|
|
{Cmd.ARG_EVENT: wipeCalendarEvents,
|
|
}
|
|
),
|
|
}
|
|
|
|
# User commands aliases
|
|
USER_COMMANDS_ALIASES = {
|
|
'del': 'delete',
|
|
'delegates': 'delegate',
|
|
'deprov': 'deprovision',
|
|
'imap4': 'imap',
|
|
'pop3': 'pop',
|
|
'sig': 'signature',
|
|
'utf': 'unicode',
|
|
'utf-8': 'unicode',
|
|
'utf8': 'unicode',
|
|
}
|
|
|
|
USER_COMMANDS_OBJ_ALIASES = {
|
|
Cmd.ARG_3LO: Cmd.ARG_TOKEN,
|
|
Cmd.ARG_ALIASES: Cmd.ARG_ALIAS,
|
|
Cmd.ARG_APPLICATIONSPECIFICPASSWORDS: Cmd.ARG_ASP,
|
|
Cmd.ARG_ANALYTICACCOUNTS: Cmd.ARG_ANALYTICACCOUNT,
|
|
Cmd.ARG_ANALYTICACCOUNTSUMMARIES: Cmd.ARG_ANALYTICACCOUNTSUMMARY,
|
|
Cmd.ARG_ANALYTICDATASTREAMS: Cmd.ARG_ANALYTICDATASTREAM,
|
|
Cmd.ARG_ANALYTICPROPERTIES: Cmd.ARG_ANALYTICPROPERTY,
|
|
Cmd.ARG_ANALYTICUAPROPERTIES: Cmd.ARG_ANALYTICUAPROPERTY,
|
|
Cmd.ARG_ASPS: Cmd.ARG_ASP,
|
|
Cmd.ARG_BACKUPCODES: Cmd.ARG_BACKUPCODE,
|
|
Cmd.ARG_CALENDARS: Cmd.ARG_CALENDAR,
|
|
Cmd.ARG_CALENDARACLS: Cmd.ARG_CALENDARACL,
|
|
Cmd.ARG_CLASSIFICATIONLABEL: Cmd.ARG_DRIVELABEL,
|
|
Cmd.ARG_CLASSIFICATIONLABELS: Cmd.ARG_DRIVELABEL,
|
|
Cmd.ARG_CLASSIFICATIONLABELPERMISSION: Cmd.ARG_DRIVELABELPERMISSION,
|
|
Cmd.ARG_CLASSIFICATIONLABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
|
|
Cmd.ARG_CLASSROOMINVITATIONS: Cmd.ARG_CLASSROOMINVITATION,
|
|
Cmd.ARG_CHATEVENTS: Cmd.ARG_CHATEVENT,
|
|
Cmd.ARG_CHATMEMBERS: Cmd.ARG_CHATMEMBER,
|
|
Cmd.ARG_CHATMESSAGES: Cmd.ARG_CHATMESSAGE,
|
|
Cmd.ARG_CHATSPACES: Cmd.ARG_CHATSPACE,
|
|
Cmd.ARG_CIMEMBER: Cmd.ARG_CIGROUPMEMBERS,
|
|
Cmd.ARG_CIMEMBERS: Cmd.ARG_CIGROUPMEMBERS,
|
|
Cmd.ARG_CONTACT: Cmd.ARG_PEOPLECONTACT,
|
|
Cmd.ARG_CONTACTS: Cmd.ARG_PEOPLECONTACT,
|
|
Cmd.ARG_CONTACTDELEGATES: Cmd.ARG_CONTACTDELEGATE,
|
|
Cmd.ARG_CONTACTGROUP: Cmd.ARG_PEOPLECONTACTGROUP,
|
|
Cmd.ARG_CONTACTGROUPS: Cmd.ARG_PEOPLECONTACTGROUP,
|
|
Cmd.ARG_CONTACTPHOTO: Cmd.ARG_PEOPLECONTACTPHOTO,
|
|
Cmd.ARG_CONTACTPHOTOS: Cmd.ARG_PEOPLECONTACTPHOTO,
|
|
Cmd.ARG_CSEIDENTITIES: Cmd.ARG_CSEIDENTITY,
|
|
Cmd.ARG_CSEKEYPAIRS: Cmd.ARG_CSEKEYPAIR,
|
|
Cmd.ARG_COUNTS: Cmd.ARG_COUNT,
|
|
Cmd.ARG_DATASTUDIOASSET: Cmd.ARG_LOOKERSTUDIOASSET,
|
|
Cmd.ARG_DATASTUDIOPERMISSION: Cmd.ARG_LOOKERSTUDIOPERMISSION,
|
|
Cmd.ARG_DATASTUDIOASSETS: Cmd.ARG_LOOKERSTUDIOASSET,
|
|
Cmd.ARG_DATASTUDIOPERMISSIONS: Cmd.ARG_LOOKERSTUDIOPERMISSION,
|
|
Cmd.ARG_LOOKERSTUDIOASSETS: Cmd.ARG_LOOKERSTUDIOASSET,
|
|
Cmd.ARG_LOOKERSTUDIOPERMISSIONS: Cmd.ARG_LOOKERSTUDIOPERMISSION,
|
|
Cmd.ARG_DELEGATES: Cmd.ARG_DELEGATE,
|
|
Cmd.ARG_DOMAINCONTACT: Cmd.ARG_PEOPLECONTACT,
|
|
Cmd.ARG_DOMAINCONTACTS: Cmd.ARG_PEOPLECONTACT,
|
|
Cmd.ARG_DRIVEFILEACLS: Cmd.ARG_DRIVEFILEACL,
|
|
Cmd.ARG_DRIVEFILESHORTCUTS: Cmd.ARG_DRIVEFILESHORTCUT,
|
|
Cmd.ARG_DRIVELABELS: Cmd.ARG_DRIVELABEL,
|
|
Cmd.ARG_DRIVELABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
|
|
Cmd.ARG_DRIVELASTMODIFICATIONS: Cmd.ARG_DRIVELASTMODIFICATION,
|
|
Cmd.ARG_EVENTS: Cmd.ARG_EVENT,
|
|
Cmd.ARG_FILECOMMENTS: Cmd.ARG_FILECOMMENT,
|
|
Cmd.ARG_FILECOUNTS: Cmd.ARG_FILECOUNT,
|
|
Cmd.ARG_FILEDRIVELABELS: Cmd.ARG_FILEDRIVELABEL,
|
|
Cmd.ARG_FILEPATHS: Cmd.ARG_FILEPATH,
|
|
Cmd.ARG_FILEREVISIONS: Cmd.ARG_FILEREVISION,
|
|
Cmd.ARG_FILESHARECOUNTS: Cmd.ARG_FILESHARECOUNT,
|
|
Cmd.ARG_FILTERS: Cmd.ARG_FILTER,
|
|
Cmd.ARG_FOCUSTIMES: Cmd.ARG_FOCUSTIME,
|
|
Cmd.ARG_FORMS: Cmd.ARG_FORM,
|
|
Cmd.ARG_FORMRESPONSES: Cmd.ARG_FORMRESPONSE,
|
|
Cmd.ARG_FORWARDS: Cmd.ARG_FORWARD,
|
|
Cmd.ARG_FORWARDINGADDRESSES: Cmd.ARG_FORWARDINGADDRESS,
|
|
Cmd.ARG_GROUPS: Cmd.ARG_GROUP,
|
|
Cmd.ARG_GROUPLIST: Cmd.ARG_GROUPSLIST,
|
|
Cmd.ARG_GROUPSMEMBERS: Cmd.ARG_GROUPMEMBERS,
|
|
Cmd.ARG_GUARDIANINVITATIONS: Cmd.ARG_GUARDIANINVITATION,
|
|
Cmd.ARG_GUARDIANINVITE: Cmd.ARG_GUARDIANINVITATION,
|
|
Cmd.ARG_GUARDIANS: Cmd.ARG_GUARDIAN,
|
|
Cmd.ARG_HOLD: Cmd.ARG_VAULTHOLD,
|
|
Cmd.ARG_HOLDS: Cmd.ARG_VAULTHOLD,
|
|
Cmd.ARG_IMAP4: Cmd.ARG_IMAP,
|
|
Cmd.ARG_INVITEGUARDIAN: Cmd.ARG_GUARDIANINVITATION,
|
|
Cmd.ARG_LABELS: Cmd.ARG_LABEL,
|
|
Cmd.ARG_LICENCE: Cmd.ARG_LICENSE,
|
|
Cmd.ARG_LICENCES: Cmd.ARG_LICENSE,
|
|
Cmd.ARG_LICENSES: Cmd.ARG_LICENSE,
|
|
Cmd.ARG_MEETCONFERENCES: Cmd.ARG_MEETCONFERENCE,
|
|
Cmd.ARG_MEETPARTICIPANTS: Cmd.ARG_MEETPARTICIPANT,
|
|
Cmd.ARG_MEETRECORDINGS: Cmd.ARG_MEETRECORDING,
|
|
Cmd.ARG_MEETTRANSCRIPTS: Cmd.ARG_MEETTRANSCRIPT,
|
|
Cmd.ARG_MEETSPACES: Cmd.ARG_MEETSPACE,
|
|
Cmd.ARG_MEMBER: Cmd.ARG_GROUPMEMBERS,
|
|
Cmd.ARG_MEMBERS: Cmd.ARG_GROUPMEMBERS,
|
|
Cmd.ARG_MESSAGES: Cmd.ARG_MESSAGE,
|
|
Cmd.ARG_NOTES: Cmd.ARG_NOTE,
|
|
Cmd.ARG_NOTEACLS: Cmd.ARG_NOTEACL,
|
|
Cmd.ARG_NOTESACL: Cmd.ARG_NOTEACL,
|
|
Cmd.ARG_NOTESACLS: Cmd.ARG_NOTEACL,
|
|
Cmd.ARG_NOTEATTACHMENTS: Cmd.ARG_NOTEATTACHMENT,
|
|
Cmd.ARG_OAUTH: Cmd.ARG_TOKEN,
|
|
Cmd.ARG_OTHERCONTACTS: Cmd.ARG_OTHERCONTACT,
|
|
Cmd.ARG_OUTOFOFFICES: Cmd.ARG_OUTOFOFFICE,
|
|
Cmd.ARG_PEOPLECONTACTS: Cmd.ARG_PEOPLECONTACT,
|
|
Cmd.ARG_PEOPLECONTACTGROUPS: Cmd.ARG_PEOPLECONTACTGROUP,
|
|
Cmd.ARG_PEOPLECONTACTPHOTOS: Cmd.ARG_PEOPLECONTACTPHOTO,
|
|
Cmd.ARG_PEOPLE: Cmd.ARG_PEOPLEPROFILE,
|
|
Cmd.ARG_PEOPLEPROFILES: Cmd.ARG_PEOPLEPROFILE,
|
|
Cmd.ARG_PERMISSIONS: Cmd.ARG_PERMISSION,
|
|
Cmd.ARG_POP3: Cmd.ARG_POP,
|
|
Cmd.ARG_SECCALS: Cmd.ARG_CALENDAR,
|
|
Cmd.ARG_SHAREDDRIVES: Cmd.ARG_SHAREDDRIVE,
|
|
Cmd.ARG_SHEETS: Cmd.ARG_SHEET,
|
|
Cmd.ARG_SHEETRANGES: Cmd.ARG_SHEETRANGE,
|
|
Cmd.ARG_SIG: Cmd.ARG_SIGNATURE,
|
|
Cmd.ARG_SITES: Cmd.ARG_SITE,
|
|
Cmd.ARG_SITEACLS: Cmd.ARG_SITEACL,
|
|
Cmd.ARG_SMIMES: Cmd.ARG_SMIME,
|
|
Cmd.ARG_TASKS: Cmd.ARG_TASK,
|
|
Cmd.ARG_TASKLISTS: Cmd.ARG_TASKLIST,
|
|
Cmd.ARG_TEAMDRIVE: Cmd.ARG_SHAREDDRIVE,
|
|
Cmd.ARG_TEAMDRIVES: Cmd.ARG_SHAREDDRIVE,
|
|
Cmd.ARG_TEAMDRIVEACLS: Cmd.ARG_SHAREDDRIVEACLS,
|
|
Cmd.ARG_TEAMDRIVEINFO: Cmd.ARG_SHAREDDRIVEINFO,
|
|
Cmd.ARG_TEAMDRIVETHEMES: Cmd.ARG_SHAREDDRIVETHEMES,
|
|
Cmd.ARG_THREADS: Cmd.ARG_THREAD,
|
|
Cmd.ARG_TOKENS: Cmd.ARG_TOKEN,
|
|
Cmd.ARG_USERS: Cmd.ARG_USER,
|
|
Cmd.ARG_VAULTHOLDS: Cmd.ARG_VAULTHOLD,
|
|
Cmd.ARG_VERIFICATIONCODES: Cmd.ARG_BACKUPCODE,
|
|
Cmd.ARG_WORKINGLOCATIONS: Cmd.ARG_WORKINGLOCATION,
|
|
Cmd.ARG_YOUTUBECHANNELS: Cmd.ARG_YOUTUBECHANNEL,
|
|
}
|
|
|
|
def showAPICallsRetryData():
|
|
if GC.Values.get(GC.SHOW_API_CALLS_RETRY_DATA) and GM.Globals[GM.API_CALLS_RETRY_DATA]:
|
|
Ind.Reset()
|
|
writeStderr(Msg.API_CALLS_RETRY_DATA)
|
|
Ind.Increment()
|
|
for k, v in sorted(iter(GM.Globals[GM.API_CALLS_RETRY_DATA].items())):
|
|
m, s = divmod(int(v[1]), 60)
|
|
h, m = divmod(m, 60)
|
|
writeStderr(formatKeyValueList(Ind.Spaces(), [k, f'{v[0]}/{h}:{m:02d}:{s:02d}'], '\n'))
|
|
Ind.Decrement()
|
|
|
|
def adjustRedirectedSTDFilesIfNotMultiprocessing():
|
|
def adjustRedirectedSTDFile(stdtype):
|
|
rdFd = GM.Globals[stdtype].get(GM.REDIRECT_FD)
|
|
rdMultiFd = GM.Globals[stdtype].get(GM.REDIRECT_MULTI_FD)
|
|
if rdFd and rdMultiFd and rdFd != rdMultiFd:
|
|
try:
|
|
rdFd.write(rdMultiFd.getvalue())
|
|
rdMultiFd.close()
|
|
GM.Globals[stdtype][GM.REDIRECT_MULTI_FD] = rdFd
|
|
if (stdtype == GM.STDOUT) and (GM.Globals.get(GM.SAVED_STDOUT) is not None):
|
|
sys.stdout = rdFd
|
|
except IOError as e:
|
|
systemErrorExit(FILE_ERROR_RC, e)
|
|
|
|
adjustRedirectedSTDFile(GM.STDOUT)
|
|
if GM.Globals[GM.STDERR].get(GM.REDIRECT_NAME) != 'stdout':
|
|
adjustRedirectedSTDFile(GM.STDERR)
|
|
else:
|
|
GM.Globals[GM.STDERR][GM.REDIRECT_MULTI_FD] = GM.Globals[GM.STDOUT][GM.REDIRECT_MULTI_FD]
|
|
|
|
def closeSTDFilesIfNotMultiprocessing(closeSTD):
|
|
def closeSTDFile(stdtype, stdfile):
|
|
rdFd = GM.Globals[stdtype].get(GM.REDIRECT_FD)
|
|
rdMultiFd = GM.Globals[stdtype].get(GM.REDIRECT_MULTI_FD)
|
|
if rdFd and rdMultiFd and (rdFd == rdMultiFd) and (rdFd != stdfile):
|
|
try:
|
|
rdFd.flush()
|
|
if closeSTD:
|
|
rdFd.close()
|
|
except BrokenPipeError:
|
|
pass
|
|
|
|
closeSTDFile(GM.STDOUT, sys.stdout)
|
|
if GM.Globals[GM.STDERR].get(GM.REDIRECT_NAME) != 'stdout':
|
|
closeSTDFile(GM.STDERR, sys.stderr)
|
|
|
|
# Process GAM command
|
|
def ProcessGAMCommand(args, processGamCfg=True, inLoop=False, closeSTD=True):
|
|
setSysExitRC(0)
|
|
Cmd.InitializeArguments(args)
|
|
Ind.Reset()
|
|
try:
|
|
logCmd = Cmd.QuotedArgumentList(args)
|
|
if checkArgumentPresent(Cmd.LOOP_CMD):
|
|
if processGamCfg and (not SetGlobalVariables()):
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
doLoop(logCmd)
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
if processGamCfg and (not SetGlobalVariables()):
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
if checkArgumentPresent(Cmd.LOOP_CMD):
|
|
doLoop(logCmd)
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
if not Cmd.ArgumentsRemaining():
|
|
doUsage()
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
CL_command = getChoice(BATCH_CSV_COMMANDS, defaultChoice=None)
|
|
if CL_command:
|
|
Act.Set(BATCH_CSV_COMMANDS[CL_command][CMD_ACTION])
|
|
if GM.Globals[GM.CMDLOG_LOGGER]:
|
|
writeGAMCommandLog(GM.Globals, logCmd, '*')
|
|
BATCH_CSV_COMMANDS[CL_command][CMD_FUNCTION]()
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
CL_command = getChoice(MAIN_COMMANDS, defaultChoice=None)
|
|
if CL_command:
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
Act.Set(MAIN_COMMANDS[CL_command][CMD_ACTION])
|
|
MAIN_COMMANDS[CL_command][CMD_FUNCTION]()
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
CL_command = getChoice(MAIN_COMMANDS_WITH_OBJECTS, defaultChoice=None)
|
|
if CL_command:
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
Act.Set(MAIN_COMMANDS_WITH_OBJECTS[CL_command][CMD_ACTION])
|
|
CL_objectName = getChoice(MAIN_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION], choiceAliases=MAIN_COMMANDS_OBJ_ALIASES)
|
|
MAIN_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION][CL_objectName]()
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
CL_command = getChoice(COMMANDS_MAP, choiceAliases=COMMANDS_ALIASES, defaultChoice=None)
|
|
if CL_command:
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
COMMANDS_MAP[CL_command]()
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
GM.Globals[GM.ENTITY_CL_START] = Cmd.Location()
|
|
entityType, entityList = getEntityToModify(crosAllowed=True, delayGet=True)
|
|
if entityType == Cmd.ENTITY_USERS:
|
|
CL_command = getChoice(list(USER_COMMANDS)+list(USER_COMMANDS_WITH_OBJECTS), choiceAliases=USER_COMMANDS_ALIASES)
|
|
if (CL_command != 'list') and (GC.Values[GC.AUTO_BATCH_MIN] > 0):
|
|
_, count, entityList = getEntityArgument(entityList)
|
|
if count > GC.Values[GC.AUTO_BATCH_MIN]:
|
|
if GM.Globals[GM.CMDLOG_LOGGER]:
|
|
writeGAMCommandLog(GM.Globals, logCmd, '*')
|
|
doAutoBatch(Cmd.ENTITY_USER, entityList, CL_command)
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
if CL_command in USER_COMMANDS:
|
|
Act.Set(USER_COMMANDS[CL_command][CMD_ACTION])
|
|
USER_COMMANDS[CL_command][CMD_FUNCTION](entityList)
|
|
else:
|
|
Act.Set(USER_COMMANDS_WITH_OBJECTS[CL_command][CMD_ACTION])
|
|
CL_objectName = getChoice(USER_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION], choiceAliases=USER_COMMANDS_OBJ_ALIASES,
|
|
defaultChoice=[Cmd.ARG_USER, NO_DEFAULT][CL_command != 'print'])
|
|
USER_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION][CL_objectName](entityList)
|
|
else:
|
|
CL_command = getChoice(list(CROS_COMMANDS)+list(CROS_COMMANDS_WITH_OBJECTS))
|
|
if (CL_command != 'list') and (GC.Values[GC.AUTO_BATCH_MIN] > 0):
|
|
_, count, entityList = getEntityArgument(entityList)
|
|
if count > GC.Values[GC.AUTO_BATCH_MIN]:
|
|
if GM.Globals[GM.CMDLOG_LOGGER]:
|
|
writeGAMCommandLog(GM.Globals, logCmd, '*')
|
|
doAutoBatch(Cmd.ENTITY_CROS, entityList, CL_command)
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
if CL_command in CROS_COMMANDS:
|
|
Act.Set(CROS_COMMANDS[CL_command][CMD_ACTION])
|
|
CROS_COMMANDS[CL_command][CMD_FUNCTION](entityList)
|
|
else:
|
|
Act.Set(CROS_COMMANDS_WITH_OBJECTS[CL_command][CMD_ACTION])
|
|
CL_objectName = getChoice(CROS_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION], choiceAliases=CROS_COMMANDS_OBJ_ALIASES,
|
|
defaultChoice=NO_DEFAULT)
|
|
CROS_COMMANDS_WITH_OBJECTS[CL_command][CMD_FUNCTION][CL_objectName](entityList)
|
|
sys.exit(GM.Globals[GM.SYSEXITRC])
|
|
except KeyboardInterrupt:
|
|
batchWriteStderr('\nControl-C\n')
|
|
setSysExitRC(KEYBOARD_INTERRUPT_RC)
|
|
showAPICallsRetryData()
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
except OSError as e:
|
|
printErrorMessage(SOCKET_ERROR_RC, str(e))
|
|
showAPICallsRetryData()
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
except MemoryError:
|
|
printErrorMessage(MEMORY_ERROR_RC, Msg.GAM_OUT_OF_MEMORY)
|
|
showAPICallsRetryData()
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
except (GAPI.permissionDenied, GAPI.accessNotConfigured):
|
|
SvcAcctAPIDisabledExit()
|
|
except SystemExit as e:
|
|
GM.Globals[GM.SYSEXITRC] = e.code
|
|
if GM.Globals[GM.SYSEXITRC] != STDOUT_STDERR_ERROR_RC and not inLoop:
|
|
showAPICallsRetryData()
|
|
try:
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
except SystemExit:
|
|
pass
|
|
except Exception:
|
|
print_exc(file=sys.stderr)
|
|
setSysExitRC(UNKNOWN_ERROR_RC)
|
|
showAPICallsRetryData()
|
|
adjustRedirectedSTDFilesIfNotMultiprocessing()
|
|
if processGamCfg:
|
|
if not inLoop:
|
|
if GM.Globals.get(GM.SAVED_STDOUT) is not None:
|
|
sys.stdout = GM.Globals[GM.SAVED_STDOUT]
|
|
closeSTDFilesIfNotMultiprocessing(closeSTD)
|
|
if GM.Globals[GM.PID] == 0 and GM.Globals[GM.CMDLOG_LOGGER]:
|
|
writeGAMCommandLog(GM.Globals, logCmd, GM.Globals[GM.SYSEXITRC])
|
|
if not inLoop:
|
|
closeGAMCommandLog(GM.Globals)
|
|
return GM.Globals[GM.SYSEXITRC]
|
|
|
|
# Process GAM command
|
|
def CallGAMCommand(args, processGamCfg=True, inLoop=False, closeSTD=False):
|
|
return ProcessGAMCommand(args, processGamCfg=processGamCfg, inLoop=inLoop, closeSTD=closeSTD)
|