diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 46ea4fff..d0e46acf 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -1,3 +1,9 @@ +7.10.08 + +Fixed bug in commands that modify messages where the `labelids ` option +could not be used unless one of these options was also specified: `query`, `matchlabel`, `ids`; +it can be now be used by itself. + 7.10.07 Updated `gam copy|move drivefile` to hanndle additional instances of diff --git a/src/gam/__init__.py b/src/gam/__init__.py index dd2c5f82..a449ba38 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.10.07' +__version__ = '7.10.08' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' #pylint: disable=wrong-import-position @@ -5187,6 +5187,12 @@ def checkGAPIError(e, softErrors=False, retryOnHttpError=False, mapNotFound=True return (0, None, None) else: systemErrorExit(HTTP_ERROR_RC, eContent) + requiredScopes = '' + wwwAuthenticate = e.resp.get('www-authenticate', '') + if 'insufficient_scope' in wwwAuthenticate: + mg = re.match(r'.+scope="(.+)"', wwwAuthenticate) + if mg: + requiredScopes = mg.group(1).split(' ') if 'error' in error: http_status = error['error']['code'] reason = '' @@ -5256,6 +5262,8 @@ def checkGAPIError(e, softErrors=False, retryOnHttpError=False, mapNotFound=True elif 'the authenticated user cannot access this service' in lmessage: error = makeErrorDict(http_status, GAPI.SERVICE_NOT_AVAILABLE, message) elif status == 'PERMISSION_DENIED' or 'the caller does not have permission' in lmessage or 'permission iam.serviceaccountkeys' in lmessage: + if requiredScopes: + message += f', {Msg.NO_SCOPES_FOR_API.format(API.findAPIforScope(requiredScopes))}' 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: @@ -70247,14 +70255,16 @@ def _finalizeMessageSelectParameters(parameters, queryOrIdsRequired): 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') + elif queryOrIdsRequired and parameters['messageEntity'] is None and not parameters['labelIds']: + missingArgumentExit('query|matchlabel|ids|labelids') else: parameters['query'] = None parameters['maxItems'] = parameters['maxToProcess'] if parameters['quick'] and not parameters['labelMatchPattern'] else 0 # gam archive messages -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_archive ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_archive ])|(ids ) # [csv [todrive *]] def archiveMessages(users): def _processMessageFailed(user, idsList, errMsg, j=0, jcount=0): @@ -70539,39 +70549,59 @@ def _processMessagesThreads(users, entityType): csvPF.writeCSVfile(f'{Act.ToPerform()} Messages') # gam delete message|messages -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_delete ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_delete ])|(ids ) # [csv [todrive *]] # gam modify message|messages -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_modify ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_modify ])|(ids ) # (addlabel )* (removelabel )* # [csv [todrive *]] # gam spam message|messages -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_spam ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_spam ])|(ids ) # [csv [todrive *]] # gam trash message|messages -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_trash ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_trash ])|(ids ) # [csv [todrive *]] # gam untrash message|messages -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_untrash ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_untrash ])|(ids ) # [csv [todrive *]] def processMessages(users): _processMessagesThreads(users, Ent.MESSAGE) # gam delete thread|threads -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_delete ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_delete ])|(ids ) # [csv [todrive *]] # gam modify thread|threads -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_modify ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_modify ])|(ids ) # (addlabel )* (removelabel )* # [csv [todrive *]] # gam spam thread|threads -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_spam ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_spam ])|(ids ) # [csv [todrive *]] # gam trash thread|threads -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_trash ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_trash ])|(ids ) # [csv [todrive *]] # gam untrash thread|threads -# (((query [querytime ]*) (matchlabel ) [or|and])+ [quick|notquick] [doit] [max_to_untrash ])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])+ +# [labelids ] +# [quick|notquick] [doit] [max_to_untrash ])|(ids ) # [csv [todrive *]] def processThreads(users): _processMessagesThreads(users, Ent.THREAD) @@ -71993,7 +72023,9 @@ def printShowMessagesThreads(users, entityType): csvPF.writeCSVfile('Message Counts' if not show_labels else 'Message Label Counts') # gam print message|messages [todrive *] -# (((query [querytime ]*) (matchlabel ) [or|and])* [quick|notquick] [max_to_print ] [includespamtrash])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])* +# [labelids ] +# [quick|notquick] [max_to_print ] [includespamtrash])|(ids ) # [labelmatchpattern ] [sendermatchpattern ] # [headers all|] [dateheaderformat iso|rfc2822|] [dateheaderconverttimezone []] # [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet] @@ -72003,7 +72035,9 @@ def printShowMessagesThreads(users, entityType): # [showattachments [noshowtextplain]]] # (addcsvdata )* # gam show message|messages -# (((query [querytime ]*) (matchlabel ) [or|and])* [quick|notquick] [max_to_show ] [includespamtrash])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])* +# [labelids ] +# [quick|notquick] [max_to_show ] [includespamtrash])|(ids ) # [labelmatchpattern ] [sendermatchpattern ] # [headers all|] [dateheaderformat iso|rfc2822|] [dateheaderconverttimezone []] # [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet] @@ -72016,7 +72050,9 @@ def printShowMessages(users): printShowMessagesThreads(users, Ent.MESSAGE) # gam print thread|threads [todrive *] -# (((query [querytime ]*) (matchlabel ) [or|and])* [quick|notquick] [max_to_print ] [includespamtrash])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])* +# [labelids ] +# [quick|notquick] [max_to_print ] [includespamtrash])|(ids ) # [labelmatchpattern ] # [headers all|] [dateheaderformat iso|rfc2822|] [dateheaderconverttimezone []] # [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet] @@ -72026,7 +72062,9 @@ def printShowMessages(users): # [showattachments [noshowtextplain]]] # (addcsvdata )* # gam show thread|threads -# (((query [querytime ]*) (matchlabel ) [or|and])* [quick|notquick] [max_to_show ] [includespamtrash])|(ids ) +# (((query [querytime ]*) (matchlabel ) [or|and])* +# [labelids ] +# [quick|notquick] [max_to_show ] [includespamtrash])|(ids ) # [labelmatchpattern ] # [headers all|] [dateheaderformat iso|rfc2822|] [dateheaderconverttimezone []] # [showlabels] [showbody] [showhtml] [showdate] [showsize] [showsnippet] diff --git a/src/gam/gamlib/glapi.py b/src/gam/gamlib/glapi.py index 56552eb8..4f2e2471 100644 --- a/src/gam/gamlib/glapi.py +++ b/src/gam/gamlib/glapi.py @@ -226,15 +226,15 @@ _INFO = { CHROMEMANAGEMENT_TELEMETRY: {'name': 'Chrome Management API - Telemetry', 'version': 'v1', 'v2discovery': True, 'mappedAPI': CHROMEMANAGEMENT}, CHROMEPOLICY: {'name': 'Chrome Policy API', 'version': 'v1', 'v2discovery': True}, CHROMEVERSIONHISTORY: {'name': 'Chrome Version History API', 'version': 'v1', 'v2discovery': True}, - CLOUDCHANNEL: {'name': 'Channel Channel API', 'version': 'v1', 'v2discovery': True}, - CLOUDIDENTITY_DEVICES: {'name': 'Cloud Identity Devices API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, - CLOUDIDENTITY_GROUPS: {'name': 'Cloud Identity Groups API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, - CLOUDIDENTITY_GROUPS_BETA: {'name': 'Cloud Identity Groups API', 'version': 'v1beta1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, - CLOUDIDENTITY_INBOUND_SSO: {'name': 'Cloud Identity Inbound SSO API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, - CLOUDIDENTITY_ORGUNITS: {'name': 'Cloud Identity OrgUnits API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, - CLOUDIDENTITY_ORGUNITS_BETA: {'name': 'Cloud Identity OrgUnits API', 'version': 'v1beta1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, - CLOUDIDENTITY_POLICY: {'name': 'Cloud Identity Policy API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, - CLOUDIDENTITY_USERINVITATIONS: {'name': 'Cloud Identity User Invitations API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, + CLOUDCHANNEL: {'name': 'Cloud Channel API', 'version': 'v1', 'v2discovery': True}, + CLOUDIDENTITY_DEVICES: {'name': 'Cloud Identity API - Devices', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, + CLOUDIDENTITY_GROUPS: {'name': 'Cloud Identity API - Groups', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, + CLOUDIDENTITY_GROUPS_BETA: {'name': 'Cloud Identity API - Groups Beta', 'version': 'v1beta1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, + CLOUDIDENTITY_INBOUND_SSO: {'name': 'Cloud Identity API - Inbound SSO Settings', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, + CLOUDIDENTITY_ORGUNITS: {'name': 'Cloud Identity API - OrgUnits', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, + CLOUDIDENTITY_ORGUNITS_BETA: {'name': 'Cloud Identity API - OrgUnits Beta', 'version': 'v1beta1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, + CLOUDIDENTITY_POLICY: {'name': 'Cloud Identity API - Policy', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, + CLOUDIDENTITY_USERINVITATIONS: {'name': 'Cloud Identity API - User Invitations', 'version': 'v1', 'v2discovery': True, 'mappedAPI': 'cloudidentity'}, CLOUDRESOURCEMANAGER: {'name': 'Cloud Resource Manager API v3', 'version': 'v3', 'v2discovery': True}, CONTACTS: {'name': 'Contacts API', 'version': 'v3', 'v2discovery': False}, CONTACTDELEGATION: {'name': 'Contact Delegation API', 'version': 'v1', 'v2discovery': True, 'localjson': True}, @@ -258,13 +258,13 @@ _INFO = { LICENSING: {'name': 'License Manager API', 'version': 'v1', 'v2discovery': True}, LOOKERSTUDIO: {'name': 'Looker Studio API', 'version': 'v1', 'v2discovery': True, 'localjson': True}, MEET: {'name': 'Meet API', 'version': 'v2', 'v2discovery': True}, - MEET_BETA: {'name': 'Meet API', 'version': 'v2beta', 'v2discovery': True, 'localjson': True, 'mappedAPI': MEET}, + MEET_BETA: {'name': 'Meet API Beta', 'version': 'v2beta', 'v2discovery': True, 'localjson': True, 'mappedAPI': MEET}, OAUTH2: {'name': 'OAuth2 API', 'version': 'v2', 'v2discovery': False}, ORGPOLICY: {'name': 'Organization Policy API', 'version': 'v2', 'v2discovery': True}, PEOPLE: {'name': 'People API', 'version': 'v1', 'v2discovery': True}, PEOPLE_DIRECTORY: {'name': 'People Directory API', 'version': 'v1', 'v2discovery': True, 'mappedAPI': PEOPLE}, PEOPLE_OTHERCONTACTS: {'name': 'People API - Other Contacts', 'version': 'v1', 'v2discovery': True, 'mappedAPI': PEOPLE}, - PRINTERS: {'name': 'Directory API Printers', 'version': 'directory_v1', 'v2discovery': True, 'mappedAPI': 'admin'}, + PRINTERS: {'name': 'Directory API - Printers', 'version': 'directory_v1', 'v2discovery': True, 'mappedAPI': 'admin'}, PUBSUB: {'name': 'Pub / Sub API', 'version': 'v1', 'v2discovery': True}, REPORTS: {'name': 'Reports API', 'version': 'reports_v1', 'v2discovery': True, 'mappedAPI': 'admin'}, RESELLER: {'name': 'Reseller API', 'version': 'v1', 'v2discovery': True}, @@ -362,29 +362,29 @@ _CLIENT_SCOPES = [ 'subscopes': READONLY, 'offByDefault': True, 'scope': 'https://www.googleapis.com/auth/apps.order'}, - {'name': 'Cloud Identity Groups API', + {'name': 'Cloud Identity API - Groups', 'api': CLOUDIDENTITY_GROUPS, 'subscopes': READONLY, 'scope': 'https://www.googleapis.com/auth/cloud-identity.groups'}, - {'name': 'Cloud Identity Groups API Beta (Enables group locking/unlocking)', + {'name': 'Cloud Identity API - Groups Beta (Enables group locking/unlocking)', 'api': CLOUDIDENTITY_GROUPS_BETA, 'subscopes': [], 'scope': 'https://www.googleapis.com/auth/cloud-identity.groups'}, - {'name': 'Cloud Identity - Inbound SSO Settings', + {'name': 'Cloud Identity API - Inbound SSO Settings', 'api': CLOUDIDENTITY_INBOUND_SSO, 'subscopes': READONLY, 'scope': 'https://www.googleapis.com/auth/cloud-identity.inboundsso'}, - {'name': 'Cloud Identity OrgUnits API', + {'name': 'Cloud Identity API - OrgUnits Beta', 'api': CLOUDIDENTITY_ORGUNITS_BETA, 'subscopes': READONLY, 'scope': 'https://www.googleapis.com/auth/cloud-identity.orgunits'}, - {'name': 'Cloud Identity - Policy', + {'name': 'Cloud Identity API - Policy', 'api': CLOUDIDENTITY_POLICY, 'subscopes': READONLY, 'roByDefault': True, 'scope': 'https://www.googleapis.com/auth/cloud-identity.policies' }, - {'name': 'Cloud Identity User Invitations API', + {'name': 'Cloud Identity API - User Invitations', 'api': CLOUDIDENTITY_USERINVITATIONS, 'subscopes': READONLY, 'scope': 'https://www.googleapis.com/auth/cloud-identity.userinvitations'}, @@ -833,3 +833,27 @@ def getSvcAcctScopesList(userServiceAccountAccessOnly, svcAcctSpecialScopes): def hasLocalJSON(api): return _INFO[api].get('localjson', False) + +def findAPIforScope(scopesList): + def checkScopeMatch(scope, cscope): + if cscope['scope'] == scope: + requiredAPIs.append(cscope['name']) + return True + if cscope['subscopes'] == READONLY and cscope['scope']+'.readonly' == scope: + requiredAPIs.append(cscope['name']+' (supports readonly)') + return True + return False + + requiredAPIs = [] + for scope in scopesList: + for cscope in _CLIENT_SCOPES: + if checkScopeMatch(scope, cscope): + break + else: + for cscope in _SVCACCT_SCOPES: + if checkScopeMatch(scope, cscope): + break + if not requiredAPIs: + requiredAPIs = scopesList + return ' or '.join(requiredAPIs) + diff --git a/src/gam/gamlib/glmsgs.py b/src/gam/gamlib/glmsgs.py index 498994e0..f58f6ad0 100644 --- a/src/gam/gamlib/glmsgs.py +++ b/src/gam/gamlib/glmsgs.py @@ -184,8 +184,8 @@ ALREADY_EXISTS_IN_TARGET_FOLDER = 'Already exists in {0}: {1}' ALREADY_EXISTS_USE_MERGE_ARGUMENT = 'Already exists; use the "merge" argument to merge the labels' API_ACCESS_DENIED = 'API access Denied' API_CALLS_RETRY_DATA = 'API calls retry data\n' -API_CHECK_CLIENT_AUTHORIZATION = 'Please make sure the Client ID: {0} is authorized for the appropriate API or scopes:\n{1}\n\nRun: gam oauth create\n' -API_CHECK_SVCACCT_AUTHORIZATION = 'Please make sure the Service Account Client ID: {0} is authorized for the appropriate API or scopes:\n{1}\n\nRun: gam user {2} update serviceaccount\n' +API_CHECK_CLIENT_AUTHORIZATION = 'Please make sure the Client ID: {0} is authorized for the appropriate API or scopes: {1}\n\nRun: gam oauth create\n' +API_CHECK_SVCACCT_AUTHORIZATION = 'Please make sure the Service Account Client ID: {0} is authorized for the appropriate API or scopes: {1}\n\nRun: gam user {2} update serviceaccount\n' API_ERROR_SETTINGS = 'API error, some settings not set' ARE_BOTH_REQUIRED = 'Arguments {0} and {1} are both required' ARE_MUTUALLY_EXCLUSIVE = 'Arguments {0} and {1} are mutually exclusive' @@ -425,7 +425,7 @@ NO_LABELS_TO_PROCESS = 'No Labels to process' NO_MESSAGES_WITH_LABEL = 'No Messages with Label' NO_PARENTS_TO_CONVERT_TO_SHORTCUTS = 'No parents to convert to shortcuts' NO_REPORT_AVAILABLE = 'No {0} report available.' -NO_SCOPES_FOR_API = 'There are no scopes authorized for the {0}' +NO_SCOPES_FOR_API = 'There are no scopes authorized for the API(s): {0}' NO_SERIAL_NUMBERS_SPECIFIED = 'No serial numbers specified' NO_SSO_PROFILE_MATCHES = 'No SSO profile matches display name {0}' NO_SSO_PROFILE_ASSIGNED = 'No SSO profile assigned to {0} {1}'