Compare commits

..

41 Commits
v6.42 ... v6.53

Author SHA1 Message Date
Jay Lee
27461b067a Update var.py 2023-03-31 09:17:10 -04:00
Jay Lee
017712742b Merge branch 'main' of https://github.com/GAM-team/GAM 2023-03-30 17:37:23 +00:00
Jay Lee
afce21a1bd Add steps to trust GAM client_ID 2023-03-30 17:34:59 +00:00
Jay Lee
030e2e270f Another attempt at fixing cryptography 2023-03-30 10:14:24 -04:00
Jay Lee
c69a86b535 use constraints.txt to prevent any downgrades during pip install 2023-03-28 08:49:03 -04:00
Jay Lee
b64e4cf3dc [no ci] that didn't work, we'll try rolling forward... 2023-03-28 08:43:57 -04:00
Jay Lee
a2e06adbbe pin cryptography to 39.0.2 for time being 2023-03-28 08:21:46 -04:00
Jay Lee
43b3397541 Rebuild for cryptography 2023-03-25 10:00:29 -04:00
Jay Lee
bd0bb1542c AppSheet licenses 2023-03-22 12:54:27 +00:00
Jay Lee
a92a07f9c0 Update var.py 2023-03-16 12:35:11 -04:00
Ross Scroggs
42ed5509ee Updates/cleanup (#1614)
* Handle socks error when checking local time offset

* Keep pylint happy

* Avoid trap when authentication flow blows up

* Fix bug that causes trap on create project
2023-03-16 12:31:20 -04:00
Jay Lee
a6582503f2 Update var.py 2023-03-15 18:31:35 -04:00
Jay Lee
7aecb889d2 Allow setting sakey validity_hours 2023-03-15 20:41:25 +00:00
Jay Lee
c273f87cc7 clear cache to pickup OpenSSL 3.1.0 2023-03-14 10:08:02 -04:00
Jay Lee
76d00c993a Update build.yml 2023-03-06 10:13:17 -05:00
Jay Lee
013b47e6e7 Update build.yml 2023-03-06 09:43:50 -05:00
Jay Lee
9f1e9934ff Update build.yml 2023-03-06 09:29:08 -05:00
Jay Lee
48b218bd9c Update build.yml 2023-03-06 08:40:59 -05:00
Jay Lee
af5baa4f3a Update build.yml 2023-03-06 08:37:37 -05:00
Jay Lee
a2cf38d904 Update build.yml 2023-03-06 07:40:01 -05:00
Jay Lee
185522d943 Update build.yml 2023-03-05 15:29:27 -05:00
Jay Lee
a42e4dd080 openssl security level 2 2023-03-05 15:28:02 -05:00
Ross Scroggs
3a5486889f Update chromepolicy.py (#1610)
split of an empty string returns [''], the API wants []
2023-03-04 11:33:22 -05:00
Jay Lee
1a1f100902 completely disalbe TLS 1.0/1.1 2023-03-03 07:09:46 -05:00
Ross Scroggs
c67b214298 Allow user to optionally specify serial number on resetpiv (#1607) 2023-02-27 11:17:20 -05:00
Ross Scroggs
3ad1d5c661 Give error on invalid subargument; handle no YubiKeys in resetpiv (#1606) 2023-02-27 10:37:22 -05:00
Jay Lee
13400d9bde Update build.yml 2023-02-24 14:20:37 -05:00
Ross Scroggs
048e8dfef5 Add gam version checkrc (#1605)
* Add gam version checkrc

* Fix code

* gam version checkrc cleanup
2023-02-24 10:58:23 -05:00
Ross Scroggs
aaf7a89192 Fix documenataion error (#1604) 2023-02-24 09:45:22 -05:00
Jay Lee
e3ee9135ff Update build.yml 2023-02-23 16:30:23 -05:00
Ross Scroggs
a774fc0beb GCP cleanup (#1602) 2023-02-23 11:44:52 -05:00
Jay Lee
f3429bd537 Update build.yml 2023-02-23 08:50:03 -05:00
Jay Lee
37876acfda Update var.py 2023-02-23 08:17:22 -05:00
Jay Lee
2a6dd0d1a2 fix building iamcredentials 2023-02-22 17:30:10 +00:00
Jay Lee
b0626dd37a improve on gam enable apis 2023-02-17 22:07:36 +00:00
Jay Lee
ed0ed8d7fc fix Id 2023-02-17 20:33:47 +00:00
Jay Lee
d67d999930 enable APIs command for signjwt 2023-02-17 20:32:29 +00:00
Jay Lee
ac79cff6b9 create signjwtserviceaccount 2023-02-17 19:39:02 +00:00
Jay Lee
50aadc6ea7 allow forcing OAuth for service account 2023-02-17 15:40:36 +00:00
Jay Lee
9036d114ed signjwt key_type for key-less service account auth 2023-02-17 15:17:01 +00:00
Jay Lee
75c19104ae fix ipv6 with checkconn 2023-02-15 17:22:34 +00:00
9 changed files with 338 additions and 63 deletions

View File

@@ -12,7 +12,7 @@ defaults:
working-directory: src
env:
OPENSSL_CONFIG_OPTS: no-fips
OPENSSL_CONFIG_OPTS: no-fips --api=3.0.0
OPENSSL_INSTALL_PATH: ${{ github.workspace }}/bin/ssl
OPENSSL_SOURCE_PATH: ${{ github.workspace }}/src/openssl
PYTHON_INSTALL_PATH: ${{ github.workspace }}/bin/python
@@ -103,7 +103,7 @@ jobs:
path: |
bin.tar.xz
src/cpython
key: gam-${{ matrix.jid }}-20230208
key: gam-${{ matrix.jid }}-20230326
- name: Untar Cache archive
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'
@@ -144,12 +144,6 @@ jobs:
sudo apt-get -qq --yes update
sudo apt-get -qq --yes install swig libpcsclite-dev
#- name: MacOS remove Homebrew
# if: runner.os == 'macOS'
# run: |
# # remove everything except the libraries needed by yubikey-manager
# brew uninstall $(brew list | grep -v 'pcre\|swig\|pcsc-lite')
- name: MacOS install tools
if: runner.os == 'macOS'
run: |
@@ -175,7 +169,7 @@ jobs:
staticx: ${{ matrix.staticx }}
run: |
echo "We are running on ${RUNNER_OS}"
LD_LIBRARY_PATH="${OPENSSL_INSTALL_PATH}/lib:${PYTHON_INSTALL_PATH}/lib"
LD_LIBRARY_PATH="${OPENSSL_INSTALL_PATH}/lib:${PYTHON_INSTALL_PATH}/lib:/usr/local/lib"
if [[ "${arch}" == "Win64" ]]; then
PYEXTERNALS_PATH="amd64"
PYBUILDRELEASE_ARCH="x64"
@@ -298,7 +292,7 @@ jobs:
rm -rf ${GITHUB_WORKSPACE}/bin/ssl-darwin64-arm64
echo "LDFLAGS=-L${OPENSSL_INSTALL_PATH}/lib" >> $GITHUB_ENV
echo "CRYPTOGRAPHY_SUPPRESS_LINK_FLAGS=1" >> $GITHUB_ENV
echo "CFLAGS=-I${OPENSSL_INSTALL_PATH}/include -arch arm64 -arch x86_64" >> $GITHUB_ENV
echo "CFLAGS=-I${OPENSSL_INSTALL_PATH}/include -arch arm64 -arch x86_64 ${CFLAGS}" >> $GITHUB_ENV
echo "ARCHFLAGS=-arch x86_64 -arch arm64" >> $GITHUB_ENV
else
cd "${GITHUB_WORKSPACE}/src/openssl-${openssl_archs}"
@@ -310,6 +304,7 @@ jobs:
if: matrix.goal == 'build'
run: |
"${OPENSSL_INSTALL_PATH}/bin/openssl" version
"${OPENSSL_INSTALL_PATH}/bin/openssl" version -f
file "${OPENSSL_INSTALL_PATH}/bin/openssl"
- name: Get latest stable Python source
@@ -415,6 +410,7 @@ jobs:
- name: Install pip requirements
run: |
"${PYTHON}" -m pip install --upgrade -r requirements.txt ${PIP_ARGS}
if [[ "${RUNNER_OS}" == "macOS" ]]; then
"${PYTHON}" -m pip install --upgrade cffi ${PIP_ARGS}
"${PYTHON}" -m pip download --only-binary :all: \
@@ -424,8 +420,9 @@ jobs:
--platform macosx_10_15_universal2 \
cryptography
"${PYTHON}" -m pip install --force-reinstall --no-deps cryptography*.whl
else
"${PYTHON}" -m pip install --force-reinstall --no-deps --upgrade cryptography
fi
"${PYTHON}" -m pip install --upgrade -r requirements.txt ${PIP_ARGS}
"${PYTHON}" -m pip list
- name: Install PyInstaller

View File

@@ -911,6 +911,12 @@ gam oauth|oauth2 refresh
gam <UserTypeEntity> check serviceaccount [scope|scopes <APIScopeURLList>]
gam yubikey resetpiv [yubikeyserialnumber <Number>]
gam rotate sakey yubikey yubikey_pin yubikey_slot AUTHENTICATION yubikeyserialnumber <Number>
gam create [gcpserviceaccount|signjwtserviceaccount]
gam enable apis [auto|manual]
gam whatis <EmailItem>
<ResoldCustomerAttribute> ::=

View File

@@ -51,6 +51,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
import gam.auth.oauth
from gam.auth import signjwt
from gam import auth
from gam import controlflow
from gam import display
@@ -588,7 +589,7 @@ def SetGlobalVariables():
GM_Globals[GM_ENABLEDASA_TXT] = os.path.join(
GC_Values[GC_CONFIG_DIR], FN_ENABLEDASA_TXT)
if not GC_Values[GC_NO_UPDATE_CHECK]:
doGAMCheckForUpdates()
doGAMCheckForUpdates(forceCheck=0)
# domain must be set and customer_id must be set and != my_customer when enable_dasa = true
if GC_Values[GC_ENABLE_DASA]:
@@ -635,8 +636,10 @@ TIME_OFFSET_UNITS = [('day', 86400), ('hour', 3600), ('minute', 60),
def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
# If local time is well off, it breaks https because the server certificate
# will be seen as too old or new and thus invalid; http doesn't have that issue.
# Try with http first, if time is close (<MAX_LOCAL_GOOGLE_TIME_OFFSET seconds),
# retry with https
# retry with https as it should be OK
badhttp = transport.create_http()
for prot in ['http', 'https']:
localUTC = datetime.datetime.now(datetime.timezone.utc)
@@ -645,6 +648,12 @@ def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
badhttp.request(f'{prot}://' + testLocation, 'HEAD')[0]['date'])
except (httplib2.ServerNotFoundError, RuntimeError, ValueError) as e:
controlflow.system_error_exit(4, str(e))
except httplib2.socks.HTTPError as e:
# If user has specified an HTTPS proxy, the http request will probably fail as httplib2
# turns a GET into a CONNECT which is not valid for an http address
if prot == 'http':
continue
handleServerError(e)
offset = remainder = int(abs((localUTC - googleUTC).total_seconds()))
if offset < MAX_LOCAL_GOOGLE_TIME_OFFSET and prot == 'http':
continue
@@ -659,7 +668,7 @@ def getLocalGoogleTimeOffset(testLocation='admin.googleapis.com'):
return (offset, nicetime)
def doGAMCheckForUpdates(forceCheck=False):
def doGAMCheckForUpdates(forceCheck=0):
def _gamLatestVersionNotAvailable():
if forceCheck:
@@ -702,6 +711,8 @@ def doGAMCheckForUpdates(forceCheck=False):
print(
f'Version Check:\n Current: {current_version}\n Latest: {latest_version}'
)
if forceCheck < 0:
sys.exit(1 if latest_version > current_version else 0)
if latest_version <= current_version:
fileutils.write_file(GM_Globals[GM_LAST_UPDATE_CHECK_TXT],
str(now_time),
@@ -782,9 +793,9 @@ def checkConnection():
success_count = 0
for host in hosts:
try_count += 1
ip = socket.gethostbyname(host)
ip = socket.getaddrinfo(host, None)[0][-1][0] # works with ipv6
check_line = f'Checking {host} ({ip}) ({try_count}/{host_count})...'
sys.stdout.write(f'{check_line:<80}')
sys.stdout.write(f'{check_line:<100}')
sys.stdout.flush()
try:
httpc.request(f'https://{host}/', 'HEAD', headers=headers)
@@ -821,14 +832,18 @@ def checkConnection():
controlflow.system_error_exit(3, createYellowText('Some hosts failed to connect! Please follow the recommendations for those hosts to correct any issues and try again.'))
def doGAMVersion(checkForArgs=True):
force_check = extended = simple = timeOffset = False
forceCheck = 0
extended = simple = timeOffset = False
testLocation = 'admin.googleapis.com'
if checkForArgs:
i = 2
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'check':
force_check = True
forceCheck = 1
i += 1
elif myarg == 'checkrc':
forceCheck = -1
i += 1
elif myarg == 'simple':
simple = True
@@ -868,8 +883,8 @@ def doGAMVersion(checkForArgs=True):
(testLocation, nicetime))
if offset > MAX_LOCAL_GOOGLE_TIME_OFFSET:
controlflow.system_error_exit(4, 'Please fix your system time.')
if force_check:
doGAMCheckForUpdates(forceCheck=True)
if forceCheck:
doGAMCheckForUpdates(forceCheck)
if extended:
print(ssl.OPENSSL_VERSION)
libs = ['cryptography',
@@ -925,14 +940,12 @@ def _getSvcAcctData():
controlflow.system_error_exit(6, None)
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA] = json.loads(json_string)
jwt_apis = ['chat',
'cloudresourcemanager',
'accesscontextmanager'] # APIs which can handle OAuthless JWT tokens
def getSvcAcctCredentials(scopes, act_as, api=None):
def getSvcAcctCredentials(scopes, act_as, api=None, force_oauth=False):
try:
_getSvcAcctData()
sign_method = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if act_as or api not in jwt_apis:
if act_as or force_oauth:
# DwD means we need to go about things differently...
if sign_method == 'default':
credentials = google.oauth2.service_account.Credentials.from_service_account_info(
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
@@ -940,6 +953,10 @@ def getSvcAcctCredentials(scopes, act_as, api=None):
yksigner = yubikey.YubiKey(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = google.oauth2.service_account.Credentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
elif sign_method == 'signjwt':
sjsigner = signjwt.SignJwt(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = signjwt.Credentials._from_signer_and_info(sjsigner.sign,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = credentials.with_scopes(scopes)
if act_as:
credentials = credentials.with_subject(act_as)
@@ -953,6 +970,11 @@ def getSvcAcctCredentials(scopes, act_as, api=None):
credentials = JWTCredentials._from_signer_and_info(yksigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
audience=audience)
elif sign_method == 'signjwt':
sjsigner = signjwt.SignJwt(GM_Globals[GM_OAUTH2SERVICE_JSON_DATA])
credentials = signjwt.JWTCredentials._from_signer_and_info(sjsigner,
GM_Globals[GM_OAUTH2SERVICE_JSON_DATA],
audience=audience)
credentials.project_id = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA]['project_id']
GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID] = GM_Globals[
GM_OAUTH2SERVICE_JSON_DATA]['client_id']
@@ -1078,9 +1100,10 @@ def getService(api, httpObj):
controlflow.invalid_json_exit(disc_file)
def buildGAPIObject(api):
def buildGAPIObject(api, credentials=None):
GM_Globals[GM_CURRENT_API_USER] = None
credentials = getValidOauth2TxtCredentials(api=getAPIVersion(api)[0])
if not credentials:
credentials = getValidOauth2TxtCredentials(api=getAPIVersion(api)[0])
credentials.user_agent = GAM_INFO
httpObj = transport.AuthorizedHttp(
credentials, transport.create_http(cache=GM_Globals[GM_CACHE_DIR]))
@@ -1295,7 +1318,9 @@ def doCheckServiceAccount(users):
# We are explicitly not doing DwD here, just confirming service account can auth
auth_error = ''
try:
credentials = getSvcAcctCredentials([USERINFO_EMAIL_SCOPE], None)
credentials = getSvcAcctCredentials([USERINFO_EMAIL_SCOPE],
None,
force_oauth=True)
request = transport.create_request()
credentials.refresh(request)
sa_token_info = gapi.call(oa2,
@@ -1315,12 +1340,10 @@ def doCheckServiceAccount(users):
'Invalid private key in oauth2service.json. Please delete the file and then\nrecreate with "gam create project" or "gam use project"'
)
key_type = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('key_type', 'default')
if key_type == 'yubikey':
printPassFail('Skipping age check. YubiKey rotation not necessary.', test_pass)
else:
if key_type == 'default':
print(
'Checking key age. Google recommends rotating keys on a routine basis...'
)
)
try:
iam = buildGAPIServiceObject('iam', None)
project = GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]
@@ -1346,6 +1369,8 @@ def doCheckServiceAccount(users):
key_days = 'UNKNOWN'
print('Unable to check key age, please run "gam update project"')
printPassFail(f'Key is {key_days} days old', key_age_result)
else:
printPassFail(f'Skipping age check. {key_type} rotation not necessary.', test_pass)
if not check_scopes:
for _, scopes in list(API_SCOPE_MAPPING.items()):
for scope in scopes:
@@ -7158,6 +7183,43 @@ def getGAMProjectFile(filepath):
return c.decode(UTF8)
def enable_apis():
a_or_m = None
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower()
if myarg in ['auto', 'manual']:
a_or_m = myarg[0]
i += 1
else:
controlflow.invalid_argument_exit(sys.argv[i],
'gam enable apis')
GAMProjectAPIs = getGAMProjectFile('project-apis.txt').splitlines()
try:
_, projectId = google.auth.default()
except google.auth.exceptions.DefaultCredentialsError as e:
projectId = input('Please enter your project ID: ')
while a_or_m not in ['a', 'm']:
a_or_m = input('Do you want to enable projects [a]utomatically or [m]anually? (a/m): ').strip().lower()
if a_or_m in ['a', 'm']:
break
print('Please enter A or M....')
if a_or_m == 'a':
login_hint = _getValidateLoginHint()
_, httpObj = getCRMService(login_hint)
enableGAMProjectAPIs(GAMProjectAPIs,
httpObj,
projectId=projectId,
checkEnabled=True)
else:
chunk_size = 20
print('Using an account with project access, please use ALL of these URLs to enable 20 APIs at a time:\n\n')
for chunk in range(0, len(GAMProjectAPIs), chunk_size):
apiid = ",".join(GAMProjectAPIs[chunk:chunk+chunk_size])
url = f'https://console.cloud.google.com/apis/enableflow?apiid={apiid}&project={projectId}'
print(f' {url}\n\n')
def enableGAMProjectAPIs(GAMProjectAPIs,
httpObj,
projectId,
@@ -7364,7 +7426,7 @@ def _createClientSecretsOauth2service(httpObj, projectId, login_hint):
while True:
print(f'''Please go to:
{console_url}
{console_url}
1. Choose "Desktop App" or "Other" for "Application type".
2. Enter a desired value for "Name" or leave as is.
@@ -7403,6 +7465,24 @@ def _createClientSecretsOauth2service(httpObj, projectId, login_hint):
fileutils.write_file(GC_Values[GC_CLIENT_SECRETS_JSON],
cs_data,
continue_on_error=False)
print(f'''
Now it's important to mark the GAM Client ID as trusted by your Workspace instance.
1. Please go to:
https://admin.google.com/ac/owl/list?tab=configuredApps
2. Click on: Add app > OAuth App Name Or Client ID.
3. Enter the following Client ID value:
{client_id}
4. Search for the ID, select the GAM app, check the box and press Select.
5. Keep the default scope or select a preferred scope that includes your GAM admin.
6. Press Continue
7. Select Trusted radio button, Continue and Finish.
''')
input('Press Enter when complete.')
print('That\'s it! Your GAM Project is created and ready to use.')
@@ -7743,7 +7823,7 @@ def doUpdateProjects():
_grantRotateRights(iam, sa_email, sa_email)
def _generatePrivateKeyAndPublicCert(client_id, key_size, b64enc_pub=True):
def _generatePrivateKeyAndPublicCert(client_id, key_size, b64enc_pub=True, validity_hours=0):
print(' Generating new private key...')
private_key = rsa.generate_private_key(public_exponent=65537,
key_size=key_size,
@@ -7760,10 +7840,16 @@ def _generatePrivateKeyAndPublicCert(client_id, key_size, b64enc_pub=True):
builder = builder.issuer_name(
x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, client_id)]))
# Gooogle seems to enforce the not before date strictly. Set the not before
# date to be UTC one hour ago should cover any clock skew.
builder = builder.not_valid_before(datetime.datetime.utcnow() - datetime.timedelta(hours=1))
# Google uses 12/31/9999 date for end time
builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
# date to be UTC two minutes ago which should cover any clock skew.
now = datetime.datetime.utcnow()
builder = builder.not_valid_before(now - datetime.timedelta(minutes=2))
# Google defaults to 12/31/9999 date for end time if there's no
# policy to restrict key age
if validity_hours:
expires = now + datetime.timedelta(hours=validity_hours) - datetime.timedelta(minutes=2)
builder = builder.not_valid_after(expires)
else:
builder = builder.not_valid_after(datetime.datetime(9999, 12, 31, 23, 59))
builder = builder.serial_number(x509.random_serial_number())
builder = builder.public_key(public_key)
builder = builder.add_extension(x509.BasicConstraints(ca=False,
@@ -7824,12 +7910,12 @@ def doShowServiceAccountKeys():
else:
controlflow.invalid_argument_exit(myarg, 'gam show sakeys')
name = f'projects/-/serviceAccounts/{GM_Globals[GM_OAUTH2SERVICE_ACCOUNT_CLIENT_ID]}'
currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA][
'private_key_id']
currentPrivateKeyId = GM_Globals[GM_OAUTH2SERVICE_JSON_DATA].get('private_key_id')
keys = gapi.get_items(iam.projects().serviceAccounts().keys(),
'list',
'keys',
name=name,
fields='*',
keyTypes=keyTypes)
if not keys:
print('No keys')
@@ -7844,11 +7930,60 @@ def doShowServiceAccountKeys():
display.print_json(keys)
def getYubiKeySerialNumber(new_data, serial_number):
try:
new_data['yubikey_serial_number'] = int(serial_number)
except ValueError:
controlflow.system_error_exit(
3,
'yubikey_serial_number must be a number')
def doResetYubiKeyPIV():
new_data = {}
i = 3
while i < len(sys.argv):
myarg = sys.argv[i].lower().replace('_', '')
if myarg == 'yubikeyserialnumber':
getYubiKeySerialNumber(new_data, sys.argv[i+1])
i += 2
else:
controlflow.invalid_argument_exit(myarg, 'gam yubikey resetpiv')
yk = yubikey.YubiKey(new_data)
yk.serial_number = yk.get_serial_number()
yk.reset_piv()
def create_signjwt_serviceaccount():
i = 3
if i < len(sys.argv):
controlflow.invalid_argument_exit(sys.argv[i], f'gam create {sys.argv[2]}')
_checkForExistingProjectFiles()
sa_info = {
'type': 'service_account',
'key_type': 'signjwt',
'token_uri': 'https://oauth2.googleapis.com/token'
}
try:
creds, sa_info['project_id'] = google.auth.default()
except google.auth.exceptions.DefaultCredentialsError as e:
controlflow.system_error_exit(2, e)
request = transport.create_request()
creds.refresh(request)
sa_info['client_email'] = creds.service_account_email
oa2 = buildGAPIObjectNoAuthentication('oauth2')
token_info = gapi.call(oa2, 'tokeninfo', access_token=creds.token)
sa_info['client_id'] = token_info['issued_to']
sa_output = json.dumps(sa_info, indent=4, sort_keys=True)
fileutils.write_file(GC_Values[GC_OAUTH2SERVICE_JSON],
sa_output,
continue_on_error=False)
def doCreateOrRotateServiceAccountKeys(iam=None,
project_id=None,
client_email=None,
client_id=None):
local_key_size = 2048
validity_hours = 0
mode = 'retainexisting'
body = {}
if iam:
@@ -7899,12 +8034,10 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
new_data['yubikey_pin'] = input('Enter your YubiKey PIN: ')
i += 1
elif myarg == 'yubikeyserialnumber':
try:
new_data['yubikey_serial_number'] = int(sys.argv[i+1])
except ValueError:
controlflow.system_error_exit(
3,
'yubikey_serial_number must be a number')
getYubiKeySerialNumber(new_data, sys.argv[i+1])
i += 2
elif myarg == 'validityhours':
validity_hours = int(sys.argv[i + 1])
i += 2
elif myarg in ['retainnone', 'retainexisting', 'replacecurrent']:
mode = myarg
@@ -7926,7 +8059,7 @@ def doCreateOrRotateServiceAccountKeys(iam=None,
elif local_key_size:
# Generate private key locally, store in file
new_data['private_key'], publicKeyData = _generatePrivateKeyAndPublicCert(
sa_name, local_key_size)
sa_name, local_key_size, validity_hours=validity_hours)
new_data['key_type'] = 'default'
for key in list(new_data):
if key.startswith('yubikey_'):
@@ -11564,6 +11697,8 @@ def ProcessGAMCommand(args):
gapi_chat.create_message()
elif argument in ['caalevel']:
gapi_caa.create_access_level()
elif argument in ['gcpserviceaccount', 'signjwtserviceaccount']:
create_signjwt_serviceaccount()
else:
controlflow.invalid_argument_exit(argument, 'gam create')
sys.exit(0)
@@ -11802,6 +11937,8 @@ def ProcessGAMCommand(args):
argument = sys.argv[2].lower()
if argument in ['browsertoken', 'browserokens']:
gapi_cbcm.revoketoken()
else:
controlflow.invalid_argument_exit(argument, 'gam revoke')
sys.exit(0)
elif command in ['close', 'reopen']:
# close and reopen will have to be split apart if either takes a new argument
@@ -11966,6 +12103,8 @@ def ProcessGAMCommand(args):
argument = sys.argv[2].lower()
if argument in ['browser', 'browsers']:
gapi_cbcm.move()
else:
controlflow.invalid_argument_exit(argument, 'gam move')
sys.exit(0)
elif command in ['oauth', 'oauth2']:
argument = sys.argv[2].lower()
@@ -12063,6 +12202,8 @@ def ProcessGAMCommand(args):
argument = sys.argv[2].lower()
if argument in ['isinvitable', 'userinvitation', 'userinvitations']:
gapi_cloudidentity_userinvitations.check()
else:
controlflow.invalid_argument_exit(argument, 'gam check')
sys.exit(0)
elif command in ['cancelwipe', 'wipe', 'approve', 'block', 'sync']:
target = sys.argv[2].lower().replace('_', '')
@@ -12082,6 +12223,8 @@ def ProcessGAMCommand(args):
gapi_cloudidentity_devices.approve_user()
elif command == 'block':
gapi_cloudidentity_devices.block_user()
else:
controlflow.invalid_argument_exit(target, f'gam {command}')
sys.exit(0)
elif command in ['issuecommand', 'getcommand']:
target = sys.argv[2].lower().replace('_', '')
@@ -12090,13 +12233,22 @@ def ProcessGAMCommand(args):
gapi_directory_cros.issue_command()
elif command == 'getcommand':
gapi_directory_cros.get_command()
else:
controlflow.invalid_argument_exit(target, f'gam {command}')
sys.exit(0)
elif command in ['yubikey']:
action = sys.argv[2].lower().replace('_', '')
if action == 'resetpiv':
yk = yubikey.YubiKey()
yk.serial_number = yk.get_serial_number()
yk.reset_piv()
doResetYubiKeyPIV()
else:
controlflow.invalid_argument_exit(action, f'gam yubikey')
sys.exit(0)
elif command == 'enable':
enable_what = sys.argv[2].lower().replace('_', '')
if enable_what in ['api', 'apis']:
enable_apis()
else:
controlflow.invalid_argument_exit(enable_what, 'gam enable')
sys.exit(0)
users = getUsersToModify()
command = sys.argv[3].lower()

View File

@@ -5,10 +5,10 @@ import os
from google.auth.jwt import Credentials as JWTCredentials
import gam
from gam import utils
from gam.auth import oauth
from gam.auth import signjwt
from gam.var import _FN_OAUTH2_TXT
from gam.var import _FN_OAUTH2SERVICE_JSON
from gam.var import GC_OAUTH2_TXT
@@ -28,8 +28,7 @@ def get_admin_credentials_filename():
# some custom name in it. Otherwise, just use the default name.
if GC_Values[GC_ENABLE_DASA]:
return GC_Values[GC_OAUTH2SERVICE_JSON] if GC_Values[GC_OAUTH2SERVICE_JSON] else _FN_OAUTH2SERVICE_JSON
else:
return GC_Values[GC_OAUTH2_TXT] if GC_Values[GC_OAUTH2_TXT] else _FN_OAUTH2_TXT
return GC_Values[GC_OAUTH2_TXT] if GC_Values[GC_OAUTH2_TXT] else _FN_OAUTH2_TXT
def get_admin_credentials(api=None):
@@ -40,17 +39,22 @@ def get_admin_credentials(api=None):
with open(credential_file) as f:
creds_data = json.load(f)
# Validate that enable DASA matches content of authorization file
if GC_Values[GC_ENABLE_DASA] and 'private_key_id' in creds_data:
if GC_Values[GC_ENABLE_DASA] and 'key_type' in creds_data:
audience = f'https://{api}.googleapis.com/'
key_type = creds_data.get('key_type', 'default')
if key_type == 'default':
return JWTCredentials.from_service_account_info(creds_data,
audience=audience)
elif key_type == 'yubikey':
if key_type == 'yubikey':
yksigner = yubikey.YubiKey(creds_data)
return JWTCredentials._from_signer_and_info(yksigner,
creds_data,
audience=audience)
if key_type == 'signjwt':
sjsigner = signjwt.SignJwt(creds_data)
return signjwt.JWTCredentials._from_signer_and_info(sjsigner,
creds_data,
audience=audience)
elif not GC_Values[GC_ENABLE_DASA] and 'token' in creds_data:
return oauth.Credentials.from_credentials_file(credential_file)
else:

View File

@@ -50,6 +50,7 @@ MESSAGE_LOCAL_SERVER_SUCCESS = ('The authentication flow has completed. You may'
' close this browser window and return to GAM.')
MESSAGE_AUTHENTICATION_COMPLETE = ('\nThe authentication flow has completed.\n')
MESSAGE_AUTHENTICATION_FAILED = ('\nThe authentication flow failed, reissue command')
class CredentialsError(Exception):
@@ -629,15 +630,22 @@ class _ShortURLFlow(google_auth_oauthlib.flow.InstalledAppFlow):
print(MESSAGE_CONSOLE_AUTHORIZATION_PROMPT.format(url=d['auth_url']))
user_input.start()
userInput = False
while True:
alive = 2
while alive > 0:
sleep(0.1)
if not http_client.is_alive():
user_input.terminate()
break
elif not user_input.is_alive():
if 'code' in d:
user_input.terminate()
break
alive -= 1
if not user_input.is_alive():
userInput = True
http_client.terminate()
break
if 'code' in d:
http_client.terminate()
break
alive -= 1
if 'code' not in d:
controlflow.system_error_exit(8, MESSAGE_AUTHENTICATION_FAILED)
while True:
code = d['code']
if code.startswith('http'):

89
src/gam/auth/signjwt.py Normal file
View File

@@ -0,0 +1,89 @@
''' Use Google Application Default Credentials '''
import datetime
import json
import google.auth
from google.auth._helpers import datetime_to_secs, scopes_to_string, utcnow
import google.oauth2.service_account
import gam
from gam import controlflow
from gam import gapi
from gam import transport
from gam.var import GM_Globals, GM_CACHE_DIR
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
class JWTCredentials(google.auth.jwt.Credentials):
''' Class used for DASA '''
def _make_jwt(self):
now = utcnow()
lifetime = datetime.timedelta(seconds=self._token_lifetime)
expiry = now + lifetime
payload = {
"iss": self._issuer,
"sub": self._subject,
"iat": datetime_to_secs(now),
"exp": datetime_to_secs(expiry),
}
if self._audience:
payload["aud"] = self._audience
payload.update(self._additional_claims)
jwt = self._signer.sign(payload)
return jwt, expiry
class Credentials(google.oauth2.service_account.Credentials):
''' Class used for DwD '''
def _make_authorization_grant_assertion(self):
now = utcnow()
lifetime = datetime.timedelta(seconds=_DEFAULT_TOKEN_LIFETIME_SECS)
expiry = now + lifetime
payload = {
"iat": datetime_to_secs(now),
"exp": datetime_to_secs(expiry),
"iss": self._service_account_email,
"aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
"scope": scopes_to_string(self._scopes or ()),
}
payload.update(self._additional_claims)
# The subject can be a user email for domain-wide delegation.
if self._subject:
payload.setdefault("sub", self._subject)
token = self._signer(payload)
return token
class SignJwt(google.auth.crypt.Signer):
''' Signer class for SignJWT '''
def __init__(self, service_account_info):
self.service_account_email = service_account_info['client_email']
self.name = f'projects/-/serviceAccounts/{self.service_account_email}'
self._key_id = None
@property # type: ignore
def key_id(self):
return self._key_id
def sign(self, message):
''' Call IAM Credentials SignJWT API to get our signed JWT '''
try:
credentials, _ = google.auth.default()
except google.auth.exceptions.DefaultCredentialsError as e:
controlflow.system_error_exit(2, e)
httpObj = transport.AuthorizedHttp(
credentials,
transport.create_http(cache=GM_Globals[GM_CACHE_DIR]))
iamc = gam.getService('iamcredentials', httpObj)
response = gapi.call(iamc.projects().serviceAccounts(),
'signJwt',
name=self.name,
body={'payload': json.dumps(message)})
signed_jwt = response.get('signedJwt')
return signed_jwt

View File

@@ -87,6 +87,9 @@ class YubiKey():
def get_serial_number(self):
try:
devices = list_all_devices()
if not devices:
msg = f'Could not find any YubiKey'
controlflow.system_error_exit(3, msg)
if self.serial_number:
for (device, info) in devices:
if info.serial == self.serial_number:

View File

@@ -408,7 +408,7 @@ def update_policy():
f'{expected_enums}, got {value}'
controlflow.system_error_exit(8, msg)
elif vtype in ['TYPE_LIST']:
value = value.split(',')
value = value.split(',') if value else []
if myarg == 'chrome.users.chromebrowserupdates' and \
cased_field == 'targetVersionPrefixSetting':
mg = re.compile(r'^([a-z]+)-(\d+)$').match(value)

View File

@@ -8,7 +8,7 @@ import platform
import re
GAM_AUTHOR = 'Jay Lee <jay0lee@gmail.com>'
GAM_VERSION = '6.42'
GAM_VERSION = '6.53'
GAM_LICENSE = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)'
GAM_URL = 'https://jaylee.us/gam'
@@ -62,6 +62,21 @@ SKUS = {
'aliases': ['cloudsearch'],
'displayName': 'Google Cloud Search',
},
'1010380001': {
'product': '101038',
'aliases': ['appsheetcore'],
'displayName': 'AppSheet Core',
},
'1010380002': {
'product': '101038',
'aliases': ['appsheetstandard', 'appsheetenterprisestandard'],
'displayName': 'AppSheet Enterprise Standard',
},
'1010380003': {
'product': '101038',
'aliases': ['appsheetplus', 'appsheetenterpriseplus'],
'displayName': 'AppSheet Enterprise Plus',
},
'1010310002': {
'product': '101031',
'aliases': ['gsefe', 'e4e', 'gsuiteenterpriseeducation'],
@@ -300,6 +315,7 @@ PRODUCTID_NAME_MAPPINGS = {
'101035': 'Cloud Search',
'101036': 'Google Meet Global Dialing',
'101037': 'G Suite Workspace for Education',
'101038': 'AppSheet',
'101039': 'Assured Controls',
'101040': 'Beyond Corp',
'Google-Apps': 'Google Workspace',