diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 80d9ef03..0fe25cbc 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -1,3 +1,24 @@ +7.25.00 + +Removed a capabilty added in 7.24.00 that allowed reading command data from Google Docs and Sheets +when a user's service account access to Drive and Sheets had been disabled. Jay was concerned +that this change could be exploited to give access to all user's files. + +This capability has been replaced by issuing the following commands. The admin specified in `gam oauth create` +can read command data from Docs and Sheets to which it has access. +``` +gam config commanddata_clientaccess true save +gam oauth create +Enable the following and proceed to authorization. + +[*] 42) Drive API - commanddata_clientaccess +[*] 54) Sheets API - commanddata_clientaccess +``` + +Fixed in bug in `gam report` that caused a trap with either of the `thismonth` or `previousmonths` options were used. + +Upgraded to Python 3.14.0. + 7.24.01 Updated GAM to handle the following error that occurs when GAM tries to authenticate diff --git a/src/gam/__init__.py b/src/gam/__init__.py index 436829b5..5c8a390c 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki """ __author__ = 'GAM Team ' -__version__ = '7.24.01' +__version__ = '7.25.00' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' #pylint: disable=wrong-import-position @@ -2113,12 +2113,12 @@ class StartEndTime(): else: firstMonth = getInteger(minVal=1, maxVal=6) currDate = todaysDate() - self.startDateTime = currDate.shift(months=-firstMonth, day=1, hour=0, minute=0, second=0, microsecond=0) + self.startDateTime = currDate.replace(day=1, hour=0, minute=0, second=0, microsecond=0).shift(months=-firstMonth) self.startTime = ISOformatTimeStamp(self.startDateTime) if myarg == 'thismonth': self.endDateTime = todaysTime() else: - self.endDateTime = currDate.shift(day=1, hour=23, minute=59, second=59, microsecond=0).shift(days=-1) + self.endDateTime = currDate.replace(day=1, hour=23, minute=59, second=59, microsecond=0).shift(days=-1) self.endTime = ISOformatTimeStamp(self.endDateTime) if self.startDateTime and self.endDateTime and self.endDateTime < self.startDateTime: Cmd.Backup() @@ -3049,7 +3049,10 @@ def getGDocData(gformat): mimeType = GDOC_FORMAT_MIME_TYPES[gformat] user = getEmailAddress() fileIdEntity = getDriveFileEntity(queryShortcutsOK=False) - _, drive = buildGAPIServiceObject(chooseSaAPI(API.DRIVECD, API.DRIVE3), user) + if not GC.Values[GC.COMMANDDATA_CLIENTACCESS]: + _, drive = buildGAPIServiceObject(API.DRIVE3, user) + else: + drive = buildGAPIObject(API.DRIVE3) if not drive: sys.exit(GM.Globals[GM.SYSEXITRC]) _, _, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity, drive=drive) @@ -3106,7 +3109,10 @@ def getGSheetData(): user = getEmailAddress() fileIdEntity = getDriveFileEntity(queryShortcutsOK=False) sheetEntity = getSheetEntity(False) - user, drive = buildGAPIServiceObject(chooseSaAPI(API.DRIVECD, API.DRIVE3), user) + if not GC.Values[GC.COMMANDDATA_CLIENTACCESS]: + user, drive = buildGAPIServiceObject(API.DRIVE3, user) + else: + drive = buildGAPIObject(API.DRIVE3) if not drive: sys.exit(GM.Globals[GM.SYSEXITRC]) _, _, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity, drive=drive) @@ -3114,7 +3120,10 @@ def getGSheetData(): 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(chooseSaAPI(API.SHEETSCD, API.SHEETS), user) + if not GC.Values[GC.COMMANDDATA_CLIENTACCESS]: + _, sheet = buildGAPIServiceObject(API.SHEETS, user) + else: + sheet = buildGAPIObject(API.SHEETS) if not sheet: sys.exit(GM.Globals[GM.SYSEXITRC]) fileId = fileIdEntity['list'][0] @@ -11137,7 +11146,7 @@ class Credentials(google.oauth2.credentials.Credentials): def doOAuthRequest(currentScopes, login_hint, verifyScopes=False): client_id, client_secret = getOAuthClientIDAndSecret() - scopesList = API.getClientScopesList(GC.Values[GC.TODRIVE_CLIENTACCESS]) + scopesList = API.getClientScopesList(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS]) if not currentScopes or verifyScopes: selectedScopes = getScopesFromUser(scopesList, True, currentScopes) if selectedScopes is None: @@ -11183,7 +11192,7 @@ def doOAuthCreate(): else: login_hint = None scopes = [] - scopesList = API.getClientScopesList(GC.Values[GC.TODRIVE_CLIENTACCESS]) + scopesList = API.getClientScopesList(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS]) while Cmd.ArgumentsRemaining(): myarg = getArgument() if myarg == 'admin': @@ -11199,7 +11208,9 @@ def doOAuthCreate(): scopes.append(uscope) break else: - invalidChoiceExit(uscope, API.getClientScopesURLs(GC.Values[GC.TODRIVE_CLIENTACCESS]), True) + invalidChoiceExit(uscope, + API.getClientScopesURLs(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS]), + True) else: unknownArgumentExit() if len(scopes) == 0: @@ -11310,7 +11321,7 @@ def doOAuthUpdate(): if 'scopes' in jsonDict: currentScopes = jsonDict['scopes'] else: - currentScopes = API.getClientScopesURLs(GC.Values[GC.TODRIVE_CLIENTACCESS]) + currentScopes = API.getClientScopesURLs(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS]) else: currentScopes = [] except (AttributeError, IndexError, KeyError, SyntaxError, TypeError, ValueError) as e: diff --git a/src/gam/gamlib/glapi.py b/src/gam/gamlib/glapi.py index 87a68a56..fac450a8 100644 --- a/src/gam/gamlib/glapi.py +++ b/src/gam/gamlib/glapi.py @@ -60,7 +60,6 @@ DIRECTORY = 'directory' DOCS = 'docs' DRIVE2 = 'drive2' DRIVE3 = 'drive3' -DRIVECD = 'drivecd' DRIVETD = 'drivetd' DRIVEACTIVITY = 'driveactivity' DRIVELABELS = 'drivelabels' @@ -92,7 +91,6 @@ SERVICEACCOUNTLOOKUP = 'serviceaccountlookup' SERVICEMANAGEMENT = 'servicemanagement' SERVICEUSAGE = 'serviceusage' SHEETS = 'sheets' -SHEETSCD = 'sheetscd' SHEETSTD = 'sheetstd' SITEVERIFICATION = 'siteVerification' STORAGE = 'storage' @@ -107,6 +105,8 @@ YOUTUBE = 'youtube' BUSINESSACCOUNTMANAGEMENT_SCOPE = 'https://www.googleapis.com/auth/business.manage' CHROMEVERSIONHISTORY_URL = 'https://versionhistory.googleapis.com/v1/chrome/platforms' DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive' +DRIVE_FILE_SCOPE = 'https://www.googleapis.com/auth/drive.file' +DRIVE_READONLY_SCOPE = 'https://www.googleapis.com/auth/drive.readonly' GMAIL_SEND_SCOPE = 'https://www.googleapis.com/auth/gmail.send' GOOGLE_AUTH_PROVIDER_X509_CERT_URL = 'https://www.googleapis.com/oauth2/v1/certs' GOOGLE_OAUTH2_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth' @@ -256,7 +256,6 @@ _INFO = { DOCS: {'name': 'Docs API', 'version': 'v1', 'v2discovery': True}, DRIVE2: {'name': 'Drive API v2', 'version': 'v2', 'v2discovery': False, 'mappedAPI': 'drive'}, DRIVE3: {'name': 'Drive API v3', 'version': 'v3', 'v2discovery': False, 'mappedAPI': 'drive'}, - DRIVECD: {'name': 'Drive API v3 - read command data', 'version': 'v3', 'v2discovery': False, 'mappedAPI': 'drive'}, DRIVETD: {'name': 'Drive API v3 - write todrive data', 'version': 'v3', 'v2discovery': False, 'mappedAPI': 'drive'}, DRIVEACTIVITY: {'name': 'Drive Activity API v2', 'version': 'v2', 'v2discovery': True}, DRIVELABELS_ADMIN: {'name': 'Drive Labels API - Admin', 'version': 'v2', 'v2discovery': True, 'mappedAPI': DRIVELABELS}, @@ -287,7 +286,6 @@ _INFO = { SERVICEMANAGEMENT: {'name': 'Service Management API', 'version': 'v1', 'v2discovery': True}, SERVICEUSAGE: {'name': 'Service Usage API', 'version': 'v1', 'v2discovery': True}, SHEETS: {'name': 'Sheets API', 'version': 'v4', 'v2discovery': True}, - SHEETSCD: {'name': 'Sheets API - read command data', 'version': 'v4', 'v2discovery': True, 'mappedAPI': SHEETS}, SHEETSTD: {'name': 'Sheets API - write todrive data', 'version': 'v4', 'v2discovery': True, 'mappedAPI': SHEETS}, SITEVERIFICATION: {'name': 'Site Verification API', 'version': 'v1', 'v2discovery': True}, STORAGE: {'name': 'Cloud Storage API', 'version': 'v1', 'v2discovery': True}, @@ -535,6 +533,17 @@ _CLIENT_SCOPES = [ 'scope': 'https://www.googleapis.com/auth/ediscovery'}, ] +_COMMANDDATA_CLIENT_SCOPES = [ + {'name': 'Drive API - commanddata_clientaccess', + 'api': DRIVE3, + 'subscopes': [], + 'scope': DRIVE_READONLY_SCOPE}, + {'name': 'Sheets API - commanddata_clientaccess', + 'api': SHEETS, + 'subscopes': [], + 'scope': 'https://www.googleapis.com/auth/spreadsheets.readonly'}, + ] + _TODRIVE_CLIENT_SCOPES = [ {'name': 'Drive API - todrive_clientaccess', 'api': DRIVE3, @@ -543,7 +552,7 @@ _TODRIVE_CLIENT_SCOPES = [ {'name': 'Drive File API - todrive_clientaccess', 'api': DRIVE3, 'subscopes': [], - 'scope': 'https://www.googleapis.com/auth/drive.file'}, + 'scope': DRIVE_FILE_SCOPE}, {'name': 'Gmail API - todrive_clientaccess', 'api': GMAIL, 'subscopes': [], @@ -648,7 +657,8 @@ _SVCACCT_SCOPES = [ {'name': 'Drive Activity API v2 - must pair with Drive API', 'api': DRIVEACTIVITY, 'subscopes': [], - 'scope': 'https://www.googleapis.com/auth/drive.activity'}, + 'scope': [DRIVE_READONLY_SCOPE, + 'https://www.googleapis.com/auth/drive.activity']}, {'name': 'Drive Labels API - Admin', 'api': DRIVELABELS_ADMIN, 'subscopes': READONLY, @@ -661,10 +671,12 @@ _SVCACCT_SCOPES = [ 'api': DOCS, 'subscopes': READONLY, 'scope': 'https://www.googleapis.com/auth/documents'}, - {'name': 'Forms API', + {'name': 'Forms API - must pair with Drive API', 'api': FORMS, 'subscopes': [], - 'scope': DRIVE_SCOPE}, + 'scope': [DRIVE_READONLY_SCOPE, + 'https://www.googleapis.com/auth/forms.body', + 'https://www.googleapis.com/auth/forms.responses.readonly']}, {'name': 'Gmail API - Full Access (Labels, Messages)', 'api': GMAIL, 'subscopes': [], @@ -796,14 +808,18 @@ def getVersion(api): def getClientScopesSet(api): return {scope['scope'] for scope in _CLIENT_SCOPES if scope['api'] == api} -def getClientScopesList(todriveClientAccess): +def getClientScopesList(commanddataClientAccess, todriveClientAccess): caScopes = _CLIENT_SCOPES[:] + if commanddataClientAccess: + caScopes.extend(_COMMANDDATA_CLIENT_SCOPES) if todriveClientAccess: caScopes.extend(_TODRIVE_CLIENT_SCOPES) return sorted(caScopes, key=lambda k: k['name']) -def getClientScopesURLs(todriveClientAccess): +def getClientScopesURLs(commanddataClientAccess, todriveClientAccess): caScopes = _CLIENT_SCOPES[:] + if commanddataClientAccess: + caScopes.extend(_COMMANDDATA_CLIENT_SCOPES) if todriveClientAccess: caScopes.extend(_TODRIVE_CLIENT_SCOPES) return sorted({scope['scope'] for scope in _CLIENT_SCOPES}) diff --git a/src/gam/gamlib/glcfg.py b/src/gam/gamlib/glcfg.py index 3c1023b3..f8b484dd 100644 --- a/src/gam/gamlib/glcfg.py +++ b/src/gam/gamlib/glcfg.py @@ -85,6 +85,8 @@ CMDLOG_MAX__BACKUPS = 'cmdlog_max__backups' CMDLOG_MAX_BACKUPS = 'cmdlog_max_backups' # Command logging max kilo bytes per log file CMDLOG_MAX_KILO_BYTES = 'cmdlog_max_kilo_bytes' +# Use client access for command data from Google Docs/Sheets +COMMANDDATA_CLIENTACCESS = 'commanddata_clientaccess' # GAM config directory containing client_secrets.json, oauth2.txt, oauth2service.json, extra_args.txt CONFIG_DIR = 'config_dir' # When retrieving lists of Google Contacts from API, how many should be retrieved in each chunk @@ -344,6 +346,7 @@ Defaults = { CMDLOG: '', CMDLOG_MAX_BACKUPS: 5, CMDLOG_MAX_KILO_BYTES: 1000, + COMMANDDATA_CLIENTACCESS: FALSE, CONFIG_DIR: '', CONTACT_MAX_RESULTS: '100', CSV_INPUT_COLUMN_DELIMITER: ',', @@ -512,6 +515,7 @@ VAR_INFO = { CMDLOG: {VAR_TYPE: TYPE_FILE, VAR_ACCESS: os.W_OK}, CMDLOG_MAX_BACKUPS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10)}, CMDLOG_MAX_KILO_BYTES: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (100, 10000)}, + COMMANDDATA_CLIENTACCESS: {VAR_TYPE: TYPE_BOOLEAN}, CONFIG_DIR: {VAR_TYPE: TYPE_DIRECTORY, VAR_ENVVAR: 'GAMUSERCONFIGDIR'}, CONTACT_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10000)}, CSV_INPUT_COLUMN_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},