Update limited command data access

This commit is contained in:
Ross Scroggs
2025-10-08 18:10:05 -07:00
parent 0422bf22ea
commit e03086866a
4 changed files with 72 additions and 20 deletions

View File

@@ -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 7.24.01
Updated GAM to handle the following error that occurs when GAM tries to authenticate Updated GAM to handle the following error that occurs when GAM tries to authenticate

View File

@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
""" """
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>' __author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
__version__ = '7.24.01' __version__ = '7.25.00'
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
#pylint: disable=wrong-import-position #pylint: disable=wrong-import-position
@@ -2113,12 +2113,12 @@ class StartEndTime():
else: else:
firstMonth = getInteger(minVal=1, maxVal=6) firstMonth = getInteger(minVal=1, maxVal=6)
currDate = todaysDate() 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) self.startTime = ISOformatTimeStamp(self.startDateTime)
if myarg == 'thismonth': if myarg == 'thismonth':
self.endDateTime = todaysTime() self.endDateTime = todaysTime()
else: 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) self.endTime = ISOformatTimeStamp(self.endDateTime)
if self.startDateTime and self.endDateTime and self.endDateTime < self.startDateTime: if self.startDateTime and self.endDateTime and self.endDateTime < self.startDateTime:
Cmd.Backup() Cmd.Backup()
@@ -3049,7 +3049,10 @@ def getGDocData(gformat):
mimeType = GDOC_FORMAT_MIME_TYPES[gformat] mimeType = GDOC_FORMAT_MIME_TYPES[gformat]
user = getEmailAddress() user = getEmailAddress()
fileIdEntity = getDriveFileEntity(queryShortcutsOK=False) 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: if not drive:
sys.exit(GM.Globals[GM.SYSEXITRC]) sys.exit(GM.Globals[GM.SYSEXITRC])
_, _, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity, drive=drive) _, _, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity, drive=drive)
@@ -3106,7 +3109,10 @@ def getGSheetData():
user = getEmailAddress() user = getEmailAddress()
fileIdEntity = getDriveFileEntity(queryShortcutsOK=False) fileIdEntity = getDriveFileEntity(queryShortcutsOK=False)
sheetEntity = getSheetEntity(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: if not drive:
sys.exit(GM.Globals[GM.SYSEXITRC]) sys.exit(GM.Globals[GM.SYSEXITRC])
_, _, jcount = _validateUserGetFileIDs(user, 0, 0, fileIdEntity, drive=drive) _, _, 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))) getGDocSheetDataFailedExit([Ent.USER, user], Msg.NO_ENTITIES_FOUND.format(Ent.Singular(Ent.DRIVE_FILE)))
if jcount > 1: if jcount > 1:
getGDocSheetDataFailedExit([Ent.USER, user], Msg.MULTIPLE_ENTITIES_FOUND.format(Ent.Plural(Ent.DRIVE_FILE), jcount, ','.join(fileIdEntity['list']))) 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: if not sheet:
sys.exit(GM.Globals[GM.SYSEXITRC]) sys.exit(GM.Globals[GM.SYSEXITRC])
fileId = fileIdEntity['list'][0] fileId = fileIdEntity['list'][0]
@@ -11137,7 +11146,7 @@ class Credentials(google.oauth2.credentials.Credentials):
def doOAuthRequest(currentScopes, login_hint, verifyScopes=False): def doOAuthRequest(currentScopes, login_hint, verifyScopes=False):
client_id, client_secret = getOAuthClientIDAndSecret() 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: if not currentScopes or verifyScopes:
selectedScopes = getScopesFromUser(scopesList, True, currentScopes) selectedScopes = getScopesFromUser(scopesList, True, currentScopes)
if selectedScopes is None: if selectedScopes is None:
@@ -11183,7 +11192,7 @@ def doOAuthCreate():
else: else:
login_hint = None login_hint = None
scopes = [] 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(): while Cmd.ArgumentsRemaining():
myarg = getArgument() myarg = getArgument()
if myarg == 'admin': if myarg == 'admin':
@@ -11199,7 +11208,9 @@ def doOAuthCreate():
scopes.append(uscope) scopes.append(uscope)
break break
else: 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: else:
unknownArgumentExit() unknownArgumentExit()
if len(scopes) == 0: if len(scopes) == 0:
@@ -11310,7 +11321,7 @@ def doOAuthUpdate():
if 'scopes' in jsonDict: if 'scopes' in jsonDict:
currentScopes = jsonDict['scopes'] currentScopes = jsonDict['scopes']
else: else:
currentScopes = API.getClientScopesURLs(GC.Values[GC.TODRIVE_CLIENTACCESS]) currentScopes = API.getClientScopesURLs(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS])
else: else:
currentScopes = [] currentScopes = []
except (AttributeError, IndexError, KeyError, SyntaxError, TypeError, ValueError) as e: except (AttributeError, IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:

View File

@@ -60,7 +60,6 @@ DIRECTORY = 'directory'
DOCS = 'docs' DOCS = 'docs'
DRIVE2 = 'drive2' DRIVE2 = 'drive2'
DRIVE3 = 'drive3' DRIVE3 = 'drive3'
DRIVECD = 'drivecd'
DRIVETD = 'drivetd' DRIVETD = 'drivetd'
DRIVEACTIVITY = 'driveactivity' DRIVEACTIVITY = 'driveactivity'
DRIVELABELS = 'drivelabels' DRIVELABELS = 'drivelabels'
@@ -92,7 +91,6 @@ SERVICEACCOUNTLOOKUP = 'serviceaccountlookup'
SERVICEMANAGEMENT = 'servicemanagement' SERVICEMANAGEMENT = 'servicemanagement'
SERVICEUSAGE = 'serviceusage' SERVICEUSAGE = 'serviceusage'
SHEETS = 'sheets' SHEETS = 'sheets'
SHEETSCD = 'sheetscd'
SHEETSTD = 'sheetstd' SHEETSTD = 'sheetstd'
SITEVERIFICATION = 'siteVerification' SITEVERIFICATION = 'siteVerification'
STORAGE = 'storage' STORAGE = 'storage'
@@ -107,6 +105,8 @@ YOUTUBE = 'youtube'
BUSINESSACCOUNTMANAGEMENT_SCOPE = 'https://www.googleapis.com/auth/business.manage' BUSINESSACCOUNTMANAGEMENT_SCOPE = 'https://www.googleapis.com/auth/business.manage'
CHROMEVERSIONHISTORY_URL = 'https://versionhistory.googleapis.com/v1/chrome/platforms' CHROMEVERSIONHISTORY_URL = 'https://versionhistory.googleapis.com/v1/chrome/platforms'
DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive' 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' GMAIL_SEND_SCOPE = 'https://www.googleapis.com/auth/gmail.send'
GOOGLE_AUTH_PROVIDER_X509_CERT_URL = 'https://www.googleapis.com/oauth2/v1/certs' GOOGLE_AUTH_PROVIDER_X509_CERT_URL = 'https://www.googleapis.com/oauth2/v1/certs'
GOOGLE_OAUTH2_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth' GOOGLE_OAUTH2_ENDPOINT = 'https://accounts.google.com/o/oauth2/v2/auth'
@@ -256,7 +256,6 @@ _INFO = {
DOCS: {'name': 'Docs API', 'version': 'v1', 'v2discovery': True}, DOCS: {'name': 'Docs API', 'version': 'v1', 'v2discovery': True},
DRIVE2: {'name': 'Drive API v2', 'version': 'v2', 'v2discovery': False, 'mappedAPI': 'drive'}, DRIVE2: {'name': 'Drive API v2', 'version': 'v2', 'v2discovery': False, 'mappedAPI': 'drive'},
DRIVE3: {'name': 'Drive API v3', 'version': 'v3', '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'}, DRIVETD: {'name': 'Drive API v3 - write todrive data', 'version': 'v3', 'v2discovery': False, 'mappedAPI': 'drive'},
DRIVEACTIVITY: {'name': 'Drive Activity API v2', 'version': 'v2', 'v2discovery': True}, DRIVEACTIVITY: {'name': 'Drive Activity API v2', 'version': 'v2', 'v2discovery': True},
DRIVELABELS_ADMIN: {'name': 'Drive Labels API - Admin', 'version': 'v2', 'v2discovery': True, 'mappedAPI': DRIVELABELS}, 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}, SERVICEMANAGEMENT: {'name': 'Service Management API', 'version': 'v1', 'v2discovery': True},
SERVICEUSAGE: {'name': 'Service Usage API', 'version': 'v1', 'v2discovery': True}, SERVICEUSAGE: {'name': 'Service Usage API', 'version': 'v1', 'v2discovery': True},
SHEETS: {'name': 'Sheets API', 'version': 'v4', '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}, SHEETSTD: {'name': 'Sheets API - write todrive data', 'version': 'v4', 'v2discovery': True, 'mappedAPI': SHEETS},
SITEVERIFICATION: {'name': 'Site Verification API', 'version': 'v1', 'v2discovery': True}, SITEVERIFICATION: {'name': 'Site Verification API', 'version': 'v1', 'v2discovery': True},
STORAGE: {'name': 'Cloud Storage 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'}, '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 = [ _TODRIVE_CLIENT_SCOPES = [
{'name': 'Drive API - todrive_clientaccess', {'name': 'Drive API - todrive_clientaccess',
'api': DRIVE3, 'api': DRIVE3,
@@ -543,7 +552,7 @@ _TODRIVE_CLIENT_SCOPES = [
{'name': 'Drive File API - todrive_clientaccess', {'name': 'Drive File API - todrive_clientaccess',
'api': DRIVE3, 'api': DRIVE3,
'subscopes': [], 'subscopes': [],
'scope': 'https://www.googleapis.com/auth/drive.file'}, 'scope': DRIVE_FILE_SCOPE},
{'name': 'Gmail API - todrive_clientaccess', {'name': 'Gmail API - todrive_clientaccess',
'api': GMAIL, 'api': GMAIL,
'subscopes': [], 'subscopes': [],
@@ -648,7 +657,8 @@ _SVCACCT_SCOPES = [
{'name': 'Drive Activity API v2 - must pair with Drive API', {'name': 'Drive Activity API v2 - must pair with Drive API',
'api': DRIVEACTIVITY, 'api': DRIVEACTIVITY,
'subscopes': [], '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', {'name': 'Drive Labels API - Admin',
'api': DRIVELABELS_ADMIN, 'api': DRIVELABELS_ADMIN,
'subscopes': READONLY, 'subscopes': READONLY,
@@ -661,10 +671,12 @@ _SVCACCT_SCOPES = [
'api': DOCS, 'api': DOCS,
'subscopes': READONLY, 'subscopes': READONLY,
'scope': 'https://www.googleapis.com/auth/documents'}, 'scope': 'https://www.googleapis.com/auth/documents'},
{'name': 'Forms API', {'name': 'Forms API - must pair with Drive API',
'api': FORMS, 'api': FORMS,
'subscopes': [], '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)', {'name': 'Gmail API - Full Access (Labels, Messages)',
'api': GMAIL, 'api': GMAIL,
'subscopes': [], 'subscopes': [],
@@ -796,14 +808,18 @@ def getVersion(api):
def getClientScopesSet(api): def getClientScopesSet(api):
return {scope['scope'] for scope in _CLIENT_SCOPES if scope['api'] == api} return {scope['scope'] for scope in _CLIENT_SCOPES if scope['api'] == api}
def getClientScopesList(todriveClientAccess): def getClientScopesList(commanddataClientAccess, todriveClientAccess):
caScopes = _CLIENT_SCOPES[:] caScopes = _CLIENT_SCOPES[:]
if commanddataClientAccess:
caScopes.extend(_COMMANDDATA_CLIENT_SCOPES)
if todriveClientAccess: if todriveClientAccess:
caScopes.extend(_TODRIVE_CLIENT_SCOPES) caScopes.extend(_TODRIVE_CLIENT_SCOPES)
return sorted(caScopes, key=lambda k: k['name']) return sorted(caScopes, key=lambda k: k['name'])
def getClientScopesURLs(todriveClientAccess): def getClientScopesURLs(commanddataClientAccess, todriveClientAccess):
caScopes = _CLIENT_SCOPES[:] caScopes = _CLIENT_SCOPES[:]
if commanddataClientAccess:
caScopes.extend(_COMMANDDATA_CLIENT_SCOPES)
if todriveClientAccess: if todriveClientAccess:
caScopes.extend(_TODRIVE_CLIENT_SCOPES) caScopes.extend(_TODRIVE_CLIENT_SCOPES)
return sorted({scope['scope'] for scope in _CLIENT_SCOPES}) return sorted({scope['scope'] for scope in _CLIENT_SCOPES})

View File

@@ -85,6 +85,8 @@ CMDLOG_MAX__BACKUPS = 'cmdlog_max__backups'
CMDLOG_MAX_BACKUPS = 'cmdlog_max_backups' CMDLOG_MAX_BACKUPS = 'cmdlog_max_backups'
# Command logging max kilo bytes per log file # Command logging max kilo bytes per log file
CMDLOG_MAX_KILO_BYTES = 'cmdlog_max_kilo_bytes' 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 # GAM config directory containing client_secrets.json, oauth2.txt, oauth2service.json, extra_args.txt
CONFIG_DIR = 'config_dir' CONFIG_DIR = 'config_dir'
# When retrieving lists of Google Contacts from API, how many should be retrieved in each chunk # When retrieving lists of Google Contacts from API, how many should be retrieved in each chunk
@@ -344,6 +346,7 @@ Defaults = {
CMDLOG: '', CMDLOG: '',
CMDLOG_MAX_BACKUPS: 5, CMDLOG_MAX_BACKUPS: 5,
CMDLOG_MAX_KILO_BYTES: 1000, CMDLOG_MAX_KILO_BYTES: 1000,
COMMANDDATA_CLIENTACCESS: FALSE,
CONFIG_DIR: '', CONFIG_DIR: '',
CONTACT_MAX_RESULTS: '100', CONTACT_MAX_RESULTS: '100',
CSV_INPUT_COLUMN_DELIMITER: ',', CSV_INPUT_COLUMN_DELIMITER: ',',
@@ -512,6 +515,7 @@ VAR_INFO = {
CMDLOG: {VAR_TYPE: TYPE_FILE, VAR_ACCESS: os.W_OK}, CMDLOG: {VAR_TYPE: TYPE_FILE, VAR_ACCESS: os.W_OK},
CMDLOG_MAX_BACKUPS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10)}, CMDLOG_MAX_BACKUPS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10)},
CMDLOG_MAX_KILO_BYTES: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (100, 10000)}, 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'}, CONFIG_DIR: {VAR_TYPE: TYPE_DIRECTORY, VAR_ENVVAR: 'GAMUSERCONFIGDIR'},
CONTACT_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10000)}, CONTACT_MAX_RESULTS: {VAR_TYPE: TYPE_INTEGER, VAR_LIMITS: (1, 10000)},
CSV_INPUT_COLUMN_DELIMITER: {VAR_TYPE: TYPE_CHARACTER}, CSV_INPUT_COLUMN_DELIMITER: {VAR_TYPE: TYPE_CHARACTER},