diff --git a/.github/workflows/winget.yml b/.github/workflows/winget.yml index 98e02c59..c36acec6 100644 --- a/.github/workflows/winget.yml +++ b/.github/workflows/winget.yml @@ -10,7 +10,7 @@ jobs: - uses: vedantmgoyal2009/winget-releaser@v2 with: identifier: taers232c.GAMADV-XTD3 - release-tag: v7.02.00 + release-tag: v7.02.01 max-versions-to-keep: 1 # keep only latest versions installers-regex: '\.msi$' token: ${{ secrets.WINGET_TOKEN }} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 369e9dfc..6cd2920b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -96,11 +96,11 @@ script: before_deploy: # Comment out for Linux Xenial and Trusty -#- yes | gem update --system --force -#- gem install bundler -#- gem install faraday-net_http -v '3.3.0' # Fix faraday version -#- gem install uri -#- gem install logger +- yes | gem update --system --force +- gem install bundler +- gem install faraday-net_http -v '3.3.0' # Fix faraday version +- gem install uri +- gem install logger - export TRAVIS_TAG="preview" - unset LD_LIBRARY_PATH @@ -115,8 +115,8 @@ deploy: # Linux 64-Bit Bionic and Linux ARM64 Focal and Linux ARM64 Bionic and Linux ARM64 Xenial # edge: true # Linux Xenial and Trusty - edge: - branch: v2.0.3-beta.4 +# edge: +# branch: v2.0.3-beta.4 # branch: v2.0.5-beta.1 on: repo: taers232c/GAMADV-XTD3 diff --git a/src/GamCommands.txt b/src/GamCommands.txt index b8cecd0a..825f3ea1 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -3929,6 +3929,7 @@ gam update cigroup [copyfrom ] [security|makesecuritygroup| dynamicsecurity|makedynamicsecuritygroup| lockedsecurity|makelockedsecuritygroup] + [locked|unlocked] [dynamic ] [memberrestrictions ] gam update cigroups create|add [] diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 6208aea5..142a112d 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -1,3 +1,14 @@ +7.02.01 + +Added options `locked` and `unlocked` to `gam update cigroups` that allow locking/unlocking groups. + +* See: https://workspaceupdates.googleblog.com/2024/12/locked-groups-open-beta.html + +You'll have to do a `gam oauth create` and enable the following scope to use these options: +``` +[*] 22) Cloud Identity Groups API Beta (Enables group locking/unlocking) +``` + 7.02.00 Improved the error message displayed for user service account access commands when: diff --git a/src/gam/__init__.py b/src/gam/__init__.py index ecec3ec3..27392a6a 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -25,7 +25,7 @@ """ __author__ = 'Ross Scroggs ' -__version__ = '7.02.00' +__version__ = '7.02.01' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' #pylint: disable=wrong-import-position @@ -5791,6 +5791,8 @@ def convertUIDtoEmailAddressWithType(emailAddressOrUID, cd=None, sal=None, email def convertUIDtoEmailAddress(emailAddressOrUID, cd=None, emailTypes=None, checkForCustomerId=False, ciGroupsAPI=False, aliasAllowed=True): if ciGroupsAPI: + if emailAddressOrUID.startswith('cbcm-browser.') or emailAddressOrUID.startswith('chrome-os-device.'): + return emailAddressOrUID normalizedEmailAddressOrUID = normalizeEmailAddressOrUID(emailAddressOrUID, ciGroupsAPI=ciGroupsAPI) if normalizedEmailAddressOrUID.startswith('cbcm-browser.') or normalizedEmailAddressOrUID.startswith('chrome-os-device.'): return normalizedEmailAddressOrUID @@ -5974,11 +5976,8 @@ def _checkMemberCategory(member, memberDisplayOptions): return True return False -CIGROUP_MEMBER_API = API.CLOUDIDENTITY_GROUPS -CIGROUP_MEMBERKEY = 'preferredMemberKey' - def _checkCIMemberCategory(member, memberDisplayOptions): - member_email = member.get(CIGROUP_MEMBERKEY,{}).get('id', '') + member_email = member.get('preferredMemberKey', {}).get('id', '') if member_email.find('@') > 0: _, domain = member_email.lower().split('@', 1) category = 'internal' if domain in memberDisplayOptions['internalDomains'] else 'external' @@ -5992,7 +5991,7 @@ def _checkCIMemberCategory(member, memberDisplayOptions): def getCIGroupMemberRoleFixType(member): ''' fixes missing type and returns the highest role of member ''' if 'type' not in member: - if member[CIGROUP_MEMBERKEY]['id'] == GC.Values[GC.CUSTOMER_ID]: + if member['preferredMemberKey']['id'] == GC.Values[GC.CUSTOMER_ID]: member['type'] = Ent.TYPE_CUSTOMER else: member['type'] = Ent.TYPE_OTHER @@ -6012,7 +6011,7 @@ def getCIGroupTransitiveMemberRoleFixType(groupName, tmember): ''' map transitive member to normal member ''' tid = tmember['preferredMemberKey'][0].get('id', GC.Values[GC.CUSTOMER_ID]) if tmember['preferredMemberKey'] else '' ttype, tname = tmember['member'].split('/') - member = {'name': f'{groupName}/membershipd/{tname}', CIGROUP_MEMBERKEY: {'id': tid}} + member = {'name': f'{groupName}/membershipd/{tname}', 'preferredMemberKey': {'id': tid}} if 'type' not in tmember: if tid == GC.Values[GC.CUSTOMER_ID]: member['type'] = Ent.TYPE_CUSTOMER @@ -6091,6 +6090,11 @@ def convertGroupEmailToCloudID(ci, group, i=0, count=0): Act.Set(action) return (ci, None, None) +CIGROUP_DISCUSSION_FORUM_LABEL = 'cloudidentity.googleapis.com/groups.discussion_forum' +CIGROUP_DYNAMIC_LABEL = 'cloudidentity.googleapis.com/groups.dynamic' +CIGROUP_SECURITY_LABEL = 'cloudidentity.googleapis.com/groups.security' +CIGROUP_LOCKED_LABEL = 'cloudidentity.googleapis.com/groups.locked' + def getCIGroupMembershipGraph(ci, member): if not ci: ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS) @@ -6099,7 +6103,7 @@ def getCIGroupMembershipGraph(ci, member): result = callGAPI(ci.groups().memberships(), 'getMembershipGraph', throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS, parent=parent, - query=f"member_key_id == '{member}' && 'cloudidentity.googleapis.com/groups.discussion_forum' in labels") + query=f"member_key_id == '{member}' && CIGROUP_DISCUSSION_FORUM_LABEL in labels") return (ci, result.get('response', {})) except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, @@ -6190,7 +6194,7 @@ def _addCIGroupUsersToUsers(groupName, groupEmail, recursive): pageMessage=getPageMessageForWhom(), throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS, parent=groupName, view='FULL', - fields=f'nextPageToken,memberships(name,{CIGROUP_MEMBERKEY}(id),roles(name),type)', pageSize=GC.Values[GC.MEMBER_MAX_RESULTS]) + fields='nextPageToken,memberships(name,preferredMemberKey(id),roles(name),type)', pageSize=GC.Values[GC.MEMBER_MAX_RESULTS]) except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, GAPI.systemError, GAPI.permissionDenied, GAPI.serviceNotAvailable): @@ -6200,7 +6204,7 @@ def _addCIGroupUsersToUsers(groupName, groupEmail, recursive): for member in result: getCIGroupMemberRoleFixType(member) if member['type'] == Ent.TYPE_USER: - email = member.get(CIGROUP_MEMBERKEY, {}).get('id', '') + email = member.get('preferredMemberKey', {}).get('id', '') if (email and _checkMemberRole(member, validRoles) and email not in entitySet): entitySet.add(email) entityList.append(email) @@ -6350,7 +6354,7 @@ def _addCIGroupUsersToUsers(groupName, groupEmail, recursive): else: _showInvalidEntity(Ent.GROUP, group) elif entityType in {Cmd.ENTITY_CIGROUP, Cmd.ENTITY_CIGROUPS}: - ci = buildGAPIObject(CIGROUP_MEMBER_API) + ci = buildGAPIObject(API.CLOUDIDENTITY_GROUPS) groups = convertEntityToList(entity, nonListEntityType=entityType in {Cmd.ENTITY_CIGROUP}) for group in groups: if validateEmailAddressOrUID(group, checkPeople=False, ciGroupsAPI=True): @@ -6362,7 +6366,7 @@ def _addCIGroupUsersToUsers(groupName, groupEmail, recursive): pageMessage=getPageMessageForWhom(), throwReasons=GAPI.CIGROUP_LIST_THROW_REASONS, retryReasons=GAPI.CIGROUP_RETRY_REASONS, parent=name, view='FULL', - fields=f'nextPageToken,memberships({CIGROUP_MEMBERKEY}(id),roles(name),type)', + fields='nextPageToken,memberships(preferredMemberKey(id),roles(name),type)', pageSize=GC.Values[GC.MEMBER_MAX_RESULTS]) except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.domainCannotUseApis, GAPI.forbidden, GAPI.badRequest, GAPI.invalid, GAPI.invalidArgument, @@ -6372,7 +6376,7 @@ def _addCIGroupUsersToUsers(groupName, groupEmail, recursive): continue for member in result: getCIGroupMemberRoleFixType(member) - email = member.get(CIGROUP_MEMBERKEY, {}).get('id', '') + email = member.get('preferredMemberKey', {}).get('id', '') if (email and (groupMemberType in ('ALL', member['type'])) and _checkMemberRole(member, validRoles) and email not in entitySet): entitySet.add(email) @@ -22682,7 +22686,7 @@ def _makeFilenameFromPattern(resourceName): with open(os.path.expanduser(filename), 'rb') as f: image_data = f.read() callGAPI(people.people(), function, - throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS, + throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR]+GAPI.PEOPLE_ACCESS_THROW_REASONS, resourceName=resourceName, body={'photoBytes': base64.urlsafe_b64encode(image_data).decode(UTF8)}) entityActionPerformed([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount) @@ -22718,7 +22722,7 @@ def _makeFilenameFromPattern(resourceName): else: #elif function == 'deleteContactPhoto': filename = '' callGAPI(people.people(), function, - throwReasons=[GAPI.NOT_FOUND, GAPI.INTERNAL_ERROR, GAPI.PHOTO_NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS, + throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.INTERNAL_ERROR, GAPI.PHOTO_NOT_FOUND]+GAPI.PEOPLE_ACCESS_THROW_REASONS, resourceName=resourceName) entityActionPerformed([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount) except (GAPI.notFound, GAPI.internalError): @@ -22726,7 +22730,7 @@ def _makeFilenameFromPattern(resourceName): continue except GAPI.photoNotFound: entityDoesNotHaveItemWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], j, jcount) - except (OSError, IOError) as e: + except (GAPI.invalidArgument, OSError, IOError) as e: entityActionFailedWarning([entityType, user, peopleEntityType, resourceName, Ent.PHOTO, filename], str(e), j, jcount) except (GAPI.serviceNotAvailable, GAPI.forbidden): ClientAPIAccessDeniedExit() @@ -31649,7 +31653,7 @@ def doCreateGroup(ciGroupsAPI=False): parent = f'customers/{GC.Values[GC.CUSTOMER_ID]}' body = {'groupKey': {'id': groupEmail}, 'parent': parent, - 'labels': {'cloudidentity.googleapis.com/groups.discussion_forum': ''}, + 'labels': {CIGROUP_DISCUSSION_FORUM_LABEL: ''}, } gs_body = {} while Cmd.ArgumentsRemaining(): @@ -32171,8 +32175,8 @@ def _batchUpdateGroupMembers(group, i, count, updateMembers, role, delivery_sett elif myarg == 'getbeforeupdate': getBeforeUpdate = True elif myarg in {'security', 'makesecuritygroup'}: - ci_body['labels'] = {'cloudidentity.googleapis.com/groups.discussion_forum': '', - 'cloudidentity.googleapis.com/groups.security': ''} + ci_body['labels'] = {CIGROUP_DISCUSSION_FORUM_LABEL: '', + CIGROUP_SECURITY_LABEL: ''} elif myarg == 'json': gs_body.update(getJSON(GROUP_JSON_SKIP_FIELDS)) elif myarg == 'accesstype': @@ -32691,7 +32695,7 @@ def checkMemberMatch(member, memberOptions): def checkCIMemberMatch(member, memberOptions): if not memberOptions[MEMBEROPTION_MATCHPATTERN]: return True - if memberOptions[MEMBEROPTION_MATCHPATTERN].match(member.get(CIGROUP_MEMBERKEY, {}).get('id', '')): + if memberOptions[MEMBEROPTION_MATCHPATTERN].match(member.get('preferredMemberKey', {}).get('id', '')): return memberOptions[MEMBEROPTION_DISPLAYMATCH] return not memberOptions[MEMBEROPTION_DISPLAYMATCH] @@ -33279,7 +33283,7 @@ def addMemberInfoToRow(row, groupMembers, typesSet, memberOptions, memberDisplay if not ciGroupsAPI: member_email = member.get('email', member.get('id', None)) else: - member_email = member.get(CIGROUP_MEMBERKEY, {}).get('id', member['name']) + member_email = member.get('preferredMemberKey', {}).get('id', member['name']) if not member_email: writeStderr(f' Not sure what to do with: {member}\n') continue @@ -33789,9 +33793,9 @@ def _writeCompleteRows(): csvPF.writeCSVfile('Groups') def mapCIGroupMemberFieldNames(member): - member['email'] = member[CIGROUP_MEMBERKEY].pop('id') - if not member[CIGROUP_MEMBERKEY]: - member.pop(CIGROUP_MEMBERKEY) + member['email'] = member['preferredMemberKey'].pop('id') + if not member['preferredMemberKey']: + member.pop('preferredMemberKey') if 'name' in member: member['id'] = member.pop('name') @@ -34556,7 +34560,8 @@ def doCreateCIGroup(): # [security|makesecuritygroup|dynamicsecurity|makedynamicsecuritygroup] # [dynamic ] # [memberrestrictions ] -# gam update cigroups create [] +# [locked|unlocked] +# gam update cigroups add|create [] # [usersonly|groupsonly] # [notsuspended|suspended] [notarchived|archived] # [expire|expires