mirror of
https://github.com/GAM-team/GAM.git
synced 2026-07-04 12:51:36 +00:00
Refactor scope selection menu (#882)
-Add flexibility to menu creation and feature customization -Colorize menu error messages on supported platforms -Add 'email' as a required scope for increased transparency to the user -Improve readability of menu creation and operation
This commit is contained in:
580
src/gam.py
580
src/gam.py
@@ -209,6 +209,41 @@ def getCharSet(i):
|
|||||||
return (i, GC_Values.get(GC_CHARSET, GM_Globals[GM_SYS_ENCODING]))
|
return (i, GC_Values.get(GC_CHARSET, GM_Globals[GM_SYS_ENCODING]))
|
||||||
return (i+2, sys.argv[i+1])
|
return (i+2, sys.argv[i+1])
|
||||||
|
|
||||||
|
def supportsColoredText():
|
||||||
|
"""Determines if the current terminal environment supports colored text.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Bool, True if the current terminal environment supports colored text via
|
||||||
|
ANSI escape characters.
|
||||||
|
"""
|
||||||
|
# Make a rudimentary check for Windows. Though Windows does seem to support
|
||||||
|
# colorization with VT100 emulation, it is disabled by default. Therefore,
|
||||||
|
# we'll simply disable it in GAM on Windows for now.
|
||||||
|
return not GM_Globals[GM_WINDOWS]
|
||||||
|
|
||||||
|
def createColoredText(text, color):
|
||||||
|
"""Uses ANSI escape characters to create colored text in supported terminals.
|
||||||
|
|
||||||
|
See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text: String, The text to colorize using ANSI escape characters.
|
||||||
|
color: String, An ANSI escape sequence denoting the color of the text to be
|
||||||
|
created. See more at https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The input text with appropriate ANSI escape characters to create
|
||||||
|
colorization in a supported terminal environment.
|
||||||
|
"""
|
||||||
|
END_COLOR_SEQUENCE = '\033[0m' # Ends the applied color formatting
|
||||||
|
if supportsColoredText():
|
||||||
|
return color + text + END_COLOR_SEQUENCE
|
||||||
|
return text # Hand back the plain text, uncolorized.
|
||||||
|
|
||||||
|
def createRedText(text):
|
||||||
|
"""Uses ANSI encoding to create red colored text, if supported."""
|
||||||
|
return createColoredText(text, '\033[91m')
|
||||||
|
|
||||||
COLORHEX_PATTERN = re.compile(r'^#[0-9a-fA-F]{6}$')
|
COLORHEX_PATTERN = re.compile(r'^#[0-9a-fA-F]{6}$')
|
||||||
|
|
||||||
def getColor(color):
|
def getColor(color):
|
||||||
@@ -12626,111 +12661,480 @@ OAUTH2_SCOPES = [
|
|||||||
{u'name': u'Cloud Storage (Vault Export - read only)',
|
{u'name': u'Cloud Storage (Vault Export - read only)',
|
||||||
u'subscopes': [],
|
u'subscopes': [],
|
||||||
u'scopes': u'https://www.googleapis.com/auth/devstorage.read_only'},
|
u'scopes': u'https://www.googleapis.com/auth/devstorage.read_only'},
|
||||||
|
{u'name': u'User Profile (Email address - read only)',
|
||||||
|
u'subscopes': [],
|
||||||
|
u'scopes': u'email',
|
||||||
|
u'required': True},
|
||||||
]
|
]
|
||||||
|
|
||||||
OAUTH2_MENU = u'''
|
def getScopesFromUser(menu_options=None):
|
||||||
|
"""Prompts the user to choose from a list of scopes to authorize.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
menu_options: An optional list of ScopeMenuOptions to be presented in the
|
||||||
|
menu. If no menu_options are provided, menu options will be generated
|
||||||
|
from static OAUTH2_SCOPES definitions.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of user-selected scopes to authorize.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not menu_options:
|
||||||
|
menu_options = [ScopeMenuOption.from_gam_oauth2scope(definition)
|
||||||
|
for definition in OAUTH2_SCOPES]
|
||||||
|
menu = ScopeSelectionMenu(menu_options)
|
||||||
|
try:
|
||||||
|
menu.run()
|
||||||
|
except ScopeSelectionMenu.UserRequestedExitException:
|
||||||
|
systemErrorExit(0, '')
|
||||||
|
|
||||||
|
return menu.get_selected_scopes()
|
||||||
|
|
||||||
|
class ScopeMenuOption():
|
||||||
|
"""A single GAM API/feature with scopes that can be turned on/off."""
|
||||||
|
|
||||||
|
def __init__(self, oauth_scopes, description,
|
||||||
|
is_required=False,
|
||||||
|
is_selected=False,
|
||||||
|
supported_restrictions=None,
|
||||||
|
restriction=None):
|
||||||
|
"""A data structure for storing and toggling feature/API scope attributes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
oauth_scopes: A list of Google OAuth scope strings required for the
|
||||||
|
feature or API. If the applicable scopes can vary in permission level,
|
||||||
|
the scopes provided in this list should contain the highest level of
|
||||||
|
permissions. More restrictive scopes are implemented by utilizing the
|
||||||
|
`supported_restrictions` argument.
|
||||||
|
description: String, a name or brief description of this API/feature.
|
||||||
|
is_required: Bool, whether this API/feature is required for GAM. If True,
|
||||||
|
the ScopeMenuOption cannot be unselected/disabled.
|
||||||
|
is_selected: Bool, whether the ScopeMenuOption is currently
|
||||||
|
selected/enabled.
|
||||||
|
supported_restrictions: A list of strings that can be appended to the
|
||||||
|
oauth_scopes, separated by '.', to restrict their permissions.
|
||||||
|
For example, the directory API supports a 'readonly' mode on most
|
||||||
|
scopes such that
|
||||||
|
https://www.googleapis.com/auth/admin.directory.domain
|
||||||
|
becomes
|
||||||
|
https://www.googleapis.com/auth/admin.directory.domain.readonly
|
||||||
|
restriction: String, the currently enabled restriction on all scopes in
|
||||||
|
this ScopeMenuOption. Default is no restrictions (highest permission).
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.scopes = oauth_scopes
|
||||||
|
self.description = description
|
||||||
|
self.is_required = is_required
|
||||||
|
# Required scopes must be selected
|
||||||
|
self.is_selected = is_required or is_selected
|
||||||
|
self.supported_restrictions = (
|
||||||
|
supported_restrictions if supported_restrictions is not None else [])
|
||||||
|
self._restriction = restriction
|
||||||
|
|
||||||
|
def select(self, restriction=None):
|
||||||
|
"""Selects/enables the ScopeMenuOption with an optional restriction."""
|
||||||
|
if restriction is not None:
|
||||||
|
self.restrict_to(restriction)
|
||||||
|
self.is_selected = True
|
||||||
|
|
||||||
|
def unselect(self):
|
||||||
|
"""Unselects/disables the ScopeMenuOption."""
|
||||||
|
self.is_selected = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_selected(self):
|
||||||
|
return self._is_selected
|
||||||
|
|
||||||
|
@is_selected.setter
|
||||||
|
def is_selected(self, is_selected):
|
||||||
|
if self.is_required and not is_selected:
|
||||||
|
raise ValueError('Required scope cannot be unselected')
|
||||||
|
if not is_selected:
|
||||||
|
# Disable all applied restrictions
|
||||||
|
self.unrestrict()
|
||||||
|
self._is_selected = is_selected
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_restricted(self):
|
||||||
|
return self._restriction is not None
|
||||||
|
|
||||||
|
def supports_restriction(self, restriction):
|
||||||
|
"""Determines if a scope restriction is supported by this ScopeMenuOption.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
restriction: String, the text appended to a full permission scope which
|
||||||
|
will restrict its permissiveness. e.g. 'readonly' or 'action'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Bool, True if the scope restriction can be applied to this option.
|
||||||
|
"""
|
||||||
|
return restriction in self.supported_restrictions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def restriction(self):
|
||||||
|
return self._restriction
|
||||||
|
|
||||||
|
@restriction.setter
|
||||||
|
def restriction(self, restriction):
|
||||||
|
self.restrict_to(restriction)
|
||||||
|
|
||||||
|
def restrict_to(self, restriction):
|
||||||
|
"""Applies a scope restriction to all scopes associated with this option.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
restriction: String, a scope restriction which is appended to the
|
||||||
|
full-permission scope with a leading '.'. e.g. if the full scope is
|
||||||
|
https://www.googleapis.com/auth/admin.directory.domain
|
||||||
|
providing 'readonly' here will make the effective scope
|
||||||
|
https://www.googleapis.com/auth/admin.directory.domain.readonly
|
||||||
|
"""
|
||||||
|
if self.supports_restriction(restriction):
|
||||||
|
self._restriction = restriction
|
||||||
|
else:
|
||||||
|
error = 'Scope does not support a %s restriction.' % restriction
|
||||||
|
if self.supported_restrictions is not None:
|
||||||
|
restriction_list = ', '.join(self.supported_restrictions)
|
||||||
|
error.append(' Supported restrictions are: %s' % restriction_list)
|
||||||
|
raise ValueError(error)
|
||||||
|
|
||||||
|
def unrestrict(self):
|
||||||
|
"""Removes all scope restrictions currently applied."""
|
||||||
|
self._restriction = None
|
||||||
|
|
||||||
|
def get_effective_scopes(self):
|
||||||
|
"""Gets all scopes for this option, including their restrictions.
|
||||||
|
|
||||||
|
Restrictions are applied in the form of trailing text which limit the
|
||||||
|
scope's capabilities.
|
||||||
|
"""
|
||||||
|
effective_scopes = []
|
||||||
|
for scope in self.scopes:
|
||||||
|
if self.is_restricted:
|
||||||
|
scope = '%s.%s' % (scope, self._restriction)
|
||||||
|
effective_scopes.append(scope)
|
||||||
|
return effective_scopes
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_gam_oauth2scope(cls, scope_definition):
|
||||||
|
"""Generates a ScopeMenuOption from a dict-style OAUTH2_SCOPES definition.
|
||||||
|
|
||||||
|
Dict fields:
|
||||||
|
name: Some description of the API/feature.
|
||||||
|
subscopes: A list of compatible scope restrictions such as 'action' or
|
||||||
|
'readonly'. Each scope in the scopes list must support this
|
||||||
|
restriction text appended to the end of its normal scope text.
|
||||||
|
scopes: A list of scopes that are required for the API/feature.
|
||||||
|
offByDefault: A bool indicating whether this feature/scope should be off
|
||||||
|
by default (when no prior selection has been made). Default is False
|
||||||
|
(the item will be on/selected by default).
|
||||||
|
required: A bool indicating the API/feature is required. This scope
|
||||||
|
cannot be unselected. Default is False.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
{
|
||||||
|
'name': 'Made up API',
|
||||||
|
'subscopes': ['action'],
|
||||||
|
'scopes': ['https://www.googleapis.com/auth/some.scope'],
|
||||||
|
}
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scope_definition: A dict following the syntax of scopes defined in
|
||||||
|
OAUTH2_SCOPES.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A ScopeMenuOption object.
|
||||||
|
"""
|
||||||
|
scopes = scope_definition.get('scopes', [])
|
||||||
|
# If the scope is a single string, make it into a list.
|
||||||
|
scope_list = scopes if isinstance(scopes, list) else [scopes]
|
||||||
|
return cls(
|
||||||
|
oauth_scopes=scope_list,
|
||||||
|
description=scope_definition.get('name'),
|
||||||
|
is_selected=not scope_definition.get('offByDefault'),
|
||||||
|
supported_restrictions=scope_definition.get('subscopes', []),
|
||||||
|
is_required=scope_definition.get('required', False))
|
||||||
|
|
||||||
|
class ScopeSelectionMenu():
|
||||||
|
"""A text menu which prompts the user to select the scopes to authorize."""
|
||||||
|
|
||||||
|
class MenuChoiceError(Exception):
|
||||||
|
"""Error when an invalid or incompatible user choice is made."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UserRequestedExitException(Exception):
|
||||||
|
"""Exception when the user requests immediate exit."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __init__(self, options):
|
||||||
|
"""A menu of scope options to choose from.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
options: A list of ScopeMenuOption objects from which to generate the menu
|
||||||
|
Options will be presented on screen in the same order as the provided
|
||||||
|
list.
|
||||||
|
""""
|
||||||
|
self._options = options
|
||||||
|
|
||||||
|
def get_options(self):
|
||||||
|
"""Returns all options that are available on this menu."""
|
||||||
|
return self._options
|
||||||
|
|
||||||
|
def get_selected_options(self):
|
||||||
|
"""Returns all currently selected ScopeMenuOptions."""
|
||||||
|
return [option for option in self._options if option.is_selected]
|
||||||
|
|
||||||
|
def get_selected_scopes(self):
|
||||||
|
"""Returns the aggregate set of oauth scopes currently selected."""
|
||||||
|
selected_scopes = [scope for option in self.get_selected_options()
|
||||||
|
for scope in option.get_effective_scopes()]
|
||||||
|
return set(selected_scopes)
|
||||||
|
|
||||||
|
MENU_CHOICE = {
|
||||||
|
'SELECT_ALL_SCOPES': 's',
|
||||||
|
'UNSELECT_ALL_SCOPES': 'u',
|
||||||
|
'EXIT': 'e',
|
||||||
|
'CONTINUE': 'c'
|
||||||
|
}
|
||||||
|
|
||||||
|
_MENU_DISPLAY_TEXT = '''
|
||||||
Select the authorized scopes by entering a number.
|
Select the authorized scopes by entering a number.
|
||||||
Append an 'r' to grant read-only access or an 'a' to grant action-only access.
|
Append an 'r' to grant read-only access or an 'a' to grant action-only access.
|
||||||
|
|
||||||
'''
|
%s
|
||||||
for a_scope in OAUTH2_SCOPES:
|
|
||||||
OAUTH2_MENU += u'[%%%%s] %%2d) %s' % (a_scope[u'name'])
|
|
||||||
if a_scope[u'subscopes']:
|
|
||||||
OAUTH2_MENU += u' (supports %s)' % (u' and '.join(a_scope[u'subscopes']))
|
|
||||||
OAUTH2_MENU += '\n'
|
|
||||||
OAUTH2_MENU += '''
|
|
||||||
|
|
||||||
s) Select all scopes
|
s) Select all scopes
|
||||||
u) Unselect all scopes
|
u) Unselect all scopes
|
||||||
e) Exit without changes
|
e) Exit without changes
|
||||||
c) Continue to authorization
|
c) Continue to authorization
|
||||||
|
|
||||||
'''
|
'''
|
||||||
OAUTH2_CMDS = [u's', u'u', u'e', u'c']
|
|
||||||
MAXIMUM_SCOPES = 48 # max of 50 - 2 for email scope always included
|
|
||||||
|
|
||||||
def getScopesFromUser():
|
def get_menu_text(self):
|
||||||
"""Prompts the user to choose from a list of scopes to authorize."""
|
"""Returns a text menu with numbered options."""
|
||||||
def _checkMakeScopesList(scopes):
|
scope_menu_items = [
|
||||||
del scopes[:]
|
self._build_scope_menu_item(option, counter)
|
||||||
for i in range(num_scopes):
|
for counter, option in enumerate(self._options)
|
||||||
if selected_scopes[i] == u'*':
|
]
|
||||||
if not isinstance(OAUTH2_SCOPES[i][u'scopes'], list):
|
return ScopeSelectionMenu._MENU_DISPLAY_TEXT % '\n'.join(scope_menu_items)
|
||||||
scopes.append(OAUTH2_SCOPES[i][u'scopes'])
|
|
||||||
else:
|
|
||||||
scopes += OAUTH2_SCOPES[i][u'scopes']
|
|
||||||
elif selected_scopes[i] == u'R':
|
|
||||||
scopes.append(u'%s.readonly' % OAUTH2_SCOPES[i][u'scopes'])
|
|
||||||
elif selected_scopes[i] == u'A':
|
|
||||||
scopes.append(u'%s.action' % OAUTH2_SCOPES[i][u'scopes'])
|
|
||||||
if len(scopes) > MAXIMUM_SCOPES:
|
|
||||||
return (False, u'ERROR: {0} scopes selected, maximum is {1}, please unselect some.\n'.format(len(scopes), MAXIMUM_SCOPES))
|
|
||||||
if len(scopes) == 0:
|
|
||||||
return (False, u'ERROR: No scopes selected, please select at least one.\n')
|
|
||||||
scopes.insert(0, u'email') # Email Display Scope, always included
|
|
||||||
return (True, u'')
|
|
||||||
|
|
||||||
num_scopes = len(OAUTH2_SCOPES)
|
def _build_scope_menu_item(self, scope_option, option_number):
|
||||||
menu = OAUTH2_MENU % tuple(range(num_scopes))
|
"""Builds a text line representing a single scope selection in the menu.
|
||||||
selected_scopes = []
|
|
||||||
for scope in OAUTH2_SCOPES:
|
The returned line is in the format:
|
||||||
if scope.get(u'offByDefault', False):
|
|
||||||
selected_scopes.append(u' ')
|
[<*>] <##>) <Feature description> (<supported scope modifiers>) [required]
|
||||||
|
|
||||||
|
* = A single character indicating the option's selection status.
|
||||||
|
'*' - All scopes are selected with full permissions.
|
||||||
|
' ' - No scopes are selected.
|
||||||
|
'X' - Where 'X' is the first letter of the selected scope restriction,
|
||||||
|
such as 'R' for readonly or 'A' for action, indicates scopes are
|
||||||
|
selected with the corresponding restriction.
|
||||||
|
## = The line item number associated to this option.
|
||||||
|
Feature description = The ScopeMenuOption description.
|
||||||
|
scope modifiers = The supported scope restrictions for the ScopeMenuOption.
|
||||||
|
When appended to the unrestricted, full oauth scope, these strings
|
||||||
|
modify or restrict the access level of the given scope.
|
||||||
|
[required] = Will only appear if the ScopeMenuOption is required and cannot
|
||||||
|
be unselected.
|
||||||
|
|
||||||
|
e.g. [*] 2) Directory API - Domains (supports 'readonly')
|
||||||
|
|
||||||
|
Args:
|
||||||
|
scope_option: The ScopeMenuOption associated with this line item.
|
||||||
|
option_number: The selectable option number that is associated with
|
||||||
|
modifying this line item.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A string containing the line item text without a trailing newline.
|
||||||
|
"""
|
||||||
|
SELECTION_INDICATOR = {
|
||||||
|
'ALL_SELECTED': '*',
|
||||||
|
'UNSELECTED': ' ',
|
||||||
|
}
|
||||||
|
indicator = SELECTION_INDICATOR['UNSELECTED']
|
||||||
|
if scope_option.is_selected:
|
||||||
|
if scope_option.is_restricted:
|
||||||
|
# Use the first letter of the restriction as the indicator.
|
||||||
|
indicator = scope_option.restriction[0].upper()
|
||||||
else:
|
else:
|
||||||
selected_scopes.append(u'*')
|
indicator = SELECTION_INDICATOR['ALL_SELECTED']
|
||||||
scopes = []
|
|
||||||
prompt = u'Please enter 0-{0}[a|r] or {1}: '.format(num_scopes-1, u'|'.join(OAUTH2_CMDS))
|
item_description = [
|
||||||
message = u''
|
'[%s]' % indicator,
|
||||||
|
'%2d)' % option_number,
|
||||||
|
scope_option.description,
|
||||||
|
]
|
||||||
|
|
||||||
|
if scope_option.supported_restrictions:
|
||||||
|
item_description.append(
|
||||||
|
'(supports %s)' % ' and '.join(scope_option.supported_restrictions))
|
||||||
|
|
||||||
|
if scope_option.is_required:
|
||||||
|
item_description.append('[required]')
|
||||||
|
|
||||||
|
return ' '.join(item_description)
|
||||||
|
|
||||||
|
def get_prompt_text(self):
|
||||||
|
"""Builds and returns a prompt requesting user input."""
|
||||||
|
# Get all the available restrictions and create the list of available
|
||||||
|
# commands that the user can input.
|
||||||
|
restrictions = set([
|
||||||
|
restriction for option in self._options
|
||||||
|
for restriction in option.supported_restrictions
|
||||||
|
])
|
||||||
|
restriction_choices = [
|
||||||
|
restriction[0].lower() for restriction in restrictions
|
||||||
|
]
|
||||||
|
return ('Please enter 0-%d[%s] or %s: ' %
|
||||||
|
(len(self._options)-1, # Keep the menu options 0-based
|
||||||
|
'|'.join(restriction_choices),
|
||||||
|
'|'.join(ScopeSelectionMenu.MENU_CHOICE.values())))
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""Displays the ScopeSelectionMenu to the user and prompts for input.
|
||||||
|
|
||||||
|
The menu will continue to display until the user finishes adjusting all
|
||||||
|
desired items and requests to return.
|
||||||
|
|
||||||
|
After the menu is run, callers may use `get_selected_scopes()` or
|
||||||
|
`get_selected_options()` methods to understand what the user's final choice
|
||||||
|
was.
|
||||||
|
|
||||||
|
Raises: ScopeSelectionMenu.UserRequestedExitException if the user chooses
|
||||||
|
to exit the application entirely, rather than continue execution. This
|
||||||
|
allows callers to decide how to handle the exit.
|
||||||
|
"""
|
||||||
|
error_message = None
|
||||||
while True:
|
while True:
|
||||||
os.system([u'clear', u'cls'][GM_Globals[GM_WINDOWS]])
|
os.system(['clear', 'cls'][GM_Globals[GM_WINDOWS]])
|
||||||
if message:
|
sys.stdout.write(self.get_menu_text())
|
||||||
sys.stdout.write(message)
|
if error_message is not None:
|
||||||
message = u''
|
colored_error = createRedText(ERROR_PREFIX + error_message + '\n')
|
||||||
sys.stdout.write(menu % tuple(selected_scopes))
|
sys.stdout.write(colored_error)
|
||||||
while True:
|
error_message = None # Clear the pending error message
|
||||||
choice = raw_input(prompt)
|
|
||||||
if choice:
|
user_input = raw_input(self.get_prompt_text())
|
||||||
selection = choice.lower()
|
try:
|
||||||
if selection.find(u'r') >= 0:
|
prompt_again = self._process_menu_input(user_input)
|
||||||
mode = u'R'
|
if not prompt_again:
|
||||||
selection = selection.replace(u'r', u'')
|
return
|
||||||
elif selection.find(u'a') >= 0:
|
except ScopeSelectionMenu.MenuChoiceError as e:
|
||||||
mode = u'A'
|
error_message = e.message
|
||||||
selection = selection.replace(u'a', u'')
|
|
||||||
|
_SINGLE_SCOPE_CHANGE_REGEX = re.compile(
|
||||||
|
'\s*(?P<scope_number>\d{1,2})\s*(?P<restriction>[a-z]?)', re.IGNORECASE)
|
||||||
|
|
||||||
|
# Google-defined maximum number of scopes that can be authorized on a single
|
||||||
|
# access token.
|
||||||
|
MAXIMUM_NUM_SCOPES = 50
|
||||||
|
|
||||||
|
def _process_menu_input(self, raw_menu_input):
|
||||||
|
"""Processes the raw user input provided to the menu prompt.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
raw_menu_input: The raw, unaltered string provided by the user in response
|
||||||
|
to the menu prompt.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True, if the user should be prompted for further input. False, if the
|
||||||
|
user has finished input and requested to continue execution of the
|
||||||
|
program.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ScopeSelectionMenu.UserRequestedExitException if the user requests to exit
|
||||||
|
the application immediately.
|
||||||
|
ScopeSelectionMenu.MenuChoiceError upon invalid user input.
|
||||||
|
"""
|
||||||
|
user_input = raw_menu_input.lower().strip()
|
||||||
|
single_scope_change = (
|
||||||
|
ScopeSelectionMenu._SINGLE_SCOPE_CHANGE_REGEX.match(user_input))
|
||||||
|
|
||||||
|
if single_scope_change:
|
||||||
|
scope_number, restriction_command = single_scope_change.group(
|
||||||
|
'scope_number', 'restriction')
|
||||||
|
# Make sure we get an actual number to deal with.
|
||||||
|
scope_number = int(scope_number)
|
||||||
|
# Scope option numbers displayed in the menu are 0-based and map directly
|
||||||
|
# to the indices in the list of scopes.
|
||||||
|
if scope_number < 0 or scope_number > len(self._options) - 1:
|
||||||
|
raise ScopeSelectionMenu.MenuChoiceError(
|
||||||
|
'Invalid scope number "%d"' % scope_number)
|
||||||
|
selected_option = self._options[scope_number]
|
||||||
|
|
||||||
|
# Find the restriction that the user intended to apply.
|
||||||
|
if restriction_command != '':
|
||||||
|
matching_restrictions = filter(
|
||||||
|
lambda r: r.startswith(restriction_command),
|
||||||
|
selected_option.supported_restrictions)
|
||||||
|
if len(matching_restrictions) < 1:
|
||||||
|
raise ScopeSelectionMenu.MenuChoiceError(
|
||||||
|
'Scope "%s" does not support "%s" mode!' % (
|
||||||
|
selected_option.description, restriction_command))
|
||||||
|
restriction = matching_restrictions[0]
|
||||||
else:
|
else:
|
||||||
mode = u' '
|
restriction = None
|
||||||
if selection and selection.isdigit():
|
self._update_option(selected_option, restriction=restriction)
|
||||||
selection = int(selection)
|
|
||||||
if isinstance(selection, int) and selection < num_scopes:
|
elif user_input == ScopeSelectionMenu.MENU_CHOICE['SELECT_ALL_SCOPES']:
|
||||||
if mode == u'R':
|
for option in self._options:
|
||||||
if u'readonly' not in OAUTH2_SCOPES[selection][u'subscopes']:
|
self._update_option(option, selected=True)
|
||||||
sys.stdout.write(u'{0}Scope {1} does not support read-only mode!\n'.format(ERROR_PREFIX, selection))
|
elif user_input == ScopeSelectionMenu.MENU_CHOICE['UNSELECT_ALL_SCOPES']:
|
||||||
continue
|
for option in self._options:
|
||||||
elif mode == u'A':
|
# Force-select required options
|
||||||
if u'action' not in OAUTH2_SCOPES[selection][u'subscopes']:
|
self._update_option(option, selected=option.is_required)
|
||||||
sys.stdout.write(u'{0}Scope {1} does not support action-only mode!\n'.format(ERROR_PREFIX, selection))
|
elif user_input == ScopeSelectionMenu.MENU_CHOICE['CONTINUE']:
|
||||||
continue
|
return False
|
||||||
elif selected_scopes[selection] != u'*':
|
elif user_input == ScopeSelectionMenu.MENU_CHOICE['EXIT']:
|
||||||
mode = u'*'
|
raise ScopeSelectionMenu.UserRequestedExitException()
|
||||||
else:
|
else:
|
||||||
mode = u' '
|
raise ScopeSelectionMenu.MenuChoiceError(
|
||||||
selected_scopes[selection] = mode
|
'Invalid input "%s"' % user_input)
|
||||||
break
|
|
||||||
elif isinstance(selection, str) and selection in OAUTH2_CMDS:
|
return True
|
||||||
if selection == u's':
|
|
||||||
for i in range(num_scopes):
|
def _update_option(self, option, selected=None, restriction=None):
|
||||||
selected_scopes[i] = u'*'
|
"""Validates changes and updates the internal state of options on the menu.
|
||||||
elif selection == u'u':
|
|
||||||
for i in range(num_scopes):
|
Args:
|
||||||
selected_scopes[i] = u' '
|
option: The ScopeMenuOption to update
|
||||||
elif selection == u'e':
|
selected: If provided, updates the "selected" status of the option. If
|
||||||
return None
|
not provided, the "selected" status will be toggled to its opposite
|
||||||
break
|
state.
|
||||||
sys.stdout.write(u'{0}Invalid input "{1}"\n'.format(ERROR_PREFIX, choice))
|
restriction: If provided, applies a restriction to the provided option.
|
||||||
if selection == u'c':
|
|
||||||
status, message = _checkMakeScopesList(scopes)
|
Raises:
|
||||||
if status:
|
ScopeSelectionMenu.MenuChoiceError on change validation errors.
|
||||||
break
|
"""
|
||||||
return scopes
|
if option.is_required and (not selected or selected is None):
|
||||||
|
raise ScopeSelectionMenu.MenuChoiceError(
|
||||||
|
'Scope "%s" is required and cannot be unselected!' %
|
||||||
|
option.description)
|
||||||
|
elif selected and not option.is_selected:
|
||||||
|
# Make sure we're not about to exceed the maximum number of scopes
|
||||||
|
# authorized on a single token.
|
||||||
|
num_scopes_to_add = len(option.get_effective_scopes())
|
||||||
|
num_selected_scopes = len(self.get_selected_scopes())
|
||||||
|
expected_num_scopes = num_scopes_to_add + num_selected_scopes
|
||||||
|
if expected_num_scopes > ScopeSelectionMenu.MAXIMUM_NUM_SCOPES:
|
||||||
|
raise ScopeSelectionMenu.MenuChoiceError(
|
||||||
|
'Too many scopes selected (%d). Maximum is %d. Please remove some '
|
||||||
|
'scopes and try again.' % (
|
||||||
|
expected_num_scopes, ScopeSelectionMenu.MAXIMUM_NUM_SCOPES))
|
||||||
|
|
||||||
|
if restriction is None:
|
||||||
|
if selected is None:
|
||||||
|
# Toggle the option on/off
|
||||||
|
option.is_selected = not option.is_selected
|
||||||
|
else:
|
||||||
|
option.is_selected = selected
|
||||||
|
else:
|
||||||
|
if option.supports_restriction(restriction):
|
||||||
|
option.select(restriction)
|
||||||
|
else:
|
||||||
|
raise ScopeSelectionMenu.MenuChoiceError(
|
||||||
|
'Scope "%s" does not support %s mode!' % (
|
||||||
|
option.description, restriction))
|
||||||
|
|
||||||
def init_gam_worker():
|
def init_gam_worker():
|
||||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||||
|
|||||||
Reference in New Issue
Block a user