From 1a96622366421fef9529b238050b496232b34d38 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Sat, 20 May 2017 12:34:25 -0700 Subject: [PATCH 1/4] Update calendar to allow access to user's secondary calendars (#499) * Update calendar to allow access to user's secondary calendars * Code cleanup * Code cleanup * Code cleanup --- src/GamCommands.txt | 2 +- src/gam.py | 93 +++++++++++++++++++++------------------------ 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 3453eadf..6c014cb4 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -772,7 +772,7 @@ gam delete|del backupcodes|backupcode|verificationcodes gam show backupcodes|backupcode|verificationcodes gam add calendar * -gam update calendar + +gam update calendar |primary + gam delete|del calendar gam show calendars gam info calendar |primary diff --git a/src/gam.py b/src/gam.py index 8e0e6324..3f9f2ec4 100755 --- a/src/gam.py +++ b/src/gam.py @@ -818,12 +818,27 @@ def buildActivityGAPIObject(user): userEmail = convertUserUIDtoEmailAddress(user) return (userEmail, buildGAPIServiceObject(u'appsactivity', userEmail)) -def buildCalendarGAPIObject(calname): +def normalizeCalendarId(calname, checkPrimary=False): + calname = calname.lower() + if checkPrimary and calname == u'primary': + return calname if not GC_Values[GC_DOMAIN]: - GC_Values[GC_DOMAIN] = _getValueFromOAuth(u'hd').lower() - calendarId = convertUserUIDtoEmailAddress(calname) + GC_Values[GC_DOMAIN] = _getValueFromOAuth(u'hd') + return convertUserUIDtoEmailAddress(calname) + +def buildCalendarGAPIObject(calname): + calendarId = normalizeCalendarId(calname) return (calendarId, buildGAPIServiceObject(u'calendar', calendarId)) +def buildCalendarDataGAPIObject(calname): + calendarId, cal = buildCalendarGAPIObject(calname) + try: + # Force service account token request. If we fail fall back to using admin for authentication + cal._http.request.credentials.refresh(httplib2.Http(disable_ssl_certificate_validation=GC_Values[GC_NO_VERIFY_SSL])) + except oauth2client.client.HttpAccessTokenRefreshError: + _, cal = buildCalendarGAPIObject(_getValueFromOAuth(u'email')) + return (calendarId, cal) + def buildDriveGAPIObject(user): userEmail = convertUserUIDtoEmailAddress(user) return (userEmail, buildGAPIServiceObject(u'drive', userEmail)) @@ -1379,14 +1394,14 @@ def doGetCustomerInfo(): print u'Phone: %s' % customer_info[u'phoneNumber'] print u'Admin Secondary Email: %s' % customer_info[u'alternateEmail'] user_counts_map = { - u'accounts:num_users': u'Total Users', - u'accounts:gsuite_basic_total_licenses': u'G Suite Basic Licenses', - u'accounts:gsuite_basic_used_licenses': u'G Suite Basic Users', - u'accounts:gsuite_enterprise_total_licenses': u'G Suite Enterprise Licenses', - u'accounts:gsuite_enterprise_used_licenses': u'G Suite Enterprise Users', - u'accounts:gsuite_unlimited_total_licenses': u'G Suite Business Licenses', - u'accounts:gsuite_unlimited_used_licenses': u'G Suite Business Users' - } + u'accounts:num_users': u'Total Users', + u'accounts:gsuite_basic_total_licenses': u'G Suite Basic Licenses', + u'accounts:gsuite_basic_used_licenses': u'G Suite Basic Users', + u'accounts:gsuite_enterprise_total_licenses': u'G Suite Enterprise Licenses', + u'accounts:gsuite_enterprise_used_licenses': u'G Suite Enterprise Users', + u'accounts:gsuite_unlimited_total_licenses': u'G Suite Business Licenses', + u'accounts:gsuite_unlimited_used_licenses': u'G Suite Business Users' + } parameters = u','.join(user_counts_map.keys()) try_date = str(datetime.date.today()) customerId = GC_Values[GC_CUSTOMER_ID] @@ -1407,14 +1422,10 @@ def doGetCustomerInfo(): try_date = _adjustDate(message) print u'User counts as of %s:' % try_date for item in usage[0][u'parameters']: - if not u'intValue' in item or int(item[u'intValue']) == 0: - continue - api_value = int(item[u'intValue']) - try: - api_name = user_counts_map[item[u'name']] - except KeyError: - continue - print u' {}: {:,}'.format(api_name, api_value) + api_name = user_counts_map.get(item[u'name']) + api_value = int(item.get(u'intValue', 0)) + if api_name and api_value: + print u' {}: {:,}'.format(api_name, api_value) def doUpdateCustomer(): cd = buildGAPIObject(u'directory') @@ -2506,9 +2517,7 @@ def changeCalendarAttendees(users): break def deleteCalendar(users): - calendarId = sys.argv[5] - if calendarId.find(u'@') == -1: - calendarId = u'%s@%s' % (calendarId, GC_Values[GC_DOMAIN]) + calendarId = normalizeCalendarId(sys.argv[5]) for user in users: user, cal = buildCalendarGAPIObject(user) if not cal: @@ -2588,9 +2597,7 @@ def getCalendarAttributes(i, body, function): return colorRgbFormat def addCalendar(users): - calendarId = sys.argv[5] - if calendarId.find(u'@') == -1: - calendarId = u'%s@%s' % (calendarId, GC_Values[GC_DOMAIN]) + calendarId = normalizeCalendarId(sys.argv[5]) body = {u'id': calendarId, u'selected': True, u'hidden': False} colorRgbFormat = getCalendarAttributes(6, body, u'add') i = 0 @@ -2604,9 +2611,7 @@ def addCalendar(users): callGAPI(cal.calendarList(), u'insert', soft_errors=True, body=body, colorRgbFormat=colorRgbFormat) def updateCalendar(users): - calendarId = sys.argv[5] - if calendarId.find(u'@') == -1: - calendarId = u'%s@%s' % (calendarId, GC_Values[GC_DOMAIN]) + calendarId = normalizeCalendarId(sys.argv[5], checkPrimary=True) body = {} colorRgbFormat = getCalendarAttributes(6, body, u'update') i = 0 @@ -3018,13 +3023,9 @@ def formatACLRule(rule): return u'(Scope: {0}, Role: {1})'.format(rule[u'scope'][u'type'], rule[u'role']) def doCalendarShowACL(): - calendarId, cal = buildCalendarGAPIObject(sys.argv[2]) - try: - # Force service account token request. If we fail fall back to - # using admin for delegation - cal._http.request.credentials.refresh(httplib2.Http(disable_ssl_certificate_validation=GC_Values[GC_NO_VERIFY_SSL])) - except oauth2client.client.HttpAccessTokenRefreshError: - _, cal = buildCalendarGAPIObject(_getValueFromOAuth(u'email')) + calendarId, cal = buildCalendarDataGAPIObject(sys.argv[2]) + if not cal: + return acls = callGAPIitems(cal.acl(), u'list', u'items', calendarId=calendarId) i = 0 count = len(acls) @@ -3033,18 +3034,14 @@ def doCalendarShowACL(): print u'Calendar: {0}, ACL: {1}{2}'.format(calendarId, formatACLRule(rule), currentCount(i, count)) def doCalendarAddACL(calendarId=None, act_as=None, role=None, scope=None, entity=None): - if not GC_Values[GC_DOMAIN]: - GC_Values[GC_DOMAIN] = _getValueFromOAuth(u'hd').lower() if calendarId is None: calendarId = sys.argv[2] - if calendarId.find(u'@') == -1: - calendarId = u'%s@%s' % (calendarId, GC_Values[GC_DOMAIN]) if not act_as: + calendarId = normalizeCalendarId(calendarId) act_as = calendarId _, cal = buildCalendarGAPIObject(act_as) try: - # Force service account token request. If we fail fall back to - # using admin for delegation + # Force service account token request. If we fail fall back to using admin for authentication cal._http.request.credentials.refresh(httplib2.Http(disable_ssl_certificate_validation=GC_Values[GC_NO_VERIFY_SSL])) except oauth2client.client.HttpAccessTokenRefreshError: _, cal = buildCalendarGAPIObject(_getValueFromOAuth(u'email')) @@ -3053,8 +3050,8 @@ def doCalendarAddACL(calendarId=None, act_as=None, role=None, scope=None, entity body[u'role'] = role else: body[u'role'] = sys.argv[4].lower() - if body[u'role'] not in [u'freebusy', u'read', u'reader', u'editor', u'owner', u'none']: - print u'ERROR: Role must be one of freebusy, read, editor, owner, none; got %s' % body[u'role'] + if body[u'role'] not in [u'freebusy', u'read', u'reader', u'editor', u'writer', u'owner', u'none']: + print u'ERROR: Role must be one of freebusy, reader, editor, writer, owner, none; got %s' % body[u'role'] sys.exit(2) if body[u'role'] == u'freebusy': body[u'role'] = u'freeBusyReader' @@ -3108,13 +3105,13 @@ def doCalendarDelACL(): doCalendarAddACL(calendarId=calendarId, role=u'none', scope=scope, entity=entity) def doCalendarWipeData(): - calendarId, cal = buildCalendarGAPIObject(sys.argv[2]) + calendarId, cal = buildCalendarDataGAPIObject(sys.argv[2]) if not cal: return callGAPI(cal.calendars(), u'clear', calendarId=calendarId) def doCalendarDeleteEvent(): - calendarId, cal = buildCalendarGAPIObject(sys.argv[2]) + calendarId, cal = buildCalendarDataGAPIObject(sys.argv[2]) if not cal: return events = [] @@ -3150,7 +3147,7 @@ def doCalendarDeleteEvent(): print u' would delete eventId %s. Add doit to command to actually delete event' % eventId def doCalendarAddEvent(): - calendarId, cal = buildCalendarGAPIObject(sys.argv[2]) + calendarId, cal = buildCalendarDataGAPIObject(sys.argv[2]) if not cal: return sendNotifications = timeZone = None @@ -3412,9 +3409,7 @@ def _showCalendar(userCalendar, j, jcount): print u' Method: {0}, Type: {1}'.format(notification[u'method'], notification[u'type']) def infoCalendar(users): - calendarId = sys.argv[5].lower() - if calendarId != u'primary' and calendarId.find(u'@') == -1: - calendarId = u'%s@%s' % (calendarId, GC_Values[GC_DOMAIN]) + calendarId = normalizeCalendarId(sys.argv[5], checkPrimary=True) i = 0 count = len(users) for user in users: From efdaa6a64e87c1123023b8a1040c16f49a0d6369 Mon Sep 17 00:00:00 2001 From: Jay Lee Date: Sat, 20 May 2017 16:14:59 -0400 Subject: [PATCH 2/4] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1215d9ae..92549bd7 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ The GAM documentation is hosted in the [GitHub Wiki] # Mailing List / Discussion group The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself. # Author -GAM is maintained by Jay Lee. Please direct "how do I?" questions to the mailing list. +GAM is maintained by Jay Lee. Please direct "how do I?" questions to [Google Groups]. [GAM release]: https://git.io/gamreleases [GitHub Releases]: https://github.com/jay0lee/GAM/releases From 20e84b9c9a8430acc3113af727e2bb829e17cbae Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Sat, 20 May 2017 14:33:57 -0700 Subject: [PATCH 3/4] On create group, use Group Settings to set description with new lines (#500) --- src/gam.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/gam.py b/src/gam.py index 3f9f2ec4..5c9a97c5 100755 --- a/src/gam.py +++ b/src/gam.py @@ -6957,7 +6957,14 @@ def doCreateGroup(): got_name = True i += 2 elif sys.argv[i].lower() == u'description': - body[u'description'] = sys.argv[i+1].replace(u'\\n', u'\n') + description = sys.argv[i+1].replace(u'\\n', u'\n') + if description.find(u'\n') != -1: + gs_body[u'description'] = description + if not gs: + gs = buildGAPIObject(u'groupssettings') + gs_object = gs._rootDesc + else: + body[u'description'] = description i += 2 else: value = sys.argv[i+1] From 4998c30d20ea12d1e9b9a666d64309ae75124787 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Mon, 22 May 2017 16:43:58 -0700 Subject: [PATCH 4/4] Update create/update group, use update semantics (#501) --- src/gam.py | 124 ++++++++++++++++++++++------------------------------- 1 file changed, 52 insertions(+), 72 deletions(-) diff --git a/src/gam.py b/src/gam.py index 5c9a97c5..9c842afd 100755 --- a/src/gam.py +++ b/src/gam.py @@ -6942,6 +6942,38 @@ def doCreateUser(): def GroupIsAbuseOrPostmaster(emailAddr): return emailAddr.startswith(u'abuse@') or emailAddr.startswith(u'postmaster@') +def getGroupAttrValue(myarg, value, gs_object, gs_body, function): + for (attrib, params) in gs_object[u'schemas'][u'Groups'][u'properties'].items(): + if attrib in [u'kind', u'etag', u'email']: + continue + if myarg == attrib.lower(): + if params[u'type'] == u'integer': + try: + if value[-1:].upper() == u'M': + value = int(value[:-1]) * 1024 * 1024 + elif value[-1:].upper() == u'K': + value = int(value[:-1]) * 1024 + elif value[-1].upper() == u'B': + value = int(value[:-1]) + else: + value = int(value) + except ValueError: + print u'ERROR: %s must be a number ending with M (megabytes), K (kilobytes) or nothing (bytes); got %s' % value + sys.exit(2) + elif params[u'type'] == u'string': + if attrib == u'description': + value = value.replace(u'\\n', u'\n') + elif params[u'description'].find(value.upper()) != -1: # ugly hack because API wants some values uppercased. + value = value.upper() + elif value.lower() in true_values: + value = u'true' + elif value.lower() in false_values: + value = u'false' + gs_body[attrib] = value + return + print u'ERROR: %s is not a valid argument for "gam %s group"' % (myarg, function) + sys.exit(2) + def doCreateGroup(): cd = buildGAPIObject(u'directory') body = {u'email': sys.argv[3]} @@ -6952,11 +6984,12 @@ def doCreateGroup(): gs_body = {} gs = None while i < len(sys.argv): - if sys.argv[i].lower() == u'name': + myarg = sys.argv[i].lower().replace(u'_', u'') + if myarg == u'name': body[u'name'] = sys.argv[i+1] got_name = True i += 2 - elif sys.argv[i].lower() == u'description': + elif myarg == u'description': description = sys.argv[i+1].replace(u'\\n', u'\n') if description.find(u'\n') != -1: gs_body[u'description'] = description @@ -6967,48 +7000,22 @@ def doCreateGroup(): body[u'description'] = description i += 2 else: - value = sys.argv[i+1] if not gs: gs = buildGAPIObject(u'groupssettings') gs_object = gs._rootDesc - matches_gs_setting = False - for (attrib, params) in gs_object[u'schemas'][u'Groups'][u'properties'].items(): - if attrib in [u'kind', u'etag', u'email', u'name', u'description']: - continue - if sys.argv[i].lower().replace(u'_', u'') == attrib.lower(): - matches_gs_setting = True - if params[u'type'] == u'integer': - try: - if value[-1:].upper() == u'M': - value = int(value[:-1]) * 1024 * 1024 - elif value[-1:].upper() == u'K': - value = int(value[:-1]) * 1024 - elif value[-1].upper() == u'B': - value = int(value[:-1]) - else: - value = int(value) - except ValueError: - print u'ERROR: %s must be a number ending with M (megabytes), K (kilobytes) or nothing (bytes); got %s' % value - sys.exit(2) - elif params[u'type'] == u'string': - if params[u'description'].find(value.upper()) != -1: # ugly hack because API wants some values uppercased. - value = value.upper() - elif value.lower() in true_values: - value = u'true' - elif value.lower() in false_values: - value = u'false' - break - if not matches_gs_setting: - print u'ERROR: %s is not a valid argument for "gam create group"' % sys.argv[i] - sys.exit(2) - gs_body[attrib] = value + getGroupAttrValue(myarg, sys.argv[i+1], gs_object, gs_body, u'create') i += 2 if not got_name: body[u'name'] = body[u'email'] print u"Creating group %s" % body[u'email'] callGAPI(cd.groups(), u'insert', body=body, fields=u'email') if gs and not GroupIsAbuseOrPostmaster(body[u'email']): - callGAPI(gs.groups(), u'patch', retry_reasons=[u'serviceLimit'], groupUniqueId=body[u'email'], body=gs_body) + settings = callGAPI(gs.groups(), u'get', + retry_reasons=[u'serviceLimit'], + groupUniqueId=body[u'email'], fields=u'*') + if settings is not None: + settings.update(gs_body) + callGAPI(gs.groups(), u'update', retry_reasons=[u'serviceLimit'], groupUniqueId=body[u'email'], body=settings) def doCreateAlias(): cd = buildGAPIObject(u'directory') @@ -7262,11 +7269,12 @@ def doUpdateGroup(): gs_body = {} cd_body = {} while i < len(sys.argv): - if sys.argv[i].lower() == u'email': + myarg = sys.argv[i].lower().replace(u'_', u'') + if myarg == u'email': use_cd_api = True cd_body[u'email'] = sys.argv[i+1] i += 2 - elif sys.argv[i].lower() == u'admincreated': + elif myarg == u'admincreated': use_cd_api = True cd_body[u'adminCreated'] = sys.argv[i+1].lower() if cd_body[u'adminCreated'] not in [u'true', u'false']: @@ -7274,43 +7282,10 @@ def doUpdateGroup(): sys.exit(2) i += 2 else: - value = sys.argv[i+1] if not gs: gs = buildGAPIObject(u'groupssettings') gs_object = gs._rootDesc - matches_gs_setting = False - for (attrib, params) in gs_object[u'schemas'][u'Groups'][u'properties'].items(): - if attrib in [u'kind', u'etag', u'email']: - continue - if sys.argv[i].lower().replace(u'_', u'') == attrib.lower(): - matches_gs_setting = True - if params[u'type'] == u'integer': - try: - if value[-1:].upper() == u'M': - value = int(value[:-1]) * 1024 * 1024 - elif value[-1:].upper() == u'K': - value = int(value[:-1]) * 1024 - elif value[-1].upper() == u'B': - value = int(value[:-1]) - else: - value = int(value) - except ValueError: - print u'ERROR: %s must be a number ending with M (megabytes), K (kilobytes) or nothing (bytes); got %s' % value - sys.exit(2) - elif params[u'type'] == u'string': - if attrib == u'description': - value = value.replace(u'\\n', u'\n') - elif params[u'description'].find(value.upper()) != -1: # ugly hack because API wants some values uppercased. - value = value.upper() - elif value.lower() in true_values: - value = u'true' - elif value.lower() in false_values: - value = u'false' - break - if not matches_gs_setting: - print u'ERROR: %s is not a valid argument for "gam update group"' % sys.argv[i] - sys.exit(2) - gs_body[attrib] = value + getGroupAttrValue(myarg, sys.argv[i+1], gs_object, gs_body, u'update') i += 2 if group[:4].lower() == u'uid:': # group settings API won't take uid so we make sure cd API is used so that we can grab real email. use_cd_api = True @@ -7328,7 +7303,12 @@ def doUpdateGroup(): if use_cd_api: group = cd_result[u'email'] if not GroupIsAbuseOrPostmaster(group): - callGAPI(gs.groups(), u'patch', retry_reasons=[u'serviceLimit'], groupUniqueId=group, body=gs_body) + settings = callGAPI(gs.groups(), u'get', + retry_reasons=[u'serviceLimit'], + groupUniqueId=group, fields=u'*') + if settings is not None: + settings.update(gs_body) + callGAPI(gs.groups(), u'update', retry_reasons=[u'serviceLimit'], groupUniqueId=group, body=settings) print u'updated group %s' % group def doUpdateAlias():