mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-21 06:31:37 +00:00
76542 lines
3.3 MiB
Executable File
76542 lines
3.3 MiB
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# GAM
|
|
#
|
|
# 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.00.03'
|
|
__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 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
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from filelock import FileLock
|
|
|
|
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
|
|
|
|
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 atom
|
|
import gdata.apps.service
|
|
import gdata.apps.audit
|
|
import gdata.apps.audit.service
|
|
import gdata.apps.contacts
|
|
import gdata.apps.contacts.service
|
|
import gdata.apps.sites
|
|
import gdata.apps.sites.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_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])
|
|
|
|
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):
|
|
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],
|
|
'')
|
|
return None
|
|
|
|
def accessErrorExit(cd):
|
|
systemErrorExit(INVALID_DOMAIN_RC, accessErrorMessage(cd or buildGAPIObject(API.DIRECTORY)))
|
|
|
|
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():
|
|
stderrErrorMsg(Msg.API_ACCESS_DENIED)
|
|
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 SvcAcctAPIAccessDeniedExit():
|
|
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_CLIENT_ID],
|
|
apiOrScopes,
|
|
GM.Globals[GM.CURRENT_SVCACCT_USER] or _getAdminEmail()))
|
|
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}>')
|
|
|
|
# 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:
|
|
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:
|
|
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 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
|
|
|
|
SITENAME_PATTERN = re.compile(r'^[a-z0-9\-_]+$')
|
|
SITENAME_FORMAT_REQUIRED = '[a-z,0-9,-_]+'
|
|
|
|
def validateSplitSiteName(fullSite):
|
|
siteParts = fullSite.lower().split('/', 1)
|
|
if (len(siteParts) == 1) or not siteParts[1]:
|
|
domain = GC.Values[GC.DOMAIN]
|
|
site = siteParts[0]
|
|
elif not siteParts[0]:
|
|
domain = GC.Values[GC.DOMAIN]
|
|
site = siteParts[1]
|
|
else:
|
|
domain = siteParts[0]
|
|
site = siteParts[1]
|
|
if SITENAME_PATTERN.match(site):
|
|
return (domain, site, f'{domain}/{site}')
|
|
return (domain, site, None)
|
|
|
|
def getSiteName():
|
|
if Cmd.ArgumentsRemaining():
|
|
domain, site, domainSite = validateSplitSiteName(Cmd.Current())
|
|
if domainSite:
|
|
Cmd.Advance()
|
|
return (domain, site, domainSite)
|
|
invalidArgumentExit(SITENAME_FORMAT_REQUIRED)
|
|
missingArgumentExit(SITENAME_FORMAT_REQUIRED)
|
|
|
|
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 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)))
|
|
|
|
def userSvcNotApplicableOrDriveDisabled(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)
|
|
else:
|
|
entityActionNotPerformedWarning([Ent.USER, user], errMessage, 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 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 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 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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)):
|
|
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.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_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]
|
|
# Create/set mode for oauth2.txt.lock
|
|
if not GM.Globals[GM.OAUTH2_TXT_LOCK]:
|
|
fileName = f'{GC.Values[GC.OAUTH2_TXT]}.lock'
|
|
if not os.path.isfile(fileName):
|
|
closeFile(openFile(fileName, mode=DEFAULT_FILE_APPEND_MODE))
|
|
os.chmod(fileName, 0o666)
|
|
GM.Globals[GM.OAUTH2_TXT_LOCK] = fileName
|
|
# 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>] [noescapechar <Boolean>] [quotechar <Character>]]
|
|
# [sortheaders <StringList>] [timestampcolumn <String>]
|
|
# [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)
|
|
_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_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_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):
|
|
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 softErrors:
|
|
return None
|
|
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
|
|
ClientAPIAccessDeniedExit()
|
|
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 softErrors:
|
|
return None
|
|
if not GM.Globals[GM.CURRENT_SVCACCT_USER]:
|
|
ClientAPIAccessDeniedExit()
|
|
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]
|
|
|
|
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):
|
|
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()
|
|
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'
|
|
error = makeErrorDict(http_status, GAPI.AUTH_ERROR, message)
|
|
elif status == 'PERMISSION_DENIED':
|
|
error = makeErrorDict(http_status, GAPI.PERMISSION_DENIED, message)
|
|
elif http_status == 403:
|
|
if 'quota exceeded for quota metric' in lmessage:
|
|
error = makeErrorDict(http_status, GAPI.QUOTA_EXCEEDED, 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)
|
|
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)
|
|
if displayError:
|
|
entityServiceNotApplicableWarning(Ent.USER, userEmail, 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)
|
|
entityServiceNotApplicableWarning(Ent.USER, user, 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 getSitesObject(entityType=Ent.DOMAIN, entityName=None, i=0, count=0):
|
|
if entityType == Ent.DOMAIN:
|
|
sitesObject = initGDataObject(gdata.apps.sites.service.SitesService(), API.SITES)
|
|
return (entityName or GC.Values[GC.DOMAIN], sitesObject)
|
|
userEmail, credentials = getGDataUserCredentials(API.SITES, entityName, i, count)
|
|
if not credentials:
|
|
return (userEmail, None)
|
|
if GC.Values[GC.NO_VERIFY_SSL]:
|
|
ssl._create_default_https_context = ssl._create_unverified_context
|
|
sitesObject = gdata.apps.sites.service.SitesService(source=GAM_USER_AGENT,
|
|
additional_headers={'Authorization': f'Bearer {credentials.token}'})
|
|
if GC.Values[GC.DEBUG_LEVEL] > 0:
|
|
sitesObject.debug = True
|
|
return (userEmail, sitesObject)
|
|
|
|
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())
|
|
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')
|
|
|
|
# Convert UID to email address
|
|
def convertUIDtoEmailAddress(emailAddressOrUID, cd=None, emailTypes=None,
|
|
checkForCustomerId=False, ciGroupsAPI=False, aliasAllowed=True):
|
|
if emailAddressOrUID.startswith('cbcm-browser.'):
|
|
return emailAddressOrUID
|
|
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)
|
|
|
|
CIGROUP_MEMBER_API = API.CLOUDIDENTITY_GROUPS
|
|
CIGROUP_MEMBERKEY = 'preferredMemberKey'
|
|
#CIGROUP_MEMBER_API = API.CLOUDIDENTITY_GROUPS_BETA
|
|
#CIGROUP_MEMBERKEY = 'memberKey'
|
|
|
|
def getCIGroupMemberRoleFixType(member):
|
|
''' fixes missing type and returns the highest role of member '''
|
|
if 'type' not in member:
|
|
if member[CIGROUP_MEMBERKEY]['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}', CIGROUP_MEMBERKEY: {'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
|
|
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)
|
|
|
|
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}' && 'cloudidentity.googleapis.com/groups.discussion_forum' 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):
|
|
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=f'nextPageToken,memberships(name,{CIGROUP_MEMBERKEY}(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(CIGROUP_MEMBERKEY, {}).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 = recursive = False
|
|
domains = []
|
|
rolesSet = set()
|
|
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(CIGROUP_MEMBER_API)
|
|
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=f'nextPageToken,memberships({CIGROUP_MEMBERKEY}(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(CIGROUP_MEMBERKEY, {}).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)
|
|
recursive = False
|
|
rolesSet = set()
|
|
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 <RegularExpression>] [keyvalue <String>] [delimiter <Character>]
|
|
# subkeyfield <FieldName> [keypattern <RegularExpression>] [keyvalue <String>] [delimiter <Character>]
|
|
# (matchfield|skipfield <FieldName> <RegularExpression>)*
|
|
# [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 _addInitialField(fieldsList, initialField):
|
|
if isinstance(initialField, list):
|
|
fieldsList.extend(initialField)
|
|
else:
|
|
fieldsList.append(initialField)
|
|
|
|
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 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(fieldsList, initialField)
|
|
addMappedFields(fieldsChoiceMap[myarg])
|
|
elif myarg == fieldsArg:
|
|
if not fieldsList and initialField is not None:
|
|
_addInitialField(fieldsList, initialField)
|
|
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.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)
|
|
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:
|
|
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:
|
|
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 GetFieldsListTitles(self, fieldName, fieldsChoiceMap, fieldsList, initialField=None):
|
|
if fieldName in fieldsChoiceMap:
|
|
if not fieldsList and initialField is not None:
|
|
_addInitialField(fieldsList, initialField)
|
|
self.AddField(fieldName, fieldsChoiceMap, fieldsList)
|
|
elif fieldName == 'fields':
|
|
if not fieldsList and initialField is not None:
|
|
_addInitialField(fieldsList, initialField)
|
|
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):
|
|
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('size', '0'))
|
|
|
|
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)
|
|
|
|
@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:
|
|
userSvcNotApplicableOrDriveDisabled(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()
|
|
if self.timestampColumn:
|
|
self.AddTitle(self.timestampColumn)
|
|
titlesList = self.titlesList
|
|
else:
|
|
if self.fixPaths:
|
|
self.FixPathsTitles(self.JSONtitlesList)
|
|
if not self.rows and self.nodataFields is not None:
|
|
self.FixNodataTitles()
|
|
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)
|
|
titlesList = self.JSONtitlesList
|
|
normalizeSortHeaders()
|
|
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',
|
|
}
|
|
|
|
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',
|
|
'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,
|
|
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_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(sig, frame):
|
|
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_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> <RegularExpression>)* [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> <RegularExpression>)* [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':
|
|
selectedScopes[i] = 'R'
|
|
break
|
|
i += 1
|
|
else:
|
|
i = 0
|
|
for a_scope in scopesList:
|
|
selectedScopes[i] = ' ' if a_scope.get('offByDefault', False) else '*'
|
|
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 _grantRotateRights(iam, projectId, service_account, email, account_type='serviceAccount'):
|
|
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, email],
|
|
Msg.HAS_RIGHTS_TO_ROTATE_OWN_PRIVATE_KEY.format(email, service_account))
|
|
body = {'policy': {'bindings': [{'role': 'roles/iam.serviceAccountKeyAdmin',
|
|
'members': [f'{account_type}:{email}']}]}}
|
|
callGAPI(iam.projects().serviceAccounts(), 'setIamPolicy',
|
|
resource=f'projects/{projectId}/serviceAccounts/{service_account}', body=body)
|
|
|
|
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]
|
|
_grantRotateRights(iam, projectInfo['projectId'], sa_email, sa_email)
|
|
return True
|
|
|
|
def setGAMProjectConsentScreen(httpObj, projectId, appInfo):
|
|
sys.stdout.write(Msg.SETTING_GAM_PROJECT_CONSENT_SCREEN)
|
|
iap = getAPIService(API.IAP, httpObj)
|
|
try:
|
|
callGAPI(iap.projects().brands(), 'create',
|
|
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.INVALID_ARGUMENT],
|
|
parent=f'projects/{projectId}', body=appInfo)
|
|
except (GAPI.invalidArgument, GAPI.alreadyExists):
|
|
pass
|
|
|
|
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': 'urn:ietf:wg:oauth:2.0:oob', 'grant_type': 'authorization_code'}
|
|
'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
|
|
if appInfo:
|
|
setGAMProjectConsentScreen(httpObj, projectInfo['projectId'], appInfo)
|
|
console_url = f'https://console.cloud.google.com/apis/credentials/oauthclient?project={projectInfo["projectId"]}&authuser={login_hint}'
|
|
csHttpObj = getHttpObj()
|
|
while True:
|
|
sys.stdout.write(Msg.CREATE_PROJECT_INSTRUCTIONS.format(console_url))
|
|
client_id = readStdin(Msg.ENTER_YOUR_CLIENT_ID).strip()
|
|
if not client_id:
|
|
client_id = readStdin('').strip()
|
|
sys.stdout.write(Msg.GO_BACK_TO_YOUR_BROWSER_AND_COPY_YOUR_CLIENT_SECRET_VALUE)
|
|
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')
|
|
# Deleted: "redirect_uris": ["http://localhost", "urn:ietf:wg:oauth:2.0:oob"],
|
|
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.GO_BACK_TO_YOUR_BROWSER_AND_CLICK_OK_TO_CLOSE_THE_OAUTH_CLIENT_POPUP)
|
|
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:
|
|
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 [{'projectId': pfilter[3:], 'state': 'NF'}]
|
|
except (GAPI.badRequest, GAPI.invalidArgument) as e:
|
|
entityActionFailedExit([Ent.PROJECT, pfilter], str(e))
|
|
except GAPI.permissionDenied:
|
|
return []
|
|
|
|
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):
|
|
psaId = prefix
|
|
for _ in range(3):
|
|
psaId += f'-{"".join(random.choice(LOWERNUMERIC_CHARS) for _ in range(3))}'
|
|
return psaId
|
|
|
|
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 createCmd and _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
|
|
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:
|
|
_getSvcAcctData()
|
|
if (GM.Globals[GM.SVCACCT_SCOPES_DEFINED] and
|
|
(API.CLOUDRESOURCEMANAGER in GM.Globals[GM.SVCACCT_SCOPES] or
|
|
API.CLOUDRESOURCEMANAGER_V1 in GM.Globals[GM.SVCACCT_SCOPES])): #Backwards compatibility hack
|
|
# Removed 6.21.05
|
|
# _, crm = buildGAPIServiceObject(API.CLOUDRESOURCEMANAGER, login_hint)
|
|
_, 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)
|
|
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.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>]
|
|
# [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, _, projectInfo, svcAcctInfo, create_key = _getLoginHintProjectInfo(False)
|
|
_createClientSecretsOauth2service(httpObj, login_hint, {}, 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], str(e), i, count)
|
|
return {}
|
|
|
|
crm, _, login_hint, projects = _getLoginHintProjects(printShowCmd=True, readOnly=True)
|
|
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'], [])
|
|
saScopes[scope['api']].append(f'{scope["scope"]}.readonly')
|
|
checkScopesSet.add(f'{scope["scope"]}.readonly')
|
|
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()
|
|
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()
|
|
|
|
def waitForCompletion(i):
|
|
sleep_time = i*5
|
|
if i > 3:
|
|
sys.stdout.write(Msg.WAITING_FOR_SERVICE_ACCOUNT_CREATION_TO_COMPLETE_SLEEPING.format(sleep_time))
|
|
time.sleep(sleep_time)
|
|
|
|
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
|
|
waitForCompletion(retry)
|
|
except GAPI.permissionDenied:
|
|
if retry == maxRetries:
|
|
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
|
return False
|
|
waitForCompletion(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):
|
|
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
|
|
waitForCompletion(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
|
|
waitForCompletion(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
|
|
waitForCompletion(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)
|
|
warnings = 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 warnings:
|
|
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',
|
|
'classroom',
|
|
'cros',
|
|
'device_management',
|
|
'docs',
|
|
'drive',
|
|
'gmail',
|
|
'gplus',
|
|
'meet',
|
|
'sites',
|
|
}
|
|
|
|
USER_REPORT_SERVICES = {
|
|
'accounts',
|
|
'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:
|
|
accessErrorExit(None)
|
|
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 [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 = eventRowFilter = exitUserLoop = \
|
|
noAuthorizedApps = normalizeUsers = select = summary = 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 == 'summary':
|
|
summary = 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'))
|
|
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:
|
|
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:
|
|
accessErrorExit(None)
|
|
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:
|
|
accessErrorExit(None)
|
|
startDateTime += oneDay
|
|
csvPF.writeCSVfile(f'Customer Report - {tryDate}')
|
|
else: # activityReports
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if select:
|
|
pageMessage = None
|
|
normalizeUsers = True
|
|
orgUnitId = None
|
|
elif userKey == 'all':
|
|
if orgUnitId:
|
|
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
|
|
if not eventNames:
|
|
eventNames.append(None)
|
|
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', activity['actor'].get('key', UNKNOWN))
|
|
if showOrgUnit:
|
|
activity['actor']['orgUnitPath'] = userOrgUnits.get(actor, 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):
|
|
if not summary:
|
|
eventCounts.setdefault(actor, {})
|
|
eventCounts[actor].setdefault(event['name'], 0)
|
|
eventCounts[actor][event['name']] += 1
|
|
else:
|
|
eventCounts.setdefault(event['name'], 0)
|
|
eventCounts[event['name']] += 1
|
|
elif not summary:
|
|
eventCounts.setdefault(actor, {})
|
|
for event in events:
|
|
eventCounts[actor].setdefault(event['name'], 0)
|
|
eventCounts[actor][event['name']] += 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)
|
|
csvPF.SetSortTitles(['name'])
|
|
else:
|
|
if eventRowFilter:
|
|
csvPF.SetRowFilter([], GC.Values[GC.CSV_OUTPUT_ROW_FILTER_MODE])
|
|
if not summary:
|
|
csvPF.SetTitles('emailAddress')
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if eventCounts:
|
|
for actor, events in iter(eventCounts.items()):
|
|
row = {'emailAddress': actor}
|
|
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)
|
|
csvPF.SetSortTitles(['emailAddress'])
|
|
else:
|
|
csvPF.SetTitles(['event', 'count'])
|
|
if addCSVData:
|
|
csvPF.AddTitles(sorted(addCSVData.keys()))
|
|
if eventCounts:
|
|
for event in sorted(eventCounts):
|
|
row = {'event': event, 'count': eventCounts[event]}
|
|
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(tagReplacements, allowSubs):
|
|
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': ''}
|
|
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': ''}
|
|
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': ''}
|
|
else:
|
|
tagReplacements['tags'][matchTag] = {'value': matchReplacement}
|
|
|
|
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)
|
|
|
|
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>)*
|
|
# [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>)*
|
|
# [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>)*
|
|
# [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 myarg == 'replace':
|
|
_getTagReplacement(tagReplacements, False)
|
|
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> [customer_auth_token <String>] <ResoldCustomerAttribute>+
|
|
def doUpdateResoldCustomer():
|
|
res = buildGAPIObject(API.RESELLER)
|
|
customerId = getString(Cmd.OB_CUSTOMER_ID)
|
|
customerAuthToken, body = _getResoldCustomerAttr()
|
|
try:
|
|
callGAPI(res.customers(), 'patch',
|
|
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
|
customerId=customerId, body=body, customerAuthToken=customerAuthToken, 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 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:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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):
|
|
accessErrorExit(cd)
|
|
|
|
# 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):
|
|
accessErrorExit(cd)
|
|
|
|
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):
|
|
accessErrorExit(cd)
|
|
|
|
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):
|
|
accessErrorExit(cd)
|
|
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.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.badRequest, GAPI.notFound, GAPI.forbidden):
|
|
accessErrorExit(cd)
|
|
|
|
# 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):
|
|
accessErrorExit(cd)
|
|
|
|
# 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):
|
|
accessErrorExit(cd)
|
|
|
|
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.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):
|
|
accessErrorExit(cd)
|
|
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 = ','.join(set(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)
|
|
cd = buildGAPIObject(API.DIRECTORY_BETA)
|
|
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')
|
|
cd = buildGAPIObject(API.DIRECTORY_BETA)
|
|
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, response, 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, response, 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 = ','.join(set(localFieldsList))
|
|
else:
|
|
fields = ','.join(set(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 <RegularExpression>]
|
|
# [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],
|
|
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.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],
|
|
orderBy='email', fields=f'nextPageToken,users({",".join(userFields)})',
|
|
maxResults=GC.Values[GC.USER_MAX_RESULTS], **kwargs)
|
|
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 <RegularExpression>] [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)
|
|
|
|
def _processContactPhotos(function):
|
|
def _makeFilenameFromPattern():
|
|
filename = filenamePattern[:]
|
|
if subForContactId:
|
|
filename = filename.replace('#contactid#', contactId)
|
|
if subForEmail:
|
|
for email in fields.get('Emails', []):
|
|
if email.get('primary', 'false') == 'true':
|
|
filename = filename.replace('#email#', email['value'])
|
|
break
|
|
else:
|
|
filename = filename.replace('#email#', contactId)
|
|
return filename
|
|
|
|
entityType = Ent.DOMAIN
|
|
contactsManager = ContactsManager()
|
|
entityList, contactQuery, queriedContacts = _getContactEntityList(1, False)
|
|
if function in {'ChangePhoto', 'GetPhoto'}:
|
|
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 == 'GetPhoto' 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 == 'DeletePhoto':
|
|
checkForExtraneousArguments()
|
|
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.PHOTO)
|
|
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
|
|
except (GDATA.notFound, GDATA.badRequest) as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId], str(e), j, jcount)
|
|
break
|
|
except (GDATA.forbidden, GDATA.notImplemented):
|
|
entityServiceNotApplicableWarning(entityType, user)
|
|
break
|
|
except GDATA.serviceNotApplicable:
|
|
entityUnknownWarning(entityType, user)
|
|
break
|
|
try:
|
|
if function == 'ChangePhoto':
|
|
if subForContactId or subForEmail:
|
|
filename = _makeFilenameFromPattern()
|
|
filename = os.path.join(targetFolder, filename)
|
|
callGData(contactsObject, function,
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN, GDATA.NOT_IMPLEMENTED],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
media=filename, contact_entry_or_url=contact,
|
|
content_type='image/*', content_length=os.path.getsize(filename), extra_headers={'If-Match': '*'})
|
|
entityActionPerformed([entityType, user, Ent.CONTACT, contactId, Ent.PHOTO, filename])
|
|
elif function == 'GetPhoto':
|
|
if subForContactId or subForEmail:
|
|
filename = _makeFilenameFromPattern()
|
|
filename = os.path.join(targetFolder, filename)
|
|
photo_data = callGData(contactsObject, function,
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN, GDATA.NOT_IMPLEMENTED],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
contact_entry_or_url=contact)
|
|
if photo_data:
|
|
status, e = writeFileReturnError(filename, eval(photo_data), mode='wb') #pylint: disable=eval-used
|
|
if status:
|
|
entityActionPerformed([entityType, user, Ent.CONTACT, contactId, Ent.PHOTO, filename])
|
|
else:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId, Ent.PHOTO, filename], str(e))
|
|
else:
|
|
entityDoesNotHaveItemWarning([entityType, user, Ent.CONTACT, contactId, Ent.PHOTO, ''])
|
|
else: #elif function == 'DeletePhoto':
|
|
filename = ''
|
|
callGData(contactsObject, function,
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.SERVICE_NOT_APPLICABLE, GDATA.FORBIDDEN, GDATA.NOT_IMPLEMENTED],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
contact_entry_or_url=contact, extra_headers={'If-Match': '*'})
|
|
entityActionPerformed([entityType, user, Ent.CONTACT, contactId, Ent.PHOTO, filename])
|
|
except GDATA.notFound as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId, Ent.PHOTO, filename], str(e), j, jcount)
|
|
except (GDATA.badRequest, OSError, IOError) as e:
|
|
entityActionFailedWarning([entityType, user, Ent.CONTACT, contactId, Ent.PHOTO, filename], str(e), j, jcount)
|
|
except (GDATA.forbidden, GDATA.notImplemented):
|
|
entityServiceNotApplicableWarning(entityType, user)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
# gam update contactphotos <ContactEntity>|<ContactSelection>
|
|
# [drivedir|(sourcefolder <FilePath>)] [filename <FileNamePattern>]
|
|
def doUpdateDomainContactPhoto():
|
|
_processContactPhotos('ChangePhoto')
|
|
|
|
# gam get contactphotos <ContactEntity>|<ContactSelection>
|
|
# [drivedir|(targetfolder <FilePath>)] [filename <FileNamePattern>]
|
|
def doGetDomainContactPhoto():
|
|
_processContactPhotos('GetPhoto')
|
|
|
|
# gam delete contactphotos <ContactEntity>|<ContactSelection>
|
|
def doDeleteDomainContactPhoto():
|
|
_processContactPhotos('DeletePhoto')
|
|
|
|
# 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 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):
|
|
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)
|
|
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])
|
|
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])
|
|
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.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.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.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.forbidden:
|
|
entityServiceNotApplicableWarning(entityType, 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.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.serviceNotAvailable, GAPI.forbidden):
|
|
ClientAPIAccessDeniedExit()
|
|
Ind.Decrement()
|
|
|
|
# gam <UserTypeEntity> clear contacts <PeopleResourceNameEntity>|<PeopleUserContactSelection>
|
|
# [emailclearpattern <RegularExpression>] [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.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.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)})
|
|
|
|
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)
|
|
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.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:
|
|
fieldsList = [PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP[field.lower()] for field in fieldsList if field.lower() in PEOPLE_OTHER_CONTACTS_FIELDS_CHOICE_MAP]
|
|
ofields = ','.join(set(fieldsList))
|
|
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.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.serviceNotAvailable, GAPI.forbidden, GAPI.permissionDenied):
|
|
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.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 peoplecontacts [todrive <ToDriveAttribute>*]
|
|
# [sources <PeopleSourceName>]
|
|
# [query <String>]
|
|
# [mergesources <PeopleMergeSourceName>]
|
|
# [countsonly]
|
|
# [allfields|(fields <PeopleFieldNameList>)] [showmetadata]
|
|
# [formatjson [quotechar <Character>]]
|
|
# gam show 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.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
|
|
|
|
entityType = Ent.USER
|
|
peopleEntityType = Ent.PEOPLE_CONTACT
|
|
sources = [PEOPLE_READ_SOURCES_CHOICE_MAP['contact']]
|
|
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]
|
|
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,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.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.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 (OSError, IOError) as e:
|
|
entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], str(e), j, jcount)
|
|
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 <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:
|
|
entityServiceNotApplicableWarning(entityType, 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:
|
|
entityServiceNotApplicableWarning(entityType, 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:
|
|
entityServiceNotApplicableWarning(entityType, 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:
|
|
entityServiceNotApplicableWarning(entityType, 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.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):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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):
|
|
entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
|
continue
|
|
jcount = len(delegates)
|
|
if not csvPF:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.CONTACT_DELEGATE, i, count)
|
|
if not csvStyle:
|
|
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()):
|
|
queries[i] = query.replace(f'#{queryTimeName}#', queryTimeValue)
|
|
|
|
# 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 = ','.join(set(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 = {'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)
|
|
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']
|
|
BROWSER_FULL_ACCESS_FIELDS = {'browsers', 'lastDeviceUsers', 'lastStatusReportTime', 'machinePolicies'}
|
|
|
|
# gam info browser <DeviceID>
|
|
# [basic|full|annotated] <BrowserFieldName>* [fields <BrowserFieldNameList>]
|
|
# [formatjson]
|
|
def doInfoBrowsers():
|
|
cbcm = buildGAPIObject(API.CBCM)
|
|
customerId = _getCustomerIdNoC()
|
|
deviceId = getString(Cmd.OB_DEVICE_ID)
|
|
projection = 'BASIC'
|
|
fieldsList = []
|
|
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
|
|
else:
|
|
FJQC.GetFormatJSON(myarg)
|
|
if projection == 'BASIC' and set(fieldsList).intersection(BROWSER_FULL_ACCESS_FIELDS):
|
|
projection = 'FULL'
|
|
fields = ','.join(set(fieldsList))
|
|
try:
|
|
browser = callGAPI(cbcm.chromebrowsers(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=customerId, deviceId=deviceId, projection=projection, fields=fields)
|
|
_showBrowser(browser, FJQC)
|
|
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)
|
|
|
|
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|allfields|annotated] <BrowserFieldName>* [fields <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|allfields|annotated] <BrowserFieldName>* [fields <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 = []
|
|
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
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, True)
|
|
if projection == 'BASIC' and set(fieldsList).intersection(BROWSER_FULL_ACCESS_FIELDS):
|
|
projection = 'FULL'
|
|
fields = getItemFieldsFromFieldsList('browsers', fieldsList)
|
|
if FJQC.formatJSON:
|
|
sortHeaders = False
|
|
substituteQueryTimes(queries, queryTimes)
|
|
if entityList is None:
|
|
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_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.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)
|
|
j = 0
|
|
for deviceId in entityList:
|
|
j += 1
|
|
try:
|
|
browser = callGAPI(cbcm.chromebrowsers(), 'get',
|
|
throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN],
|
|
customer=customerId, deviceId=deviceId, projection=projection, fields=fields)
|
|
_printBrowser(browser)
|
|
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', '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:
|
|
baserow = {'User': user, 'space.name': parent['name'], 'space.displayName': parent['displayName']} if user is not None else {'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_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>]
|
|
# [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()),
|
|
'memberships': []}
|
|
members = []
|
|
tbody = {}
|
|
returnIdOnly = False
|
|
updateMask = set()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if getChatSpaceParameters(myarg, body['space'], CHAT_SPACE_TYPE_MAP, updateMask):
|
|
pass
|
|
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']
|
|
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)
|
|
else: # DIRECT_MESSAGE
|
|
body['space'].pop('displayName', None)
|
|
body['space'].pop('spaceDetails', 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],
|
|
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
|
|
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',
|
|
}
|
|
|
|
# gam <UserTypeEntity> update chatspace <ChatSpace>
|
|
# [restricted|(audience <String>)]|
|
|
# ([displayname <String>]
|
|
# [type space]
|
|
# [description <String>] [guidelines|rules <String>]
|
|
# [history <Boolean>])
|
|
# [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
|
|
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],
|
|
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)
|
|
|
|
# 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],
|
|
name=name, **kwargsUAA)
|
|
entityActionPerformed(kvList, i, count)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), 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",
|
|
"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],
|
|
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)
|
|
|
|
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],
|
|
fields=fields, pageSize=CHAT_PAGE_SIZE, **kwargsCS)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.internalError,
|
|
GAPI.permissionDenied, GAPI.failedPrecondition) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), 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(cd, user, userList):
|
|
userList.append(convertEmailAddressToUID(user, cd, emailType='user'))
|
|
|
|
def normalizeGroupMember(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.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
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.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
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))
|
|
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(cd, getEmailAddress(returnUIDprefix='uid:'), userList)
|
|
elif myarg in {'member', 'members'}:
|
|
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
for user in members:
|
|
normalizeUserMember(cd, user, userList)
|
|
elif myarg == 'group':
|
|
normalizeGroupMember(cd, getEmailAddress(returnUIDprefix='uid:'), groupList)
|
|
elif myarg == 'groups':
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
normalizeGroupMember(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(cd, getEmailAddress(returnUIDprefix='uid:'), userList)
|
|
elif myarg in {'member', 'members'}:
|
|
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
for user in members:
|
|
normalizeUserMember(cd, user, userList)
|
|
elif deleteMode and myarg == 'group':
|
|
normalizeGroupMember(cd, getEmailAddress(returnUIDprefix='uid:'), groupList)
|
|
elif deleteMode and myarg == 'groups':
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
normalizeGroupMember(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.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
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)
|
|
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):
|
|
normalizeUserMember(cd, user, userList)
|
|
usersSpecified = True
|
|
elif myarg in {'member', 'members'}:
|
|
_, members = getEntityToModify(defaultEntityType=Cmd.ENTITY_USERS)
|
|
for user in members:
|
|
normalizeUserMember(cd, user, userList)
|
|
usersSpecified = True
|
|
elif myarg == 'groups':
|
|
for group in getEntityList(Cmd.OB_GROUP_ENTITY):
|
|
normalizeGroupMember(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],
|
|
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
|
|
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.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
|
|
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)
|
|
|
|
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],
|
|
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],
|
|
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, GAPI.failedPrecondition) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), 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],
|
|
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)
|
|
|
|
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],
|
|
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)
|
|
|
|
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],
|
|
name=name)
|
|
entityActionPerformed(kvList, i, count)
|
|
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
|
|
exitIfChatNotConfigured(chat, kvList, str(e), 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],
|
|
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)
|
|
|
|
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],
|
|
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],
|
|
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
|
|
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],
|
|
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)
|
|
|
|
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],
|
|
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],
|
|
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
|
|
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):
|
|
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'
|
|
}
|
|
|
|
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'
|
|
}
|
|
|
|
# [accesstype open|trusted|restricted]
|
|
# [entrypointaccess all|creatorapponly]
|
|
# [moderation <Boolean>]
|
|
# [chatrestriction hostsonly|norestriction]
|
|
# [reactionrestriction hostsonly|norestriction]
|
|
# [presentrestriction hostsonly|norestriction]
|
|
# [defaultjoinasviewer <Boolean>]
|
|
# [firstjoiner hostsonly|anyone]
|
|
def _getMeetSpaceParameters(myarg, body, updateMask):
|
|
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('moderationRestictions', {})
|
|
body['config']['moderationRestrictions'][option] = getChoice(MEET_SPACE_RESTRICTIONS_CHOICES_MAP, mapChoice=True)
|
|
option = f'moderationRestrictions.{option}'
|
|
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)
|
|
updateMask.append(f'config.{option}')
|
|
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
|
|
updateMask = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if _getMeetSpaceParameters(myarg, body, updateMask):
|
|
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': {}}
|
|
updateMask = []
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if (myarg == 'space' or myarg.startswith('spaces/') or myarg.startswith('space/')):
|
|
name = getSpaceName(myarg)
|
|
elif _getMeetSpaceParameters(myarg, body, updateMask):
|
|
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=','.join(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 updatePolicyRequests(body, targetResource, printer_id, app_id):
|
|
for request in body['requests']:
|
|
request.setdefault('policyTargetKey', {})
|
|
request['policyTargetKey']['targetResource'] = targetResource
|
|
if printer_id:
|
|
request['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printer_id}
|
|
elif app_id:
|
|
request['policyTargetKey']['additionalTargetKeys'] = {'app_id': app_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', {})
|
|
for atk in jsonData['additionalTargetKeys']:
|
|
body['requests'][-1]['policyTargetKey']['additionalTargetKeys'] = {atk['name']: atk['value']}
|
|
if not targetResource:
|
|
missingArgumentExit('ou|org|orgunit|group')
|
|
count = len(body['requests'])
|
|
if count != 1:
|
|
entityPerformActionNumItems([entityType, targetName], count, Ent.CHROME_POLICY)
|
|
if count == 0:
|
|
return
|
|
updatePolicyRequests(body, targetResource, printer_id, app_id)
|
|
kvList = [entityType, targetName, Ent.CHROME_POLICY, ','.join(schemaNameList)]
|
|
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.BrowserSwitcherDelayDuration':
|
|
{'browserswitcherdelayduration':
|
|
{'casedField': 'browserSwitcherDelayDuration',
|
|
'type': 'duration', 'minVal': 0, 'maxVal': 30, '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.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': ''})
|
|
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:
|
|
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', {})
|
|
for atk in jsonData['additionalTargetKeys']:
|
|
body['requests'][-1]['policyTargetKey']['additionalTargetKeys'] = {atk['name']: atk['value']}
|
|
if atk['name'] == 'app_id':
|
|
schemaNameAppId += f"({atk['value']})"
|
|
schemaNameList.append(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':
|
|
value = f"{schema['settings'][lowerField]['enum_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
|
|
schemaNameList.append(schemaName)
|
|
# 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},'
|
|
if not targetResource:
|
|
missingArgumentExit('ou|org|orgunit|group')
|
|
count = len(body['requests'])
|
|
if count > 0 and not body['requests'][-1]['updateMask']:
|
|
body['requests'].pop()
|
|
kvList = [entityType, targetName, Ent.CHROME_POLICY, ','.join(schemaNameList)]
|
|
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 printerId:
|
|
norm['printerId'] = printerId
|
|
elif appId:
|
|
norm['appId'] = appId
|
|
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):
|
|
value = ','.join(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)
|
|
appId = groupEmail = orgUnit = printerId = 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 (not printerId and not appId) and myarg == 'printerid':
|
|
printerId = getString(Cmd.OB_PRINTER_ID)
|
|
elif (not printerId and not appId) and myarg == 'appid':
|
|
appId = 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)
|
|
if not targetResource:
|
|
missingArgumentExit('ou|org|orgunit|group')
|
|
body = {'policyTargetKey': {'targetResource': targetResource}}
|
|
if printerId:
|
|
body['policyTargetKey']['additionalTargetKeys'] = {'printer_id': printerId}
|
|
if not psFilters:
|
|
psFilters = ['chrome.printers.*']
|
|
elif appId:
|
|
body['policyTargetKey']['additionalTargetKeys'] = {'app_id': appId}
|
|
if not psFilters:
|
|
psFilters = ['chrome.users.apps.*',
|
|
'chrome.devices.kiosk.apps.*',
|
|
'chrome.devices.managedguest.apps.*',
|
|
]
|
|
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 printerId:
|
|
csvPF.AddSortTitles(['printerId'])
|
|
elif appId:
|
|
csvPF.AddSortTitles(['appId'])
|
|
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 = [entityType, targetName]
|
|
if printerId:
|
|
kvList.extend([Ent.PRINTER_ID, printerId])
|
|
elif appId:
|
|
kvList.extend([Ent.APP_ID, appId])
|
|
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 = {}
|
|
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',
|
|
'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.WriteRow(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': {'cloudidentity.googleapis.com/groups.discussion_forum': ''},
|
|
}
|
|
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 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 <RegularExpression>]
|
|
# [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, response, 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, response, 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, response, 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'] = {'cloudidentity.googleapis.com/groups.discussion_forum': '',
|
|
'cloudidentity.googleapis.com/groups.security': ''}
|
|
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_TYPES_MAP = {
|
|
'customer': Ent.TYPE_CUSTOMER,
|
|
'group': Ent.TYPE_GROUP,
|
|
'user': Ent.TYPE_USER,
|
|
}
|
|
ALL_GROUP_TYPES = {Ent.TYPE_CUSTOMER, Ent.TYPE_GROUP, Ent.TYPE_USER}
|
|
|
|
def getGroupTypes(myarg, typesSet):
|
|
if myarg in {'type', 'types'}:
|
|
for gtype in getString(Cmd.OB_GROUP_TYPE_LIST).lower().replace(',', ' ').split():
|
|
if gtype in GROUP_TYPES_MAP:
|
|
typesSet.add(GROUP_TYPES_MAP[gtype])
|
|
else:
|
|
invalidChoiceExit(gtype, GROUP_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(CIGROUP_MEMBERKEY, {}).get('id', '')):
|
|
return memberOptions[MEMBEROPTION_DISPLAYMATCH]
|
|
return not memberOptions[MEMBEROPTION_DISPLAYMATCH]
|
|
|
|
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()
|
|
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 getGroupRoles(myarg, rolesSet):
|
|
getUsers = True
|
|
elif getGroupTypes(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_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)
|
|
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)):
|
|
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:
|
|
printKeyValueList([member.get('role', Ent.ROLE_MEMBER).lower(), f'{member.get("email", member["id"])} ({member["type"].lower()})'])
|
|
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]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [types <GroupTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <RegularExpression>]
|
|
# [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
|
|
|
|
def initGroupMemberDisplayOptions():
|
|
return {'members': False, 'membersCountOnly': False,
|
|
'managers': False, 'managersCountOnly': False,
|
|
'owners': False, 'ownersCountOnly': False,
|
|
'totalCount': False}
|
|
|
|
def getGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
def setMemberDisplayOptionsRole():
|
|
if Ent.ROLE_MEMBER in rolesSet:
|
|
memberDisplayOptions['members'] = True
|
|
if Ent.ROLE_MANAGER in rolesSet:
|
|
memberDisplayOptions['managers'] = True
|
|
if Ent.ROLE_OWNER in rolesSet:
|
|
memberDisplayOptions['owners'] = 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 in {'members', 'memberscount'}:
|
|
rolesSet.add(Ent.ROLE_MEMBER)
|
|
memberDisplayOptions['members'] = True
|
|
if myarg == 'memberscount':
|
|
memberDisplayOptions['membersCountOnly'] = True
|
|
elif myarg in {'managers', 'managerscount'}:
|
|
rolesSet.add(Ent.ROLE_MANAGER)
|
|
memberDisplayOptions['managers'] = True
|
|
if myarg == 'managerscount':
|
|
memberDisplayOptions['managersCountOnly'] = True
|
|
elif myarg in {'owners', 'ownerscount'}:
|
|
rolesSet.add(Ent.ROLE_OWNER)
|
|
memberDisplayOptions['owners'] = True
|
|
if myarg == 'ownerscount':
|
|
memberDisplayOptions['ownersCountOnly'] = True
|
|
elif myarg == 'totalcount':
|
|
memberDisplayOptions['totalCount'] = True
|
|
elif myarg == 'countsonly':
|
|
memberDisplayOptions['membersCountOnly'] = memberDisplayOptions['managersCountOnly'] = memberDisplayOptions['ownersCountOnly'] = True
|
|
else:
|
|
return False
|
|
return True
|
|
|
|
def setMemberDisplayTitles(memberDisplayOptions, csvPF):
|
|
if memberDisplayOptions['totalCount']:
|
|
csvPF.AddTitles('TotalCount')
|
|
if memberDisplayOptions['members']:
|
|
csvPF.AddTitles('MembersCount')
|
|
if not memberDisplayOptions['membersCountOnly']:
|
|
csvPF.AddTitles('Members')
|
|
if memberDisplayOptions['managers']:
|
|
csvPF.AddTitles('ManagersCount')
|
|
if not memberDisplayOptions['managersCountOnly']:
|
|
csvPF.AddTitles('Managers')
|
|
if memberDisplayOptions['owners']:
|
|
csvPF.AddTitles('OwnersCount')
|
|
if not memberDisplayOptions['ownersCountOnly']:
|
|
csvPF.AddTitles('Owners')
|
|
|
|
def setMemberDisplaySortTitles(memberDisplayOptions, sortTitles):
|
|
if memberDisplayOptions['totalCount']:
|
|
sortTitles.append('TotalCount')
|
|
if memberDisplayOptions['members']:
|
|
sortTitles.append('MembersCount')
|
|
if not memberDisplayOptions['membersCountOnly']:
|
|
sortTitles.append('Members')
|
|
if memberDisplayOptions['managers']:
|
|
sortTitles.append('ManagersCount')
|
|
if not memberDisplayOptions['managersCountOnly']:
|
|
sortTitles.append('Managers')
|
|
if memberDisplayOptions['owners']:
|
|
sortTitles.append('OwnersCount')
|
|
if not memberDisplayOptions['ownersCountOnly']:
|
|
sortTitles.append('Owners')
|
|
|
|
def addMemberInfoToRow(row, groupMembers, typesSet, memberOptions, memberDisplayOptions, delimiter,
|
|
isSuspended, isArchived, ciGroupsAPI):
|
|
membersCount = managersCount = ownersCount = 0
|
|
if memberDisplayOptions['members']:
|
|
membersList = []
|
|
if memberDisplayOptions['managers']:
|
|
managersList = []
|
|
if memberDisplayOptions['owners']:
|
|
ownersList = []
|
|
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(CIGROUP_MEMBERKEY, {}).get('id', member['name'])
|
|
if not member_email:
|
|
writeStderr(f' Not sure what to do with: {member}\n')
|
|
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 == Ent.ROLE_MEMBER:
|
|
if memberDisplayOptions['members']:
|
|
membersCount += 1
|
|
if not memberDisplayOptions['membersCountOnly']:
|
|
membersList.append(member_email)
|
|
elif role == Ent.ROLE_MANAGER:
|
|
if memberDisplayOptions['managers']:
|
|
managersCount += 1
|
|
if not memberDisplayOptions['managersCountOnly']:
|
|
managersList.append(member_email)
|
|
elif role == Ent.ROLE_OWNER:
|
|
if memberDisplayOptions['owners']:
|
|
ownersCount += 1
|
|
if not memberDisplayOptions['ownersCountOnly']:
|
|
ownersList.append(member_email)
|
|
elif memberDisplayOptions['members']:
|
|
membersCount += 1
|
|
if not memberDisplayOptions['membersCountOnly']:
|
|
membersList.append(member_email)
|
|
if memberDisplayOptions['totalCount']:
|
|
row['TotalCount'] = membersCount+managersCount+ownersCount
|
|
if memberDisplayOptions['members']:
|
|
row['MembersCount'] = membersCount
|
|
if not memberDisplayOptions['membersCountOnly']:
|
|
row['Members'] = delimiter.join(membersList)
|
|
if memberDisplayOptions['managers']:
|
|
row['ManagersCount'] = managersCount
|
|
if not memberDisplayOptions['managersCountOnly']:
|
|
row['Managers'] = delimiter.join(managersList)
|
|
if memberDisplayOptions['owners']:
|
|
row['OwnersCount'] = ownersCount
|
|
if not memberDisplayOptions['ownersCountOnly']:
|
|
row['Owners'] = delimiter.join(ownersList)
|
|
|
|
PRINT_GROUPS_JSON_TITLES = ['email', 'JSON']
|
|
|
|
# gam print groups [todrive <ToDriveAttribute>*]
|
|
# [([domain|domains <DomainNameEntity>] ([member|showownedby <EmailItem>]|[(query <QueryGroup>)|(queries <QueryUserList>)]))|
|
|
# (select <GroupEntity>)]
|
|
# [emailmatchpattern [not] <RegularExpression>] [namematchpattern [not] <RegularExpression>]
|
|
# [descriptionmatchpattern [not] <RegularExpression>] (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]
|
|
# [includederivedmembership]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [types <GroupTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <RegularExpression>]
|
|
# [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(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(groupSettings, ensure_ascii=False, sort_keys=True)
|
|
groupCloudIdentity = ciGroups.get(row['email'], {})
|
|
if groupCloudIdentity:
|
|
row['JSON-cloudIdentity'] = json.dumps(groupCloudIdentity, 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 = initGroupMemberDisplayOptions()
|
|
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 == '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 getGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
pass
|
|
elif getGroupTypes(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_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[CIGROUP_MEMBERKEY].pop('id')
|
|
if not member[CIGROUP_MEMBERKEY]:
|
|
member.pop(CIGROUP_MEMBERKEY)
|
|
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])])
|
|
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, 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
|
|
if not memberOptions[MEMBEROPTION_RECURSIVE]:
|
|
if memberOptions[MEMBEROPTION_NODUPLICATES]:
|
|
for member in groupMembers:
|
|
if (_checkMemberRoleIsSuspendedIsArchived(member, validRoles, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) 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]):
|
|
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_USER:
|
|
if ((member['type'] in typesSet and
|
|
checkMemberMatch(member, memberOptions) and
|
|
_checkMemberRoleIsSuspendedIsArchived(member, validRoles, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED]) 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)
|
|
elif member['type'] == Ent.TYPE_GROUP:
|
|
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):
|
|
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, level+1, typesSet)
|
|
else:
|
|
for member in groupMembers:
|
|
if member['type'] == Ent.TYPE_USER:
|
|
if ((member['type'] in typesSet) and
|
|
checkMemberMatch(member, memberOptions) and
|
|
_checkMemberRoleIsSuspendedIsArchived(member, validRoles, memberOptions[MEMBEROPTION_ISSUSPENDED], memberOptions[MEMBEROPTION_ISARCHIVED])):
|
|
if memberOptions[MEMBEROPTION_GETDELIVERYSETTINGS]:
|
|
_getMemberDeliverySettings(member)
|
|
member['level'] = level
|
|
member['subgroup'] = groupEmail
|
|
membersList.append(member)
|
|
elif member['type'] == Ent.TYPE_GROUP:
|
|
if member['type'] in typesSet and checkMemberMatch(member, memberOptions):
|
|
member['level'] = level
|
|
member['subgroup'] = groupEmail
|
|
membersList.append(member)
|
|
getGroupMembers(cd, member['email'], memberRoles, membersList, membersSet, i, count, memberOptions, 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] <RegularExpression>] [namematchpattern [not] <RegularExpression>]
|
|
# [descriptionmatchpattern [not] <RegularExpression>]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [types <GroupTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <RegularExpression>]
|
|
# [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):
|
|
pass
|
|
return unknownName
|
|
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
people = None
|
|
memberOptions = initMemberOptions()
|
|
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 getGroupRoles(myarg, rolesSet):
|
|
pass
|
|
elif getGroupTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif csvPF.GetFieldsListTitles(myarg, GROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList):
|
|
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_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 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, 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
|
|
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 memberNames:
|
|
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]:
|
|
row['name'] = mbinfo['name'].pop('fullName')
|
|
if not mbinfo['name']:
|
|
mbinfo.pop('name')
|
|
if cacheMemberInfo:
|
|
memberNames[memberId] = row['name']
|
|
if mbinfo:
|
|
memberInfo[memberId] = mbinfo
|
|
else:
|
|
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:
|
|
csvPF.SetSortTitles(GROUPMEMBERS_DEFAULT_FIELDS)
|
|
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] <RegularExpression>] [namematchpattern [not] <RegularExpression>]
|
|
# [descriptionmatchpattern [not] <RegularExpression>]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners] [depth <Number>]
|
|
# [notsuspended|suspended] [notarchived|archived]
|
|
# [types <GroupTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <RegularExpression>]
|
|
# [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]):
|
|
if member.get('role', Ent.ROLE_MEMBER) in rolesSet and member['type'] in typesSet and checkMemberMatch(member, memberOptions):
|
|
printKeyValueList([f'{member.get("role", Ent.ROLE_MEMBER)}, {member["type"]}, {member.get("email", member["id"])}, {member.get("status", "")}'])
|
|
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()
|
|
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 getGroupRoles(myarg, rolesSet):
|
|
pass
|
|
elif getGroupTypes(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_TYPES
|
|
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>] [dynamic <QueryDynamicGroup>]
|
|
def doCreateCIGroup():
|
|
doCreateGroup(ciGroupsAPI=True)
|
|
|
|
# gam update cigroups <GroupEntity> [email <EmailAddress>]
|
|
# [copyfrom <GroupItem>] <GroupAttribute>*
|
|
# [security|makesecuritygroup|dynamicsecurity|makedynamicsecuritygroup]
|
|
# [dynamic <QueryDynamicGroup>]
|
|
# [memberrestrictions <QueryMemberRestrictions>]
|
|
# gam update cigroups <GroupEntity> 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 <RegularExpression>]
|
|
# [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(CIGROUP_MEMBERKEY, {}).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 'cloudidentity.googleapis.com/groups.dynamic' 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 = {CIGROUP_MEMBERKEY: {'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(CIGROUP_MEMBER_API)
|
|
entityType = Ent.CLOUD_IDENTITY_GROUP
|
|
csvPF = None
|
|
getBeforeUpdate = preview = False
|
|
entityList = getEntityList(Cmd.OB_GROUP_ENTITY)
|
|
CL_subCommand = getChoice(UPDATE_GROUP_SUBCMDS, defaultChoice=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'] = {'cloudidentity.googleapis.com/groups.discussion_forum': '',
|
|
'cloudidentity.googleapis.com/groups.security': ''}
|
|
elif myarg in {'dynamicsecurity', 'makedynamicsecuritygroup'}:
|
|
ci_body['labels'] = {'cloudidentity.googleapis.com/groups.discussion_forum': '',
|
|
'cloudidentity.googleapis.com/groups.dynamic': '',
|
|
'cloudidentity.googleapis.com/groups.security': ''}
|
|
elif myarg in {'locked', 'makelockedsecuritygroup'}:
|
|
ci_body['labels'] = {'cloudidentity.googleapis.com/groups.discussion_forum': '',
|
|
'cloudidentity.googleapis.com/groups.locked': '',
|
|
'cloudidentity.googleapis.com/groups.security': ''}
|
|
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 == '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:
|
|
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:
|
|
_, name, _ = convertGroupEmailToCloudID(ci, group, i, count)
|
|
if not name:
|
|
continue
|
|
if ci_body:
|
|
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
|
|
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) 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)
|
|
try:
|
|
memberName = callGAPI(ci.groups().memberships(), 'lookup',
|
|
throwReasons=GAPI.CIGROUP_GET_THROW_REASONS,
|
|
parent=parent, memberKey_id=member, 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), 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=f'nextPageToken,memberships(name,{CIGROUP_MEMBERKEY}(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(CIGROUP_MEMBERKEY, {}).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)
|
|
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=f'name,{CIGROUP_MEMBERKEY},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=f'nextPageToken,memberships(name,{CIGROUP_MEMBERKEY}(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_TYPES_MAP = {
|
|
'customer': Ent.TYPE_CUSTOMER,
|
|
'group': Ent.TYPE_GROUP,
|
|
'other': Ent.TYPE_OTHER,
|
|
'serviceaccount': Ent.TYPE_SERVICE_ACCOUNT,
|
|
'user': Ent.TYPE_USER,
|
|
}
|
|
ALL_CIGROUP_TYPES = {Ent.TYPE_CUSTOMER, Ent.TYPE_GROUP, Ent.TYPE_OTHER, Ent.TYPE_SERVICE_ACCOUNT, Ent.TYPE_USER}
|
|
|
|
def getCIGroupTypes(myarg, typesSet):
|
|
if myarg in {'type', 'types'}:
|
|
for gtype in getString(Cmd.OB_GROUP_TYPE_LIST).lower().replace(',', ' ').split():
|
|
if gtype in CIGROUP_TYPES_MAP:
|
|
typesSet.add(CIGROUP_TYPES_MAP[gtype])
|
|
else:
|
|
invalidChoiceExit(gtype, CIGROUP_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]
|
|
# [types <CIGroupTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <RegularExpression>]
|
|
# [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(CIGROUP_MEMBERKEY, {}).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(CIGROUP_MEMBER_API)
|
|
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()
|
|
cachedGroupMembers = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'quick':
|
|
getAliases = getUsers = False
|
|
elif myarg == 'nousers':
|
|
getUsers = False
|
|
elif myarg == 'membertree':
|
|
showMemberTree = True
|
|
elif getGroupRoles(myarg, rolesSet):
|
|
getUsers = True
|
|
elif getCIGroupTypes(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_TYPES
|
|
fields = getFieldsFromFieldsList(groupFieldsLists['ci'])
|
|
if not showJoinDate and not showUpdateDate:
|
|
view = 'BASIC'
|
|
pageSize = 1000
|
|
else:
|
|
view = 'FULL'
|
|
pageSize = 500
|
|
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)):
|
|
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(CIGROUP_MEMBERKEY, {}).get('id', member['name'])
|
|
getCIGroupMemberRoleFixType(member)
|
|
kvList = [member['role'].lower(), f'{memberEmail} ({member["type"].lower()})']
|
|
if showJoinDate:
|
|
kvList.extend(['joined', formatLocalTime(member['createTime']) if 'createTime' in member else UNKNOWN])
|
|
if showUpdateDate:
|
|
kvList.extend(['updated', formatLocalTime(member['updateTime']) if 'updateTime' in member else UNKNOWN])
|
|
if 'expireTime' in member:
|
|
kvList.extend(['expires', formatLocalTime(member['expireTime'])])
|
|
printKeyValueList(kvList)
|
|
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[CIGROUP_MEMBERKEY]['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)
|
|
|
|
PRINT_CIGROUPS_JSON_TITLES = ['email', 'JSON']
|
|
|
|
# gam print cigroups [todrive <ToDriveAttribute>*]
|
|
# [(cimember|ciowner <UserItem>)|(select <GroupEntity>)|(query <String>)]
|
|
# [showownedby <UserItem>]
|
|
# [emailmatchpattern [not] <RegularExpression>] [namematchpattern [not] <RegularExpression>]
|
|
# [descriptionmatchpattern [not] <RegularExpression>]
|
|
# [basic|allfields|(<CIGroupFieldName>* [fields <CIGroupFieldNameList>])]
|
|
# [roles <GroupRoleList>] [memberrestrictions]
|
|
# [members|memberscount] [managers|managerscount] [owners|ownerscount] [totalcount] [countsonly]
|
|
# [types <CIGroupTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <RegularExpression>]
|
|
# [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(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)
|
|
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(CIGROUP_MEMBER_API)
|
|
setTrueCustomerId()
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
memberRestrictions = sortHeaders = False
|
|
memberDisplayOptions = initGroupMemberDisplayOptions()
|
|
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}' && 'cloudidentity.googleapis.com/groups.discussion_forum' 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 getGroupRolesMemberDisplayOptions(myarg, rolesSet, memberDisplayOptions):
|
|
pass
|
|
elif getCIGroupTypes(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_TYPES
|
|
csvPF.SetJSONTitles(PRINT_CIGROUPS_JSON_TITLES)
|
|
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)
|
|
if query:
|
|
method = 'search'
|
|
if 'parent' not in query:
|
|
query += f" && parent == '{parent}'"
|
|
kwargs = {'query': query}
|
|
else:
|
|
method = 'list'
|
|
kwargs = {'parent': parent}
|
|
printGettingAllAccountEntities(Ent.CLOUD_IDENTITY_GROUP, query)
|
|
try:
|
|
entityList = callGAPIpages(ci.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=[CIGROUP_MEMBERKEY, '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, level, typesSet):
|
|
printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, groupName, 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, view='FULL',
|
|
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.permissionDenied, GAPI.serviceNotAvailable):
|
|
entityUnknownWarning(Ent.CLOUD_IDENTITY_GROUP, groupName, i, count)
|
|
return
|
|
if not memberOptions[MEMBEROPTION_RECURSIVE]:
|
|
if memberOptions[MEMBEROPTION_NODUPLICATES]:
|
|
for member in groupMembers:
|
|
getCIGroupMemberRoleFixType(member)
|
|
if _checkMemberRole(member, validRoles) and member['name'] not in membersSet:
|
|
membersSet.add(member['name'])
|
|
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
|
|
membersList.append(member)
|
|
else:
|
|
for member in groupMembers:
|
|
getCIGroupMemberRoleFixType(member)
|
|
if _checkMemberRole(member, validRoles):
|
|
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
|
|
membersList.append(member)
|
|
elif memberOptions[MEMBEROPTION_NODUPLICATES]:
|
|
groupMemberList = []
|
|
for member in groupMembers:
|
|
getCIGroupMemberRoleFixType(member)
|
|
if member['type'] == Ent.TYPE_USER:
|
|
if (member['type'] in typesSet and checkCIMemberMatch(member, memberOptions) and
|
|
_checkMemberRole(member, validRoles) and
|
|
member['name'] not in membersSet):
|
|
membersSet.add(member['name'])
|
|
member['level'] = level
|
|
member['subgroup'] = groupName
|
|
membersList.append(member)
|
|
elif member['type'] == Ent.TYPE_GROUP:
|
|
if member['name'] not in membersSet:
|
|
membersSet.add(member['name'])
|
|
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
|
|
member['level'] = level
|
|
member['subgroup'] = groupName
|
|
membersList.append(member)
|
|
_, gname = member['name'].rsplit('/', 1)
|
|
groupMemberList.append(f'groups/{gname}')
|
|
for member in groupMemberList:
|
|
getCIGroupMembers(ci, member, memberRoles, membersList, membersSet, i, count, memberOptions, level+1, typesSet)
|
|
else:
|
|
for member in groupMembers:
|
|
getCIGroupMemberRoleFixType(member)
|
|
if member['type'] == Ent.TYPE_USER:
|
|
if (member['type'] in typesSet and checkCIMemberMatch(member, memberOptions) and
|
|
_checkMemberRole(member, validRoles)):
|
|
member['level'] = level
|
|
member['subgroup'] = groupName
|
|
membersList.append(member)
|
|
elif member['type'] == Ent.TYPE_GROUP:
|
|
if member['type'] in typesSet and checkCIMemberMatch(member, memberOptions):
|
|
member['level'] = level
|
|
member['subgroup'] = groupName
|
|
membersList.append(member)
|
|
_, gname = member['name'].rsplit('/', 1)
|
|
getCIGroupMembers(ci, f'groups/{gname}', memberRoles, membersList, membersSet, i, count, memberOptions, level+1, typesSet)
|
|
|
|
CIGROUPMEMBERS_FIELDS_CHOICE_MAP = {
|
|
'createtime': 'createTime',
|
|
'expiretime': 'expireTime',
|
|
'id': 'name',
|
|
'memberkey': CIGROUP_MEMBERKEY,
|
|
'name': 'name',
|
|
'preferredmemberkey': CIGROUP_MEMBERKEY,
|
|
'role': 'roles',
|
|
'roles': 'roles',
|
|
'type': 'type',
|
|
'updatetime': 'updateTime',
|
|
'useremail': CIGROUP_MEMBERKEY,
|
|
}
|
|
CIGROUPMEMBERS_DEFAULT_FIELDS = [
|
|
# 'type', 'roles', 'name', 'memberkey', 'preferredmemberkey', 'createtime', 'updatetime', 'expiretime']
|
|
'type', 'roles', 'name', CIGROUP_MEMBERKEY.lower(), 'createtime', 'updatetime', 'expiretime']
|
|
CIGROUPMEMBERS_SORT_FIELDS = [
|
|
'type', 'role', 'id', 'email',
|
|
'name', f'{CIGROUP_MEMBERKEY}.id', f'{CIGROUP_MEMBERKEY}.namespace',
|
|
'createTime', 'updateTime', 'expireTime'
|
|
]
|
|
CIGROUPMEMBERS_TIME_OBJECTS = {'createTime', 'updateTime', 'expireTime'}
|
|
|
|
# gam print cigroup-members [todrive <ToDriveAttribute>*]
|
|
# [(cimember|ciowner <UserItem>)|(cigroup <GroupItem>)|(select <GroupEntity>)]
|
|
# [showownedby <UserItem>]
|
|
# [emailmatchpattern [not] <RegularExpression>] [namematchpattern [not] <RegularExpression>]
|
|
# [descriptionmatchpattern [not] <RegularExpression>]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners]
|
|
# [types <CIGroupTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <RegularExpression>]
|
|
# <CIGroupMembersFieldName>* [fields <CIGroupMembersFieldNameList>]
|
|
# [(recursive [noduplicates])|includederivedmembership] [nogroupeemail]
|
|
# [formatjson [quotechar <Character>]]
|
|
def doPrintCIGroupMembers():
|
|
ci = buildGAPIObject(CIGROUP_MEMBER_API)
|
|
setTrueCustomerId()
|
|
memberOptions = initMemberOptions()
|
|
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 = {}
|
|
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}' && 'cloudidentity.googleapis.com/groups.discussion_forum' 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 getGroupRoles(myarg, rolesSet):
|
|
pass
|
|
elif getCIGroupTypes(myarg, typesSet):
|
|
pass
|
|
elif getMemberMatchOptions(myarg, memberOptions):
|
|
pass
|
|
elif getFieldsList(myarg, CIGROUPMEMBERS_FIELDS_CHOICE_MAP, fieldsList):
|
|
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
|
|
else:
|
|
FJQC.GetFormatJSONQuoteChar(myarg, False)
|
|
if not typesSet:
|
|
typesSet = {Ent.TYPE_USER} if memberOptions[MEMBEROPTION_RECURSIVE] else ALL_CIGROUP_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 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))
|
|
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, level, typesSet)
|
|
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']
|
|
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 []
|
|
csvPF.SetSortTitles(sortTitles+CIGROUPMEMBERS_SORT_FIELDS)
|
|
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] <RegularExpression>] [namematchpattern [not] <RegularExpression>]
|
|
# [descriptionmatchpattern [not] <RegularExpression>]
|
|
# [roles <GroupRoleList>] [members] [managers] [owners] [depth <Number>]
|
|
# [types <CIGroupTypeList>]
|
|
# [memberemaildisplaypattern|memberemailskippattern <RegularExpression>]
|
|
# [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_EXTERNAL: 3}.get(key, 4)
|
|
|
|
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, view='FULL',
|
|
fields='nextPageToken,memberships(*)', pageSize=GC.Values[GC.MEMBER_MAX_RESULTS])
|
|
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 (member['type'] in typesSet and _checkMemberRole(member, rolesSet) and checkCIMemberMatch(member, memberOptions)):
|
|
memberDetails = f'{member.get("role", Ent.ROLE_MEMBER)}, {member["type"]}, {member[CIGROUP_MEMBERKEY]["id"]}'
|
|
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[CIGROUP_MEMBERKEY]['id'], depth+1)
|
|
if depth == 0 or Ent.TYPE_GROUP in typesSet:
|
|
Ind.Decrement()
|
|
|
|
ci = buildGAPIObject(CIGROUP_MEMBER_API)
|
|
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()
|
|
matchPatterns = {}
|
|
maxdepth = -1
|
|
includeDerivedMembership = False
|
|
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}' && 'cloudidentity.googleapis.com/groups.discussion_forum' 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 getGroupRoles(myarg, rolesSet):
|
|
pass
|
|
elif getCIGroupTypes(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_CIGROUP_TYPES
|
|
fields = ','.join(set(groupFieldsLists['ci']))
|
|
entityList = getCIGroupMembersEntityList(ci, entityList, query, subTitle, matchPatterns, groupFieldsLists['ci'], None)
|
|
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):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, 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.serviceNotAvailable, GAPI.authError, GAPI.notACalendarUser, GAPI.notFound) as e:
|
|
if showMessage:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e))
|
|
return None
|
|
|
|
def validateCalendar(calId, i=0, count=0):
|
|
cal = None
|
|
if not calId.endswith('.calendar.google.com'):
|
|
calId, cal = buildGAPIServiceObject(API.CALENDAR, calId, i, count, displayError=False)
|
|
if not cal:
|
|
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.notACalendarUser, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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):
|
|
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):
|
|
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_DEFAULT = 'default'
|
|
EVENT_TYPE_FOCUSTIME = 'focusTime'
|
|
EVENT_TYPE_FROMGMAIL = 'fromGmail'
|
|
EVENT_TYPE_OUTOFOFFICE = 'outOfOffice'
|
|
EVENT_TYPE_WORKINGLOCATION = 'workingLocation'
|
|
|
|
EVENT_TYPES_CHOICE_MAP = {
|
|
'default': EVENT_TYPE_DEFAULT,
|
|
'focustime': EVENT_TYPE_FOCUSTIME,
|
|
'fromgmail': EVENT_TYPE_FROMGMAIL,
|
|
'outofoffice': EVENT_TYPE_OUTOFOFFICE,
|
|
'workinglocation': EVENT_TYPE_WORKINGLOCATION,
|
|
}
|
|
|
|
#EVENT_TYPE_DEFAULT_PROPERTIES_MAP = {
|
|
# EVENT_TYPE_DEFAULT: {'eventType': EVENT_TYPE_DEFAULT},
|
|
# EVENT_TYPE_FOCUSTIME: {'eventType': EVENT_TYPE_FOCUSTIME,
|
|
# 'focusTimeProperties': {'autoDeclineMode': 'declineNone', 'declineMessage': 'Declined', 'chatStatus': 'doNotDisturb'},
|
|
# 'transparency':'opaque'},
|
|
# EVENT_TYPE_FROMGMAIL: {'eventType': EVENT_TYPE_FROMGMAIL},
|
|
# EVENT_TYPE_OUTOFOFFICE: {'eventType': EVENT_TYPE_OUTOFOFFICE,
|
|
# 'outOfOfficeProperties': {'autoDeclineMode': 'declineOnlyNewConflictingInvitations', 'declineMessage': 'Declined'},
|
|
# 'transparency':'opaque'},
|
|
# EVENT_TYPE_WORKINGLOCATION: {'eventType': EVENT_TYPE_WORKINGLOCATION,
|
|
# 'workingLocationProperties': {},
|
|
# 'visibility': 'public', 'transparency':'transparent'},
|
|
# }
|
|
|
|
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_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)
|
|
|
|
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((getREPattern(re.IGNORECASE), getString(Cmd.OB_STRING, minLen=0)))
|
|
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 == '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 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.notACalendarUser, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
return (calId, cal, None, 0)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
else:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except (GAPI.notACalendarUser, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, calId, j, jcount)
|
|
return (calId, cal, [], 0)
|
|
|
|
def _getCalendarCreateImportUpdateEventOptions(function, entityType):
|
|
body = {}
|
|
parameters = {'clearAttendees': False, 'replaceMode': 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.notACalendarUser, GAPI.notFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
return False
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, calId, i, count)
|
|
return False
|
|
if 'start' in body:
|
|
body['start']['timeZone'] = timeZone
|
|
if 'end' in body:
|
|
body['end']['timeZone'] = timeZone
|
|
return True
|
|
|
|
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],
|
|
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.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) 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, GAPI.notACalendarUser) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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']):
|
|
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']]
|
|
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],
|
|
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):
|
|
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) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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):
|
|
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.requiredAccessLevel) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except GAPI.notACalendarUser as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, i, count)
|
|
break
|
|
entityActionFailedWarning(kvListEventNewCal, Ent.TypeNameMessage(Ent.EVENT, eventId, str(e)), j, jcount)
|
|
except GAPI.notACalendarUser as e:
|
|
entityActionFailedWarning(kvList, str(e), i, count)
|
|
break
|
|
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.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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, 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, 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.notACalendarUser, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.CALENDAR, body['summary']], str(e))
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.USER, user)
|
|
|
|
# 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],
|
|
calendarId=calId)
|
|
entityActionPerformed([Ent.CALENDAR, calId], i, count)
|
|
except (GAPI.notACalendarUser, GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.requiredAccessLevel) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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.notACalendarUser, GAPI.notFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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:
|
|
showJSON(field, event[field], skipObjects, EVENT_TIME_OBJECTS)
|
|
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)
|
|
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:
|
|
_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
|
|
_showCalendarEvent(user, calId, Ent.INSTANCE, instance, l, lcount, FJQC)
|
|
Ind.Decrement()
|
|
except (GAPI.notFound, GAPI.deleted) as e:
|
|
if not checkCalendarExists(cal, calId):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, i, count)
|
|
break
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventId], str(e), j, jcount)
|
|
except (GAPI.notACalendarUser, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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)
|
|
|
|
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
|
|
|
|
# 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.notACalendarUser, GAPI.notFound, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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.notACalendarUser, GAPI.notFound, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, calId, i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Calendar Settings')
|
|
|
|
def _validateResourceId(resourceId, i, count):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
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):
|
|
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
|
|
return None
|
|
|
|
def _normalizeResourceIdGetRuleIds(resourceId, i, count, ACLScopeEntity, showAction=True):
|
|
calId = _validateResourceId(resourceId, i, count)
|
|
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)
|
|
role, ACLScopeEntity, sendNotifications = getCalendarCreateUpdateACLsOptions(True)
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
calId, ruleIds, jcount = _normalizeResourceIdGetRuleIds(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)
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
calId, ruleIds, jcount = _normalizeResourceIdGetRuleIds(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)
|
|
ACLScopeEntity = getCalendarSiteACLScopeEntity()
|
|
FJQC = _getCalendarInfoACLOptions()
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
calId, ruleIds, jcount = _normalizeResourceIdGetRuleIds(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)
|
|
csvPF, FJQC, noSelfOwner, addCSVData = _getCalendarPrintShowACLOptions(['resourceId', 'resourceEmail'])
|
|
i = 0
|
|
count = len(entityList)
|
|
for resourceId in entityList:
|
|
i += 1
|
|
calId = _validateResourceId(resourceId, i, count)
|
|
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],
|
|
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:
|
|
ClientAPIAccessDeniedExit()
|
|
nameOrIdlower = nameOrId.lower()
|
|
try:
|
|
exports = callGAPIpages(v.matters().exports(), 'list', 'exports',
|
|
throwReasons=[GAPI.FORBIDDEN],
|
|
matterId=matterId, fields='exports(id,name),nextPageToken')
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
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],
|
|
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:
|
|
ClientAPIAccessDeniedExit()
|
|
nameOrIdlower = nameOrId.lower()
|
|
try:
|
|
holds = callGAPIpages(v.matters().holds(), 'list', 'holds',
|
|
throwReasons=[GAPI.FORBIDDEN],
|
|
matterId=matterId, fields='holds(holdId,name),nextPageToken')
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
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],
|
|
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],
|
|
view='BASIC', state=state, fields='matters(matterId,name,state),nextPageToken')
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
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],
|
|
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:
|
|
ClientAPIAccessDeniedExit()
|
|
nameOrIdlower = nameOrId.lower()
|
|
try:
|
|
queries = callGAPIpages(v.matters().savedQueries(), 'list', 'savedQueries',
|
|
throwReasons=[GAPI.FORBIDDEN],
|
|
matterId=matterId, fields='savedQueries(savedQueryId,displayName),nextPageToken')
|
|
except GAPI.forbidden:
|
|
ClientAPIAccessDeniedExit()
|
|
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],
|
|
matterId=matter['matterId'], view='BASIC', fields='state')['state']
|
|
except (GAPI.notFound, GAPI.forbidden):
|
|
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',
|
|
'mail': 'MAIL',
|
|
'groups': 'GROUPS',
|
|
'hangoutschat': 'HANGOUTS_CHAT',
|
|
'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',
|
|
}
|
|
VAULT_CORPUS_EXPORT_FORMATS = {
|
|
'CALENDAR': ['ICS', 'PST'],
|
|
'DRIVE': [],
|
|
'GROUPS': ['MBOX', 'PST'],
|
|
'HANGOUTS_CHAT': ['MBOX', 'PST'],
|
|
'MAIL': ['MBOX', 'PST'],
|
|
'VOICE' : ['MBOX', 'PST'],
|
|
}
|
|
VAULT_EXPORT_REGION_MAP = {
|
|
'any': 'ANY',
|
|
'europe': 'EUROPE',
|
|
'us': 'US',
|
|
}
|
|
VAULT_CORPUS_OPTIONS_MAP = {
|
|
'CALENDAR': 'calendarOptions',
|
|
'DRIVE': 'driveOptions',
|
|
'MAIL': 'mailOptions',
|
|
'GROUPS': 'groupsOptions',
|
|
'HANGOUTS_CHAT': 'hangoutsChatOptions',
|
|
'VOICE': 'voiceOptions',
|
|
}
|
|
VAULT_CORPUS_QUERY_MAP = {
|
|
'CALENDAR': None,
|
|
'DRIVE': 'driveQuery',
|
|
'MAIL': 'mailQuery',
|
|
'GROUPS': 'groupsQuery',
|
|
'HANGOUTS_CHAT': 'hangoutsChatQuery',
|
|
'VOICE': 'voiceQuery',
|
|
}
|
|
VAULT_QUERY_ARGS = [
|
|
'corpus', 'scope', 'terms', 'start', 'starttime', 'end', 'endtime', 'timezone',
|
|
# calendar
|
|
'locationquery', 'peoplequery', 'minuswords', 'responsestatuses', 'caldendarversiondate',
|
|
# drive
|
|
'driveversiondate', 'includeshareddrives', 'includeteamdrives',
|
|
# hangoutsChat
|
|
'includerooms',
|
|
# mail
|
|
'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()
|
|
# hangoutsChat
|
|
elif myarg == 'includerooms':
|
|
query['hangoutsChatOptions'] = {'includeRooms': getBoolean()}
|
|
# mail
|
|
elif myarg == 'excludedrafts':
|
|
query['mailOptions'] = {'excludeDrafts': getBoolean()}
|
|
# 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|mail|groups|hangouts_chat|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>]
|
|
# [includerooms <Boolean>]
|
|
# [excludedrafts <Boolean>] [format mbox|pst]
|
|
# [showconfidentialmodecontent <Boolean>] [usenewexport <Boolean>] [exportlinkeddrivefiles <Boolean>]
|
|
# [covereddata calllogs|textmessages|voicemails]
|
|
# [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],
|
|
matterId=matterId, exportId=exportId)
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_EXPORT, exportNameId])
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden) 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],
|
|
matterId=matterId, exportId=exportId, fields=fields)
|
|
_showVaultExport(matterNameId, export, cd, FJQC)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden) 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],
|
|
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
|
|
except GAPI.forbidden 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],
|
|
matterId=matterId, fields=fields)
|
|
except GAPI.failedPrecondition:
|
|
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
|
|
continue
|
|
except GAPI.forbidden 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 <RegularExpression>] [objectmatchpattern <RegularExpression>]
|
|
# [copyattempts <Integer>] [retryinterval <Integer>]
|
|
# gam copy vaultexport|export <MatterItem> <ExportItem>
|
|
# [targetbucket <String>] [targetprefix <String>]
|
|
# [bucketmatchpattern <RegularExpression>] [objectmatchpattern <RegularExpression>]
|
|
# [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],
|
|
matterId=matterId, exportId=exportId)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden) 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 <RegularExpression>] [objectmatchpattern <RegularExpression>]
|
|
# [downloadattempts <Integer>] [retryinterval <Integer>]
|
|
# gam download vaultexport|export <MatterItem> <ExportItem>
|
|
# [targetfolder <FilePath>] [targetname <FileName>] [noverify] [noextract] [ziptostdout]
|
|
# [bucketmatchpattern <RegularExpression>] [objectmatchpattern <RegularExpression>]
|
|
# [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],
|
|
matterId=matterId, exportId=exportId)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden) 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],
|
|
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) 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],
|
|
matterId=matterId, holdId=holdId, fields='name,corpus,query,orgUnit')
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden) 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',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.BAD_REQUEST, GAPI.FORBIDDEN],
|
|
matterId=matterId, holdId=holdId, body=body)
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId])
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden) 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],
|
|
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 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],
|
|
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 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],
|
|
matterId=matterId, holdId=holdId)
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.VAULT_HOLD, holdNameId])
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden) 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],
|
|
matterId=matterId, holdId=holdId, fields=fields)
|
|
_showVaultHold(matterNameId, hold, cd, FJQC)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden) 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],
|
|
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
|
|
except GAPI.forbidden 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],
|
|
matterId=matterId, fields=fields)
|
|
except GAPI.failedPrecondition:
|
|
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
|
|
continue
|
|
except GAPI.forbidden 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],
|
|
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
|
|
except GAPI.forbidden 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],
|
|
matterId=matterId, fields='holds(holdId,name,accounts(accountId,email),orgUnit(orgUnitId)),nextPageToken')
|
|
except GAPI.failedPrecondition:
|
|
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
|
|
except GAPI.forbidden 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, drive):
|
|
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 drive is not None:
|
|
if 'sharedDriveInfo' in query['query']:
|
|
query['query']['sharedDriveInfo']['sharedDriveNames'] = []
|
|
for sharedDriveId in query['query']['sharedDriveInfo']['sharedDriveIds']:
|
|
query['query']['sharedDriveInfo']['sharedDriveNames'].append(_getSharedDriveNameFromId(drive, sharedDriveId, useDomainAdminAccess=True))
|
|
query['query'].pop('searchMethod', None)
|
|
query['query'].pop('teamDriveInfo', None)
|
|
|
|
VAULT_QUERY_TIME_OBJECTS = {'createTime', 'endTime', 'startTime', 'versionDate'}
|
|
|
|
def _showVaultQuery(matterNameId, query, cd, drive, FJQC, k=0, kcount=0):
|
|
_cleanVaultQuery(query, cd, drive)
|
|
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 = drive = 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)
|
|
_, drive = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
|
|
if 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],
|
|
matterId=matterId, savedQueryId=queryId, fields=fields)
|
|
_showVaultQuery(matterNameId, query, cd, drive, FJQC)
|
|
except (GAPI.notFound, GAPI.badRequest, GAPI.forbidden) 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 = drive = 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)
|
|
_, drive = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
|
|
if 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],
|
|
view='BASIC', state='OPEN', fields='matters(matterId,name,state),nextPageToken')
|
|
except GAPI.forbidden 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],
|
|
matterId=matterId, fields=fields)
|
|
except GAPI.failedPrecondition:
|
|
warnMatterNotOpen(v, matter, matterNameId, j, jcount)
|
|
continue
|
|
except GAPI.forbidden 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, drive, FJQC, k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
for query in queries:
|
|
_cleanVaultQuery(query, cd, drive)
|
|
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],
|
|
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) 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],
|
|
matterId=matterId, body=cbody)
|
|
if not returnIdOnly:
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.COLLABORATOR, collaborator['email']], j, jcount)
|
|
except (GAPI.failedPrecondition, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.VAULT_MATTER, matterNameId], str(e))
|
|
break
|
|
Ind.Decrement()
|
|
if showDetails:
|
|
_showVaultMatter(None, 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],
|
|
matterId=matterId, **action_kwargs)
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId])
|
|
except (GAPI.notFound, GAPI.failedPrecondition, GAPI.forbidden) 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],
|
|
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) 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],
|
|
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) 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],
|
|
matterId=matterId, body={'accountId': collaborator['id']})
|
|
entityActionPerformed([Ent.VAULT_MATTER, matterNameId, Ent.COLLABORATOR, collaborator['email']], j, jcount)
|
|
except (GAPI.failedPrecondition, GAPI.forbidden) 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],
|
|
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) 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],
|
|
view=view, state=stateParm, fields=fields)
|
|
except GAPI.forbidden 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')
|
|
|
|
def checkSiteExists(sitesObject, domain, site):
|
|
try:
|
|
callGData(sitesObject, 'GetSite',
|
|
throwErrors=[GDATA.NOT_FOUND],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=site)
|
|
return True
|
|
except GDATA.notFound:
|
|
return None
|
|
|
|
SITE_ACLS = 'ACLs'
|
|
SITE_CATEGORIES = 'Categories'
|
|
SITE_LINK = 'Link'
|
|
SITE_NAME = 'Name'
|
|
SITE_SITE = 'Site'
|
|
SITE_SOURCELINK = 'SourceLink'
|
|
SITE_SUMMARY = 'Summary'
|
|
SITE_THEME = 'Theme'
|
|
SITE_UPDATED = 'Updated'
|
|
SITE_WEB_ADDRESS_MAPPINGS = 'WebAddressMappings'
|
|
|
|
SITE_DATA_DOMAIN = 'domain'
|
|
SITE_DATA_SITE = 'site'
|
|
SITE_DATA_DOMAIN_SITE = 'domainSite'
|
|
SITE_DATA_FIELDS = 'fields'
|
|
|
|
class SitesManager():
|
|
|
|
SITE_ARGUMENT_TO_PROPERTY_MAP = {
|
|
'categories': SITE_CATEGORIES,
|
|
'name': SITE_NAME,
|
|
'sourcelink': SITE_SOURCELINK,
|
|
'summary': SITE_SUMMARY,
|
|
'theme': SITE_THEME,
|
|
}
|
|
|
|
@staticmethod
|
|
def AclEntryToFields(acl_entry):
|
|
|
|
def GetAclAttr(attrlist):
|
|
objAttr = acl_entry
|
|
for attr in attrlist:
|
|
objAttr = getattr(objAttr, attr)
|
|
if not objAttr:
|
|
return None
|
|
return objAttr
|
|
|
|
fields = {}
|
|
fields['role'] = GetAclAttr(['role', 'value'])
|
|
if not fields['role']:
|
|
fields['role'] = GetAclAttr(['withKey', 'role', 'value'])+' (with link)'
|
|
fields['scope'] = {'type': GetAclAttr(['scope', 'type']),
|
|
'value': GetAclAttr(['scope', 'value'])}
|
|
link = acl_entry.FindInviteLink()
|
|
if link:
|
|
fields['inviteLink'] = link
|
|
return fields
|
|
|
|
@staticmethod
|
|
def FieldsToAclEntry(fields):
|
|
acl_entry = gdata.apps.sites.AclEntry()
|
|
acl_entry.role = gdata.apps.sites.AclRole(value=fields['role'])
|
|
acl_entry.scope = gdata.apps.sites.AclScope(stype=fields['scope']['type'], value=fields['scope'].get('value'))
|
|
return acl_entry
|
|
|
|
@staticmethod
|
|
def ActivityEntryToFields(activity_entry):
|
|
fields = {}
|
|
|
|
def GetActivityField(fieldName, attrlist):
|
|
objAttr = activity_entry
|
|
for attr in attrlist:
|
|
objAttr = getattr(objAttr, attr)
|
|
if not objAttr:
|
|
return
|
|
fields[fieldName] = objAttr
|
|
|
|
def GetActivityFieldData(objAttr, attrlist, default):
|
|
for attr in attrlist:
|
|
objAttr = getattr(objAttr, attr)
|
|
if not objAttr:
|
|
return default
|
|
return objAttr
|
|
|
|
def AppendItemToFieldsList(fieldName, fieldValue):
|
|
fields.setdefault(fieldName, [])
|
|
fields[fieldName].append(fieldValue)
|
|
|
|
GetActivityField('Summary', ['title', 'text'])
|
|
GetActivityField('Updated', ['updated', 'text'])
|
|
for author in activity_entry.author:
|
|
AppendItemToFieldsList('Authors', f'{GetActivityFieldData(author, ["name", "text"], "Unknown Name")}/{GetActivityFieldData(author, ["email", "text"], "Unknown Email")}')
|
|
fields['Operation'] = activity_entry.Kind()
|
|
return fields
|
|
|
|
@staticmethod
|
|
def SiteToFields(site_entry):
|
|
fields = {}
|
|
|
|
def GetSiteField(fieldName, attrlist):
|
|
objAttr = site_entry
|
|
for attr in attrlist:
|
|
objAttr = getattr(objAttr, attr)
|
|
if not objAttr:
|
|
return
|
|
fields[fieldName] = objAttr
|
|
|
|
def AppendItemToFieldsList(fieldName, fieldValue):
|
|
fields.setdefault(fieldName, [])
|
|
fields[fieldName].append(fieldValue)
|
|
|
|
GetSiteField(SITE_SITE, ['siteName', 'text'])
|
|
GetSiteField(SITE_NAME, ['title', 'text'])
|
|
GetSiteField(SITE_SUMMARY, ['summary', 'text'])
|
|
GetSiteField(SITE_THEME, ['theme', 'text'])
|
|
GetSiteField(SITE_UPDATED, ['updated', 'text'])
|
|
if site_entry.category:
|
|
for category in site_entry.category:
|
|
if category.term:
|
|
AppendItemToFieldsList(SITE_CATEGORIES, category.term)
|
|
link = site_entry.FindAlternateLink()
|
|
if link:
|
|
fields[SITE_LINK] = link
|
|
link = site_entry.FindSourceLink()
|
|
if link:
|
|
fields[SITE_SOURCELINK] = link
|
|
for link in site_entry.FindWebAddressMappings():
|
|
AppendItemToFieldsList(SITE_WEB_ADDRESS_MAPPINGS, link)
|
|
return fields
|
|
|
|
@staticmethod
|
|
def GetSiteFields():
|
|
|
|
fields = {}
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg in SitesManager.SITE_ARGUMENT_TO_PROPERTY_MAP:
|
|
fieldName = SitesManager.SITE_ARGUMENT_TO_PROPERTY_MAP[myarg]
|
|
if fieldName == SITE_NAME:
|
|
fields[fieldName] = getString(Cmd.OB_STRING)
|
|
elif fieldName == SITE_SOURCELINK:
|
|
fields[fieldName] = getString(Cmd.OB_URI)
|
|
elif fieldName == SITE_SUMMARY:
|
|
fields[fieldName] = getStringWithCRsNLs()
|
|
elif fieldName == SITE_THEME:
|
|
fields[fieldName] = getString(Cmd.OB_STRING)
|
|
elif fieldName == SITE_CATEGORIES:
|
|
fields[fieldName] = getString(Cmd.OB_STRING, minLen=0).split(',')
|
|
else:
|
|
unknownArgumentExit()
|
|
return fields
|
|
|
|
@staticmethod
|
|
def FieldsToSite(fields):
|
|
def GetField(fieldName):
|
|
return fields.get(fieldName)
|
|
|
|
def GetSiteField(fieldName, fieldClass):
|
|
value = fields.get(fieldName)
|
|
if value:
|
|
return fieldClass(text=value)
|
|
return None
|
|
|
|
site_entry = gdata.apps.sites.SiteEntry(sourceSite=GetField(SITE_SOURCELINK))
|
|
site_entry.siteName = GetSiteField(SITE_SITE, gdata.apps.sites.SiteName)
|
|
site_entry.title = GetSiteField(SITE_NAME, atom.Title)
|
|
site_entry.summary = GetSiteField(SITE_SUMMARY, atom.Summary)
|
|
site_entry.theme = GetSiteField(SITE_THEME, gdata.apps.sites.Theme)
|
|
value = GetField(SITE_CATEGORIES)
|
|
if value:
|
|
for category in value:
|
|
site_entry.category.append(atom.Category(term=category, scheme=gdata.apps.sites.TAG_KIND_TERM))
|
|
return site_entry
|
|
|
|
def getSiteEntity():
|
|
siteEntity = {'list': getEntityList(Cmd.OB_SITE_ENTITY), 'dict': None}
|
|
if isinstance(siteEntity['list'], dict):
|
|
siteEntity['dict'] = siteEntity['list']
|
|
return siteEntity
|
|
|
|
def _validateUserGetSites(entityType, user, i, count, siteEntity, itemType=None, modifier=None):
|
|
if siteEntity['dict']:
|
|
sites = siteEntity['dict'][user]
|
|
else:
|
|
sites = siteEntity['list']
|
|
user, sitesObject = getSitesObject(entityType, user, i, count)
|
|
if not sitesObject:
|
|
return (user, None, None, 0)
|
|
jcount = len(sites)
|
|
if not itemType:
|
|
entityPerformActionNumItems([entityType, user], jcount, Ent.SITE, i, count)
|
|
else:
|
|
entityPerformActionSubItemModifierNumItems([entityType, user], itemType, modifier, jcount, Ent.SITE, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
return (user, sitesObject, sites, jcount)
|
|
|
|
def _validateSite(fullSite, i, count):
|
|
domain, site, domainSite = validateSplitSiteName(fullSite)
|
|
if domainSite:
|
|
return (domain, site, domainSite)
|
|
entityActionNotPerformedWarning([Ent.SITE, site], Msg.INVALID_SITE.format(site, SITENAME_FORMAT_REQUIRED), i, count)
|
|
return (domain, site, None)
|
|
|
|
def _validateSiteGetRuleIds(origUser, fullSite, j, jcount, ACLScopeEntity, showAction=True):
|
|
domain, site, domainSite = _validateSite(fullSite, j, jcount)
|
|
if not domainSite:
|
|
return (domain, site, None, None, 0)
|
|
if ACLScopeEntity:
|
|
if ACLScopeEntity['dict']:
|
|
if not GM.Globals[GM.CSV_SUBKEY_FIELD]:
|
|
ruleIds = ACLScopeEntity['dict'][fullSite]
|
|
else:
|
|
ruleIds = ACLScopeEntity['dict'][origUser][fullSite]
|
|
else:
|
|
ruleIds = ACLScopeEntity['list']
|
|
kcount = len(ruleIds)
|
|
if kcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
else:
|
|
ruleIds = []
|
|
kcount = 0
|
|
if showAction:
|
|
entityPerformActionNumItems([Ent.SITE, domainSite], kcount, Ent.SITE_ACL, j, jcount)
|
|
return (domain, site, domainSite, ruleIds, kcount)
|
|
|
|
def _createSite(users, entityType):
|
|
sitesManager = SitesManager()
|
|
domain, site, domainSite = getSiteName()
|
|
fields = sitesManager.GetSiteFields()
|
|
if not fields.get(SITE_NAME):
|
|
fields[SITE_NAME] = site
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sitesObject = getSitesObject(entityType, user, i, count)
|
|
if not sitesObject:
|
|
continue
|
|
try:
|
|
siteEntry = sitesManager.FieldsToSite(fields)
|
|
callGData(sitesObject, 'CreateSite',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.ENTITY_EXISTS, GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
siteentry=siteEntry, domain=domain, site=None)
|
|
entityActionPerformed([Ent.SITE, domainSite])
|
|
except GDATA.notFound as e:
|
|
entityActionFailedWarning([Ent.DOMAIN, domain], str(e))
|
|
except (GDATA.entityExists, GDATA.badRequest, GDATA.forbidden) as e:
|
|
entityActionFailedWarning([Ent.SITE, domainSite], str(e))
|
|
|
|
# gam [<UserTypeEntity>] create site <SiteName> <SiteAttribute>*
|
|
def createUserSite(users):
|
|
_createSite(users, Ent.USER)
|
|
|
|
def doCreateDomainSite():
|
|
_createSite([GC.Values[GC.DOMAIN]], Ent.DOMAIN)
|
|
|
|
def _updateSites(users, entityType):
|
|
sitesManager = SitesManager()
|
|
siteEntity = getSiteEntity()
|
|
updateFields = sitesManager.GetSiteFields()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sitesObject, sites, jcount = _validateUserGetSites(entityType, user, i, count, siteEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for site in sites:
|
|
j += 1
|
|
domain, site, domainSite = _validateSite(site, j, jcount)
|
|
if not domainSite:
|
|
continue
|
|
try:
|
|
siteEntry = callGData(sitesObject, 'GetSite',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=site)
|
|
fields = sitesManager.SiteToFields(siteEntry)
|
|
for field, value in iter(updateFields.items()):
|
|
if field != SITE_SOURCELINK:
|
|
fields[field] = value
|
|
newSiteEntry = sitesManager.FieldsToSite(fields)
|
|
callGData(sitesObject, 'UpdateSite',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
siteentry=newSiteEntry, domain=domain, site=site, extra_headers={'If-Match': siteEntry.etag})
|
|
entityActionPerformed([Ent.SITE, domainSite])
|
|
except (GDATA.notFound, GDATA.badRequest, GDATA.forbidden) as e:
|
|
entityActionFailedWarning([Ent.SITE, domainSite], str(e))
|
|
|
|
# gam [<UserTypeEntity>] update site <SiteEntity> <SiteAttribute>+
|
|
def updateUserSites(users):
|
|
_updateSites(users, Ent.USER)
|
|
|
|
def doUpdateDomainSites():
|
|
_updateSites([GC.Values[GC.DOMAIN]], Ent.DOMAIN)
|
|
|
|
SITE_FIELD_PRINT_ORDER = [
|
|
SITE_UPDATED,
|
|
SITE_NAME,
|
|
SITE_SUMMARY,
|
|
SITE_THEME,
|
|
SITE_SOURCELINK,
|
|
SITE_CATEGORIES,
|
|
SITE_LINK,
|
|
]
|
|
|
|
def _showSite(sitesManager, sitesObject, domain, site, roles, j, jcount):
|
|
fields = sitesManager.SiteToFields(site)
|
|
domainSite = f'{domain}/{fields[SITE_SITE]}'
|
|
printKeyValueListWithCount([SITE_SITE, domainSite], j, jcount)
|
|
Ind.Increment()
|
|
for field in SITE_FIELD_PRINT_ORDER:
|
|
if field in fields:
|
|
if not isinstance(fields[field], list):
|
|
if field != SITE_SUMMARY:
|
|
printKeyValueList([field, fields[field]])
|
|
else:
|
|
printKeyValueWithCRsNLs(field, fields[field])
|
|
else:
|
|
printKeyValueList([field, ','.join(fields[field])])
|
|
if fields.get(SITE_WEB_ADDRESS_MAPPINGS):
|
|
printKeyValueList([SITE_WEB_ADDRESS_MAPPINGS, None])
|
|
Ind.Increment()
|
|
for link in fields[SITE_WEB_ADDRESS_MAPPINGS]:
|
|
printKeyValueList([link, None])
|
|
Ind.Decrement()
|
|
if roles:
|
|
try:
|
|
acls = callGDataPages(sitesObject, 'GetAclFeed',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=fields[SITE_SITE])
|
|
printKeyValueList([SITE_ACLS, None])
|
|
Ind.Increment()
|
|
for acl in acls:
|
|
fields = sitesManager.AclEntryToFields(acl)
|
|
if fields['role'] in roles:
|
|
printKeyValueList([formatACLRule(fields)])
|
|
Ind.Decrement()
|
|
except GDATA.notFound as e:
|
|
entityActionFailedWarning([Ent.SITE, domainSite], str(e))
|
|
except GDATA.forbidden:
|
|
pass
|
|
Ind.Decrement()
|
|
|
|
SITE_ACL_ROLES_MAP = {
|
|
'editor': 'writer',
|
|
'invite': 'invite',
|
|
'owner': 'owner',
|
|
'read': 'reader',
|
|
'reader': 'reader',
|
|
'writer': 'writer',
|
|
}
|
|
|
|
def _infoSites(users, entityType):
|
|
siteEntity = getSiteEntity()
|
|
url_params = {}
|
|
roles = set()
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'withmappings':
|
|
url_params['with-mappings'] = 'true'
|
|
elif myarg in {'role', 'roles'}:
|
|
roles = getACLRoles(SITE_ACL_ROLES_MAP)
|
|
else:
|
|
unknownArgumentExit()
|
|
sitesManager = SitesManager()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
user, sitesObject, sites, jcount = _validateUserGetSites(entityType, user, i, count, siteEntity)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for site in sites:
|
|
j += 1
|
|
domain, site, domainSite = _validateSite(site, j, jcount)
|
|
if not domainSite:
|
|
continue
|
|
try:
|
|
result = callGData(sitesObject, 'GetSite',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=site, url_params=url_params)
|
|
if result:
|
|
_showSite(sitesManager, sitesObject, domain, result, roles, j, jcount)
|
|
except (GDATA.notFound, GDATA.forbidden) as e:
|
|
entityActionFailedWarning([Ent.SITE, domainSite], str(e))
|
|
Ind.Decrement()
|
|
|
|
# gam [<UserTypeEntity>] info site <SiteEntity> [withmappings] [role|roles all|<SiteACLRoleList>]
|
|
def infoUserSites(users):
|
|
_infoSites(users, Ent.USER)
|
|
|
|
def doInfoDomainSites():
|
|
_infoSites([GC.Values[GC.DOMAIN]], Ent.DOMAIN)
|
|
|
|
def printShowSites(entityList, entityType):
|
|
def _getSites(domain, i, count):
|
|
try:
|
|
return callGDataPages(sitesObject, 'GetSiteFeed',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, url_params=url_params)
|
|
except (GDATA.notFound, GDATA.forbidden) as e:
|
|
entityActionFailedWarning([Ent.DOMAIN, domain], str(e), i, count)
|
|
return []
|
|
|
|
def _printSites(entity, i, count, domain, sites):
|
|
for site in sites:
|
|
fields = sitesManager.SiteToFields(site)
|
|
if fields[SITE_SITE] in sitesSet:
|
|
continue
|
|
sitesSet.add(fields[SITE_SITE])
|
|
domainSite = f'{domain}/{fields[SITE_SITE]}'
|
|
siteRow = {Ent.Singular(entityType): entity, SITE_SITE: domainSite}
|
|
for field, value in iter(fields.items()):
|
|
if field != SITE_SITE:
|
|
if not isinstance(value, list):
|
|
if field != SITE_SUMMARY or not convertCRNL:
|
|
siteRow[field] = value
|
|
else:
|
|
siteRow[field] = escapeCRsNLs(value)
|
|
else:
|
|
siteRow[field] = delimiter.join(value)
|
|
rowShown = False
|
|
if roles:
|
|
try:
|
|
acls = callGDataPages(sitesObject, 'GetAclFeed',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=fields[SITE_SITE])
|
|
for acl in acls:
|
|
fields = sitesManager.AclEntryToFields(acl)
|
|
if fields['role'] in roles:
|
|
siteACLRow = siteRow.copy()
|
|
siteACLRow.update(ACLRuleDict(fields))
|
|
csvPF.WriteRowTitles(siteACLRow)
|
|
rowShown = True
|
|
except GDATA.notFound as e:
|
|
entityActionFailedWarning([Ent.SITE, domainSite], str(e), i, count)
|
|
except GDATA.forbidden:
|
|
pass
|
|
if not rowShown:
|
|
csvPF.WriteRowTitles(siteRow)
|
|
|
|
def _showSites(entity, i, count, domain, sites):
|
|
jcount = len(sites)
|
|
if entityType == Ent.USER:
|
|
entityPerformActionNumItems([entityType, entity, Ent.DOMAIN, domain], jcount, Ent.SITE, i, count)
|
|
else:
|
|
entityPerformActionNumItems([entityType, entity], jcount, Ent.SITE, i, count)
|
|
Ind.Increment()
|
|
j = 0
|
|
for site in sites:
|
|
j += 1
|
|
_showSite(sitesManager, sitesObject, domain, site, roles, j, jcount)
|
|
Ind.Decrement()
|
|
|
|
domains = []
|
|
domainLists = []
|
|
url_params = {}
|
|
includeAllSites = 'true' if entityType == Ent.DOMAIN else 'false'
|
|
roles = set()
|
|
convertCRNL = GC.Values[GC.CSV_OUTPUT_CONVERT_CR_NL]
|
|
delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER]
|
|
csvPF = CSVPrintFile([Ent.Singular(entityType), SITE_SITE, SITE_NAME, SITE_SUMMARY], 'sortall') if Act.csvFormat() else None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if csvPF and myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg in {'domain', 'domains'}:
|
|
if entityType == Ent.DOMAIN:
|
|
entityList = getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY)
|
|
else:
|
|
domains = getEntityList(Cmd.OB_DOMAIN_NAME_ENTITY)
|
|
domainLists = domains if isinstance(domains, dict) else None
|
|
elif myarg == 'includeallsites':
|
|
includeAllSites = 'true'
|
|
elif myarg == 'maxresults':
|
|
url_params['max-results'] = getInteger(minVal=1)
|
|
elif myarg == 'startindex':
|
|
url_params['start-index'] = getInteger(minVal=1)
|
|
elif myarg == 'withmappings':
|
|
url_params['with-mappings'] = 'true'
|
|
elif myarg in {'role', 'roles'}:
|
|
roles = getACLRoles(SITE_ACL_ROLES_MAP)
|
|
elif myarg in {'convertcrnl', 'converttextnl', 'convertsummarynl'}:
|
|
convertCRNL = True
|
|
elif myarg == 'delimiter':
|
|
delimiter = getCharacter()
|
|
else:
|
|
unknownArgumentExit()
|
|
sitesManager = SitesManager()
|
|
sitesSet = set()
|
|
i, count, entityList = getEntityArgument(entityList)
|
|
if entityType == Ent.USER:
|
|
for user in entityList:
|
|
i += 1
|
|
if domainLists:
|
|
domainList = domainLists[user]
|
|
elif domains:
|
|
domainList = domains
|
|
else:
|
|
_, domain = splitEmailAddress(user)
|
|
domainList = [domain]
|
|
user, sitesObject = getSitesObject(entityType, user, i, count)
|
|
if not sitesObject:
|
|
continue
|
|
jcount = len(domainList)
|
|
j = 0
|
|
for domain in domainList:
|
|
j += 1
|
|
if domain != 'site':
|
|
url_params['include-all-sites'] = includeAllSites
|
|
else:
|
|
url_params.pop('include-all-sites', None)
|
|
printGettingAllEntityItemsForWhom(Ent.SITE, f'{Ent.Singular(Ent.USER)}: {user}, {Ent.Singular(Ent.DOMAIN)}: {domain}')
|
|
sites = _getSites(domain, i, count)
|
|
if not csvPF:
|
|
_showSites(domain, j, jcount, domain, sites)
|
|
else:
|
|
_printSites(user, j, jcount, domain, sites)
|
|
else:
|
|
for domain in entityList:
|
|
i += 1
|
|
domain, sitesObject = getSitesObject(entityType, domain, i, count)
|
|
if not sitesObject:
|
|
continue
|
|
if domain != 'site':
|
|
url_params['include-all-sites'] = includeAllSites
|
|
else:
|
|
url_params.pop('include-all-sites', None)
|
|
printGettingAllEntityItemsForWhom(Ent.SITE, f'{Ent.Singular(Ent.DOMAIN)}: {domain}')
|
|
sites = _getSites(domain, i, count)
|
|
if not csvPF:
|
|
_showSites(domain, i, count, domain, sites)
|
|
else:
|
|
_printSites(domain, i, count, domain, sites)
|
|
if csvPF:
|
|
csvPF.SortTitles()
|
|
csvPF.SetSortTitles([])
|
|
if roles:
|
|
csvPF.MoveTitlesToEnd(['Scope', 'Role'])
|
|
csvPF.writeCSVfile('Sites')
|
|
|
|
# gam print sites [todrive <ToDriveAttribute>*] [domain|domains <DomainNameEntity>] [includeallsites]
|
|
# [withmappings] [role|roles all|<SiteACLRoleList>] [startindex <Number>] [maxresults <Number>] [convertcrnl] [delimiter <Character>]
|
|
# gam show sites [domain|domains <DomainNameEntity>] [includeallsites]
|
|
# [withmappings] [role|roles all|<SiteACLRoleList>] [startindex <Number>] [maxresults <Number>] [convertcrnl]
|
|
def doPrintShowDomainSites():
|
|
printShowSites([GC.Values[GC.DOMAIN]], Ent.DOMAIN)
|
|
|
|
# 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]
|
|
def printShowUserSites(users):
|
|
printShowSites(users, Ent.USER)
|
|
|
|
SITE_ACTION_TO_MODIFIER_MAP = {
|
|
Act.ADD: Act.MODIFIER_TO,
|
|
Act.UPDATE: Act.MODIFIER_IN,
|
|
Act.DELETE: Act.MODIFIER_FROM,
|
|
Act.INFO: Act.MODIFIER_FROM,
|
|
Act.PRINT: Act.MODIFIER_FROM,
|
|
Act.SHOW: Act.MODIFIER_FROM,
|
|
}
|
|
|
|
def _processSiteACLs(users, entityType):
|
|
action = Act.Get()
|
|
siteEntity = getSiteEntity()
|
|
csvPF = None
|
|
if action in [Act.ADD, Act.UPDATE]:
|
|
role = getChoice(SITE_ACL_ROLES_MAP, mapChoice=True)
|
|
elif action == Act.PRINT:
|
|
csvPF = CSVPrintFile([Ent.Singular(entityType), SITE_SITE, 'Scope', 'Role'])
|
|
else:
|
|
role = None
|
|
actionPrintShow = action in [Act.PRINT, Act.SHOW]
|
|
ACLScopeEntity = getCalendarSiteACLScopeEntity() if not actionPrintShow else {}
|
|
getTodriveOnly(csvPF)
|
|
modifier = SITE_ACTION_TO_MODIFIER_MAP[action]
|
|
sitesManager = SitesManager()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
origUser = user
|
|
user, sitesObject, sites, jcount = _validateUserGetSites(entityType, user, i, count, siteEntity, Ent.SITE_ACL, modifier)
|
|
if jcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for site in sites:
|
|
j += 1
|
|
domain, site, domainSite, ruleIds, kcount = _validateSiteGetRuleIds(origUser, site, j, jcount, ACLScopeEntity, showAction=not actionPrintShow)
|
|
if not domainSite:
|
|
continue
|
|
if not actionPrintShow:
|
|
Ind.Increment()
|
|
k = 0
|
|
for ruleId in ruleIds:
|
|
k += 1
|
|
ruleId = normalizeRuleId(ruleId)
|
|
try:
|
|
if action in [Act.CREATE, Act.ADD]:
|
|
acl = callGData(sitesObject, 'CreateAclEntry',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.ENTITY_EXISTS, GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
aclentry=sitesManager.FieldsToAclEntry(makeRoleRuleIdBody(role, ruleId)), domain=domain, site=site)
|
|
fields = sitesManager.AclEntryToFields(acl)
|
|
if not fields.get('inviteLink'):
|
|
entityActionPerformed([Ent.SITE, domainSite, Ent.SITE_ACL, formatACLRule(fields)], k, kcount)
|
|
else:
|
|
entityActionPerformed([Ent.SITE, domainSite, Ent.SITE_ACL, f'{formatACLRule(fields)} (Link: {fields["inviteLink"]})'], k, kcount)
|
|
elif action == Act.UPDATE:
|
|
acl = callGData(sitesObject, 'GetAclEntry',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=site, ruleId=ruleId)
|
|
acl.role.value = role
|
|
acl = callGData(sitesObject, 'UpdateAclEntry',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
aclentry=acl, domain=domain, site=site, ruleId=ruleId, extra_headers={'If-Match': acl.etag})
|
|
fields = sitesManager.AclEntryToFields(acl)
|
|
entityActionPerformed([Ent.SITE, domainSite, Ent.SITE_ACL, formatACLRule(fields)], k, kcount)
|
|
elif action == Act.DELETE:
|
|
acl = callGData(sitesObject, 'GetAclEntry',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=site, ruleId=ruleId)
|
|
callGData(sitesObject, 'DeleteAclEntry',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=site, ruleId=ruleId, extra_headers={'If-Match': acl.etag})
|
|
entityActionPerformed([Ent.SITE, domainSite, Ent.SITE_ACL, formatACLScopeRole(ruleId, None)], k, kcount)
|
|
elif action == Act.INFO:
|
|
acl = callGData(sitesObject, 'GetAclEntry',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.BAD_REQUEST, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=site, ruleId=ruleId)
|
|
fields = sitesManager.AclEntryToFields(acl)
|
|
printEntity([Ent.SITE, domainSite, Ent.SITE_ACL, formatACLRule(fields)], k, kcount)
|
|
except GDATA.notFound as e:
|
|
if not checkSiteExists(sitesObject, domain, site):
|
|
entityUnknownWarning(Ent.SITE, domainSite, j, jcount)
|
|
break
|
|
entityActionFailedWarning([Ent.SITE, domainSite, Ent.SITE_ACL, formatACLScopeRole(ruleId, role)], str(e), k, kcount)
|
|
except (GDATA.entityExists, GDATA.badRequest, GDATA.forbidden) as e:
|
|
entityActionFailedWarning([Ent.SITE, domainSite, Ent.SITE_ACL, formatACLScopeRole(ruleId, role)], str(e), k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
try:
|
|
acls = callGDataPages(sitesObject, 'GetAclFeed',
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=site)
|
|
if not csvPF:
|
|
kcount = len(acls)
|
|
entityPerformActionNumItems([Ent.SITE, domainSite], kcount, Ent.SITE_ACL, j, jcount)
|
|
if kcount == 0:
|
|
continue
|
|
Ind.Increment()
|
|
k = 0
|
|
for acl in acls:
|
|
k += 1
|
|
fields = sitesManager.AclEntryToFields(acl)
|
|
printEntity([Ent.SITE, domainSite, Ent.SITE_ACL, formatACLRule(fields)], k, kcount)
|
|
Ind.Decrement()
|
|
else:
|
|
siteRow = {Ent.Singular(entityType): user, SITE_SITE: domainSite}
|
|
for acl in acls:
|
|
fields = sitesManager.AclEntryToFields(acl)
|
|
siteACLRow = siteRow.copy()
|
|
siteACLRow.update(ACLRuleDict(fields))
|
|
csvPF.WriteRowTitles(siteACLRow)
|
|
except GDATA.notFound:
|
|
entityUnknownWarning(Ent.SITE, domainSite, j, jcount)
|
|
except GDATA.forbidden as e:
|
|
entityActionFailedWarning([Ent.SITE, domainSite], str(e), j, jcount)
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Site ACLs')
|
|
|
|
# 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>*]
|
|
def processUserSiteACLs(users):
|
|
_processSiteACLs(users, Ent.USER)
|
|
|
|
def doProcessDomainSiteACLs():
|
|
_processSiteACLs([GC.Values[GC.DOMAIN]], Ent.DOMAIN)
|
|
|
|
def _printSiteActivity(users, entityType):
|
|
sitesManager = SitesManager()
|
|
url_params = {}
|
|
csvPF = CSVPrintFile([SITE_SITE, SITE_SUMMARY, SITE_UPDATED], 'sortall')
|
|
sites = getEntityList(Cmd.OB_SITE_ENTITY)
|
|
siteLists = sites if isinstance(sites, dict) else None
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'todrive':
|
|
csvPF.GetTodriveParameters()
|
|
elif myarg == 'maxresults':
|
|
url_params['max-results'] = getInteger(minVal=1)
|
|
elif myarg == 'startindex':
|
|
url_params['start-index'] = getInteger(minVal=1)
|
|
elif myarg == 'updatedmin':
|
|
url_params['updated-min'] = getYYYYMMDD()
|
|
elif myarg == 'updatedmax':
|
|
url_params['updated-max'] = getYYYYMMDD()
|
|
else:
|
|
unknownArgumentExit()
|
|
i, count, users = getEntityArgument(users)
|
|
for user in users:
|
|
i += 1
|
|
if siteLists:
|
|
sites = siteLists[user]
|
|
user, sitesObject = getSitesObject(entityType, user, i, count)
|
|
if not sitesObject:
|
|
continue
|
|
jcount = len(sites)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
continue
|
|
Ind.Increment()
|
|
j = 0
|
|
for site in sites:
|
|
j += 1
|
|
domain, site, domainSite = _validateSite(site, j, jcount)
|
|
if not domainSite:
|
|
continue
|
|
printGettingAllEntityItemsForWhom(Ent.ACTIVITY, domainSite)
|
|
try:
|
|
activities = callGDataPages(sitesObject, 'GetActivityFeed',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwErrors=[GDATA.NOT_FOUND, GDATA.FORBIDDEN],
|
|
retryErrors=[GDATA.INTERNAL_SERVER_ERROR],
|
|
domain=domain, site=site, url_params=url_params)
|
|
for activity in activities:
|
|
fields = sitesManager.ActivityEntryToFields(activity)
|
|
activityRow = {SITE_SITE: domainSite}
|
|
for key, value in iter(fields.items()):
|
|
if not isinstance(value, list):
|
|
activityRow[key] = value
|
|
else:
|
|
activityRow[key] = ','.join(value)
|
|
csvPF.WriteRowTitles(activityRow)
|
|
except GDATA.notFound:
|
|
entityUnknownWarning(Ent.SITE, domainSite, j, jcount)
|
|
except GDATA.forbidden as e:
|
|
entityActionFailedWarning([Ent.SITE, domainSite], str(e), j, jcount)
|
|
csvPF.writeCSVfile('Site Activities')
|
|
|
|
# gam [<UserTypeEntity>] print siteactivity <SiteEntity> [todrive <ToDriveAttribute>*]
|
|
# [startindex <Number>] [maxresults <Number>] [updated_min <Date>] [updated_max <Date>]
|
|
def printUserSiteActivity(users):
|
|
_printSiteActivity(users, Ent.USER)
|
|
|
|
def doPrintDomainSiteActivity():
|
|
_printSiteActivity([GC.Values[GC.DOMAIN]], Ent.DOMAIN)
|
|
|
|
# <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.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:
|
|
notFoundBody[up] = password
|
|
if notFoundBody[up].lower() in {'blocklogin'}:
|
|
self.makeCleanPassword = False
|
|
notFoundBody[up] = self.CreateRandomPassword()
|
|
self.notFoundPassword = notFoundBody[up]
|
|
elif notFoundBody[up].lower() in {'random', 'uniquerandom'}:
|
|
self.SetCleanPasswordLen()
|
|
self.makeCleanPassword = True
|
|
notFoundBody[up] = self.CreateRandomPassword()
|
|
self.notFoundPassword = notFoundBody[up]
|
|
elif myarg == 'lograndompassword':
|
|
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 = 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 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):
|
|
up = 'password'
|
|
if self.makeRandomPassword or self.makeUniqueRandomPassword:
|
|
body[up] = self.CreateRandomPassword()
|
|
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 myarg == 'replace':
|
|
_getTagReplacement(tagReplacements, True)
|
|
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':
|
|
search = getString(Cmd.OB_RE_PATTERN)
|
|
pattern = validateREPattern(search, re.IGNORECASE)
|
|
replace = getString(Cmd.OB_EMAIL_REPLACEMENT)
|
|
patternGroups = pattern.groups
|
|
replSubs = REPLACE_GROUP_PATTERN.findall(replace)
|
|
for replSub in replSubs:
|
|
if int(replSub) > patternGroups:
|
|
Cmd.Backup()
|
|
usageErrorExit(Msg.MISMATCH_RE_SEARCH_REPLACE_SUBFIELDS.format(pattern.groups, search, int(replSub), replace))
|
|
updatePrimaryEmail['search'] = search
|
|
updatePrimaryEmail['pattern'] = pattern
|
|
updatePrimaryEmail['replace'] = replace
|
|
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', {})
|
|
# sigh, the API is wonky. If we set just displayName
|
|
# we get an error. But if we also "set" fullName which is
|
|
# really just a concat of first/last name and can't be set
|
|
# then it works. Go figure.
|
|
body['name']['displayName'] = body['name']['fullName'] = 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)
|
|
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 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>)*]
|
|
# [lograndompassword <FileName>] [ignorenullpassword]
|
|
# [verifynotinvitable]
|
|
# [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],
|
|
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) 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 <RegularExpression> <EmailReplacement>]
|
|
# [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>)*]
|
|
# [notifyonupdate [<Boolean>]]
|
|
# [lograndompassword <FileName>] [ignorenullpassword]
|
|
def updateUsers(entityList):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
ci = None
|
|
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['pattern'].search(user) is not None:
|
|
body['primaryEmail'] = updatePrimaryEmail['pattern'].sub(updatePrimaryEmail['replace'], user)
|
|
else:
|
|
body.pop('primaryEmail', None)
|
|
if not body:
|
|
entityActionNotPerformedWarning([Ent.USER, user], Msg.PRIMARY_EMAIL_DID_NOT_MATCH_PATTERN.format(updatePrimaryEmail['search']), 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:
|
|
PwdOpts.AssignPassword(body, notify, notFoundBody, parameters['createIfNotFound'])
|
|
try:
|
|
result = callGAPI(cd.users(), 'update',
|
|
throwReasons=[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)
|
|
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)
|
|
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],
|
|
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) 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(request_id, 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(CIGROUP_MEMBERKEY, {}).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]
|
|
# [issuspended <Boolean>] [aliasmatchpattern <RegularExpression>]
|
|
# [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]
|
|
# [issuspended <Boolean>] [aliasmatchpattern <RegularExpression>]
|
|
# [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 <RegularExpression>]
|
|
# [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],
|
|
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.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:
|
|
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):
|
|
coursesInfo = {}
|
|
for courseId in courseIds:
|
|
courseId = addCourseIdScope(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[courseId] = {'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 <RegularExpression>]
|
|
# [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 = f'{",".join(set(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 = f'{",".join(set(fieldsList))}' if fieldsList else None
|
|
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, response, 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, response, 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)
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
i += 1
|
|
if courseParticipantLists:
|
|
addItems = courseParticipantLists[courseId]
|
|
firstTeacher = None
|
|
if makeFirstTeacherOwner and addItems:
|
|
firstTeacher = normalizeEmailAddressOrUID(addItems[0])
|
|
_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)
|
|
for courseId, courseInfo in coursesInfo.items():
|
|
i += 1
|
|
if courseParticipantLists:
|
|
removeItems = courseParticipantLists[courseId]
|
|
_batchRemoveItemsFromCourse(courseInfo['croom'], courseId, i, count, removeItems, role)
|
|
|
|
# gam courses <CourseEntity> clear teachers|students
|
|
# gam course <CourseID> clear teacher|student
|
|
def doCourseClearParticipants(courseIdList, getEntityListArg):
|
|
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, getEntityListArg):
|
|
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])
|
|
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])
|
|
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)
|
|
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 as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
return (user, None, None, 0)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
|
return (user, None, None, 0)
|
|
if newCalId:
|
|
newcal = buildGAPIObject(API.CALENDAR)
|
|
if not checkCalendarExists(newcal, newCalId):
|
|
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, calId, j, jcount, cal, function, **kwargs):
|
|
try:
|
|
callGAPI(cal.calendarList(), function,
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.DUPLICATE, GAPI.UNKNOWN_ERROR, GAPI.SERVICE_NOT_AVAILABLE,
|
|
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)
|
|
|
|
# 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, 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, 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.notACalendarUser, GAPI.forbidden) as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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 as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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.serviceNotAvailable, GAPI.authError, 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.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)
|
|
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)
|
|
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 as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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)
|
|
if not cal:
|
|
continue
|
|
try:
|
|
feed = callGAPIpages(cal.settings(), 'list', 'items',
|
|
throwReasons=GAPI.CALENDAR_THROW_REASONS)
|
|
except GAPI.notACalendarUser as e:
|
|
entityActionFailedWarning([Ent.USER, user], str(e), i, count)
|
|
continue
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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)
|
|
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.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
|
|
if updateBody:
|
|
Act.Set(Act.UPDATE)
|
|
try:
|
|
if appendFields:
|
|
body = callGAPI(targetCal.calendars(), 'get',
|
|
throwReasons=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.notACalendarUser, GAPI.notFound) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, calId, j, jcount)
|
|
if addToNewOwner:
|
|
Act.Set(Act.ADD)
|
|
targetListBody['id'] = calId
|
|
try:
|
|
callGAPI(targetCal.calendarList(), 'insert',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.DUPLICATE, GAPI.UNKNOWN_ERROR, GAPI.SERVICE_NOT_AVAILABLE,
|
|
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.serviceNotAvailable,
|
|
GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnPrimarySubscription) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
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.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):
|
|
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)
|
|
else:
|
|
try:
|
|
callGAPI(targetCal.acl(), 'delete',
|
|
throwReasons=[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)
|
|
if deleteFromOldOwner:
|
|
Act.Set(Act.DELETE)
|
|
try:
|
|
callGAPI(sourceCal.calendarList(), 'delete',
|
|
throwReasons=[GAPI.NOT_FOUND, GAPI.DUPLICATE, GAPI.UNKNOWN_ERROR, GAPI.SERVICE_NOT_AVAILABLE,
|
|
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.serviceNotAvailable,
|
|
GAPI.cannotChangeOwnAcl, GAPI.cannotChangeOwnPrimarySubscription) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
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, 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):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
break
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventSummary], str(e), k, kcount)
|
|
status = False
|
|
except (GAPI.notACalendarUser, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, calId, j, jcount)
|
|
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):
|
|
entityUnknownWarning(Ent.CALENDAR, calId, j, jcount)
|
|
break
|
|
entityActionFailedWarning([Ent.CALENDAR, calId, Ent.EVENT, eventSummary], str(e), k, kcount)
|
|
except (GAPI.notACalendarUser, GAPI.forbidden, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), j, jcount)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, calId, j, jcount)
|
|
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: # 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.notACalendarUser, 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.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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.notACalendarUser, GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, user], str(e), j, jcount)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, 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):
|
|
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 as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.CALENDAR, calId, 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.notACalendarUser, GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.badRequest) as e:
|
|
entityActionFailedWarning([Ent.CALENDAR, user], str(e), j, jcount)
|
|
break
|
|
except (GAPI.serviceNotAvailable, GAPI.authError):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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',
|
|
'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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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(drive, sharedDriveId, useDomainAdminAccess=False):
|
|
sharedDriveName = GM.Globals[GM.MAP_SHAREDDRIVE_ID_TO_NAME].get(sharedDriveId)
|
|
if not sharedDriveName:
|
|
try:
|
|
sharedDriveName = callGAPI(drive.drives(), 'get',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.NOT_FOUND],
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
driveId=sharedDriveId, fields='name')['name']
|
|
except (GAPI.notFound, GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy):
|
|
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(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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'
|
|
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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}
|
|
|
|
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 == '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((getREPattern(re.IGNORECASE), getString(Cmd.OB_STRING, minLen=0)))
|
|
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 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
entityServiceNotApplicableWarning(Ent.USER, user, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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(drive, 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',
|
|
'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',
|
|
'candownload': 'canDownload',
|
|
'canedit': 'canEdit',
|
|
'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',
|
|
'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_DRIVE_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)
|
|
|
|
def _stripControlCharsFromName(name):
|
|
for cc in ['\x00', '\r', '\n']:
|
|
name = name.replace(cc, '')
|
|
return name
|
|
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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,
|
|
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,
|
|
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(drive, driveId)
|
|
if DFF.showSharedDriveNames:
|
|
result['driveName'] = _getSharedDriveNameFromId(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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):
|
|
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) 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) else [SHARED_WITHME]
|
|
else:
|
|
if result['name'] == TEAM_DRIVE:
|
|
result['name'] = _getSharedDriveNameFromId(drive, result['driveId'])
|
|
result['parents'] = [SHARED_DRIVES] if 'sharedWithMeTime' not in f_file 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', []))
|
|
|
|
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 <RegularExpression>]
|
|
# <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(drive, 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)
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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)
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
continue
|
|
if filepath:
|
|
filePathInfo = initFilePathInfo(pathDelimiter)
|
|
filesPrinted = set()
|
|
mimeTypeInfo = {}
|
|
getSharedDriveACLsCount = 0
|
|
if buildTree:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, 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):
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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]
|
|
# [fields <CommentsFieldNameList>] [showphotolinks]
|
|
# [countsonly]
|
|
# [formatjson]
|
|
# gam <UserTypeEntity> print filecomments <DriveFileEntity> [todrive <ToDriveAttribute>*]
|
|
# [showdeleted] [start <Date>|<Time>]
|
|
# [fields <CommentsFieldNameList>] [showphotolinks]
|
|
# [countsonly]
|
|
# (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 = 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
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
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:
|
|
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(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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')
|
|
|
|
# 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 <RegularExpression>]
|
|
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
|
# [excludetrashed] (addcsvdata <FieldName> <String>)*
|
|
# [showsize] [showmimetypesize] [showlastmodification]
|
|
# (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 <RegularExpression>]
|
|
# <PermissionMatch>* [<PermissionMatchMode>] [<PermissionMatchAction>]
|
|
# [excludetrashed]
|
|
# [showsize] [showmimetypesize] [showlastmodification]
|
|
# [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(emailAddress)'])
|
|
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:
|
|
printKeyValueList(['lastModifiedFile', f"{lastModification['lastModifiedFileName']}({lastModification['lastModifiedFileId']})",
|
|
'lastModifyingUser', lastModification['lastModifyingUser'],
|
|
'lastModifiedTime', formatLocalTime(lastModification['lastModifiedTime'])])
|
|
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:
|
|
row.update({'lastModifiedFileId': lastModification['lastModifiedFileId'],
|
|
'lastModifiedFileName': lastModification['lastModifiedFileName'],
|
|
'lastModifyingUser': lastModification['lastModifyingUser'],
|
|
'lastModifiedTime': formatLocalTime(lastModification['lastModifiedTime'])})
|
|
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})
|
|
sharedDriveId = sharedDriveName = ''
|
|
continueOnInvalidQuery = showSize = showLastModification = showMimeTypeSize = False
|
|
sizeField = 'quotaBytesUsed'
|
|
summary = FILECOUNT_SUMMARY_NONE
|
|
summaryUser = FILECOUNT_SUMMARY_USER
|
|
summaryMimeTypeInfo = {}
|
|
fileIdEntity = {}
|
|
addCSVData = {}
|
|
summaryLastModification = {
|
|
'lastModifiedFileId': '', 'lastModifiedFileName': '',
|
|
'lastModifyingUser': '', 'lastModifiedTime': NEVER_TIME}
|
|
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 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', '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', '')
|
|
if sharedDriveId:
|
|
sharedDriveName = _getSharedDriveNameFromId(drive, sharedDriveId)
|
|
else:
|
|
sharedDriveName = ''
|
|
mimeTypeInfo = {}
|
|
userLastModification = {
|
|
'lastModifiedFileId': '', 'lastModifiedFileName': '',
|
|
'lastModifyingUser': '', 'lastModifiedTime': NEVER_TIME}
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, 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:
|
|
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['lastModifiedTime'] = f_file['modifiedTime']
|
|
userLastModification['lastModifyingUser'] = f_file['lastModifyingUser'].get('emailAddress', UNKNOWN)
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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')
|
|
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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()
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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 <RegularExpression>]
|
|
# <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 <RegularExpression>]
|
|
# <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:
|
|
userSvcNotApplicableOrDriveDisabled(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
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, user, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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 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:
|
|
userSvcNotApplicableOrDriveDisabled(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
|
|
|
|
createReturnItemMap = {
|
|
'returnidonly': 'id',
|
|
'returnlinkonly': 'webViewLink',
|
|
'returneditlinkonly': 'editLink'
|
|
}
|
|
|
|
# gam <UserTypeEntity> create drivefile
|
|
# [(localfile <FileName>|-)|(url <URL>)]
|
|
# [(drivefilename|newfilename <DriveFileName>) | (replacefilename <RegularExpression> <String>)*]
|
|
# [stripnameprefix <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_LOCALFILEPATH]:
|
|
if parameters[DFA_LOCALFILEPATH] != '-' and parameters[DFA_PRESERVE_FILE_TIMES]:
|
|
setPreservedFileTimes(body, parameters, False)
|
|
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.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.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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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 <RegularExpression> <String>)*]
|
|
# [stripnameprefix <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
|
|
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 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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,
|
|
'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,
|
|
}
|
|
|
|
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((getREPattern(re.IGNORECASE), getString(Cmd.OB_STRING, minLen=0)))
|
|
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()
|
|
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 == '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:
|
|
userSvcNotApplicableOrDriveDisabled(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', '')
|
|
domain = ''
|
|
if copyMoveOptions['excludePermissionsFromDomains'] or copyMoveOptions['includePermissionsFromDomains']:
|
|
if permission['type'] in {'group', 'user'}:
|
|
atLoc = emailAddress.find('@')
|
|
if atLoc > 0:
|
|
domain = emailAddress[atLoc+1:]
|
|
elif permission['type'] == 'domain':
|
|
domain = permission.get('domain', '')
|
|
if 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):
|
|
notCopiedMessage = f"{permission['type']} {permission['emailAddress']} deleted"
|
|
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)
|
|
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],
|
|
fileId=newFileId, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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],
|
|
fileId=newFileId, permissionId=permissionId, useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'], 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:
|
|
userSvcNotApplicableOrDriveDisabled(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],
|
|
fileId=newFileId, permissionId=permissionId, removeExpiration=removeExpiration,
|
|
body=permission['updates'], useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'], 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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 <RegularExpression> <String>)*
|
|
# [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 <RegularExpression>] [filemimetype [not] <MimeTypeList>]
|
|
# [copysubfolders [<Boolean>]] [foldernamematchpattern <RegularExpression>]
|
|
# [copysubshortcuts [<Boolean>]] [shortcutnamematchpattern <RegularExpression>]
|
|
# [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]
|
|
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
|
# (mappermissionsdomain <DomainName> <DomainName>)*
|
|
# [copysheetprotectedranges [<Boolean>]]
|
|
# [copysheetprotectedrangesinheritedpermissions [<Boolean>]]
|
|
# [copysheetprotectedrangesnoninheritedpermissions [<Boolean>]]
|
|
# [sendemailifrequired [<Boolean>]]
|
|
# [suppressnotselectedmessages [<Boolean>]]
|
|
# [verifyorganizer [<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:
|
|
userSvcNotApplicableOrDriveDisabled(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(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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],
|
|
fileId=fileId, permissionId=permissionId, useDomainAdminAccess=copyMoveOptions['useDomainAdminAccess'], 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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]
|
|
# [synctopfoldernoniheritedpermissions [<Boolean>]] [syncsubfoldernoninheritedpermissions [<Boolean>]]
|
|
# [excludepermissionsfromdomains|includepermissionsfromdomains <DomainNameList>]
|
|
# (mappermissionsdomain <DomainName> <DomainName>)*
|
|
# [updatefilepermissions [<Boolean>]]
|
|
# [retainsourcefolders [<Boolean>]]
|
|
# [sendemailifrequired [<Boolean>]]
|
|
# [verifyorganizer [<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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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 = False
|
|
movedFiles = {}
|
|
updateFilePermissions = False
|
|
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(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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_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'}],
|
|
'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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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,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'):
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userSvcNotApplicableOrDriveDisabled(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])*
|
|
# [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:
|
|
userSvcNotApplicableOrDriveDisabled(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],
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
# userSvcNotApplicableOrDriveDisabled(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],
|
|
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) 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:
|
|
userSvcNotApplicableOrDriveDisabled(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],
|
|
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) 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:
|
|
userSvcNotApplicableOrDriveDisabled(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) 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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 = 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 == '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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(sourceUser, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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]
|
|
# [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):
|
|
try:
|
|
children = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy, q=WITH_PARENTS.format(fileEntry['id']),
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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 = 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
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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],
|
|
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],
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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]
|
|
# [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):
|
|
try:
|
|
children = callGAPIpages(drive.files(), 'list', 'files',
|
|
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
|
|
retryReasons=[GAPI.UNKNOWN_ERROR],
|
|
orderBy=OBY.orderBy, q=WITH_PARENTS.format(fileEntry['id']),
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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],
|
|
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) as e:
|
|
entityActionFailedWarning([Ent.USER, oldOwner, entityType, fileDesc], str(e), l, lcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userSvcNotApplicableOrDriveDisabled(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 = []
|
|
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 == '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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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],
|
|
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],
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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(drive, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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.expirationDateNotAllowedForSharedDriveMembers,
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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>]]
|
|
# [showtitles] [nodetails|(csv [todrive <ToDriveAttribute>*] [formatjson [quotechar <Character>]])]
|
|
def updateDriveFileACLs(users, useDomainAdminAccess=False):
|
|
fileIdEntity = getDriveFileEntity()
|
|
isEmail, permissionId = getPermissionId()
|
|
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 == '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)}')
|
|
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],
|
|
useDomainAdminAccess=useDomainAdminAccess,
|
|
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.expirationDateNotAllowedForSharedDriveMembers,
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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, response, 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.expirationDateNotAllowedForSharedDriveMembers,
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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>]]
|
|
# [showtitles]
|
|
def deleteDriveFileACLs(users, useDomainAdminAccess=False):
|
|
fileIdEntity = getDriveFileEntity()
|
|
isEmail, permissionId = getPermissionId()
|
|
showTitles = updateSheetProtectedRanges = False
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if myarg == 'showtitles':
|
|
showTitles = getBoolean()
|
|
elif myarg == 'updatesheetprotectedranges':
|
|
updateSheetProtectedRanges = getBoolean()
|
|
elif myarg in ADMIN_ACCESS_OPTIONS:
|
|
useDomainAdminAccess = True
|
|
else:
|
|
unknownArgumentExit()
|
|
_checkFileIdEntityDomainAccess(fileIdEntity, useDomainAdminAccess)
|
|
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],
|
|
useDomainAdminAccess=useDomainAdminAccess, 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) 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:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def doDeleteDriveFileACLs():
|
|
deleteDriveFileACLs([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] delete permissions <DriveFileEntity> <DriveFilePermissionIDEntity> [asadmin]
|
|
# <PermissionMatch>* [<PermissionMatchAction>]
|
|
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, response, 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, 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.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'})
|
|
while Cmd.ArgumentsRemaining():
|
|
myarg = getArgument()
|
|
if 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:
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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.DRIVE_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 drivelabels <DriveLabelNameEntity>
|
|
# [[basic|full] [languagecode <BCP47LanguageCode>]
|
|
# [formatjson] [asadmin]
|
|
def infoDriveLabels(users, useAdminAccess=False):
|
|
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_DRIVE_LABEL_NAME, Ent.DRIVE_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.DRIVE_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:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def doInfoDriveLabels():
|
|
infoDriveLabels([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] print drivelabels> [todrive <ToDriveAttribute>*]
|
|
# [basic|full] [languagecode <BCP47LanguageCode>]
|
|
# [publishedonly [<Boolean>]] [minimumrole applier|editor|organizer|reader]
|
|
# [formatjson [quotechar <Character>]] [asadmin]
|
|
# gam [<UserTypeEntity>] show drivelabels
|
|
# [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.DRIVE_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.DRIVE_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.DRIVE_LABEL, None], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive 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.DRIVE_LABEL_PERMISSION_NAME, f'{labelperm["name"]}'], j, jcount)
|
|
Ind.Increment()
|
|
showJSON(None, labelperm)
|
|
Ind.Decrement()
|
|
|
|
# gam [<UserTypeEntity>] create drivelabelpermission <DriveLabelNameEntity>
|
|
# (user <UserItem>) | (group <GroupItem) | (audience <String>)
|
|
# role applier|editor|organizer|reader
|
|
# [nodetails|formatjson] [asadmin]
|
|
def createDriveLabelPermissions(users, useAdminAccess=False):
|
|
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_DRIVE_LABEL_NAME, Ent.DRIVE_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.DRIVE_LABEL_NAME, name, Ent.DRIVE_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.DRIVE_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:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def doCreateDriveLabelPermissions():
|
|
createDriveLabelPermissions([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] delete drivelabelpermission <DriveLabelNameEntity>
|
|
# (user <UserItem>) | (group <GroupItem) | (audience <String>)
|
|
# [asadmin]
|
|
# gam [<UserTypeEntity>] remove drivelabelpermission <DriveLabelPermissionNameEntity>
|
|
# [asadmin]
|
|
def deleteDriveLabelPermissions(users, useAdminAccess=False):
|
|
doDelete = Act.Get() == Act.DELETE
|
|
if doDelete:
|
|
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_DRIVE_LABEL_NAME, Ent.DRIVE_LABEL, shlexSplit=True)
|
|
else:
|
|
driveLabelNameEntity = getUserObjectEntity(Cmd.OB_DRIVE_LABEL_PERMISSION_NAME, Ent.DRIVE_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.DRIVE_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:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
|
|
def doDeleteDriveLabelPermissions():
|
|
deleteDriveLabelPermissions([_getAdminEmail()], True)
|
|
|
|
# gam [<UserTypeEntity>] print drivelabelpermissions <DriveLabelNameEntity> [todrive <ToDriveAttribute>*]
|
|
# [formatjson [quotechar <Character>]] [asadmin]
|
|
# gam [<UserTypeEntity>] show drivelabelpermissions <DriveLabelNameEntity>
|
|
# [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_DRIVE_LABEL_NAME, Ent.DRIVE_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.DRIVE_LABEL_NAME, name, Ent.DRIVE_LABEL_PERMISSION, None]
|
|
name = validateDriveLabelName(name, kvList, j, jcount, False)
|
|
if name is None:
|
|
continue
|
|
kvList = [Ent.USER, user, Ent.DRIVE_LABEL_NAME, name]
|
|
if csvPF:
|
|
printGettingAllEntityItemsForWhom(Ent.DRIVE_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.DRIVE_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:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
break
|
|
Ind.Decrement()
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Drive 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 <DriveLabelIDList>)*
|
|
# (deletelabel <DriveLabelIDList>)*
|
|
# (addlabelfield <DriveLabelID> <DriveLabelFieldID>
|
|
# (text <String>)|selection <DriveLabelSelectionIDList>)|
|
|
# (integer <Number>)|(date <Date>)|(user <EmailAddressList>))*
|
|
# (deletelabelfield <DriveLabelID> <DriveLabelFieldID>)*
|
|
# [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_DRIVE_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_DRIVE_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_DRIVE_LABEL_ID))
|
|
fieldId = getString(Cmd.OB_DRIVE_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_DRIVE_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_DRIVE_LABEL_ID))
|
|
fieldId = getString(Cmd.OB_DRIVE_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.DRIVE_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.DRIVE_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.DRIVE_LABEL_ID, action['labelModifications'][0]['labelId']])
|
|
if 'fieldModifications' in action['labelModifications'][0]:
|
|
xkvList.extend([Ent.DRIVE_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:
|
|
userSvcNotApplicableOrDriveDisabled(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.ABORTED],
|
|
name=name, body=cibody)
|
|
if not returnIdOnly:
|
|
Act.Set(Act.MOVE)
|
|
entityModifierNewValueActionPerformed([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) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.SHAREDDRIVE_ID, driveId], str(e), i, count)
|
|
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
|
|
userSvcNotApplicableOrDriveDisabled(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_SHARED_DRIVE_CREATION_TO_COMPLETE_SLEEPING.format(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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 = ','.join(set(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:
|
|
userSvcNotApplicableOrDriveDisabled(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 <RegularExpression>] [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 <RegularExpression>] [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:
|
|
userSvcNotApplicableOrDriveDisabled(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 not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.USER, user], jcount, Ent.SHAREDDRIVE, i, count)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
else:
|
|
if not csvPF:
|
|
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(drive, shareddrive['driveId'], useDomainAdminAccess=True)
|
|
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)
|
|
_, drive = buildGAPIServiceObject(API.DRIVE3, _getAdminEmail())
|
|
if not 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 not csvPF:
|
|
if not FJQC.formatJSON:
|
|
entityPerformActionNumItems([Ent.ORGANIZATIONAL_UNIT, orgUnitPath], jcount, Ent.SHAREDDRIVE)
|
|
if jcount == 0:
|
|
setSysExitRC(NO_ENTITIES_FOUND_RC)
|
|
else:
|
|
if not csvPF:
|
|
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(drive, 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(drive, 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 <RegularExpression>] [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 <RegularExpression>] [orgunit|org|ou <OrgUnitPath>]
|
|
# [user|group <EmailAddress> [checkgroups]] (role|roles <SharedDriveACLRoleList>)*
|
|
# <PermissionMatch>* [<PermissionMatchAction>] [pmselect]
|
|
# [oneitemperrow] [maxitems <Integer>]
|
|
# [shownopermissionsdrives false|true|only]
|
|
# [<DrivePermissionsFieldName>*|(fields <DrivePermissionsFieldNameList>)]
|
|
# [formatjsn]
|
|
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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
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:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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] <RegularExpression>)|<GroupEntity>]
|
|
def deleteUserFromGroups(users):
|
|
cd = buildGAPIObject(API.DIRECTORY)
|
|
groupKeys = None
|
|
matchPattern = {}
|
|
kwargs = _getUserGroupOptionalDomainCustomerId()
|
|
if not kwargs:
|
|
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:
|
|
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()
|
|
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}
|
|
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)
|
|
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]
|
|
def updateLicense(users):
|
|
lic, parameters = getLicenseParameters('patch')
|
|
j, jcount, users = getEntityArgument(users)
|
|
Act.Set([Act.UPDATE, Act.UPDATE_PREVIEW][parameters['preview']])
|
|
if parameters['preview']:
|
|
message = Act.PREVIEW
|
|
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['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],
|
|
userKey=user, body=body, fields='')
|
|
entityActionPerformed([Ent.USER, user, Ent.PHOTO, filename], i, count)
|
|
except GAPI.invalidInput 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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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:
|
|
userSvcNotApplicableOrDriveDisabled(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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
|
#
|
|
def cleanLabelQuery(labelQuery):
|
|
for ch in '/ (){}':
|
|
labelQuery = labelQuery.replace(ch, '-')
|
|
return labelQuery.lower()
|
|
|
|
# gam <UserTypeEntity> update label|labels [search <RegularExpression>] [replace <LabelReplacement>] [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_LABEL_REPLACEMENT)
|
|
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:
|
|
patternGroups = pattern.groups
|
|
replSubs = REPLACE_GROUP_PATTERN.findall(replace)
|
|
for replSub in replSubs:
|
|
if int(replSub) > patternGroups:
|
|
Cmd.SetLocation(replaceLocation)
|
|
usageErrorExit(Msg.MISMATCH_RE_SEARCH_REPLACE_SUBFIELDS.format(pattern.groups, search, int(replSub), 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
|
|
newLabelName = pattern.sub(replace, label['name']) if useRegexSub else 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):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, response, 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):
|
|
entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
|
|
|
# gam <UserTypeEntity> delete label|labels <LabelName>|regex:<RegularExpression>
|
|
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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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):
|
|
if label.find('/') != -1:
|
|
(parent, base) = label.rsplit('/', 1)
|
|
if parent in labelTree:
|
|
if label in labelTree:
|
|
labelTree[label]['info']['base'] = base
|
|
labelTree[parent]['children'].append(labelTree.pop(label))
|
|
_checkChildLabel(parent)
|
|
|
|
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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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,
|
|
'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)
|
|
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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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.BAD_REQUEST, GAPI.INVALID,
|
|
GAPI.FAILED_PRECONDITION, GAPI.FORBIDDEN],
|
|
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:
|
|
entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
|
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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
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, response, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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 myarg == 'replace':
|
|
_getTagReplacement(tagReplacements, True)
|
|
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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
|
|
|
# gam <UserTypeEntity> draft message
|
|
# <MessageContent> (replace <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>)*
|
|
# (<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>)*
|
|
# (<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 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:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
Act.Set(action)
|
|
except (GAPI.serviceNotAvailable, GAPI.badRequest, 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 parameters['maxToProcess'] and parameters['messagesProcessed'] == parameters['maxToProcess']:
|
|
return (False, None)
|
|
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):
|
|
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()
|
|
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):
|
|
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)
|
|
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 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)
|
|
Ind.Decrement()
|
|
|
|
def _printThread(user, result):
|
|
if senderMatchPattern:
|
|
for message in result['messages']:
|
|
if _checkSenderMatch(message):
|
|
break
|
|
else:
|
|
return
|
|
for message in result['messages']:
|
|
_printMessage(user, message)
|
|
messageThreadCounts['threads'] += 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:
|
|
for message in result['messages']:
|
|
messageThreadCounts['size'] += message['sizeEstimate']
|
|
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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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_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 == '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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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 <RegularExpression>] [sendermatchpattern <RegularExpression>]
|
|
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
|
# [showlabels] [showbody] [showdate] [showsize] [showsnippet]
|
|
# [convertcrnl] [delimiter <Character>]
|
|
# [countsonly|positivecountsonly] [useronly]
|
|
# [[attachmentnamepattern <RegularExpression>]
|
|
# [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 <RegularExpression>] [sendermatchpattern <RegularExpression>]
|
|
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
|
# [showlabels] [showbody] [showdate] [showsize] [showsnippet]
|
|
# [countsonly|positivecountsonly] [useronly]
|
|
# [[attachmentnamepattern <RegularExpression>]
|
|
# [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 <RegularExpression>]
|
|
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
|
# [showlabels] [showbody] [showdate] [showsize] [showsnippet]
|
|
# [convertcrnl] [delimiter <Character>]
|
|
# [countsonly|positivecountsonly] [useronly]
|
|
# [[attachmentnamepattern <RegularExpression>]
|
|
# [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 <RegularExpression>]
|
|
# [headers all|<SMTPHeaderList>] [dateheaderformat iso|rfc2822|<String>] [dateheaderconverttimezone [<Boolean>]]
|
|
# [showlabels] [showbody] [showdate] [showsize] [showsnippet]
|
|
# [countsonly|positivecountsonly] [useronly]
|
|
# [[attachmentnamepattern <RegularExpression>]
|
|
# [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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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>]]
|
|
# [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 == '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:
|
|
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
|
|
if csvPF:
|
|
csvPF.writeCSVfile('Forms')
|
|
|
|
# gam <UserTypeEntity> update form <DriveFileEntity> [title <String>] [description <String>] [isquiz [Boolean>]
|
|
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)
|
|
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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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],
|
|
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) as e:
|
|
entityActionFailedWarning([Ent.USER, user, entityType, emailAddress], str(e), j, jcount)
|
|
except (GAPI.serviceNotAvailable, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
|
userDefined = False
|
|
return userDefined
|
|
|
|
def getSendAsAttributes(myarg, body, tagReplacements):
|
|
if myarg == 'replace':
|
|
_getTagReplacement(tagReplacements, True)
|
|
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>)*]
|
|
# [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>)*]
|
|
# [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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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>)*
|
|
# [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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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>)*]
|
|
# [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 myarg == 'replace':
|
|
_getTagReplacement(tagReplacements, True)
|
|
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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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, GAPI.badRequest):
|
|
entityServiceNotApplicableWarning(Ent.USER, 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.serviceNotAvailable:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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.serviceNotAvailable:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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.serviceNotAvailable:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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:
|
|
userSvcNotApplicableOrDriveDisabled(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.serviceNotAvailable:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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,
|
|
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) as e:
|
|
entityActionFailedWarning(entityKVList, str(e), i, count)
|
|
except GAPI.serviceNotAvailable:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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.serviceNotAvailable:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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.serviceNotAvailable:
|
|
entityServiceNotApplicableWarning(Ent.USER, user, i, count)
|
|
break
|
|
|
|
def getTaskLists(svc, user, i, count):
|
|
try:
|
|
results = callGAPIpages(svc.tasklists(), 'list', 'items',
|
|
pageMessage=getPageMessageForWhom(),
|
|
throwReasons=GAPI.TASKLIST_THROW_REASONS,
|
|
maxResults=100)
|
|
except GAPI.notFound:
|
|
results = []
|
|
except (GAPI.badRequest, GAPI.invalid) as e:
|
|
entityActionFailedWarning([Ent.USER, user, Ent.TASKLIST, None], str(e), i, count)
|
|
results = None
|
|
except GAPI.serviceNotAvailable:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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:
|
|
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()
|
|
entityServiceNotApplicableWarning(Ent.USER, 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:
|
|
entityServiceNotApplicableWarning(Ent.USER, 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:
|
|
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 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()
|
|
entityServiceNotApplicableWarning(Ent.USER, 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
|
|
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)
|
|
else:
|
|
unknownArgumentExit()
|
|
|
|
# gam create caalevel <String> [description <String>] (basic <CAABasicAttribute>+)|(custom <String>)
|
|
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>)
|
|
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: doCreateDomainSite,
|
|
Cmd.ARG_SITEACL: doProcessDomainSiteACLs,
|
|
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_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: doProcessDomainSiteACLs,
|
|
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_CHROMESCHEMA: doInfoChromePolicySchemas,
|
|
Cmd.ARG_CIGROUP: doInfoCIGroups,
|
|
Cmd.ARG_CIGROUPMEMBERS: doInfoCIGroupMembers,
|
|
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: doInfoDomainSites,
|
|
Cmd.ARG_SITEACL: doProcessDomainSiteACLs,
|
|
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_CHROMESCHEMA: doPrintShowChromeSchemas,
|
|
Cmd.ARG_CHROMESNVALIDITY: doPrintChromeSnValidity,
|
|
Cmd.ARG_CHROMEVERSIONS: doPrintShowChromeVersions,
|
|
Cmd.ARG_CIGROUP: doPrintCIGroups,
|
|
Cmd.ARG_CIGROUPMEMBERS: doPrintCIGroupMembers,
|
|
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: doPrintShowDomainSites,
|
|
Cmd.ARG_SITEACL: doProcessDomainSiteACLs,
|
|
Cmd.ARG_SITEACTIVITY: doPrintDomainSiteActivity,
|
|
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_CHROMESCHEMA: doPrintShowChromeSchemas,
|
|
Cmd.ARG_CHROMEVERSIONS: doPrintShowChromeVersions,
|
|
Cmd.ARG_CIGROUPMEMBERS: doShowCIGroupMembers,
|
|
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: doPrintShowDomainSites,
|
|
Cmd.ARG_SITEACL: doProcessDomainSiteACLs,
|
|
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: doUpdateDomainSites,
|
|
Cmd.ARG_SITEACL: doProcessDomainSiteACLs,
|
|
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_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_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: createUserSite,
|
|
Cmd.ARG_SITEACL: processUserSiteACLs,
|
|
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: processUserSiteACLs,
|
|
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: infoUserSites,
|
|
Cmd.ARG_SITEACL: processUserSiteACLs,
|
|
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_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: printShowUserSites,
|
|
Cmd.ARG_SITEACL: processUserSiteACLs,
|
|
Cmd.ARG_SITEACTIVITY: printUserSiteActivity,
|
|
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_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: printShowUserSites,
|
|
Cmd.ARG_SITEACL: processUserSiteACLs,
|
|
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: updateUserSites,
|
|
Cmd.ARG_SITEACL: processUserSiteACLs,
|
|
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_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_FILEDRIVELABELS: Cmd.ARG_FILEDRIVELABEL,
|
|
Cmd.ARG_DRIVEFILESHORTCUTS: Cmd.ARG_DRIVEFILESHORTCUT,
|
|
Cmd.ARG_DRIVELABELS: Cmd.ARG_DRIVELABEL,
|
|
Cmd.ARG_DRIVELABELPERMISSIONS: Cmd.ARG_DRIVELABELPERMISSION,
|
|
Cmd.ARG_EVENTS: Cmd.ARG_EVENT,
|
|
Cmd.ARG_FILECOMMENTS: Cmd.ARG_FILECOMMENT,
|
|
Cmd.ARG_FILECOUNTS: Cmd.ARG_FILECOUNT,
|
|
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)
|