From ac198781b270e799dd2d09c9937d90028f06b6c4 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Sat, 4 Jul 2026 06:46:31 -0400 Subject: [PATCH] fix vault errors --- src/gam/__init__.py | 8 +- src/gam/cmd/calendar.py | 6 +- src/gam/cmd/cigroups/members.py | 12 +-- src/gam/cmd/ciuserinvitations.py | 2 +- src/gam/cmd/cros.py | 4 +- src/gam/cmd/drive/core.py | 6 +- src/gam/cmd/drive/files.py | 4 +- src/gam/cmd/oauth.py | 2 +- src/gam/cmd/project.py | 2 +- src/gam/cmd/reports.py | 2 +- src/gam/cmd/userservices.py | 6 +- src/gam/util/access.py | 44 +--------- src/gam/util/api.py | 64 ++++++++++++--- src/gam/util/args.py | 7 +- src/gam/util/batch.py | 7 +- src/gam/util/connection.py | 3 +- src/gam/util/csv_pf.py | 4 +- src/gam/util/entity.py | 133 +------------------------------ src/gam/util/fileio.py | 3 +- src/gam/util/gdoc.py | 4 +- src/gam/util/output.py | 9 +++ src/gam/util/uid.py | 129 ++++++++++++++++++++++++++++++ 22 files changed, 233 insertions(+), 228 deletions(-) create mode 100644 src/gam/util/uid.py diff --git a/src/gam/__init__.py b/src/gam/__init__.py index d7ebfd83..2a49caac 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -157,12 +157,14 @@ import gdata.apps.contacts.service from gam.util.html import _DeHTMLParser, dehtml # noqa: F401 # re-export from gam.util.access import ( # noqa: F401 # re-export accessErrorMessage, accessErrorExit, accessErrorExitNonDirectory, - ClientAPIAccessDeniedExit, SvcAcctAPIAccessDenied, SvcAcctAPIAccessDeniedExit, - SvcAcctAPIDisabledExit, APIAccessDeniedExit, checkEntityDNEorAccessErrorExit, checkEntityAFDNEorAccessErrorExit, checkEntityItemValueAFDNEorAccessErrorExit, entityUnknownWarning, entityOrEntityUnknownWarning, duplicateAliasGroupUserWarning, ) +from gam.util.api import ( # noqa: F401 # re-export (moved from access.py) + ClientAPIAccessDeniedExit, SvcAcctAPIAccessDenied, SvcAcctAPIAccessDeniedExit, + SvcAcctAPIDisabledExit, APIAccessDeniedExit, +) from gam.util.email import ( # noqa: F401 # re-export _addAttachmentsToMessage, _addEmbeddedImagesToMessage, send_email, ) @@ -170,7 +172,7 @@ from gam.util.email import ( # noqa: F401 # re-export from gam.constants import * -from util.args import ISOformatTimeStamp, currentISOformatTimeStamp # noqa: E402,F401 +from util.output import ISOformatTimeStamp, currentISOformatTimeStamp # noqa: E402,F401 from util.fileio import adjustRedirectedSTDFilesIfNotMultiprocessing, closeSTDFilesIfNotMultiprocessing # noqa: E402,F401 from gam.var import Act, Cmd, Ent, Ind # noqa: E402,F401 diff --git a/src/gam/cmd/calendar.py b/src/gam/cmd/calendar.py index 1530d93c..6694ac90 100644 --- a/src/gam/cmd/calendar.py +++ b/src/gam/cmd/calendar.py @@ -138,7 +138,7 @@ def makeRoleRuleIdBody(role, ruleId): def normalizeCalendarId(calId, user): if not user or calId.lower() != 'primary': - return convertUIDtoEmailAddress(calId, emailTypes=['user', 'resource']) + return convertUIDtoEmailAddress(calId, buildGAPIObject(API.DIRECTORY), emailTypes=['user', 'resource']) return user def checkCalendarExists(cal, calId, i, count, showMessage=False): @@ -1525,7 +1525,7 @@ def _getCalendarMoveEventsOptions(calendarEventEntity=None): 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)) + newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM), buildGAPIObject(API.DIRECTORY)) else: unknownArgumentExit() return (parameters, newCalId) @@ -1572,7 +1572,7 @@ def _moveCalendarEvents(origUser, user, origCal, calIds, count, calendarEventEnt def doCalendarsMoveEvents(calIds): calendarEventEntity = getCalendarEventEntity() checkArgumentPresent(['to', 'destination']) - newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM)) + newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM), buildGAPIObject(API.DIRECTORY)) parameters, _ = _getCalendarMoveEventsOptions() if not checkCalendarExists(None, newCalId, 0, 0, True): return diff --git a/src/gam/cmd/cigroups/members.py b/src/gam/cmd/cigroups/members.py index 351da823..ee21c627 100644 --- a/src/gam/cmd/cigroups/members.py +++ b/src/gam/cmd/cigroups/members.py @@ -759,9 +759,9 @@ def doPrintCIGroups(): if myarg == 'todrive': csvPF.GetTodriveParameters() elif myarg == 'showownedby': - showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user']) + showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), buildGAPIObject(API.DIRECTORY), emailTypes=['user']) elif myarg in {'cimember', 'enterprisemember', 'ciowner'}: - emailAddress = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user', 'group']) + emailAddress = convertUIDtoEmailAddress(getEmailAddress(), buildGAPIObject(API.DIRECTORY), emailTypes=['user', 'group']) memberQuery = f"member_key_id == '{emailAddress}' && '{CIGROUP_DISCUSSION_FORUM_LABEL}' in labels && parent == '{parent}'" entitySelection = None if myarg == 'ciowner': @@ -1218,9 +1218,9 @@ def doPrintCIGroupMembers(): if myarg == 'todrive': csvPF.GetTodriveParameters() elif myarg == 'showownedby': - showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user']) + showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), buildGAPIObject(API.DIRECTORY), emailTypes=['user']) elif myarg in {'cimember', 'enterprisemember', 'ciowner'}: - emailAddress = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user', 'group']) + emailAddress = convertUIDtoEmailAddress(getEmailAddress(), buildGAPIObject(API.DIRECTORY), emailTypes=['user', 'group']) query = f"member_key_id == '{emailAddress}' && '{CIGROUP_DISCUSSION_FORUM_LABEL}' in labels && parent == '{parent}'" entityList = None if myarg == 'ciowner': @@ -1473,9 +1473,9 @@ def doShowCIGroupMembers(): while Cmd.ArgumentsRemaining(): myarg = getArgument() if myarg == 'showownedby': - showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user']) + showOwnedBy = convertUIDtoEmailAddress(getEmailAddress(), buildGAPIObject(API.DIRECTORY), emailTypes=['user']) elif myarg in {'cimember', 'enterprisemember', 'ciowner'}: - emailAddress = convertUIDtoEmailAddress(getEmailAddress(), emailTypes=['user', 'group']) + emailAddress = convertUIDtoEmailAddress(getEmailAddress(), buildGAPIObject(API.DIRECTORY), emailTypes=['user', 'group']) query = f"member_key_id == '{emailAddress}' && '{CIGROUP_DISCUSSION_FORUM_LABEL}' in labels parent == '{parent}'" entityList = None if myarg == 'ciowner': diff --git a/src/gam/cmd/ciuserinvitations.py b/src/gam/cmd/ciuserinvitations.py index 15153c92..dab5aa3a 100644 --- a/src/gam/cmd/ciuserinvitations.py +++ b/src/gam/cmd/ciuserinvitations.py @@ -230,7 +230,7 @@ def checkCIUserIsInvitable(users): i, count, users = getEntityArgument(users) for user in users: i += 1 - user = convertUIDtoEmailAddress(user) + user = convertUIDtoEmailAddress(user, buildGAPIObject(API.DIRECTORY)) name = quotedCIUserInvitatonsEmail(customer, user) try: result = callGAPI(ci.customers().userinvitations(), 'isInvitableUser', diff --git a/src/gam/cmd/cros.py b/src/gam/cmd/cros.py index d30079ab..a4c58187 100644 --- a/src/gam/cmd/cros.py +++ b/src/gam/cmd/cros.py @@ -25,9 +25,9 @@ from gam.util.api import ( callGAPIpages, checkGAPIError, ) +from gam.util.output import ISOformatTimeStamp from gam.util.args import ( - ISOformatTimeStamp, - SORTORDER_CHOICE_MAP, + SORTORDER_CHOICE_MAP, StartEndTime, YYYYMMDDTHHMMSS_FORMAT_REQUIRED, YYYYMMDD_FORMAT, diff --git a/src/gam/cmd/drive/core.py b/src/gam/cmd/drive/core.py index 4808398d..88920e83 100644 --- a/src/gam/cmd/drive/core.py +++ b/src/gam/cmd/drive/core.py @@ -22,7 +22,7 @@ from gamlib import glgapi as GAPI from gamlib import glglobals as GM from gamlib import glindent from gamlib import glmsgs as Msg -from gam.util.api import buildGAPIServiceObject, callGAPI, callGAPIpages, getHttpObj +from gam.util.api import buildGAPIObject, buildGAPIServiceObject, callGAPI, callGAPIpages, getHttpObj from gam.util.args import ( LANGUAGE_CODES_MAP, checkArgumentPresent, @@ -561,7 +561,7 @@ def _validateUserGetFileIDs(user, i, count, fileIdEntity, drive=None, entityType if not drive: return (user, None, 0) else: - user = convertUIDtoEmailAddress(user) + user = convertUIDtoEmailAddress(user, buildGAPIObject(API.DIRECTORY)) if fileIdEntity['list'] and _simpleFileIdEntityList(fileIdEntity['list']): l = len(fileIdEntity['list']) if ROOT in fileIdEntity['list'] and fileIdEntity[ROOT]: @@ -760,7 +760,7 @@ def _validateUserGetSharedDriveFileIDs(user, i, count, fileIdEntity, drive=None, if not drive: return (user, None, 0) else: - user = convertUIDtoEmailAddress(user) + user = convertUIDtoEmailAddress(user, buildGAPIObject(API.DIRECTORY)) if fileIdEntity.get('shareddrivename') and not _convertSharedDriveNameToId(drive, user, i, count, fileIdEntity): return (user, None, 0) if fileIdEntity['shareddrivefilequery']: diff --git a/src/gam/cmd/drive/files.py b/src/gam/cmd/drive/files.py index 66aed269..359440ee 100644 --- a/src/gam/cmd/drive/files.py +++ b/src/gam/cmd/drive/files.py @@ -21,8 +21,7 @@ from gamlib import glindent from gamlib import glmsgs as Msg from gam.util.api import buildGAPIServiceObject, callGAPI, callGAPIitems, callGAPIpages from gam.util.args import ( - ISOformatTimeStamp, - getAddCSVData, + getAddCSVData, getArgument, getBoolean, getCharacter, @@ -31,6 +30,7 @@ from gam.util.args import ( getString, protectedSheetId, ) +from gam.util.output import ISOformatTimeStamp from gam.util.csv_pf import CSVPrintFile from gam.util.display import ( entityActionFailedWarning, diff --git a/src/gam/cmd/oauth.py b/src/gam/cmd/oauth.py index 32c27d5e..68aeb974 100644 --- a/src/gam/cmd/oauth.py +++ b/src/gam/cmd/oauth.py @@ -39,7 +39,6 @@ from gam.util.api import ( writeClientCredentials, ) from gam.util.args import ( - ISOformatTimeStamp, UTF8, YYYYMMDDTHHMMSSZ_FORMAT, checkForExtraneousArguments, @@ -47,6 +46,7 @@ from gam.util.args import ( getEmailAddress, getString, ) +from gam.util.output import ISOformatTimeStamp from gam.util.display import ( entityActionNotPerformedWarning, entityActionPerformed, diff --git a/src/gam/cmd/project.py b/src/gam/cmd/project.py index fd4ce0f3..b75d62e9 100644 --- a/src/gam/cmd/project.py +++ b/src/gam/cmd/project.py @@ -1218,7 +1218,7 @@ def checkServiceAccount(users): for user in users: i += 1 allScopesPass = True - user = convertUIDtoEmailAddress(user) + user = convertUIDtoEmailAddress(user, buildGAPIObject(API.DIRECTORY)) printKeyValueListWithCount([Msg.DOMAIN_WIDE_DELEGATION_AUTHENTICATION, '', Ent.Singular(Ent.USER), user, Ent.Choose(Ent.SCOPE, jcount), jcount], diff --git a/src/gam/cmd/reports.py b/src/gam/cmd/reports.py index feb7cd69..bd06d86d 100644 --- a/src/gam/cmd/reports.py +++ b/src/gam/cmd/reports.py @@ -16,7 +16,6 @@ from gam.util.api import _getAdminEmail, buildGAPIObject, callGAPI, callGAPIpage from gam.util.args import ( DELTA_DATE_FORMAT_REQUIRED, DELTA_DATE_PATTERN, - ISOformatTimeStamp, PLUS_MINUS, StartEndTime, TODAY_NOW, @@ -39,6 +38,7 @@ from gam.util.args import ( orgUnitPathQuery, todaysDate, ) +from gam.util.output import ISOformatTimeStamp from gam.util.csv_pf import CSVPrintFile, flattenJSON, getTodriveOnly from gam.util.display import ( BAD_REQUEST_RC, diff --git a/src/gam/cmd/userservices.py b/src/gam/cmd/userservices.py index ff7d7859..f3375171 100644 --- a/src/gam/cmd/userservices.py +++ b/src/gam/cmd/userservices.py @@ -36,7 +36,6 @@ from gam.util.api import ( from gam.util.args import ( BCP47_LANGUAGE_CODES_MAP, CALENDAR_COLOR_MAP, - ISOformatTimeStamp, YYYYMMDD_FORMAT, checkArgumentPresent, checkForExtraneousArguments, @@ -56,6 +55,7 @@ from gam.util.args import ( normalizeEmailAddressOrUID, splitEmailAddress, ) +from gam.util.output import ISOformatTimeStamp from gam.util.csv_pf import ( CSVPrintFile, FormatJSONQuoteChar, @@ -1142,7 +1142,7 @@ def printShowCalendarACLs(users): j = 0 for calId in calIds: j += 1 - calId = convertUIDtoEmailAddress(calId) + calId = convertUIDtoEmailAddress(calId, buildGAPIObject(API.DIRECTORY)) _printShowCalendarACLs(cal, user, Ent.CALENDAR, calId, j, jcount, csvPF, FJQC, noSelfOwner, addCSVData) Ind.Decrement() if csvPF: @@ -1269,7 +1269,7 @@ def moveCalendarEvents(users): calendarEntity = getUserCalendarEntity() calendarEventEntity = getCalendarEventEntity() checkArgumentPresent(['to', 'destination']) - newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM)) + newCalId = convertUIDtoEmailAddress(getString(Cmd.OB_CALENDAR_ITEM), buildGAPIObject(API.DIRECTORY)) parameters, _ = _getCalendarMoveEventsOptions() if not checkCalendarExists(None, newCalId, 0, 0, True): return diff --git a/src/gam/util/access.py b/src/gam/util/access.py index 84a9b3e2..dec24263 100644 --- a/src/gam/util/access.py +++ b/src/gam/util/access.py @@ -15,7 +15,7 @@ from gamlib import glglobals as GM from gamlib import glindent from gamlib import glmsgs as Msg from gam.constants import API_ACCESS_DENIED_RC, INVALID_DOMAIN_RC -from util.api import _getAdminEmail, _getSvcAcctData, buildGAPIObject, callGAPI +from util.api import _getAdminEmail, _getSvcAcctData, buildGAPIObject, callGAPI, APIAccessDeniedExit, ClientAPIAccessDeniedExit, SvcAcctAPIAccessDeniedExit, SvcAcctAPIDisabledExit from util.args import getEmailAddressDomain, getPhraseDNEorSNA from util.display import ENTITY_DOES_NOT_EXIST_RC, ENTITY_DUPLICATE_RC, entityActionFailedWarning, entityDoesNotExistWarning, entityServiceNotApplicableWarning from util.errors import OAUTH2SERVICE_JSON_REQUIRED_RC @@ -72,48 +72,6 @@ def accessErrorExitNonDirectory(api, errMsg): Ent.API, api])+[errMsg], '')) -def ClientAPIAccessDeniedExit(errMsg=None): - if errMsg is None: - 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) - else: - stderrErrorMsg(errMsg) - systemErrorExit(API_ACCESS_DENIED_RC, Msg.REAUTHENTICATION_IS_NEEDED) - -def SvcAcctAPIAccessDenied(): - _getSvcAcctData() - if (GM.Globals[GM.CURRENT_SVCACCT_API] == API.GMAIL and - GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] and - GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES][0] == API.GMAIL_SEND_SCOPE): - systemErrorExit(OAUTH2SERVICE_JSON_REQUIRED_RC, Msg.NO_SVCACCT_ACCESS_ALLOWED) - stderrErrorMsg(Msg.API_ACCESS_DENIED) - apiOrScopes = API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]) if GM.Globals[GM.CURRENT_SVCACCT_API] else ','.join(sorted(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES])) - writeStderr(Msg.API_CHECK_SVCACCT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id'], - apiOrScopes, - GM.Globals[GM.CURRENT_SVCACCT_USER] or _getAdminEmail())) -def SvcAcctAPIAccessDeniedExit(): - SvcAcctAPIAccessDenied() - systemErrorExit(API_ACCESS_DENIED_RC, None) - -def SvcAcctAPIDisabledExit(): - if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]: - ClientAPIAccessDeniedExit() - if GM.Globals[GM.CURRENT_SVCACCT_API]: - stderrErrorMsg(Msg.SERVICE_ACCOUNT_API_DISABLED.format(API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]))) - systemErrorExit(API_ACCESS_DENIED_RC, None) - systemErrorExit(API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED) - -def APIAccessDeniedExit(): - if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]: - ClientAPIAccessDeniedExit() - if GM.Globals[GM.CURRENT_SVCACCT_API]: - SvcAcctAPIAccessDeniedExit() - systemErrorExit(API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED) - def checkEntityDNEorAccessErrorExit(cd, entityType, entityName, i=0, count=0): message = accessErrorMessage(cd) if message: diff --git a/src/gam/util/api.py b/src/gam/util/api.py index c99078a6..5b2f389a 100644 --- a/src/gam/util/api.py +++ b/src/gam/util/api.py @@ -264,7 +264,6 @@ class signjwtSignJwt(google.auth.crypt.Signer): return signed_jwt def handleOAuthTokenError(e, softErrors, displayError=False, i=0, count=0): - from util.access import APIAccessDeniedExit, ClientAPIAccessDeniedExit, SvcAcctAPIAccessDeniedExit errMsg = str(e).replace('.', '') if ((errMsg in API.OAUTH2_TOKEN_ERRORS) or errMsg.startswith('Invalid response') or @@ -641,7 +640,6 @@ def _getSvcAcctData(): GM.Globals[GM.SVCACCT_SCOPES] = GM.Globals[GM.OAUTH2SERVICE_JSON_DATA].pop(API.OAUTH2SA_SCOPES) def getSvcAcctCredentials(scopesOrAPI, userEmail, softErrors=False, forceOauth=False): - from util.access import SvcAcctAPIAccessDeniedExit _getSvcAcctData() if isinstance(scopesOrAPI, str): GM.Globals[GM.CURRENT_SVCACCT_API] = scopesOrAPI @@ -844,7 +842,6 @@ def callGData(service, function, bailOnInternalServerError=False, softErrors=False, throwErrors=None, retryErrors=None, triesLimit=0, **kwargs): - from util.access import APIAccessDeniedExit if throwErrors is None: throwErrors = [] if retryErrors is None: @@ -1137,7 +1134,6 @@ def callGAPI(service, function, softErrors=False, mapNotFound=True, throwReasons=None, retryReasons=None, triesLimit=0, **kwargs): - from util.access import APIAccessDeniedExit if throwReasons is None: throwReasons = [] if retryReasons is None: @@ -1415,14 +1411,6 @@ def buildGAPIObject(api, credentials=None): GM.Globals[GM.OAUTH2_CLIENT_ID] = credentials.client_id return service -def getSaUser(user): - from util.entity import convertUIDtoEmailAddress - 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 chooseSaAPI(api1, api2): _getSvcAcctData() @@ -1513,3 +1501,55 @@ def getContactsQuery(**kwargs): def getEmailAuditObject(): return initGDataObject(gdata.apps.audit.service.AuditService(), API.EMAIL_AUDIT) + +# API access denied handlers (moved from access.py to break cycle) +def ClientAPIAccessDeniedExit(errMsg=None): + if errMsg is None: + 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) + else: + stderrErrorMsg(errMsg) + systemErrorExit(API_ACCESS_DENIED_RC, Msg.REAUTHENTICATION_IS_NEEDED) + + +def SvcAcctAPIAccessDenied(): + _getSvcAcctData() + if (GM.Globals[GM.CURRENT_SVCACCT_API] == API.GMAIL and + GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES] and + GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES][0] == API.GMAIL_SEND_SCOPE): + systemErrorExit(OAUTH2SERVICE_JSON_REQUIRED_RC, Msg.NO_SVCACCT_ACCESS_ALLOWED) + stderrErrorMsg(Msg.API_ACCESS_DENIED) + apiOrScopes = API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]) if GM.Globals[GM.CURRENT_SVCACCT_API] else ','.join(sorted(GM.Globals[GM.CURRENT_SVCACCT_API_SCOPES])) + writeStderr(Msg.API_CHECK_SVCACCT_AUTHORIZATION.format(GM.Globals[GM.OAUTH2SERVICE_JSON_DATA]['client_id'], + apiOrScopes, + GM.Globals[GM.CURRENT_SVCACCT_USER] or _getAdminEmail())) + +def SvcAcctAPIAccessDeniedExit(): + SvcAcctAPIAccessDenied() + systemErrorExit(API_ACCESS_DENIED_RC, None) + + +def SvcAcctAPIDisabledExit(): + if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]: + ClientAPIAccessDeniedExit() + if GM.Globals[GM.CURRENT_SVCACCT_API]: + stderrErrorMsg(Msg.SERVICE_ACCOUNT_API_DISABLED.format(API.getAPIName(GM.Globals[GM.CURRENT_SVCACCT_API]))) + systemErrorExit(API_ACCESS_DENIED_RC, None) + systemErrorExit(API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED) + + +def APIAccessDeniedExit(): + if not GM.Globals[GM.CURRENT_SVCACCT_USER] and GM.Globals[GM.CURRENT_CLIENT_API]: + ClientAPIAccessDeniedExit() + if GM.Globals[GM.CURRENT_SVCACCT_API]: + SvcAcctAPIAccessDeniedExit() + systemErrorExit(API_ACCESS_DENIED_RC, Msg.API_ACCESS_DENIED) + + +# Late import: uid.py imports callGAPI/buildGAPIObject from this module, +# so we import from uid after those are defined to avoid circular ImportError. +from util.uid import getSaUser # noqa: E402 diff --git a/src/gam/util/args.py b/src/gam/util/args.py index c5a3f23d..29741dec 100644 --- a/src/gam/util/args.py +++ b/src/gam/util/args.py @@ -100,6 +100,7 @@ from util.errors import ( ) from util.fileio import readFile +from util.output import ISOformatTimeStamp from gam.var import Cmd, Ent # Lazy accessor for main module @@ -131,12 +132,6 @@ SECONDS_PER_HOUR = 3600 SECONDS_PER_DAY = 86400 SECONDS_PER_WEEK = 604800 -def ISOformatTimeStamp(timestamp): - return timestamp.isoformat('T', 'seconds') - -def currentISOformatTimeStamp(timespec='milliseconds'): - return arrow.now(GC.Values[GC.TIMEZONE]).isoformat('T', timespec) - def checkArgumentPresent(choices, required=False): choiceList = choices if isinstance(choices, (list, set)) else [choices] if Cmd.ArgumentsRemaining(): diff --git a/src/gam/util/batch.py b/src/gam/util/batch.py index d0d591bb..9efdd6d8 100644 --- a/src/gam/util/batch.py +++ b/src/gam/util/batch.py @@ -27,14 +27,14 @@ from gamlib import glmsgs as Msg from util.csv_pf import CSVPrintFile from gam.constants import HARD_ERROR_RC, KEYBOARD_INTERRUPT_RC from util.api import buildGAPIObject -from util.args import UTF8, checkArgumentPresent, currentISOformatTimeStamp, checkForExtraneousArguments, checkMatchSkipFields, getBoolean, getCharSet, getDelimiter, getInteger, getMatchSkipFields, getString, normalizeEmailAddressOrUID, todaysTime +from util.args import UTF8, checkArgumentPresent, checkForExtraneousArguments, checkMatchSkipFields, getBoolean, getCharSet, getDelimiter, getInteger, getMatchSkipFields, getString, normalizeEmailAddressOrUID, todaysTime from util.csv_pf import CheckInputRowFilterHeaders from util.display import actionPerformedNumItems from util.entity import getEntityArgument, getEntityList, getEntityToModify from util.errors import USAGE_ERROR_RC, csvFieldErrorExit, formatChoiceList, missingArgumentExit, usageErrorExit -from util.fileio import FILE_ERROR_RC, StringIOobject, closeFile, closeGAMCommandLog, fdErrorMessage, fileErrorMessage, openFile, openGAMCommandLog, setFilePath, writeGAMCommandLog +from util.fileio import FILE_ERROR_RC, StringIOobject, adjustRedirectedSTDFilesIfNotMultiprocessing, closeFile, closeGAMCommandLog, fdErrorMessage, fileErrorMessage, openFile, openGAMCommandLog, setFilePath, writeGAMCommandLog from util.gdoc import getGDocData, getStorageFileData, openCSVFileReader -from util.output import ERROR_PREFIX, flushStderr, flushStdout, readStdin, setSysExitRC, systemErrorExit, writeStderr, writeStdout +from util.output import ERROR_PREFIX, currentISOformatTimeStamp, flushStderr, flushStdout, readStdin, setSysExitRC, systemErrorExit, writeStderr, writeStdout from gam.constants import GAM from gam.var import Cmd, Ind @@ -800,7 +800,6 @@ def doBatch(threadBatch=False): # gam tbatch [showcmds []] def doThreadBatch(): - from util.fileio import adjustRedirectedSTDFilesIfNotMultiprocessing adjustRedirectedSTDFilesIfNotMultiprocessing() doBatch(True) diff --git a/src/gam/util/connection.py b/src/gam/util/connection.py index c28ed1c8..e9a410eb 100644 --- a/src/gam/util/connection.py +++ b/src/gam/util/connection.py @@ -26,7 +26,8 @@ from gam.constants import ( GOOGLE_TIMECHECK_LOCATION, MAX_LOCAL_GOOGLE_TIME_OFFSET, NETWORK_ERROR_RC, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE, ) -from util.args import ISOformatTimeStamp, getArgument, getString, todaysTime +from util.args import getArgument, getString, todaysTime +from util.output import ISOformatTimeStamp from util.display import printBlankLine, printKeyValueList from util.errors import unknownArgumentExit from util.output import ( diff --git a/src/gam/util/csv_pf.py b/src/gam/util/csv_pf.py index 2b6bbc97..6d79f77c 100644 --- a/src/gam/util/csv_pf.py +++ b/src/gam/util/csv_pf.py @@ -21,13 +21,13 @@ from gamlib import glmsgs as Msg from gam.constants import DEFAULT_FILE_APPEND_MODE, INSUFFICIENT_PERMISSIONS_RC, MAX_GOOGLE_SHEET_CELLS, NO_CSV_DATA_TO_UPLOAD_RC, ROOT from tempfile import TemporaryFile from util.api import _getAdminEmail, buildGAPIObject, buildGAPIServiceObject, callGAPI, callGAPIpages, chooseSaAPI -from util.args import ISOformatTimeStamp, LOCALE_CODES_MAP, NEVER_TIME, TRUE_FALSE, UTF8, YYYYMMDD_FORMAT, YYYYMMDD_PATTERN, escapeCRsNLs, formatLocalTime, formatLocalTimestamp, getArgument, getBoolean, getCharacter, getChoice, getInteger, getLanguageCode, getSheetEntity, getSheetIdFromSheetEntity, getString, normalizeEmailAddressOrUID, protectedSheetId, todaysTime +from util.args import LOCALE_CODES_MAP, NEVER_TIME, TRUE_FALSE, UTF8, YYYYMMDD_FORMAT, YYYYMMDD_PATTERN, escapeCRsNLs, formatLocalTime, formatLocalTimestamp, getArgument, getBoolean, getCharacter, getChoice, getInteger, getLanguageCode, getSheetEntity, getSheetIdFromSheetEntity, getString, normalizeEmailAddressOrUID, protectedSheetId, todaysTime from util.display import ACTION_FAILED_RC, entityActionFailedWarning, entityActionPerformed, printBlankLine, printJSONKey, printJSONValue, printKeyValueList, userDriveServiceNotEnabledWarning from util.email import send_email from util.entity import MIMETYPE_GA_FOLDER, MIMETYPE_GA_SPREADSHEET, checkUserExists, getEntityArgument from util.errors import USAGE_ERROR_RC, invalidArgumentExit, invalidChoiceExit, missingArgumentExit, unknownArgumentExit, usageErrorExit from util.fileio import FILE_ERROR_RC, StringIOobject, closeFile, fdErrorMessage, openFile -from util.output import WARNING, currentCountNL, formatKeyValueList, printWarningMessage, setSysExitRC, stderrErrorMsg, stderrWarningMsg, systemErrorExit, writeStdout +from util.output import ISOformatTimeStamp, WARNING, currentCountNL, formatKeyValueList, printWarningMessage, setSysExitRC, stderrErrorMsg, stderrWarningMsg, systemErrorExit, writeStdout from gam.var import Act, Cmd, Ent, Ind diff --git a/src/gam/util/entity.py b/src/gam/util/entity.py index 1e5152d3..08e22230 100644 --- a/src/gam/util/entity.py +++ b/src/gam/util/entity.py @@ -25,7 +25,6 @@ from gamlib import glmsgs as Msg def getUserEmailFromID(uid, cd): - from util.api import callGAPI try: result = callGAPI(cd.users(), 'get', throwReasons=GAPI.USER_GET_THROW_REASONS, @@ -37,7 +36,6 @@ def getUserEmailFromID(uid, cd): return None def getGroupEmailFromID(uid, cd): - from util.api import callGAPI try: result = callGAPI(cd.groups(), 'get', throwReasons=GAPI.GROUP_GET_THROW_REASONS, @@ -49,7 +47,6 @@ def getGroupEmailFromID(uid, cd): return None def getServiceAccountEmailFromID(account_id, sal=None): - from util.api import buildGAPIObject, callGAPI if sal is None: sal = buildGAPIObject(API.SERVICEACCOUNTLOOKUP) try: @@ -72,113 +69,9 @@ def getServiceAccountEmailFromID(account_id, sal=None): 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): - from util.api import buildGAPIObject, callGAPI - if emailTypes is None: - emailTypes = ['user'] - elif not isinstance(emailTypes, list): - emailTypes = [emailTypes] if emailTypes != 'any' else ['user', 'group'] - if checkForCustomerId and (emailAddressOrUID == GC.Values[GC.CUSTOMER_ID]): - return (emailAddressOrUID, 'email') - normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID, ciGroupsAPI=ciGroupsAPI) - if ciGroupsAPI and emailAddressOrUID.startswith('groups/'): - return emailAddressOrUID - if normalizedEmailAddressOrUID.find('@') > 0 and aliasAllowed: - return (normalizedEmailAddressOrUID, 'email') - if cd is None: - cd = buildGAPIObject(API.DIRECTORY) - if 'user' in emailTypes and 'group' in emailTypes: - # Google User IDs *TEND* to be integers while groups tend to have letters - # thus we can optimize which check we try first. We'll still check - # both since there is no guarantee this will always be true. - if normalizedEmailAddressOrUID.isdigit(): - uid = getUserEmailFromID(normalizedEmailAddressOrUID, cd) - if uid: - return (uid, 'user') - uid = getGroupEmailFromID(normalizedEmailAddressOrUID, cd) - if uid: - return (uid, 'group') - else: - uid = getGroupEmailFromID(normalizedEmailAddressOrUID, cd) - if uid: - return (uid, 'group') - uid = getUserEmailFromID(normalizedEmailAddressOrUID, cd) - if uid: - return (uid, 'user') - elif 'user' in emailTypes: - uid = getUserEmailFromID(normalizedEmailAddressOrUID, cd) - if uid: - return (uid, 'user') - elif 'group' in emailTypes: - uid = getGroupEmailFromID(normalizedEmailAddressOrUID, cd) - if uid: - return (uid, 'group') - if 'resource' in emailTypes: - try: - result = callGAPI(cd.resources().calendars(), 'get', - throwReasons=[GAPI.BAD_REQUEST, GAPI.RESOURCE_NOT_FOUND, GAPI.FORBIDDEN], - calendarResourceId=normalizedEmailAddressOrUID, - customer=GC.Values[GC.CUSTOMER_ID], fields='resourceEmail') - if 'resourceEmail' in result: - return (result['resourceEmail'].lower(), 'resource') - except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden): - pass - if 'serviceaccount' in emailTypes: - uid = getServiceAccountEmailFromID(normalizedEmailAddressOrUID, sal) - if uid: - return (uid, 'serviceaccount') - return (normalizedEmailAddressOrUID, 'unknown') - -NON_EMAIL_MEMBER_PREFIXES = ( - "cbcm-browser.", - "chrome-os-device.", - ) -# Convert UID to email address -def convertUIDtoEmailAddress(emailAddressOrUID, cd=None, emailTypes=None, - checkForCustomerId=False, ciGroupsAPI=False, aliasAllowed=True): - if ciGroupsAPI: - if emailAddressOrUID.startswith(NON_EMAIL_MEMBER_PREFIXES): - return emailAddressOrUID - normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID, ciGroupsAPI=ciGroupsAPI) - if normalizedEmailAddressOrUID.startswith(NON_EMAIL_MEMBER_PREFIXES): - return normalizedEmailAddressOrUID - email, _ = convertUIDtoEmailAddressWithType(emailAddressOrUID, cd, None, 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): - from util.api import buildGAPIObject, callGAPI - 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): - from util.api import buildGAPIObject, callGAPI primaryEmail = GM.Globals[GM.MAP_USER_ID_TO_NAME].get(uid) if not primaryEmail: if cd is None: @@ -196,7 +89,6 @@ def convertUserIDtoEmail(uid, cd=None): # Convert UID to split email address # Return (foo@bar.com, foo, bar.com) def splitEmailAddressOrUID(emailAddressOrUID): - from util.api import buildGAPIObject, callGAPI normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID) atLoc = normalizedEmailAddressOrUID.find('@') if atLoc > 0: @@ -217,7 +109,6 @@ def splitEmailAddressOrUID(emailAddressOrUID): # Convert Org Unit Id to Org Unit Path def convertOrgUnitIDtoPath(cd, orgUnitId): - from util.api import buildGAPIObject, callGAPI if orgUnitId.lower().startswith('orgunits/'): orgUnitId = f'id:{orgUnitId[9:]}' orgUnitPath = GM.Globals[GM.MAP_ORGUNIT_ID_TO_NAME].get(orgUnitId) @@ -234,6 +125,7 @@ def convertOrgUnitIDtoPath(cd, orgUnitId): return orgUnitPath from util.args import shlexSplitList, shlexSplitListStatus # noqa: E402,F401 - moved to args, re-exported for compat +from util.uid import convertUIDtoEmailAddress, convertUIDtoEmailAddressWithType, convertEmailAddressToUID # noqa: F401 - re-export from gam.constants import DATA_ERROR_RC, INVALID_ENTITY_RC, NO_ENTITIES_FOUND_RC, UNKNOWN_ERROR_RC from gamlib import glskus as SKU from util.args import ARCHIVED_ARGUMENTS, FALSE_VALUES, SUSPENDED_ARGUMENTS, TRUE_VALUES, _getIsArchived, _getIsSuspended, checkArgumentPresent, checkDataField, checkMatchSkipFields, checkSubkeyField, getArgument, getCharSet, getChoice, getDelimiter, getMatchSkipFields, getPhraseDNEorSNA, getREPattern, getString, makeOrgUnitPathAbsolute, normalizeEmailAddressOrUID, orgUnitPathQuery, removeCourseIdScope, splitEmailAddress, validateEmailAddressOrUID @@ -242,6 +134,9 @@ from util.errors import csvDataAlreadySavedErrorExit, csvFieldErrorExit, entityD from util.fileio import closeFile, openFile, setFilePath from util.gdoc import getGDocData, getStorageFileData, openCSVFileReader from util.output import formatKeyValueList, printErrorMessage, setSysExitRC, stderrErrorMsg, systemErrorExit, writeStderr +from gam.util.access import ClientAPIAccessDeniedExit, accessErrorExit +from util.access import accessErrorExit, checkEntityDNEorAccessErrorExit, entityUnknownWarning +from util.api import _getAdminEmail, buildGAPIObject, buildGAPIServiceObject, callGAPI, callGAPIitems, callGAPIpages, yieldGAPIpages from gam.var import Act, Cmd, Ent def getQueries(myarg): @@ -422,7 +317,6 @@ def getCIGroupTransitiveMemberRoleFixType(groupName, tmember): return member def convertGroupCloudIDToEmail(ci, group, i=0, count=0): - from util.api import buildGAPIObject, callGAPI if not group.startswith('groups/'): group = normalizeEmailAddressOrUID(group, ciGroupsAPI=True) if not group.startswith('groups/'): @@ -444,7 +338,6 @@ def convertGroupCloudIDToEmail(ci, group, i=0, count=0): return (ci, None, None) def convertGroupEmailToCloudID(ci, group, i=0, count=0): - from util.api import buildGAPIObject, callGAPI group = normalizeEmailAddressOrUID(group, ciGroupsAPI=True) if not group.startswith('groups/') and group.find('@') == -1: group = 'groups/'+group @@ -473,7 +366,6 @@ CIGROUP_SECURITY_LABEL = 'cloudidentity.googleapis.com/groups.security' CIGROUP_LOCKED_LABEL = 'cloudidentity.googleapis.com/groups.locked' def getCIGroupMembershipGraph(ci, member): - from util.api import buildGAPIObject, callGAPI if not ci: ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS) parent = 'groups/-' @@ -493,8 +385,6 @@ def getCIGroupMembershipGraph(ci, member): return (ci, None) def checkGroupExists(cd, ci, ciGroupsAPI, group, i=0, count=0): - from util.access import entityUnknownWarning - from util.api import callGAPI group = normalizeEmailAddressOrUID(group, ciGroupsAPI=ciGroupsAPI) if not ciGroupsAPI: if not group.startswith('groups/'): @@ -539,8 +429,6 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA printErrorMessage(INVALID_ENTITY_RC, formatKeyValueList('', [Ent.Singular(entityType), entityName, Msg.INVALID], '')) def _addGroupUsersToUsers(group, domains, recursive, includeDerivedMembership): - from util.access import entityUnknownWarning - from util.api import callGAPIpages 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: @@ -571,8 +459,6 @@ def getItemsToModify(entityType, entity, memberRoles=None, isSuspended=None, isA def _addCIGroupUsersToUsers(groupName, groupEmail, recursive): from gam.cmd.licenses import doPrintLicenses from gam.cmd.courses.courses import _getCoursesOwnerInfo - from util.access import ClientAPIAccessDeniedExit, accessErrorExit, accessErrorExitNonDirectory, checkEntityDNEorAccessErrorExit, entityUnknownWarning - from util.api import buildGAPIObject, callGAPI, callGAPIpages, yieldGAPIpages printGettingAllEntityItemsForWhom(memberRoles if memberRoles else Ent.ROLE_MANAGER_MEMBER_OWNER, groupEmail, entityType=Ent.CLOUD_IDENTITY_GROUP) validRoles = _getCIRoleVerification(memberRoles) try: @@ -1347,7 +1233,6 @@ def getEntityArgument(entityList): def getEntityToModify(defaultEntityType=None, browserAllowed=False, crosAllowed=False, userAllowed=True, typeMap=None, isSuspended=None, isArchived=None, groupMemberType='USER', delayGet=False): - from util.api import _getAdminEmail, buildGAPIObject if GC.Values[GC.USER_SERVICE_ACCOUNT_ACCESS_ONLY]: crosAllowed = False selectorChoices = Cmd.SERVICE_ACCOUNT_ONLY_ENTITY_SELECTORS[:] @@ -1514,7 +1399,6 @@ def getUserObjectEntity(clObject, itemType, shlexSplit=False): return entity def _validateUserGetObjectList(user, i, count, entity, api=API.GMAIL, showAction=True): - from util.api import buildGAPIServiceObject if entity['dict']: entityList = entity['dict'][user] else: @@ -1530,7 +1414,6 @@ def _validateUserGetObjectList(user, i, count, entity, api=API.GMAIL, showAction return (user, svc, entityList, jcount) def _validateUserGetMessageIds(user, i, count, entity): - from util.api import buildGAPIServiceObject if entity: if entity['dict']: entityList = entity['dict'][user] @@ -1544,8 +1427,6 @@ def _validateUserGetMessageIds(user, i, count, entity): return (user, gmail, entityList) def checkUserExists(cd, user, entityType=None, i=0, count=0): - from util.access import entityUnknownWarning - from util.api import callGAPI if entityType is None: entityType = Ent.USER user = normalizeEmailAddressOrUID(user) @@ -1559,8 +1440,6 @@ def checkUserExists(cd, user, entityType=None, i=0, count=0): return None def checkUserSuspended(cd, user, entityType=None, i=0, count=0): - from util.access import entityUnknownWarning - from util.api import callGAPI if entityType is None: entityType = Ent.USER user = normalizeEmailAddressOrUID(user) @@ -1598,8 +1477,6 @@ def _getCustomersCustomerIdWithC(): return f'customers/{customerId}' def _getDomainList(cd, customer, fields): - from util.api import callGAPIitems - from gam.util.access import accessErrorExit, ClientAPIAccessDeniedExit try: return callGAPIitems(cd.domains(), 'list', 'domains', throwReasons=[GAPI.BAD_REQUEST, GAPI.NOT_FOUND, @@ -1611,8 +1488,6 @@ def _getDomainList(cd, customer, fields): ClientAPIAccessDeniedExit(str(e)) def setTrueCustomerId(cd=None, forceUpdate=False): - from util.api import buildGAPIObject, callGAPI - from gam.util.access import ClientAPIAccessDeniedExit if GC.Values[GC.CUSTOMER_ID] == GC.MY_CUSTOMER or forceUpdate: if not cd: cd = buildGAPIObject(API.DIRECTORY) diff --git a/src/gam/util/fileio.py b/src/gam/util/fileio.py index 2e1e3525..027605c8 100644 --- a/src/gam/util/fileio.py +++ b/src/gam/util/fileio.py @@ -20,7 +20,7 @@ from gamlib import glmsgs as Msg from gam.var import Act, Ent, Ind -from util.output import ( +from util.output import (currentISOformatTimeStamp, stderrErrorMsg, stderrWarningMsg, setSysExitRC, @@ -258,7 +258,6 @@ def openGAMCommandLog(Globals, name): systemErrorExit(CONFIG_ERROR_RC, Msg.LOGGING_INITIALIZATION_ERROR.format(str(e))) def writeGAMCommandLog(Globals, logCmd, sysRC): - from util.args import currentISOformatTimeStamp Globals[GM.CMDLOG_LOGGER].info(f'{currentISOformatTimeStamp()},{sysRC},{logCmd}') def closeGAMCommandLog(Globals): diff --git a/src/gam/util/gdoc.py b/src/gam/util/gdoc.py index c61aef8d..be237f7c 100644 --- a/src/gam/util/gdoc.py +++ b/src/gam/util/gdoc.py @@ -29,6 +29,7 @@ from util.display import ACTION_NOT_PERFORMED_RC, userDriveServiceNotEnabledWarn from util.errors import entityActionFailedExit, entityDoesNotExistExit from util.fileio import FILE_ERROR_RC, UTF8_SIG, fileErrorMessage, getGDocSheetDataFailedExit, getGDocSheetDataRetryWarning, openFile, setFilePath from util.output import stderrWarningMsg, systemErrorExit +from util.api import buildGAPIObject, buildGAPIServiceObject, callGAPI @@ -41,7 +42,6 @@ GDOC_FORMAT_MIME_TYPES = { # gdoc | def getGDocData(gformat): - from util.api import buildGAPIObject, buildGAPIServiceObject, callGAPI mimeType = GDOC_FORMAT_MIME_TYPES[gformat] user = getEmailAddress() fileIdEntity = getDriveFileEntity(queryShortcutsOK=False) @@ -103,7 +103,6 @@ HTML_TITLE_PATTERN = re.compile(r'.*(.+)') # gsheet | def getGSheetData(): - from util.api import buildGAPIObject, buildGAPIServiceObject, callGAPI user = getEmailAddress() fileIdEntity = getDriveFileEntity(queryShortcutsOK=False) sheetEntity = getSheetEntity(False) @@ -197,7 +196,6 @@ GCS_FORMAT_MIME_TYPES = { # gcscsv|gcshtml|gcsdoc def getStorageFileData(gcsformat, returnData=True): - from util.api import buildGAPIObject, callGAPI mimeType = GCS_FORMAT_MIME_TYPES[gcsformat] bucket, s_object, bucketObject = getBucketObjectName() s = buildGAPIObject(API.STORAGEREAD) diff --git a/src/gam/util/output.py b/src/gam/util/output.py index 81abe72f..c74665fd 100644 --- a/src/gam/util/output.py +++ b/src/gam/util/output.py @@ -202,3 +202,12 @@ def showAPICallsRetryData(): writeStderr(formatKeyValueList(Ind.Spaces(), [k, f'{v[0]}/{h}:{m:02d}:{s:02d}'], '\n')) Ind.Decrement() +# Timestamp functions (moved from args.py to break fileio↔args cycle) +def ISOformatTimeStamp(timestamp): + return timestamp.isoformat('T', 'seconds') + + +def currentISOformatTimeStamp(timespec='milliseconds'): + return arrow.now(GC.Values[GC.TIMEZONE]).isoformat('T', timespec) + + diff --git a/src/gam/util/uid.py b/src/gam/util/uid.py new file mode 100644 index 00000000..2f0f5afa --- /dev/null +++ b/src/gam/util/uid.py @@ -0,0 +1,129 @@ +"""UID-to-email-address resolution utilities. + +Convert UIDs to email addresses by querying the Directory/CI APIs. +Extracted from entity.py to break the entity↔api circular dependency. +""" + +from gamlib import glapi as API +from gamlib import glcfg as GC +from gamlib import glgapi as GAPI +from gamlib import glglobals as GM +from gamlib import glmsgs as Msg +from gam.var import Ent +from util.api import buildGAPIObject, callGAPI +from util.args import normalizeEmailAddressOrUID +from util.display import entityDoesNotExistWarning, printGettingAllEntityItemsForWhom +from util.errors import usageErrorExit +from util.output import currentCountNL + +NON_EMAIL_MEMBER_PREFIXES = ( + "cbcm-browser.", + "chrome-os-device.", + ) + +def convertUIDtoEmailAddressWithType(emailAddressOrUID, cd, 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 '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') + + +def convertUIDtoEmailAddress(emailAddressOrUID, cd, emailTypes=None, + checkForCustomerId=False, ciGroupsAPI=False, aliasAllowed=True): + if ciGroupsAPI: + if emailAddressOrUID.startswith(NON_EMAIL_MEMBER_PREFIXES): + return emailAddressOrUID + normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID, ciGroupsAPI=ciGroupsAPI) + if normalizedEmailAddressOrUID.startswith(NON_EMAIL_MEMBER_PREFIXES): + return normalizedEmailAddressOrUID + email, _ = convertUIDtoEmailAddressWithType(emailAddressOrUID, cd, None, emailTypes, + checkForCustomerId, ciGroupsAPI, aliasAllowed) + return email + + +def convertEmailAddressToUID(emailAddressOrUID, cd, emailType='user', savedLocation=None): + normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID) + if normalizedEmailAddressOrUID.find('@') == -1: + return normalizedEmailAddressOrUID + 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)) + + +def getSaUser(user): + """Resolve a user UID/email for service account impersonation.""" + currentClientAPI = GM.Globals[GM.CURRENT_CLIENT_API] + currentClientAPIScopes = GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] + if user: + cd = buildGAPIObject(API.DIRECTORY) + userEmail = convertUIDtoEmailAddress(user, cd) + else: + userEmail = None + GM.Globals[GM.CURRENT_CLIENT_API] = currentClientAPI + GM.Globals[GM.CURRENT_CLIENT_API_SCOPES] = currentClientAPIScopes + return userEmail