mirror of
https://github.com/GAM-team/GAM.git
synced 2026-07-04 12:51:36 +00:00
Initial commit of a new experimental modular GAM.
This commit is contained in:
785
src/gam/cmd/oauth.py
Normal file
785
src/gam/cmd/oauth.py
Normal file
@@ -0,0 +1,785 @@
|
||||
"""_getMain().GAM OAuth flows and credential management.
|
||||
|
||||
Extracted from gam/__init__.py. Provides OAuth2 authentication flows,
|
||||
credential creation/deletion/info/update/refresh/export commands.
|
||||
"""
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
import multiprocessing
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import webbrowser
|
||||
import wsgiref.simple_server
|
||||
|
||||
import arrow
|
||||
import google.oauth2.credentials
|
||||
import google_auth_oauthlib.flow
|
||||
|
||||
from filelock import FileLock
|
||||
from urllib.parse import urlparse, parse_qs, urlencode
|
||||
|
||||
from gamlib import glaction
|
||||
from gamlib import glapi as API
|
||||
from gamlib import glcfg as GC
|
||||
from gamlib import glclargs
|
||||
from gamlib import glentity
|
||||
from gamlib import glgapi as GAPI
|
||||
from gamlib import glglobals as GM
|
||||
from gamlib import glindent
|
||||
from gamlib import glmsgs as Msg
|
||||
|
||||
Act = glaction.GamAction()
|
||||
Ent = glentity.GamEntity()
|
||||
Ind = glindent.GamIndent()
|
||||
Cmd = glclargs.GamCLArgs()
|
||||
|
||||
|
||||
def _getMain():
|
||||
return sys.modules['gam']
|
||||
|
||||
def __getattr__(name):
|
||||
"""Fall back to gam module for any undefined names."""
|
||||
main = _getMain()
|
||||
try:
|
||||
return getattr(main, name)
|
||||
except AttributeError:
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
ERROR_PREFIX = 'ERROR: '
|
||||
|
||||
|
||||
VALIDEMAIL_PATTERN = re.compile(r'^[^@]+@[^@]+\.[^@]+$')
|
||||
|
||||
def _getValidateLoginHint(login_hint, projectId=None):
|
||||
while True:
|
||||
if not login_hint:
|
||||
if not projectId:
|
||||
login_hint = _getMain().readStdin(Msg.ENTER_GSUITE_ADMIN_EMAIL_ADDRESS).strip()
|
||||
else:
|
||||
login_hint = _getMain().readStdin(Msg.ENTER_MANAGE_GCP_PROJECT_EMAIL_ADDRESS.format(projectId)).strip()
|
||||
if login_hint.find('@') == -1 and GC.Values[GC.DOMAIN]:
|
||||
login_hint = f'{login_hint}@{GC.Values[GC.DOMAIN]}'
|
||||
if VALIDEMAIL_PATTERN.match(login_hint):
|
||||
return login_hint
|
||||
sys.stdout.write(f'{ERROR_PREFIX}Invalid email address: {login_hint}\n')
|
||||
login_hint = None
|
||||
|
||||
def getOAuthClientIDAndSecret():
|
||||
cs_data = _getMain().readFile(GC.Values[GC.CLIENT_SECRETS_JSON], continueOnError=True, displayError=True)
|
||||
if not cs_data:
|
||||
_getMain().invalidClientSecretsJsonExit(Msg.NO_DATA)
|
||||
try:
|
||||
cs_json = json.loads(cs_data)
|
||||
if not cs_json:
|
||||
_getMain().systemErrorExit(_getMain().CLIENT_SECRETS_JSON_REQUIRED_RC, Msg.NO_CLIENT_ACCESS_CREATE_UPDATE_ALLOWED)
|
||||
return (cs_json['installed']['client_id'], cs_json['installed']['client_secret'])
|
||||
except (IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
||||
_getMain().invalidClientSecretsJsonExit(str(e))
|
||||
|
||||
def getScopesFromUser(scopesList, clientAccess, currentScopes=None):
|
||||
OAUTH2_CMDS = ['s', 'u', 'e', 'c']
|
||||
oauth2_menu = ''
|
||||
numScopes = len(scopesList)
|
||||
for a_scope in scopesList:
|
||||
oauth2_menu += f"[%%s] %2d) {a_scope['name']}"
|
||||
if a_scope.get('subscopes'):
|
||||
oauth2_menu += f' (supports {" and ".join(a_scope["subscopes"])})'
|
||||
oauth2_menu += '\n'
|
||||
oauth2_menu += '''
|
||||
Select an unselected scope [ ] by entering a number; yields [*]
|
||||
For scopes that optionally support readonly, enter a number and an 'r' to grant readonly access; yields [R]
|
||||
For scopes that optionally support actiononly, enter a number and an 'a' to grant actiononly access; yields [A]
|
||||
Clear readonly access [R] or actiononly access [A] from a scope by entering a number; yields [*]
|
||||
Unselect a selected scope [*] by entering a number; yields [ ]
|
||||
Select all default scopes by entering an 's'; yields [*] for default scopes, [ ] for others
|
||||
Unselect all scopes by entering a 'u'; yields [ ] for all scopes
|
||||
Exit without changes/authorization by entering an 'e'
|
||||
Continue to authorization by entering a 'c'
|
||||
'''
|
||||
menu = oauth2_menu % tuple(range(numScopes))
|
||||
selectedScopes = ['*'] * numScopes
|
||||
if currentScopes is None and clientAccess:
|
||||
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK], mode=GC.Values[GC.OAUTH2_TXT_LOCK_MODE])
|
||||
with lock:
|
||||
_, credentials = _getMain().getOauth2TxtCredentials(exitOnError=False)
|
||||
if credentials and credentials.scopes is not None:
|
||||
currentScopes = sorted(credentials.scopes)
|
||||
if currentScopes is not None:
|
||||
if clientAccess:
|
||||
i = 0
|
||||
for a_scope in scopesList:
|
||||
selectedScopes[i] = ' '
|
||||
possibleScope = a_scope['scope']
|
||||
subScopes = a_scope.get('subscopes', [])
|
||||
for currentScope in currentScopes:
|
||||
if currentScope == possibleScope:
|
||||
selectedScopes[i] = '*'
|
||||
break
|
||||
if 'readonly' in subScopes:
|
||||
if currentScope == possibleScope+'.readonly':
|
||||
selectedScopes[i] = 'R'
|
||||
break
|
||||
if 'actiononly' in subScopes:
|
||||
if currentScope == possibleScope+'.action':
|
||||
selectedScopes[i] = 'A'
|
||||
break
|
||||
i += 1
|
||||
else:
|
||||
i = 0
|
||||
for a_scope in scopesList:
|
||||
selectedScopes[i] = ' '
|
||||
api = a_scope['api']
|
||||
possibleScope = a_scope['scope']
|
||||
subScopes = a_scope.get('subscopes', [])
|
||||
if api in currentScopes:
|
||||
if not isinstance(possibleScope, list):
|
||||
for scope in currentScopes[api]:
|
||||
if scope == possibleScope:
|
||||
selectedScopes[i] = '*'
|
||||
break
|
||||
if 'readonly' in subScopes:
|
||||
if (scope == possibleScope+'.readonly') or (scope == a_scope.get('roscope')):
|
||||
selectedScopes[i] = 'R'
|
||||
break
|
||||
else:
|
||||
for scope in possibleScope:
|
||||
if scope not in currentScopes[api]:
|
||||
break
|
||||
else:
|
||||
selectedScopes[i] = '*'
|
||||
i += 1
|
||||
else:
|
||||
i = 0
|
||||
for a_scope in scopesList:
|
||||
if a_scope.get('offByDefault'):
|
||||
selectedScopes[i] = ' '
|
||||
elif a_scope.get('roByDefault'):
|
||||
selectedScopes[i] = 'R'
|
||||
else:
|
||||
selectedScopes[i] = '*'
|
||||
i += 1
|
||||
prompt = f'\nPlease enter 0-{numScopes-1}[a|r] or {"|".join(OAUTH2_CMDS)}: '
|
||||
while True:
|
||||
os.system(['clear', 'cls'][sys.platform.startswith('win')])
|
||||
sys.stdout.write(menu % tuple(selectedScopes))
|
||||
while True:
|
||||
choice = _getMain().readStdin(prompt)
|
||||
if choice:
|
||||
selection = choice.lower()
|
||||
if selection.find('r') >= 0:
|
||||
mode = 'R'
|
||||
selection = selection.replace('r', '')
|
||||
elif selection.find('a') >= 0:
|
||||
mode = 'A'
|
||||
selection = selection.replace('a', '')
|
||||
else:
|
||||
mode = ' '
|
||||
if selection and selection.isdigit():
|
||||
selection = int(selection)
|
||||
if isinstance(selection, int) and selection < numScopes:
|
||||
if mode == 'R':
|
||||
if 'readonly' not in scopesList[selection].get('subscopes',[]):
|
||||
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support readonly mode!\n')
|
||||
continue
|
||||
elif mode == 'A':
|
||||
if 'actiononly' not in scopesList[selection].get('subscopes', []):
|
||||
sys.stdout.write(f'{ERROR_PREFIX}Scope {selection} does not support actiononly mode!\n')
|
||||
continue
|
||||
elif selectedScopes[selection] != '*':
|
||||
mode = '*'
|
||||
else:
|
||||
mode = ' '
|
||||
selectedScopes[selection] = mode
|
||||
break
|
||||
if isinstance(selection, str) and selection in OAUTH2_CMDS:
|
||||
if selection == 's':
|
||||
i = 0
|
||||
for a_scope in scopesList:
|
||||
selectedScopes[i] = ' ' if a_scope.get('offByDefault', False) else '*'
|
||||
i += 1
|
||||
elif selection == 'u':
|
||||
for i in range(numScopes):
|
||||
selectedScopes[i] = ' '
|
||||
elif selection == 'e':
|
||||
return None
|
||||
break
|
||||
sys.stdout.write(f'{ERROR_PREFIX}Invalid input "{choice}"\n')
|
||||
if selection == 'c':
|
||||
if clientAccess:
|
||||
numSelectedScopes = 0
|
||||
i = 0
|
||||
for a_scope in scopesList:
|
||||
if selectedScopes[i] == '*':
|
||||
if a_scope['scope']:
|
||||
numSelectedScopes += 1
|
||||
elif selectedScopes[i] != ' ':
|
||||
numSelectedScopes += 1
|
||||
i += 1
|
||||
if numSelectedScopes <= API.NUM_CLIENT_SCOPES_ERROR_LIMIT:
|
||||
break
|
||||
# If number of scopes is > 48 we'll probably get an error
|
||||
_getMain().writeStdout(Msg.NUM_SELECTED_CLIENT_SCOPES.format(numSelectedScopes, API.NUM_CLIENT_SCOPES_ERROR_LIMIT))
|
||||
choice = _getMain().readStdin('\nPlease enter c to continue to authorization or any other key to amend selection: ')
|
||||
if choice and choice.lower() == 'c':
|
||||
break
|
||||
else:
|
||||
break
|
||||
return selectedScopes
|
||||
|
||||
def _localhost_to_ip():
|
||||
'''returns IPv4 or IPv6 loopback address which localhost resolves to.
|
||||
If localhost does not resolve to valid loopback IP address then returns
|
||||
127.0.0.1'''
|
||||
# TODO gethostbyname() will only ever return ipv4
|
||||
# find a way to support IPv6 here and get preferred IP
|
||||
# note that IPv6 may be broken on some systems also :-(
|
||||
# for now IPv4 should do.
|
||||
local_ip = socket.gethostbyname('localhost')
|
||||
# local_ip = socket.getaddrinfo('localhost', None)[0][-1][0] # works with ipv6, makes wsgiref fail
|
||||
if not ipaddress.ip_address(local_ip).is_loopback:
|
||||
local_ip = '127.0.0.1'
|
||||
return local_ip
|
||||
|
||||
def _waitForHttpClient(d):
|
||||
wsgi_app = google_auth_oauthlib.flow._RedirectWSGIApp(Msg.AUTHENTICATION_FLOW_COMPLETE_CLOSE_BROWSER.format(_getMain().GAM))
|
||||
wsgiref.simple_server.WSGIServer.allow_reuse_address = False
|
||||
# Convert hostname to IP since apparently binding to the IP
|
||||
# reduces odds of firewall blocking us
|
||||
local_ip = _localhost_to_ip()
|
||||
for port in range(8080, 8099):
|
||||
try:
|
||||
local_server = wsgiref.simple_server.make_server(
|
||||
local_ip,
|
||||
port,
|
||||
wsgi_app,
|
||||
handler_class=wsgiref.simple_server.WSGIRequestHandler
|
||||
)
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
redirect_uri_format = "http://{}:{}/" if d['trailing_slash'] else "http://{}:{}"
|
||||
# provide redirect_uri to main process so it can formulate auth_url
|
||||
d['redirect_uri'] = redirect_uri_format.format(*local_server.server_address)
|
||||
# wait until main process provides auth_url
|
||||
# so we can open it in web browser.
|
||||
while 'auth_url' not in d:
|
||||
time.sleep(0.1)
|
||||
if d['open_browser']:
|
||||
webbrowser.open(d['auth_url'], new=1, autoraise=True)
|
||||
try:
|
||||
local_server.handle_request()
|
||||
authorization_response = wsgi_app.last_request_uri.replace("http", "https")
|
||||
d['code'] = authorization_response
|
||||
except:
|
||||
pass
|
||||
local_server.server_close()
|
||||
|
||||
def _waitForUserInput(d):
|
||||
sys.stdin = open(0, _getMain().DEFAULT_FILE_READ_MODE, encoding=_getMain().UTF8)
|
||||
d['code'] = _getMain().readStdin(Msg.ENTER_VERIFICATION_CODE_OR_URL)
|
||||
|
||||
class _GamOauthFlow(google_auth_oauthlib.flow.InstalledAppFlow):
|
||||
def run_dual(self, **kwargs):
|
||||
mgr = multiprocessing.Manager()
|
||||
d = mgr.dict()
|
||||
d['trailing_slash'] = True
|
||||
d['open_browser'] = not GC.Values[GC.NO_BROWSER]
|
||||
httpClientProcess = multiprocessing.Process(target=_waitForHttpClient, args=(d,))
|
||||
userInputProcess = multiprocessing.Process(target=_waitForUserInput, args=(d,))
|
||||
httpClientProcess.start()
|
||||
# we need to wait until web server starts on avail port
|
||||
# so we know redirect_uri to use
|
||||
while 'redirect_uri' not in d:
|
||||
time.sleep(0.1)
|
||||
self.redirect_uri = d['redirect_uri']
|
||||
d['auth_url'], _ = super().authorization_url(**kwargs)
|
||||
d['auth_url'] = _getMain().shortenURL(d['auth_url'])
|
||||
print(Msg.OAUTH2_GO_TO_LINK_MESSAGE.format(url=d['auth_url']))
|
||||
userInputProcess.start()
|
||||
userInput = False
|
||||
checkHttp = checkUser = True
|
||||
alive = 2
|
||||
while alive > 0:
|
||||
time.sleep(0.1)
|
||||
if checkHttp and not httpClientProcess.is_alive():
|
||||
if 'code' in d:
|
||||
if checkUser:
|
||||
userInputProcess.terminate()
|
||||
break
|
||||
checkHttp = False
|
||||
alive -= 1
|
||||
if checkUser and not userInputProcess.is_alive():
|
||||
userInput = True
|
||||
if 'code' in d:
|
||||
if checkHttp:
|
||||
httpClientProcess.terminate()
|
||||
break
|
||||
checkUser = False
|
||||
alive -= 1
|
||||
if 'code' not in d:
|
||||
_getMain().systemErrorExit(_getMain().SYSTEM_ERROR_RC, Msg.AUTHENTICATION_FLOW_FAILED)
|
||||
while True:
|
||||
code = d['code']
|
||||
if code.startswith('http'):
|
||||
parsed_url = urlparse(code)
|
||||
parsed_params = parse_qs(parsed_url.query)
|
||||
code = parsed_params.get('code', [None])[0]
|
||||
try:
|
||||
fetch_args = {'code': code}
|
||||
if GC.Values[GC.CACERTS_PEM]:
|
||||
fetch_args['verify'] = GC.Values[GC.CACERTS_PEM]
|
||||
self.fetch_token(**fetch_args)
|
||||
break
|
||||
except Exception as e:
|
||||
if not userInput:
|
||||
_getMain().systemErrorExit(_getMain().INVALID_TOKEN_RC, str(e))
|
||||
_getMain().stderrErrorMsg(str(e))
|
||||
_waitForUserInput(d)
|
||||
print(Msg.AUTHENTICATION_FLOW_COMPLETE)
|
||||
return self.credentials
|
||||
|
||||
class Credentials(google.oauth2.credentials.Credentials):
|
||||
"""Google OAuth2.0 Credentials with GAM-specific properties and methods."""
|
||||
|
||||
def __init__(self,
|
||||
token,
|
||||
refresh_token=None,
|
||||
id_token=None,
|
||||
token_uri=None,
|
||||
client_id=None,
|
||||
client_secret=None,
|
||||
scopes=None,
|
||||
quota_project_id=None,
|
||||
expiry=None,
|
||||
id_token_data=None,
|
||||
filename=None):
|
||||
"""A thread-safe OAuth2.0 credentials object.
|
||||
|
||||
Credentials adds additional utility properties and methods to a
|
||||
standard OAuth2.0 credentials object. When used to store credentials on
|
||||
disk, it implements a file lock to avoid collision during writes.
|
||||
|
||||
Args:
|
||||
token: Optional String, The OAuth 2.0 access token. Can be None if refresh
|
||||
information is provided.
|
||||
refresh_token: String, The OAuth 2.0 refresh token. If specified,
|
||||
credentials can be refreshed.
|
||||
id_token: String, The Open _getMain().ID Connect _getMain().ID Token.
|
||||
token_uri: String, The OAuth 2.0 authorization server's token endpoint
|
||||
URI. Must be specified for refresh, can be left as None if the token can
|
||||
not be refreshed.
|
||||
client_id: String, The OAuth 2.0 client _getMain().ID. Must be specified for refresh,
|
||||
can be left as None if the token can not be refreshed.
|
||||
client_secret: String, The OAuth 2.0 client secret. Must be specified for
|
||||
refresh, can be left as None if the token can not be refreshed.
|
||||
scopes: Sequence[str], The scopes used to obtain authorization.
|
||||
This parameter is used by :meth:`has_scopes`. OAuth 2.0 credentials can
|
||||
not request additional scopes after authorization. The scopes must be
|
||||
derivable from the refresh token if refresh information is provided
|
||||
(e.g. The refresh token scopes are a superset of this or contain a
|
||||
wild card scope like
|
||||
'https://www.googleapis.com/auth/any-api').
|
||||
quota_project_id: String, The project _getMain().ID used for quota and billing. This
|
||||
project may be different from the project used to create the
|
||||
credentials.
|
||||
expiry: datetime.datetime, The time at which the provided token will
|
||||
expire.
|
||||
id_token_data: Oauth2.0 _getMain().ID Token data which was previously fetched for
|
||||
this access token against the google.oauth2.id_token library.
|
||||
filename: String, Path to a file that will be used to store the
|
||||
credentials. If provided, a lock file of the same name and a ".lock"
|
||||
extension will be created for concurrency controls. Note: New
|
||||
credentials are not saved to disk until write() or refresh() are
|
||||
called.
|
||||
|
||||
Raises:
|
||||
TypeError: If id_token_data is not the required dict type.
|
||||
"""
|
||||
super().__init__(token=token,
|
||||
refresh_token=refresh_token,
|
||||
id_token=id_token,
|
||||
token_uri=token_uri,
|
||||
client_id=client_id,
|
||||
client_secret=client_secret,
|
||||
scopes=scopes,
|
||||
quota_project_id=quota_project_id)
|
||||
|
||||
# Load data not restored by the super class
|
||||
self.expiry = expiry
|
||||
if id_token_data and not isinstance(id_token_data, dict):
|
||||
raise TypeError(f'Expected type id_token_data dict but received {type(id_token_data)}')
|
||||
self._id_token_data = id_token_data.copy() if id_token_data else None
|
||||
|
||||
# If a filename is provided, use a lock file to control concurrent access
|
||||
# to the resource. If no filename is provided, use a thread lock that has
|
||||
# the same interface as FileLock in order to simplify the implementation.
|
||||
if filename:
|
||||
# Convert relative paths into absolute
|
||||
self._filename = os.path.abspath(filename)
|
||||
else:
|
||||
self._filename = None
|
||||
|
||||
# Use a property to prevent external mutation of the filename.
|
||||
@property
|
||||
def filename(self):
|
||||
return self._filename
|
||||
|
||||
@classmethod
|
||||
def from_authorized_user_info_gam(cls, info, filename=None):
|
||||
"""Generates Credentials from _getMain().JSON containing authorized user info.
|
||||
|
||||
Args:
|
||||
info: Dict, authorized user info in Google format.
|
||||
filename: String, the filename used to store these credentials on disk. If
|
||||
no filename is provided, the credentials will not be saved to disk.
|
||||
|
||||
Raises:
|
||||
ValueError: If missing fields are detected in the info.
|
||||
"""
|
||||
# We need all of these keys
|
||||
keys_needed = {'client_id', 'client_secret'}
|
||||
# We need 1 or more of these keys
|
||||
keys_need_one_of = {'refresh_token', 'auth_token', 'token'}
|
||||
missing = keys_needed.difference(info.keys())
|
||||
has_one_of = set(info) & keys_need_one_of
|
||||
if missing or not has_one_of:
|
||||
raise ValueError(
|
||||
'Authorized user info was not in the expected format, missing '
|
||||
f'fields {", ".join(missing)} and one of {", ".join(keys_need_one_of)}.')
|
||||
|
||||
expiry = info.get('token_expiry')
|
||||
if expiry:
|
||||
# Convert the raw expiry to datetime
|
||||
expiry = arrow.Arrow.strptime(expiry, _getMain().YYYYMMDDTHHMMSSZ_FORMAT, tzinfo='UTC').naive
|
||||
id_token_data = info.get('decoded_id_token')
|
||||
|
||||
# Provide backwards compatibility with field names when loading from _getMain().JSON.
|
||||
# Some field names may be different, depending on when/how the credentials
|
||||
# were pickled.
|
||||
return cls(token=info.get('token', info.get('auth_token', '')),
|
||||
refresh_token=info.get('refresh_token', ''),
|
||||
id_token=info.get('id_token_jwt', info.get('id_token')),
|
||||
token_uri=info.get('token_uri'),
|
||||
client_id=info['client_id'],
|
||||
client_secret=info['client_secret'],
|
||||
scopes=info.get('scopes'),
|
||||
quota_project_id=info.get('quota_project_id'),
|
||||
expiry=expiry,
|
||||
id_token_data=id_token_data,
|
||||
filename=filename)
|
||||
|
||||
@classmethod
|
||||
def from_google_oauth2_credentials(cls, credentials, filename=None):
|
||||
"""Generates Credentials from a google.oauth2.Credentials object."""
|
||||
info = json.loads(credentials.to_json())
|
||||
# Add properties which are not exported with the native to_json() output.
|
||||
info['id_token'] = credentials.id_token
|
||||
if credentials.expiry:
|
||||
info['token_expiry'] = credentials.expiry.strftime(_getMain().YYYYMMDDTHHMMSSZ_FORMAT)
|
||||
info['quota_project_id'] = credentials.quota_project_id
|
||||
|
||||
return cls.from_authorized_user_info_gam(info, filename=filename)
|
||||
|
||||
@classmethod
|
||||
def from_client_secrets(cls,
|
||||
client_id,
|
||||
client_secret,
|
||||
scopes,
|
||||
access_type='offline',
|
||||
login_hint=None,
|
||||
filename=None,
|
||||
open_browser=True):
|
||||
"""Runs an OAuth Flow from client secrets to generate credentials.
|
||||
|
||||
Args:
|
||||
client_id: String, The OAuth2.0 Client _getMain().ID.
|
||||
client_secret: String, The OAuth2.0 Client Secret.
|
||||
scopes: Sequence[str], A list of scopes to include in the credentials.
|
||||
access_type: String, 'offline' or 'online'. Indicates whether your
|
||||
application can refresh access tokens when the user is not present at
|
||||
the browser. Valid parameter values are online, which is the default
|
||||
value, and offline. Set the value to offline if your application needs
|
||||
to refresh access tokens when the user is not present at the browser.
|
||||
This is the method of refreshing access tokens described later in this
|
||||
document. This value instructs the Google authorization server to return
|
||||
a refresh token and an access token the first time that your application
|
||||
exchanges an authorization code for tokens.
|
||||
login_hint: String, The email address that will be displayed on the Google
|
||||
login page as a hint for the user to login to the correct account.
|
||||
filename: String, the path to a file to use to save the credentials.
|
||||
open_browser: Boolean: whether or not _getMain().GAM should try to open the browser
|
||||
automatically.
|
||||
|
||||
Returns:
|
||||
Credentials
|
||||
"""
|
||||
client_config = {
|
||||
'installed': {
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'redirect_uris': ['http://localhost'],
|
||||
'auth_uri': API.GOOGLE_OAUTH2_ENDPOINT,
|
||||
'token_uri': API.GOOGLE_OAUTH2_TOKEN_ENDPOINT,
|
||||
}
|
||||
}
|
||||
|
||||
flow = _GamOauthFlow.from_client_config(client_config,
|
||||
scopes,
|
||||
autogenerate_code_verifier=True)
|
||||
flow_kwargs = {'access_type': access_type,
|
||||
'open_browser': open_browser}
|
||||
if login_hint:
|
||||
flow_kwargs['login_hint'] = login_hint
|
||||
flow.run_dual(**flow_kwargs)
|
||||
return cls.from_google_oauth2_credentials(flow.credentials, filename=filename)
|
||||
|
||||
def to_json(self, strip=None):
|
||||
"""Creates a _getMain().JSON representation of a Credentials.
|
||||
|
||||
Args:
|
||||
strip: Sequence[str], Optional list of members to exclude from the
|
||||
generated _getMain().JSON.
|
||||
|
||||
Returns:
|
||||
str: A _getMain().JSON representation of this instance, suitable to pass to
|
||||
from_json().
|
||||
"""
|
||||
expiry = self.expiry.strftime(_getMain().YYYYMMDDTHHMMSSZ_FORMAT) if self.expiry else None
|
||||
prep = {
|
||||
'token': self.token,
|
||||
'refresh_token': self.refresh_token,
|
||||
'token_uri': self.token_uri,
|
||||
'client_id': self.client_id,
|
||||
'client_secret': self.client_secret,
|
||||
'id_token': self.id_token,
|
||||
# Google auth doesn't currently give us scopes back on refresh.
|
||||
# 'scopes': sorted(self.scopes),
|
||||
'token_expiry': expiry,
|
||||
'decoded_id_token': self._id_token_data,
|
||||
}
|
||||
|
||||
# Remove empty entries
|
||||
prep = {k: v for k, v in prep.items() if v is not None}
|
||||
|
||||
# Remove entries that explicitly need to be removed
|
||||
if strip is not None:
|
||||
prep = {k: v for k, v in prep.items() if k not in strip}
|
||||
|
||||
return json.dumps(prep, indent=2, sort_keys=True)
|
||||
|
||||
def doOAuthRequest(currentScopes, login_hint, verifyScopes=False):
|
||||
client_id, client_secret = getOAuthClientIDAndSecret()
|
||||
scopesList = API.getClientScopesList(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS])
|
||||
if not currentScopes or verifyScopes:
|
||||
selectedScopes = getScopesFromUser(scopesList, True, currentScopes)
|
||||
if selectedScopes is None:
|
||||
return False
|
||||
scopes = set(API.REQUIRED_SCOPES)
|
||||
i = 0
|
||||
for scope in scopesList:
|
||||
if selectedScopes[i] == '*':
|
||||
if scope['scope']:
|
||||
if not isinstance(scope['scope'], list):
|
||||
scopes.add(scope['scope'])
|
||||
else:
|
||||
scopes.update(scope['scope'])
|
||||
elif selectedScopes[i] == 'R':
|
||||
scopes.add(f'{scope["scope"]}.readonly')
|
||||
elif selectedScopes[i] == 'A':
|
||||
scopes.add(f'{scope["scope"]}.action')
|
||||
i += 1
|
||||
else:
|
||||
scopes = set(currentScopes+API.REQUIRED_SCOPES)
|
||||
if API.STORAGE_READWRITE_SCOPE in scopes:
|
||||
scopes.discard(API.STORAGE_READONLY_SCOPE)
|
||||
login_hint = _getValidateLoginHint(login_hint)
|
||||
# Needs to be set so oauthlib doesn't puke when Google changes our scopes
|
||||
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = 'true'
|
||||
credentials = Credentials.from_client_secrets(
|
||||
client_id,
|
||||
client_secret,
|
||||
scopes=list(scopes),
|
||||
access_type='offline',
|
||||
login_hint=login_hint,
|
||||
open_browser=not GC.Values[GC.NO_BROWSER])
|
||||
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK], mode=GC.Values[GC.OAUTH2_TXT_LOCK_MODE])
|
||||
with lock:
|
||||
_getMain().writeClientCredentials(credentials, GC.Values[GC.OAUTH2_TXT])
|
||||
_getMain().entityActionPerformed([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]])
|
||||
return True
|
||||
|
||||
# gam oauth|oauth2 create|request [<EmailAddress>]
|
||||
# gam oauth|oauth2 create|request [admin <EmailAddress>] [scope|scopes <APIScopeURLList>]
|
||||
def doOAuthCreate():
|
||||
Cmd = _getMain().Cmd
|
||||
if not Cmd.PeekArgumentPresent(['admin', 'scope', 'scopes']):
|
||||
login_hint = _getMain().getEmailAddress(noUid=True, optional=True)
|
||||
scopes = None
|
||||
_getMain().checkForExtraneousArguments()
|
||||
else:
|
||||
login_hint = None
|
||||
scopes = []
|
||||
scopesList = API.getClientScopesList(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS])
|
||||
while Cmd.ArgumentsRemaining():
|
||||
myarg = _getMain().getArgument()
|
||||
if myarg == 'admin':
|
||||
login_hint = _getMain().getEmailAddress(noUid=True)
|
||||
elif myarg in {'scope', 'scopes'}:
|
||||
for uscope in _getMain().getString(Cmd.OB_API_SCOPE_URL_LIST).lower().replace(',', ' ').split():
|
||||
if uscope in {'openid', 'email', API.USERINFO_EMAIL_SCOPE, 'profile', API.USERINFO_PROFILE_SCOPE}:
|
||||
continue
|
||||
for scope in scopesList:
|
||||
subScopes = scope.get('subscopes', [])
|
||||
if ((uscope == scope['scope']) or
|
||||
(uscope.endswith('.action') and 'actiononly' in subScopes) or
|
||||
(uscope.endswith('.readonly') and 'readonly' in subScopes)):
|
||||
scopes.append(uscope)
|
||||
break
|
||||
else:
|
||||
_getMain().invalidChoiceExit(uscope,
|
||||
API.getClientScopesURLs(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS]),
|
||||
True)
|
||||
else:
|
||||
_getMain().unknownArgumentExit()
|
||||
if len(scopes) == 0:
|
||||
scopes = None
|
||||
doOAuthRequest(scopes, login_hint)
|
||||
|
||||
def exitIfNoOauth2Txt():
|
||||
if not os.path.isfile(GC.Values[GC.OAUTH2_TXT]):
|
||||
_getMain().entityActionNotPerformedWarning([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], Msg.DOES_NOT_EXIST)
|
||||
sys.exit(GM.Globals[GM.SYSEXITRC])
|
||||
|
||||
# gam oauth|oauth2 delete|revoke
|
||||
def doOAuthDelete():
|
||||
_getMain().checkForExtraneousArguments()
|
||||
exitIfNoOauth2Txt()
|
||||
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK], mode=GC.Values[GC.OAUTH2_TXT_LOCK_MODE], timeout=10)
|
||||
with lock:
|
||||
_, credentials = _getMain().getOauth2TxtCredentials(noScopes=True)
|
||||
if not credentials:
|
||||
return
|
||||
entityType = Ent.OAUTH2_TXT_FILE
|
||||
entityName = GC.Values[GC.OAUTH2_TXT]
|
||||
sys.stdout.write(f'{Ent.Singular(entityType)}: {entityName}, will be Deleted in 3...')
|
||||
sys.stdout.flush()
|
||||
time.sleep(1)
|
||||
sys.stdout.write('2...')
|
||||
sys.stdout.flush()
|
||||
time.sleep(1)
|
||||
sys.stdout.write('1...')
|
||||
sys.stdout.flush()
|
||||
time.sleep(1)
|
||||
sys.stdout.write('boom!\n')
|
||||
sys.stdout.flush()
|
||||
httpObj = _getMain().getHttpObj()
|
||||
params = {'token': credentials.refresh_token}
|
||||
revoke_uri = f'https://accounts.google.com/o/oauth2/revoke?{urlencode(params)}'
|
||||
httpObj.request(revoke_uri, 'GET')
|
||||
_getMain().deleteFile(GC.Values[GC.OAUTH2_TXT], continueOnError=True)
|
||||
_getMain().entityActionPerformed([entityType, entityName])
|
||||
|
||||
# gam oauth|oauth2 info|verify [showsecret] [accesstoken <AccessToken> idtoken <IDToken>] [showdetails]
|
||||
def doOAuthInfo():
|
||||
Cmd = _getMain().Cmd
|
||||
Ind = _getMain().Ind
|
||||
credentials = access_token = id_token = None
|
||||
showDetails = showSecret = False
|
||||
while Cmd.ArgumentsRemaining():
|
||||
myarg = _getMain().getArgument()
|
||||
if myarg == 'accesstoken':
|
||||
access_token = _getMain().getString(Cmd.OB_ACCESS_TOKEN)
|
||||
elif myarg == 'idtoken':
|
||||
id_token = _getMain().getString(Cmd.OB_ID_TOKEN)
|
||||
elif myarg == 'showdetails':
|
||||
showDetails = True
|
||||
elif myarg == 'showsecret':
|
||||
showSecret = True
|
||||
else:
|
||||
_getMain().unknownArgumentExit()
|
||||
exitIfNoOauth2Txt()
|
||||
if not access_token and not id_token:
|
||||
credentials = _getMain().getClientCredentials(noScopes=True)
|
||||
access_token = credentials.token
|
||||
_getMain().printEntity([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]])
|
||||
oa2 = _getMain().buildGAPIObject(API.OAUTH2)
|
||||
try:
|
||||
token_info = _getMain().callGAPI(oa2, 'tokeninfo',
|
||||
throwReasons=[GAPI.INVALID],
|
||||
access_token=access_token, id_token=id_token)
|
||||
except GAPI.invalid as e:
|
||||
_getMain().entityActionFailedExit([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], str(e))
|
||||
if 'issued_to' in token_info:
|
||||
_getMain().printKeyValueList(['Client ID', token_info['issued_to']])
|
||||
if credentials is not None and showSecret:
|
||||
_getMain().printKeyValueList(['Secret', credentials.client_secret])
|
||||
if 'scope' in token_info:
|
||||
scopes = token_info['scope'].split(' ')
|
||||
_getMain().printKeyValueList(['Scopes', len(scopes)])
|
||||
Ind.Increment()
|
||||
for scope in sorted(scopes):
|
||||
_getMain().printKeyValueList([scope])
|
||||
Ind.Decrement()
|
||||
if 'email' in token_info:
|
||||
_getMain().printKeyValueList(['Google Workspace Admin', f'{token_info["email"]}'])
|
||||
if 'expires_in' in token_info:
|
||||
_getMain().printKeyValueList(['Expires', _getMain().ISOformatTimeStamp(arrow.now(GC.Values[GC.TIMEZONE]).shift(seconds=token_info['expires_in']))])
|
||||
if showDetails:
|
||||
for k, v in sorted(token_info.items()):
|
||||
if k not in ['email', 'expires_in', 'issued_to', 'scope']:
|
||||
_getMain().printKeyValueList([k, v])
|
||||
_getMain().printBlankLine()
|
||||
|
||||
# gam oauth|oauth2 update [<EmailAddress>]
|
||||
# gam oauth|oauth2 update [admin <EmailAddress>]
|
||||
def doOAuthUpdate():
|
||||
Cmd = _getMain().Cmd
|
||||
if Cmd.PeekArgumentPresent(['admin']):
|
||||
Cmd.Advance()
|
||||
login_hint = _getMain().getEmailAddress(noUid=True)
|
||||
else:
|
||||
login_hint = _getMain().getEmailAddress(noUid=True, optional=True)
|
||||
_getMain().checkForExtraneousArguments()
|
||||
exitIfNoOauth2Txt()
|
||||
lock = FileLock(GM.Globals[GM.OAUTH2_TXT_LOCK], mode=GC.Values[GC.OAUTH2_TXT_LOCK_MODE])
|
||||
with lock:
|
||||
jsonData = _getMain().readFile(GC.Values[GC.OAUTH2_TXT], continueOnError=True, displayError=False)
|
||||
if not jsonData:
|
||||
_getMain().invalidOauth2TxtExit(Msg.NO_DATA)
|
||||
try:
|
||||
jsonDict = json.loads(jsonData)
|
||||
if 'client_id' in jsonDict:
|
||||
if 'scopes' in jsonDict:
|
||||
currentScopes = jsonDict['scopes']
|
||||
else:
|
||||
currentScopes = API.getClientScopesURLs(GC.Values[GC.COMMANDDATA_CLIENTACCESS], GC.Values[GC.TODRIVE_CLIENTACCESS])
|
||||
else:
|
||||
currentScopes = []
|
||||
except (AttributeError, IndexError, KeyError, SyntaxError, TypeError, ValueError) as e:
|
||||
_getMain().invalidOauth2TxtExit(str(e))
|
||||
if not doOAuthRequest(currentScopes, login_hint, verifyScopes=True):
|
||||
_getMain().entityActionNotPerformedWarning([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], Msg.USER_CANCELLED)
|
||||
sys.exit(GM.Globals[GM.SYSEXITRC])
|
||||
|
||||
# gam oauth|oauth2 refresh
|
||||
def doOAuthRefresh():
|
||||
_getMain().checkForExtraneousArguments()
|
||||
exitIfNoOauth2Txt()
|
||||
_getMain().getClientCredentials(forceRefresh=True, forceWrite=True, filename=GC.Values[GC.OAUTH2_TXT], refreshOnly=True)
|
||||
_getMain().entityActionPerformed([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]])
|
||||
|
||||
# gam oauth|oauth2 export [<FileName>]
|
||||
def doOAuthExport():
|
||||
Cmd = _getMain().Cmd
|
||||
if Cmd.ArgumentsRemaining():
|
||||
filename = _getMain().getString(Cmd.OB_FILE_NAME)
|
||||
_getMain().checkForExtraneousArguments()
|
||||
else:
|
||||
filename = GC.Values[GC.OAUTH2_TXT]
|
||||
_getMain().getClientCredentials(forceRefresh=True, forceWrite=True, filename=filename, refreshOnly=True)
|
||||
if filename != '-':
|
||||
_getMain().entityModifierNewValueActionPerformed([Ent.OAUTH2_TXT_FILE, GC.Values[GC.OAUTH2_TXT]], Act.MODIFIER_TO, filename)
|
||||
Reference in New Issue
Block a user