diff --git a/docs/GamUpdates.md b/docs/GamUpdates.md index 3f6d8242..a25e9e63 100644 --- a/docs/GamUpdates.md +++ b/docs/GamUpdates.md @@ -11,6 +11,24 @@ Add the `-s` option to the end of the above commands to suppress creating the `g See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Windows or other options, including manual installation +### 6.67.20 + +Added option `onelicenseperrow|onelicenceperrow` to `gam print users ... licenses` that causes GAM to print +a seperate user information row for each license a user is assigned. This makes processing +the licenses in a script possible and allows better sorting in a CSV File. + +By default, all licenses for a user are displayed in a list on one row: +``` +primaryEmail,LicensesCount,Licenses,LicensesDisplay +user@domain.com,2,1010020020 1010330004,Google Workspace Enterprise Plus Google Voice Standard +``` +With `onelicenseperrow|onelicenceperrow`, each license is on a separate row: +``` +primaryEmail,License,LicenseDisplay +user@domain.com,1010020020,Google Workspace Enterprise Plus +user@domain.com 1010330004,Google Voice Standard +``` + ### 6.67.19 Updated `gam create|update user ... notify` to encode the characters `<>&` in the password diff --git a/docs/How-to-Upgrade-from-Standard-GAM.md b/docs/How-to-Upgrade-from-Standard-GAM.md index 29f623e2..836f0ac4 100644 --- a/docs/How-to-Upgrade-from-Standard-GAM.md +++ b/docs/How-to-Upgrade-from-Standard-GAM.md @@ -334,7 +334,7 @@ writes the credentials into the file oauth2.txt. admin@server:/Users/admin/bin/gamadv-xtd3$ rm -f /Users/admin/GAMConfig/oauth2.txt admin@server:/Users/admin/bin/gamadv-xtd3$ ./gam version WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found -GAMADV-XTD3 6.17.19 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.67.20 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.1 64-bit final MacOS Sonoma 14.2.1 x86_64 @@ -1002,7 +1002,7 @@ writes the credentials into the file oauth2.txt. C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt C:\GAMADV-XTD3>gam version WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found -GAMADV-XTD3 6.17.19 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.67.20 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.1 64-bit final Windows-10-10.0.17134 AMD64 diff --git a/docs/Users.md b/docs/Users.md index 14640e7f..e47fe841 100644 --- a/docs/Users.md +++ b/docs/Users.md @@ -980,7 +980,9 @@ gam print users [todrive *] ([domain|domains ] [(query )|(queries )] [limittoou ] [deleted_only|only_deleted]) [orderby [ascending|descending]] - [groups|groupsincolumns] [license|licenses|licence|licences] + [groups|groupsincolumns] + [license|licenses|licence|licences] + [onelicenseperrow|onelicenceperrow] [schemas|custom|customschemas all|] [emailpart|emailparts|username] [userview] [allfields|basic|full|(*|fields )] @@ -1003,6 +1005,7 @@ gam print users [todrive *] select [orderby [ascending|descending]] [groups|groupsincolumns] [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser] + [onelicenseperrow|onelicenceperrow] [(products|product )|(skus|sku )] [schemas|custom|customschemas all|] [emailpart|emailparts|username][schemas|custom all|] @@ -1014,6 +1017,7 @@ gam print users [todrive *] [orderby [ascending|descending]] [groups|groupsincolumns] [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser] + [onelicenseperrow|onelicenceperrow] [(products|product )|(skus|sku )] [schemas|custom|customschemas all|] [emailpart|emailparts|username] @@ -1058,6 +1062,17 @@ to limit the display of aliases to those that match ``. By default, the entries in lists of groups and licenses are separated by the `csv_output_field_delimiter` from `gam.cfg`. * `delimiter ` - Separate list items with `` +By default, all licenses for a user are displayed in a list on one row: +``` +primaryEmail,LicensesCount,Licenses,LicensesDisplay +user@domain.com,2,1010020020 1010330004,Google Workspace Enterprise Plus Google Voice Standard +``` +With `onelicenseperrow|onelicenceperrow`, each license is on a separate row: +``` +primaryEmail,License,LicenseDisplay +user@domain.com,1010020020,Google Workspace Enterprise Plus +user@domain.com 1010330004,Google Voice Standard +``` In the output, primaryEmail is the always the first column; these options control the sorting of the remaining columns. * `allfields|basic|full` - All other columns are sorted by name. * `sortheaders [true]` - All other columns are sorted by name. diff --git a/docs/Version-and-Help.md b/docs/Version-and-Help.md index f522d177..b6941789 100644 --- a/docs/Version-and-Help.md +++ b/docs/Version-and-Help.md @@ -4,7 +4,7 @@ Print the current version of Gam with details ``` gam version -GAMADV-XTD3 6.17.19 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.67.20 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.1 64-bit final MacOS Sonoma 14.2.1 x86_64 @@ -16,7 +16,7 @@ Time: 2023-06-02T21:10:00-07:00 Print the current version of Gam with details and time offset information ``` gam version timeoffset -GAMADV-XTD3 6.17.19 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.67.20 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.1 64-bit final MacOS Sonoma 14.2.1 x86_64 @@ -28,7 +28,7 @@ Your system time differs from www.googleapis.com by less than 1 second Print the current version of Gam with extended details and SSL information ``` gam version extended -GAMADV-XTD3 6.17.19 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.67.20 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.1 64-bit final MacOS Sonoma 14.2.1 x86_64 @@ -65,7 +65,7 @@ MacOS High Sierra 10.13.6 x86_64 Path: /Users/Admin/bin/gamadv-xtd3 Version Check: Current: 5.35.08 - Latest: 6.17.19 + Latest: 6.67.20 echo $? 1 ``` @@ -73,7 +73,7 @@ echo $? Print the current version number without details ``` gam version simple -6.17.19 +6.67.20 ``` In Linux/MacOS you can do: ``` @@ -83,7 +83,7 @@ echo $VER Print the current version of Gam and address of this Wiki ``` gam help -GAM 6.17.19 - https://github.com/taers232c/GAMADV-XTD3 +GAM 6.67.20 - https://github.com/taers232c/GAMADV-XTD3 Ross Scroggs Python 3.12.1 64-bit final MacOS Sonoma 14.2.1 x86_64 diff --git a/src/GamCommands.txt b/src/GamCommands.txt index 9e5bbf11..658ec310 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -5348,6 +5348,7 @@ gam print users [todrive *] [orderby [ascending|descending]] [groups|groupsincolumns] [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser] + [onelicenseperrow|onelicenceperrow] [(products|product )|(skus|sku )] [schemas|custom|customschemas all|] [emailpart|emailparts|username] @@ -5363,6 +5364,7 @@ gam print users [todrive *] select [orderby [ascending|descending]] [groups|groupsincolumns] [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser] + [onelicenseperrow|onelicenceperrow] [(products|product )|(skus|sku )] [schemas|custom|customschemas all|] [emailpart|emailparts|username] @@ -5376,6 +5378,7 @@ gam print users [todrive *] [orderby [ascending|descending]] [groups|groupsincolumns] [license|licenses|licence|licences|licensebyuser|licensesbyuser|licencebyuser|licencesbyuser] + [onelicenseperrow|onelicenceperrow] [(products|product )|(skus|sku )] [schemas|custom|customschemas all|] [emailpart|emailparts|username] @@ -5778,7 +5781,7 @@ gam create workinglocation (home| (custom )| (office [building|buildingid ] [floor|floorname ] - [section|floorsection ] [desk|deskcode ])) + [section|floorsection ] [desk|deskcode ])) ((date yyyy-mm-dd)| (range yyyy-mm-dd yyyy-mm-dd)| (daily yyyy-mm-dd N)| diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 866a5b4b..3536d286 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -2,6 +2,24 @@ Merged GAM-Team version +6.67.20 + +Added option `onelicenseperrow|onelicenceperrow` to `gam print users ... licenses` that causes GAM to print +a seperate user information row for each license a user is assigned. This makes processing +the licenses in a script possible and allows better sorting in a CSV File. + +By default, all licenses for a user are displayed in a list on one row: +``` +primaryEmail,LicensesCount,Licenses,LicensesDisplay +user@domain.com,2,1010020020 1010330004,Google Workspace Enterprise Plus Google Voice Standard +``` +With `onelicenseperrow|onelicenceperrow`, each license is on a separate row: +``` +primaryEmail,License,LicenseDisplay +user@domain.com,1010020020,Google Workspace Enterprise Plus +user@domain.com 1010330004,Google Voice Standard +``` + 6.67.19 Updated `gam create|update user ... notify` to encode the characters `<>&` in the password diff --git a/src/gam/__init__.py b/src/gam/__init__.py index b4e2039b..dca3d437 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -42598,6 +42598,15 @@ USERS_INDEXED_TITLES = ['addresses', 'aliases', 'nonEditableAliases', 'emails', # [issuspended ] # [showitemcountonly] def doPrintUsers(entityList=None): + def _writeUserEntity(userEntity): + row = flattenJSON(userEntity, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS) + if not FJQC.formatJSON: + csvPF.WriteRowTitles(row) + elif csvPF.CheckRowTitles(row): + csvPF.WriteRowNoFilter({'primaryEmail': userEntity['primaryEmail'], + 'JSON': json.dumps(cleanJSON(userEntity, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS), + ensure_ascii=False, sort_keys=True)}) + def _printUser(userEntity, i, count): if isSuspended is None or isSuspended == userEntity.get('suspended', isSuspended): userEmail = userEntity['primaryEmail'] @@ -42635,24 +42644,31 @@ def doPrintUsers(entityList=None): badRequestWarning(Ent.GROUP, Ent.MEMBER, userEmail) except (GAPI.resourceNotFound, GAPI.domainNotFound, GAPI.forbidden, GAPI.badRequest): accessErrorExit(cd) + if aliasMatchPattern and 'aliases' in userEntity: + userEntity['aliases'] = [alias for alias in userEntity['aliases'] if aliasMatchPattern.match(alias)] if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']: if printOptions['getLicenseFeed']: u_licenses = licenses.get(userEmail.lower(), []) else: u_licenses = getUserLicenses(lic, userEntity, skus) - userEntity['LicensesCount'] = len(u_licenses) + if not oneLicensePerRow: + userEntity['LicensesCount'] = len(u_licenses) + if u_licenses: + userEntity['Licenses'] = delimiter.join(u_licenses) + userEntity['LicensesDisplay'] = delimiter.join([SKU.skuIdToDisplayName(skuId) for skuId in u_licenses]) + else: + u_licenses = [] + if not oneLicensePerRow: + _writeUserEntity(userEntity) + else: if u_licenses: - userEntity['Licenses'] = delimiter.join(u_licenses) - userEntity['LicensesDisplay'] = delimiter.join([SKU.skuIdToDisplayName(skuId) for skuId in u_licenses]) - if aliasMatchPattern and 'aliases' in userEntity: - userEntity['aliases'] = [alias for alias in userEntity['aliases'] if aliasMatchPattern.match(alias)] - row = flattenJSON(userEntity, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS) - if not FJQC.formatJSON: - csvPF.WriteRowTitles(row) - elif csvPF.CheckRowTitles(row): - csvPF.WriteRowNoFilter({'primaryEmail': userEmail, - 'JSON': json.dumps(cleanJSON(userEntity, skipObjects=USER_SKIP_OBJECTS, timeObjects=USER_TIME_OBJECTS), - ensure_ascii=False, sort_keys=True)}) + for skuId in u_licenses: + userEntity['License'] = skuId + userEntity['LicenseDisplay'] = SKU.skuIdToDisplayName(skuId) + _writeUserEntity(userEntity) + else: + userEntity['License'] = userEntity['LicenseDisplay'] = '' + _writeUserEntity(userEntity) def _updateDomainCounts(emailAddress): atLoc = emailAddress.find('@') @@ -42718,7 +42734,7 @@ def doPrintUsers(entityList=None): projection = 'basic' projectionSet = False customFieldMask = None - quotePlusPhoneNumbers = showDeleted = False + oneLicensePerRow = quotePlusPhoneNumbers = showDeleted = False aliasMatchPattern = isSuspended = orgUnitPath = orgUnitPathLower = orderBy = sortOrder = None viewType = 'admin_view' delimiter = GC.Values[GC.CSV_OUTPUT_FIELD_DELIMITER] @@ -42781,6 +42797,8 @@ def doPrintUsers(entityList=None): elif myarg in {'licensebyuser', 'licensesbyuser', 'licencebyuser', 'licencesbyuser'}: printOptions['getLicenseFeedByUser'] = True printOptions['getLicenseFeed'] = False + elif myarg in {'onelicenseperrow', 'onelicenceperrow'}: + oneLicensePerRow = True elif myarg in {'products', 'product'}: skus = SKU.convertProductListToSKUList(getGoogleProductList()) elif myarg in {'sku', 'skus'}: @@ -42818,7 +42836,10 @@ def doPrintUsers(entityList=None): else: csvPF.AddTitles(['Groups']) if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']: - csvPF.AddTitles(['LicensesCount', 'Licenses', 'LicensesDisplay']) + if not oneLicensePerRow: + csvPF.AddTitles(['LicensesCount', 'Licenses', 'LicensesDisplay']) + else: + csvPF.AddTitles(['License', 'LicenseDisplay']) if printOptions['getLicenseFeed']: if skus is None and GM.Globals[GM.LICENSE_SKUS]: skus = GM.Globals[GM.LICENSE_SKUS] @@ -42958,7 +42979,10 @@ def doPrintUsers(entityList=None): else: csvPF.MoveTitlesToEnd(['Groups']+[f'Groups{GC.Values[GC.CSV_OUTPUT_SUBFIELD_DELIMITER]}{j}' for j in range(printOptions['maxGroups'])]) if printOptions['getLicenseFeed'] or printOptions['getLicenseFeedByUser']: - csvPF.MoveTitlesToEnd(['LicensesCount', 'Licenses', 'LicensesDisplay']) + if not oneLicensePerRow: + csvPF.MoveTitlesToEnd(['LicensesCount', 'Licenses', 'LicensesDisplay']) + else: + csvPF.MoveTitlesToEnd(['License', 'LicenseDisplay']) elif not FJQC.formatJSON: for domain, count in sorted(iter(domainCounts.items())): csvPF.WriteRowNoFilter({'domain': domain, 'count': count})