From 4d3f982f38cc51854f5a41b6cf1d6076766c3937 Mon Sep 17 00:00:00 2001 From: Devrim Date: Wed, 1 Feb 2023 20:42:54 +0300 Subject: [PATCH] fix: jans-cli-tui working branch 5 (#3649) * fix: jans-cli-tui typo * fix: jans-cli-tui re-post those propery we don't modify for client (ref: #3650) * fix:jans-cli-tui disabling line wrap * fix:jans-cli-tui fix all List JansVerticalNav and give it one space to the right (ref: #3659) * fix: jans-cli-tui remove the cursor from all List items (ref: #3663) * fix: jans-cli-tui check if valid json for json claims in edit user * fix: jans-cli-tui code smell * fix: jans-cli-tui rename config-api swagger file * feat: jans-cli-tui jans-auth mode * fix:jans-cli-tui fix focus on jans_label_container when empty (ref: #3722) * fix: jans-cli-tui ssa management (ref: #3686) * fix: jans-cli-tui code smells * fix: jans-cli-tui ssa never expire is 50 years lifetime * fix: jans-cli-tui ssa timestamp check --------- Co-authored-by: AbdelwahabAdam --- jans-cli-tui/cli_tui/cli/config_cli.py | 164 ++++++++---- jans-cli-tui/cli_tui/cli_style.py | 4 +- jans-cli-tui/cli_tui/jans_cli_tui.py | 18 +- .../010_auth_server/.#edit_client_dialog.py | 1 + .../010_auth_server/edit_client_dialog.py | 26 +- .../cli_tui/plugins/010_auth_server/main.py | 140 ++++++---- .../cli_tui/plugins/010_auth_server/ssa.py | 249 ++++++++++++++++++ jans-cli-tui/cli_tui/plugins/030_scim/main.py | 2 +- .../plugins/060_scripts/edit_script_dialog.py | 1 + .../cli_tui/plugins/070_users/main.py | 26 +- jans-cli-tui/cli_tui/utils/static.py | 1 + jans-cli-tui/cli_tui/utils/utils.py | 8 +- .../cli_tui/wui_components/jans_cli_dialog.py | 9 +- .../cli_tui/wui_components/jans_drop_down.py | 2 +- .../wui_components/jans_label_container.py | 16 +- .../wui_components/jans_vetrical_nav.py | 33 ++- jans-cli-tui/setup.py | 12 + 17 files changed, 559 insertions(+), 153 deletions(-) create mode 120000 jans-cli-tui/cli_tui/plugins/010_auth_server/.#edit_client_dialog.py create mode 100644 jans-cli-tui/cli_tui/plugins/010_auth_server/ssa.py diff --git a/jans-cli-tui/cli_tui/cli/config_cli.py b/jans-cli-tui/cli_tui/cli/config_cli.py index bbc25ed4262..f2f8a3bc544 100755 --- a/jans-cli-tui/cli_tui/cli/config_cli.py +++ b/jans-cli-tui/cli_tui/cli/config_cli.py @@ -29,7 +29,7 @@ import pyDes import stat import ruamel.yaml - +import urllib.parse from pathlib import Path from types import SimpleNamespace @@ -46,7 +46,14 @@ config_ini_fn = config_dir.joinpath('jans-cli.ini') sys.path.append(cur_dir) -my_op_mode = 'scim' if 'scim' in os.path.basename(sys.argv[0]) or '-scim' in sys.argv else 'jca' + +if 'scim' in os.path.basename(sys.argv[0]) or '-scim' in sys.argv: + my_op_mode = 'scim' +elif 'auth' in os.path.basename(sys.argv[0]) or '-auth' in sys.argv: + my_op_mode = 'auth' +else: + my_op_mode = 'jca' + plugins = [] warning_color = 214 @@ -110,26 +117,30 @@ def get_plugin_name_from_title(title): return title[n+1:].strip() return '' -# load yaml files cfg_yaml = {} -op_list = [] -cfg_yaml[my_op_mode] = {} -for yaml_fn in glob.glob(os.path.join(cur_dir, 'ops', my_op_mode, '*.yaml')): - fn, ext = os.path.splitext(os.path.basename(yaml_fn)) - with open(yaml_fn) as f: - config_ = ruamel.yaml.load(f.read().replace('\t', ''), ruamel.yaml.RoundTripLoader) - plugin_name = get_plugin_name_from_title(config_['info']['title']) - cfg_yaml[my_op_mode][plugin_name] = config_ - - for path in config_['paths']: - for method in config_['paths'][path]: - if isinstance(config_['paths'][path][method], dict): - for tag_ in config_['paths'][path][method].get('tags', []): - tag = get_named_tag(tag_) - if not tag in op_list: - op_list.append(tag) +# load yaml files +def read_swagger(op_mode): + op_list = [] + cfg_yaml[op_mode] = {} + for yaml_fn in glob.glob(os.path.join(cur_dir, 'ops', op_mode, '*.yaml')): + fn, ext = os.path.splitext(os.path.basename(yaml_fn)) + with open(yaml_fn) as f: + config_ = ruamel.yaml.load(f.read().replace('\t', ''), ruamel.yaml.RoundTripLoader) + plugin_name = get_plugin_name_from_title(config_['info']['title']) + cfg_yaml[op_mode][plugin_name] = config_ + + for path in config_['paths']: + for method in config_['paths'][path]: + if isinstance(config_['paths'][path][method], dict): + for tag_ in config_['paths'][path][method].get('tags', []): + tag = get_named_tag(tag_) + if not tag in op_list: + op_list.append(tag) + return op_list + +op_list = read_swagger(my_op_mode) op_list.sort() parser = argparse.ArgumentParser() @@ -164,6 +175,7 @@ def get_plugin_name_from_title(title): parser.add_argument("--log-dir", help="Log directory", default=log_dir) parser.add_argument("-revoke-session", help="Revokes session", action='store_true') parser.add_argument("-scim", help="SCIM Mode", action='store_true', default=False) +parser.add_argument("-auth", help="Jans OAuth Server Mode", action='store_true', default=False) parser.add_argument("--data", help="Path to json data file") args = parser.parse_args() @@ -242,11 +254,13 @@ def write_config(): class JCA_CLI: - def __init__(self, host, client_id, client_secret, access_token, test_client=False, wrapped=None): + def __init__(self, host, client_id, client_secret, access_token, test_client=False, op_mode=None, wrapped=None): self.host = self.idp_host = host self.client_id = client_id self.client_secret = client_secret self.use_test_client = test_client + self.my_op_mode = op_mode if op_mode else my_op_mode + self.getCredentials() self.wrapped = wrapped if wrapped == None: @@ -258,11 +272,15 @@ def __init__(self, host, client_id, client_secret, access_token, test_client=Fal self.set_user() self.plugins() - if my_op_mode == 'jca': - self.host += '/jans-config-api' + if self.my_op_mode not in cfg_yaml: + read_swagger(self.my_op_mode) - if my_op_mode == 'scim': + if self.my_op_mode == 'jca': + self.host += '/jans-config-api' + elif self.my_op_mode == 'scim': self.host += '/jans-scim/restv1/v2' + elif self.my_op_mode == 'auth': + self.host += '/jans-auth/restv1' self.set_logging() self.ssl_settings() @@ -350,7 +368,7 @@ def set_user(self): sys.exit() def plugins(self): - for plugin_s in config['DEFAULT'].get(my_op_mode + '_plugins', '').split(','): + for plugin_s in config['DEFAULT'].get(self.my_op_mode + '_plugins', '').split(','): plugin = plugin_s.strip() if plugin: plugins.append(plugin) @@ -370,13 +388,17 @@ def drop_to_shell(self, mylocals): code.interact(local=locals_) sys.exit() - def get_request_header(self, headers={}, access_token=None): + + def get_request_header(self, headers=None, access_token=None): + if headers is None: + headers = {} + if not access_token: access_token = self.access_token - user = self.get_user_info() - if 'inum' in user: - headers['User-inum'] = user['inum'] + user = self.get_user_info() + if 'inum' in user: + headers['User-inum'] = user['inum'] ret_val = {'Authorization': 'Bearer {}'.format(access_token)} ret_val.update(headers) @@ -507,7 +529,7 @@ def validate_date_time(self, date_str): return False - def get_scoped_access_token(self, scope): + def get_scoped_access_token(self, scope, set_access_token=True): if not self.wrapped: scope_text = " for scope {}\n".format(scope) if scope else '' @@ -521,9 +543,11 @@ def get_scoped_access_token(self, scope): else: post_params = {"grant_type": "client_credentials", "scope": scope} + client = self.use_test_client or self.client_id + response = requests.post( url, - auth=(self.use_test_client, self.client_secret), + auth=(client, self.client_secret), data=post_params, verify=self.verify_ssl, cert=self.mtls_client_cert @@ -532,7 +556,10 @@ def get_scoped_access_token(self, scope): try: result = response.json() if 'access_token' in result: - self.access_token = result['access_token'] + if set_access_token: + self.access_token = result['access_token'] + else: + return result['access_token'] else: sys.stderr.write("Error while getting access token") sys.stderr.write(result) @@ -692,11 +719,12 @@ def get_jwt_access_token(self, device_verified=None): return True, '' def get_access_token(self, scope): - if self.use_test_client: - self.get_scoped_access_token(scope) - elif not self.access_token and not self.wrapped: - self.check_access_token() - self.get_jwt_access_token() + if self.my_op_mode != 'auth': + if self.use_test_client: + self.get_scoped_access_token(scope) + elif not self.access_token and not self.wrapped: + self.check_access_token() + self.get_jwt_access_token() return True, '' def print_exception(self, e): @@ -1010,12 +1038,12 @@ def obtain_parameters(self, endpoint, single=False): def get_path_by_id(self, operation_id): retVal = {} - for plugin in cfg_yaml[my_op_mode]: - for path in cfg_yaml[my_op_mode][plugin]['paths']: - for method in cfg_yaml[my_op_mode][plugin]['paths'][path]: - if 'operationId' in cfg_yaml[my_op_mode][plugin]['paths'][path][method] and cfg_yaml[my_op_mode][plugin]['paths'][path][method][ + for plugin in cfg_yaml[self.my_op_mode]: + for path in cfg_yaml[self.my_op_mode][plugin]['paths']: + for method in cfg_yaml[self.my_op_mode][plugin]['paths'][path]: + if 'operationId' in cfg_yaml[self.my_op_mode][plugin]['paths'][path][method] and cfg_yaml[self.my_op_mode][plugin]['paths'][path][method][ 'operationId'] == operation_id: - retVal = cfg_yaml[my_op_mode][plugin]['paths'][path][method].copy() + retVal = cfg_yaml[self.my_op_mode][plugin]['paths'][path][method].copy() retVal['__path__'] = path retVal['__method__'] = method retVal['__urlsuffix__'] = self.get_url_param(path) @@ -1075,8 +1103,10 @@ def get_requests(self, endpoint, params={}): self.print_exception(e) def get_mime_for_endpoint(self, endpoint, req='requestBody'): - for key in endpoint.info[req]['content']: - return key + if req in endpoint.info: + for key in endpoint.info[req]['content']: + return key + def post_requests(self, endpoint, data): url = 'https://{}{}'.format(self.host, endpoint.path) @@ -1085,6 +1115,8 @@ def post_requests(self, endpoint, data): mime_type = self.get_mime_for_endpoint(endpoint) headers = self.get_request_header({'Accept': 'application/json', 'Content-Type': mime_type}) + if mime_type: + headers['Content-Type'] = mime_type response = requests.post(url, headers=headers, @@ -1101,20 +1133,33 @@ def post_requests(self, endpoint, data): try: return response.json() except: - print(response.text) + return {'server_error': response.text} def delete_requests(self, endpoint, url_param_dict): security = self.get_scope_for_endpoint(endpoint) self.get_access_token(security) + url_params = self.get_url_param(endpoint.path) + + if url_params: + url_path = endpoint.path.format(**url_param_dict) + for param in url_params: + del url_param_dict[param] + else: + url_path = endpoint.path + + if url_param_dict: + url_path += '?'+ urllib.parse.urlencode(url_param_dict) response = requests.delete( - url='https://{}{}'.format(self.host, endpoint.path.format(**url_param_dict)), + url='https://{}{}'.format(self.host, url_path), headers=self.get_request_header({'Accept': 'application/json'}), verify=self.verify_ssl, cert=self.mtls_client_cert ) + self.log_response(response) + if response.status_code in (200, 204): return None @@ -1217,15 +1262,14 @@ def help_for(self, op_name): schema_path = None - for plugin in cfg_yaml[my_op_mode]: - for path_name in cfg_yaml[my_op_mode][plugin]['paths']: - for method in cfg_yaml[my_op_mode][plugin]['paths'][path_name]: - path = cfg_yaml[my_op_mode][plugin]['paths'][path_name][method] + for plugin in cfg_yaml[self.my_op_mode]: + for path_name in cfg_yaml[self.my_op_mode][plugin]['paths']: + for method in cfg_yaml[self.my_op_mode][plugin]['paths'][path_name]: + path = cfg_yaml[self.my_op_mode][plugin]['paths'][path_name][method] if isinstance(path, dict): for tag_ in path.get('tags', []): tag = get_named_tag(tag_) if tag == op_name: - title = cfg_yaml[my_op_mode][plugin]['info']['title'] mode_suffix = plugin+ ':' if plugin else '' print('Operation ID:', path['operationId']) print(' Description:', path['description']) @@ -1247,7 +1291,13 @@ def help_for(self, op_name): if 'requestBody' in path: for apptype in path['requestBody'].get('content', {}): if 'schema' in path['requestBody']['content'][apptype]: - if path['requestBody']['content'][apptype]['schema'].get('type') == 'array': + if path['requestBody']['content'][apptype]['schema'].get('type') == 'object' and '$ref' not in path['requestBody']['content'][apptype]['schema']: + print(' Parameters:') + for param in path['requestBody']['content'][apptype]['schema']['properties']: + req_s = '*' if param in path['requestBody']['content'][apptype]['schema'].get('required', []) else '' + print(' {}{}: {}'.format(param, req_s, path['requestBody']['content'][apptype]['schema']['properties'][param].get('description') or "Description not found for this property")) + + elif path['requestBody']['content'][apptype]['schema'].get('type') == 'array': schema_path = path['requestBody']['content'][apptype]['schema']['items']['$ref'] print(' Schema: Array of {}{}'.format(mode_suffix, os.path.basename(schema_path))) else: @@ -1327,7 +1377,7 @@ def process_command_post(self, path, suffix_param, endpoint_params, data_fn, dat endpoint = self.get_fake_endpoint(path) - if not data: + if not data and data_fn: if data_fn.endswith('jwt'): with open(data_fn) as reader: @@ -1389,6 +1439,7 @@ def process_command_patch(self, path, suffix_param, endpoint_params, data_fn, da def process_command_delete(self, path, suffix_param, endpoint_params, data_fn, data=None): endpoint = self.get_fake_endpoint(path) response = self.delete_requests(endpoint, suffix_param) + if self.wrapped: return response @@ -1440,16 +1491,15 @@ def process_command_by_id(self, operation_id, url_suffix, endpoint_args, data_fn def get_schema_reference_from_name(self, plugin_name, schema_name): - for plugin in cfg_yaml[my_op_mode]: - if plugin_name == get_plugin_name_from_title(title = cfg_yaml[my_op_mode][plugin]['info']['title']): - for schema in cfg_yaml[my_op_mode][plugin]['components']['schemas']: + for plugin in cfg_yaml[self.my_op_mode]: + if plugin_name == get_plugin_name_from_title(title = cfg_yaml[self.my_op_mode][plugin]['info']['title']): + for schema in cfg_yaml[self.my_op_mode][plugin]['components']['schemas']: if schema == schema_name: return '#/components/schemas/' + schema def get_schema_from_reference(self, plugin_name, ref): - schema_path_list = ref.strip('/#').split('/') - schema = cfg_yaml[my_op_mode][plugin_name][schema_path_list[0]] + schema = cfg_yaml[self.my_op_mode][plugin_name][schema_path_list[0]] schema_ = schema.copy() diff --git a/jans-cli-tui/cli_tui/cli_style.py b/jans-cli-tui/cli_tui/cli_style.py index 33b33815b8e..e611f62336d 100755 --- a/jans-cli-tui/cli_tui/cli_style.py +++ b/jans-cli-tui/cli_tui/cli_style.py @@ -136,7 +136,7 @@ "tab-nav-background": "fg:#b0e0e6 bg:#a9a9a9", "tab-unselected": "fg:#b0e0e6 bg:#a9a9a9 underline", "tab-selected": "fg:#000080 bg:#d3d3d3", - + ##scim "scim-widget": "bg:black fg:white", @@ -172,7 +172,7 @@ def get_color_for_style(style_name:str)->SimpleNamespace: date_picker_Time = "green" ## only color date_picker_TimeSelected = "black" -date_picker_calender_prevSelected = "red" #>black >> defult bold +date_picker_calender_prevSelected = "red" #>black >> default bold date_picker_calenderNSelected = "blue"#>black date_picker_calenderSelected = "red" diff --git a/jans-cli-tui/cli_tui/jans_cli_tui.py b/jans-cli-tui/cli_tui/jans_cli_tui.py index 7b6874f976e..4f779459ef7 100755 --- a/jans-cli-tui/cli_tui/jans_cli_tui.py +++ b/jans-cli-tui/cli_tui/jans_cli_tui.py @@ -35,7 +35,7 @@ sys.exit() import prompt_toolkit -from prompt_toolkit.application import Application +from prompt_toolkit.application import Application, get_app_session from prompt_toolkit.application.current import get_app from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous @@ -187,7 +187,8 @@ async def progress_coroutine(self) -> None: self.invalidate() def cli_requests(self, args: dict) -> Response: - response = self.cli_object.process_command_by_id( + cli_object = args['cli_object'] if 'cli_object' in args else self.cli_object + response = cli_object.process_command_by_id( operation_id=args['operation_id'], url_suffix=args.get('url_suffix', ''), endpoint_args=args.get('endpoint_args', ''), @@ -250,15 +251,22 @@ def remove_plugin(self, pid: str) -> None: self._plugins.remove(plugin_object) return - @property - def dialog_width(self) -> None: + def dialog_width(self) -> int: return int(self.output.get_size().columns*0.8) @property - def dialog_height(self) -> None: + def dialog_height(self) -> int: return int(self.output.get_size().rows*0.9) + def get_column_sizes(self, *args: tuple) -> list: + col_size_list = [] + w = get_app_session().output.get_size().columns - 3 + for col_ratio in args: + col_size_list.append(int(w*col_ratio)) + + return col_size_list + def init_logger(self) -> None: self.logger = logging.getLogger('JansCli') self.logger.setLevel(logging.DEBUG) diff --git a/jans-cli-tui/cli_tui/plugins/010_auth_server/.#edit_client_dialog.py b/jans-cli-tui/cli_tui/plugins/010_auth_server/.#edit_client_dialog.py new file mode 120000 index 00000000000..b7534fa5351 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/010_auth_server/.#edit_client_dialog.py @@ -0,0 +1 @@ +mbaser@ubuntu.7412 \ No newline at end of file diff --git a/jans-cli-tui/cli_tui/plugins/010_auth_server/edit_client_dialog.py b/jans-cli-tui/cli_tui/plugins/010_auth_server/edit_client_dialog.py index 0f9468dac20..b7498d9d6da 100755 --- a/jans-cli-tui/cli_tui/plugins/010_auth_server/edit_client_dialog.py +++ b/jans-cli-tui/cli_tui/plugins/010_auth_server/edit_client_dialog.py @@ -77,6 +77,8 @@ def __init__( self.data = data self.title = title self.nav_dialog_width = int(self.myparent.dialog_width*1.1) + self.client_scopes_entries=[] + self.fill_client_scopes() self.prepare_tabs() self.create_window() @@ -92,8 +94,10 @@ def save(self) -> None: """method to invoked when saving the dialog (Save button is pressed) """ + current_data = self.data self.data = self.make_data_from_dialog() self.data['disabled'] = not self.data['disabled'] + for list_key in ( 'redirectUris', 'postLogoutRedirectUris', @@ -118,6 +122,7 @@ def save(self) -> None: self.data['attributes']={'redirectUrisRegex':self.data['redirectUrisRegex']} self.data['attributes']={'parLifetime':self.data['parLifetime']} self.data['attributes']={'requirePar':self.data['requirePar']} + for list_key in ( 'backchannelLogoutUri', @@ -146,6 +151,7 @@ def save(self) -> None: if self.data[list_key]: self.data['attributes'][list_key] = self.data[list_key] + self.data['displayName'] = self.data['clientName'] cfr = self.check_required_fields() @@ -156,6 +162,10 @@ def save(self) -> None: if ditem in self.data and self.data[ditem] is None: self.data.pop(ditem) + for prop in current_data: + if prop not in self.data: + self.data[prop] = current_data[prop] + if self.save_handler: self.save_handler(self) @@ -188,12 +198,12 @@ def create_window(self) -> None: def fill_client_scopes(self): - self.client_scopes.entries = [] for scope_dn in self.data.get('scopes', []): scope = self.get_scope_by_inum(scope_dn) if scope: label = scope.get('displayName') or scope.get('inum') or scope_dn - self.client_scopes.add_label(scope_dn, label) + if [scope_dn, label] not in self.client_scopes_entries: + self.client_scopes_entries.append([scope_dn, label]) def prepare_tabs(self) -> None: """Prepare the tabs for Edil Client Dialogs @@ -321,17 +331,18 @@ def prepare_tabs(self) -> None: jans_help=_("Add Scopes"), handler=self.add_scopes) ]) - + self.client_scopes = JansLabelContainer( title=_('Scopes'), width=self.nav_dialog_width - 26, on_display=self.myparent.data_display_dialog, on_delete=self.delete_scope, - buttonbox=add_scope_button + buttonbox=add_scope_button, + entries = self.client_scopes_entries, ) - self.fill_client_scopes() + basic_tab_widgets.append(self.client_scopes) @@ -398,7 +409,7 @@ def prepare_tabs(self) -> None: style=cli_style.check_box), self.myparent.getTitledText( - _("Defult max authn age"), + _("Default max authn age"), name='defaultMaxAge', value=self.data.get('defaultMaxAge',''), jans_help=self.myparent.get_help_from_schema(schema, 'defaultMaxAge'), @@ -855,10 +866,9 @@ def add_selected_claims(dialog): def delete_scope(self, scope: list) -> None: - def do_delete_scope(dialog): self.data['scopes'].remove(scope[0]) - self.fill_client_scopes() + self.client_scopes_entries.remove([scope[0],scope[1]]) dialog = self.myparent.get_confirm_dialog( message=_("Are you sure want to delete Scope:\n {} ?".format(scope[1])), diff --git a/jans-cli-tui/cli_tui/plugins/010_auth_server/main.py b/jans-cli-tui/cli_tui/plugins/010_auth_server/main.py index 841ac78d10f..641875555a1 100755 --- a/jans-cli-tui/cli_tui/plugins/010_auth_server/main.py +++ b/jans-cli-tui/cli_tui/plugins/010_auth_server/main.py @@ -21,9 +21,13 @@ TextArea ) from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.application import Application + from utils.static import DialogResult, cli_style, common_strings from utils.utils import DialogUtils from utils.utils import common_data +from utils.multi_lang import _ from wui_components.jans_nav_bar import JansNavBar from wui_components.jans_vetrical_nav import JansVerticalNav @@ -32,10 +36,12 @@ from view_property import ViewProperty from edit_client_dialog import EditClientDialog from edit_scope_dialog import EditScopeDialog -from prompt_toolkit.buffer import Buffer -from prompt_toolkit.application import Application -from utils.multi_lang import _ +from ssa import SSA +from prompt_toolkit.widgets import ( + HorizontalLine, + VerticalLine, +) QUESTION_TEMP = "\n {} ?" class Plugin(DialogUtils): @@ -55,11 +61,14 @@ def __init__( self.name = '[A]uth Server' self.search_text= None self.oauth_update_properties_start_index = 0 + self.ssa = SSA(app) self.app_configuration = {} self.oauth_containers = {} + self.oauth_prepare_navbar() self.oauth_prepare_containers() self.oauth_nav_selection_changed(self.nav_bar.navbar_entries[0][0]) + def init_plugin(self) -> None: """The initialization for this plugin @@ -175,6 +184,7 @@ def oauth_prepare_containers(self) -> None: DynamicContainer(lambda: self.oauth_data_container['properties']) ],style='class:outh_containers_scopes') + self.oauth_containers['ssa'] = self.ssa.main_container self.oauth_containers['logging'] = DynamicContainer(lambda: self.oauth_data_container['logging']) self.oauth_main_container = HSplit([ @@ -190,7 +200,7 @@ def oauth_prepare_navbar(self) -> None: """ self.nav_bar = JansNavBar( self.app, - entries=[('clients', 'C[l]ients'), ('scopes', 'Sc[o]pes'), ('keys', '[K]eys'), ('defaults', '[D]efaults'), ('properties', 'Properti[e]s'), ('logging', 'Lo[g]ging')], + entries=[('clients', 'C[l]ients'), ('scopes', 'Sc[o]pes'), ('keys', '[K]eys'), ('defaults', '[D]efaults'), ('properties', 'Properti[e]s'), ('logging', 'Lo[g]ging'), ('ssa', '[S]SA')], selection_changed=self.oauth_nav_selection_changed, select=0, jans_name='oauth:nav_bar' @@ -256,20 +266,23 @@ async def coroutine(): ) if data: - clients = JansVerticalNav( - myparent=self.app, - headers=['Client ID', 'Client Name', 'Grant Types', 'Subject Type'], - preferred_size= [0,0,30,0], - data=data, - on_enter=self.edit_client, - on_display=self.app.data_display_dialog, - on_delete=self.delete_client, - get_help=(self.get_help,'Client'), - selectes=0, - headerColor=cli_style.navbar_headcolor, - entriesColor=cli_style.navbar_entriescolor, - all_data=result['entries'] - ) + clients = VSplit([ + Label(text=" ",width=1), + JansVerticalNav( + myparent=self.app, + headers=['Client ID', 'Client Name', 'Grant Types', 'Subject Type'], + preferred_size= [0,0,30,0], + data=data, + on_enter=self.edit_client, + on_display=self.app.data_display_dialog, + on_delete=self.delete_client, + get_help=(self.get_help,'Client'), + selectes=0, + headerColor=cli_style.navbar_headcolor, + entriesColor=cli_style.navbar_entriescolor, + all_data=result['entries'] + ) + ]) buttons = [] if start_index > 0: handler_partial = partial(self.oauth_update_clients, start_index-self.app.entries_per_page, pattern) @@ -418,20 +431,23 @@ async def coroutine(): if data: - scopes = JansVerticalNav( - myparent=self.app, - headers=['id', 'Description', 'Type','inum'], - preferred_size= [30,40,8,12], - data=data, - on_enter=self.edit_scope_dialog, - on_display=self.app.data_display_dialog, - on_delete=self.delete_scope, - get_help=(self.get_help,'Scope'), - selectes=0, - headerColor=cli_style.navbar_headcolor, - entriesColor=cli_style.navbar_entriescolor, - all_data=result['entries'] - ) + scopes =VSplit([ + Label(text=" ",width=1), + JansVerticalNav( + myparent=self.app, + headers=['id', 'Description', 'Type','inum'], + preferred_size= [30,40,8,12], + data=data, + on_enter=self.edit_scope_dialog, + on_display=self.app.data_display_dialog, + on_delete=self.delete_scope, + get_help=(self.get_help,'Scope'), + selectes=0, + headerColor=cli_style.navbar_headcolor, + entriesColor=cli_style.navbar_entriescolor, + all_data=result['entries'] + ) + ]) buttons = [] if start_index > 0: @@ -484,18 +500,21 @@ def add_property(**params: Any) -> None: self.view_property(passed=passed, op_type='add') - properties = JansVerticalNav( - myparent=self.app, - headers=['Property Name'], - preferred_size=[0], - data=missing_properties_data, - on_enter=add_property, - get_help=(self.get_help,'AppConfiguration'), - selectes=0, - headerColor=cli_style.navbar_headcolor, - entriesColor=cli_style.navbar_entriescolor, - all_data=missing_properties - ) + properties = VSplit([ + Label(text=" ",width=1), + JansVerticalNav( + myparent=self.app, + headers=['Property Name'], + preferred_size=[0], + data=missing_properties_data, + on_enter=add_property, + get_help=(self.get_help,'AppConfiguration'), + selectes=0, + headerColor=cli_style.navbar_headcolor, + entriesColor=cli_style.navbar_entriescolor, + all_data=missing_properties + ) + ]) body = HSplit([properties]) buttons = [Button(_("Cancel"))] @@ -558,19 +577,23 @@ def oauth_update_properties( data_now = data[start_index*20:start_index*20+20] - properties = JansVerticalNav( - myparent=self.app, - headers=['Property Name', 'Property Value'], - preferred_size= [0,0], - data=data_now, - on_enter=self.view_property, - on_display=self.properties_display_dialog, - get_help=(self.get_help,'AppConfiguration'), - selectes=0, - headerColor=cli_style.navbar_headcolor, - entriesColor=cli_style.navbar_entriescolor, - all_data=list(self.app_configuration.values()) + properties =VSplit([ + Label(text=" ",width=1), + JansVerticalNav( + myparent=self.app, + headers=['Property Name', 'Property Value'], + preferred_size= [0,0], + data=data_now, + on_enter=self.view_property, + on_display=self.properties_display_dialog, + get_help=(self.get_help,'AppConfiguration'), + selectes=0, + headerColor=cli_style.navbar_headcolor, + entriesColor=cli_style.navbar_entriescolor, + all_data=list(self.app_configuration.values()) ) + ]) + self.oauth_data_container['properties'] = HSplit([ properties, @@ -648,7 +671,9 @@ def oauth_update_keys(self) -> None: if data: - keys = JansVerticalNav( + keys = VSplit([ + Label(text=" ",width=1), + JansVerticalNav( myparent=self.app, headers=['Name', 'Expiration','Kid'], data=data, @@ -659,6 +684,7 @@ def oauth_update_keys(self) -> None: entriesColor=cli_style.navbar_entriescolor, all_data=self.jwks_keys['keys'] ) + ]) self.oauth_data_container['keys'] = HSplit([keys]) get_app().invalidate() diff --git a/jans-cli-tui/cli_tui/plugins/010_auth_server/ssa.py b/jans-cli-tui/cli_tui/plugins/010_auth_server/ssa.py new file mode 100644 index 00000000000..73e5cdf8c96 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/010_auth_server/ssa.py @@ -0,0 +1,249 @@ +import os +import asyncio +from datetime import datetime +from typing import Any + +from prompt_toolkit.application import Application +from prompt_toolkit.eventloop import get_event_loop +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.containers import HSplit, VSplit +from prompt_toolkit.layout.containers import DynamicContainer, Window +from prompt_toolkit.widgets import Button, Label, Checkbox +from prompt_toolkit.buffer import Buffer + +from utils.utils import DialogUtils +from utils.static import cli_style, common_strings +from wui_components.jans_vetrical_nav import JansVerticalNav +from wui_components.jans_cli_dialog import JansGDialog +from wui_components.jans_label_container import JansLabelContainer +from wui_components.jans_date_picker import DateSelectWidget + +from cli import config_cli + + +from utils.multi_lang import _ + +class SSA(DialogUtils): + def __init__( + self, + app: Application + ) -> None: + + self.app = self.myparent = app + self.data = [] + self.working_container = JansVerticalNav( + myparent=app, + headers=[_("Software ID"), _("Organisation"), _("Software Roles"), _("Status"), _("Exp.")], + preferred_size= self.app.get_column_sizes(.25, .25 , .3, .1, .1), + on_display=self.app.data_display_dialog, + on_delete=self.delete_ssa, + selectes=0, + headerColor=cli_style.navbar_headcolor, + entriesColor=cli_style.navbar_entriescolor, + hide_headers = True + ) + + self.main_container = HSplit([ + VSplit([ + self.app.getButton(text=_("Get SSA"), name='oauth:ssa:get', jans_help=_("Retreive all SSA"), handler=self.get_ssa), + self.app.getTitledText(_("Search"), name='oauth:ssa:search', jans_help=_(common_strings.enter_to_search), accept_handler=self.search_ssa, style=cli_style.edit_text), + self.app.getButton(text=_("Add SSA"), name='oauth:ssa:add', jans_help=_("To add a new SSA press this button"), handler=self.add_ssa), + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.working_container) + ],style='class:outh_containers_scopes') + + + self.cli_object = config_cli.JCA_CLI( + host=config_cli.host, + client_id=config_cli.client_id, + client_secret=config_cli.client_secret, + access_token=config_cli.access_token, + op_mode = 'auth' + ) + + + def update_ssa_container(self, start_index=0, search_str=''): + + self.working_container.clear() + data_display = [] + + for ssa in self.data: + if search_str and (search_str not in ssa['ssa']['software_id'] and search_str not in str(ssa['ssa']['org_id']) and search_str not in ssa['ssa']['software_roles']): + continue + try: + dt_object = datetime.fromtimestamp(ssa['ssa']['exp']) + except Exception: + continue + data_display.append(( + str(ssa['ssa']['software_id']), + str(ssa['ssa']['org_id']), + ','.join(ssa['ssa']['software_roles']), + '??', + '{:02d}/{:02d}/{}'.format(dt_object.day, dt_object.month, str(dt_object.year)[2:]) + )) + + if not data_display: + self.app.show_message(_("Oops"), _(common_strings.no_matching_result), tobefocused = self.main_container) + return + + self.working_container.hide_headers = False + for datum in data_display[start_index:start_index+self.app.entries_per_page]: + self.working_container.add_item(datum) + + self.app.layout.focus(self.working_container) + + def get_ssa(self, search_str=''): + async def coroutine(): + cli_args = {'operation_id': 'get-ssa', 'cli_object': self.cli_object} + self.app.start_progressing(_("Retreiving ssa...")) + response = await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.data = response.json() + self.working_container.all_data = self.data + self.update_ssa_container(search_str=search_str) + + asyncio.ensure_future(coroutine()) + + def add_ssa(self): + self.edit_ssa_dialog() + + + def edit_ssa(self, **params: Any) -> None: + data = self.data[params['selected']]['ssa'] + self.edit_ssa_dialog(data=data) + + def save_ssa(self, dialog): + new_data = self.make_data_from_dialog(tabs={'ssa': dialog.body}) + + if self.never_expire_cb.checked: + # set expiration to 50 years + new_data['expiration'] = int(datetime.now().timestamp()) + 1576800000 + else: + new_data['expiration'] = int(datetime.fromisoformat(self.expire_widget.value).timestamp()) + + if self.check_required_fields(dialog.body, data=new_data): + + async def coroutine(): + operation_id = 'post-register-ssa' + cli_args = {'operation_id': operation_id, 'cli_object': self.cli_object, 'data': new_data} + self.app.start_progressing(_("Saving ssa...")) + await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.get_ssa() + dialog.future.set_result(True) + + asyncio.ensure_future(coroutine()) + + def edit_ssa_dialog(self, data=None): + if data: + title = _("Edit SSA") + else: + data = {} + title = _("Add new SSA") + + expiration_label = _("Expiration") + never_expire_label = _("Never") + self.never_expire_cb = Checkbox(never_expire_label) + expiration_iso = datetime.fromtimestamp(data['exp']).isoformat() if 'exp' in data else '' + self.expire_widget = DateSelectWidget(value=expiration_iso, parent=self) + + body = HSplit([ + + self.app.getTitledText( + title=_("Software ID"), + name='software_id', + value=data.get('software_id',''), + style=cli_style.edit_text_required + ), + + self.app.getTitledText( + title=_("Organisation"), + name='org_id', + value=str(data.get('org_id','')), + text_type='integer', + style=cli_style.edit_text_required + ), + + self.app.getTitledText( + title=_("Description"), + name='description', + value=data.get('description',''), + style=cli_style.edit_text_required + ), + + self.app.getTitledCheckBoxList( + title=_("Grant Types"), + name='grant_types', + values=[('authorization_code', 'Authorization Code'), ('refresh_token', 'Refresh Token'), ('urn:ietf:params:oauth:grant-type:uma-ticket', 'UMA Ticket'), ('client_credentials', 'Client Credentials'), ('password', 'Password'), ('implicit', 'Implicit')], + current_values=data.get('grant_types', []), + style=cli_style.check_box, + ), + + self.app.getTitledText( + title=_("Software Roles"), + name='software_roles', + value='\n'.join(data.get('software_roles', [])), + height=3, + style=cli_style.edit_text_required + ), + + self.app.getTitledCheckBox( + _("One Time Use"), + name='one_time_use', + checked=data.get('one_time_use', False), + style=cli_style.check_box + ), + + self.app.getTitledCheckBox( + _("Rotate SSA"), + name='rotate_ssa', + checked=data.get('rotate_ssa', False), + style=cli_style.check_box + ), + + VSplit([ + Label(expiration_label + ': ', width=len(expiration_label)+2, style=cli_style.edit_text), + HSplit([self.never_expire_cb], width=len(never_expire_label)+7), + self.expire_widget, + ], height=1), + + ]) + + save_button = Button(_("Save"), handler=self.save_ssa) + save_button.keep_dialog = True + canncel_button = Button(_("Cancel")) + buttons = [save_button, canncel_button] + dialog = JansGDialog(self.app, title=title, body=body, buttons=buttons) + self.app.show_jans_dialog(dialog) + + def search_ssa(self, tbuffer:Buffer) -> None: + if self.data: + self.update_ssa_container(search_str=tbuffer.text) + else: + self.get_ssa(search_str=tbuffer.text) + + def delete_ssa(self, **kwargs: Any) -> None: + jti = self.data[kwargs['selected_idx']]['ssa']['jti'] + + def do_delete_ssa(result): + + async def coroutine(): + cli_args = {'operation_id': 'delete-ssa', 'cli_object': self.cli_object, 'url_suffix': 'jti:{}'.format(jti)} + self.app.start_progressing(_("Deleting ssa {}".format(jti))) + await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.get_ssa() + + asyncio.ensure_future(coroutine()) + + dialog = self.app.get_confirm_dialog( + message = _("Are you sure want to delete SSA jti:") + ' ' + jti, + confirm_handler=do_delete_ssa + ) + + self.app.show_jans_dialog(dialog) + + diff --git a/jans-cli-tui/cli_tui/plugins/030_scim/main.py b/jans-cli-tui/cli_tui/plugins/030_scim/main.py index 96127155eb4..1e307acedc0 100755 --- a/jans-cli-tui/cli_tui/plugins/030_scim/main.py +++ b/jans-cli-tui/cli_tui/plugins/030_scim/main.py @@ -22,7 +22,7 @@ def __init__( """ self.app = app self.pid = 'scim' - self.name = '[S]CIM' + self.name = 'S[C]IM' self.server_side_plugin = True self.app_config = {} self.widgets_ready = False diff --git a/jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py b/jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py index 7d6dee4acd2..0387279e6f1 100755 --- a/jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py +++ b/jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py @@ -424,6 +424,7 @@ def edit_script_dialog(self) -> None: focusable=True, scrollbar=True, line_numbers=True, + wrap_lines=False, lexer=PygmentsLexer(PythonLexer if self.cur_lang == 'PYTHON' else JavaLexer), ) diff --git a/jans-cli-tui/cli_tui/plugins/070_users/main.py b/jans-cli-tui/cli_tui/plugins/070_users/main.py index 2babeb13f38..c2e8c767c20 100755 --- a/jans-cli-tui/cli_tui/plugins/070_users/main.py +++ b/jans-cli-tui/cli_tui/plugins/070_users/main.py @@ -1,19 +1,23 @@ +import json import asyncio from functools import partial from types import SimpleNamespace from typing import Any, Optional + +from prompt_toolkit import HTML from prompt_toolkit.buffer import Buffer from prompt_toolkit.application import Application from prompt_toolkit.layout.containers import HSplit, VSplit, DynamicContainer, HorizontalAlign from prompt_toolkit.layout.dimension import D from prompt_toolkit.widgets import Button, Dialog +from prompt_toolkit.eventloop import get_event_loop + from wui_components.jans_vetrical_nav import JansVerticalNav from edit_user_dialog import EditUserDialog from utils.utils import DialogUtils, common_data from utils.static import DialogResult from utils.multi_lang import _ from wui_components.jans_cli_dialog import JansGDialog -from prompt_toolkit.eventloop import get_event_loop from utils.static import DialogResult, cli_style, common_strings common_data.users = SimpleNamespace() @@ -232,14 +236,15 @@ def save_user(self, dialog: Dialog) -> None: _type_: bool value to check the status code response """ + fix_title = _("Please fix!") raw_data = self.make_data_from_dialog(tabs={'user': dialog.edit_user_container.content}) if not (raw_data['userId'].strip() and raw_data['mail'].strip()): - self.app.show_message(_("Please fix!"), _("Username and/or Email is empty")) + self.app.show_message(fix_title, _("Username and/or Email is empty")) return - + if 'baseDn' not in dialog.data and not raw_data['userPassword'].strip(): - self.app.show_message(_("Please fix!"), _("Please enter Password")) + self.app.show_message(fix_title, _("Please enter Password")) return user_info = {'customObjectClasses':['top', 'jansCustomPerson'], 'customAttributes':[]} @@ -260,6 +265,19 @@ def save_user(self, dialog: Dialog) -> None: for key_ in raw_data: multi_valued = False + key_prop = dialog.get_claim_properties(key_) + + if key_prop.get('dataType') == 'json': + try: + json.loads(raw_data[key_]) + except Exception as e: + display_name = key_prop.get('displayName') or key_ + self.app.show_message( + fix_title, + _(HTML("Can't convert {} to json. Conversion error: {}").format(display_name, e)) + ) + return + user_info['customAttributes'].append({ 'name': key_, 'multiValued': multi_valued, diff --git a/jans-cli-tui/cli_tui/utils/static.py b/jans-cli-tui/cli_tui/utils/static.py index 0a909300b5d..8bffee7f711 100755 --- a/jans-cli-tui/cli_tui/utils/static.py +++ b/jans-cli-tui/cli_tui/utils/static.py @@ -18,6 +18,7 @@ class cli_style: tab_selected = 'class:tab-selected' scim_widget = 'class:scim-widget' black_bg = 'class:plugin-black-bg' + textarea = 'class:textarea' class common_strings: enter_to_search = "Press enter to perform search" diff --git a/jans-cli-tui/cli_tui/utils/utils.py b/jans-cli-tui/cli_tui/utils/utils.py index eee45e5ff1b..c2ca7f5df5f 100755 --- a/jans-cli-tui/cli_tui/utils/utils.py +++ b/jans-cli-tui/cli_tui/utils/utils.py @@ -65,15 +65,19 @@ def make_data_from_dialog( return data - def check_required_fields(self, container=None): + def check_required_fields(self, container=None, data=None): missing_fields = [] + if not data: + data = self.data + containers = [container] if container else [self.tabs[tab] for tab in self.tabs] for container in containers: for item in container.children: if hasattr(item, 'children') and len(item.children)>1 and hasattr(item.children[1], 'jans_name'): - if 'required' in item.children[0].style and not self.data.get(item.children[1].jans_name, None): + if 'required' in item.children[0].style and not data.get(item.children[1].jans_name, None): missing_fields.append(item.children[1].jans_name) + if missing_fields: self.myparent.show_message("Please fill required fields", "The following fields are required:\n" + ', '.join(missing_fields)) return False diff --git a/jans-cli-tui/cli_tui/wui_components/jans_cli_dialog.py b/jans-cli-tui/cli_tui/wui_components/jans_cli_dialog.py index 78c493e860d..530d51fe635 100755 --- a/jans-cli-tui/cli_tui/wui_components/jans_cli_dialog.py +++ b/jans-cli-tui/cli_tui/wui_components/jans_cli_dialog.py @@ -36,20 +36,21 @@ def __init__( self.body = body self.myparent = parent - if not width: width = parent.dialog_width if not buttons: buttons = [Button(text=_("OK"))] - def do_handler(button_text, handler): + def do_handler(button_text, handler, keep_dialog): if handler: handler(self) - self.future.set_result(button_text) + + if not keep_dialog: + self.future.set_result(button_text) for button in buttons: - button.handler = partial(do_handler, button.text, button.handler) + button.handler = partial(do_handler, button.text, button.handler, getattr(button, 'keep_dialog', False)) self.dialog = Dialog( title=title, diff --git a/jans-cli-tui/cli_tui/wui_components/jans_drop_down.py b/jans-cli-tui/cli_tui/wui_components/jans_drop_down.py index 4ef4406bdac..9bd6750f3d2 100755 --- a/jans-cli-tui/cli_tui/wui_components/jans_drop_down.py +++ b/jans-cli-tui/cli_tui/wui_components/jans_drop_down.py @@ -158,7 +158,7 @@ def __init__( """init for DropDownWidget Args: values (list, optional): List of values to select one from them. Defaults to []. - value (str, optional): The defult selected value. Defaults to None. + value (str, optional): The defualt selected value. Defaults to None. Examples: widget=DropDownWidget( diff --git a/jans-cli-tui/cli_tui/wui_components/jans_label_container.py b/jans-cli-tui/cli_tui/wui_components/jans_label_container.py index 5a06f7a7aec..e0c3c17a056 100644 --- a/jans-cli-tui/cli_tui/wui_components/jans_label_container.py +++ b/jans-cli-tui/cli_tui/wui_components/jans_label_container.py @@ -1,12 +1,16 @@ from typing import Callable, Optional from prompt_toolkit.application import get_app -from prompt_toolkit.formatted_text import HTML, AnyFormattedText, merge_formatted_text +from prompt_toolkit.formatted_text import HTML, AnyFormattedText, merge_formatted_text,to_formatted_text from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout import FormattedTextControl, Window from prompt_toolkit.widgets import Label, Frame, Box, Button from prompt_toolkit.layout.containers import HSplit - +from prompt_toolkit.layout.containers import ( + HSplit, + VSplit, + DynamicContainer, +) class JansLabelContainer: def __init__( self, @@ -15,7 +19,8 @@ def __init__( on_enter: Optional[Callable]=None, on_delete: Optional[Callable]=None, on_display: Optional[Callable]=None, - buttonbox: Optional[Button]=None + buttonbox: Optional[Button]=None, + entries: Optional=None, ) -> None: """Label container for Jans @@ -33,13 +38,13 @@ def __init__( self.on_delete = on_delete self.on_display = on_display self.height=2 - self.entries = [] + self.entries = [] if not entries else entries self.invalidate = False self.selected_entry = 0 self.body = Window( content=FormattedTextControl( text=self._get_formatted_text, - focusable=True, + focusable=True if self.entries != [] else False, key_bindings=self._get_key_bindings(), ), width=self.width-2, @@ -53,7 +58,6 @@ def __init__( self.container = Box(Frame(HSplit(widgets), title=title, width=self.width)) - def _get_formatted_text(self) -> AnyFormattedText: """Internal function for formatting entries diff --git a/jans-cli-tui/cli_tui/wui_components/jans_vetrical_nav.py b/jans-cli-tui/cli_tui/wui_components/jans_vetrical_nav.py index e6bbc4e8ea7..ede47c76f25 100644 --- a/jans-cli-tui/cli_tui/wui_components/jans_vetrical_nav.py +++ b/jans-cli-tui/cli_tui/wui_components/jans_vetrical_nav.py @@ -1,10 +1,12 @@ from prompt_toolkit.layout.containers import HSplit, Window, FloatContainer + from prompt_toolkit.layout.controls import FormattedTextControl from prompt_toolkit.layout.margins import ScrollbarMargin from prompt_toolkit.formatted_text import merge_formatted_text from prompt_toolkit.key_binding import KeyBindings from prompt_toolkit.layout.dimension import D from prompt_toolkit.widgets import HorizontalLine +from prompt_toolkit.widgets.base import Border from typing import Tuple, TypeVar, Callable from prompt_toolkit.layout.dimension import AnyDimension from typing import Optional, Sequence, Union @@ -34,6 +36,7 @@ def __init__( jans_name: Optional[str]= '', max_height: AnyDimension = None, jans_help: Optional[str]= '', + hide_headers: Optional[bool]= False, )->FloatContainer : """init for JansVerticalNav @@ -54,6 +57,7 @@ def __init__( jans_name (str, optional): Widget name max_height (int, optional): Maximum hegight of container jans_help (str, optional): Status bar help message + hide_headers (bool, optional): Hide or display headers Examples: clients = JansVerticalNav( myparent=self, @@ -87,12 +91,16 @@ def __init__( self.on_delete = on_delete self.on_display = on_display self.change_password = change_password + self.hide_headers = hide_headers + self.spaces = [len(header)+1 for header in self.headers] + if get_help: self.get_help, self.scheme = get_help if self.data : self.get_help(data=self.data[self.selectes], scheme=self.scheme) else: self.get_help= None + self.all_data=all_data self.underline_headings = underline_headings @@ -134,11 +142,14 @@ def create_window(self) -> None: style='class:select-box', height=D(preferred=len(self.data), max=len(self.data)), cursorline=True, + always_hide_cursor=True, right_margins=[ScrollbarMargin(display_arrows=True), ], ) if self.jans_help: self.list_box.jans_help = self.jans_help + headers_height = 2 if self.underline_headings else 1 + self.container_content = [ Window( content=FormattedTextControl( @@ -148,15 +159,12 @@ def create_window(self) -> None: style=self.headerColor, ), style='class:select-box', - height=D(preferred=1, max=1), + height=D(preferred=headers_height, max=headers_height), cursorline=False, ), self.list_box , ] - if self.underline_headings: - self.container_content.insert(1, HorizontalLine()) - self.container = FloatContainer( content=HSplit(self.container_content+[Window(height=1)], width=D(max=self.max_width)), floats=[], @@ -165,7 +173,8 @@ def create_window(self) -> None: def handle_header_spaces(self) -> None: """Make header evenlly spaced """ - + if not self.data: + return data = self.view_data(self.data) self.spaces = [] data_length_list = [] @@ -211,6 +220,9 @@ def get_spaced_data(self) -> list: return spaced_data def _get_head_text(self) -> AnyFormattedText: + if self.hide_headers: + return '' + """Get all headers entries Returns: @@ -222,8 +234,12 @@ def _get_head_text(self) -> AnyFormattedText: for k in range(len(self.headers)): y += self.headers[k] + ' ' * \ (self.spaces[k] - len(self.headers[k]) + 3) + result.append(y) + if self.underline_headings: + result.append('\n' + Border.HORIZONTAL*len(y)) + return merge_formatted_text(result) def _get_formatted_text(self) -> AnyFormattedText: @@ -270,6 +286,11 @@ def replace_item( self.data[item_index] = item self.handle_header_spaces() + def clear(self) -> None: + self.data = [] + self.container_content[-1].height = self.max_height + + def _get_key_bindings(self) -> KeyBindingsBase: """All key binding for the Dialog with Navigation bar @@ -329,7 +350,7 @@ def _(event): def _(event): if self.data and self.on_delete: selected_line = self.data[self.selectes] - self.on_delete(selected=selected_line, event=event, jans_name=self.jans_name) + self.on_delete(selected=selected_line, selected_idx=self.selectes, event=event, jans_name=self.jans_name) return kb diff --git a/jans-cli-tui/setup.py b/jans-cli-tui/setup.py index b8249a88923..a99dcf86fe3 100644 --- a/jans-cli-tui/setup.py +++ b/jans-cli-tui/setup.py @@ -44,6 +44,18 @@ def run(self): os.path.join(scim_yaml_dir, os.path.basename(scim_plugin_yaml_file)) ) + auth_yaml_dir = os.path.join(self.install_lib, 'cli_tui/cli/ops/auth') + if not os.path.exists(auth_yaml_dir): + os.makedirs(auth_yaml_dir, exist_ok=True) + + auth_plugin_yaml_file = 'https://raw.githubusercontent.com/JanssenProject/jans/main/jans-auth-server/docs/swagger.yaml' + print("downloding", os.path.basename(auth_plugin_yaml_file)) + urlretrieve( + auth_plugin_yaml_file, + os.path.join(auth_yaml_dir, os.path.basename(auth_plugin_yaml_file)) + ) + + def find_version(*file_paths): here = os.path.abspath(os.path.dirname(__file__)) with codecs.open(os.path.join(here, *file_paths), 'r') as f: