mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-20 06:01:37 +00:00
Compare commits
17 Commits
v7.03.00
...
20250206.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ce83b4623 | ||
|
|
a58a998b49 | ||
|
|
4e04bd7c51 | ||
|
|
779ac0a6a0 | ||
|
|
f18b7258bb | ||
|
|
d4932c9d39 | ||
|
|
352845e482 | ||
|
|
ff49c67580 | ||
|
|
efee86cd33 | ||
|
|
a42eebdae1 | ||
|
|
05333d9521 | ||
|
|
b04ba4b618 | ||
|
|
c8108dace0 | ||
|
|
83a70d656e | ||
|
|
3a38609fbb | ||
|
|
e744aa29e3 | ||
|
|
367c23a13c |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -17,7 +17,7 @@ defaults:
|
||||
working-directory: src
|
||||
|
||||
env:
|
||||
SCRATCH_COUNTER: 7
|
||||
SCRATCH_COUNTER: 9
|
||||
OPENSSL_CONFIG_OPTS: no-fips --api=3.0.0
|
||||
OPENSSL_INSTALL_PATH: ${{ github.workspace }}/bin/ssl
|
||||
OPENSSL_SOURCE_PATH: ${{ github.workspace }}/src/openssl
|
||||
@@ -129,7 +129,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
cache.tar.xz
|
||||
key: gam-${{ matrix.jid }}-20250116
|
||||
key: gam-${{ matrix.jid }}-20250204
|
||||
|
||||
- name: Untar Cache archive
|
||||
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'
|
||||
|
||||
39
pyproject.toml
Normal file
39
pyproject.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[project]
|
||||
name = "gam7"
|
||||
dynamic = [
|
||||
"dependencies",
|
||||
"version",
|
||||
]
|
||||
authors = [
|
||||
{ name="Jay Lee", email="jay0lee@gmail.com" },
|
||||
{ name="Ross Scroggs", email="Ross.Scroggs@gmail.com" },
|
||||
]
|
||||
description = "CLI tool to manage Google Workspace"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.9"
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"Operating System :: OS Independent",
|
||||
]
|
||||
license = {text = "Apache License (2.0)"}
|
||||
license-files = ["LICEN[CS]E*"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/GAM-team/GAM"
|
||||
Issues = "https://github.com/GAM-team/GAM/issues"
|
||||
|
||||
[tool.hatch.version]
|
||||
path = "src/gam/__init__.py"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/gam"]
|
||||
|
||||
[tool.hatch.metadata.hooks.requirements_txt]
|
||||
files = ["src/requirements.txt"]
|
||||
|
||||
[build-system]
|
||||
requires = [
|
||||
"hatchling",
|
||||
"hatch-requirements_txt",
|
||||
]
|
||||
build-backend = "hatchling.build"
|
||||
@@ -3927,8 +3927,11 @@ gam print group-members [todrive <ToDriveAttribute>*]
|
||||
updatetime
|
||||
<CIGroupFieldNameList> ::= "<CIGroupFieldName>(,<CIGroupFieldName>)*"
|
||||
|
||||
gam create cigroup <EmailAddress> [copyfrom <GroupItem>] <GroupAttribute>*
|
||||
[makeowner] [alias|aliases <CIGroupAliasList>] [dynamic <QueryDynamicGroup>]
|
||||
gam create cigroup <EmailAddress>
|
||||
[copyfrom <GroupItem>] <GroupAttribute>*
|
||||
[makeowner] [alias|aliases <CIGroupAliasList>]
|
||||
[security|makesecuritygroup]
|
||||
[dynamic <QueryDynamicGroup>]
|
||||
gam update cigroup <GroupEntity> [copyfrom <GroupItem>] <GroupAttribute>
|
||||
[security|makesecuritygroup|
|
||||
dynamicsecurity|makedynamicsecuritygroup|
|
||||
@@ -4515,7 +4518,7 @@ gam report users|user [todrive <ToDriveAttribute>*]
|
||||
(country|countrycode <String>)
|
||||
|
||||
gam create|add resoldcustomer <CustomerDomain> (customer_auth_token <String>) <ResoldCustomerAttribute>+
|
||||
gam update resoldcustomer <CustomerID> [customer_auth_token <String>] <ResoldCustomerAttribues>+
|
||||
gam update resoldcustomer <CustomerID> <ResoldCustomerAttribues>+
|
||||
gam info resoldcustomer <CustomerID> [formatjson]
|
||||
|
||||
gam create|add resoldsubscription <CustomerID> (sku <SKUID>)
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
7.03.04
|
||||
|
||||
Added option `security` to `gam create cigroup` that allows creation of a security group
|
||||
in a single command.
|
||||
|
||||
Updated to Python 3.13.2 where possible.
|
||||
|
||||
7.03.03
|
||||
|
||||
Fixed bug in `gam update resoldcustomer` that caused the following error:
|
||||
```
|
||||
ERROR: Got an unexpected keyword argument customerAuthToken
|
||||
```
|
||||
|
||||
7.03.02
|
||||
|
||||
Updated `gam <UserTypeEntity> show labels nested` to properly display label nesting
|
||||
when labels have embedded `/` characters in their names.
|
||||
|
||||
7.03.01
|
||||
|
||||
Updated `gam create project` to retry the following unexpected error:
|
||||
```
|
||||
ERROR: 400 - invalidArgument - Service account gam-project-a1b2c@gam-project-a1b2c.iam.gserviceaccount.com does not exist.
|
||||
```
|
||||
|
||||
7.03.00
|
||||
|
||||
Updated `gam create|use project` to discontinue use of the `Identity-Aware Proxy (IAP) OAuth Admin APIs`
|
||||
|
||||
@@ -112,6 +112,12 @@ else
|
||||
check_type="authenticated"
|
||||
curl_opts=( "$GHCLIENT" )
|
||||
fi
|
||||
curl_ver=$(curl --version|head -1|cut -d " " -f 2)
|
||||
if [[ "${curl_ver:0:4}" < "7.76" ]]; then
|
||||
curl_fail=( )
|
||||
else
|
||||
curl_fail=( "--fail-with-body" )
|
||||
fi
|
||||
echo_yellow "Checking GitHub URL $release_url for $gamversion GAM release ($check_type)..."
|
||||
release_json=$(curl \
|
||||
--silent \
|
||||
@@ -119,7 +125,7 @@ release_json=$(curl \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$release_url" \
|
||||
--fail-with-body)
|
||||
"${curl_fail[@]}")
|
||||
curl_exit_code=$?
|
||||
if [ $curl_exit_code -ne 0 ]; then
|
||||
echo_red "ERROR retrieving URL: ${release_json}"
|
||||
|
||||
@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
|
||||
"""
|
||||
|
||||
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
|
||||
__version__ = '7.03.00'
|
||||
__version__ = '7.03.04'
|
||||
__license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
|
||||
|
||||
#pylint: disable=wrong-import-position
|
||||
@@ -11361,13 +11361,32 @@ def doEnableAPIs():
|
||||
url = f'https://console.cloud.google.com/apis/enableflow?apiid={apiid}&project={projectId}'
|
||||
writeStdout(f' {url}\n\n')
|
||||
|
||||
def _waitForSvcAcctCompletion(i):
|
||||
sleep_time = i*5
|
||||
if i > 3:
|
||||
sys.stdout.write(Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.SVCACCT), sleep_time))
|
||||
time.sleep(sleep_time)
|
||||
|
||||
def _grantRotateRights(iam, projectId, service_account, email, account_type='serviceAccount'):
|
||||
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, email],
|
||||
Msg.HAS_RIGHTS_TO_ROTATE_OWN_PRIVATE_KEY.format(email, service_account))
|
||||
body = {'policy': {'bindings': [{'role': 'roles/iam.serviceAccountKeyAdmin',
|
||||
'members': [f'{account_type}:{email}']}]}}
|
||||
callGAPI(iam.projects().serviceAccounts(), 'setIamPolicy',
|
||||
resource=f'projects/{projectId}/serviceAccounts/{service_account}', body=body)
|
||||
maxRetries = 10
|
||||
printEntityMessage([Ent.PROJECT, projectId, Ent.SVCACCT, email],
|
||||
Msg.HAS_RIGHTS_TO_ROTATE_OWN_PRIVATE_KEY.format(email, service_account))
|
||||
for retry in range(1, maxRetries+1):
|
||||
try:
|
||||
callGAPI(iam.projects().serviceAccounts(), 'setIamPolicy',
|
||||
throwReasons=[GAPI.INVALID_ARGUMENT],
|
||||
resource=f'projects/{projectId}/serviceAccounts/{service_account}', body=body)
|
||||
return True
|
||||
except GAPI.invalidArgument as e:
|
||||
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, service_account], str(e))
|
||||
if 'does not exist' not in str(e) or retry == maxRetries:
|
||||
return False
|
||||
_waitForSvcAcctCompletion(retry)
|
||||
except Exception as e:
|
||||
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, service_account], str(e))
|
||||
return False
|
||||
|
||||
def _createOauth2serviceJSON(httpObj, projectInfo, svcAcctInfo, create_key=True):
|
||||
iam = getAPIService(API.IAM, httpObj)
|
||||
@@ -11392,8 +11411,7 @@ def _createOauth2serviceJSON(httpObj, projectInfo, svcAcctInfo, create_key=True)
|
||||
clientId=service_account['uniqueId']):
|
||||
return False
|
||||
sa_email = service_account['name'].rsplit('/', 1)[-1]
|
||||
_grantRotateRights(iam, projectInfo['projectId'], sa_email, sa_email)
|
||||
return True
|
||||
return _grantRotateRights(iam, projectInfo['projectId'], sa_email, sa_email)
|
||||
|
||||
def _createClientSecretsOauth2service(httpObj, login_hint, appInfo, projectInfo, svcAcctInfo, create_key=True):
|
||||
def _checkClientAndSecret(csHttpObj, client_id, client_secret):
|
||||
@@ -12563,12 +12581,6 @@ def doProcessSvcAcctKeys(mode=None, iam=None, projectId=None, clientEmail=None,
|
||||
else:
|
||||
unknownArgumentExit()
|
||||
|
||||
def waitForCompletion(i):
|
||||
sleep_time = i*5
|
||||
if i > 3:
|
||||
sys.stdout.write(Msg.WAITING_FOR_ITEM_CREATION_TO_COMPLETE_SLEEPING.format(Ent.Singular(Ent.SVCACCT), sleep_time))
|
||||
time.sleep(sleep_time)
|
||||
|
||||
local_key_size = 2048
|
||||
validityHours = 0
|
||||
body = {}
|
||||
@@ -12638,12 +12650,12 @@ def doProcessSvcAcctKeys(mode=None, iam=None, projectId=None, clientEmail=None,
|
||||
if retry == maxRetries:
|
||||
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
||||
return False
|
||||
waitForCompletion(retry)
|
||||
_waitForSvcAcctCompletion(retry)
|
||||
except GAPI.permissionDenied:
|
||||
if retry == maxRetries:
|
||||
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
||||
return False
|
||||
waitForCompletion(retry)
|
||||
_waitForSvcAcctCompletion(retry)
|
||||
except GAPI.badRequest as e:
|
||||
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
||||
return False
|
||||
@@ -12656,7 +12668,7 @@ def doProcessSvcAcctKeys(mode=None, iam=None, projectId=None, clientEmail=None,
|
||||
new_data['private_key'] = ''
|
||||
newPrivateKeyId = ''
|
||||
break
|
||||
waitForCompletion(retry)
|
||||
_waitForSvcAcctCompletion(retry)
|
||||
new_data['private_key_id'] = newPrivateKeyId
|
||||
oauth2service_data = _formatOAuth2ServiceData(new_data)
|
||||
else:
|
||||
@@ -12673,7 +12685,7 @@ def doProcessSvcAcctKeys(mode=None, iam=None, projectId=None, clientEmail=None,
|
||||
if retry == maxRetries:
|
||||
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
||||
return False
|
||||
waitForCompletion(retry)
|
||||
_waitForSvcAcctCompletion(retry)
|
||||
except GAPI.badRequest as e:
|
||||
entityActionFailedWarning([Ent.PROJECT, projectId, Ent.SVCACCT, clientEmail], str(e))
|
||||
return False
|
||||
@@ -12714,7 +12726,7 @@ def doProcessSvcAcctKeys(mode=None, iam=None, projectId=None, clientEmail=None,
|
||||
if retry == maxRetries:
|
||||
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], Msg.UPDATE_PROJECT_TO_VIEW_MANAGE_SAKEYS)
|
||||
break
|
||||
waitForCompletion(retry)
|
||||
_waitForSvcAcctCompletion(retry)
|
||||
except GAPI.badRequest as e:
|
||||
entityActionFailedWarning([Ent.SVCACCT_KEY, keyName], str(e), i, count)
|
||||
break
|
||||
@@ -15094,15 +15106,15 @@ def doCreateResoldCustomer():
|
||||
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
||||
entityActionFailedWarning([Ent.CUSTOMER_DOMAIN, body['customerDomain']], str(e))
|
||||
|
||||
# gam update resoldcustomer <CustomerID> [customer_auth_token <String>] <ResoldCustomerAttribute>+
|
||||
# gam update resoldcustomer <CustomerID> <ResoldCustomerAttribute>+
|
||||
def doUpdateResoldCustomer():
|
||||
res = buildGAPIObject(API.RESELLER)
|
||||
customerId = getString(Cmd.OB_CUSTOMER_ID)
|
||||
customerAuthToken, body = _getResoldCustomerAttr()
|
||||
_, body = _getResoldCustomerAttr()
|
||||
try:
|
||||
callGAPI(res.customers(), 'patch',
|
||||
throwReasons=GAPI.RESELLER_THROW_REASONS,
|
||||
customerId=customerId, body=body, customerAuthToken=customerAuthToken, fields='')
|
||||
customerId=customerId, body=body, fields='')
|
||||
entityActionPerformed([Ent.CUSTOMER_ID, customerId])
|
||||
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden, GAPI.invalid) as e:
|
||||
entityActionFailedWarning([Ent.CUSTOMER_ID, customerId], str(e))
|
||||
@@ -15121,6 +15133,7 @@ def doInfoResoldCustomer():
|
||||
customerId=customerId)
|
||||
if not FJQC.formatJSON:
|
||||
printKeyValueList(['Customer ID', customerInfo['customerId']])
|
||||
printKeyValueList(['Customer Type', customerInfo['customerType']])
|
||||
printKeyValueList(['Customer Domain', customerInfo['customerDomain']])
|
||||
if 'customerDomainVerified' in customerInfo:
|
||||
printKeyValueList(['Customer Domain Verified', customerInfo['customerDomainVerified']])
|
||||
@@ -31719,6 +31732,8 @@ def doCreateGroup(ciGroupsAPI=False):
|
||||
'query': getString(Cmd.OB_QUERY)})
|
||||
elif ciGroupsAPI and myarg == 'makeowner':
|
||||
initialGroupConfig = 'WITH_INITIAL_OWNER'
|
||||
elif ciGroupsAPI and myarg in {'security', 'makesecuritygroup'}:
|
||||
body['labels'][CIGROUP_SECURITY_LABEL] = ''
|
||||
elif myarg == 'verifynotinvitable':
|
||||
verifyNotInvitable = True
|
||||
else:
|
||||
@@ -34595,8 +34610,11 @@ def doPrintShowGroupTree():
|
||||
if csvPF:
|
||||
csvPF.writeCSVfile('Group Tree')
|
||||
|
||||
# gam create cigroup <EmailAddress> [copyfrom <GroupItem>] <GroupAttribute>
|
||||
# [makeowner] [alias|aliases <CIGroupAliasList>] [dynamic <QueryDynamicGroup>]
|
||||
# gam create cigroup <EmailAddress>
|
||||
# [copyfrom <GroupItem>] <GroupAttribute>
|
||||
# [makeowner] [alias|aliases <CIGroupAliasList>]
|
||||
# [security|makesecuritygroup]
|
||||
# [dynamic <QueryDynamicGroup>]
|
||||
def doCreateCIGroup():
|
||||
doCreateGroup(ciGroupsAPI=True)
|
||||
|
||||
@@ -37431,7 +37449,7 @@ def _doDeleteResourceCalendars(entityList):
|
||||
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
||||
customer=GC.Values[GC.CUSTOMER_ID], calendarResourceId=resourceId)
|
||||
entityActionPerformed([Ent.RESOURCE_CALENDAR, resourceId], i, count)
|
||||
except GAPI.serviceNotAvailable as e:
|
||||
except GAPI.serviceNotAvailable as e:
|
||||
entityActionFailedWarning([Ent.RESOURCE_CALENDAR, resourceId], str(e), i, count)
|
||||
except (GAPI.badRequest, GAPI.resourceNotFound, GAPI.forbidden):
|
||||
checkEntityAFDNEorAccessErrorExit(cd, Ent.RESOURCE_CALENDAR, resourceId, i, count)
|
||||
@@ -39150,10 +39168,12 @@ def _wipeCalendarEvents(user, origCal, calIds, count):
|
||||
continue
|
||||
try:
|
||||
callGAPI(cal.calendars(), 'clear',
|
||||
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID, GAPI.REQUIRED_ACCESS_LEVEL],
|
||||
throwReasons=GAPI.CALENDAR_THROW_REASONS+[GAPI.NOT_FOUND, GAPI.FORBIDDEN, GAPI.INVALID,
|
||||
GAPI.REQUIRED_ACCESS_LEVEL, GAPI.SERVICE_NOT_AVAILABLE],
|
||||
retryReasons=GAPI.SERVICE_NOT_AVAILABLE_RETRY_REASONS,
|
||||
calendarId=calId)
|
||||
entityActionPerformed([Ent.CALENDAR, calId], i, count)
|
||||
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.requiredAccessLevel) as e:
|
||||
except (GAPI.notFound, GAPI.forbidden, GAPI.invalid, GAPI.requiredAccessLevel, GAPI.serviceNotAvailable) as e:
|
||||
entityActionFailedWarning([Ent.CALENDAR, calId], str(e), i, count)
|
||||
except GAPI.notACalendarUser:
|
||||
userCalServiceNotEnabledWarning(calId, i, count)
|
||||
@@ -69387,13 +69407,18 @@ LABEL_COUNTS_FIELDS = ','.join(LABEL_COUNTS_FIELDS_LIST)
|
||||
def printShowLabels(users):
|
||||
def _buildLabelTree(labels):
|
||||
def _checkChildLabel(label):
|
||||
if label.find('/') != -1:
|
||||
(parent, base) = label.rsplit('/', 1)
|
||||
labelItemList = label.split('/')
|
||||
i = len(labelItemList)-1
|
||||
while i > 0:
|
||||
parent = '/'.join(labelItemList[:i])
|
||||
base = '/'.join(labelItemList[i:])
|
||||
if parent in labelTree:
|
||||
if label in labelTree:
|
||||
labelTree[label]['info']['base'] = base
|
||||
labelTree[parent]['children'].append(labelTree.pop(label))
|
||||
_checkChildLabel(parent)
|
||||
return
|
||||
i -= 1
|
||||
|
||||
labelTree = {}
|
||||
for label in labels['labels']:
|
||||
|
||||
@@ -11,4 +11,4 @@ lxml
|
||||
passlib>=1.7.2
|
||||
pathvalidate
|
||||
python-dateutil
|
||||
yubikey-manager>=5.0
|
||||
yubikey-manager[yubikey]>=5.0
|
||||
|
||||
Reference in New Issue
Block a user