From c9c502b5328677bd3ef4895acf296aa3e05bb333 Mon Sep 17 00:00:00 2001 From: Devrim Date: Tue, 22 Nov 2022 10:44:27 +0300 Subject: [PATCH] feat: add jans cli tui (#2384) * fix:jans-cli remove unused imports * fix:jans-cli remove white spaces and refactor keybinding names * fix:jans-cli revert hopa changes * fix:jans-cli try search uma-resources by client id * fix:jans-cli scripts skelton - not finished (commented for now) * feat:jans-cli uma-resources search and get -tie to client (pattern is missing) * fix:jans-cli fix the focuse after wrong serach or less than 3 char * feat:jans-cli add UMA dialog to view data or delete it * fix:jans-cli clean and refactor * fix:jans-cli fix the focuse after wrong serach or less than 3 char * fix:jans-cli add parent * fix:jans-cli remove white spaces and depuging lines * fix:jans-cli add Scope (or Expression) in clients/UMA * fix:jans-cli add umaAuthorizationPolicies insted of (Claims Gathering Script-RPT Mofification Script) * fix:jans-cli uncomment self.get_data_width() * feat:jans-cli add side navbar and main content for Person Authentication * fix:jans-cli add multilang * fix:jans-cli add search button in scopes/openid/claims * fix:jans-cli add multilang support * fix:jans-cli clean code and refactor * fix:jans-cli add width to getTitledText * fix:jans-cli add doc strings * fix:jans-cli add buttons to the docs and some missing pages * fix:jans-cli add new pages and enhance structure * fix:jans-cli add images tp gallery and Home >> (URL images hosted on git> main repo) * fix:jans-cli remove toc auto hide * fix:jans-cli fix error in remove toc auto hide * fix: jans-cli url-suffix * fix:jans-cli fix get-uma-resources tie to client * fix:jans-cli comment Client URI-Policy URI-Logo URI-Term of service URI * fix:jans-cli depuging the dropdown setter * fix:jans-cli fix Authn Method token endpoint * fix:jans-cli test get-oauth-scopes pages * fix:jans-cli implement Save, add, and delete for scopes * fix:jans-cli add on_delete and implement Save, add, and delete for scopes * feat:jans-cli scopes dont write repeated field twice * fix:jans-cli comment all logger debug * fix:jans-cli add showInConfigurationEndpoint checkbox saver * fix:jans-cli add showInConfigurationEndpoint checkboxsave * fix: jans-cli add scope page is dynamic according to scope type * fix: jans-cli remove tmp file * fix:jans-cli add title to EditScopeDialog * fix:jans-cli add other tabs, save, search, deleted, and title * fix:jans-cli remove debuging lines * fix:jans-cli fix scopeType selection * fix:jans-cli remove \n from scopeType * fix:jans-cli fix typo * fix: jans-cli \n to hide black spaces * fix: jans-cli scope type fields * feat: jans-cli pagination for scopes * fix:jans-cli add pagination for clients and fix no-scopetype for scopes * fix:jans-cli remove comments and white spaces * fix:jans-cli remove the \n from wedgit titles * fix:jans-cli fix scope-dialog white spaces * fix:jans-cli fix scope-dialog white spaces - JansDialogWithNav- without navbar * fix:jans-cli add condition without self.navbar * fix:jans-cli remove a depug file * fix:jans-cli uma-rescources search and delete * fix:jans-cli fix the delete UMA-resource > clients dialog * fix:jans-cli test delete UMA * fix: jans-cli displayName of client uma resource * fix: jans-cli headers' underline is optional in lists * fix: jans-cli remove endline in list header * fix:jans-cli add style-sheet to every thing * fix:jans-cli add none to the on_enter * fix:jans-cli add style-sheet to every thing * fix:jans-cli add style for all plugins * fix:jans-cli fix style error * fix:jans-cli add style to style sheet * fix:jans-cli add style to style sheet and fix issue of focus * fix:jans-cli add all wui_componenets style * fix:jans-cli add str to fix error of None in scopes name * fix:jans-cli fix the pageup, pagedown error * fix:jans-cli add last styling * fix:jans-cli fix scopes and Expression values * fix:jans-cli fix focus issue * fix: jans-cli do not allow edit/add spontaneous scope * fix: jans-cli uma scopes are not editable * fix: jans-cli scopes tyep can't be changed to uma * fix: jans-cli enable delation of scope claims * fix: jans-cli refactor jans_vetrical_nav.py * fix:jans-cli fix issue in pre_selection * fix:jans-cli fix focus issue on last deletion >> try UMA-Res on clients dialog * fix:jans-cli remove debuging lines * fix:jans-cli fix Error in get_scopes when data=[] * fix: jans-cli store userinfo and set creator-id for uma scope (ref: #2562) * fix:jans-cli fix no data on JansVerticalNav close#2563 * fix:jans-cli fix no data on JansVerticalNav Close#2563 * fix:jans-cli fix no data on JansVerticalNav Close#2563 * fix:jans-cli claims_name instead of dn * fix: jans-cli add claims to scope * fix:jans-cli disable getting UMA resource for new client * fix:jans-cli exclude prev_selected claims from scope * fix:jans-cli test get-all-attribute * fix: jans-cli unescaped split for params * fix:jans-cli handle long column data * fix:jans-cli fix preferred size for jansvertical nav in claims * fix:jans-cli adjust column sizes in scopes * fix: jans-cli Scripts plugin skeleton * fix: jans-cli getting scripts * fix: jans-cli saving scope claims * fix: jans-cli exclude __key__ in data * fix: jans-cli search scripts * fix:jans-cli view Spontaneous Scope * feat: jans-cli edit script dialog * feat:fans-cli escape-key to exit all dialogs * feat: jans-cli alt shortcut keys * fix: jans-cli alt key shortcuts for submenu * fix:jans-cli change client dialog structure - trying to make F2 save >> self.save not in init * fix:jans-cli fix no len in escape key bindings * fix:jans-cli fix navigation on dialog open or on firrent tab * fix: jans-cli re-authorize after access token expired * fix: jans-cli script conf property edit dialog * fix: jans-cli obtain data from edit script dialog * fix: jans-cli edit script dialog * fix:jans-cli adding unit-tests for widgets * fix:jans-cli f2 for save dialogs * fix:jans-cli fix get_scoeps when no data * fix: jans-cli finish edit scripts * fix:jans-cli fix dropdown float when s+tab * fix:jans-cli fix view Spontaneous Scopes on adding new client * fix:jans-cli fix some fields types * fix:jans-cli specify types for function args * fix:jans-cli fix navbar not-required * fix:jans-cli specify types for function arg-oxauth * fix:jans-cli specify return type * fix:jans-cli specify types for function arg-scripts * fix:jans-cli specify types for function args * fix: jans-cli update config-api yaml file * fix:jans-cli fix unsaved values * fix: jans-cli remove client-api addon * fix: jans-cli Auth Serber Keys screen * feat: jans-cli auth server logging screen * feat: jans-cli logging screen * fix:jans-cli fix unsaved values * fix: jans-cli more styling * fix: jans-cli plugin initialization * fix:jans-cli specify types for function args * fix:jans-cli add some arg feat and fix scope deletiong * fix:jans-cli client and uma-res deletion * fix:jans-cli client and uma-res deletion * fix: jans-cli view based shortcuts * fix: jans-cli missing components after rebase * fix:jans-cli function args * fix: jans-cli on_page_enter * fix: jans-cli fill fido entries in background process * fix: jans-cli get appconfiguration in background process * fix:jans-cli Auth/properties get all * fix:jans-cli add init function arg for plugins * feat:jans-cli add properties tab * feat:jans-cli add search, get, buttons and popup-dialog for properties * fixt:jans-cli focus lost after wrong search * fix:jans-cli view list of dicts--not saved yet * fix: jans-cli fido2 items * fix: jans-cli only users have admin role can use TUI (ref: #2129) * fix:jans-cli Error in type of some fields >> added to TODO only * fix:jans-cli remove some un-existing values from properties * fix:jans-cli display all fields >> missing the : in view and save for all * fix:jans-cli save all except the list of dicts (ref: #2674) * feat: jans-cli FIDO Static Configuration screen * fix:jans-cli properties tab is working well * fix:jans-cli rename view_property to be lower case and delete preview.ipynb * fix:jans-cli add help for properties. client, and Scopes (ref: #2731) * fix:jans-cli add jans_help for all fields (ref: #2739) * fix:jans-cli add jans_help for all fields scopes (ref: #2739) * fix:jans-cli add jans_help for all fields scripts (ref: #2739) * fix:jans-cli error if no data * fix: jans-cli remove FIDO/Registrations tab * fix:jans-cli semi-solved for error in alt key shortcuts for submenu (ref: #2748) * feat: jans-cli integer validator (ref: #2758) * fix: jans-cli fido integer fields * fix: jans-cli re-orginise files * fix: jans-cli use auto-generated yaml files * feat: jans-cli file .enable should exists to load plugin * fix: jans-cli stop using pynput * fix: jans-cli catch exeption when getting device verification code * fix: jans-cli authorization for auto-generated swagger file * fix: jans-cli JSONWebKey * feat: jans-cli generate merged yaml file * fix: jans-cli external pyjwt module * fix: jans-cli always log * feat: jans-cli enable run remotely * fix jans-cli remote install doc * fix: jans-cli add integer validator * feat:jans-cli add config-api skelton * feat: jans-cli progress icon * fix: jans-cli more progressing * fix: jans-cli auto-generated files separately (closes #2820) * fix: jans-cli fixes for seperate yaml files * fix: jans-cli SCIM app configuration * feat: jans-cli user-management main screen * feat: jans-cli cli requests without thread * fix: jans-cli progress icon color * fix: jans-cli Error on utils when integer value and none (ref: #2866) * fix: jans-cli Operation ID change after changing yaml file #2867 #2868 * fix: jans-cli add threads for Clients Delete (ref: #2868) * feat: jans-cli edit-user dialog skeleton * fix:jans-cli revert > no threed needed (ref: Close #2868) * fix: jans-cli user-management:admin-ui roles * fix: jans-cli user-management:remove groups * feat: jans-cli Config-api (ref: #2872) * feat: jans-cli Extend Next and Prev buttons to all plugins (ref: #2875) * feat: jans-cli user-mgt: add claim * fix: jans-cli user-mgt password * feat: jans-cli fid02 save configuration * fix: jans-cli remove debug lines * feat: jans-cli user-mgt save user * feat: jans-cli typo * fix: jans-cli user-mgt finish user management * fix: jans-cli user-mgt pagination index * fix: jans-cli clients non-threaded * fix:jans-cli remove debug lines * feat:jans-cli config-api (ref: #2872 #2720) * fix:jans-cli remove transparent box (ref: #2940) * fix: jans-cli directory restructure * feat: jans-cli build * fix: jans-cli remove local yaml files * fix: jans-cli local gitignore * fix: jans-cli scim non-threaded * fix: jans-cli download scim yaml file when building * fix: jans-cli scripts non-threading and fixes * fix: jans-cli directory restructure * fix:jans-cli fix typo in responce name * fix: jans-cli delete script * fix: jans-cli device verification in exceutor * fix: jans-cli use app.loop instead of get_event_loop() * fix: jans-cli ending string * fix: jans-cli add Deletable for config-api roles (ref: #2965) * fix:jans-cli three more fields are savable now (ref: #2638) * feat: jans-cli menu for exit, logout and configure * fix:jans-cli Error on Get-Clients (ref: #2976) * fix: jans-cli fix wrong property in client properties (ref: #2638) * fix: jans-cli fix 3 wrong property in Auth/clients (ref: #2638) * fix: jans-cli call revoke session on logout * fix: jans-cli progress while revoking session * fix: jans-cli all Auth/clients are savable (ref: Close #2638) * fix:jans-cli remove necessary Comments * eat:jans-cli hotkey for top navigation focus (ref: #2994) * doc: jans-cli building pyz * docs: jans-cli-tui simplified pip3 install * fix: jans-cli admin-ui roles asyncio * fix: jans-cli saving scopes * fix: jans-cli display reason for not deleting admin-ui role * fix: jans-cli save admin-ui roles in asyncio * fix: disable Config-API if admin-ui plugin is not available * fix: jans-cli search scope (ref: #3045) * fix: jans-cli fido2 (ref: #3046) Co-authored-by: AbdelwahabAdam --- jans-cli-tui/.gitignore | 137 ++ jans-cli-tui/LICENSE | 201 +++ jans-cli-tui/Makefile | 13 + jans-cli-tui/README.md | 145 ++ jans-cli-tui/cli_tui/__init__.py | 0 jans-cli-tui/cli_tui/cli/__init__.py | 0 jans-cli-tui/cli_tui/cli/config_cli.py | 1555 +++++++++++++++++ jans-cli-tui/cli_tui/cli_style.py | 138 ++ jans-cli-tui/cli_tui/jans_cli_tui.py | 706 ++++++++ jans-cli-tui/cli_tui/mkdocs.yml | 136 ++ .../cli_tui/plugins/010_oxauth/.enabled | 0 .../cli_tui/plugins/010_oxauth/__init__.py | 0 .../plugins/010_oxauth/edit_client_dialog.py | 976 +++++++++++ .../plugins/010_oxauth/edit_scope_dialog.py | 469 +++++ .../cli_tui/plugins/010_oxauth/main.py | 930 ++++++++++ .../plugins/010_oxauth/view_property.py | 385 ++++ .../plugins/010_oxauth/view_uma_dialog.py | 216 +++ .../cli_tui/plugins/020_fido/.enabled | 0 .../cli_tui/plugins/020_fido/__init__.py | 0 jans-cli-tui/cli_tui/plugins/020_fido/main.py | 285 +++ .../cli_tui/plugins/030_scim/.enabled | 0 .../cli_tui/plugins/030_scim/__init__.py | 0 jans-cli-tui/cli_tui/plugins/030_scim/main.py | 134 ++ .../cli_tui/plugins/040_config_api/.enabled | 0 .../plugins/040_config_api/__init__.py | 0 .../cli_tui/plugins/040_config_api/main.py | 915 ++++++++++ .../cli_tui/plugins/060_scripts/.enabled | 0 .../cli_tui/plugins/060_scripts/__init__.py | 0 .../plugins/060_scripts/edit_script_dialog.py | 421 +++++ .../cli_tui/plugins/060_scripts/main.py | 255 +++ .../cli_tui/plugins/070_users/.enabled | 0 .../cli_tui/plugins/070_users/__init__.py | 0 .../plugins/070_users/edit_user_dialog.py | 261 +++ .../cli_tui/plugins/070_users/main.py | 277 +++ .../cli_tui/plugins/999_jans/.enabled | 0 .../cli_tui/plugins/999_jans/__init__.py | 0 jans-cli-tui/cli_tui/plugins/999_jans/main.py | 84 + jans-cli-tui/cli_tui/plugins/__init__.py | 0 jans-cli-tui/cli_tui/utils/__init__.py | 0 jans-cli-tui/cli_tui/utils/multi_lang.py | 15 + jans-cli-tui/cli_tui/utils/static.py | 6 + jans-cli-tui/cli_tui/utils/utils.py | 75 + jans-cli-tui/cli_tui/utils/validators.py | 20 + jans-cli-tui/cli_tui/version.py | 6 + .../cli_tui/wui_components/__init__.py | 0 .../cli_tui/wui_components/jans_cli_dialog.py | 66 + .../wui_components/jans_data_picker.py | 549 ++++++ .../cli_tui/wui_components/jans_dialog.py | 64 + .../wui_components/jans_dialog_with_nav.py | 137 ++ .../cli_tui/wui_components/jans_drop_down.py | 269 +++ .../wui_components/jans_message_dialog.py | 76 + .../cli_tui/wui_components/jans_nav_bar.py | 174 ++ .../wui_components/jans_side_nav_bar.py | 145 ++ .../cli_tui/wui_components/jans_spinner.py | 51 + .../wui_components/jans_vetrical_nav.py | 322 ++++ jans-cli-tui/docs/build.md | 23 + jans-cli-tui/docs/docs/Gallery/cli.md | 12 + jans-cli-tui/docs/docs/Gallery/gallery.md | 26 + jans-cli-tui/docs/docs/Gallery/tui.md | 1 + .../installation/dynamic-download.md | 29 + .../docs/getting_started/installation/rhel.md | 30 + .../docs/getting_started/installation/suse.md | 23 + .../getting_started/installation/ubuntu.md | 26 + .../installation/vm-requirements.md | 8 + jans-cli-tui/docs/docs/home/about.md | 2 + jans-cli-tui/docs/docs/home/index.md | 10 + .../docs/docs/home/janssen_modules.md | 23 + .../docs/plugins/client_api/client_api.md | 1 + .../docs/plugins/config_api/config_api.md | 1 + jans-cli-tui/docs/docs/plugins/fido/fido.md | 1 + .../docs/plugins/oauth/edit_client_dialog.md | 1 + .../docs/plugins/oauth/edit_scope_dialog.md | 1 + .../docs/plugins/oauth/edit_uma_dialog.md | 1 + jans-cli-tui/docs/docs/plugins/oauth/oauth.md | 1 + jans-cli-tui/docs/docs/plugins/plugins.md | 11 + jans-cli-tui/docs/docs/plugins/scim/scim.md | 1 + .../docs/docs/plugins/scripts/scripts.md | 1 + jans-cli-tui/docs/docs/stylesheets/extra.css | 5 + .../docs/wui_components/jans_cli_dialog.md | 4 + .../docs/wui_components/jans_data_picker.md | 2 + .../docs/docs/wui_components/jans_dialog.md | 2 + .../wui_components/jans_dialog_with_nav.md | 2 + .../docs/wui_components/jans_drop_down.md | 2 + .../wui_components/jans_message_dialog.md | 2 + .../docs/docs/wui_components/jans_nav_bar.md | 2 + .../docs/wui_components/jans_side_nav_bar.md | 2 + .../docs/wui_components/jans_vetrical_nav.md | 2 + .../docs/wui_components/wui_components.md | 45 + jans-cli-tui/docs/img/favicon.ico | Bin 0 -> 15406 bytes jans-cli-tui/docs/img/logo.png | Bin 0 -> 4373 bytes jans-cli-tui/docs/img/new_tui/new_tui1.png | Bin 0 -> 95761 bytes jans-cli-tui/docs/remote_install.md | 24 + jans-cli-tui/run_test.py | 125 ++ jans-cli-tui/setup.py | 88 + jans-cli-tui/test.py | 147 ++ jans-cli-tui/wrapper_test.py | 185 ++ 96 files changed, 11486 insertions(+) create mode 100644 jans-cli-tui/.gitignore create mode 100644 jans-cli-tui/LICENSE create mode 100644 jans-cli-tui/Makefile create mode 100644 jans-cli-tui/README.md create mode 100644 jans-cli-tui/cli_tui/__init__.py create mode 100644 jans-cli-tui/cli_tui/cli/__init__.py create mode 100755 jans-cli-tui/cli_tui/cli/config_cli.py create mode 100755 jans-cli-tui/cli_tui/cli_style.py create mode 100755 jans-cli-tui/cli_tui/jans_cli_tui.py create mode 100755 jans-cli-tui/cli_tui/mkdocs.yml create mode 100644 jans-cli-tui/cli_tui/plugins/010_oxauth/.enabled create mode 100644 jans-cli-tui/cli_tui/plugins/010_oxauth/__init__.py create mode 100755 jans-cli-tui/cli_tui/plugins/010_oxauth/edit_client_dialog.py create mode 100755 jans-cli-tui/cli_tui/plugins/010_oxauth/edit_scope_dialog.py create mode 100755 jans-cli-tui/cli_tui/plugins/010_oxauth/main.py create mode 100644 jans-cli-tui/cli_tui/plugins/010_oxauth/view_property.py create mode 100644 jans-cli-tui/cli_tui/plugins/010_oxauth/view_uma_dialog.py create mode 100644 jans-cli-tui/cli_tui/plugins/020_fido/.enabled create mode 100644 jans-cli-tui/cli_tui/plugins/020_fido/__init__.py create mode 100755 jans-cli-tui/cli_tui/plugins/020_fido/main.py create mode 100644 jans-cli-tui/cli_tui/plugins/030_scim/.enabled create mode 100644 jans-cli-tui/cli_tui/plugins/030_scim/__init__.py create mode 100755 jans-cli-tui/cli_tui/plugins/030_scim/main.py create mode 100644 jans-cli-tui/cli_tui/plugins/040_config_api/.enabled create mode 100644 jans-cli-tui/cli_tui/plugins/040_config_api/__init__.py create mode 100755 jans-cli-tui/cli_tui/plugins/040_config_api/main.py create mode 100644 jans-cli-tui/cli_tui/plugins/060_scripts/.enabled create mode 100644 jans-cli-tui/cli_tui/plugins/060_scripts/__init__.py create mode 100755 jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py create mode 100755 jans-cli-tui/cli_tui/plugins/060_scripts/main.py create mode 100644 jans-cli-tui/cli_tui/plugins/070_users/.enabled create mode 100644 jans-cli-tui/cli_tui/plugins/070_users/__init__.py create mode 100644 jans-cli-tui/cli_tui/plugins/070_users/edit_user_dialog.py create mode 100755 jans-cli-tui/cli_tui/plugins/070_users/main.py create mode 100644 jans-cli-tui/cli_tui/plugins/999_jans/.enabled create mode 100644 jans-cli-tui/cli_tui/plugins/999_jans/__init__.py create mode 100755 jans-cli-tui/cli_tui/plugins/999_jans/main.py create mode 100644 jans-cli-tui/cli_tui/plugins/__init__.py create mode 100644 jans-cli-tui/cli_tui/utils/__init__.py create mode 100755 jans-cli-tui/cli_tui/utils/multi_lang.py create mode 100755 jans-cli-tui/cli_tui/utils/static.py create mode 100755 jans-cli-tui/cli_tui/utils/utils.py create mode 100644 jans-cli-tui/cli_tui/utils/validators.py create mode 100644 jans-cli-tui/cli_tui/version.py create mode 100644 jans-cli-tui/cli_tui/wui_components/__init__.py create mode 100755 jans-cli-tui/cli_tui/wui_components/jans_cli_dialog.py create mode 100755 jans-cli-tui/cli_tui/wui_components/jans_data_picker.py create mode 100755 jans-cli-tui/cli_tui/wui_components/jans_dialog.py create mode 100755 jans-cli-tui/cli_tui/wui_components/jans_dialog_with_nav.py create mode 100755 jans-cli-tui/cli_tui/wui_components/jans_drop_down.py create mode 100755 jans-cli-tui/cli_tui/wui_components/jans_message_dialog.py create mode 100755 jans-cli-tui/cli_tui/wui_components/jans_nav_bar.py create mode 100755 jans-cli-tui/cli_tui/wui_components/jans_side_nav_bar.py create mode 100644 jans-cli-tui/cli_tui/wui_components/jans_spinner.py create mode 100644 jans-cli-tui/cli_tui/wui_components/jans_vetrical_nav.py create mode 100644 jans-cli-tui/docs/build.md create mode 100755 jans-cli-tui/docs/docs/Gallery/cli.md create mode 100755 jans-cli-tui/docs/docs/Gallery/gallery.md create mode 100755 jans-cli-tui/docs/docs/Gallery/tui.md create mode 100644 jans-cli-tui/docs/docs/getting_started/installation/dynamic-download.md create mode 100644 jans-cli-tui/docs/docs/getting_started/installation/rhel.md create mode 100644 jans-cli-tui/docs/docs/getting_started/installation/suse.md create mode 100644 jans-cli-tui/docs/docs/getting_started/installation/ubuntu.md create mode 100644 jans-cli-tui/docs/docs/getting_started/installation/vm-requirements.md create mode 100755 jans-cli-tui/docs/docs/home/about.md create mode 100755 jans-cli-tui/docs/docs/home/index.md create mode 100644 jans-cli-tui/docs/docs/home/janssen_modules.md create mode 100755 jans-cli-tui/docs/docs/plugins/client_api/client_api.md create mode 100755 jans-cli-tui/docs/docs/plugins/config_api/config_api.md create mode 100755 jans-cli-tui/docs/docs/plugins/fido/fido.md create mode 100755 jans-cli-tui/docs/docs/plugins/oauth/edit_client_dialog.md create mode 100755 jans-cli-tui/docs/docs/plugins/oauth/edit_scope_dialog.md create mode 100755 jans-cli-tui/docs/docs/plugins/oauth/edit_uma_dialog.md create mode 100755 jans-cli-tui/docs/docs/plugins/oauth/oauth.md create mode 100755 jans-cli-tui/docs/docs/plugins/plugins.md create mode 100755 jans-cli-tui/docs/docs/plugins/scim/scim.md create mode 100755 jans-cli-tui/docs/docs/plugins/scripts/scripts.md create mode 100755 jans-cli-tui/docs/docs/stylesheets/extra.css create mode 100755 jans-cli-tui/docs/docs/wui_components/jans_cli_dialog.md create mode 100755 jans-cli-tui/docs/docs/wui_components/jans_data_picker.md create mode 100755 jans-cli-tui/docs/docs/wui_components/jans_dialog.md create mode 100755 jans-cli-tui/docs/docs/wui_components/jans_dialog_with_nav.md create mode 100755 jans-cli-tui/docs/docs/wui_components/jans_drop_down.md create mode 100755 jans-cli-tui/docs/docs/wui_components/jans_message_dialog.md create mode 100755 jans-cli-tui/docs/docs/wui_components/jans_nav_bar.md create mode 100755 jans-cli-tui/docs/docs/wui_components/jans_side_nav_bar.md create mode 100755 jans-cli-tui/docs/docs/wui_components/jans_vetrical_nav.md create mode 100755 jans-cli-tui/docs/docs/wui_components/wui_components.md create mode 100755 jans-cli-tui/docs/img/favicon.ico create mode 100755 jans-cli-tui/docs/img/logo.png create mode 100755 jans-cli-tui/docs/img/new_tui/new_tui1.png create mode 100644 jans-cli-tui/docs/remote_install.md create mode 100644 jans-cli-tui/run_test.py create mode 100644 jans-cli-tui/setup.py create mode 100644 jans-cli-tui/test.py create mode 100755 jans-cli-tui/wrapper_test.py diff --git a/jans-cli-tui/.gitignore b/jans-cli-tui/.gitignore new file mode 100644 index 00000000000..467d9cfe18f --- /dev/null +++ b/jans-cli-tui/.gitignore @@ -0,0 +1,137 @@ +swagger_yaml.json +config.ini + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +.idea/ +.idea + +# do not upload local yaml files +cli_tui/cli/ops/ diff --git a/jans-cli-tui/LICENSE b/jans-cli-tui/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/jans-cli-tui/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/jans-cli-tui/Makefile b/jans-cli-tui/Makefile new file mode 100644 index 00000000000..0fb3c8f47fe --- /dev/null +++ b/jans-cli-tui/Makefile @@ -0,0 +1,13 @@ +.DEFAULT_GOAL := develop + +develop: + pip3 install -e . + +install: + pip3 install . + +uninstall: + pip3 uninstall jans-cli-tui -y + +zipapp: + shiv --compressed -o config-cli-tui.pyz -p '/usr/bin/env python3' -e cli_tui.jans_cli_tui:run . --no-cache diff --git a/jans-cli-tui/README.md b/jans-cli-tui/README.md new file mode 100644 index 00000000000..3c026839c95 --- /dev/null +++ b/jans-cli-tui/README.md @@ -0,0 +1,145 @@ +# _Janssen Command Line Interface_ +`jans-cli` is a **Command Line Interface** for Janssen Configuration. It also has `menu-driven` interface that makes it easier to understand how to use [Janssen Server](https://github.com/JanssenProject/home) through the Interactive Mode. + +Table of Contents +================= + + * [Janssen Command Line Interface](#janssen-command-line-interface) + * [Installation](#installation) + * [Quick Start](#quick-start) + +# _Installation_ + +You can directly download the `jans-cli` package file as below: + +### For macOs: + +``` +wget https://github.com/JanssenProject/jans-cli/releases/latest/download/jans-cli-macos-amd64.pyz +``` + +### for linux: + +``` +wget https://github.com/JanssenProject/jans-cli/releases/latest/download/jans-cli-linux-amd64.pyz +``` + +## Build `jans-cli.pyz` manually + +If you would like to build `jans-cli` manually, you can go through the following steps noted here: + +## Prerequisites +1. wget +1. unzip +1. Python 3.6+. +1. Python `pip3` package. + +### Building + +1. Install dependencies + + ```sh + apt install -y wget unzip python3-pip python3-dev + pip3 install shiv + ``` + +2. Download the repository: + + ```sh + wget https://github.com/JanssenProject/jans/archive/refs/heads/main.zip + ``` + +3. Unzip package, and change to directory + + ```sh + unzip main.zip + cd jans-main/jans-cli + ``` + +4. Build + + ```sh + make zipapp + ``` + +You can verify with the following command line if everything is done successfully. + +``` +python3 config-cli.pyz -h +``` + + +### Standard Python package +1. Install venv module + ```sh + pip3 install virtualenv + ``` + +1. Create virtual environment and activate: + + ```sh + python3 -m virtualenv .venv + source .venv/bin/activate + ``` + +1. Download and install the package: + + ``` + wget https://github.com/JanssenProject/jans/archive/refs/heads/main.zip + unzip main.zip + cd jans-main/jans-cli + make install + ``` + + This command will install executable called `jans-cli` available in virtual environment `PATH`. + + +![](../docs/assets/image-build-jans-cli-pyz-manually-03042021.png) + + +## Virtual Machine Setup + +**jans-cli** is automatically installed if you choose `jans-config-api` during [Janssen Server](https://github.com/JanssenProject/home/blob/main/development.md#install-janssen-into-vm) Installation on Virtual Machine. + +![](../docs/assets/image-jans-config-api-03042021.png) + +After successfully installed Janssen Server, you will get two command-line arguments as below: + +![](../docs/assets/image-installed-03042021.png) + +# _Quick Start_ + +As you have seen, CLI supports both of the `config-cli` and `scim-cli`. For a quick start, let's run the following command. + +``` +/opt/jans/jans-cli/config-cli.py +``` +If you get an error, you can try in this way: + +``` +python3 /opt/jans/jans-cli/config-cli.py +``` + +Alternatively, you can make python3 to default version: +``` +sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 +/opt/jans/jans-cli/config-cli.py +``` + +You will get a menu as below image: + +![main-menu.png](../docs/assets/image-im-main-03042021.png) + +From the following list, you can choose any options by selecting its number. For example, let's say number 2, +to get **Default Authentication Method**. + +That returns another two options as below: + +![option-2-option.png](../docs/assets/image-im-default-auth-02-03042021.png) + +Now selecting 1 and it returns our desired result as below image: + +![default-authentication-method.png](../docs/assets/image-im-cur-default-auth-03042021.png) + +So, That was a quick start to view how this _jans-cli_ Interactive Mode works. Please, follow this [link](../docs/admin/jans-cli) to read the _jans-cli_ docs for a better understanding of the Janssen Command-Line. + diff --git a/jans-cli-tui/cli_tui/__init__.py b/jans-cli-tui/cli_tui/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/cli/__init__.py b/jans-cli-tui/cli_tui/cli/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/cli/config_cli.py b/jans-cli-tui/cli_tui/cli/config_cli.py new file mode 100755 index 00000000000..af8f3fdfb57 --- /dev/null +++ b/jans-cli-tui/cli_tui/cli/config_cli.py @@ -0,0 +1,1555 @@ +#!/usr/bin/env python3 + +import sys +import os +import json +import re +import urllib3 +import configparser +import readline +import argparse +import random +import datetime +import code +import traceback +import ast +import base64 +import requests +import html +import glob +import logging +import http.client +import jwt +import pyDes +import stat +import ruamel.yaml + + +from pathlib import Path +from types import SimpleNamespace +from urllib.parse import urlencode +from collections import OrderedDict +from urllib.parse import urljoin +from http.client import HTTPConnection +from pygments import highlight, lexers, formatters + +home_dir = Path.home() +config_dir = home_dir.joinpath('.config') +config_dir.mkdir(parents=True, exist_ok=True) +config_ini_fn = config_dir.joinpath('jans-cli.ini') +cur_dir = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(cur_dir) + + + +my_op_mode = 'scim' if 'scim' in os.path.basename(sys.argv[0]) else 'jca' +plugins = [] + +warning_color = 214 +error_color = 196 +success_color = 10 +bold_color = 15 +grey_color = 242 + + +def clear(): + if not debug: + os.system('clear') + +urllib3.disable_warnings() +config = configparser.ConfigParser() + +host = os.environ.get('jans_host') +client_id = os.environ.get(my_op_mode + '_client_id') +client_secret = os.environ.get(my_op_mode + '_client_secret') +access_token = None +debug = os.environ.get('jans_client_debug') +log_dir = os.environ.get('cli_log_dir', os.path.join('jans_cli_logs', home_dir)) + +if not os.path.exists(log_dir): + os.makedirs(log_dir, exist_ok=True) + +salt_fn = '/etc/jans/conf/salt' +if not os.path.exists(salt_fn): + salt_fn = os.path.join(config_dir, 'jans-cli-salt') + if not os.path.exists(salt_fn): + with open(salt_fn, 'w') as w: + w.write('encodeSalt = {}'.format(os.urandom(12).hex())) + os.chmod(salt_fn, stat.S_IREAD|stat.S_IWRITE) + +with open(salt_fn) as f: + salt_property = f.read() + +key = salt_property.split("=")[1].strip() + +def obscure(data=''): + engine = pyDes.triple_des(key, pyDes.ECB, pad=None, padmode=pyDes.PAD_PKCS5) + data = data.encode('utf-8') + en_data = engine.encrypt(data) + return base64.b64encode(en_data).decode('utf-8') + +def unobscure(s=''): + engine = pyDes.triple_des(key, pyDes.ECB, pad=None, padmode=pyDes.PAD_PKCS5) + cipher = pyDes.triple_des(key) + decrypted = cipher.decrypt(base64.b64decode(s), padmode=pyDes.PAD_PKCS5) + return decrypted.decode('utf-8') + + +name_regex = re.compile('[^a-zA-Z0-9]') + +def get_named_tag(tag): + return name_regex.sub('', tag.title()) + +def get_plugin_name_from_title(title): + n = title.find('-') + if n > -1: + return title[n+1:].strip() + return '' + +# load yaml files +cfg_yaml = {} +op_list = [] +for opmode in os.listdir(os.path.join(cur_dir, 'ops')): + cfg_yaml[opmode] = {} + for yaml_fn in glob.glob(os.path.join(cur_dir, 'ops', opmode, '*.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[opmode][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) + +op_list.sort() + +parser = argparse.ArgumentParser() +parser.add_argument("--host", help="Hostname of server") +parser.add_argument("--client-id", help="Jans Config Api Client ID") +parser.add_argument("--client-secret", "--client_secret", help="Jans Config Api Client ID secret") +parser.add_argument("--access-token", help="JWT access token or path to file containing JWT access token") +parser.add_argument("--plugins", help="Available plugins separated by comma") +parser.add_argument("-debug", help="Run in debug mode", action='store_true') +parser.add_argument("--debug-log-file", default='swagger.log', help="Log file name when run in debug mode") +parser.add_argument("--operation-id", help="Operation ID to be done") +parser.add_argument("--url-suffix", help="Argument to be added api endpoint url. For example inum:2B29") +parser.add_argument("--info", choices=op_list, help="Help for operation") +parser.add_argument("--op-mode", choices=['get', 'post', 'put', 'patch', 'delete'], default='get', + help="Operation mode to be done") +parser.add_argument("--endpoint-args", + help="Arguments to pass endpoint separated by comma. For example limit:5,status:INACTIVE") +parser.add_argument("--schema", help="Get sample json schema") + +parser.add_argument("-CC", "--config-api-mtls-client-cert", help="Path to SSL Certificate file") +parser.add_argument("-CK", "--config-api-mtls-client-key", help="Path to SSL Key file") +parser.add_argument("--key-password", help="Password for SSL Key file") +parser.add_argument("-noverify", help="Ignore verifying the SSL certificate", action='store_true', default=True) + +parser.add_argument("-use-test-client", help="Use test client without device authorization", action='store_true') + + +parser.add_argument("--patch-add", help="Colon delimited key:value pair for add patch operation. For example loggingLevel:DEBUG") +parser.add_argument("--patch-replace", help="Colon delimited key:value pair for replace patch operation. For example loggingLevel:DEBUG") +parser.add_argument("--patch-remove", help="Key for remove patch operation. For example imgLocation") +parser.add_argument("-no-color", help="Do not colorize json dumps", action='store_true') +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("--data", help="Path to json data file") +args = parser.parse_args() + + +################## end of arguments ################# + +test_client = args.use_test_client + + +if args.plugins: + for plugin in args.plugins.split(','): + plugins.append(plugin.strip()) + + +if not(host and (client_id and client_secret or access_token)): + host = args.host + client_id = args.client_id + client_secret = args.client_secret + debug = args.debug + log_dir = args.log_dir + + access_token = args.access_token + if access_token and os.path.isfile(access_token): + with open(access_token) as f: + access_token = f.read() + + +if not(host and (client_id and client_secret or access_token)): + + if config_ini_fn.exists(): + config.read_string(config_ini_fn.read_text()) + host = config['DEFAULT']['jans_host'] + + if 'jca_test_client_id' in config['DEFAULT'] and test_client: + client_id = config['DEFAULT']['jca_test_client_id'] + secret_key_str = 'jca_test_client_secret' + else: + client_id = config['DEFAULT']['jca_client_id'] + secret_key_str = 'jca_client_secret' + + secret_enc_key_str = secret_key_str + '_enc' + if config['DEFAULT'].get(secret_key_str): + client_secret = config['DEFAULT'][secret_key_str] + elif config['DEFAULT'].get(secret_enc_key_str): + client_secret_enc = config['DEFAULT'][secret_enc_key_str] + client_secret = unobscure(client_secret_enc) + + if 'access_token' in config['DEFAULT']: + access_token = config['DEFAULT']['access_token'] + elif 'access_token_enc' in config['DEFAULT']: + access_token = unobscure(config['DEFAULT']['access_token_enc']) + + debug = config['DEFAULT'].get('debug') + log_dir = config['DEFAULT'].get('log_dir', log_dir) + + +def get_bool(val): + if str(val).lower() in ('yes', 'true', '1', 'on'): + return True + return False + +def write_config(): + with open(config_ini_fn, 'w') as w: + config.write(w) + os.chmod(config_ini_fn, stat.S_IREAD|stat.S_IWRITE) + +debug = get_bool(debug) + + +class JCA_CLI: + + def __init__(self, host, client_id, client_secret, access_token, test_client=False): + self.host = self.idp_host = host + self.client_id = client_id + self.client_secret = client_secret + self.use_test_client = test_client + self.getCredentials() + self.wrapped = __name__ != "__main__" + self.access_token = access_token or config['DEFAULT'].get('access_token') + self.jwt_validation_url = 'https://{}/jans-config-api/api/v1/acrs'.format(self.idp_host) + self.discovery_endpoint = '/.well-known/openid-configuration' + self.openid_configuration = {} + self.set_user() + self.plugins() + + if my_op_mode == 'jca': + self.host += '/jans-config-api' + + if my_op_mode == 'scim': + self.host += '/jans-scim/restv1/v2' + + self.set_logging() + self.ssl_settings() + + + def getCredentials(self): + if self.host == '' or self.client_id == '' or self.client_secret == '' : + if config_ini_fn.exists(): + config.read_string(config_ini_fn.read_text()) + host_data = config['DEFAULT']['jans_host'] + + if 'jca_test_client_id' in config['DEFAULT'] and test_client: + client_id_data = config['DEFAULT']['jca_test_client_id'] + secret_key_str = 'jca_test_client_secret' + else: + client_id_data = config['DEFAULT']['jca_client_id'] + secret_key_str = 'jca_client_secret' + + secret_enc_key_str = secret_key_str + '_enc' + if config['DEFAULT'].get(secret_key_str): + client_secret_data = config['DEFAULT'][secret_key_str] + elif config['DEFAULT'].get(secret_enc_key_str): + client_secret_enc = config['DEFAULT'][secret_enc_key_str] + client_secret_data = unobscure(client_secret_enc) + + self.host = self.idp_host=host_data.replace("'","") + self.client_id = client_id_data.replace("'","") + self.client_secret = client_secret_data.replace("'","") + + def get_user_info(self): + user_info = {} + if 'user_data' in config['DEFAULT']: + user_info = jwt.decode(config['DEFAULT']['user_data'], + options={ + 'verify_signature': False, + 'verify_exp': True, + 'verify_aud': False + } + ) + return user_info + + + def set_logging(self): + self.cli_logger = logging.getLogger("urllib3") + self.cli_logger.setLevel(logging.DEBUG) + self.cli_logger.propagate = True + HTTPConnection.debuglevel = 1 + file_handler = logging.FileHandler(os.path.join(log_dir, 'cli_debug.log')) + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s")) + self.cli_logger.addHandler(file_handler) + def print_to_log(*args): + self.cli_logger.debug(" ".join(args)) + http.client.print = print_to_log + + + def log_response(self, response): + if debug: + self.cli_logger.debug('requests response status: %s', str(response.status_code)) + self.cli_logger.debug('requests response headers: %s', str(response.headers)) + self.cli_logger.debug('requests response text: %s', str(response.text)) + + def set_user(self): + self.auth_username = None + self.auth_password = None + self.askuser = get_bool(config['DEFAULT'].get('askuser')) + + if self.askuser: + if args.username: + self.auth_username = args.username + if args.password: + self.auth_password = args.password + elif args.j: + if os.path.isfile(args.j): + with open(args.j) as reader: + self.auth_password = reader.read() + else: + print(args.j, "does not exist. Exiting ...") + sys.exit() + if not (self.auth_username and self.auth_password): + print("I need username and password. Exiting ...") + sys.exit() + + def plugins(self): + for plugin_s in config['DEFAULT'].get(my_op_mode + '_plugins', '').split(','): + plugin = plugin_s.strip() + if plugin: + plugins.append(plugin) + + def ssl_settings(self): + if args.noverify: + self.verify_ssl = False + else: + self.verify_ssl = True + self.mtls_client_cert = None + if args.config_api_mtls_client_cert and args.config_api_mtls_client_key: + self.mtls_client_cert = (args.config_api_mtls_client_cert, args.config_api_mtls_client_key) + + def drop_to_shell(self, mylocals): + locals_ = locals() + locals_.update(mylocals) + code.interact(local=locals_) + sys.exit() + + def get_request_header(self, headers={}, access_token=None): + if not access_token: + access_token = self.access_token + + ret_val = {'Authorization': 'Bearer {}'.format(access_token)} + ret_val.update(headers) + return ret_val + + + def check_connection(self): + self.cli_logger.debug("Checking connection") + url = 'https://{}/jans-auth/restv1/token'.format(self.idp_host) + try: + + response = requests.post( + url=url, + auth=(self.client_id, self.client_secret), + data={"grant_type": "client_credentials"}, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + except Exception as e: + self.cli_logger.error(str(e)) + if self.wrapped: + return str(e) + + raise ValueError( + self.colored_text("Unable to connect jans-auth server:\n {}".format(str(e)), error_color)) + + + self.log_response(response) + if response.status_code != 200: + if self.wrapped: + return response.text + + raise ValueError( + self.colored_text("Unable to connect jans-auth server:\n {}".format(response.text), error_color)) + + if not self.use_test_client and self.access_token: + response = requests.get( + url = self.jwt_validation_url, + headers=self.get_request_header({'Accept': 'application/json'}), + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + + if not response.status_code == 200: + config['DEFAULT']['access_token_enc'] = '' + self.access_token = None + write_config() + return response.text + + try: + response = requests.get( + url = 'https://{}{}'.format(self.idp_host, self.discovery_endpoint), + headers=self.get_request_header({'Accept': 'application/json'}), + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + except Exception as e: + self.cli_logger.error(str(e)) + if self.wrapped: + return str(e) + + raise ValueError( + self.colored_text("Unable to get OpenID configuration:\n {}".format(str(e)), error_color)) + + self.openid_configuration = response.json() + + return True + + def revoke_session(self): + self.cli_logger.debug("Revoking session info") + url = 'https://{}/jans-auth/restv1/revoke'.format(self.idp_host) + + try: + + response = requests.post( + url=url, + auth=(self.client_id, self.client_secret), + data={"token": self.access_token, 'token_type_hint': 'access_token'}, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + except Exception as e: + self.cli_logger.error(str(e)) + if self.wrapped: + return str(e) + + raise ValueError( + self.colored_text("Unable to connect jans-auth server:\n {}".format(str(e)), error_color)) + + + self.log_response(response) + + if self.wrapped: + return response + else: + print(response.status_code) + print(response.text) + + + + def check_access_token(self): + + if not self.access_token : + print(self.colored_text("Access token was not found.", warning_color)) + return + + try: + jwt.decode(self.access_token, + options={ + 'verify_signature': False, + 'verify_exp': True, + 'verify_aud': False + } + ) + except Exception as e: + print(self.colored_text("Unable to validate access token: {}".format(e), error_color)) + self.access_token = None + + + def validate_date_time(self, date_str): + try: + datetime.datetime.fromisoformat(date_str) + return True + except Exception as e: + self.log_response('invalid date-time format: %s'.format(str(e))) + return False + + + def get_scoped_access_token(self, scope): + + if not self.wrapped: + scope_text = " for scope {}\n".format(scope) if scope else '' + sys.stderr.write("Getting access token{}".format(scope_text)) + + url = 'https://{}/jans-auth/restv1/token'.format(self.idp_host) + + if self.askuser: + post_params = {"grant_type": "password", "scope": scope, "username": self.auth_username, + "password": self.auth_password} + else: + post_params = {"grant_type": "client_credentials", "scope": scope} + + response = requests.post( + url, + auth=(self.use_test_client, self.client_secret), + data=post_params, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + self.log_response(response) + try: + result = response.json() + if 'access_token' in result: + self.access_token = result['access_token'] + else: + sys.stderr.write("Error while getting access token") + sys.stderr.write(result) + sys.stderr.write('\n') + except Exception as e: + print("Error while getting access token") + sys.stderr.write(response.text) + sys.stderr.write(str(e)) + sys.stderr.write('\n') + + def get_device_authorization (self): + response = requests.post( + url='https://{}/jans-auth/restv1/device_authorization'.format(self.idp_host), + auth=(self.client_id, self.client_secret), + data={'client_id': self.client_id, 'scope': 'openid+profile+email+offline_access'}, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + self.log_response(response) + if response.status_code != 200: + raise ValueError( + self.colored_text("Unable to get device authorization user code: {}".format(response.reason), error_color)) + + return response.json() + + + def get_device_verification_code(self): + response = requests.post( + url='https://{}/jans-auth/restv1/device_authorization'.format(self.idp_host), + auth=(self.client_id, self.client_secret), + data={'client_id': self.client_id, 'scope': 'openid+profile+email+offline_access'}, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + + self.log_response(response) + + return response + + + def raise_error(self, msg): + if not self.wrapped: + msg = self.colored_text(msg, error_color) + print(msg) + sys.exit() + + raise ValueError(msg) + + + def get_jwt_access_token(self, device_verified=None): + + + """ + STEP 1: Get device verification code + This fucntion requests user code from jans-auth, print result and + waits untill verification done. + """ + if not device_verified: + response = self.get_device_verification_code() + if response.status_code != 200: + msg = "Unable to get device authorization user code: {}".format(response.reason) + self.raise_error(msg) + + result = response.json() + + if 'verification_uri' in result and 'user_code' in result: + + msg = "Please visit verification url {} and enter user code {} within {} secods".format( + self.colored_text(result['verification_uri'], success_color), + self.colored_text(result['user_code'], bold_color), + result['expires_in'] + ) + print(msg) + input(self.colored_text("Please press «Enter» when ready", warning_color)) + + else: + msg = "Unable to get device authorization user code" + self.raise_error(msg) + + else: + result = device_verified + + """ + STEP 2: Get access token for retrieving user info + After device code was verified, we use it to retreive refresh token + """ + response = requests.post( + url='https://{}/jans-auth/restv1/token'.format(self.idp_host), + auth=(self.client_id, self.client_secret), + data=[ + ('client_id',self.client_id), + ('scope','openid+profile+email+offline_access'), + ('grant_type', 'urn:ietf:params:oauth:grant-type:device_code'), + ('grant_type', 'refresh_token'), + ('device_code',result['device_code']) + ], + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + self.log_response(response) + if response.status_code != 200: + self.raise_error("Unable to get access token") + + result = response.json() + + headers_basic_auth = self.get_request_header(access_token=result['access_token']) + + """ + STEP 3: Get user info + refresh token is used for retrieving user information to identify user roles + """ + response = requests.post( + url='https://{}/jans-auth/restv1/userinfo'.format(self.idp_host), + headers=headers_basic_auth, + data={'access_token': result['access_token']}, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + self.log_response(response) + if response.status_code != 200: + self.raise_error("Unable to get user info") + + + result = response.text + config['DEFAULT']['user_data'] = result + + + user_info = self.get_user_info() + + if 'api-admin' not in user_info.get('jansAdminUIRole', []): + config['DEFAULT']['user_data'] = '' + self.raise_error("The logged user do not have valid role.") + + """ + STEP 4: Get access token for config-api endpoints + Use client creditentials to retreive access token for client endpoints. + Since introception script will be executed, access token will have permissions with all scopes + """ + response = requests.post( + url='https://{}/jans-auth/restv1/token'.format(self.idp_host), + headers=headers_basic_auth, + data={'grant_type': 'client_credentials', 'scope': 'openid', 'ujwt': result}, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + self.log_response(response) + if response.status_code != 200: + self.raise_error("Unable to get access token") + + result = response.json() + + self.access_token = result['access_token'] + access_token_enc = obscure(self.access_token) + config['DEFAULT']['access_token_enc'] = access_token_enc + write_config() + + return True, '' + + def get_access_token(self, scope): + if self.use_test_client: + self.get_scoped_access_token(scope) + elif not self.access_token: + self.check_access_token() + self.get_jwt_access_token() + return True, '' + + def print_exception(self, e): + error_printed = False + if hasattr(e, 'body'): + try: + jsdata = json.loads(e.body.decode()) + self.raise_error(e.body.decode()) + error_printed = True + except: + pass + if not error_printed: + msg = "Error retrieving data: " + err = 'None' + if isinstance(e, str): + err = e + if hasattr(e, 'reason'): + err = e.reason + if hasattr(e, 'body'): + err = e.body + if hasattr(e, 'args'): + err = ', '.join(e.args) + + self.raise_error(msg + str(err)) + + def colored_text(self, text, color=255): + return u"\u001b[38;5;{}m{}\u001b[0m".format(color, text) + + + def guess_bool(self, val): + if val == '_false': + return False + if val == '_true': + return True + + + def check_type(self, val, vtype): + if vtype == 'string' and val: + return str(val) + elif vtype == 'integer': + if isinstance(val, int): + return val + if val.isnumeric(): + return int(val) + elif vtype == 'object': + try: + retVal = json.loads(val) + if isinstance(retVal, dict): + return retVal + except: + pass + elif vtype == 'boolean': + guessed_val = self.guess_bool(val) + if not guessed_val is None: + return guessed_val + + error_text = "Please enter a(n) {} value".format(vtype) + if vtype == 'boolean': + error_text += ': _true, _false' + + raise TypeError(self.colored_text(error_text, warning_color)) + + def get_input(self, values=[], text='Selection', default=None, itype=None, + help_text=None, sitype=None, enforce='__true__', + example=None, spacing=0, iformat=None + ): + if isinstance(default, str): + default = html.escape(default) + + if 'b' in values and 'q' in values and 'x' in values: + greyed_help_list = [ ('b', 'back'), ('q', 'quit'), ('x', 'logout and quit') ] + for k,v in (('w', 'write result'), ('y', 'yes'), ('n', 'no')): + if k in values: + greyed_help_list.insert(1, (k, v)) + grey_help_text = ', '.join(['{}: {}'.format(k,v) for k,v in greyed_help_list]) + print(self.colored_text(grey_help_text, grey_color)) + print() + type_text = '' + if itype: + if itype == 'array': + type_text = "Type: array of {} separated by _,".format(sitype) + if values: + type_text += ' Valid values: {}'.format(', '.join(values)) + elif itype == 'boolean': + type_text = "Type: " + itype + if default is None: + default = False + else: + type_text = "Type: " + itype + if values: + type_text += ', Valid values: {}'.format(self.colored_text(', '.join(values), bold_color)) + + if help_text: + help_text = help_text.strip('.') + '. ' + type_text + else: + help_text = type_text + + if help_text: + print(' ' * spacing, self.colored_text('«{}»'.format(help_text), 244), sep='') + + if example: + if isinstance(example, list): + example_str = ', '.join(example) + else: + example_str = str(example) + print(' ' * spacing, self.colored_text('Example: {}'.format(example_str), 244), sep='') + + if not default is None: + default_text = str(default).lower() if itype == 'boolean' else str(default) + text += ' [' + default_text + ']' + if itype == 'integer': + default = int(default) + + if not text.endswith('?'): + text += ':' + + if itype == 'boolean' and not values: + values = ['_true', '_false'] + + while True: + + selection = input(' ' * spacing + self.colored_text(text, 20) + ' ') + + selection = selection.strip() + if selection.startswith('_file '): + fname = selection.split()[1] + if os.path.isfile(fname): + with open(fname) as f: + selection = f.read().strip() + else: + print(self.colored_text("File {} does not exist".format(fname), warning_color)) + continue + + if itype == 'boolean' and not selection: + return False + + if not selection and default: + return default + + if enforce and not selection: + continue + + if not enforce and not selection: + if itype == 'array': + return [] + return None + + if selection and iformat: + if iformat == 'date-time' and not self.validate_date_time(selection): + print(' ' * spacing, + self.colored_text('Please enter date-time string, i.e. 2001-07-04T12:08:56.235', warning_color), + sep='') + continue + + if 'q' in values and selection == 'q': + print("Quiting...") + sys.exit() + + if 'x' in values and selection == 'x': + print("Logging out...") + if 'access_token_enc' in config['DEFAULT']: + config['DEFAULT'].pop('access_token_enc') + write_config() + print("Quiting...") + sys.exit() + break + + + if itype == 'object' and sitype: + try: + object_ = self.check_type(selection, itype) + except Exception as e: + print(' ' * spacing, e, sep='') + continue + + data_ok = True + for items in object_: + try: + self.check_type(object_[items], sitype) + except Exception as e: + print(' ' * spacing, e, sep='') + data_ok = False + if data_ok: + return object_ + else: + continue + + if itype == 'array' and default and not selection: + return default + + if itype == 'array' and sitype: + if selection == '_null': + selection = [] + data_ok = True + else: + selection = selection.split('_,') + for i, item in enumerate(selection): + data_ok = True + try: + selection[i] = self.check_type(item.strip(), sitype) + if selection[i] == '_null': + selection[i] = None + if values: + if not selection[i] in values: + data_ok = False + print(' ' * spacing, self.colored_text( + "Please enter array of {} separated by _,".format(', '.join(values)), + warning_color), sep='') + break + except TypeError as e: + print(' ' * spacing, e, sep='') + data_ok = False + if data_ok: + break + else: + if not itype is None: + try: + selection = self.check_type(selection, itype) + except TypeError as e: + if enforce: + print(' ' * spacing, e, sep='') + continue + + if values: + if selection in values: + break + elif itype == 'boolean': + if isinstance(selection, bool): + break + else: + continue + else: + print(' ' * spacing, + self.colored_text('Please enter one of {}'.format(', '.join(values)), warning_color), + sep='') + + if not values and not selection and not enforce: + break + + if not values and selection: + break + + if selection == '_null': + selection = None + elif selection == '_q': + selection = 'q' + + return selection + + + def print_underlined(self, text): + print() + print(text) + print('-' * len(text.splitlines()[-1])) + + + def pretty_print(self, data): + pp_string = json.dumps(data, indent=2) + if args.no_color: + print(pp_string) + else: + colorful_json = highlight(pp_string, lexers.JsonLexer(), formatters.TerminalFormatter()) + print(colorful_json) + + def get_url_param(self, url): + if url.endswith('}'): + pname = re.findall('/\{(.*?)\}$', url)[0] + return pname + + def get_endpiont_url_param(self, endpoint): + param = {} + pname = self.get_url_param(endpoint.path) + if pname: + param = {'name': pname, 'description': pname, 'schema': {'type': 'string'}} + + return param + + + def obtain_parameters(self, endpoint, single=False): + parameters = {} + + endpoint_parameters = [] + if 'parameters' in endpoint.info: + endpoint_parameters = endpoint.info['parameters'] + + end_point_param = self.get_endpiont_url_param(endpoint) + if end_point_param and not end_point_param in endpoint_parameters: + endpoint_parameters.insert(0, end_point_param) + + n = 1 if single else len(endpoint_parameters) + + for param in endpoint_parameters[0:n]: + param_name = param['name'] + if param_name not in parameters: + text_ = param['name'] + help_text = param.get('description') or param.get('summary') + enforce = True if param['schema']['type'] == 'integer' or (end_point_param and end_point_param['name'] == param['name']) else False + + parameters[param_name] = self.get_input( + text=text_.strip('.'), + itype=param['schema']['type'], + default=param['schema'].get('default'), + enforce=enforce, + help_text=help_text, + example=param.get('example'), + values=param['schema'].get('enum', []) + ) + + return parameters + + + 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][ + 'operationId'] == operation_id: + retVal = cfg_yaml[my_op_mode][plugin]['paths'][path][method].copy() + retVal['__path__'] = path + retVal['__method__'] = method + retVal['__urlsuffix__'] = self.get_url_param(path) + + return retVal + + + def get_scope_for_endpoint(self, endpoint): + scope = [] + for security in endpoint.info.get('security', []): + for stype in security: + scope += security[stype] + + return ' '.join(scope) + + + def get_requests(self, endpoint, params={}): + if not self.wrapped: + sys.stderr.write("Please wait while retrieving data ...\n") + + security = self.get_scope_for_endpoint(endpoint) + self.get_access_token(security) + + url_param_name = self.get_url_param(endpoint.path) + + url = 'https://{}{}'.format(self.host, endpoint.path) + if params and url_param_name in params: + url = url.format(**{url_param_name: params.pop(url_param_name)}) + + response = requests.get( + url = url, + headers=self.get_request_header({'Accept': 'application/json'}), + params=params, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + self.log_response(response) + + if self.wrapped: + return response + + if response.status_code in (404, 401): + if response.text == 'ID Token is expired': + self.access_token = None + self.get_access_token(security) + self.get_requests(endpoint, params) + return + else: + print(self.colored_text("Server returned {}".format(response.status_code), error_color)) + print(self.colored_text(response.text, error_color)) + return None + + try: + return response.json() + print(response.status_code) + except Exception as e: + print("An error ocurred while retrieving data") + self.print_exception(e) + + def get_mime_for_endpoint(self, endpoint, req='requestBody'): + for key in endpoint.info[req]['content']: + return key + + def post_requests(self, endpoint, data): + url = 'https://{}{}'.format(self.host, endpoint.path) + security = self.get_scope_for_endpoint(endpoint) + self.get_access_token(security) + mime_type = self.get_mime_for_endpoint(endpoint) + + headers = self.get_request_header({'Accept': 'application/json', 'Content-Type': mime_type}) + + response = requests.post(url, + headers=headers, + json=data, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + + self.log_response(response) + + if self.wrapped: + return response + + try: + return response.json() + except: + print(response.text) + + + def delete_requests(self, endpoint, url_param_dict): + security = self.get_scope_for_endpoint(endpoint) + self.get_access_token(security) + + response = requests.delete( + url='https://{}{}'.format(self.host, endpoint.path.format(**url_param_dict)), + 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 + + return response.text.strip() + + + def patch_requests(self, endpoint, url_param_dict, data): + url = 'https://{}{}'.format(self.host, endpoint.path.format(**url_param_dict)) + security = self.get_scope_for_endpoint(endpoint) + self.get_access_token(security) + + headers = self.get_request_header({'Accept': 'application/json', 'Content-Type': 'application/json-patch+json'}) + data = data + response = requests.patch( + url=url, + headers=headers, + json=data, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + self.log_response(response) + try: + return response.json() + except: + self.print_exception(response.text) + + + def put_requests(self, endpoint, data): + + security = self.get_scope_for_endpoint(endpoint) + self.get_access_token(security) + + mime_type = self.get_mime_for_endpoint(endpoint) + + response = requests.put( + url='https://{}{}'.format(self.host, endpoint.path), + headers=self.get_request_header({'Accept': mime_type}), + json=data, + verify=self.verify_ssl, + cert=self.mtls_client_cert + ) + + self.log_response(response) + + if self.wrapped: + return response + + try: + result = response.json() + except Exception: + self.exit_with_error(response.text) + + return result + + + def parse_command_args(self, args): + args_dict = {} + + if args: + tokens = self.unescaped_split(args, ',') + for arg in tokens: + neq = arg.find(':') + if neq > 1: + arg_name = arg[:neq].strip() + arg_val = arg[neq + 1:].strip() + if arg_name and arg_val: + args_dict[arg_name] = arg_val + + return args_dict + + def parse_args(self, args, path): + param_names = [] + if not 'parameters' in path: + return {} + for param in path['parameters']: + param_names.append(param['name']) + + args_dict = self.parse_command_args(args) + + for arg_name in args_dict: + if not arg_name in param_names: + self.exit_with_error("valid endpoint args are: {}".format(', '.join(param_names))) + + return args_dict + + + + + 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] + if isinstance(path, dict): + for tag_ in path['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']) + if path.get('__urlsuffix__'): + print(' url-suffix:', path['__urlsuffix__']) + if 'parameters' in path: + param_names = [] + for param in path['parameters']: + desc = param.get('description', 'No description is provided for this parameter') + param_type = param.get('schema', {}).get('type') + if param_type: + desc += ' [{}]'.format(param_type) + param_names.append((param['name'], desc)) + if param_names: + print(' Parameters:') + for param in param_names: + print(' {}: {}'.format(param[0], param[1])) + + 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': + schema_path = path['requestBody']['content'][apptype]['schema']['items']['$ref'] + print(' Schema: Array of {}{}'.format(mode_suffix, os.path.basename(schema_path))) + else: + schema_path = path['requestBody']['content'][apptype]['schema']['$ref'] + print(' Schema: {}{}'.format(mode_suffix, os.path.basename(schema_path))) + break + if schema_path: + print() + print("To get sample schema type {0} --schema , for example {0} --schema {2}{1}".format(sys.argv[0], os.path.basename(schema_path), mode_suffix)) + + def render_json_entry(self, val): + if isinstance(val, str) and val.startswith('_file '): + file_path = val[6:].strip() + if os.path.exists(file_path): + with open(file_path) as f: + val = f.read() + else: + raise ValueError("File '{}' not found".format(file_path)) + return val + + def get_json_from_file(self, data_fn): + + if not os.path.exists(data_fn): + self.exit_with_error("Can't find file {}".format(data_fn)) + + try: + with open(data_fn) as f: + data = json.load(f) + except: + self.exit_with_error("Error parsing json file {}".format(data_fn)) + + if isinstance(data, list): + for entry in data: + if isinstance(entry, dict): + for k in entry: + entry[k] = self.render_json_entry(entry[k]) + + if isinstance(data, dict): + for k in data: + data[k] = self.render_json_entry(data[k]) + + return data + + + def process_command_get(self, path, suffix_param, endpoint_params, data_fn, data=None): + endpoint = self.get_fake_endpoint(path) + params = {**suffix_param, **endpoint_params} + response = self.get_requests(endpoint, params) + if not self.wrapped: + self.pretty_print(response) + else: + return response + + def exit_with_error(self, error_text): + error_text += '\n' + sys.stderr.write(self.colored_text(error_text, error_color)) + print() + sys.exit() + + + def get_fake_endpoint(self, path): + endpoint = SimpleNamespace() + endpoint.path = path['__path__'] + endpoint.info = path + return endpoint + + + def print_response(self, response): + if response: + sys.stderr.write("Server Response:\n") + self.pretty_print(response) + + def process_command_post(self, path, suffix_param, endpoint_params, data_fn, data): + + # TODO: suffix_param, endpoint_params + + endpoint = self.get_fake_endpoint(path) + + if not data: + + if data_fn.endswith('jwt'): + with open(data_fn) as reader: + data = jwt.decode(reader.read(), + options={"verify_signature": False, "verify_exp": False, "verify_aud": False}) + else: + try: + data = self.get_json_from_file(data_fn) + except ValueError as ve: + self.exit_with_error(str(ve)) + + if path['__method__'] == 'post': + response = self.post_requests(endpoint, data) + elif path['__method__'] == 'put': + response = self.put_requests(endpoint, data) + + if self.wrapped: + return response + + self.print_response(response) + + def process_command_put(self, path, suffix_param, endpoint_params, data_fn, data=None): + return self.process_command_post(path, suffix_param, endpoint_params, data_fn, data) + + def process_command_patch(self, path, suffix_param, endpoint_params, data_fn, data=None): + # TODO: suffix_param, endpoint_params + + endpoint = self.get_fake_endpoint(path) + + if not data: + try: + data = self.get_json_from_file(data_fn) + except ValueError as ve: + self.exit_with_error(str(ve)) + + if not isinstance(data, list): + self.exit_with_error("{} must be array of /components/schemas/PatchRequest".format(data_fn)) + + op_modes = ('add', 'remove', 'replace', 'move', 'copy', 'test') + + for item in data: + if not item['op'] in op_modes: + print("op must be one of {}".format(', '.join(op_modes))) + sys.exit() + if not item['path'].startswith('/'): + item['path'] = '/' + item['path'] + + response = self.patch_requests(endpoint, suffix_param, data) + + if self.wrapped: + return response + else: + self.print_response(response) + + + 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 + + if response: + self.print_response(response) + else: + print(self.colored_text("Object was successfully deleted.", success_color)) + + def process_command_by_id(self, operation_id, url_suffix, endpoint_args, data_fn, data=None): + path = self.get_path_by_id(operation_id) + + if not path: + self.exit_with_error("No Operation ID {} was found.".format(operation_id)) + + suffix_param = self.parse_command_args(url_suffix) + endpoint_params = self.parse_command_args(endpoint_args) + + if path.get('__urlsuffix__') and not path['__urlsuffix__'] in suffix_param: + self.exit_with_error("This operation requires a value for url-suffix {}".format(path['__urlsuffix__'])) + + if not data: + op_path = self.get_path_by_id(operation_id) + if op_path['__method__'] == 'patch' and not data_fn: + pop, pdata = '', '' + if args.patch_add: + pop = 'add' + pdata = args.patch_add + elif args.patch_remove: + pop = 'remove' + pdata = args.patch_remove + elif args.patch_replace: + pop = 'replace' + pdata = args.patch_replace + + if pop: + if pop != 'remove' and pdata.count(':') != 1: + self.exit_with_error("Please provide --patch-data as colon delimited key:value pair") + + if pop != 'remove': + ppath, pval = pdata.split(':') + data = [{'op': pop, 'path': '/'+ ppath.lstrip('/'), 'value': pval}] + else: + data = [{'op': pop, 'path': '/'+ pdata.lstrip('/')}] + + caller_function = getattr(self, 'process_command_' + path['__method__']) + return caller_function(path, suffix_param, endpoint_params, data_fn, data=data) + + + 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']: + 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_ = schema.copy() + + for p in schema_path_list[1:]: + schema_ = schema_[p] + + if 'allOf' in schema_: + all_schema = OrderedDict() + all_schema['required'] = [] + + all_schema['properties'] = OrderedDict() + for sch in schema_['allOf']: + if '$ref' in sch: + all_schema.update(self.get_schema_from_reference(plugin_name, sch['$ref'])) + elif 'properties' in sch: + for sprop in sch['properties']: + all_schema['properties'][sprop] = sch['properties'][sprop] + all_schema['required'] += sch.get('required', []) + + schema_ = all_schema + + for key_ in schema_.get('properties', []): + if '$ref' in schema_['properties'][key_]: + schema_['properties'][key_] = self.get_schema_from_reference(plugin_name, schema_['properties'][key_]['$ref']) + elif schema_['properties'][key_].get('type') == 'array' and '$ref' in schema_['properties'][key_]['items']: + ref_path = schema_['properties'][key_]['items'].pop('$ref') + ref_schema = self.get_schema_from_reference(plugin_name, ref_path) + schema_['properties'][key_]['properties'] = ref_schema['properties'] + schema_['properties'][key_]['title'] = ref_schema['title'] + schema_['properties'][key_]['description'] = ref_schema.get('description', '') + schema_['properties'][key_]['__schema_name__'] = ref_schema['__schema_name__'] + + if not 'title' in schema_: + schema_['title'] = p + + schema_['__schema_name__'] = p + + return schema_ + + + def get_sample_schema(self, schema_name): + + if ':' in schema_name: + plugin_name, schema_str = schema_name.split(':') + else: + plugin_name, schema_str = '', schema_name + + schema = None + schema_reference = self.get_schema_reference_from_name(plugin_name, schema_str) + if schema_reference: + schema = self.get_schema_from_reference(plugin_name, schema_reference) + + if schema is None: + print(self.colored_text("Schema not found.", error_color)) + return + + sample_schema = OrderedDict() + for prop_name in schema.get('properties', {}): + prop = schema['properties'][prop_name] + if 'default' in prop: + sample_schema[prop_name] = prop['default'] + elif 'example' in prop: + sample_schema[prop_name] = prop['example'] + elif 'enum' in prop: + sample_schema[prop_name] = random.choice(prop['enum']) + elif prop.get('type') == 'object': + sample_schema[prop_name] = prop.get('properties', {}) + elif prop.get('type') == 'array': + if 'items' in prop: + if 'enum' in prop['items']: + sample_schema[prop_name] = [random.choice(prop['items']['enum'])] + elif 'type' in prop['items']: + sample_schema[prop_name] = [prop['items']['type']] + else: + sample_schema[prop_name] = [] + elif prop.get('type') == 'boolean': + sample_schema[prop_name] = random.choice((True, False)) + elif prop.get('type') == 'integer': + sample_schema[prop_name] = random.randint(1,200) + else: + sample_schema[prop_name]='string' + + print(json.dumps(sample_schema, indent=2)) + + + def unescaped_split(self, s, delimeter, escape_char='\\'): + ret_val = [] + cur_list = [] + iter_ = iter(s) + for char_ in iter_: + if char_ == escape_char: + try: + cur_list.append(next(iter_)) + except StopIteration: + pass + elif char_ == delimeter: + ret_val.append(''.join(cur_list)) + cur_list = [] + else: + cur_list.append(char_) + ret_val.append(''.join(cur_list)) + + return ret_val + + + +def main(): + + + error_log_file = os.path.join(log_dir, 'cli_eorror.log') + cli_object = JCA_CLI(host, client_id, client_secret, access_token, test_client) + + if args.revoke_session: + cli_object.revoke_session() + sys.exit() + + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + if 1: + #try: + if not access_token: + cli_object.check_connection() + + if args.info: + cli_object.help_for(args.info) + elif args.schema: + cli_object.get_sample_schema(args.schema) + elif args.operation_id: + cli_object.process_command_by_id(args.operation_id, args.url_suffix, args.endpoint_args, args.data) + + #except Exception as e: + # print(u"\u001b[38;5;{}mAn Unhandled error raised: {}\u001b[0m".format(error_color, e)) + # with open(error_log_file, 'a') as w: + # traceback.print_exc(file=w) + # print("Error is logged to {}".format(error_log_file)) + + +if __name__ == "__main__": + main() diff --git a/jans-cli-tui/cli_tui/cli_style.py b/jans-cli-tui/cli_tui/cli_style.py new file mode 100755 index 00000000000..ac940f37f25 --- /dev/null +++ b/jans-cli-tui/cli_tui/cli_style.py @@ -0,0 +1,138 @@ +from prompt_toolkit.styles import Style + +style = Style.from_dict( + { + "window.border": "#888888", + "shadow": "bg:#222222", + "menu-bar": "bg:#aaaaaa #888888", + "menu-bar.selected-item": "bg:#ffffff #000000", + "menu": "bg:#888888 #ffffff", + "menu.border": "#aaaaaa", + "window.border shadow": "#444444", + "focused button": "bg:#880000 #ffffff noinherit", + # Styling for Dialog widgets. + "button-bar": "bg:#4D4D4D", + "textarea-readonly": "bg:#ffffff fg:#4D4D4D", + "required-field": "#8b000a", + "textarea":"bg:#ffffff fg:#0000ff", + "status": "bg:ansigray fg:black", + "progress": "bg:ansigray fg:ansired", + "select-box cursor-line": "nounderline bg:ansired fg:ansiwhite", + "checkbox":"nounderline bg:#ffffff fg:#d1c0c0 #ff0000", + ### Jans_cli_tui + ### main + "jans-main-navbar":"fg:ansired bg:green", + "jans-main-verificationuri":"", + "jans-main-verificationuri.text":"", + "jans-main-usercredintial":"", + "jans-main-usercredintial.titletext":"", + "jans-main-datadisplay":"", + "jans-main-datadisplay.text":"", + + ### Styling for Scripts plugin + ### main + "script_maincontainer":"bg:#908F90", + "script-sidenav":"red", + "script-mainarea":"yellow", + "script-navbar-bgcolor":"#2600ff", + "script-checkbox":"green", + "script-titledtext":"green", + "script-label":"blue", + ### Styling for oauth plugin + ## main + "outh_maincontainer":"", + "outh_containers_scopes":"", + "outh_containers_scopes.text":"green", + "outh_containers_clients":"", + "outh_containers_clients.text":"green", + + "sub-navbar": "fg:Silver bg:MidnightBlue", + + "outh-navbar":"fg:#f92672 bg:#4D4D4D", + "outh-verticalnav-headcolor":'green', + "outh-verticalnav-entriescolor":'white', + + "outh-waitclientdata":'', + "outh-waitclientdata.label":'', + "outh-waitscopedata":'' , + "outh-waitscopedata.label":'', + + "outh-titledtext":"green", + "outh-label":"blue", + ## edit_client_dialog + "outh-client-navbar":"#2600ff", + "outh-client-navbar-headcolor":"green", + "outh-client-navbar-entriescolor":"blue", + "outh-client-tabs":"", + "outh-client-text":"green", + "outh-client-textsearch":"", + "outh-client-label":"bold", + "outh-client-textrequired":"#8b000a", + "outh-client-checkbox":"green", + "outh-client-checkboxlist":"green", + "outh-client-radiobutton":"green", + "outh-client-dropdown":"green", + "outh-client-widget":"green", + + ## edit_scope_dialog + "outh-scope-navbar":"#2600ff", + "outh-scope-navbar-headcolor":"green", + "outh-scope-navbar-entriescolor":"blue", + "outh-scope-tabs":"", + "outh-scope-text":"green", + "outh-scope-textsearch":"fg:green", + "outh-scope-label":"bold", + "outh-scope-textrequired":"#8b000a", + "outh-scope-checkbox":"green", + "outh-scope-checkboxlist":"green", + "outh-scope-radiobutton":"green", + "outh-scope-dropdown":"green", + "outh-scope-widget":"green", + + ## view-uma_dialog + "outh-uma-navbar":"fg:#4D4D4D bg:#ffffff", + "outh-uma-tabs":"", + "outh-uma-text":"green", + "outh-uma-textsearch":"fg:green", + "outh-uma-label":"green bold", + "outh-uma-textrequired":"#8b000a", + "outh-uma-checkbox":"green", + "outh-uma-checkboxlist":"green", + "outh-uma-radiobutton":"green", + "outh-uma-dropdown":"green", + "outh-uma-widget":"green", + + "script-label":"fg:green", + + + ### WUI Componenets + ## jans_data_picker + "date-picker-monthandyear":"bg:#1e51fa", + "date-picker-day":"bg:#D3D3D3", + "date-picker-time":"bg:#bab1b1", + "dialog-titled-widget":"bg:#ffffff fg:green", + + } +) + +## jans nav bar +main_navbar_bgcolor = "DimGray" +outh_navbar_bgcolor = "#ADD8E6" +sub_navbar_selected_bgcolor = "Navy" +sub_navbar_selected_fgcolor = "OldLace" +shorcut_color= 'OrangeRed' + + +### WUI Componenets +## jans_data_picker +date_picker_TimeTitle = "yellow" ## only color >> HTML '' +date_picker_Time = "green" ## only color +date_picker_TimeSelected = "black" + +date_picker_calender_prevSelected = "red" #>black >> defult bold +date_picker_calenderNSelected = "blue"#>black +date_picker_calenderSelected = "red" + +## jans_drop_down +drop_down_hover = '#00FF00' +drop_down_itemSelect = '#ADD8E6' diff --git a/jans-cli-tui/cli_tui/jans_cli_tui.py b/jans-cli-tui/cli_tui/jans_cli_tui.py new file mode 100755 index 00000000000..92e2f613447 --- /dev/null +++ b/jans-cli-tui/cli_tui/jans_cli_tui.py @@ -0,0 +1,706 @@ +#!/usr/bin/env python3 +""" +""" +import os +import json +import time +import logging +import importlib +import sys +import asyncio +import concurrent.futures + +from pathlib import Path +from itertools import cycle +from requests.models import Response + +cur_dir = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(cur_dir) + +import prompt_toolkit +from prompt_toolkit.application import Application +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 +from prompt_toolkit.layout.containers import Float, HSplit, VSplit +from prompt_toolkit.formatted_text import HTML, merge_formatted_text + +from prompt_toolkit.layout.containers import ( + Float, + HSplit, + VSplit, + HorizontalAlign, + DynamicContainer, + FloatContainer, + Window, + FormattedTextControl +) +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.layout import Layout +from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer +from prompt_toolkit.widgets import ( + Button, + Frame, + Label, + RadioList, + TextArea, + CheckboxList, + Checkbox, +) + +from typing import Any, Optional, OrderedDict, Sequence, Union +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.layout.containers import ( + AnyContainer, +) +from prompt_toolkit.layout.dimension import AnyDimension +from prompt_toolkit.formatted_text import AnyFormattedText +from typing import TypeVar, Callable +from prompt_toolkit.widgets import Button, Dialog, Label + +# -------------------------------------------------------------------------- # +from cli import config_cli +from utils.validators import IntegerValidator +from wui_components.jans_cli_dialog import JansGDialog +from wui_components.jans_nav_bar import JansNavBar +from wui_components.jans_message_dialog import JansMessageDialog + +from cli_style import style + +import cli_style + +from utils.multi_lang import _ +# -------------------------------------------------------------------------- # + +home_dir = Path.home() +config_dir = home_dir.joinpath('.config') +config_dir.mkdir(parents=True, exist_ok=True) +config_ini_fn = config_dir.joinpath('jans-cli.ini') + +def accept_yes() -> None: + get_app().exit(result=True) + +def accept_no() -> None: + get_app().exit(result=False) + +def do_exit(*c) -> None: + get_app().exit(result=False) + +class JansCliApp(Application): + + entries_per_page = 20 # we can make this configurable + + def __init__(self): + self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) + self.set_keybindings() + self.init_logger() + self.disabled_plugins = [] + self.status_bar_text = '' + self.progress_char = ' ' + self.progress_active = False + self.progress_iterator = cycle(['⣾', '⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽']) + self.styles = dict(style.style_rules) + self._plugins = [] + self._load_plugins() + self.cli_object_ok = False + + self.not_implemented = Frame( + body=HSplit([Label(text=_("Not imlemented yet")), Button(text=_("MyButton"))], width=D()), + height=D()) + + self.yes_button = Button(text=_("Yes"), handler=accept_yes) + self.no_button = Button(text=_("No"), handler=accept_no) + self.pbar_window = Window(char=lambda: self.progress_char, style='class:progress', width=1) + self.status_bar = VSplit([ + Window(FormattedTextControl(self.update_status_bar), style='class:status', height=1), + self.pbar_window, + ], height=1 + ) + + self.center_container = self.not_implemented + + self.nav_bar = JansNavBar( + self, + entries=[(plugin.pid, plugin.name) for plugin in self._plugins], + selection_changed=self.main_nav_selection_changed, + select=0, + jans_name='main:nav_bar', + last_to_right=True, + ) + self.center_frame = FloatContainer(content= + Frame( + body=DynamicContainer(lambda: self.center_container), + height=D() + ), + floats=[], + ) + self.root_layout = FloatContainer( + HSplit([ + Frame(self.nav_bar.nav_window), + self.center_frame, + self.status_bar, + ], + ), + floats=[] + ) + super(JansCliApp, self).__init__( + layout=Layout(self.root_layout), + key_bindings=self.bindings, + style=style, + full_screen=True, + mouse_support=True, ## added + ) + self.main_nav_selection_changed(self.nav_bar.navbar_entries[0][0]) + self.create_background_task(self.progress_coroutine()) + self.plugins_initialised = False + + self.create_background_task(self.check_jans_cli_ini()) + + + async def progress_coroutine(self) -> None: + """asyncio corotune for progress bar + """ + while True: + if self.progress_active: + self.progress_char = next(self.progress_iterator) + self.invalidate() + await asyncio.sleep(0.15) + + + def cli_requests(self, args: dict) -> Response: + response = self.cli_object.process_command_by_id( + operation_id=args['operation_id'], + url_suffix=args.get('url_suffix', ''), + endpoint_args=args.get('endpoint_args', ''), + data_fn=args.get('data_fn'), + data=args.get('data', {}) + ) + return response + + def start_progressing(self): + self.progress_active = True + + def stop_progressing(self): + self.progress_active = False + self.progress_char = ' ' + self.invalidate() + + def _load_plugins(self) -> None: + # check if admin-ui plugin is available: + + plugin_dir = os.path.join(cur_dir, 'plugins') + for plugin_file in sorted(Path(plugin_dir).glob('*/main.py')): + if plugin_file.parent.joinpath('.enabled').exists(): + sys.path.append(plugin_file.parent.as_posix()) + spec = importlib.util.spec_from_file_location(plugin_file.stem, plugin_file.as_posix()) + plugin = importlib.util.module_from_spec(spec) + spec.loader.exec_module(plugin) + plugin_object = plugin.Plugin(self) + self._plugins.append(plugin_object) + + def init_plugins(self) -> None: + for plugin in self._plugins: + if hasattr(plugin, 'init_plugin'): + plugin.init_plugin() + self.plugins_initialised = True + + def plugin_enabled(self, pid: str) -> bool: + """Checks whether plugin is enabled or not + Args: + pid (str): PID of plugin + """ + for plugin_object in self._plugins: + if plugin_object.pid == pid: + return True + return False + + + def remove_plugin(self, pid: str) -> None: + """Removes plugin object + Args: + pid (str): PID of plugin + """ + for plugin_object in self._plugins: + if plugin_object.pid == pid: + self._plugins.remove(plugin_object) + return + + + @property + def dialog_width(self) -> None: + return int(self.output.get_size().columns*0.8) + + @property + def dialog_height(self) -> None: + return int(self.output.get_size().rows*0.9) + + def init_logger(self) -> None: + self.logger = logging.getLogger('JansCli') + self.logger.setLevel(logging.DEBUG) + logs_dir = os.path.join('jans_cli_logs', home_dir) + if not os.path.exists(logs_dir): + os.makedirs(logs_dir, exist_ok=True) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + file_handler = logging.FileHandler(os.path.join(logs_dir, 'dev-tui.log')) + + file_handler.setLevel(logging.DEBUG) + file_handler.setFormatter(formatter) + self.logger.addHandler(file_handler) + self.logger.debug('JANS CLI Started') + + + def create_cli(self) -> None: + test_client = config_cli.client_id if config_cli.test_client else None + 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, + test_client=test_client + ) + + status = self.cli_object.check_connection() + + self.invalidate() + + if status not in (True, 'ID Token is expired'): + buttons = [Button(_("OK"), handler=self.jans_creds_dialog)] + self.show_message(_("Error getting Connection Config Api"), status, buttons=buttons) + + else: + if not test_client and not self.cli_object.access_token: + + response = self.cli_object.get_device_verification_code() + result = response.json() + + msg = _("Please visit verification url %s and enter user code %s within %d seconds.") + body = HSplit([Label(msg % (result['verification_uri'], result['user_code'], result['expires_in']),style='class:jans-main-verificationuri.text')],style='class:jans-main-verificationuri') + dialog = JansGDialog(self, title=_("Waiting Response"), body=body) + + async def coroutine(): + app = get_app() + focused_before = app.layout.current_window + await self.show_dialog_as_float(dialog) + try: + app.layout.focus(focused_before) + except: + app.layout.focus(self.center_frame) + + self.start_progressing() + try: + response = await self.loop.run_in_executor(self.executor, self.cli_object.get_jwt_access_token, result) + except Exception as e: + self.stop_progressing() + err_dialog = JansGDialog(self, title=_("Error!"), body=HSplit([Label(str(e))])) + await self.show_dialog_as_float(err_dialog) + self.cli_object_ok = False + self.create_cli() + return + + self.stop_progressing() + + + self.cli_object_ok = True + if not self.plugins_initialised: + self.init_plugins() + asyncio.ensure_future(coroutine()) + + else: + self.cli_object_ok = True + if not self.plugins_initialised: + self.init_plugins() + + if self.cli_object_ok: + response = self.cli_requests({'operation_id': 'is-license-active'}) + if response.status_code == 404: + for entry in self.nav_bar.navbar_entries: + if entry[0] == 'config_api': + self.nav_bar.navbar_entries.remove(entry) + self.remove_plugin(entry[0]) + self.invalidate() + + + async def check_jans_cli_ini(self) -> None: + if not(config_cli.host and (config_cli.client_id and config_cli.client_secret or config_cli.access_token)): + self.jans_creds_dialog() + else : + self.create_cli() + + if self.cli_object_ok and not self.plugins_initialised: + self.init_plugins() + + + def jans_creds_dialog(self, *params: Any) -> None: + body=HSplit([ + self.getTitledText(_("Hostname"), name='jans_host', value=config_cli.host or '', jans_help=_("FQN name of Jannsen Config Api Server"),style='class:jans-main-usercredintial.titletext'), + self.getTitledText(_("Client ID"), name='jca_client_id', value=config_cli.client_id or '', jans_help=_("Jannsen Config Api Client ID"),style='class:jans-main-usercredintial.titletext'), + self.getTitledText(_("Client Secret"), name='jca_client_secret', value=config_cli.client_secret or '', jans_help=_("Jannsen Config Api Client Secret"),style='class:jans-main-usercredintial.titletext'), + ],style='class:jans-main-usercredintial') + + buttons = [Button(_("Save"), handler=self.save_creds)] + dialog = JansGDialog(self, title=_("Janssen Config Api Client Credidentials"), body=body, buttons=buttons) + async def coroutine(): + app = get_app() + focused_before = app.layout.current_window + result = await self.show_dialog_as_float(dialog) + try: + app.layout.focus(focused_before) + except: + app.layout.focus(self.center_frame) + + self.create_cli() + + asyncio.ensure_future(coroutine()) + + def set_keybindings(self) -> None: + # Global key bindings. + self.bindings = KeyBindings() + self.bindings.add('tab')(self.focus_next) + self.bindings.add('s-tab')(self.focus_previous) + self.bindings.add('c-c')(do_exit) + self.bindings.add('f1')(self.help) + self.bindings.add('escape')(self.escape) + self.bindings.add('s-up')(self.up) + + def up(self, ev: KeyPressEvent) -> None: + get_app().layout.focus(Frame(self.nav_bar.nav_window)) + + def focus_next(self, ev: KeyPressEvent) -> None: + focus_next(ev) + + def focus_previous(self, ev: KeyPressEvent) -> None: + focus_previous(ev) + + def help(self,ev: KeyPressEvent) -> None: + self.logger.debug("ev:"+str(ev)) + self.logger.debug("ev:"+str(type(ev))) + self.show_message(_("Help"),''' {} \n {}\n {}'''.format(_("Edit current selection"),_("Display current item in JSON format"),_("Delete current selection"))) + + def escape(self,ev: KeyPressEvent) -> None: + try: + if get_app().layout.container.floats: + if len(get_app().layout.container.floats) >=2 : + get_app().layout.container.floats.remove(get_app().layout.container.floats[-1]) + get_app().layout.focus(get_app().layout.container.floats[-1].content) + else: + get_app().layout.container.floats.remove(get_app().layout.container.floats[0]) + get_app().layout.focus(self.center_frame) + except Exception as e: + pass + + def get_help_from_schema( + self, + schema: OrderedDict, + jans_name: str + ) -> str: + for prop in schema.get('properties', {}): + if prop == jans_name: + return schema['properties'][jans_name].get('description', '') + + def getTitledText( + self, + title: AnyFormattedText = "", + name: AnyFormattedText = "", + value: AnyFormattedText = "", + height: Optional[int] = 1, + jans_help: AnyFormattedText = "", + accept_handler: Callable = None, + read_only: Optional[bool] = False, + focusable: Optional[bool] = None, + width: AnyDimension = None, + style: AnyFormattedText = '', + scrollbar: Optional[bool] = False, + line_numbers: Optional[bool] = False, + lexer: PygmentsLexer = None, + text_type: Optional[str] = 'string' + ) -> AnyContainer: + + title += ': ' + + ta = TextArea( + text=str(value), + multiline=height > 1, + height=height, + width=width, + read_only=read_only, + style=self.styles['textarea-readonly'] if read_only else self.styles['textarea'], + accept_handler=accept_handler, + focusable=not read_only if focusable is None else focusable, + scrollbar=scrollbar, + line_numbers=line_numbers, + lexer=lexer, + ) + + if text_type == 'integer': + ta.buffer.on_text_insert=IntegerValidator(ta) + + ta.window.text_type = text_type + ta.window.jans_name = name + ta.window.jans_help = jans_help + + v = VSplit([Window(FormattedTextControl(title), width=len(title)+1, style=style, height=height), ta], padding=1) + v.me = ta + + return v + + def getTitledCheckBoxList( + self, + title: AnyFormattedText, + name: AnyFormattedText, + values: Optional[list] = [], + current_values: Optional[list] = [], + jans_help: AnyFormattedText= "", + style: AnyFormattedText= "", + ) -> AnyContainer: + + title += ': ' + if values and not (isinstance(values[0], tuple) or isinstance(values[0], list)): + values = [(o,o) for o in values] + cbl = CheckboxList(values=values) + cbl.current_values = current_values + cbl.window.jans_name = name + cbl.window.jans_help = jans_help + #li, cd, width = self.handle_long_string(title, values, cbl) + + v = VSplit([Window(FormattedTextControl(title), width=len(title)+1, style=style,), cbl], padding=1) + v.me = cbl + + return v + + def getTitledCheckBox( + self, + title: AnyFormattedText, + name: AnyFormattedText, + text: AnyFormattedText= "", + checked: Optional[bool] = False, + on_selection_changed: Callable= None, + jans_help: AnyFormattedText= "", + style: AnyFormattedText= "", + ) -> AnyContainer: + + title += ': ' + cb = Checkbox(text) + cb.checked = checked + cb.window.jans_name = name + cb.window.jans_help = jans_help + + handler_org = cb._handle_enter + def custom_handler(): + handler_org() + on_selection_changed(cb) + + if on_selection_changed: + cb._handle_enter = custom_handler + + #li, cd, width = self.handle_long_string(title, text, cb) + + v = VSplit([Window(FormattedTextControl(title), width=len(title)+1, style=style,), cb], padding=1) + + v.me = cb + + return v + + def getTitledRadioButton( + self, + title: AnyFormattedText, + name: AnyFormattedText, + values: Optional[list] = [], + current_value: AnyFormattedText= "", + on_selection_changed: Callable= None, + jans_help: AnyFormattedText= "", + style: AnyFormattedText= "", + ) -> AnyContainer: + + title += ': ' + if values and not (isinstance(values[0], tuple) or isinstance(values[0], list)): + values = [(o,o) for o in values] + rl = RadioList(values=values) + if current_value: + rl.current_value = current_value + rl.window.jans_name = name + rl.window.jans_help = jans_help + #li, rl2, width = self.handle_long_string(title, values, rl) + + handler_org = rl._handle_enter + def custom_handler(): + handler_org() + on_selection_changed(rl) + + if on_selection_changed: + rl._handle_enter = custom_handler + + v = VSplit([Label(text=title, width=len(title), style=style), rl]) + v.me = rl + + return v + + def getTitledWidget( + self, + title: AnyFormattedText, + name: AnyFormattedText, + widget:AnyContainer, + jans_help: AnyFormattedText= "", + style: AnyFormattedText= "", + )-> AnyContainer: + title += ': ' + widget.window.jans_name = name + widget.window.jans_help = jans_help + #li, w2, width = self.handle_long_string(title, widget.values, widget) + + v = VSplit([Label(text=title, width=len(title), style=style), widget]) + v.me = widget + + return v + + def getButton( + self, + text: AnyFormattedText, + name: AnyFormattedText, + jans_help: AnyFormattedText, + handler: Callable= None, + ) -> Button: + + b = Button(text=text, width=len(text)+2) + b.window.jans_name = name + b.window.jans_help = jans_help + if handler: + b.handler = handler + return b + + def update_status_bar(self) -> None: + text = '' + if self.status_bar_text: + text = self.status_bar_text + self.status_bar_text = '' + else: + if hasattr(self.layout.current_window, 'jans_help') and self.layout.current_window.jans_help: + text = self.layout.current_window.jans_help + + return text + + def get_plugin_by_id(self, pid: str) -> None: + for plugin in self._plugins: + if plugin.pid == pid: + return plugin + + def main_nav_selection_changed(self, selection: str) -> None: + plugin = self.get_plugin_by_id(selection) + if hasattr(plugin, 'on_page_enter'): + plugin.on_page_enter() + plugin.set_center_frame() + + async def show_dialog_as_float(self, dialog:Dialog) -> None: + 'Coroutine.' + float_ = Float(content=dialog) + self.root_layout.floats.append(float_) + self.layout.focus(dialog) + self.invalidate() + result = await dialog.future + + if float_ in self.root_layout.floats: + self.root_layout.floats.remove(float_) + + if self.root_layout.floats: + self.layout.focus(self.root_layout.floats[-1].content) + else: + self.layout.focus(self.center_frame) + + return result + + def show_jans_dialog(self, dialog:Dialog) -> None: + + async def coroutine(): + focused_before = self.layout.current_window + result = await self.show_dialog_as_float(dialog) + try: + self.layout.focus(focused_before) + except: + self.layout.focus(self.center_frame) + + return result + + asyncio.ensure_future(coroutine()) + + def data_display_dialog(self, **params: Any) -> None: + + body = HSplit([ + TextArea( + lexer=DynamicLexer(lambda: PygmentsLexer.from_filename('.json', sync_from_start=True)), + scrollbar=True, + line_numbers=True, + multiline=True, + read_only=True, + text=str(json.dumps(params['data'], indent=2)), + style='class:jans-main-datadisplay.text' + ) + ],style='class:jans-main-datadisplay') + + dialog = JansGDialog(self, title=params['selected'][0], body=body) + + self.show_jans_dialog(dialog) + + def save_creds(self, dialog:Dialog) -> None: + + for child in dialog.body.children: + prop_name = child.children[1].jans_name + prop_val = child.children[1].content.buffer.text + if prop_name == 'jca_client_secret': + config_cli.config['DEFAULT']['jca_client_secret_enc'] = config_cli.obscure(prop_val) + else: + config_cli.config['DEFAULT'][prop_name] = prop_val + config_cli.write_config() + + config_cli.config['DEFAULT']['user_data'] = '' + config_cli.write_config() + + config_cli.host = config_cli.config['DEFAULT']['jans_host'] + config_cli.client_id = config_cli.config['DEFAULT']['jca_client_id'] + if 'jca_client_secret' in config_cli.config['DEFAULT']: + config_cli.client_secret = config_cli.config['DEFAULT']['jca_client_secret'] + else: + config_cli.client_secret = config_cli.unobscure(config_cli.config['DEFAULT']['jca_client_secret_enc']) + config_cli.access_token = None + + def show_message( + self, + title: AnyFormattedText, + message: AnyFormattedText, + buttons:Optional[Sequence[Button]] = [], + tobefocused: AnyContainer= None + ) -> None: + body = HSplit([Label(message)]) + dialog = JansMessageDialog(title=title, body=body, buttons=buttons) + + if not tobefocused: + focused_before = self.root_layout.floats[-1].content if self.root_layout.floats else self.layout.current_window #show_message + else : + focused_before = self.root_layout.floats[-1].content if self.root_layout.floats else tobefocused + float_ = Float(content=dialog) + self.root_layout.floats.append(float_) + dialog.me = float_ + dialog.focus_on_exit = focused_before + self.layout.focus(dialog) + self.invalidate() + + def show_again(self) -> None: + self.show_message(_("Again"), _("Nasted Dialogs"),) + + def get_confirm_dialog( + self, + message: AnyFormattedText + ) -> Dialog: + body = VSplit([Label(message)], align=HorizontalAlign.CENTER) + buttons = [Button(_("No")), Button(_("Yes"))] + dialog = JansGDialog(self, title=_("Confirmation"), body=body, buttons=buttons) + return dialog + + +application = JansCliApp() + +def run(): + result = application.run() + print("See you next time.") + + +if __name__ == "__main__": + run() diff --git a/jans-cli-tui/cli_tui/mkdocs.yml b/jans-cli-tui/cli_tui/mkdocs.yml new file mode 100755 index 00000000000..5661304065c --- /dev/null +++ b/jans-cli-tui/cli_tui/mkdocs.yml @@ -0,0 +1,136 @@ +# Project information +site_name: Jans-Cli-Tui +site_url: 'https://gluu.org/gluu-4/' +repo_url: 'https://github.com/JanssenProject' +edit_uri: 'https://github.com/JanssenProject' +site_description: "Jans-Tui Docs" + +# Copyright +copyright: Copyright © 2022, The Janssen Project + +# Plugins +plugins: + - mkdocstrings + - search + - glightbox + +# Configuration +theme: + name: material + highlightjs: true + hljs_languages: + - yaml + - java + - bash + - python + logo: img/logo.png + favicon: img/favicon.ico + features: + - navigation.instant + - toc.follow + - search.suggest + - search.highlight + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - navigation.sections + - navigation.tracking + - content.code.annotate + - navigation.indexes + - navigation.expand + + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: white + accent: green + toggle: + icon: material/weather-sunny + name: Switch to dark mode + + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: black + accent: green + toggle: + icon: material/weather-night + name: Switch to light mode + +# Extensions +markdown_extensions: + - attr_list + - admonition + - md_in_html + - toc: + # - pymdownx.superfences: + # custom_fences: + # - name: mermaid + # class: mermaid + # format: !!python/name:pymdownx.superfences.fence_code_format + # - pymdownx.tabbed + +extra: + homepage: 'docs/home/' + generator: false + social: + - icon: fontawesome/brands/github + link: https://github.com/JanssenProject/jans + name: Janssen on GitHub + - icon: fontawesome/solid/link + link: https://jans.io/docs + name: Janssen Website + +# Navigation +nav: + - Introduction: + - Welcome : 'docs/home/index.md' + - Janssen Modules : 'docs/home/janssen_modules.md' + + - Getting Started: + - Installation : + - VM System Requirements : 'docs/getting_started/installation/vm-requirements.md' + - Ubuntu : 'docs/getting_started/installation/ubuntu.md' + - RHEL : 'docs/getting_started/installation/rhel.md' + - Suse : 'docs/getting_started/installation/suse.md' + - Dynamic download : 'docs/getting_started/installation/dynamic-download.md' + + - Guides: + - Jans Cli : 'docs/Gallery/gallery.md' + - Jans Tui : 'docs/Gallery/gallery.md' + + - Gallery: + - 'docs/Gallery/gallery.md' + - TUI : 'docs/Gallery/tui.md' + - CLI : 'docs/Gallery/cli.md' + + - Plugins : + - 'Plugins': 'docs/plugins/plugins.md' + - OAuth : + - Main OAuth : 'docs/plugins/oauth/oauth.md' + - 'Client Dialog': 'docs/plugins/oauth/edit_client_dialog.md' + - 'Scope Dialog': 'docs/plugins/oauth/edit_scope_dialog.md' + - 'UMA Dialog': 'docs/plugins/oauth/edit_uma_dialog.md' + - FIDO : + - Main FIDO : 'docs/plugins/fido/fido.md' + - SCIM : + - Main SCIM : 'docs/plugins/scim/scim.md' + - Config API : + - Main Config API : 'docs/plugins/config_api/config_api.md' + - Client API : + - Main Client API : 'docs/plugins/client_api/client_api.md' + - Scripts : + - Main Scripts : 'docs/plugins/scripts/scripts.md' + + - Components: + - docs/wui_components/wui_components.md + - Dialogs components : + - 'jans_cli_dialog': 'docs/wui_components/jans_cli_dialog.md' + - 'jans_dialog_with_nav': 'docs/wui_components/jans_dialog_with_nav.md' + - 'jans_message_dialog': 'docs/wui_components/jans_message_dialog.md' + - Navigation bar components : + - 'jans_nav_bar': 'docs/wui_components/jans_nav_bar.md' + - 'jans_side_nav_bar': 'docs/wui_components/jans_side_nav_bar.md' + - 'jans_vetrical_nav': 'docs/wui_components/jans_vetrical_nav.md' + - Custom components : + - 'jans_data_picker': 'docs/wui_components/jans_data_picker.md' + - 'jans_drop_down': 'docs/wui_components/jans_drop_down.md' diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/.enabled b/jans-cli-tui/cli_tui/plugins/010_oxauth/.enabled new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/__init__.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_client_dialog.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_client_dialog.py new file mode 100755 index 00000000000..df938590aa8 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_client_dialog.py @@ -0,0 +1,976 @@ +from typing import Any, OrderedDict +from urllib import response + +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.containers import ( + HSplit, + VSplit, + DynamicContainer, + Window +) +from prompt_toolkit.widgets import ( + Box, + Button, + Label, +) +from prompt_toolkit.widgets import ( + Button, + Frame, + Label, + RadioList, + TextArea, + CheckboxList, + Checkbox, +) +from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer + +from prompt_toolkit.application.current import get_app +from asyncio import Future, ensure_future + + +import cli_style +from cli import config_cli +from utils.static import DialogResult +from utils.multi_lang import _ +from wui_components.jans_dialog_with_nav import JansDialogWithNav +from wui_components.jans_side_nav_bar import JansSideNavBar +from wui_components.jans_cli_dialog import JansGDialog +from wui_components.jans_drop_down import DropDownWidget +from wui_components.jans_data_picker import DateSelectWidget +from utils.utils import DialogUtils +from wui_components.jans_vetrical_nav import JansVerticalNav +from view_uma_dialog import ViewUMADialog +import threading +from prompt_toolkit.widgets import ( + Button, + Dialog, + VerticalLine, +) +from prompt_toolkit.layout.containers import AnyContainer +from prompt_toolkit.buffer import Buffer + +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.layout.dimension import AnyDimension +from typing import Optional, Sequence, Union +from typing import TypeVar, Callable + +import json + + +class EditClientDialog(JansGDialog, DialogUtils): + """The Main Client Dialog that contain every thing related to The Client + """ + def __init__( + self, + parent, + data:list, + title: AnyFormattedText= "", + buttons: Optional[Sequence[Button]]= [], + save_handler: Callable= None, + delete_UMAresource: Callable= None, + )-> Dialog: + """init for `EditClientDialog`, inherits from two diffrent classes `JansGDialog` and `DialogUtils` + + JansGDialog (dialog): This is the main dialog Class Widget for all Jans-cli-tui dialogs except custom dialogs like dialogs with navbar + DialogUtils (methods): Responsable for all `make data from dialog` and `check required fields` in the form for any Edit or Add New + + Args: + parent (widget): This is the parent widget for the dialog, to access `Pageup` and `Pagedown` + title (str): The Main dialog title + data (list): selected line data + button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. + save_handler (method, optional): handler invoked when closing the dialog. Defaults to None. + """ + super().__init__(parent, title, buttons) + self.save_handler = save_handler + self.delete_UMAresource=delete_UMAresource + self.data = data + self.title=title + self.myparent.logger.debug('self.data in init: '+str(self.data)) + self.prepare_tabs() + self.create_window() + + def save(self) -> None: + + self.data = self.make_data_from_dialog() + self.data['disabled'] = not self.data['disabled'] + for list_key in ( + 'redirectUris', + 'scopes', + 'postLogoutRedirectUris', + 'contacts', + 'authorizedOrigins', + 'requestUris', + 'defaultAcrValues', + 'claimRedirectUris', + ): + if self.data[list_key]: + self.data[list_key] = self.data[list_key].splitlines() + + if 'accessTokenAsJwt' in self.data: + self.data['accessTokenAsJwt'] = self.data['accessTokenAsJwt'] == 'jwt' + + self.myparent.logger.debug('self.data: '+str(self.data)) + + if 'rptAsJwt' in self.data: + self.data['rptAsJwt'] = self.data['rptAsJwt'] == 'jwt' + + self.data['attributes'] = {} + 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', + 'additionalAudience', + 'rptClaimsScripts', + 'spontaneousScopeScriptDns', + 'jansAuthorizedAcr', + 'tlsClientAuthSubjectDn', + 'spontaneousScopes', + 'updateTokenScriptDns', + 'postAuthnScripts', + 'introspectionScripts', + 'ropcScripts', + 'consentGatheringScripts', + + ): + if self.data[list_key]: + self.data['attributes'][list_key] = self.data[list_key].splitlines() + + for list_key in ( + 'runIntrospectionScriptBeforeJwtCreation', + 'backchannelLogoutSessionRequired', + 'jansDefaultPromptLogin', + 'allowSpontaneousScopes', + ): + if self.data[list_key]: + self.data['attributes'][list_key] = self.data[list_key] + + + cfr = self.check_required_fields() + self.myparent.logger.debug('CFR: '+str(cfr)) + if not cfr: + return + + for ditem in self.drop_down_select_first: + if ditem in self.data and self.data[ditem] is None: + self.data.pop(ditem) + + close_me = True + if self.save_handler: + close_me = self.save_handler(self) + if close_me: + self.future.set_result(DialogResult.ACCEPT) + + def cancel(self) -> None: + self.future.set_result(DialogResult.CANCEL) + + def create_window(self) -> None: + self.side_nav_bar = JansSideNavBar(myparent=self.myparent, + entries=list(self.tabs.keys()), + selection_changed=(self.client_dialog_nav_selection_changed) , + select=0, + entries_color='class:outh-client-navbar', + save_handler=self.save) + + self.dialog = JansDialogWithNav( + title=self.title, + navbar=self.side_nav_bar, + content=DynamicContainer(lambda: self.tabs[self.left_nav]), + button_functions=[ + (self.save, _("Save")), + (self.cancel, _("Cancel")) + ], + height=self.myparent.dialog_height, + width=self.myparent.dialog_width, + ) + + def prepare_tabs(self) -> None: + """Prepare the tabs for Edil Client Dialogs + """ + + schema = self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/Client') + + self.tabs = OrderedDict() + + self.tabs['Basic'] = HSplit([ + self.myparent.getTitledText( + _("Client_ID"), + name='inum', + value=self.data.get('inum',''), + jans_help=self.myparent.get_help_from_schema(schema, 'inum'), + read_only=True, + style='class:outh-client-text'), + + self.myparent.getTitledCheckBox( + _("Active"), + name='disabled', + checked= not self.data.get('disabled'), + jans_help=self.myparent.get_help_from_schema(schema, 'disabled'), + style='class:outh-client-checkbox'), + + self.myparent.getTitledText( + _("Client Name"), + name='displayName', + value=self.data.get('displayName',''), + jans_help=self.myparent.get_help_from_schema(schema, 'displayName'), + style='class:outh-client-text'), + + self.myparent.getTitledText( + _("Client Secret"), + name='clientSecret', + value=self.data.get('clientSecret',''), + jans_help=self.myparent.get_help_from_schema(schema, 'clientSecret'), + style='class:outh-client-text'), + + self.myparent.getTitledText( + _("Description"), + name='description', + value=self.data.get('description',''), + jans_help=self.myparent.get_help_from_schema(schema, 'description'), + style='class:outh-client-text'), + + self.myparent.getTitledRadioButton( + _("Authn Method token endpoint"), + name='tokenEndpointAuthMethod', + values=[('client_secret_basic', 'client_secret_basic'), ('client_secret_post', 'client_secret_post'), ('client_secret_jwt', 'client_secret_jwt'), ('private_key_jwt', 'private_key_jwt')], + current_value=self.data.get('tokenEndpointAuthMethod'), + jans_help=self.myparent.get_help_from_schema(schema, 'tokenEndpointAuthMethod'), + style='class:outh-client-radiobutton'), + + self.myparent.getTitledRadioButton( + _("Subject Type"), + name='subjectType', + values=[('public', 'Public'),('pairwise', 'Pairwise')], + current_value=self.data.get('subjectType'), + jans_help=self.myparent.get_help_from_schema(schema, 'subjectType'), + style='class:outh-client-radiobutton'), + + self.myparent.getTitledText( + _("Sector Identifier URI"), + name='sectorIdentifierUri', + value=self.data.get('sectorIdentifierUri',''), + jans_help=self.myparent.get_help_from_schema(schema, 'sectorIdentifierUri'), + style='class:outh-client-text'), + + self.myparent.getTitledCheckBoxList( + _("Grant"), + name='grantTypes', + 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=self.data.get('grantTypes', []), + jans_help=self.myparent.get_help_from_schema(schema, 'grantTypes'), + style='class:outh-client-checkboxlist'), + + self.myparent.getTitledCheckBoxList( + _("Response Types"), + name='responseTypes', + values=['code', 'token', 'id_token'], + current_values=self.data.get('responseTypes', []), + jans_help=self.myparent.get_help_from_schema(schema, 'responseTypes'), + style='class:outh-client-checkboxlist'), + + self.myparent.getTitledCheckBox(_("Supress Authorization"), + name='dynamicRegistrationPersistClientAuthorizations', + checked=self.data.get('dynamicRegistrationPersistClientAuthorizations'), + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/AppConfiguration'), + 'tokenEndpointAuthMethodsSupported'), + style='class:outh-client-checkbox'), + + self.myparent.getTitledRadioButton( + _("Application Type"), + name='applicationType', + values=['native','web'], + current_value=self.data.get('applicationType'), + jans_help=self.myparent.get_help_from_schema(schema, 'applicationType'), + style='class:outh-client-radiobutton'), + + self.myparent.getTitledText( + _("Redirect Uris"), + name='redirectUris', + value='\n'.join(self.data.get('redirectUris', [])), + height=3, + jans_help=self.myparent.get_help_from_schema(schema, 'redirectUris'), + style='class:outh-client-textrequired'), + + self.myparent.getTitledText( + _("Redirect Regex"), + name='redirectUrisRegex', + value=self.data.get('attributes', {}).get('redirectUrisRegex',''), + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'redirectUrisRegex'), + style='class:outh-client-text'), + + self.myparent.getTitledText(_("Scopes"), + name='scopes', + value='\n'.join(self.data.get('scopes', [])), + height=3, + jans_help=self.myparent.get_help_from_schema(schema, 'scopes'), + style='class:outh-client-text'), + + ],width=D(), + style='class:outh-client-tabs' + ) + + self.tabs['Tokens'] = HSplit([ + self.myparent.getTitledRadioButton( + _("Access Token Type"), + name='accessTokenAsJwt', + values=[('jwt', 'JWT'), ('reference', 'Reference')], + current_value= 'jwt' if self.data.get('accessTokenAsJwt') else 'reference', + jans_help=self.myparent.get_help_from_schema(schema, 'accessTokenAsJwt'), + style='class:outh-client-radiobutton'), + + self.myparent.getTitledCheckBox( + _("Incliude Claims in id_token"), + name='includeClaimsInIdToken', + checked=self.data.get('includeClaimsInIdToken'), + jans_help=self.myparent.get_help_from_schema(schema, 'includeClaimsInIdToken'), + style='class:outh-client-checkbox'), + + self.myparent.getTitledCheckBox( + _("Run introspection script before JWT access token creation"), + name='runIntrospectionScriptBeforeJwtCreation', + checked=self.data.get('attributes', {}).get('runIntrospectionScriptBeforeJwtCreation'), + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'runIntrospectionScriptBeforeJwtCreation'), + style='class:outh-client-checkbox'), + + self.myparent.getTitledText( + title=_("Token binding confirmation method for id_token"), + name='idTokenTokenBindingCnf', + value=self.data.get('idTokenTokenBindingCnf',''), + jans_help=self.myparent.get_help_from_schema(schema, 'idTokenTokenBindingCnf'), + style='class:outh-client-text'), + + self.myparent.getTitledText( + title=_("Access token additional audiences"), + name='additionalAudience', + value='\n'.join(self.data.get('attributes', {}).get('additionalAudience',[])), + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'additionalAudience'), + style='class:outh-client-text', + height = 3), + + self.myparent.getTitledText( + _("Access token lifetime"), + name='accessTokenLifetime', + value=self.data.get('accessTokenLifetime',''), + jans_help=self.myparent.get_help_from_schema(schema, 'accessTokenLifetime'), + text_type='integer', + style='class:outh-client-text'), + + self.myparent.getTitledText( + _("Refresh token lifetime"), + name='refreshTokenLifetime', + value=self.data.get('refreshTokenLifetime',''), + jans_help=self.myparent.get_help_from_schema(schema, 'refreshTokenLifetime'), + text_type='integer', + style='class:outh-client-text'), + + self.myparent.getTitledText( + _("Defult max authn age"), + name='defaultMaxAge', + value=self.data.get('defaultMaxAge',''), + jans_help=self.myparent.get_help_from_schema(schema, 'defaultMaxAge'), + text_type='integer', + style='class:outh-client-text'), + + ],width=D(),style='class:outh-client-tabs') + + self.tabs['Logout'] = HSplit([ + + self.myparent.getTitledText( + _("Front channel logout URI"), + name='frontChannelLogoutUri', + value=self.data.get('frontChannelLogoutUri',''), + jans_help=self.myparent.get_help_from_schema(schema, 'frontChannelLogoutUri'), ## No Descritption + style='class:outh-client-text'), + + self.myparent.getTitledText( + _("Post logout redirect URIs"), + name='postLogoutRedirectUris', + value='\n'.join(self.data.get('postLogoutRedirectUris',[])), + jans_help=self.myparent.get_help_from_schema(schema, 'postLogoutRedirectUris'), + height=3, style='class:outh-client-text'), + + self.myparent.getTitledText( + _("Back channel logout URI"), + name='backchannelLogoutUri', + value='\n'.join(self.data.get('attributes', {}).get('backchannelLogoutUri',[]) ), + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'backchannelLogoutUri'), + height=3, style='class:outh-client-text' + ), + + self.myparent.getTitledCheckBox( + _("Back channel logout session required"), + name='backchannelLogoutSessionRequired', + checked=self.data.get('attributes', {}).get('backchannelLogoutSessionRequired'), + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'backchannelLogoutSessionRequired'), + style='class:outh-client-checkbox' + ), + + self.myparent.getTitledCheckBox( + _("Front channel logout session required"), + name='frontChannelLogoutSessionRequired', + checked=self.data.get('frontChannelLogoutSessionRequired'), + jans_help=self.myparent.get_help_from_schema(schema, 'frontChannelLogoutSessionRequired'),## No Descritption + style='class:outh-client-checkbox'), + + ],width=D(),style='class:outh-client-tabs' + ) + + self.tabs['SoftwareInfo'] = HSplit([ + + self.myparent.getTitledText(_("Contacts"), ### height =3 insted of the <+> button + name='contacts', + value='\n'.join(self.data.get('contacts', [])), + height=3, + jans_help=self.myparent.get_help_from_schema(schema, 'contacts'), + style='class:outh-client-text'), + + self.myparent.getTitledText(_("Authorized JS origins"), ### height =3 insted of the <+> button + name='authorizedOrigins', + value='\n'.join(self.data.get('authorizedOrigins', [])), + height=3, + jans_help=self.myparent.get_help_from_schema(schema, 'authorizedOrigins'), + style='class:outh-client-text'), + + self.myparent.getTitledText( + title =_("Software id"), + name='softwareId', + value=self.data.get('softwareId',''), + jans_help=self.myparent.get_help_from_schema(schema, 'softwareId'), + style='class:outh-client-text'), + + self.myparent.getTitledText( + title =_("Software version"), + name='softwareVersion', + value=self.data.get('softwareVersion',''), + jans_help=self.myparent.get_help_from_schema(schema, 'softwareVersion'), + style='class:outh-client-text'), + + self.myparent.getTitledText( + title =_("Software statement"), + name='softwareStatement', + value=self.data.get('softwareStatement',''), + jans_help=self.myparent.get_help_from_schema(schema, 'softwareStatement'), + style='class:outh-client-text'), + + ],width=D(),style='class:outh-client-tabs') + + + self.uma_resources = HSplit([],width=D()) + self.resources = HSplit([ + VSplit([ + self.myparent.getButton(text=_("Get Resources"), name='oauth:Resources:get', jans_help=_("Retreive UMA Resources"), handler=self.oauth_get_uma_resources), + self.myparent.getTitledText(_("Search"), name='oauth:Resources:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_uma_resources,style='class:outh-client-textsearch'), + + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.uma_resources) + ],style='class:outh-client-tabs') + + self.tabs['CIBA/PAR/UMA'] = HSplit([ + Label(text=_("CIBA"),style='class:outh-client-label'), + self.myparent.getTitledRadioButton( + _("Token delivery method"), + name='backchannelTokenDeliveryMode', + current_value=self.data.get('backchannelTokenDeliveryMode'), + values=['poll','push', 'ping'], + jans_help=self.myparent.get_help_from_schema(schema, 'backchannelTokenDeliveryMode'), + style='class:outh-client-radiobutton'), + + self.myparent.getTitledText( + title =_("Client notification endpoint"), + name='backchannelClientNotificationEndpoint', + value=self.data.get('backchannelClientNotificationEndpoint',''), + jans_help=self.myparent.get_help_from_schema(schema, 'backchannelClientNotificationEndpoint'), + style='class:outh-client-text'), + + self.myparent.getTitledCheckBox( + _("Require user code param"), + name='backchannelUserCodeParameter', + checked=self.data.get('backchannelUserCodeParameter', ''), + style='class:outh-client-checkbox', + jans_help=self.myparent.get_help_from_schema(schema, 'backchannelUserCodeParameter'), + + ), + + Label(text=_("PAR"),style='class:outh-client-label'), + + self.myparent.getTitledText( + title =_("Request lifetime"), + name='parLifetime', + value=self.data.get('attributes', {}).get('parLifetime',0), + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'parLifetime'), + text_type='integer', + style='class:outh-client-text'), + + self.myparent.getTitledCheckBox( + _("Request PAR"), + name='requirePar', + checked=self.data.get('attributes', {}).get('requirePar',''), + style='class:outh-client-checkbox', + jans_help=self.myparent.get_help_from_schema(schema, 'requirePar'), + ), + + Label(_("UMA"), style='class:outh-client-label'), + + self.myparent.getTitledRadioButton( + _("PRT token type"), + name='rptAsJwt', + values=[('jwt', 'JWT'), ('reference', 'Reference')], + current_value='jwt' if self.data.get('rptAsJwt') else 'reference', + jans_help=self.myparent.get_help_from_schema(schema, 'rptAsJwt'), + style='class:outh-client-radiobutton'), + + self.myparent.getTitledText( + title =_("Claims redirect URI"), + name='claimRedirectUris', + value='\n'.join(self.data.get('claimRedirectUris','')), + jans_help=self.myparent.get_help_from_schema(schema, 'claimRedirectUris'), + height=3, + style='class:outh-client-text'), + + self.myparent.getTitledText(_("RPT Modification Script"), + name='rptClaimsScripts', + value='\n'.join(self.data.get('attributes', {}).get('rptClaimsScripts',[]) ), + height=3, + style='class:outh-client-text', + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/Scope'), + 'rptClaimsScripts'), + ), + + self.resources if self.data.get('inum','') else HSplit([],width=D()) + + ],width=D(),style='class:outh-client-tabs' + ) + + + encryption_signing = [ + self.myparent.getTitledText( + title =_("Client JWKS URI"), + name='jwksUri', + value=self.data.get('jwksUri',''), + jans_help=self.myparent.get_help_from_schema(schema, 'jwksUri'), + style='class:outh-client-text'), + + self.myparent.getTitledText( + title =_("Client JWKS"), + name='jwks', + value=self.data.get('jwks',''), + jans_help=self.myparent.get_help_from_schema(schema, 'jwks'), + style='class:outh-client-text'), + ] + + + self.drop_down_select_first = [] + + + # keep this line until this issue is closed https://github.com/JanssenProject/jans/issues/2372 + self.myparent.cli_object.openid_configuration['access_token_singing_alg_values_supported'] = ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'PS256', 'PS384', 'PS512'] + + + for title, swagger_key, openid_key in ( + + (_("ID Token Alg for Signing "), 'idTokenSignedResponseAlg', 'id_token_signing_alg_values_supported'), + (_("ID Token Alg for Encryption"), 'idTokenEncryptedResponseAlg', 'id_token_encryption_alg_values_supported'), + (_("ID Token Enc for Encryption"), 'idTokenEncryptedResponseEnc', 'id_token_encryption_enc_values_supported'), + (_("Access Token Alg for Signing "), 'accessTokenSigningAlg', 'access_token_singing_alg_values_supported'), #?? openid key + + (_("User Info for Signing "), 'userInfoSignedResponseAlg', 'userinfo_signing_alg_values_supported'), + (_("User Info Alg for Encryption"), 'userInfoEncryptedResponseAlg', 'userinfo_encryption_alg_values_supported'), + (_("User Info Enc for Encryption"), 'userInfoEncryptedResponseEnc', 'userinfo_encryption_enc_values_supported'), + + (_("Request Object Alg for Signing "), 'requestObjectSigningAlg', 'request_object_signing_alg_values_supported'), + (_("Request Object Alg for Encryption"), 'requestObjectEncryptionAlg', 'request_object_encryption_alg_values_supported'), + (_("Request Object Enc for Encryption"), 'requestObjectEncryptionEnc', 'request_object_encryption_enc_values_supported'), + ): + + self.drop_down_select_first.append(swagger_key) + + values = [ (alg, alg) for alg in self.myparent.cli_object.openid_configuration[openid_key] ] + + encryption_signing.append(self.myparent.getTitledWidget( + title, + name=swagger_key, + widget=DropDownWidget( + values=values, + value=self.data.get(swagger_key) + ), + jans_help=self.myparent.get_help_from_schema(schema, swagger_key), + style='class:outh-client-dropdown')) + + self.tabs['Encryption/Signing'] = HSplit(encryption_signing) + + def allow_spontaneous_changed(cb): + self.spontaneous_scopes.me.window.style = 'underline ' + (self.myparent.styles['textarea'] if cb.checked else self.myparent.styles['textarea-readonly']) + self.spontaneous_scopes.me.text = '' + self.spontaneous_scopes.me.read_only = not cb.checked + + self.spontaneous_scopes = self.myparent.getTitledText( + _("Spontaneos scopes validation regex"), + name='spontaneousScopeScriptDns', + value='\n'.join(self.data.get('attributes', {}).get('spontaneousScopeScriptDns',[]) ), + read_only=False if 'allowSpontaneousScopes' in self.data and self.data.get('attributes', {}).get('allowSpontaneousScopes') else True, + focusable=True, + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'spontaneousScopeScriptDns'), + height=3, + style='class:outh-client-text') + + + self.tabs['Advanced Client Properties'] = HSplit([ + + self.myparent.getTitledCheckBox( + _("Default Prompt login"), + name='jansDefaultPromptLogin', + checked=self.data.get('attributes', {}).get('jansDefaultPromptLogin'), + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'jansDefaultPromptLogin'), + + style='class:outh-client-checkbox' + ), + + self.myparent.getTitledCheckBox( + _("Persist Authorizations"), + name='persistClientAuthorizations', + checked=self.data.get('persistClientAuthorizations'), + jans_help=self.myparent.get_help_from_schema(schema, 'persistClientAuthorizations'), + style='class:outh-client-checkbox'), + + self.myparent.getTitledCheckBox( + _("Allow spontaneos scopes"), + name='allowSpontaneousScopes', + checked=self.data.get('attributes', {}).get('allowSpontaneousScopes'), + on_selection_changed=allow_spontaneous_changed, + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'allowSpontaneousScopes'), + style='class:outh-client-checkbox' + ), + + self.spontaneous_scopes, + + + VSplit([ + Label(text=_("Spontaneous scopes"),style='class:outh-client-label',width=len(_("Spontaneous scopes")*2)), ## TODO + Button( + _("View current"), + handler=self.show_client_scopes, + left_symbol='<', + right_symbol='>', + width=len(_("View current"))+2) + + ]) if self.data.get('inum','') else HSplit([],width=D()), + + self.myparent.getTitledText( + _("Initial Login URI"), + name='initiateLoginUri', + value=self.data.get('initiateLoginUri',''), + jans_help=self.myparent.get_help_from_schema(schema, 'initiateLoginUri'), + style='class:outh-client-text'), + + self.myparent.getTitledText(_("Request URIs"), ### height =3 insted of the <+> button + name='requestUris', + value='\n'.join(self.data.get('requestUris', [])), + height=3, + jans_help=self.myparent.get_help_from_schema(schema, 'requestUris'), + style='class:outh-client-text'), + + self.myparent.getTitledText(_("Default ACR"), ### height =3 >> "the type is array" cant be dropdown + name='defaultAcrValues', + value='\n'.join(self.data.get('defaultAcrValues', [])), + height=3, + jans_help=self.myparent.get_help_from_schema(schema, 'defaultAcrValues'), + style='class:outh-client-text'), + + self.myparent.getTitledText(_("Allowed ACR"), ### height =3 insted of the <+> button + name='jansAuthorizedAcr', + value='\n'.join(self.data.get('attributes', {}).get('jansAuthorizedAcr',[])), + height=3, + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'jansAuthorizedAcr'), + style='class:outh-client-text'), + + self.myparent.getTitledText( + _("TLS Subject DN"), + name='tlsClientAuthSubjectDn', + value='\n'.join(self.data.get('attributes', {}).get('tlsClientAuthSubjectDn',[])), + height=3, style='class:outh-client-text', + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'tlsClientAuthSubjectDn'), + ), + + self.myparent.getTitledWidget( + _("Client Expiration Date"), + name='expirationDate', + widget=DateSelectWidget( + value=self.data.get('expirationDate', ''),parent=self + ), + jans_help=self.myparent.get_help_from_schema(schema, 'expirationDate'), + style='class:outh-client-widget' + ), + + ],width=D(),style='class:outh-client-tabs' + ) + + self.tabs['Client Scripts'] = HSplit([ + + + self.myparent.getTitledText(_("Spontaneous Scopes"), + name='spontaneousScopes', + value='\n'.join(self.data.get('attributes', {}).get('spontaneousScopes',[])), + height=3, + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'spontaneousScopes'), + style='class:outh-client-text'), + + self.myparent.getTitledText(_("Update Token"), + name='updateTokenScriptDns', + value='\n'.join(self.data.get('attributes', {}).get('updateTokenScriptDns',[])), + height=3, + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'updateTokenScriptDns'), + style='class:outh-client-text'), + + self.myparent.getTitledText(_("Post Authn"), + name='postAuthnScripts', + value='\n'.join(self.data.get('attributes', {}).get('postAuthnScripts',[])), + height=3, + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'postAuthnScripts'), + style='class:outh-client-text'), + + self.myparent.getTitledText(_("Introspection"), + name='introspectionScripts', + value='\n'.join(self.data.get('attributes', {}).get('introspectionScripts',[])), + height=3, + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'introspectionScripts'), + style='class:outh-client-text'), + + self.myparent.getTitledText(_("Password Grant"), + name='ropcScripts', + value='\n'.join(self.data.get('attributes', {}).get('ropcScripts',[])), + height=3, + style='class:outh-client-text', + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'ropcScripts'), + ), + + self.myparent.getTitledText(_("OAuth Consent"), + name='consentGatheringScripts', + value='\n'.join(self.data.get('attributes', {}).get('consentGatheringScripts',[]) ), + height=3, + jans_help=self.myparent.get_help_from_schema( + self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/ClientAttributes'), + 'consentGatheringScripts'), + style='class:outh-client-text'), + ],width=D(),style='class:outh-client-tabs' + ) + + self.left_nav = list(self.tabs.keys())[0] + + def show_client_scopes(self) -> None: + client_scopes = self.data.get('scopes') + self.myparent.logger.debug('client_scopes: '+str(client_scopes)) + data = [] + for i in client_scopes : + try : + inum = i.split(',')[0][5:] + rsponse = self.myparent.cli_object.process_command_by_id( + operation_id='get-oauth-scopes-by-inum', + url_suffix='inum:{}'.format(inum), + endpoint_args="", + data_fn=None, + data={} + ) + + except Exception as e: + # self.myparent.show_message(_("Error getting clients"), str(e)) + pass + + if rsponse.status_code not in (200, 201): + # self.myparent.show_message(_("Error getting clients"), str(rsponse.text)) + pass + if rsponse.json().get('scopeType','') == 'spontaneous': + data.append(rsponse.json()) + + + self.myparent.logger.debug('datadata: '+str(data)) + if not data : + data = "No Scope of type: Spontaneous" + + body = HSplit([ + TextArea( + lexer=DynamicLexer(lambda: PygmentsLexer.from_filename('.json', sync_from_start=True)), + scrollbar=True, + line_numbers=True, + multiline=True, + read_only=True, + text=str(json.dumps(data, indent=2)), + style='class:jans-main-datadisplay.text' + ) + ],style='class:jans-main-datadisplay') + + dialog = JansGDialog(self.myparent, title='View Scopes', body=body) + + self.myparent.show_jans_dialog(dialog) + + def oauth_get_uma_resources(self) -> None: + """Method to get the clients data from server + """ + t = threading.Thread(target=self.oauth_update_uma_resources, daemon=True) + t.start() + + def search_uma_resources( + self, + tbuffer: Buffer, + ) -> None: + if not len(tbuffer.text) > 2: + self.myparent.show_message(_("Error!"), _("Search string should be at least three characters")) + return + + t = threading.Thread(target=self.oauth_update_uma_resources, args=(tbuffer.text,), daemon=True) + t.start() + + def oauth_update_uma_resources ( + self, + pattern: Optional[str]= '', + ) -> None: + """update the current uma_resources data to server + + Args: + pattern (str, optional): endpoint arguments for the uma_resources data. Defaults to ''. + """ + endpoint_args ='limit:10' + if pattern: + endpoint_args +=',pattern:'+pattern + + + self.myparent.logger.debug('DATA endpoint_args: '+str(endpoint_args)) + try : + rsponse = self.myparent.cli_object.process_command_by_id( + operation_id='get-oauth-uma-resources-by-clientid', + url_suffix='clientId:{}'.format(self.data['inum']), + endpoint_args=endpoint_args, + data_fn=None, + data={} + ) + + except Exception as e: + self.myparent.show_message(_("Error getting clients"), str(e)) + return + + if rsponse.status_code not in (200, 201): + self.myparent.show_message(_("Error getting clients"), str(rsponse.text)) + return + + result = {} + try: + result = rsponse.json() + except Exception: + self.myparent.show_message(_("Error getting clients"), str(rsponse.text)) + return + data =[] + + for d in result: + scopes_of_resource = [] + for scope_dn in d.get('scopes', []): + + inum = scope_dn.split(',')[0].split('=')[1] + scope_result = {} + try : + scope_response = self.myparent.cli_object.process_command_by_id( + operation_id='get-oauth-scopes-by-inum', + url_suffix='inum:{}'.format(inum), + endpoint_args='', + data_fn=None, + data={} + ) + scope_result = scope_response.json() + except Exception as e: + display_name = 'None' + pass + display_name = scope_result.get('displayName') or scope_result.get('inum') + + if display_name: + scopes_of_resource.append(display_name) + else: + scopes_of_resource.append(str(d.get('scopes', [''])[0] )) + data.append( + [ + d.get('id'), + str(d.get('description', '')), + ','.join(scopes_of_resource) + ] + ) + + if data : + self.uma_resources = HSplit([ + JansVerticalNav( + myparent=self.myparent, + headers=['id', 'Description', 'Scopes'], + preferred_size= [36,0,0], + data=data, + on_enter=self.view_uma_resources, + on_display=self.myparent.data_display_dialog, + on_delete=self.delete_UMAresource, + # selection_changed=self.data_selection_changed, + selectes=0, + headerColor='class:outh-client-navbar-headcolor', + entriesColor='class:outh-client-navbar-entriescolor', + all_data=result, + underline_headings=False, + ), + ]) + + get_app().invalidate() + self.myparent.layout.focus(self.uma_resources) + + else: + self.uma_resources = HSplit([],width=D()) + self.myparent.show_message(_("Oops"), _("No matching result"),tobefocused=self.resources.children[0].children[0]) + + def client_dialog_nav_selection_changed( + self, + selection: str + ) -> None: + self.left_nav = selection + + def view_uma_resources(self, **params: Any) -> None: + + selected_line_data = params['data'] ##self.uma_result + title = _("Edit user Data (Clients)") + + dialog = ViewUMADialog(self.myparent, title=title, data=selected_line_data, deleted_uma=self.delete_UMAresource) + + self.myparent.show_jans_dialog(dialog) + + def __pt_container__(self)-> Dialog: + return self.dialog + diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_scope_dialog.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_scope_dialog.py new file mode 100755 index 00000000000..5d101f9aa6b --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/edit_scope_dialog.py @@ -0,0 +1,469 @@ +from typing import Any, OrderedDict, Optional, Sequence, Union, TypeVar, Callable +from asyncio import ensure_future + +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.containers import ( + HSplit, + VSplit, + DynamicContainer, + Window, + AnyContainer +) +from prompt_toolkit.widgets import ( + Box, + Button, + Label, +) +from prompt_toolkit.application.current import get_app +from prompt_toolkit.widgets import ( + Button, + Dialog, + VerticalLine, + HorizontalLine, + CheckboxList, +) + +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.widgets.base import RadioList +from prompt_toolkit.layout.containers import FloatContainer +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.layout.dimension import AnyDimension + +from cli import config_cli +from utils.static import DialogResult +from utils.utils import DialogUtils +from utils.multi_lang import _ +from wui_components.jans_dialog_with_nav import JansDialogWithNav +from wui_components.jans_side_nav_bar import JansSideNavBar +from wui_components.jans_cli_dialog import JansGDialog +from wui_components.jans_drop_down import DropDownWidget +from wui_components.jans_data_picker import DateSelectWidget +from wui_components.jans_vetrical_nav import JansVerticalNav +from view_uma_dialog import ViewUMADialog + + + + +class EditScopeDialog(JansGDialog, DialogUtils): + """The Main Scope Dialog that contain every thing related to The Scope + """ + def __init__( + self, + parent, + title: AnyFormattedText, + data: list, + buttons: Optional[Sequence[Button]]= [], + save_handler: Callable= None, + )-> Dialog: + """init for `EditScopeDialog`, inherits from two diffrent classes `JansGDialog` and `DialogUtils` + + DialogUtils (methods): Responsable for all `make data from dialog` and `check required fields` in the form for any Edit or Add New + + Args: + parent (widget): This is the parent widget for the dialog, to access `Pageup` and `Pagedown` + title (str): The Main dialog title + data (list): selected line data + button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. + save_handler (method, optional): handler invoked when closing the dialog. Defaults to None. + """ + super().__init__(parent, title, buttons) + self.save_handler = save_handler + self.data = data + self.title = title + self.claims_container = None + self.showInConfigurationEndpoint = self.data.get('attributes', {}).get('showInConfigurationEndpoint', '') + self.defaultScope = self.data.get('defaultScope', '') + self.schema = self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/Scope') + self.tbuffer = None + self.prepare_tabs() + self.create_window() + self.sope_type = self.data.get('scopeType') or 'oauth' + + def save(self) -> None: + self.myparent.logger.debug('SAVE SCOPE') + + data = {} + + for item in self.dialog.content.children + self.alt_tabs[self.sope_type].children: + item_data = self.get_item_data(item) + if item_data: + data[item_data['key']] = item_data['value'] + + if data['scopeType'] in ('openid', 'dynamic') and hasattr(self, 'claims_container') and self.claims_container: + claims = [claim[0] for claim in self.claims_container.data] + data['claims'] = claims + + self.myparent.logger.debug('DATA: ' + str(data)) + self.data = data + if 'attributes' in self.data.keys(): + self.data['attributes'] = {'showInConfigurationEndpoint':self.data['attributes']} + + close_me = True + if self.save_handler: + close_me = self.save_handler(self) + if close_me: + self.future.set_result(DialogResult.ACCEPT) + + def cancel(self) -> None: + self.future.set_result(DialogResult.CANCEL) + + def create_window(self) -> None: + scope_types = [('oauth', 'OAuth'), ('openid', 'OpenID'), ('dynamic', 'Dynamic'), ('uma', 'UMA')] + buttons = [(self.save, _("Save")), (self.cancel, _("Cancel"))] + if self.data: + if self.data.get('scopeType') == 'spontaneous': + scope_types.insert(3, ('spontaneous', 'Spontaneous')) + buttons.pop(0) + + if self.data.get('scopeType') == 'uma': + buttons.pop(0) + else: + for stype in scope_types[:]: + if stype[0] == 'uma': + scope_types.remove(stype) + + self.dialog = JansDialogWithNav( + title=self.title, + content= HSplit([ + self.myparent.getTitledRadioButton( + _("Scope Type"), + name='scopeType', + current_value=self.data.get('scopeType'), + values=scope_types, + on_selection_changed=self.scope_selection_changed, + jans_help=self.myparent.get_help_from_schema(self.schema, 'scopeType'), + + style='class:outh-scope-radiobutton'), + + self.myparent.getTitledText( + _("id"), + name='id', + value=self.data.get('id',''), + jans_help=self.myparent.get_help_from_schema(self.schema, 'id'), + style='class:outh-scope-text'), + + self.myparent.getTitledText( + _("inum"), + name='inum', + value=self.data.get('inum',''), + jans_help=self.myparent.get_help_from_schema(self.schema, 'inum'), + style='class:outh-scope-text', + read_only=True,), + + self.myparent.getTitledText( + _("Display Name"), + name='displayName', + value=self.data.get('displayName',''), + jans_help=self.myparent.get_help_from_schema(self.schema, 'displayName'), + style='class:outh-scope-text'), + + self.myparent.getTitledText( + _("Description"), + name='description', + value=self.data.get('description',''), + jans_help=self.myparent.get_help_from_schema(self.schema, 'description'), + style='class:outh-scope-text'), + + DynamicContainer(lambda: self.alt_tabs[self.sope_type]), + ], style='class:outh-scope-tabs'), + button_functions=buttons, + height=self.myparent.dialog_height, + width=self.myparent.dialog_width, + ) + + def scope_selection_changed( + self, + cb: RadioList, + ) -> None: + self.sope_type = cb.current_value + + def get_named_claims( + self, + claims_list:list + ) -> list: + try : + responce = self.myparent.cli_object.process_command_by_id( + operation_id='get-attributes', + url_suffix='', + endpoint_args='', + data_fn=None, + data={} + ) + except Exception as e: + self.myparent.show_message(_("Error getting claims"), str(e)) + return + + if responce.status_code not in (200, 201): + self.myparent.show_message(_("Error getting clients"), str(responce.text)) + return + + + result = responce.json() + + calims_names=[] + for entry in result["entries"] : + for claim in claims_list: + if claim == entry['dn']: + calims_names.append([entry['dn'], entry['displayName']]) + + return calims_names + + + def delete_claim(self, **kwargs: Any) -> None: + """This method for the deletion of claim + + Args: + selected (_type_): The selected claim + event (_type_): _description_ + + """ + + dialog = self.myparent.get_confirm_dialog(_("Are you sure want to delete claim dn:")+"\n {} ?".format(selected[0])) + async def coroutine(): + focused_before = self.myparent.layout.current_window + result = await self.myparent.show_dialog_as_float(dialog) + try: + self.myparent.layout.focus(focused_before) + except: + self.myparent.layout.focus(self.myparent.center_frame) ## + + if result.lower() == 'yes': + self.data['claims'].remove(kwargs['selected'][0]) + self.claims_container.data.remove(kwargs['selected']) + + + ensure_future(coroutine()) + + def prepare_tabs(self) -> None: + """Prepare the tabs for Edil Scope Dialogs + """ + + self.alt_tabs = {} + + + self.alt_tabs['oauth'] = HSplit([ + self.myparent.getTitledCheckBox( + _("Default Scope"), + name='defaultScope', + checked=self.data.get('defaultScope'), + jans_help=self.myparent.get_help_from_schema(self.schema, 'defaultScope'), + style='class:outh-scope-checkbox', + ), + + self.myparent.getTitledCheckBox( + _("Show in configuration endpoint"), + name='showInConfigurationEndpoint', + checked=self.data.get('attributes',{}).get('showInConfigurationEndpoint',''), + jans_help='Configuration Endpoint', + style='class:outh-scope-checkbox', + ), + + ],width=D(),) + + + open_id_widgets = [ + self.myparent.getTitledCheckBox( + _("Default Scope"), + name='defaultScope', + checked=self.data.get('defaultScope'), + jans_help=self.myparent.get_help_from_schema(self.schema, 'defaultScope'), + style='class:outh-scope-checkbox', + ), + + self.myparent.getTitledCheckBox( + _("Show in configuration endpoint"), + name='showInConfigurationEndpoint', + checked=self.data.get('attributes',{}).get('showInConfigurationEndpoint',''), + jans_help='Configuration Endpoint', + style='class:outh-scope-checkbox', + ), + + # Window(char='-', height=1), + + # HorizontalLine(), + self.myparent.getTitledText( + _("Search"), + name='__search_claims__', + style='class:outh-scope-textsearch',width=10, + jans_help=_("Press enter to perform search"), + accept_handler=self.search_claims, + ), + ] + calims_data = self.get_named_claims(self.data.get('claims', [])) + + if calims_data : + self.claims_container = JansVerticalNav( + myparent=self.myparent, + headers=['dn', 'Display Name'], + preferred_size= [0,0], + data=calims_data, + on_display=self.myparent.data_display_dialog, + on_delete=self.delete_claim, + selectes=0, + headerColor='class:outh-client-navbar-headcolor', + entriesColor='class:outh-client-navbar-entriescolor', + all_data=calims_data + ) + + open_id_widgets.append(self.claims_container) + + self.alt_tabs['openid'] = HSplit(open_id_widgets, width=D()) + + self.alt_tabs['dynamic'] = HSplit([ + + self.myparent.getTitledText(_("Dynamic Scope Script"), + name='dynamicScopeScripts', + value='\n'.join(self.data.get('dynamicScopeScripts', [])), + jans_help=self.myparent.get_help_from_schema(self.schema, 'dynamicScopeScripts'), + height=3, + style='class:outh-scope-text'), + + # Window(char='-', height=1), + self.myparent.getTitledText( + _("Search"), + name='__search_claims__', + style='class:outh-scope-textsearch',width=10, + jans_help=_("Press enter to perform search"), ),#accept_handler=self.search_scopes + + self.myparent.getTitledText( + _("Claims"), + name='claims', + value='\n'.join(self.data.get('claims', [])), + height=3, + jans_help=self.myparent.get_help_from_schema(self.schema, 'claims'), + style='class:outh-scope-text'), + + # Label(text=_("Claims"),style='red'), ## name = claims TODO + + ],width=D(), + ) + + self.alt_tabs['spontaneous'] = HSplit([ + self.myparent.getTitledText( + _("Associated Client"), + name='none', + value=self.data.get('none',''), + style='class:outh-scope-text', + read_only=True, + jans_help=self.myparent.get_help_from_schema(self.schema, 'none'), + height=3,),## Not fount + + self.myparent.getTitledText( + _("Creationg time"), + name='creationDate', + value=self.data.get('creationDate',''), + jans_help=self.myparent.get_help_from_schema(self.schema, 'creationDate'), + style='class:outh-scope-text', + read_only=True,), + + ],width=D(), + ) + + uma_creator = self.data.get('creatorId','') or self.myparent.cli_object.get_user_info().get('inum','') + + + self.alt_tabs['uma'] = HSplit([ + self.myparent.getTitledText( + _("IconURL"), + name='iconUrl', + value=self.data.get('iconUrl',''), + jans_help=self.myparent.get_help_from_schema(self.schema, 'iconUrl'), + style='class:outh-scope-text'), + + + self.myparent.getTitledText(_("Authorization Policies"), + name='umaAuthorizationPolicies', + value='\n'.join(self.data.get('umaAuthorizationPolicies', [])), + height=3, + jans_help=self.myparent.get_help_from_schema(self.schema, 'umaAuthorizationPolicies'), + style='class:outh-scope-text'), + + self.myparent.getTitledText( + _("Associated Client"), + name='none', + value=self.data.get('none',''), + jans_help=self.myparent.get_help_from_schema(self.schema, 'none'), + style='class:outh-scope-text', + read_only=True, + height=3,), ## Not fount + + self.myparent.getTitledText( + _("Creationg time"), + name='description', + value=self.data.get('description',''), + jans_help=self.myparent.get_help_from_schema(self.schema, 'description'), + style='class:outh-scope-text', + read_only=True,), + + self.myparent.getTitledText( + _("Creator"), + name='Creator', + style='class:outh-scope-text', + jans_help=self.myparent.get_help_from_schema(self.schema, 'Creator'), + read_only=True, + value=uma_creator + ), + ], + width=D(), + ) + + def search_claims( + self, + textbuffer: Buffer, + ) -> None: + + try : + responce = self.myparent.cli_object.process_command_by_id( + operation_id='get-attributes', + url_suffix='', + endpoint_args='pattern:{}'.format(textbuffer.text), + data_fn=None, + data={} + ) + except Exception as e: + self.myparent.show_message(_("Error searching claims"), str(e)) + return + + result = responce.json() + + if not result.get('entries'): + self.myparent.show_message(_("Ooops"), _("Can't find any claim for ʹ%sʹ.") % textbuffer.text) + return + + + def add_selected_claims(dialog): + if 'claims' not in self.data: + self.data['claims'] = [] + + for item in dialog.body.current_values: + self.claims_container.add_item(item) + self.data['claims'].append(item[0]) + + current_data = self.get_named_claims(self.data.get('claims', [])) + + for i in range(len(current_data)): + current_data[i] = current_data[i][0] + + values = [([claim['dn'], claim['displayName']], claim['displayName']) for claim in result['entries']] + values_uniqe = [] + + for k in values: + if k[0][0] in current_data: + pass + else: + values_uniqe.append(k) + + if not values_uniqe: + self.myparent.show_message(_("Ooops"), _("Can't find any New claim for ʹ%sʹ.") % textbuffer.text) + return + + check_box_list = CheckboxList( + values=values_uniqe, + ) + buttons = [Button(_("Cancel")), Button(_("OK"), handler=add_selected_claims)] + dialog = JansGDialog(self.myparent, title=_("Select claims to add"), body=check_box_list, buttons=buttons) + self.myparent.show_jans_dialog(dialog) + + def __pt_container__(self) -> Dialog: + return self.dialog + diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/main.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/main.py new file mode 100755 index 00000000000..7c460759bae --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/main.py @@ -0,0 +1,930 @@ +import os +import sys +import time +import json + +import threading +import asyncio +from functools import partial +from typing import Any, Optional + + +import prompt_toolkit +from prompt_toolkit.application.current import get_app +from prompt_toolkit.eventloop import get_event_loop +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous +from prompt_toolkit.layout.containers import ( + HSplit, + VSplit, + HorizontalAlign, + DynamicContainer, + Window, +) +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.widgets import ( + Box, + Button, + Label, + Frame, + Dialog, + CheckboxList, + TextArea +) +from prompt_toolkit.lexers import PygmentsLexer, DynamicLexer + + +from utils.static import DialogResult +from prompt_toolkit.layout import ScrollablePane +from asyncio import Future + +from cli import config_cli +from utils.utils import DialogUtils +from wui_components.jans_nav_bar import JansNavBar +from wui_components.jans_side_nav_bar import JansSideNavBar +from wui_components.jans_vetrical_nav import JansVerticalNav +from wui_components.jans_dialog import JansDialog +from wui_components.jans_dialog_with_nav import JansDialogWithNav +from wui_components.jans_drop_down import DropDownWidget +from wui_components.jans_data_picker import DateSelectWidget +from wui_components.jans_cli_dialog import JansGDialog + +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 _ +import cli_style + +class Plugin(DialogUtils): + """This is a general class for plugins + """ + def __init__( + self, + app: Application + ) -> None: + """init for Plugin class "oxauth" + + Args: + app (_type_): _description_ + """ + self.app = app + self.pid = 'oxauth' + self.name = '[A]uth Server' + self.search_text= None + + 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: + + self.app.create_background_task(self.get_appconfiguration()) + self.schema = self.app.cli_object.get_schema_from_reference('', '#/components/schemas/AppConfiguration') + + + async def get_appconfiguration(self) -> None: + """Coroutine for getting application configuration. + """ + try: + response = self.app.cli_object.process_command_by_id( + operation_id='get-properties', + url_suffix='', + endpoint_args='', + data_fn=None, + data={} + ) + + except Exception as e: + self.app.show_message(_("Error getting Jans configuration"), str(e)) + return + + if response.status_code not in (200, 201): + self.app.show_message(_("Error getting Jans configuration"), str(response.text)) + return + + self.app_configuration = response.json() + self.oauth_logging() + + def process(self): + pass + + def set_center_frame(self) -> None: + """center frame content + """ + self.app.center_container = self.oauth_main_container + + def oauth_prepare_containers(self) -> None: + """prepare the main container (tabs) for the current Plugin + """ + + self.oauth_data_container = { + 'clients': HSplit([],width=D()), + 'scopes': HSplit([],width=D()), + 'keys': HSplit([],width=D()), + 'properties': HSplit([],width=D()), + 'logging': HSplit([],width=D()), + } + + self.oauth_main_area = HSplit([],width=D()) + + self.oauth_containers['scopes'] = HSplit([ + VSplit([ + self.app.getButton(text=_("Get Scopes"), name='oauth:scopes:get', jans_help=_("Retreive first {} Scopes").format(self.app.entries_per_page), handler=self.oauth_get_scopes), + self.app.getTitledText(_("Search: "), name='oauth:scopes:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_scope,style='class:outh_containers_scopes.text'), + self.app.getButton(text=_("Add Scope"), name='oauth:scopes:add', jans_help=_("To add a new scope press this button"), handler=self.add_scope), + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.oauth_data_container['scopes']) + ],style='class:outh_containers_scopes') + + self.oauth_containers['clients'] = HSplit([ + VSplit([ + self.app.getButton(text=_("Get Clients"), name='oauth:clients:get', jans_help=_("Retreive first {} OpenID Connect clients").format(self.app.entries_per_page), handler=self.oauth_update_clients), + self.app.getTitledText(_("Search"), name='oauth:clients:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_clients,style='class:outh_containers_clients.text'), + self.app.getButton(text=_("Add Client"), name='oauth:clients:add', jans_help=_("To add a new client press this button"), handler=self.add_client), + + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.oauth_data_container['clients']) + ],style='class:outh_containers_clients') + + + self.oauth_containers['keys'] = HSplit([ + VSplit([ + self.app.getButton(text=_("Get Keys"), name='oauth:keys:get', jans_help=_("Retreive Auth Server keys"), handler=self.oauth_get_keys), + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.oauth_data_container['keys']) + ], style='class:outh_containers_clients') + + self.oauth_containers['properties'] = HSplit([ + VSplit([ + self.app.getButton(text=_("Get properties"), name='oauth:scopes:get', jans_help=_("Retreive first {} Scopes").format(self.app.entries_per_page), handler=self.oauth_get_properties), + self.app.getTitledText( + _("Search: "), + name='oauth:properties:search', + jans_help=_("Press enter to perform search"), + accept_handler=self.search_properties, + style='class:outh_containers_scopes.text') ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.oauth_data_container['properties']) + ],style='class:outh_containers_scopes') + + + self.oauth_containers['logging'] = DynamicContainer(lambda: self.oauth_data_container['logging']) + + self.oauth_main_container = HSplit([ + Box(self.nav_bar.nav_window, style='class:sub-navbar', height=1), + DynamicContainer(lambda: self.oauth_main_area), + ], + height=D(), + style='class:outh_maincontainer' + ) + + def oauth_prepare_navbar(self) -> None: + """prepare the navbar for the current Plugin + """ + 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')], + selection_changed=self.oauth_nav_selection_changed, + select=0, + jans_name='oauth:nav_bar' + ) + + def oauth_nav_selection_changed( + self, + selection + ) -> None: + """This method for the selection change + + Args: + selection (str): the current selected tab + """ + if selection in self.oauth_containers: + self.oauth_main_area = self.oauth_containers[selection] + else: + self.oauth_main_area = self.app.not_implemented + + def oauth_update_clients( + self, + start_index: Optional[int]= 0, + pattern: Optional[str]= '', + ) -> None: + """update the current clients data to server + + Args: + pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + """ + + def get_next( + start_index: int, + pattern: Optional[str]= '', + ) -> None: + self.oauth_update_clients(start_index, pattern='') + + async def coroutine(): + endpoint_args ='limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) + if pattern: + endpoint_args +=',pattern:'+pattern + cli_args = {'operation_id': 'get-oauth-openid-clients', 'endpoint_args': endpoint_args} + self.app.start_progressing() + response = await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + + if response.status_code not in (200, 201): + self.app.show_message(_("Error getting clients"), str(response.text)) + return + + try: + result = response.json() + except Exception: + self.app.show_message(_("Error getting clients"), str(response.text)) + return + + data =[] + + file1 = open("hopa.log", "w") + file1.write(str(response.status_code)+'\n') + file1.write(str(response.json())) + file1.close() + + for d in result.get('entries', []): + data.append( + [ + d['inum'], + d.get('clientName', ''), + ','.join(d.get('grantTypes', [])), + d.get('subjectType', '') + ] + ) + + 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_dialog, + on_display=self.app.data_display_dialog, + on_delete=self.delete_client, + get_help=(self.get_help,'Client'), + selectes=0, + headerColor='class:outh-verticalnav-headcolor', + entriesColor='class:outh-verticalnav-entriescolor', + all_data=result['entries'] + ) + buttons = [] + if start_index > 0: + handler_partial = partial(get_next, start_index-self.app.entries_per_page, pattern) + prev_button = Button(_("Prev"), handler=handler_partial) + prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + buttons.append(prev_button) + if result['start'] + self.app.entries_per_page < result['totalEntriesCount']: + handler_partial = partial(get_next, start_index+self.app.entries_per_page, pattern) + next_button = Button(_("Next"), handler=handler_partial) + next_button.window.jans_help = _("Retreives next %d entries") % self.app.entries_per_page + buttons.append(next_button) + + self.app.layout.focus(clients) # clients.focuse..!? TODO >> DONE + self.oauth_data_container['clients'] = HSplit([ + clients, + VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) + ]) + + get_app().invalidate() + self.app.layout.focus(clients) ### it fix focuse on the last item deletion >> try on UMA-res >> edit_client_dialog >> oauth_update_uma_resources + + else: + self.app.show_message(_("Oops"), _("No matching result"),tobefocused = self.oauth_containers['clients']) + + self.app.start_progressing() + asyncio.ensure_future(coroutine()) + + + def delete_client(self, **kwargs: Any) -> None: + """This method for the deletion of the clients data + + Args: + selected (_type_): The selected Client + event (_type_): _description_ + + Returns: + str: The server response + """ + + dialog = self.app.get_confirm_dialog(_("Are you sure want to delete client inum:")+"\n {} ?".format(kwargs ['selected'][0])) + + async def coroutine(): + focused_before = self.app.layout.current_window + result = await self.app.show_dialog_as_float(dialog) + try: + self.app.layout.focus(focused_before) + except: + self.app.stop_progressing() + self.app.layout.focus(self.app.center_frame) + + if result.lower() == 'yes': + result = self.app.cli_object.process_command_by_id( + operation_id='delete-oauth-openid-client-by-inum', + url_suffix='inum:{}'.format(kwargs ['selected'][0]), + endpoint_args='', + data_fn='', + data={} + ) + # TODO Need to do `self.oauth_get_clients()` only if clients list is not empty + self.app.stop_progressing() + self.oauth_update_clients() + + return result + + asyncio.ensure_future(coroutine()) + + + def oauth_get_scopes( + self, + start_index: Optional[int]= 0, + pattern: Optional[str]= '', + ) -> None: + """update the current Scopes data to server + + Args: + start_index (int, optional): add Button("Prev") to the layout. Defaults to 0. + """ + + async def coroutine(): + + endpoint_args ='withAssociatedClients:true,limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) + if pattern: + endpoint_args +=',pattern:'+pattern + + cli_args = {'operation_id': 'get-oauth-scopes', 'endpoint_args':endpoint_args} + self.app.start_progressing() + response = await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + try: + result = response.json() + except Exception as e: + self.app.show_message(_("Error getting response"), str(response)) + return + + + data =[] + + for d in result.get('entries', []): + data.append( + [ + d['id'], + d.get('description', ''), + d.get('scopeType',''), ## some scopes have no scopetypr + d['inum'] + ] + ) + + 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='class:outh-verticalnav-headcolor', + entriesColor='class:outh-verticalnav-entriescolor', + all_data=result['entries'] + ) + + buttons = [] + if start_index > 0: + handler_partial = partial(self.oauth_get_scopes, start_index-self.app.entries_per_page, pattern) + prev_button = Button(_("Prev"), handler=handler_partial) + prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + buttons.append(prev_button) + if result['start'] + self.app.entries_per_page < result['totalEntriesCount']: + handler_partial = partial(self.oauth_get_scopes, start_index+self.app.entries_per_page, pattern) + next_button = Button(_("Next"), handler=handler_partial) + next_button.window.jans_help = _("Retreives next %d entries") % self.app.entries_per_page + buttons.append(next_button) + + self.app.layout.focus(scopes) # clients.focuse..!? TODO >> DONE + self.oauth_data_container['scopes'] = HSplit([ + scopes, + VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) + ]) + + get_app().invalidate() + self.app.layout.focus(scopes) ### it fix focuse on the last item deletion >> try on UMA-res >> edit_client_dialog >> oauth_update_uma_resources + + else: + self.app.show_message(_("Oops"), _("No matching result"),tobefocused = self.oauth_containers['scopes']) + + asyncio.ensure_future(coroutine()) + + + def oauth_update_properties( + self, + start_index: Optional[int]= 0, + pattern: Optional[str]= '', + ) -> None: + """update the current clients data to server + + Args: + pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + """ + def get_next( + start_index: int, + pattern: Optional[str]= '', + ) -> None: + self.app.logger.debug("start_index="+str(start_index)) + self.oauth_update_properties(start_index, pattern=pattern) + + # ------------------------------------------------------------------------------- # + # ------------------------------------------------------------------------------- # + # ------------------------------------------------------------------------------- # + + try : + rsponse = self.app.cli_object.process_command_by_id( + operation_id='get-properties', + url_suffix='', + endpoint_args='', + data_fn=None, + data={} + ) + except Exception as e: + self.app.stop_progressing() + self.app.show_message(_("Error getting properties"), str(e)) + return + + self.app.stop_progressing() + if rsponse.status_code not in (200, 201): + self.app.show_message(_("Error getting properties"), str(rsponse.text)) + return + + try: + result = rsponse.json() + except Exception: + self.app.show_message(_("Error getting properties"), str(rsponse.text)) + return + + # ------------------------------------------------------------------------------- # + # ----------------------------------- Search ------------------------------------ # + # ------------------------------------------------------------------------------- # + porp_schema = self.app.cli_object.get_schema_from_reference('', '#/components/schemas/AppConfiguration') + + data =[] + if pattern: + for k in result: + if pattern.lower() in k.lower(): + if k in porp_schema.get('properties', {}): + data.append( + [ + k, + result[k], + ] + ) + else: + for d in result: + if d in porp_schema.get('properties', {}): + data.append( + [ + d, + result[d], + ] + ) + + + # ------------------------------------------------------------------------------- # + # --------------------------------- View Data ----------------------------------- # + # ------------------------------------------------------------------------------- # + + if data: + buttons = [] + if int(len(data)/ 20) >=1 : + + if start_index< int(len(data)/ 20) : + handler_partial = partial(get_next, start_index+1, pattern) + next_button = Button(_("Next"), handler=handler_partial) + next_button.window.jans_help = _("Retreives next %d entries") % self.app.entries_per_page + buttons.append(next_button) + + if start_index!=0: + handler_partial = partial(get_next, start_index-1, pattern) + prev_button = Button(_("Prev"), handler=handler_partial) + prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + buttons.append(prev_button) + + + data_now = data[start_index*20:start_index*20+20] + + clients = 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'), + # selection_changed=self.data_selection_changed, + selectes=0, + headerColor='class:outh-verticalnav-headcolor', + entriesColor='class:outh-verticalnav-entriescolor', + all_data=list(result.values()) + ) + self.app.layout.focus(clients) # clients.focuse..!? TODO >> DONE + self.oauth_data_container['properties'] = HSplit([ + clients, + VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) + ]) + get_app().invalidate() + self.app.layout.focus(clients) ### it fix focuse on the last item deletion >> try on UMA-res >> edit_client_dialog >> oauth_update_uma_resources + else: + self.app.show_message(_("Oops"), _("No matching result"),tobefocused = self.oauth_containers['properties']) + + def properties_display_dialog(self, **params: Any) -> None: + data_property, data_value = params['selected'][0], params['selected'][1] + body = HSplit([ + TextArea( + lexer=DynamicLexer(lambda: PygmentsLexer.from_filename('.json', sync_from_start=True)), + scrollbar=True, + line_numbers=True, + multiline=True, + read_only=True, + text=str(json.dumps(data_value, indent=2)), + style='class:jans-main-datadisplay.text' + ) + ],style='class:jans-main-datadisplay') + + dialog = JansGDialog(self.app, title=data_property, body=body) + + self.app.show_jans_dialog(dialog) + + def oauth_get_properties(self) -> None: + """Method to get the clients data from server + """ + self.oauth_data_container['properties'] = HSplit([Label(_("Please wait while getting properties"),style='class:outh-waitclientdata.label')], width=D(),style='class:outh-waitclientdata') + t = threading.Thread(target=self.oauth_update_properties, daemon=True) + self.app.start_progressing() + t.start() + + def view_property(self, **params: Any) -> None: + #property, value =params['passed'] + + + selected_line_data = params['passed'] ##self.uma_result + + title = _("Edit property") + + dialog = ViewProperty(self.app, title=title, data=selected_line_data, get_properties= self.oauth_get_properties, search_properties=self.search_properties, search_text=self.search_text) + + self.app.show_jans_dialog(dialog) + + def search_properties(self, tbuffer:Buffer,) -> None: + self.app.logger.debug("tbuffer="+str(tbuffer)) + self.app.logger.debug("type tbuffer="+str(type(tbuffer))) + self.search_text=tbuffer.text + + if not len(tbuffer.text) > 2: + self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.oauth_containers['properties']) + return + t = threading.Thread(target=self.oauth_update_properties, args=(0,tbuffer.text), daemon=True) + self.app.start_progressing() + t.start() + + def oauth_update_keys(self) -> None: + + """update the current Keys fromserver + """ + + try : + rsponse = self.app.cli_object.process_command_by_id( + operation_id='get-config-jwks', + url_suffix='', + endpoint_args='', + data_fn=None, + data={} + ) + except Exception as e: + self.app.stop_progressing() + self.app.show_message(_("Error getting keys"), str(e)) + return + + self.app.stop_progressing() + if rsponse.status_code not in (200, 201): + self.app.show_message(_("Error getting keys"), str(rsponse.text)) + return + + try: + result = rsponse.json() + except Exception: + self.app.show_message(_("Error getting keys"), str(rsponse.text)) + return + + data =[] + + for d in result.get('keys', []): + try: + gmt = time.gmtime(int(d['exp'])/1000) + exps = time.strftime("%d %b %Y %H:%M:%S", gmt) + except Exception: + exps = d.get('exp', '') + data.append( + [ + d['name'], + exps, + ] + ) + + if data: + + keys = JansVerticalNav( + myparent=self.app, + headers=['Name', 'Expiration'], + data=data, + preferred_size=[0,0], + on_display=self.app.data_display_dialog, + selectes=0, + headerColor='class:outh-verticalnav-headcolor', + entriesColor='class:outh-verticalnav-entriescolor', + all_data=result['keys'] + ) + + self.oauth_data_container['keys'] = HSplit([keys]) + get_app().invalidate() + self.app.layout.focus(keys) + + else: + self.app.show_message(_("Oops"), _("Nothing to display"), tobefocused=self.oauth_containers['keys']) + + def oauth_get_keys(self) -> None: + """Method to get the Keys from server + """ + self.oauth_data_container['keys'] = HSplit([Label(_("Please wait while getting Keys"), style='class:outh-waitscopedata.label')], width=D(), style='class:outh-waitclientdata') + t = threading.Thread(target=self.oauth_update_keys, daemon=True) + self.app.start_progressing() + t.start() + + def edit_scope_dialog(self, **params: Any) -> None: + selected_line_data = params['data'] + + dialog = EditScopeDialog(self.app, title=_("Edit Scopes"), data=selected_line_data, save_handler=self.save_scope) + self.app.show_jans_dialog(dialog) + + def edit_client_dialog(self, **params: Any) -> None: + selected_line_data = params['data'] + title = _("Edit user Data (Clients)") + + file1= open("hopa.log",'a') + file1.write("selected_line_data : "+str(selected_line_data)+'\n \n') + file1.close() + + self.EditClientDialog = EditClientDialog(self.app, title=title, data=selected_line_data,save_handler=self.save_client,delete_UMAresource=self.delete_UMAresource) + self.app.show_jans_dialog(self.EditClientDialog) + + def save_client(self, dialog: Dialog) -> None: + """This method to save the client data to server + + Args: + dialog (_type_): the main dialog to save data in + + Returns: + _type_: bool value to check the status code response + """ + + + response = self.app.cli_object.process_command_by_id( + operation_id='put-oauth-openid-client' if dialog.data.get('inum') else 'post-oauth-openid-client', + url_suffix='', + endpoint_args='', + data_fn='', + data=dialog.data + ) + + self.app.stop_progressing() + if response.status_code in (200, 201): + self.oauth_update_clients() + return True + + self.app.show_message(_("Error!"), _("An error ocurred while saving client:\n") + str(response.text)) + + + def save_scope(self, dialog: Dialog) -> None: + """This method to save the client data to server + + Args: + dialog (_type_): the main dialog to save data in + + Returns: + _type_: bool value to check the status code response + """ + + async def coroutine(): + operation_id='put-oauth-scopes' if dialog.data.get('inum') else 'post-oauth-scopes' + cli_args = {'operation_id': operation_id, 'data': dialog.data} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + dialog.future.set_result(DialogResult.ACCEPT) + if response.status_code == 500: + self.app.show_message(_('Error'), response.text + '\n' + response.reason) + else: + self.oauth_get_scopes() + + asyncio.ensure_future(coroutine()) + + def search_scope(self, tbuffer:Buffer,) -> None: + if not len(tbuffer.text) > 2: + self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.oauth_containers['scopes']) + return + + self.oauth_get_scopes(pattern=tbuffer.text) + + def search_clients(self, tbuffer:Buffer,) -> None: + if not len(tbuffer.text) > 2: + self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.oauth_containers['clients']) + return + + self.oauth_update_clients(pattern=tbuffer.text) + + def add_scope(self) -> None: + """Method to display the dialog of clients + """ + dialog = EditScopeDialog(self.app, title=_("Add New Scope"), data={}, save_handler=self.save_scope) + result = self.app.show_jans_dialog(dialog) + + def add_client(self) -> None: + """Method to display the dialog of clients + """ + dialog = EditClientDialog(self.app, title=_("Add Client"), data={}, save_handler=self.save_client) + result = self.app.show_jans_dialog(dialog) + + def get_help(self, **kwargs: Any): + + self.app.logger.debug("get_help: "+str(kwargs['data'])) + self.app.logger.debug("get_help: "+str(kwargs['scheme'])) + schema = self.app.cli_object.get_schema_from_reference('', '#/components/schemas/{}'.format(str(kwargs['scheme']))) + + self.app.logger.debug("schema: "+str(schema)) + if kwargs['scheme'] == 'AppConfiguration': + self.app.status_bar_text= self.app.get_help_from_schema(schema, kwargs['data'][0]) + elif kwargs['scheme'] == 'Client': + self.app.status_bar_text= "Client Name: "+kwargs['data'][1] + elif kwargs['scheme'] == 'Scope': + self.app.status_bar_text= kwargs['data'][1] + elif kwargs['scheme'] == 'Keys': + self.app.status_bar_text= kwargs['data'][1] + self.app.logger.debug("kwargs['data']: "+str(kwargs['data'])) + + + # self.app.status_bar_text= kwargs['data'][1] + + + def delete_scope(self, **kwargs: Any): + """This method for the deletion of the clients data + + Args: + selected (_type_): The selected Client + event (_type_): _description_ + + Returns: + str: The server response + """ + + dialog = self.app.get_confirm_dialog(_("Are you sure want to delete scope inum:")+"\n {} ?".format(kwargs ['selected'][3])) + async def coroutine(): + focused_before = self.app.layout.current_window + result = await self.app.show_dialog_as_float(dialog) + try: + self.app.layout.focus(focused_before) + except: + self.app.layout.focus(self.app.center_frame) + + if result.lower() == 'yes': + result = self.app.cli_object.process_command_by_id( + operation_id='delete-oauth-scopes-by-inum', + url_suffix='inum:{}'.format(kwargs['selected'][3]), + endpoint_args='', + data_fn='', + data={} + ) + self.oauth_get_scopes() + return result + + asyncio.ensure_future(coroutine()) + + def delete_UMAresource(self, **kwargs: Any): + dialog = self.app.get_confirm_dialog(_("Are you sure want to delete UMA resoucres with id:")+"\n {} ?".format(kwargs ['selected'][0])) + async def coroutine(): + focused_before = self.app.layout.current_window + result = await self.app.show_dialog_as_float(dialog) + try: + self.app.layout.focus(focused_before) + except: + self.app.layout.focus(self.EditClientDialog) + + if result.lower() == 'yes': + result = self.app.cli_object.process_command_by_id( + operation_id='delete-oauth-uma-resources-by-id', + url_suffix='id:{}'.format(kwargs['selected'][0]), + endpoint_args='', + data_fn=None, + data={} + ) + self.EditClientDialog.oauth_get_uma_resources() + + return result + + asyncio.ensure_future(coroutine()) + + def oauth_logging(self) -> None: + self.oauth_data_container['logging'] = HSplit([ + self.app.getTitledWidget( + _('Log Level'), + name='loggingLevel', + widget=DropDownWidget( + values=[('TRACE', 'TRACE'), ('DEBUG', 'DEBUG'), ('INFO', 'INFO'), ('WARN', 'WARN'), ('ERROR', 'ERROR'), ('FATAL', 'FATAL'), ('OFF', 'OFF')], + value=self.app_configuration.get('loggingLevel') + ), + jans_help=self.app.get_help_from_schema(self.schema, 'loggingLevel'), + ), + self.app.getTitledWidget( + _('Log Layout'), + name='loggingLayout', + widget=DropDownWidget( + values=[('text', 'text'), ('json', 'json')], + value=self.app_configuration.get('loggingLayout') + ), + jans_help=self.app.get_help_from_schema(self.schema, 'loggingLayout'), + ), + self.app.getTitledCheckBox( + _("Enable HTTP Logging"), + name='httpLoggingEnabled', + checked=self.app_configuration.get('httpLoggingEnabled'), + jans_help=self.app.get_help_from_schema(self.schema, 'httpLoggingEnabled'), + style='class:outh-client-checkbox' + ), + self.app.getTitledCheckBox( + _("Disable JDK Logger"), + name='disableJdkLogger', + checked=self.app_configuration.get('disableJdkLogger'), + jans_help=self.app.get_help_from_schema(self.schema, 'disableJdkLogger'), + style='class:outh-client-checkbox' + ), + self.app.getTitledCheckBox( + _("Enable Oauth Audit Logging"), + name='enabledOAuthAuditLogging', + checked=self.app_configuration.get('enabledOAuthAuditLogging'), + jans_help=self.app.get_help_from_schema(self.schema, 'enabledOAuthAuditLogging'), + style='class:outh-client-checkbox' + ), + Window(height=1), + HSplit([ + self.app.getButton(text=_("Save Logging"), name='oauth:logging:save', jans_help=_("Save Auth Server logging configuration"), handler=self.save_logging), + Window(width=100), + ]) + ], style='class:outh_containers_clients', width=D()) + + def save_logging(self) -> None: + mod_data = self.make_data_from_dialog({'logging':self.oauth_data_container['logging']}) + pathches = [] + for key_ in mod_data: + if self.app_configuration.get(key_) != mod_data[key_]: + pathches.append({'op':'replace', 'path': key_, 'value': mod_data[key_]}) + + if pathches: + response = self.app.cli_object.process_command_by_id( + operation_id='patch-properties', + url_suffix='', + endpoint_args='', + data_fn=None, + data=pathches + ) + self.schema = response + body = HSplit([Label(_("Jans authorization server application configuration logging properties were saved."))]) + + buttons = [Button(_("Ok"))] + dialog = JansGDialog(self.app, title=_("Confirmation"), body=body, buttons=buttons) + async def coroutine(): + focused_before = self.app.layout.current_window + result = await self.app.show_dialog_as_float(dialog) + try: + self.app.layout.focus(focused_before) + except: + self.app.layout.focus(self.app.center_frame) + + asyncio.ensure_future(coroutine()) diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/view_property.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/view_property.py new file mode 100644 index 00000000000..3f786edac9c --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/view_property.py @@ -0,0 +1,385 @@ +import json +from asyncio import Future +from typing import OrderedDict + +from prompt_toolkit.widgets import Button, TextArea +from prompt_toolkit.application.current import get_app +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.layout.dimension import AnyDimension + +from prompt_toolkit.widgets import ( + Button, + Label, + TextArea, + +) +from asyncio import ensure_future + +from prompt_toolkit.widgets import ( + Button, + Dialog, + VerticalLine, +) +from cli import config_cli +from prompt_toolkit.layout.containers import ( + ConditionalContainer, + Float, + HSplit, + VSplit, + VerticalAlign, + DynamicContainer, + FloatContainer, + Window, + AnyContainer +) +from prompt_toolkit.widgets import ( + Box, + Button, + Frame, + Label, + RadioList, + TextArea, + ) + +from utils.static import DialogResult +from wui_components.jans_dialog import JansDialog +from wui_components.jans_dialog_with_nav import JansDialogWithNav +from wui_components.jans_nav_bar import JansNavBar +from wui_components.jans_side_nav_bar import JansSideNavBar +from utils.utils import DialogUtils + +from wui_components.jans_cli_dialog import JansGDialog + +from wui_components.jans_drop_down import DropDownWidget + +from typing import Optional, Sequence, Union +from typing import TypeVar, Callable +from utils.multi_lang import _ +import cli_style + +class ViewProperty(JansGDialog, DialogUtils): + """The Main UMA-resources Dialog to view UMA Resource Details + """ + def __init__( + self, + parent, + data:tuple, + title: AnyFormattedText= "", + search_text: AnyFormattedText= "", + buttons: Optional[Sequence[Button]]= [], + get_properties: Callable= None, + search_properties: Callable= None, + )-> Dialog: + + super().__init__(parent, title, buttons) + self.property, self.value = data[0],data[1] + self.myparent= parent + self.get_properties = get_properties + self.search_properties= search_properties + self.search_text=search_text + self.value_content = HSplit([],width=D()) + self.tabs = {} + self.selected_tab = 'tab0' + self.schema = self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/AppConfiguration') + + self.prepare_properties() + self.create_window() + + def cancel(self) -> None: + self.future.set_result(DialogResult.CANCEL) + + def save(self) -> None: + data_dict = {} + list_data =[] + + if type(self.value) in [str,bool,int] : + for wid in self.value_content.children: + prop_type = self.get_item_data(wid) + data = prop_type['value'] + + elif (type(self.value)==list and type(self.value[0]) not in [dict,list]): + for wid in self.value_content.children: + prop_type = self.get_item_data(wid) + data = prop_type['value'].split('\n') + + elif type(self.value) == dict : + for wid in self.value_content.children: + for k in wid.children : + prop_type = self.get_item_data(k) + data_dict[prop_type['key']]=prop_type['value'] + data = data_dict + + elif type(self.value) == list and type(self.value[0]) == dict: + for tab in self.tabs: + data_dict = {} + for k in self.tabs[tab].children : + prop_type = self.get_item_data(k.children[0]) + data_dict[prop_type['key']]=prop_type['value'] + list_data.append(data_dict) + data = list_data + else : + self.myparent.logger.debug("self.value: "+str(self.value)) + self.myparent.logger.debug("type self.value: "+str(type(self.value))) + data = [] + + # ------------------------------------------------------------# + # --------------------- Patch to server ----------------------# + # ------------------------------------------------------------# + if data : + response = self.myparent.cli_object.process_command_by_id( + operation_id='patch-properties' , + url_suffix='', + endpoint_args='', + data_fn='', + data=[ {'op':'replace', 'path': self.property, 'value': data } ] + ) + else: + return + # ------------------------------------------------------------# + # -- get_properties or serach again to see Momentary change --# + # ------------------------------------------------------------# + if response: + if self.search_text: + tbuff = Buffer(name='', ) + tbuff.text=self.search_text + self.search_properties(tbuff) + else: + self.get_properties() + self.future.set_result(DialogResult.ACCEPT) + return True + + self.myparent.show_message(_("Error!"), _("An error ocurred while saving property:\n") + str(response.text)) + + def get_type(self,prop): + try : + if self.schema.get('properties', {})[prop]['type'] == 'string': + prop_type= 'TitledText' + + elif self.schema.get('properties', {})[prop]['type'] == 'integer': + prop_type= 'int-TitledText' + + elif self.schema.get('properties', {})[prop]['type'] == 'boolean': + prop_type= 'TitledCheckBox' + + elif self.schema.get('properties', {})[prop]['type'] == 'object': + prop_type= 'dict' + + elif self.schema.get('properties', {})[prop]['type'] == 'array': + if 'enum' in self.schema.get('properties', {})[prop]: + prop_type= 'checkboxlist' + else: + if type(self.value[0]) == dict: + prop_type= 'list-dict' + else: + prop_type= 'long-TitledText' + except: + prop_type = None + + return prop_type + + def get_listValues(self,prop): + try : + list_values= self.schema.get('properties', {})[prop]['enum'] + except: + list_values = [] + + return list_values + + def prepare_properties(self): + + prop_type = self.get_type(self.property) + + if prop_type == 'TitledText': + self.value_content= HSplit([self.myparent.getTitledText( + self.property, + name=self.property, + value=self.value, + style='class:outh-scope-text' + ), + ],width=D()) + + elif prop_type == 'int-TitledText': + self.value_content= HSplit([self.myparent.getTitledText( + self.property, + name=self.property, + value=self.value, + text_type='integer', + style='class:outh-scope-text' + ), + ],width=D()) + + elif prop_type == 'long-TitledText': + self.value_content= HSplit([self.myparent.getTitledText( + self.property, + name=self.property, + height=3, + value='\n'.join(self.value), + style='class:outh-scope-text' + ), + ],width=D()) + + elif prop_type == 'checkboxlist': + self.value_content= HSplit([ + self.myparent.getTitledCheckBoxList( + self.property, + name=self.property, + values=self.get_listValues(self.property), + style='class:outh-client-checkboxlist'), + ],width=D()) + + elif prop_type == 'list-dict': + tab_num = len(self.value) + tabs = [] + for i in range(tab_num) : + tabs.append(('tab{}'.format(i),'tab{}'.format(i))) + + + for tab in self.value: + tab_list=[] + for item in tab: + if type(tab[item]) == str: + tab_list.append(HSplit([self.myparent.getTitledText( + item , + name=item, + value=tab[item], + style='class:outh-scope-text' + ), + ],width=D())) + + if type(tab[item]) == int : + tab_list.append(HSplit([self.myparent.getTitledText( + item , + name=item, + value=tab[item], + text_type='integer', + style='class:outh-scope-text' + ), + ],width=D())) + + elif type(tab[item]) == list: + tab_list.append(HSplit([self.myparent.getTitledText( + item, + name=item, + height=3, + value='\n'.join(tab[item]), + style='class:outh-scope-text' + ), + ],width=D())) + + elif type(tab[item]) == bool: + tab_list.append(HSplit([ + self.myparent.getTitledCheckBox( + item, + name=item, + checked= tab[item], + style='class:outh-client-checkbox'), + ],width=D())) + + self.tabs['tab{}'.format(self.value.index(tab))] = HSplit(tab_list,width=D()) + + self.value_content=HSplit([ + self.myparent.getTitledRadioButton( + _("Tab Num"), + name='tabNum', + current_value=self.selected_tab, + values=tabs, + on_selection_changed=self.tab_selection_changed, + style='class:outh-scope-radiobutton'), + + DynamicContainer(lambda: self.tabs[self.selected_tab]), + + ],width=D()) + + elif prop_type == 'TitledCheckBox': + self.value_content= HSplit([ + self.myparent.getTitledCheckBox( + self.property, + name=self.property, + checked= self.value, + style='class:outh-client-checkbox'), + ],width=D()) + + elif prop_type == 'dict': + dict_list=[] + for item in self.value: + if type(self.value[item]) == str: + dict_list.append(HSplit([self.myparent.getTitledText( + item , + name=item, + value=self.value[item], + style='class:outh-scope-text' + ), + ],width=D())) + + elif type(self.value[item]) == int : + dict_list.append(HSplit([self.myparent.getTitledText( + item , + name=item, + value=self.value[item], + text_type='integer', + style='class:outh-scope-text' + ), + ],width=D())) + + elif type(self.value[item]) == list: + dict_list.append(HSplit([self.myparent.getTitledText( + item, + name=item, + height=3, + value='\n'.join(self.value[item]), + style='class:outh-scope-text' + ), + ],width=D())) + + elif type(self.value[item]) == bool: + dict_list.append(HSplit([ + self.myparent.getTitledCheckBox( + item, + name=item, + checked= self.value[item], + style='class:outh-client-checkbox'), + ],width=D())) + + else : + dict_list.append(HSplit([self.myparent.getTitledText( + item, + name=item, + value="No Items Here", + style='class:outh-scope-text', + read_only=True, + ), + ],width=D())) + self.value_content= HSplit(dict_list,width=D()) + + def create_window(self): + + self.dialog = Dialog(title=self.property, + body= + HSplit([ + self.value_content, + ], padding=1,width=100,style='class:outh-uma-tabs' + ), + buttons=[ + Button( + text=_("Cancel"), + handler=self.cancel, + ) , + Button( + text=_("Save"), + handler=self.save, + ) , ], + with_background=False, + ) + + def tab_selection_changed( + self, + cb: RadioList, + ) -> None: + self.selected_tab = cb.current_value + + def __pt_container__(self)-> Dialog: + return self.dialog + diff --git a/jans-cli-tui/cli_tui/plugins/010_oxauth/view_uma_dialog.py b/jans-cli-tui/cli_tui/plugins/010_oxauth/view_uma_dialog.py new file mode 100644 index 00000000000..99d801f76d6 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/010_oxauth/view_uma_dialog.py @@ -0,0 +1,216 @@ +import json +from asyncio import Future +from typing import OrderedDict + +from prompt_toolkit.widgets import Button, TextArea +from prompt_toolkit.application.current import get_app +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.containers import ( + VSplit, + DynamicContainer, +) +from prompt_toolkit.key_binding import KeyBindings + +from prompt_toolkit.widgets import ( + Button, + Label, + TextArea, + +) +from asyncio import ensure_future + +from prompt_toolkit.widgets import ( + Button, + Dialog, + VerticalLine, +) +from cli import config_cli +from prompt_toolkit.layout.containers import ( + ConditionalContainer, + Float, + HSplit, + VSplit, + VerticalAlign, + DynamicContainer, + FloatContainer, + Window, + AnyContainer +) +from prompt_toolkit.widgets import ( + Box, + Button, + Frame, + Label, + RadioList, + TextArea, + ) + +import cli_style +from utils.multi_lang import _ +from utils.utils import DialogUtils +from utils.static import DialogResult +from wui_components.jans_dialog_with_nav import JansDialogWithNav +from wui_components.jans_nav_bar import JansNavBar +from wui_components.jans_side_nav_bar import JansSideNavBar +from wui_components.jans_dialog import JansDialog +from wui_components.jans_cli_dialog import JansGDialog +from wui_components.jans_drop_down import DropDownWidget + +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.layout.dimension import AnyDimension +from typing import Optional, Sequence, Union +from typing import TypeVar, Callable + +class ViewUMADialog(JansGDialog, DialogUtils): + """The Main UMA-resources Dialog to view UMA Resource Details + """ + def __init__( + self, + parent, + data:list, + title: AnyFormattedText= "", + buttons: Optional[Sequence[Button]]= [], + deleted_uma: Callable= None, + )-> Dialog: + """init for `ViewUMADialog`, inherits from two diffrent classes `JansGDialog` and `DialogUtils` + + JansGDialog (dialog): This is the main dialog Class Widget for all Jans-cli-tui dialogs except custom dialogs like dialogs with navbar + DialogUtils (methods): Responsable for all `make data from dialog` and `check required fields` in the form for any Edit or Add New + + Args: + parent (widget): This is the parent widget for the dialog + title (str): The Main dialog title + data (list): selected line data + button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. + save_handler (method, optional): handler invoked when closing the dialog. Defaults to None. + """ + super().__init__(parent, title, buttons) + self.data = data + self.myparent= parent + self.deleted_uma = deleted_uma + self.UMA_containers = {} + self.UMA_prepare_containers() + + def delete() -> None: + selected = [data.get('id'),data.get('description', ''),data.get('scopes', [''])[0]] + self.deleted_uma(selected,self.future.set_result(DialogResult.CANCEL)) + + def cancel() -> None: + self.future.set_result(DialogResult.CANCEL) + + self.side_nav_bar = JansNavBar( + self, + entries=[('scope', 'scope'), ('expression', 'scope expression'), ], + selection_changed=self.oauth_nav_selection_changed, + select=0, + bgcolor=cli_style.outh_navbar_bgcolor ### it is not a style > only color + ) + + self.dialog = Dialog(title='UMA-resources', + + body= + HSplit([ + + self.myparent.getTitledText( + _("Resource id"), + name='id', + value=self.data.get('id',''), + read_only=True, + style='class:outh-uma-text', + ), + + self.myparent.getTitledText( + _("Display Name"), + name='name', + value=self.data.get('name',''), + read_only=True, + style='class:outh-uma-text'), + + self.myparent.getTitledText( + _("IconURL"), + name='iconUri', + value=self.data.get('iconUri',''), + read_only=True, + style='class:outh-uma-text'), + + + VSplit([ + Label(text=_("Scope Selection"),style='class:outh-uma-label',width=len(_("Scope Selection"))), ## TODO dont know what is that + + Box(self.side_nav_bar.nav_window, style='class:outh-uma-navbar', height=1), + + ]), + + DynamicContainer(lambda: self.oauth_main_area), + + self.myparent.getTitledText( + _("Associated Client"), + name='clients', + value=self.data.get('clients',''), + read_only=True, + style='class:outh-uma-text'), + + self.myparent.getTitledText( + _("Creation time"), + name='creationDate', + value=self.data.get('creationDate',''), + read_only=True, + style='class:outh-uma-text'), + + ], padding=1,width=100,style='class:outh-uma-tabs' + # key_bindings=self.get_uma_dialog_key_bindings() + ), + buttons=[ + Button( + text=_("Cancel"), + handler=cancel, + ) , + Button( + text=_("Delete"), + handler=delete, + ) + ], + with_background=False, + # width=140, + ) + + def UMA_prepare_containers(self) -> None: + """Prepare the containers for UMA Dialog + """ + self.oauth_main_area = self.UMA_containers['scope'] = HSplit([ + self.myparent.getTitledText( + _("Scopes"), + name='scopes', + value='\n'.join(self.data.get('scopes',[])), + read_only=True, + style='class:outh-uma-text', + height=3, + ) + ],width=D()) + + self.UMA_containers['expression'] = HSplit([ + self.myparent.getTitledText( + _("Expression"), + name='scopeExpression', + value='\n'.join(self.data.get('scopeExpression',[])), + read_only=True, + style='class:outh-uma-text', + height=3, + ), + ],width=D()) + + def oauth_nav_selection_changed( + self, + selection: str + ) -> None: + """This method for the selection change for tabs + + Args: + selection (str): the current selected tab + """ + if selection in self.UMA_containers: + self.oauth_main_area = self.UMA_containers[selection] + + def __pt_container__(self)-> Dialog: + return self.dialog + diff --git a/jans-cli-tui/cli_tui/plugins/020_fido/.enabled b/jans-cli-tui/cli_tui/plugins/020_fido/.enabled new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/020_fido/__init__.py b/jans-cli-tui/cli_tui/plugins/020_fido/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/020_fido/main.py b/jans-cli-tui/cli_tui/plugins/020_fido/main.py new file mode 100755 index 00000000000..a78504df3a6 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/020_fido/main.py @@ -0,0 +1,285 @@ +import os +import sys +import asyncio +import time + +from collections import OrderedDict +from functools import partial +from typing import Any + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.layout.containers import HSplit, DynamicContainer, VSplit, Window +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.widgets import Button, Label, Frame, Box, Dialog +from prompt_toolkit.application import Application + +from wui_components.jans_nav_bar import JansNavBar +from wui_components.jans_drop_down import DropDownWidget +from wui_components.jans_vetrical_nav import JansVerticalNav +from wui_components.jans_cli_dialog import JansGDialog + + +from utils.multi_lang import _ +from utils.utils import DialogUtils + +import cli_style + +class Plugin(DialogUtils): + """This is a general class for plugins + """ + def __init__( + self, + app: Application + ) -> None: + """init for Plugin class "fido" + + Args: + app (_type_): _description_ + """ + self.app = app + self.pid = 'fido' + self.name = '[F]IDO' + self.page_entered = False + self.data = {} + self.prepare_navbar() + self.prepare_containers() + + def process(self) -> None: + pass + + def init_plugin(self) -> None: + + self.app.create_background_task(self.get_fido_configuration()) + + + def edit_requested_party(self, **kwargs: Any) -> None: + title = _("Enter Request Party Properties") + schema = self.app.cli_object.get_schema_from_reference('Fido2', '#/components/schemas/RequestedParty') + cur_data = kwargs.get('passed', ['', '']) + name_widget = self.app.getTitledText(_("Name"), name='name', value=cur_data[0], jans_help=self.app.get_help_from_schema(self.schema, 'name'), style='class:outh-scope-text') + domains_widget = self.app.getTitledText(_("Domains"), name='domains', value='\n'.join(cur_data[1].split(', ')), height=3, jans_help=self.app.get_help_from_schema(self.schema, 'domains'), style='class:dialog-titled-widget') + + def add_request_party(dialog: Dialog) -> None: + name_ = name_widget.me.text + domains_ = domains_widget.me.text + new_data = [name_, ', '.join(domains_.splitlines())] + + if not kwargs.get('data'): + self.requested_parties_container.add_item(new_data) + else: + self.requested_parties_container.replace_item(kwargs['selected'], new_data) + + body = HSplit([name_widget, domains_widget]) + buttons = [Button(_("Cancel")), Button(_("OK"), handler=add_request_party)] + dialog = JansGDialog(self.app, title=title, body=body, buttons=buttons, width=self.app.dialog_width-20) + self.app.show_jans_dialog(dialog) + + + def delete_requested_party(self, **kwargs: Any) -> None: + self.requested_parties_container.remove_item(kwargs['selected']) + + def create_widgets(self): + self.schema = self.app.cli_object.get_schema_from_reference('Fido2', '#/components/schemas/AppConfiguration') + + self.tabs['configuration'] = HSplit([ + self.app.getTitledText(_("Issuer"), name='issuer', value=self.data.get('issuer',''), jans_help=self.app.get_help_from_schema(self.schema, 'issuer'), style='class:outh-scope-text'), + self.app.getTitledText(_("Base Endpoint"), name='baseEndpoint', value=self.data.get('baseEndpoint',''), jans_help=self.app.get_help_from_schema(self.schema, 'baseEndpoint'), style='class:outh-scope-text'), + self.app.getTitledText(_("Clean Service Interval"), name='cleanServiceInterval', value=self.data.get('cleanServiceInterval',''), jans_help=self.app.get_help_from_schema(self.schema, 'cleanServiceInterval'), style='class:outh-scope-text', text_type='integer'), + self.app.getTitledText(_("Clean Service Batch ChunkSize"), name='cleanServiceBatchChunkSize', value=self.data.get('cleanServiceBatchChunkSize',''), jans_help=self.app.get_help_from_schema(self.schema, 'cleanServiceBatchChunkSize'), style='class:outh-scope-text', text_type='integer'), + self.app.getTitledCheckBox(_("Use Local Cache"), name='useLocalCache', checked=self.data.get('useLocalCache'), jans_help=self.app.get_help_from_schema(self.schema, 'useLocalCache'), style='class:outh-client-checkbox'), + self.app.getTitledCheckBox(_("Disable Jdk Logger"), name='disableJdkLogger', checked=self.data.get('disableJdkLogger'), jans_help=self.app.get_help_from_schema(self.schema, 'disableJdkLogger'), style='class:outh-client-checkbox'), + self.app.getTitledWidget( + _("Logging Level"), + name='loggingLevel', + widget=DropDownWidget( + values=[('TRACE', 'TRACE'), ('DEBUG', 'DEBUG'), ('INFO', 'INFO'), ('WARN', 'WARN'),('ERROR', 'ERROR'),('FATAL', 'FATAL'),('OFF', 'OFF')], + value=self.data.get('loggingLevel') + ), + jans_help=self.app.get_help_from_schema(self.schema, 'loggingLevel'), + style='class:outh-client-dropdown' + ), + self.app.getTitledText(_("Logging Layout"), name='loggingLayout', value=self.data.get('loggingLayout',''), jans_help=self.app.get_help_from_schema(self.schema, 'loggingLayout'), style='class:outh-scope-text'), + self.app.getTitledText(_("External Logger Configuration"), name='externalLoggerConfiguration', value=self.data.get('externalLoggerConfiguration',''), jans_help=self.app.get_help_from_schema(self.schema, 'externalLoggerConfiguration'), style='class:outh-scope-text'), + self.app.getTitledText(_("Metric Reporter Interval"), name='metricReporterInterval', value=self.data.get('metricReporterInterval',''), jans_help=self.app.get_help_from_schema(self.schema, 'metricReporterInterval'), style='class:outh-scope-text', text_type='integer'), + self.app.getTitledText(_("Metric Reporter Keep Data Days"), name='metricReporterKeepDataDays', value=self.data.get('metricReporterKeepDataDays',''), jans_help=self.app.get_help_from_schema(self.schema, 'metricReporterKeepDataDays'), style='class:outh-scope-text', text_type='integer'), + self.app.getTitledCheckBox(_("Metric Reporter Enabled"), name='metricReporterEnabled', checked=self.data.get('metricReporterEnabled'), jans_help=self.app.get_help_from_schema(self.schema, 'metricReporterEnabled'), style='class:outh-client-checkbox'), + self.app.getTitledText( + _("Person Custom Object Classes"), + name='personCustomObjectClassList', + value='\n'.join(self.data.get('personCustomObjectClassList', [])), + height=3, + jans_help=self.app.get_help_from_schema(self.schema, 'personCustomObjectClassList'), + style='class:outh-scope-text' + ), + Window(height=1), + VSplit([Window(), Button(_("Save"), handler=self.save_config), Window()]), + ], + width=D() + ) + + + static_schema = self.app.cli_object.get_schema_from_reference('Fido2', '#/components/schemas/Fido2Configuration') + static_schema = {} + + fido2_static_config = self.data.get('fido2Configuration', {}) + + requested_parties_title = _("Requested Parties") + add_party_title = _("Add Party") + + requested_parties_data = [] + for rp in fido2_static_config.get('requestedParties', {}): + requested_parties_data.append([rp.get('name',''), ', '.join(rp.get('domains', []))]) + + self.requested_parties_container = JansVerticalNav( + myparent=self.app, + headers=['Name', 'Domains'], + preferred_size=[30, 30], + data=requested_parties_data, + on_enter=self.edit_requested_party, + on_delete=self.delete_requested_party, + on_display=self.app.data_display_dialog, + selectes=0, + headerColor='class:outh-client-navbar-headcolor', + entriesColor='class:outh-client-navbar-entriescolor', + all_data=requested_parties_data, + underline_headings=False, + max_width=65, + jans_name='RequestedParties', + max_height=False + ) + + self.tabs['static'] = HSplit([ + self.app.getTitledText(_("Authenticator Certificates Folder"), name='authenticatorCertsFolder', value=fido2_static_config.get('authenticatorCertsFolder',''), jans_help=self.app.get_help_from_schema(static_schema, 'authenticatorCertsFolder'), style='class:outh-scope-text'), + self.app.getTitledText(_("MDS Access Token"), name='mdsAccessToken', value=fido2_static_config.get('mdsAccessToken',''), jans_help=self.app.get_help_from_schema(static_schema, 'mdsAccessToken'), style='class:outh-scope-text'), + self.app.getTitledText(_("MDS TOC Certificates Folder"), name='mdsCertsFolder', value=fido2_static_config.get('mdsCertsFolder',''), jans_help=self.app.get_help_from_schema(static_schema, 'mdsCertsFolder'), style='class:outh-scope-text'), + self.app.getTitledText(_("MDS TOC Files Folder"), name='mdsTocsFolder', value=fido2_static_config.get('mdsTocsFolder',''), jans_help=self.app.get_help_from_schema(static_schema, 'mdsTocsFolder'), style='class:outh-scope-text'), + self.app.getTitledCheckBox(_("Check U2f Attestations"), name='checkU2fAttestations', checked=fido2_static_config.get('checkU2fAttestations'), jans_help=self.app.get_help_from_schema(static_schema, 'checkU2fAttestations'), style='class:outh-client-checkbox'), + self.app.getTitledCheckBox(_("Check U2f Attestations"), name='checkU2fAttestations', checked=fido2_static_config.get('checkU2fAttestations'), jans_help=self.app.get_help_from_schema(static_schema, 'checkU2fAttestations'), style='class:outh-client-checkbox'), + self.app.getTitledText(_("Unfinished Request Expiration"), name='unfinishedRequestExpiration', value=fido2_static_config.get('unfinishedRequestExpiration',''), jans_help=self.app.get_help_from_schema(static_schema, 'unfinishedRequestExpiration'), style='class:outh-scope-text', text_type='integer'), + self.app.getTitledText(_("Authentication History Expiration"), name='authenticationHistoryExpiration', value=fido2_static_config.get('authenticationHistoryExpiration',''), jans_help=self.app.get_help_from_schema(static_schema, 'authenticationHistoryExpiration'), style='class:outh-scope-text', text_type='integer'), + self.app.getTitledText(_("Server Metadata Folder"), name='serverMetadataFolder', value=fido2_static_config.get('serverMetadataFolder',''), jans_help=self.app.get_help_from_schema(static_schema, 'serverMetadataFolder'), style='class:outh-scope-text'), + + self.app.getTitledCheckBox(_("User Auto Enrollment"), name='userAutoEnrollment', checked=fido2_static_config.get('userAutoEnrollment'), jans_help=self.app.get_help_from_schema(static_schema, 'userAutoEnrollment'), style='class:outh-client-checkbox'), + self.app.getTitledText( + _("Requested Credential Types"), + name='requestedCredentialTypes', + value='\n'.join(fido2_static_config.get('requestedCredentialTypes', [])), + height=3, + jans_help=self.app.get_help_from_schema(static_schema, 'requestedCredentialTypes'), + style='class:outh-scope-text' + ), + + VSplit([ + Label(text=requested_parties_title, style='class:script-label', width=len(requested_parties_title)+1), + self.requested_parties_container, + Window(width=2), + HSplit([ + Window(height=1), + Button(text=add_party_title, width=len(add_party_title)+4, handler=partial(self.edit_requested_party, jans_name='editRequestedPary')), + ]), + ], + height=5, width=D(), + ), + + VSplit([Window(), Button(_("Save"), handler=self.save_config), Window()]), + ], + width=D() + ) + + self.nav_selection_changed(list(self.tabs)[0]) + + + async def get_fido_configuration(self) -> None: + 'Coroutine for getting fido2 configuration.' + try: + response = self.app.cli_object.process_command_by_id( + operation_id='get-properties-fido2', + url_suffix='', + endpoint_args='', + data_fn=None, + data={} + ) + + except Exception as e: + self.app.show_message(_("Error getting Fido2 configuration"), str(e)) + return + + if response.status_code not in (200, 201): + self.app.show_message(_("Error getting Fido2 configuration"), str(response.text)) + return + + self.data = response.json() + self.create_widgets() + + def prepare_navbar(self) -> None: + """prepare the navbar for the current Plugin + """ + self.nav_bar = JansNavBar( + self.app, + entries=[('configuration', '[D]ynamic Configuration'), ('static', 'S[t]atic Configuration')], + selection_changed=self.nav_selection_changed, + select=0, + jans_name='fido:nav_bar' + ) + + def prepare_containers(self) -> None: + """prepare the main container (tabs) for the current Plugin + """ + + self.tabs = OrderedDict() + self.main_area = HSplit([Label("configuration")],width=D()) + + self.main_container = HSplit([ + Box(self.nav_bar.nav_window, style='class:sub-navbar', height=1), + DynamicContainer(lambda: self.main_area), + ], + height=D(), + style='class:outh_maincontainer' + ) + + def nav_selection_changed( + self, + selection: str + ) -> None: + + """This method for the selection change + + Args: + selection (str): the current selected tab + """ + + if selection in self.tabs: + self.main_area = self.tabs[selection] + else: + self.main_area = self.app.not_implemented + + + def save_config(self) -> None: + + fido2_config = self.make_data_from_dialog(tabs={'configuration': self.tabs['configuration']}) + fido2_static = self.make_data_from_dialog(tabs={'static': self.tabs['static']}) + + fido2_config['personCustomObjectClassList'] = fido2_config['personCustomObjectClassList'].splitlines() + fido2_static['requestedCredentialTypes'] = fido2_static['requestedCredentialTypes'].splitlines() + + fido2_static['requestedParties'] = [] + for name, domains in self.requested_parties_container.data: + fido2_static['requestedParties'].append({'name': name, 'domains': domains.splitlines()}) + + fido2_config['fido2Configuration'] = fido2_static + + async def coroutine(): + cli_args = {'operation_id': 'put-properties-fido2', 'data': fido2_config} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + + asyncio.ensure_future(coroutine()) + + + def set_center_frame(self) -> None: + """center frame content + """ + self.app.center_container = self.main_container + + diff --git a/jans-cli-tui/cli_tui/plugins/030_scim/.enabled b/jans-cli-tui/cli_tui/plugins/030_scim/.enabled new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/030_scim/__init__.py b/jans-cli-tui/cli_tui/plugins/030_scim/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/030_scim/main.py b/jans-cli-tui/cli_tui/plugins/030_scim/main.py new file mode 100755 index 00000000000..650d8bcc096 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/030_scim/main.py @@ -0,0 +1,134 @@ +import os +import sys +import asyncio + +from typing import Sequence + + +from prompt_toolkit.application import Application +from prompt_toolkit.layout.containers import HSplit, VSplit, Window +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.widgets import Button, Label, Frame +from prompt_toolkit.formatted_text import HTML +from wui_components.jans_drop_down import DropDownWidget + +from utils.utils import DialogUtils +from utils.multi_lang import _ + + +class Plugin(DialogUtils): + """This is a general class for plugins + """ + def __init__( + self, + app: Application + ) -> None: + """init for Plugin class "scim" + + Args: + app (_type_): _description_ + """ + self.app = app + self.pid = 'scim' + self.name = '[S]CIM' + self.app_config = {} + self.widgets_ready = False + self.container = Frame( + body=HSplit([Button(text=_("Get Scim Configuration"), handler=self.get_app_config)], width=D()), + height=D()) + + + def process(self) -> None: + pass + + def set_center_frame(self) -> None: + """center frame content + """ + self.app.center_container = self.container + + + def create_widgets(self) -> None: + """SCIM Application configuration widgets are created in this fonction + """ + self.save_button = Button(_("Save"), handler=self.save_app_config) + schema = self.app.cli_object.get_schema_from_reference('SCIM', '#/components/schemas/AppConfiguration') + self.container = HSplit([ + self.app.getTitledText(_("Base DN"), name='baseDN', value=self.app_config.get('baseDN',''), jans_help=self.app.get_help_from_schema(schema, 'baseDN'), read_only=True, style='class:outh-scope-text'), + self.app.getTitledText(_("Application Url"), name='applicationUrl', value=self.app_config.get('applicationUrl',''), jans_help=self.app.get_help_from_schema(schema, 'applicationUrl'), style='class:outh-scope-text'), + self.app.getTitledText(_("Base Endpoint"), name='baseEndpoint', value=self.app_config.get('baseEndpoint',''), jans_help=self.app.get_help_from_schema(schema, 'baseEndpoint'), style='class:outh-scope-text'), + self.app.getTitledText(_("Person Custom Object Class"), name='personCustomObjectClass', value=self.app_config.get('personCustomObjectClass',''), jans_help=self.app.get_help_from_schema(schema, 'personCustomObjectClass'), style='class:outh-scope-text'), + self.app.getTitledText(_("Person Custom Object Class"), name='oxAuthIssuer', value=self.app_config.get('oxAuthIssuer',''), jans_help=self.app.get_help_from_schema(schema, 'oxAuthIssuer'), style='class:outh-scope-text'), + self.app.getTitledRadioButton(_("Protection Mode"), name='protectionMode', values=[('OAUTH', 'OAUTH'),('BYPASS', 'BYPASS')], current_value=self.app_config.get('protectionMode'), jans_help=self.app.get_help_from_schema(schema, 'protectionMode'), style='class:outh-client-radiobutton'), + self.app.getTitledText(_("Max Count"), name='maxCount', value=self.app_config.get('maxCount',''), jans_help=self.app.get_help_from_schema(schema, 'maxCount'), text_type='integer', style='class:outh-scope-text'), + self.app.getTitledText(_("Bulk Max Operations"), name='bulkMaxOperations', value=self.app_config.get('bulkMaxOperations',''), jans_help=self.app.get_help_from_schema(schema, 'bulkMaxOperations'), text_type='integer', style='class:outh-scope-text'), + self.app.getTitledText(_("Bulk Max Payload Size"), name='bulkMaxPayloadSize', value=self.app_config.get('bulkMaxPayloadSize',''), jans_help=self.app.get_help_from_schema(schema, 'bulkMaxPayloadSize'), text_type='integer', style='class:outh-scope-text'), + self.app.getTitledText(_("User Extension Schema URI"), name='userExtensionSchemaURI', value=self.app_config.get('userExtensionSchemaURI',''), jans_help=self.app.get_help_from_schema(schema, 'userExtensionSchemaURI'), style='class:outh-scope-text'), + self.app.getTitledWidget( + _("Logging Level"), + name='loggingLevel', + widget=DropDownWidget( + values=[('TRACE', 'TRACE'), ('DEBUG', 'DEBUG'), ('INFO', 'INFO'), ('WARN', 'WARN'),('ERROR', 'ERROR'),('FATAL', 'FATAL'),('OFF', 'OFF')], + value=self.app_config.get('loggingLevel') + ), + jans_help=self.app.get_help_from_schema(schema, 'loggingLevel'), + style='class:outh-client-dropdown' + ), + self.app.getTitledText(_("Logging Layout"), name='loggingLayout', value=self.app_config.get('loggingLayout',''), jans_help=self.app.get_help_from_schema(schema, 'loggingLayout'), style='class:outh-scope-text'), + self.app.getTitledText(_("External Logger Configuration"), name='externalLoggerConfiguration', value=self.app_config.get('externalLoggerConfiguration',''), jans_help=self.app.get_help_from_schema(schema, 'externalLoggerConfiguration'), style='class:outh-scope-text'), + self.app.getTitledText(_("Metric Reporter Interval"), name='metricReporterInterval', value=self.app_config.get('metricReporterInterval',''), jans_help=self.app.get_help_from_schema(schema, 'metricReporterInterval'), style='class:outh-scope-text', text_type='integer'), + self.app.getTitledText(_("Metric Reporter Keep Data Days"), name='metricReporterKeepDataDays', value=self.app_config.get('metricReporterKeepDataDays',''), jans_help=self.app.get_help_from_schema(schema, 'metricReporterKeepDataDays'), style='class:outh-scope-text', text_type='integer'), + self.app.getTitledCheckBox(_("Metric Reporter Enabled"), name='metricReporterEnabled', checked=self.app_config.get('metricReporterEnabled'), jans_help=self.app.get_help_from_schema(schema, 'metricReporterEnabled'), style='class:outh-client-checkbox'), + self.app.getTitledCheckBox(_("Disable Jdk Logger"), name='disableJdkLogger', checked=self.app_config.get('disableJdkLogger'), jans_help=self.app.get_help_from_schema(schema, 'disableJdkLogger'), style='class:outh-client-checkbox'), + self.app.getTitledCheckBox(_("Use Local Cache"), name='useLocalCache', checked=self.app_config.get('useLocalCache'), jans_help=self.app.get_help_from_schema(schema, 'useLocalCache'), style='class:outh-client-checkbox'), + VSplit([Window(), self.save_button, Window()]) + ], + width=D() + ) + + self.app.center_container = self.container + + + def get_app_config(self) -> None: + """Gets SCIM application configurations from server. + """ + + async def coroutine(): + cli_args = {'operation_id': 'get-scim-config'} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.app_config = response.json() + self.create_widgets() + self.app.invalidate() + self.app.layout.focus(self.app.center_container) + + asyncio.ensure_future(coroutine()) + + + def save_app_config(self) -> None: + """Save button handler for saving SCIM application configurations. + Once configuration data was obtained from form, patch operations are prepared and saved to server. + """ + data = self.make_data_from_dialog({'scim': self.container}) + self.app.logger.debug("SCIM APP CONFIG {}".format(data)) + patche_list = [] + for key in self.app_config: + if self.app_config[key] != data[key]: + patche_list.append({'op':'replace', 'path': key, 'value': data[key]}) + for key in data: + if data[key] and key not in self.app_config: + patche_list.append({'op':'add', 'path': key, 'value': data[key]}) + + + if not patche_list: + self.app.show_message(_("Warning"), _("No changes was done on Scim appilication configuration. Nothing to save.")) + return + + async def coroutine(): + cli_args = {'operation_id': 'patch-scim-config', 'data': patche_list} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + + asyncio.ensure_future(coroutine()) + diff --git a/jans-cli-tui/cli_tui/plugins/040_config_api/.enabled b/jans-cli-tui/cli_tui/plugins/040_config_api/.enabled new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/040_config_api/__init__.py b/jans-cli-tui/cli_tui/plugins/040_config_api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/040_config_api/main.py b/jans-cli-tui/cli_tui/plugins/040_config_api/main.py new file mode 100755 index 00000000000..c493a292745 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/040_config_api/main.py @@ -0,0 +1,915 @@ +import os +import sys +from prompt_toolkit.application import Application +import threading +import prompt_toolkit + +from prompt_toolkit.layout.containers import ( + HSplit, + VSplit, + HorizontalAlign, + DynamicContainer, + Window, +) +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import Buffer + +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.widgets import Button, Label, Frame +from wui_components.jans_nav_bar import JansNavBar +from prompt_toolkit.layout.containers import HSplit, DynamicContainer, VSplit, Window +from prompt_toolkit.widgets import Button, Label, Frame, Box, Dialog +from wui_components.jans_cli_dialog import JansGDialog +from collections import OrderedDict +from functools import partial +from typing import Any +from wui_components.jans_vetrical_nav import JansVerticalNav +from utils.multi_lang import _ +from typing import Any, Optional +from utils.utils import DialogUtils +from utils.static import DialogResult +import asyncio +from prompt_toolkit.widgets.base import RadioList + +class Plugin(): + """This is a general class for plugins + """ + def __init__( + self, + app: Application + ) -> None: + """init for Plugin class "config_api" + + Args: + app (_type_): _description_ + """ + self.app = app + self.pid = 'config_api' + self.name = '[C]onfig-API' + self.page_entered = False + self.role_type = 'api-viewer' + self.admin_ui_roles_data = {} + + self.prepare_navbar() + self.prepare_containers() + + def process(self) -> None: + pass + + def prepare_navbar(self) -> None: + """prepare the navbar for the current Plugin + """ + self.nav_bar = JansNavBar( + self.app, + entries=[('accessroles', 'Access r[o]les'), ('permissions', '[P]ermissions'), ('mapping', '[M]apping')], + selection_changed=self.nav_selection_changed, + select=0, + jans_name='fido:nav_bar' + ) + + def prepare_containers(self) -> None: + """prepare the main container (tabs) for the current Plugin + """ + + self.containers = OrderedDict() + self.main_area = HSplit([Label("configuration")],width=D()) + + self.main_container = HSplit([ + Box(self.nav_bar.nav_window, style='class:sub-navbar', height=1), + DynamicContainer(lambda: self.main_area), + ], + height=D(), + style='class:outh_maincontainer' + ) + self.create_widgets() + + def create_widgets(self): + + self.config_data_container = { + 'accessroles': HSplit([],width=D()), + 'permissions': HSplit([],width=D()), + 'mapping': HSplit([],width=D()), + } + + self.containers['accessroles'] = HSplit([ + VSplit([ + self.app.getButton( + text=_("Get adminui roles"), + name='oauth:clients:get', + jans_help=_("Get all admin ui roles"), + handler=self.get_adminui_roles), + + self.app.getButton( + text=_("Add adminui roles"), + name='oauth:scopes:add', + jans_help=_("Add admin ui role"), + handler=self.add_adminui_roles), + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.config_data_container['accessroles']) + ],style='class:outh_containers_clients') + + self.containers['permissions'] = HSplit([ + VSplit([ + self.app.getButton( + text=_("Get adminui permissions"), + name='oauth:clients:get', + jans_help=_("Get all admin ui permissions"), + handler=self.get_adminui_permissions), + + self.app.getTitledText( + _("Search: "), + name='oauth:scopes:search', + jans_help=_("Press enter to perform search"), + accept_handler=self.search_adminui_permissions, + style='class:outh_containers_scopes.text'), + + self.app.getButton( + text=_("Add adminui permission"), + name='oauth:scopes:add', + jans_help=_("Add admin ui role"), + handler=self.add_adminui_permissions), + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.config_data_container['permissions']) + ],style='class:outh_containers_clients') + + self.containers['mapping'] = HSplit([ + VSplit([ + self.app.getButton( + text=_("Get adminui mapping"), + name='oauth:clients:get', + jans_help=_("Get all admin ui mapping"), + handler=self.get_adminui_mapping), + + self.app.getTitledText( + _("Search: "), + name='oauth:scopes:search', + jans_help=_("Press enter to perform search"), + accept_handler=self.search_adminui_mapping, + style='class:outh_containers_scopes.text'), + + # self.app.getButton( + # text=_("Add adminui mapping"), + # name='oauth:scopes:add', + # jans_help=_("Add admin ui mapping"), + # handler=self.add_adminui_mapping), + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.config_data_container['mapping']) + ],style='class:outh_containers_clients') + + self.nav_selection_changed(list(self.containers)[0]) + + #--------------------------------------------------------------------------------# + #----------------------------------- accessroles --------------------------------# + #--------------------------------------------------------------------------------# + + def get_adminui_roles(self) -> None: + """Method to get the admin ui roles from server + """ + cli_args = {'operation_id': 'get-all-adminui-roles'} + + async def coroutine(): + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + data = response.json() + if response.status_code not in (200, 201): + self.app.show_message(_("Error Getting Admin UI Roles!"), str(data), tobefocused=self.app.center_container) + return + + self.admin_ui_roles_data = data + self.adminui_update_roles() + self.app.layout.focus(self.app.center_container) + + asyncio.ensure_future(coroutine()) + + + def adminui_update_roles(self, + ) -> None: + """update the current clients data to server + + Args: + pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + """ + + data =[] + + for d in self.admin_ui_roles_data: + data.append( + [ + d.get('role'), + d.get('description'), + ] + ) + + # ------------------------------------------------------------------------------- # + # --------------------------------- View Data ----------------------------------- # + # ------------------------------------------------------------------------------- # + + if data: + clients = JansVerticalNav( + myparent=self.app, + headers=['Role', 'Description',], + preferred_size= [0,0], + data=data, + on_enter=self.edit_adminui_roles, + on_display=self.app.data_display_dialog, + on_delete= self.delete_adminui_roles, + # get_help=(self.get_help,'AdminRole'), + selectes=0, + headerColor='class:outh-verticalnav-headcolor', + entriesColor='class:outh-verticalnav-entriescolor', + all_data=self.admin_ui_roles_data + ) + self.app.layout.focus(clients) # clients.focuse..!? TODO >> DONE + self.config_data_container['accessroles'] = HSplit([ + clients, + ]) + get_app().invalidate() + self.app.layout.focus(clients) ### it fix focuse on the last item deletion >> try on UMA-res >> edit_client_dialog >> oauth_update_uma_resources + else: + self.app.show_message(_("Oops"), _("No matching result"), tobefocused=self.app.center_container) + + def add_adminui_roles(self) -> None: + """Method to display the dialog of clients + """ + """Method to display the dialog of clients + """ + + self.adminui_role = self.app.getTitledText( + _("Role"), + name='role', + height=3, + style='class:dialog-titled-widget') + + self.adminui_role_description = self.app.getTitledText( + _("Description"), + name='Description', + height=3, + style='class:dialog-titled-widget') + + def save(dialog: Dialog) -> None: + role = self.adminui_role.me.text + desc = self.adminui_role_description.me.text + + # ------------------------------------------------------------# + # --------------------- Patch to server ----------------------# + # ------------------------------------------------------------# + if desc : + response = self.app.cli_object.process_command_by_id( + operation_id='add-adminui-role' , + url_suffix='', + endpoint_args='', + data_fn='', + data={'role': '{}'.format(role), 'description': '{}'.format(desc)}, + ) + else: + return + # ------------------------------------------------------------# + # -- get_properties or serach again to see Momentary change --# + # ------------------------------------------------------------# + if response: + self.get_adminui_roles() + # self.future.set_result(DialogResult.ACCEPT) + return True + + self.app.show_message(_("Error!"), _("An error ocurred while Addin role adminui:\n") + str(response.text)) + + + body = HSplit([self.adminui_role,self.adminui_role_description]) + buttons = [Button(_("Cancel")), Button(_("OK"), handler=save)] + dialog = JansGDialog(self.app, title=_('Add New Role'), body=body, buttons=buttons, width=self.app.dialog_width-20) + self.app.show_jans_dialog(dialog) + + + def edit_adminui_roles(self, **params: Any) -> None: + """Method to display the dialog of clients + """ + + role_data = params.get('data', {}) + title = role_data.get('role','') + + self.adminui_role_description = self.app.getTitledText( + _("Domains"), + name='domains', + value=role_data.get('description',''), + height=3, + style='class:dialog-titled-widget') + + self.adminui_role_deletable = self.app.getTitledCheckBox( + "Deletable", + name='deletable', + checked= False, + jans_help= "Default to False", + style='class:outh-client-checkbox') + + def save(dialog: Dialog) -> None: + desc = self.adminui_role_description.me.text + deletable = self.adminui_role_deletable.me.checked + + # ------------------------------------------------------------# + # --------------------- Patch to server ----------------------# + # ------------------------------------------------------------# + if desc : + response = self.app.cli_object.process_command_by_id( + operation_id='edit-adminui-role' , + url_suffix='', + endpoint_args='', + data_fn='', + data={'role': '{}'.format(title), 'description': '{}'.format(desc), 'deletable':'{}'.format(deletable)}, + ) + else: + return + # ------------------------------------------------------------# + # -- get_properties or serach again to see Momentary change --# + # ------------------------------------------------------------# + if response: + self.get_adminui_roles() + # self.future.set_result(DialogResult.ACCEPT) + return True + + self.app.show_message(_("Error!"), _("An error ocurred while saving role adminui:\n") + str(response.text)) + + + body = HSplit([self.adminui_role_description,self.adminui_role_deletable]) + buttons = [Button(_("Cancel")), Button(_("OK"), handler=save)] + dialog = JansGDialog(self.app, title=title, body=body, buttons=buttons, width=self.app.dialog_width-20) + self.app.show_jans_dialog(dialog) + + def delete_adminui_roles(self, **kwargs: Any) -> None: + + dialog = self.app.get_confirm_dialog(_("Are you sure want to delete adminui_roles :")+"\n {} ?".format(kwargs['selected'][0])) + + async def coroutine(): ## Need to add editable + focused_before = self.app.layout.current_window + result = await self.app.show_dialog_as_float(dialog) + try: + self.app.layout.focus(focused_before) + except: + self.app.layout.focus(self.app.center_frame) + + if result.lower() == 'yes': ## should we delete the main roles?! + cli_args = {'operation_id': 'delete-adminui-role', 'url_suffix':'adminUIRole:{}'.format(kwargs ['selected'][0])} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + if response: + self.app.show_message(_("Error!"), str(response), tobefocused=self.app.center_container) + else: + self.get_adminui_roles() + + asyncio.ensure_future(coroutine()) + + #--------------------------------------------------------------------------------# + #------------------------------------- permissions ------------------------------# + #--------------------------------------------------------------------------------# + + def get_adminui_permissions(self) -> None: + """Method to get the adminui_permissions data from server + """ + self.config_data_container['permissions'] = HSplit([Label(_("Please wait while getting adminui_permissions"),style='class:outh-waitclientdata.label')], width=D(),style='class:outh-waitclientdata') + t = threading.Thread(target=self.adminui_update_permissions, daemon=True) + self.app.start_progressing() + t.start() + + def adminui_update_permissions( + self, + start_index: Optional[int]= 0, + pattern: Optional[str]= '', + ) -> None: + """update the current adminui_permissions data to server + + Args: + pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + """ + + def get_next( + start_index: int, + pattern: Optional[str]= '', + ) -> None: + self.adminui_update_permissions(start_index, pattern='') + + endpoint_args ='limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) + + try : + rsponse = self.app.cli_object.process_command_by_id( + operation_id='get-all-adminui-permissions', + url_suffix='', + endpoint_args='', + data_fn=None, + data={} + ) + + except Exception as e: + self.app.stop_progressing() + self.app.show_message(_("Error getting adminui_permissions"), str(e)) + return + + self.app.stop_progressing() + + if rsponse.status_code not in (200, 201): + self.app.show_message(_("Error getting adminui_permissions"), str(rsponse.text)) + return + + try: + result = rsponse.json() + except Exception: + self.app.show_message(_("Error getting adminui_permissions"), str(rsponse.text)) + return + + data =[] + if pattern: + for k in result: + if pattern.lower() in k.get('permission').lower(): + data.append( + [ + k.get('permission'), + k.get('defaultPermissionInToken'), + ] + ) + else: + for d in result: + data.append( + [ + d.get('permission'), + d.get('defaultPermissionInToken'), + ] + ) + + if data: + buttons = [] + if int(len(data)/ 20) >=1 : + + if start_index< int(len(data)/ 20) : + handler_partial = partial(get_next, start_index+1, pattern) + next_button = Button(_("Next"), handler=handler_partial) + next_button.window.jans_help = _("Retreives next %d entries") % self.app.entries_per_page + buttons.append(next_button) + + if start_index!=0: + handler_partial = partial(get_next, start_index-1, pattern) + prev_button = Button(_("Prev"), handler=handler_partial) + prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + buttons.append(prev_button) + data_now = data[start_index*20:start_index*20+20] + + adminui_permissions = JansVerticalNav( + myparent=self.app, + headers=['permission', 'defaultPermissionInToken',], + preferred_size= [0,0], + data=data_now, + on_enter=self.edit_adminui_permissions, + on_display=self.app.data_display_dialog, + on_delete=self.delete_adminui_permissions, + # get_help=(self.get_help,'AdminRole'), + selectes=0, + headerColor='class:outh-verticalnav-headcolor', + entriesColor='class:outh-verticalnav-entriescolor', + all_data=result + ) + self.app.layout.focus(adminui_permissions) + self.config_data_container['permissions'] = HSplit([ + adminui_permissions, + VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) + ]) + get_app().invalidate() + self.app.layout.focus(adminui_permissions) + else: + self.app.show_message(_("Oops"), _("No matching result"),tobefocused = self.config_data_container['permissions']) + + def add_adminui_permissions(self) -> None: + """Method to display the dialog of clients + """ + + self.adminui_permission = self.app.getTitledText( + _("Permission"), + name='permission', + height=3, + style='class:dialog-titled-widget') + + self.adminui_role_permissions= self.app.getTitledCheckBox( + 'DefaultPermissionInToken', + name='defaultpermissionInToken', + checked= False, + style='class:outh-client-checkbox') + + def save(dialog: Dialog) -> None: + + permission = self.adminui_permission.me.text + defaultPermissionInToken = self.adminui_role_permissions.me.checked + + # ------------------------------------------------------------# + # --------------------- Patch to server ----------------------# + # ------------------------------------------------------------# + if permission : + async def coroutine(): + cli_args = { + 'operation_id': 'add-adminui-permission', + 'data': {'permission': '{}'.format(permission), 'defaultPermissionInToken': '{}'.format(defaultPermissionInToken)} + } + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + + if response: + self.app.show_message(_("Error!"), _("An error ocurred while Addin role adminui permission:\n") + str(response.text)) + else: + self.get_adminui_permissions() + + asyncio.ensure_future(coroutine()) + + + body = HSplit([self.adminui_permission,self.adminui_role_permissions]) + buttons = [Button(_("Cancel")), Button(_("OK"), handler=save)] + dialog = JansGDialog(self.app, title=_('Add New Role'), body=body, buttons=buttons, width=self.app.dialog_width-20) + self.app.show_jans_dialog(dialog) + + def search_adminui_permissions(self, tbuffer:Buffer,) -> None: + if not len(tbuffer.text) > 2: + self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.containers['permissions']) + return + + t = threading.Thread(target=self.adminui_update_permissions, args=(0,tbuffer.text), daemon=True) + self.app.start_progressing() + t.start() + + def edit_adminui_permissions(self, **params: Any) -> None: + """Method to display the dialog of clients + """ + + role_data = params.get('passed', []) + permission = role_data[0] + + + defaultPermissionInToken = role_data[1] + + self.adminui_role_permissions= self.app.getTitledCheckBox( + permission[8:78] if len(permission) > 30 else permission, + name='permission', + checked= defaultPermissionInToken, + style='class:outh-client-checkbox') + + def save(dialog: Dialog) -> None: + + defaultPermissionInToken = self.adminui_role_permissions.me.checked + + # ------------------------------------------------------------# + # --------------------- Patch to server ----------------------# + # ------------------------------------------------------------# + response = self.app.cli_object.process_command_by_id( + operation_id='edit-adminui-permission' , + url_suffix='', + endpoint_args='', + data_fn='', + data={'permission': '{}'.format(permission), 'defaultPermissionInToken': '{}'.format(defaultPermissionInToken)}, + ) + + # ------------------------------------------------------------# + # -- get_properties or serach again to see Momentary change --# + # ------------------------------------------------------------# + if response: + self.get_adminui_permissions() + # self.future.set_result(DialogResult.ACCEPT) + return True + + self.app.show_message(_("Error!"), _("An error ocurred while saving role adminui:\n") + str(response.text)) + + body = HSplit([self.adminui_role_permissions]) + buttons = [Button(_("Cancel")), Button(_("OK"), handler=save)] + dialog = JansGDialog(self.app, title='admin ui permissions', body=body, buttons=buttons, width=self.app.dialog_width-20) + self.app.show_jans_dialog(dialog) + + + def delete_adminui_permissions(self, **kwargs: Any) -> None: + + + dialog = self.app.get_confirm_dialog(_("Are you sure want to delete adminui_permissions :")+"\n {} ?".format(kwargs['selected'][0])) + + async def coroutine(): + focused_before = self.app.layout.current_window + result = await self.app.show_dialog_as_float(dialog) + try: + self.app.layout.focus(focused_before) + except: + self.app.stop_progressing() + self.app.layout.focus(self.app.center_frame) + + if result.lower() == 'yes': + result = self.app.cli_object.process_command_by_id( + operation_id='delete-adminui-permission', + url_suffix='adminUIPermission:{}'.format(kwargs ['selected'][0]), + endpoint_args='', + data_fn='', + data={} + ) + self.app.stop_progressing() + self.get_adminui_permissions() + + return result ### TODO >> Role cannot be deleted. Please set ‘deletable’ property of role to true. + + asyncio.ensure_future(coroutine()) + + #--------------------------------------------------------------------------------# + #------------------------------------- mapping ----------------------------------# + #--------------------------------------------------------------------------------# + + def get_adminui_mapping(self) -> None: + """Method to get the adminui_permissions data from server + """ + self.config_data_container['mapping'] = HSplit([Label(_("Please wait while getting adminui_permissions"),style='class:outh-waitclientdata.label')], width=D(),style='class:outh-waitclientdata') + t = threading.Thread(target=self.adminui_update_mapping, daemon=True) + self.app.start_progressing() + t.start() + + def adminui_update_mapping( + self, + start_index: Optional[int]= 0, + pattern: Optional[str]= '', + ) -> None: + """update the current adminui_permissions data to server + + Args: + pattern (str, optional): endpoint arguments for the client data. Defaults to ''. + """ + + def get_next( + start_index: int, + pattern: Optional[str]= '', + ) -> None: + self.adminui_update_mapping(start_index, pattern='') + + endpoint_args ='limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) + if pattern: + endpoint_args +=',pattern:'+pattern + try : + rsponse = self.app.cli_object.process_command_by_id( + operation_id='get-all-adminui-role-permissions', + url_suffix='', + endpoint_args='', + data_fn=None, + data={} + ) + + except Exception as e: + self.app.stop_progressing() + self.app.show_message(_("Error getting adminui_permissions"), str(e)) + return + + self.app.stop_progressing() + + if rsponse.status_code not in (200, 201): + self.app.show_message(_("Error getting adminui_permissions"), str(rsponse.text)) + return + + try: + result = rsponse.json() + except Exception: + self.app.show_message(_("Error getting adminui_permissions"), str(rsponse.text)) + return + + data =[] + + # for d in result: + # data.append( + # [ + # d.get('role'), + # len(d.get('permissions')), + # ] + # ) + + if pattern: + for k in result: + if pattern.lower() in k.get('role').lower(): + data.append( + [ + k.get('role'), + len(k.get('permissions')), + ] + ) + else: + for d in result: + data.append( + [ + d.get('role'), + len(d.get('permissions')), + ] + ) + + + if data: + buttons = [] + if int(len(data)/ 20) >=1 : + + if start_index< int(len(data)/ 20) : + handler_partial = partial(get_next, start_index+1, pattern) + next_button = Button(_("Next"), handler=handler_partial) + next_button.window.jans_help = _("Retreives next %d entries") % self.app.entries_per_page + buttons.append(next_button) + + if start_index!=0: + handler_partial = partial(get_next, start_index-1, pattern) + prev_button = Button(_("Prev"), handler=handler_partial) + prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + buttons.append(prev_button) + data_now = data[start_index*20:start_index*20+20] + + adminui_permissions = JansVerticalNav( + myparent=self.app, + headers=['role', 'permissions',], + preferred_size= [0,0], + data=data_now, + on_enter=self.edit_adminui_mapping, + on_display=self.app.data_display_dialog, + # get_help=(self.get_help,'AdminRole'), + selectes=0, + headerColor='class:outh-verticalnav-headcolor', + entriesColor='class:outh-verticalnav-entriescolor', + all_data=result + ) + self.app.layout.focus(adminui_permissions) # clients.focuse..!? TODO >> DONE + self.config_data_container['mapping'] = HSplit([ + adminui_permissions, + VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) + ]) + get_app().invalidate() + self.app.layout.focus(adminui_permissions) ### it fix focuse on the last item deletion >> try on UMA-res >> edit_client_dialog >> oauth_update_uma_resources + + else: + self.app.show_message(_("Oops"), _("No matching result"),tobefocused = self.config_data_container['mapping']) + + # def add_adminui_mapping(self) -> None: + # try : + # rsponse = self.app.cli_object.process_command_by_id( + # operation_id='get-all-adminui-roles', + # url_suffix='', + # endpoint_args='', + # data_fn=None, + # data={} + # ) + + # except Exception as e: + # self.app.stop_progressing() + # self.app.show_message(_("Error getting clients"), str(e)) + # return + + # values=[] + # for i in rsponse.json(): + # values.append((i['role'],i['role'])) + + # #------------------------------------------------------------------------# + # #- values = [(api-manager,api-manager),(api-admin,api-admin),(api-editor,api-editor),(api-viewer,api-viewer)]# + # #------------------------------------------------------------------------# + + # self.alt_tabs = {} + # self.alt_tabs['api-manager'] = Label(text=_("api-manager"),style='red') + # self.alt_tabs['api-admin'] = Label(text=_("api-admin"),style='red') + # self.alt_tabs['api-editor'] = Label(text=_("api-editor"),style='red') + # self.alt_tabs['api-viewer'] = Label(text=_("api-viewer"),style='red') + # self.alt_tabs['api-editor2'] = Label(text=_("api-editor2"),style='red') + # self.alt_tabs['api-hopa'] = Label(text=_("api-hopa"),style='red') + + # def role_selection_changed( + # cb: RadioList, + # ) -> None: + # self.role_type = cb.current_value + + # self.adminui_mapping= self.app.getTitledRadioButton( + # _("role"), + # name='role', + # values=values, + # on_selection_changed=role_selection_changed, + # style='class:outh-scope-radiobutton') + + + # def save(dialog: Dialog) -> None: + + # permission = self.adminui_permission.me.text + # defaultPermissionInToken = self.adminui_role_permissions.me.checked + + # self.app.logger.debug("defaultPermissionInToken: "+str(defaultPermissionInToken)) + # # ------------------------------------------------------------# + # # --------------------- Patch to server ----------------------# + # # ------------------------------------------------------------# + # if permission : + # response = self.app.cli_object.process_command_by_id( + # operation_id='add-adminui-permission', + # url_suffix='', + # endpoint_args='', + # data_fn='', + # data={'permission': '{}'.format(permission), 'defaultPermissionInToken': '{}'.format(defaultPermissionInToken)}, + # ) + # else: + # return + # # ------------------------------------------------------------# + # # -- get_properties or serach again to see Momentary change --# + # # ------------------------------------------------------------# + # if response: + # self.get_adminui_permissions() + # # self.future.set_result(DialogResult.ACCEPT) + # return True + + # self.app.show_message(_("Error!"), _("An error ocurred while Addin role adminui permission:\n") + str(response.text)) + + + # body = HSplit([self.adminui_mapping,DynamicContainer(lambda: self.alt_tabs[self.role_type])]) + # buttons = [Button(_("Cancel")), Button(_("OK"), handler=save)] + # dialog = JansGDialog(self.app, title=_('Add New Role'), body=body, buttons=buttons, width=self.app.dialog_width-20) + # self.app.show_jans_dialog(dialog) + + def search_adminui_mapping(self, tbuffer:Buffer,) -> None: + if not len(tbuffer.text) > 2: + self.app.show_message(_("Error!"), _("Search string should be at least three characters"),tobefocused=self.containers['mapping']) + return + + t = threading.Thread(target=self.adminui_update_mapping, args=(0,tbuffer.text), daemon=True) + self.app.start_progressing() + t.start() + + def edit_adminui_mapping(self, **params: Any) -> None: + """Method to display the dialog of clients + """ + role_data = params.get('data', []) + permission = role_data.get('role') + defaultPermissionInToken = [] + + for i in role_data.get('permissions'): + defaultPermissionInToken.append([i]) + + + self.adminui_role_permissions= JansVerticalNav( + myparent=self.app, + headers=['permission'], + preferred_size= [0], + data= defaultPermissionInToken,#'defaultPermissionInToken', + on_enter=self.edit_adminui_mapping, + on_display=self.app.data_display_dialog, + # get_help=(self.get_help,'AdminRole'), + selectes=0, + headerColor='red', + entriesColor='green', + all_data=defaultPermissionInToken + ) + + def save(dialog: Dialog) -> None: + + defaultPermissionInToken = self.adminui_role_permissions.me.checked + + # ------------------------------------------------------------# + # --------------------- Patch to server ----------------------# + # ------------------------------------------------------------# + response = self.app.cli_object.process_command_by_id( + operation_id='edit-adminui-permission' , + url_suffix='', + endpoint_args='', + data_fn='', + data={'permission': '{}'.format(permission), 'defaultPermissionInToken': '{}'.format(defaultPermissionInToken)}, + ) + + # ------------------------------------------------------------# + # -- get_properties or serach again to see Momentary change --# + # ------------------------------------------------------------# + if response: + self.get_adminui_permissions() + # self.future.set_result(DialogResult.ACCEPT) + return True + + self.app.show_message(_("Error!"), _("An error ocurred while saving role adminui:\n") + str(response.text)) + + body = HSplit([self.adminui_role_permissions]) + buttons = [Button(_("Cancel"))] + dialog = JansGDialog(self.app, title='admin ui permissions', body=body, buttons=buttons, width=self.app.dialog_width-20) + self.app.show_jans_dialog(dialog) + + #--------------------------------------------------------------------------------# + #--------------------------------------------------------------------------------# + #--------------------------------------------------------------------------------# + + def nav_selection_changed( + self, + selection: str + ) -> None: + + """This method for the selection change + + Args: + selection (str): the current selected tab + """ + + if selection in self.containers: + self.main_area = self.containers[selection] + else: + self.main_area = self.app.not_implemented + + def set_center_frame(self) -> None: + """center frame content + """ + self.app.center_container = self.main_container + diff --git a/jans-cli-tui/cli_tui/plugins/060_scripts/.enabled b/jans-cli-tui/cli_tui/plugins/060_scripts/.enabled new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/060_scripts/__init__.py b/jans-cli-tui/cli_tui/plugins/060_scripts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 new file mode 100755 index 00000000000..9263b71af24 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/060_scripts/edit_script_dialog.py @@ -0,0 +1,421 @@ +import re +import threading + +from typing import OrderedDict +from asyncio import ensure_future +from functools import partial + +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.containers import ( + HSplit, + VSplit, + DynamicContainer, + Window +) +from prompt_toolkit.widgets import ( + Box, + Button, + Label, + TextArea, + RadioList, + Button, + Dialog, +) +from prompt_toolkit.application.current import get_app + +from prompt_toolkit.lexers import PygmentsLexer +from pygments.lexers.python import PythonLexer +from pygments.lexers.jvm import JavaLexer + +from cli import config_cli +from utils.static import DialogResult +from wui_components.jans_dialog_with_nav import JansDialogWithNav +from wui_components.jans_side_nav_bar import JansSideNavBar +from wui_components.jans_cli_dialog import JansGDialog +from wui_components.jans_drop_down import DropDownWidget +from wui_components.jans_data_picker import DateSelectWidget +from utils.utils import DialogUtils +from wui_components.jans_vetrical_nav import JansVerticalNav +from wui_components.jans_spinner import Spinner +from prompt_toolkit.layout.containers import ( + AnyContainer, +) +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.layout.dimension import AnyDimension +from typing import Optional, Sequence, Union +from typing import TypeVar, Callable + +from typing import Any, Optional + + +from view_uma_dialog import ViewUMADialog + +from utils.multi_lang import _ + + +class EditScriptDialog(JansGDialog, DialogUtils): + """This Script editing dialog + """ + def __init__( + self, + parent, + data:list, + title: AnyFormattedText= "", + buttons: Optional[Sequence[Button]]= [], + save_handler: Callable= None, + )-> Dialog: + """init for `EditScriptDialog`, inherits from two diffrent classes `JansGDialog` and `DialogUtils` + + DialogUtils (methods): Responsable for all `make data from dialog` and `check required fields` in the form for any Edit or Add New + + Args: + parent (widget): This is the parent widget for the dialog, to access `Pageup` and `Pagedown` + title (str): The Main dialog title + data (list): selected line data + button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. + save_handler (method, optional): handler invoked when closing the dialog. Defaults to None. + """ + super().__init__(parent, title, buttons) + self.myparent = parent + self.save_handler = save_handler + self.data = data + self.title=title + self.cur_lang = self.data.get('programmingLanguage', 'python') + self.create_window() + self.script = self.data.get('script','') + + def save(self) -> None: + + data = {} + + for item in self.edit_dialog_content: + item_data = self.get_item_data(item) + if item_data: + data[item_data['key']] = item_data['value'] + + for prop_container in (self.config_properties_container, self.module_properties_container): + + if prop_container.data: + data[prop_container.jans_name] = [] + for prop_ in prop_container.data: + key_ = prop_[0] + val_ = prop_[1] + + if key_: + prop = {'value1': key_} + if val_: + prop['value2'] = val_ + if len(prop_) > 2: + prop['hide'] = prop_[2] + data[prop_container.jans_name].append(prop) + + + data['locationType'] = 'ldap' if data['location'] == 'db' else 'file' + data['internal'] = self.data.get('internal', False) + data['modified'] = self.data.get('modified', False) + data['revision'] = self.data.get('revision', 0) + 1 + data['script'] = self.script + + del data['location'] + + if not data['inum']: + del data['inum'] + + if self.data.get('baseDn'): + data['baseDn'] = self.data['baseDn'] + + self.new_data = data + + close_me = True + if self.save_handler: + close_me = self.save_handler(self) + if close_me: + self.future.set_result(DialogResult.ACCEPT) + + def cancel(self) -> None: + self.future.set_result(DialogResult.CANCEL) + + def create_window(self) -> None: + + schema = self.myparent.cli_object.get_schema_from_reference('', '#/components/schemas/CustomScript') + + script_types = [ + ['person_authentication', 'Person Authentication'], + ['consent_gathering', 'Consent Gathering'], + ['post_authn', 'Post Authentication'], + ['id_token', 'id_token'], + ['password_grant', 'Password Grant'], + ['ciba_end_user_notification', 'CIBA End User Notification'], + #['OpenID Configuration', 'OpenID Configuration'], + ['dynamic_scope', 'Dynamic Scope', ], + ['spontaneous_scope', 'Spontaneous Scope',], + ['application_session', 'Application Session'], + ['end_session', 'End Session'], + ['client_registration', 'Client Registration'], + ['introspection', 'Introspection'], + ['update_token', 'Update Token'], + ['config_api', 'Config API'], + ['idp', 'IDP'], + ['resource_owner_password_credentials', 'Resource Owner Password Credentials'], + ['cache_refresh', 'Cache Refresh'], + ['id_generator', 'Id Generator'], + ['uma_rpt_policy', 'Uma Rpt Policy'], + ['uma_rpt_claims', 'Uma Rpt Claims'], + ['uma_claims_gathering', 'Uma Claims Gathering'], + ['scim', 'SCIM'], + ['revoke_token', 'Revoke Token'], + ['persistence_extension', 'Persistence Extension'], + ['discovery', 'Discovery'], + ] + + self.location_widget = self.myparent.getTitledText( + _(" Path"), + name='locationPath', + value=self.data.get('locationPath',''), + style='class:script-titledtext', + jans_help="locationPath" + ) + + self.set_location_widget_state(self.data.get('locationPath') == 'file') + + config_properties_title = _("Conf. Properties: ") + add_property_title = _("Add Property") + module_properties_title = _("Module Properties: ") + + config_properties_data = [] + for prop in self.data.get('configurationProperties', []): + config_properties_data.append([prop['value1'], prop.get('value2', ''), prop.get('hide', False)]) + + self.config_properties_container = JansVerticalNav( + myparent=self.myparent, + headers=['Key', 'Value', 'Hide'], + preferred_size=[15, 15, 5], + data=config_properties_data, + on_enter=self.edit_property, + on_delete=self.delete_config_property, + on_display=self.myparent.data_display_dialog, + get_help=(self.get_help,'Properties'), + selectes=0, + headerColor='class:outh-client-navbar-headcolor', + entriesColor='class:outh-client-navbar-entriescolor', + all_data=config_properties_data, + underline_headings=False, + max_width=52, + jans_name='configurationProperties', + max_height=False + ) + + module_properties_data = [] + for prop in self.data.get('moduleProperties', []): + module_properties_data.append([prop['value1'], prop.get('value2', '')]) + + self.module_properties_container = JansVerticalNav( + myparent=self.myparent, + headers=['Key', 'Value'], + preferred_size=[20, 20], + data=module_properties_data, + on_enter=self.edit_property, + on_delete=self.delete_config_property, + on_display=self.myparent.data_display_dialog, + get_help=(self.get_help,'Properties'), + selectes=0, + headerColor='class:outh-client-navbar-headcolor', + entriesColor='class:outh-client-navbar-entriescolor', + all_data=module_properties_data, + underline_headings=False, + max_width=44, + jans_name='moduleProperties', + max_height=3 + ) + + open_editor_button_title = _("Edit Script") + open_editor_button = Button(text=open_editor_button_title, width=len(open_editor_button_title)+2, handler=self.edit_script_dialog) + open_editor_button.window.jans_help="Enter to open editing window" + + self.edit_dialog_content = [ + self.myparent.getTitledText(_("Inum"), name='inum', value=self.data.get('inum',''), style='class:script-titledtext', jans_help=self.myparent.get_help_from_schema(schema, 'inum'), read_only=True), + self.myparent.getTitledWidget( + _("Script Type"), + name='scriptType', + widget=DropDownWidget( + values=script_types, + value=self.data.get('scriptType', '') + ), + jans_help=self.myparent.get_help_from_schema(schema, 'scriptType'), + style='class:outh-client-dropdown'), + + self.myparent.getTitledCheckBox(_("Enabled"), name='enabled', checked=self.data.get('enabled'), style='class:script-checkbox', jans_help=self.myparent.get_help_from_schema(schema, 'enabled')), + self.myparent.getTitledText(_("Name"), name='name', value=self.data.get('name',''), style='class:script-titledtext', jans_help=self.myparent.get_help_from_schema(schema, 'name')), + self.myparent.getTitledText(_("Description"), name='description', value=self.data.get('description',''), style='class:script-titledtext', jans_help=self.myparent.get_help_from_schema(schema, 'description')), + + self.myparent.getTitledRadioButton( + _("Location"), + name='location', + values=[('db', _("Database")), ('file', _("File System"))], + current_value= 'file' if self.data.get('locationPath') else 'db', + jans_help=_("Where to save script"), + style='class:outh-client-radiobutton', + on_selection_changed=self.script_location_changed, + ), + + self.location_widget, + + self.myparent.getTitledWidget( + _("Programming Language"), + name='programmingLanguage', + widget=DropDownWidget( + values=[['python', 'Python'], ['java', 'Java']], + value=self.cur_lang, + on_value_changed=self.script_lang_changed, + ), + jans_help=self.myparent.get_help_from_schema(schema, 'programmingLanguage'), + style='class:outh-client-dropdown'), + + self.myparent.getTitledWidget( + _("Level"), + name='level', + widget=Spinner( + value=self.data.get('level', 0) + ), + jans_help=self.myparent.get_help_from_schema(schema, 'level'), + style='class:outh-client-dropdown'), + + VSplit([ + Label(text=config_properties_title, style='class:script-label', width=len(config_properties_title)+1), + self.config_properties_container, + Window(width=2), + HSplit([ + Window(height=1), + Button(text=add_property_title, width=len(add_property_title)+4, handler=partial(self.edit_property, jans_name='configurationProperties')), + ]), + ], + height=5, width=D(), + ), + + VSplit([ + Label(text=module_properties_title, style='class:script-label', width=len(module_properties_title)+1), + self.module_properties_container, + Window(width=2), + HSplit([ + Window(height=1), + Button(text=add_property_title, width=len(add_property_title)+4, handler=partial(self.edit_property, jans_name='moduleProperties')), + ]), + ], + height=5 + ), + VSplit([open_editor_button, Window(width=D())]), + ] + + + self.dialog = JansDialogWithNav( + title=self.title, + content= HSplit( + self.edit_dialog_content, + width=D(), + height=D() + ), + button_functions=[(self.cancel, _("Cancel")), (self.save, _("Save"))], + height=self.myparent.dialog_height, + width=self.myparent.dialog_width, + ) + + + def get_help(self, **kwargs: Any): + + # schema = self.app.cli_object.get_schema_from_reference('#/components/schemas/{}'.format(str(kwargs['scheme']))) + + if kwargs['scheme'] == 'Properties': + self.myparent.status_bar_text= kwargs['data'][0] + + + + + def script_lang_changed( + self, + value: str, + ) -> None: + self.cur_lang = value + + def set_location_widget_state( + self, + state: bool, + ) -> None: + self.location_widget.me.read_only = not state + self.location_widget.me.focusable = state + if not state: + self.location_widget.me.text = '' + + def script_location_changed( + self, + redio_button: RadioList, + ) -> None: + state = redio_button.current_value == 'file' + self.set_location_widget_state(state) + + def edit_property(self, **kwargs: Any) -> None: + + if kwargs['jans_name'] == 'moduleProperties': + key, val = kwargs.get('data', ('','')) + title = _("Enter Module Properties") + else: + key, val, hide = kwargs.get('data', ('','', False)) + hide_widget = self.myparent.getTitledCheckBox(_("Hide"), name='property_hide', checked=hide, style='class:script-titledtext', jans_help=_("Hide script property?")) + title = _("Enter Configuration Properties") + + key_widget = self.myparent.getTitledText(_("Key"), name='property_key', value=key, style='class:script-titledtext', jans_help=_("Script propery Key")) + val_widget = self.myparent.getTitledText(_("Value"), name='property_val', value=val, style='class:script-titledtext', jans_help=_("Script property Value")) + + def add_property(dialog: Dialog) -> None: + key_ = key_widget.me.text + val_ = val_widget.me.text + cur_data = [key_, val_] + + if kwargs['jans_name'] == 'configurationProperties': + hide_ = hide_widget.me.checked + cur_data.append(hide_) + container = self.config_properties_container + else: + container = self.module_properties_container + if not kwargs.get('data'): + container.add_item(cur_data) + else: + container.replace_item(kwargs['selected'], cur_data) + + body_widgets = [key_widget, val_widget] + if kwargs['jans_name'] == 'configurationProperties': + body_widgets.append(hide_widget) + + body = HSplit(body_widgets) + buttons = [Button(_("Cancel")), Button(_("OK"), handler=add_property)] + dialog = JansGDialog(self.myparent, title=title, body=body, buttons=buttons, width=self.myparent.dialog_width-20) + self.myparent.show_jans_dialog(dialog) + + def delete_config_property(self, **kwargs: Any) -> None: + if kwargs['jans_name'] == 'configurationProperties': + self.config_properties_container.remove_item(kwargs['selected']) + else: + self.module_properties_container.remove_item(kwargs['selected']) + + def edit_script_dialog(self) -> None: + + text_editor = TextArea( + text=self.script, + multiline=True, + height=self.myparent.dialog_height-10, + width=D(), + focusable=True, + scrollbar=True, + line_numbers=True, + lexer=PygmentsLexer(PythonLexer if self.cur_lang == 'PYTHON' else JavaLexer), + ) + + def modify_script(arg) -> None: + self.script = text_editor.text + + buttons = [Button(_("Cancel")), Button(_("OK"), handler=modify_script)] + dialog = JansGDialog(self.myparent, title=_("Edit Script"), body=HSplit([text_editor]), buttons=buttons, width=self.myparent.dialog_width-10) + self.myparent.show_jans_dialog(dialog) + + def __pt_container__(self)-> Dialog: + return self.dialog + diff --git a/jans-cli-tui/cli_tui/plugins/060_scripts/main.py b/jans-cli-tui/cli_tui/plugins/060_scripts/main.py new file mode 100755 index 00000000000..dcc44894f33 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/060_scripts/main.py @@ -0,0 +1,255 @@ +import os +import sys + +from functools import partial +import asyncio + +import prompt_toolkit +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 +from prompt_toolkit.layout.containers import ( + HSplit, + VSplit, + HorizontalAlign, + DynamicContainer, + Window, +) +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.widgets import ( + Box, + Button, + Label, + Frame, + Dialog +) +from typing import Any, Optional +from prompt_toolkit.buffer import Buffer + +from utils.static import DialogResult + +from cli import config_cli +from wui_components.jans_nav_bar import JansNavBar +from wui_components.jans_side_nav_bar import JansSideNavBar +from wui_components.jans_vetrical_nav import JansVerticalNav +from wui_components.jans_dialog import JansDialog +from wui_components.jans_dialog_with_nav import JansDialogWithNav +from wui_components.jans_drop_down import DropDownWidget +from wui_components.jans_data_picker import DateSelectWidget + +from edit_script_dialog import EditScriptDialog +from prompt_toolkit.application import Application + +from utils.multi_lang import _ +import cli_style + + +class Plugin(): + """This is a general class for plugins + """ + def __init__( + self, + app: Application + ) -> None: + """init for Plugin class "oxauth" + + Args: + app (_type_): _description_ + """ + self.app = app + self.pid = 'scripts' + self.name = 'Sc[r]ipts' + + self.scripts_prepare_containers() + + def process(self) -> None: + pass + + def set_center_frame(self) -> None: + """center frame content + """ + self.app.center_container = self.scripts_main_area + + def scripts_prepare_containers(self) -> None: + """prepare the main container (tabs) for the current Plugin + """ + self.scripts_list_container = HSplit([],width=D(), height=D()) + + self.scripts_main_area = HSplit([ + VSplit([ + self.app.getButton(text=_("Get Scripts"), name='scripts:get', jans_help=_("Retreive first %d Scripts") % (20), handler=self.get_scripts), + self.app.getTitledText(_("Search: "), name='scripts:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_scripts, style='class:outh_containers_scopes.text'), + self.app.getButton(text=_("Add Sscript"), name='scripts:add', jans_help=_("To add a new scope press this button"), handler=self.add_script_dialog), + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.scripts_list_container) + ],style='class:outh_containers_scopes') + + + def get_scripts( + self, + start_index: Optional[int]= 1, + pattern: Optional[str]= '', + ) -> None: + + endpoint_args ='limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) + if pattern: + endpoint_args +=',pattern:'+pattern + + cli_args = {'operation_id': 'get-config-scripts', 'endpoint_args': endpoint_args} + + async def coroutine(): + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.users = response.json() + + if not self.users.get('entries'): + self.app.show_message(_("Not found"), _("No script found for this search."), tobefocused=self.app.center_container) + return + + self.data = response.json() + self.scripts_update_list(pattern) + self.app.layout.focus(self.scripts_list_container) + + asyncio.ensure_future(coroutine()) + + + def scripts_update_list( + self, + pattern: Optional[str]= '', + ) -> None: + """Updates Scripts data from server + + Args: + start_index (int, optional): add Button("Prev") to the layout. Defaults to 0. + """ + + data =[] + + for d in self.data.get('entries', []): + data.append( + [ + d['inum'], + d.get('name', ''), + d.get('description',''), + ] + ) + + self.scripts_listbox = JansVerticalNav( + myparent=self.app, + headers=['inum', 'Name', 'Description'], + preferred_size= [15, 25, 0], + data=data, + on_enter=self.add_script_dialog, + on_display=self.app.data_display_dialog, + get_help=(self.get_help,'Scripts'), + on_delete=self.delete_script, + selectes=0, + headerColor='class:outh-verticalnav-headcolor', + entriesColor='class:outh-verticalnav-entriescolor', + all_data=self.data['entries'] + ) + + buttons = [] + + if self.data['start'] > 1: + handler_partial = partial(self.get_scripts, self.data['start']-self.app.entries_per_page+1, pattern) + prev_button = Button(_("Prev"), handler=handler_partial) + prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + buttons.append(prev_button) + if self.data['totalEntriesCount'] > self.data['start'] + self.data['entriesCount']: + handler_partial = partial(self.get_scripts, self.data['start']+self.app.entries_per_page+1, pattern) + next_button = Button(_("Next"), handler=handler_partial) + next_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + buttons.append(next_button) + + + self.scripts_list_container = HSplit([ + Window(height=1), + self.scripts_listbox, + VSplit(buttons, padding=5, align=HorizontalAlign.CENTER), + ], height=D()) + self.app.layout.focus(self.scripts_listbox) + get_app().invalidate() + + + def get_help(self, **kwargs: Any): + + # schema = self.app.cli_object.get_schema_from_reference('#/components/schemas/{}'.format(str(kwargs['scheme']))) + + if kwargs['scheme'] == 'Scripts': + self.app.status_bar_text= kwargs['data'][2] + + + + def search_scripts(self, tbuffer:Buffer) -> None: + if not len(tbuffer.text) > 2: + self.app.show_message(_("Error!"), _("Search string should be at least three characters"), tobefocused=self.scripts_main_area) + return + + self.get_scripts(pattern=tbuffer.text) + + def add_script_dialog(self, **kwargs: Any): + """Method to display the edit script dialog + """ + if kwargs: + data = kwargs.get('data', {}) + else: + data = {} + + title = _("Edit Script") if data else _("Add Script") + + dialog = EditScriptDialog(self.app, title=title, data=data, save_handler=self.save_script) + result = self.app.show_jans_dialog(dialog) + + def save_script(self, dialog: Dialog) -> None: + """This method to save the script data to server + + Args: + dialog (_type_): the main dialog to save data in + + Returns: + _type_: bool value to check the status code response + """ + + async def coroutine(): + import json + operation_id = 'put-config-scripts' if dialog.new_data.get('baseDn') else 'post-config-scripts' + cli_args = {'operation_id': operation_id, 'data': dialog.new_data} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + if response.status_code == 500: + self.app.show_message(_('Error'), response.text + '\n' + response.reason) + else: + dialog.future.set_result(DialogResult.OK) + self.get_scripts() + + asyncio.ensure_future(coroutine()) + + def delete_script(self, **kwargs: Any) -> None: + + def do_delete_script(): + + async def coroutine(): + cli_args = {'operation_id': 'delete-config-scripts-by-inum', 'url_suffix':'inum:{}'.format(kwargs['selected'][0])} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + if response: + self.app.show_message(_("Error"), _("Deletion was not completed {}".format(response)), tobefocused=self.scripts_listbox) + else: + self.scripts_listbox.remove_item(kwargs['selected']) + asyncio.ensure_future(coroutine()) + + buttons = [Button(_("No")), Button(_("Yes"), handler=do_delete_script)] + + self.app.show_message( + title=_("Confirm"), + message=_("Are you sure you want to delete script {}?").format(kwargs['selected'][1]), + buttons=buttons, + tobefocused=self.scripts_listbox + ) diff --git a/jans-cli-tui/cli_tui/plugins/070_users/.enabled b/jans-cli-tui/cli_tui/plugins/070_users/.enabled new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/070_users/__init__.py b/jans-cli-tui/cli_tui/plugins/070_users/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/070_users/edit_user_dialog.py b/jans-cli-tui/cli_tui/plugins/070_users/edit_user_dialog.py new file mode 100644 index 00000000000..90b31613c96 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/070_users/edit_user_dialog.py @@ -0,0 +1,261 @@ +import re +import threading + +from typing import OrderedDict, Optional, Sequence, Union, TypeVar, Callable +import asyncio +from functools import partial + + +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.containers import ( + HSplit, + VSplit, + DynamicContainer, + Window, + AnyContainer +) +from prompt_toolkit.widgets import ( + Box, + Button, + Label, + TextArea, + RadioList, + CheckboxList, + Button, + Dialog, +) +from prompt_toolkit.eventloop import get_event_loop + +from cli import config_cli +from utils.static import DialogResult +from wui_components.jans_dialog_with_nav import JansDialogWithNav +from wui_components.jans_side_nav_bar import JansSideNavBar +from wui_components.jans_cli_dialog import JansGDialog +from wui_components.jans_drop_down import DropDownWidget +from wui_components.jans_data_picker import DateSelectWidget +from utils.utils import DialogUtils, common_data +from wui_components.jans_vetrical_nav import JansVerticalNav +from prompt_toolkit.formatted_text import AnyFormattedText + +from typing import Any, Optional + + +from view_uma_dialog import ViewUMADialog + +from utils.multi_lang import _ + + +class EditUserDialog(JansGDialog, DialogUtils): + """This user editing dialog + """ + def __init__( + self, + parent, + data:dict, + title: AnyFormattedText= "", + buttons: Optional[Sequence[Button]]= [], + save_handler: Callable= None, + )-> Dialog: + """init for `EditUserDialog`, inherits from two diffrent classes `JansGDialog` and `DialogUtils` + + DialogUtils (methods): Responsable for all `make data from dialog` and `check required fields` in the form for any Edit or Add New + + Args: + parent (widget): This is the parent widget for the dialog, to access `Pageup` and `Pagedown` + title (str): The Main dialog title + data (list): selected line data + button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. + save_handler (method, optional): handler invoked when closing the dialog. Defaults to None. + """ + super().__init__(parent, title, buttons) + self.app = parent + self.save_handler = save_handler + self.data = data + self.title=title + self.admin_ui_roles = {} + self.schema = self.app.cli_object.get_schema_from_reference('User-Mgt', '#/components/schemas/CustomUser') + self.create_window() + + def cancel(self) -> None: + self.future.set_result(DialogResult.CANCEL) + + + def get_claim_properties(self, claim): + ret_val = {} + for tmp in common_data.users.claims: + if tmp['name'] == claim: + ret_val = tmp + break + + return ret_val + + def create_window(self) -> None: + + def get_custom_attribute(attribute, multi=False): + for ca in self.data.get('customAttributes', []): + if ca['name'] == attribute: + if multi: + return ca['values'] + return ', '.join(ca['values']) + return [] if multi else '' + + if self.data: + active_checked = self.data.get('jansStatus', '').lower() == 'active' + else: + active_checked = True + + self.edit_user_content = [ + self.app.getTitledText(_("Inum"), name='inum', value=self.data.get('inum',''), style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, 'inum'), read_only=True), + self.app.getTitledText(_("Username *"), name='userId', value=self.data.get('userId',''), style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, 'userId')), + self.app.getTitledText(_("First Name"), name='givenName', value=self.data.get('givenName',''), style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, 'givenName')), + self.app.getTitledText(_("Middle Name"), name='middleName', value=get_custom_attribute('middleName'), style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, 'middleName')), + self.app.getTitledText(_("Last Name"), name='sn', value=get_custom_attribute('sn'), style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, 'sn')), + self.app.getTitledText(_("Display Name"), name='displayName', value=self.data.get('displayName',''), style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, 'displayName')), + self.app.getTitledText(_("Email *"), name='mail', value=self.data.get('mail',''), style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, 'mail')), + self.app.getTitledCheckBox(_("Active"), name='active', checked=active_checked, style='class:script-checkbox', jans_help=self.app.get_help_from_schema(self.schema, 'enabled')), + self.app.getTitledText(_("Nickname"), name='nickname', value='\n'.join(get_custom_attribute('nickname', multi=True)), style='class:script-titledtext', height=3, jans_help=self.app.get_help_from_schema(self.schema, 'nickname')), + + Button(_("Add Claim"), handler=self.add_claim), + ] + + + if self.app.plugin_enabled('config_api'): + admin_ui_roles = [[role] for role in get_custom_attribute('jansAdminUIRole', multi=True) ] + admin_ui_roles_label = _("jansAdminUIRole") + add_admin_ui_role_label = _("Add Admin UI Role") + self.admin_ui_roles_container = JansVerticalNav( + myparent=self.app, + headers=['Role'], + preferred_size=[20], + data=admin_ui_roles, + on_delete=self.delete_admin_ui_role, + selectes=0, + headerColor='class:outh-client-navbar-headcolor', + entriesColor='class:outh-client-navbar-entriescolor', + all_data=admin_ui_roles, + underline_headings=False, + max_width=25, + jans_name='configurationProperties', + max_height=False + ) + + self.edit_user_content.insert(-1, + VSplit([ + Label(text=admin_ui_roles_label, style='class:script-label', width=len(admin_ui_roles_label)+1), + self.admin_ui_roles_container, + Window(width=2), + HSplit([ + Window(height=1), + Button(text=add_admin_ui_role_label, width=len(add_admin_ui_role_label)+4, handler=self.add_admin_ui_role), + ]), + ], height=4, width=D()) + ) + + + if not self.data: + self.edit_user_content.insert(2, + self.app.getTitledText(_("Password *"), name='userPassword', value='', style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, 'userPassword')) + ) + + for ca in self.data.get('customAttributes', []): + if ca['name'] in ('middleName', 'sn', 'jansStatus', 'nickname'): + continue + claim_prop = self.get_claim_properties(ca['name']) + if claim_prop.get('dataType', 'string') in ('string', 'json'): + self.edit_user_content.insert(-1, + self.app.getTitledText(_(claim_prop.get('displayName', ca['name'])), name=ca['name'], value=get_custom_attribute(ca['name']), style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, ca['name'])) + ) + elif claim_prop.get('dataType') == 'boolean': + checked = get_custom_attribute(ca['name']).lower() == 'true' + self.edit_user_content.insert(-1, + self.app.getTitledCheckBox(_(claim_prop['displayName']), name=ca['name'], checked=checked, style='class:script-checkbox', jans_help=self.app.get_help_from_schema(self.schema, ca['name'])) + ) + + self.edit_user_container = HSplit(self.edit_user_content, height=D(), width=D()) + + self.dialog = JansDialogWithNav( + title=self.title, + content=DynamicContainer(lambda: self.edit_user_container), + button_functions=[(self.cancel, _("Cancel")), (partial(self.save_handler, self), _("Save"))], + height=self.app.dialog_height, + width=self.app.dialog_width, + ) + + + def get_admin_ui_roles(self) -> None: + async def coroutine(): + cli_args = {'operation_id': 'get-all-adminui-roles'} + self.app.start_progressing() + response = await get_event_loop().run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.admin_ui_roles = response.json() + self.add_admin_ui_role() + asyncio.ensure_future(coroutine()) + + def add_admin_ui_role(self) -> None: + if not self.admin_ui_roles: + self.get_admin_ui_roles() + return + + ui_roles_to_be_added = [] + for role in self.admin_ui_roles: + for cur_role in self.admin_ui_roles_container.data: + if cur_role[0] == role['role']: + break + else: + ui_roles_to_be_added.append([role['role'], role['role']]) + + admin_ui_roles_checkbox = CheckboxList(values=ui_roles_to_be_added) + + def add_role(dialog) -> None: + for role_ in admin_ui_roles_checkbox.current_values: + self.admin_ui_roles_container.add_item([role_]) + + body = HSplit([Label(_("Select Admin-UI role to be added to current user.")), admin_ui_roles_checkbox]) + buttons = [Button(_("Cancel")), Button(_("OK"), handler=add_role)] + dialog = JansGDialog(self.app, title=_("Select Admin-UI"), body=body, buttons=buttons, width=self.app.dialog_width-20) + self.app.show_jans_dialog(dialog) + + + def delete_admin_ui_role(self, **kwargs: Any) -> None: + self.admin_ui_roles_container.remove_item(kwargs['selected']) + + + + def add_claim(self) -> None: + cur_claims = [] + for w in self.edit_user_content: + if hasattr(w, 'me'): + cur_claims.append(w.me.window.jans_name) + + claims_list = [] + for claim in common_data.users.claims: + if not claim['oxMultiValuedAttribute'] and claim['name'] in cur_claims: + continue + if claim['name'] in ('memberOf', 'userPassword', 'uid', 'jansStatus'): + continue + claims_list.append((claim['name'], claim['displayName'])) + + claims_checkbox = CheckboxList(values=claims_list) + + def add_claim(dialog) -> None: + for claim_ in claims_checkbox.current_values: + for claim_prop in common_data.users.claims: + if claim_prop['name'] == claim_: + break + display_name = claim_prop['displayName'] + if claim_prop['dataType'] == 'boolean': + widget = self.app.getTitledCheckBox(_(display_name), name=claim_, style='class:script-checkbox', jans_help=self.app.get_help_from_schema(self.schema, claim_)) + else: + widget = self.app.getTitledText(_(display_name), name=claim_, value='', style='class:script-titledtext', jans_help=self.app.get_help_from_schema(self.schema, claim_)) + self.edit_user_content.insert(-1, widget) + self.edit_user_container = HSplit(self.edit_user_content, height=D(), width=D()) + + body = HSplit([Label(_("Select claim to be added to current user.")), claims_checkbox]) + buttons = [Button(_("Cancel")), Button(_("OK"), handler=add_claim)] + dialog = JansGDialog(self.app, title=_("Claims"), body=body, buttons=buttons, width=self.app.dialog_width-20) + self.app.show_jans_dialog(dialog) + + def __pt_container__(self)-> Dialog: + return self.dialog + diff --git a/jans-cli-tui/cli_tui/plugins/070_users/main.py b/jans-cli-tui/cli_tui/plugins/070_users/main.py new file mode 100755 index 00000000000..796c1c16ff2 --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/070_users/main.py @@ -0,0 +1,277 @@ +import os +import sys +import asyncio + +from functools import partial +from types import SimpleNamespace +from typing import Sequence, Any, Optional + +from prompt_toolkit.buffer import Buffer +from prompt_toolkit.application import Application +from prompt_toolkit.layout.containers import HSplit, VSplit, Window, DynamicContainer, HorizontalAlign +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.widgets import Button, Label, Frame, Dialog +from prompt_toolkit.formatted_text import HTML +from wui_components.jans_drop_down import DropDownWidget +from wui_components.jans_cli_dialog import JansGDialog +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 _ + +common_data.users = SimpleNamespace() + +class Plugin(DialogUtils): + """This is a general class for plugins + """ + def __init__( + self, + app: Application + ) -> None: + """init for Plugin class "users" + + Args: + app (_type_): _description_ + """ + self.app = app + self.pid = 'users' + self.name = '[U]sers' + self.users = {} + self.widgets_ready = False + + + + def process(self) -> None: + pass + + def on_page_enter(self) -> None: + """Function to perform preliminary tasks before this page entered. + """ + # we need claims everywhere + self.get_claims() + + def set_center_frame(self) -> None: + """center frame content + """ + + self.user_list_container = HSplit([],width=D()) + self.nav_buttons = VSplit([],width=D()) + self.app.center_container = HSplit([ + VSplit([ + self.app.getButton(text=_("Get Users"), name='oauth:scopes:get', jans_help=_("Retreive first {} users").format(self.app.entries_per_page), handler=self.get_users), + self.app.getTitledText(_("Search: "), name='oauth:scopes:search', jans_help=_("Press enter to perform search"), accept_handler=self.search_user, style='class:outh_containers_scopes.text'), + self.app.getButton(text=_("Add Users"), name='oauth:scopes:add', jans_help=_("To add a new user press this button"), handler=self.edit_user_dialog), + ], + padding=3, + width=D(), + ), + DynamicContainer(lambda: self.user_list_container), + DynamicContainer(lambda: self.nav_buttons), + ],style='class:outh_containers_scopes') + + + def update_user_list_container(self, pattern: Optional[str]='') -> None: + """User management list + """ + + data = [] + + for user in self.users.get('entries', []): + data.append((user.get('displayName', ''), user.get('userId',''), user.get('mail', ''))) + + self.users_list_box = JansVerticalNav( + myparent=self.app, + headers=['Name', 'User Name', 'Email'], + preferred_size= [20, 30 ,30], + data=data, + on_enter=self.edit_user_dialog, + on_display=self.app.data_display_dialog, + on_delete=self.delete_user, + #get_help=(self.get_help,'User'), + selectes=0, + headerColor='class:outh-verticalnav-headcolor', + entriesColor='class:outh-verticalnav-entriescolor', + all_data=self.users['entries'] + ) + + self.user_list_container = self.users_list_box + + buttons = [] + if self.users['start'] > 1: + handler_partial = partial(self.get_users, self.users['start']-self.app.entries_per_page+1, pattern) + prev_button = Button(_("Prev"), handler=handler_partial) + prev_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + buttons.append(prev_button) + if self.users['totalEntriesCount'] > self.users['start'] + self.users['entriesCount']: + handler_partial = partial(self.get_users, self.users['start']+self.app.entries_per_page+1, pattern) + next_button = Button(_("Next"), handler=handler_partial) + next_button.window.jans_help = _("Retreives previous %d entries") % self.app.entries_per_page + buttons.append(next_button) + + self.nav_buttons = VSplit(buttons, padding=5, align=HorizontalAlign.CENTER) + + self.app.invalidate() + + + def get_users(self, start_index: int=1, pattern: Optional[str]='') -> None: + """Gets Users from server. + """ + + endpoint_args ='limit:{},startIndex:{}'.format(self.app.entries_per_page, start_index) + if pattern: + endpoint_args += ',pattern:'+pattern + cli_args = {'operation_id': 'get-user', 'endpoint_args': endpoint_args} + + async def coroutine(): + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + self.users = response.json() + self.app.logger.debug("Users: {}".format(self.users)) + + if not self.users.get('entries'): + self.app.show_message(_("Not found"), _("No user found for this search."), tobefocused=self.app.center_container) + return + + if not self.widgets_ready: + self.update_user_list_container(pattern) + self.app.layout.focus(self.user_list_container) + + asyncio.ensure_future(coroutine()) + + + def edit_user_dialog(self, **kwargs: Any) -> None: + """Method to display the edit user dialog + """ + if kwargs: + data = kwargs.get('data', {}) + else: + data = {} + + title = _("Edit User") if data else _("Add User") + + edit_user_dialog = EditUserDialog(self.app, title=title, data=data, save_handler=self.save_user) + self.app.show_jans_dialog(edit_user_dialog) + + + def delete_user(self, **kwargs: Any) -> None: + + def do_delete_user(): + for user in self.users['entries']: + if user.get('userId') == kwargs['selected'][1]: + async def coroutine(): + cli_args = {'operation_id': 'delete-user', 'url_suffix':'inum:{}'.format(user['inum'])} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + if response: + self.app.show_message(_("Error"), _("Deletion was not completed {}".format(response)), tobefocused=self.user_list_container) + else: + self.users_list_box.remove_item(kwargs['selected']) + asyncio.ensure_future(coroutine()) + break + + buttons = [Button(_("No")), Button(_("Yes"), handler=do_delete_user)] + + self.app.show_message( + title=_("Confirm"), + message=_("Are you sure you want to delete user {}?").format(kwargs['selected'][1]), + buttons=buttons, + tobefocused=self.user_list_container + ) + + + def save_user(self, dialog: Dialog) -> None: + """This method to save user data to server + + Args: + dialog (_type_): the main dialog to save data in + + Returns: + _type_: bool value to check the status code response + """ + + raw_data = self.make_data_from_dialog(tabs={'user': dialog.edit_user_container}) + + if not (raw_data['userId'].strip() and raw_data['mail'].strip()): + self.app.show_message(_("Please fix!"), _("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")) + return + + user_info = {'customObjectClasses':['top', 'jansCustomPerson'], 'customAttributes':[]} + for key_ in ('mail', 'userId', 'displayName', 'givenName'): + user_info[key_] = raw_data.pop(key_) + + if 'baseDn' not in dialog.data: + user_info['userPassword'] = raw_data['userPassword'] + + for key_ in ('inum', 'baseDn', 'dn'): + if key_ in raw_data: + del raw_data[key_] + if key_ in dialog.data: + user_info[key_] = dialog.data[key_] + + status = raw_data.pop('active') + user_info['jansStatus'] = 'active' if status else 'inactive' + + for key_ in raw_data: + multi_valued = False + user_info['customAttributes'].append({ + 'name': key_, + 'multiValued': multi_valued, + 'values': [raw_data[key_]], + }) + + for ca in dialog.data.get('customAttributes', []): + if ca['name'] == 'memberOf': + user_info['customAttributes'].append(ca) + break + + if hasattr(dialog, 'admin_ui_roles_container'): + admin_ui_roles = [item[0] for item in dialog.admin_ui_roles_container.data] + if admin_ui_roles: + user_info['customAttributes'].append({ + 'name': 'jansAdminUIRole', + 'multiValued': len(admin_ui_roles) > 1, + 'values': admin_ui_roles, + }) + + async def coroutine(): + operation_id = 'put-user' if dialog.data.get('baseDn') else 'post-user' + cli_args = {'operation_id': operation_id, 'data': user_info} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + if response.status_code == 500: + self.app.show_message(_('Error'), response.text + '\n' + response.reason) + else: + dialog.future.set_result(DialogResult.OK) + self.get_users() + + asyncio.ensure_future(coroutine()) + + + def get_claims(self) -> None: + if hasattr(common_data.users, 'claims'): + return + async def coroutine(): + cli_args = {'operation_id': 'get-attributes', 'endpoint_args':'limit:200,status:active'} + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_requests, cli_args) + self.app.stop_progressing() + result = response.json() + common_data.users.claims = result['entries'] + + asyncio.ensure_future(coroutine()) + + def search_user(self, tbuffer:Buffer) -> None: + if not len(tbuffer.text) > 2: + self.app.show_message(_("Error!"), _("Search string should be at least three characters"), tobefocused=self.app.center_container) + return + self.get_users(pattern=tbuffer.text) + diff --git a/jans-cli-tui/cli_tui/plugins/999_jans/.enabled b/jans-cli-tui/cli_tui/plugins/999_jans/.enabled new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/999_jans/__init__.py b/jans-cli-tui/cli_tui/plugins/999_jans/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/plugins/999_jans/main.py b/jans-cli-tui/cli_tui/plugins/999_jans/main.py new file mode 100755 index 00000000000..fd74ef8009c --- /dev/null +++ b/jans-cli-tui/cli_tui/plugins/999_jans/main.py @@ -0,0 +1,84 @@ +import os +import sys +import asyncio + +from typing import Sequence + + +from prompt_toolkit.application import Application +from prompt_toolkit.layout.containers import HSplit, VSplit, Window, Float +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.widgets import Button, Label, Frame +from prompt_toolkit.formatted_text import HTML +from prompt_toolkit.widgets import Shadow +from prompt_toolkit.layout.controls import FormattedTextControl + + +from utils.multi_lang import _ +from cli import config_cli + + +class Plugin: + """This is a general class for plugins + """ + def __init__( + self, + app: Application + ) -> None: + """init for Plugin class "Jans CLI Menu" + + Args: + app (_type_): _description_ + """ + self.app = app + self.pid = 'jans-menu' + self.name = '[J]ans Cli' + + self.menu_container = Frame( + body=HSplit([ + Button(text=_("Exit Jans CLI"), handler=self.exit_cli), + Button(text=_("Logout and Exit Jans CLI"), handler=self.logout_exit_cli), + Button(text=_("Configure Jans CLI"), handler=self.configure_cli), + ], + width=D() + ), + height=D() + ) + + + def process(self) -> None: + pass + + def set_center_frame(self) -> None: + """center frame content + """ + + self.app.center_container = self.menu_container + + + def exit_cli(self) -> None: + """Exits + """ + self.app.exit(result=False) + + + def logout_exit_cli(self) -> None: + """Removes auth token and exits + """ + + async def coroutine(): + self.app.start_progressing() + response = await self.app.loop.run_in_executor(self.app.executor, self.app.cli_object.revoke_session) + self.app.stop_progressing() + + asyncio.ensure_future(coroutine()) + + del config_cli.config['DEFAULT']['access_token_enc'] + del config_cli.config['DEFAULT']['user_data'] + config_cli.write_config() + self.exit_cli() + + def configure_cli(self) -> None: + """Configures CLI creds + """ + self.app.jans_creds_dialog() diff --git a/jans-cli-tui/cli_tui/plugins/__init__.py b/jans-cli-tui/cli_tui/plugins/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/utils/__init__.py b/jans-cli-tui/cli_tui/utils/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/jans-cli-tui/cli_tui/utils/multi_lang.py b/jans-cli-tui/cli_tui/utils/multi_lang.py new file mode 100755 index 00000000000..7b67282f592 --- /dev/null +++ b/jans-cli-tui/cli_tui/utils/multi_lang.py @@ -0,0 +1,15 @@ +import locale +import gettext + +language = 'en' + +try: + current_locale, encoding = locale.getdefaultlocale() + language = gettext.translation (language, 'locale/', languages=[language] ) + language.install() +except: + pass + +_ = gettext.gettext + + diff --git a/jans-cli-tui/cli_tui/utils/static.py b/jans-cli-tui/cli_tui/utils/static.py new file mode 100755 index 00000000000..e331702ef8d --- /dev/null +++ b/jans-cli-tui/cli_tui/utils/static.py @@ -0,0 +1,6 @@ +from enum import Enum + +class DialogResult(Enum): + CANCEL = 0 + ACCEPT = 1 + OK = 2 diff --git a/jans-cli-tui/cli_tui/utils/utils.py b/jans-cli-tui/cli_tui/utils/utils.py new file mode 100755 index 00000000000..777362aa01b --- /dev/null +++ b/jans-cli-tui/cli_tui/utils/utils.py @@ -0,0 +1,75 @@ +from types import SimpleNamespace +from typing import Optional + +import prompt_toolkit + +from cli_style import style +from wui_components.jans_drop_down import DropDownWidget +from wui_components.jans_data_picker import DateSelectWidget +from wui_components.jans_spinner import Spinner + + +common_data = SimpleNamespace() + +class DialogUtils: + + + def get_item_data(self, item): + if hasattr(item, 'me'): + me = item.me + key_ = me.window.jans_name + if key_.startswith('__') and key_.endswith('__'): + return + if isinstance(me, prompt_toolkit.widgets.base.TextArea): + value_ = me.text + elif isinstance(me, prompt_toolkit.widgets.base.Checkbox): + value_ = me.checked + elif isinstance(me, prompt_toolkit.widgets.base.CheckboxList): + value_ = me.current_values + elif isinstance(me, prompt_toolkit.widgets.base.RadioList): + value_ = me.current_value + elif isinstance(me, DropDownWidget): + value_ = me.value + elif isinstance(me, DateSelectWidget): + value_ = me.value + elif isinstance(me, Spinner): + value_ = me.value + else: + return + + if getattr(me.window, 'text_type', None) == 'integer': + if value_: + value_ = int(value_) + + return {'key':key_, 'value':value_} + + + def make_data_from_dialog( + self, + tabs: Optional[dict]={} + ) -> dict: + + data = {} + process_tabs = tabs or self.tabs + + for tab in process_tabs: + for item in process_tabs[tab].children: + item_data = self.get_item_data(item) + if item_data: + data[item_data['key']] = item_data['value'] + + return data + + + def check_required_fields(self): + missing_fields = [] + for tab in self.tabs: + for item in self.tabs[tab].children: + if hasattr(item, 'children') and len(item.children)>1 and hasattr(item.children[1], 'jans_name'): + if 'required-field' in item.children[0].style and not self.data.get(item.children[1].jans_name, None): + missing_fields.append(item.children[0].content.text().strip().strip(':')) + if missing_fields: + self.myparent.show_message("Please fill required fields", "The following fields are required:\n" + ', '.join(missing_fields)) + return False + + return True diff --git a/jans-cli-tui/cli_tui/utils/validators.py b/jans-cli-tui/cli_tui/utils/validators.py new file mode 100644 index 00000000000..ee9d3b19655 --- /dev/null +++ b/jans-cli-tui/cli_tui/utils/validators.py @@ -0,0 +1,20 @@ +from prompt_toolkit.widgets import TextArea + +class IntegerValidator: + """A class for validating if entered text is integer for TextArea. + Example: + ta = TextArea() + ta.buffer.on_text_insert=IntegerValidator(ta) + """ + + def __init__(self, me: TextArea) -> None: + self.me = me + + def fire(self) -> None: + """This fucntion is called when user enters a character on TextArea + """ + cur_pos = self.me.buffer.cursor_position + for c in self.me.text: + if not c.isdigit(): + self.me.buffer._set_cursor_position(cur_pos-1) + self.me.buffer.delete(1) diff --git a/jans-cli-tui/cli_tui/version.py b/jans-cli-tui/cli_tui/version.py new file mode 100644 index 00000000000..80381fb7ede --- /dev/null +++ b/jans-cli-tui/cli_tui/version.py @@ -0,0 +1,6 @@ +""" + License terms and conditions for Janssen: + https://www.apache.org/licenses/LICENSE-2.0 +""" + +__version__ = "1.0.1-dev" diff --git a/jans-cli-tui/cli_tui/wui_components/__init__.py b/jans-cli-tui/cli_tui/wui_components/__init__.py new file mode 100644 index 00000000000..e69de29bb2d 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 new file mode 100755 index 00000000000..74210bba912 --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_cli_dialog.py @@ -0,0 +1,66 @@ +import json +from functools import partial +from asyncio import Future +from prompt_toolkit.widgets import Button, Dialog +from typing import Optional, Sequence, Union +from prompt_toolkit.layout.containers import ( + AnyContainer, +) +from prompt_toolkit.layout.dimension import AnyDimension +from prompt_toolkit.formatted_text import AnyFormattedText +from utils.multi_lang import _ + +class JansGDialog: + """This is the main dialog Class Widget for all Jans-cli-tui dialogs except custom dialogs like dialogs with navbar + """ + def __init__( + self, + parent, + body: AnyContainer, + title: Optional[str]= '', + buttons: Optional[Sequence[Button]]= [], + width: AnyDimension= None + )-> Dialog: + + """init for JansGDialog + + Args: + parent (widget): this is the parent widget for the dialog, to caluclate the size + title (String): the title for the dialog + body (Widget): The content of the dialog + buttons (list, optional): Dialog main buttons with their handlers. Defaults to []. + width (int, optional): If needed custom width. Defaults to None. + + Examples: + dialog = JansGDialog(self, title="Waiting Response", body=body) + """ + self.future = Future() + 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): + if handler: + handler(self) + self.future.set_result(button_text) + + for button in buttons: + button.handler = partial(do_handler, button.text, button.handler) + + self.dialog = Dialog( + title=title, + body=body, + buttons=buttons, + width=width, + modal=True, + with_background=True + ) + + def __pt_container__(self)-> Dialog: + return self.dialog diff --git a/jans-cli-tui/cli_tui/wui_components/jans_data_picker.py b/jans-cli-tui/cli_tui/wui_components/jans_data_picker.py new file mode 100755 index 00000000000..9123faa82d6 --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_data_picker.py @@ -0,0 +1,549 @@ + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout.containers import Float, HSplit, Window +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.formatted_text import HTML, merge_formatted_text +from prompt_toolkit.layout.margins import ScrollbarMargin +from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.containers import Float, FloatContainer, HSplit, Window, VSplit +from prompt_toolkit.widgets import Button, Label, TextArea +from prompt_toolkit.layout.containers import ( + ConditionalContainer, + Float, + HSplit, + VSplit, + VerticalAlign, + HorizontalAlign, + DynamicContainer, + FloatContainer, + Window +) +from prompt_toolkit.layout.containers import ( + AnyContainer, +) +from typing import Optional, Sequence, Union +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.widgets import Button, Dialog +from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase + +import calendar +import time +import datetime +import cli_style + +class JansSelectDate: + """_summary_ + """ + + def __init__( + self, + date: Optional[str] = '', + months: Optional[list] = [], + mytime: Optional[list] = [], + )-> HSplit: + + """_summary_ + + Args: + date (str, optional): _description_. Defaults to ''. + months (list, optional): _description_. Defaults to []. + mytime (list, optional): _description_. Defaults to []. + """ + self.hours , self.minuts , self.seconds = mytime + self.change_date = True + self.date = date #"11/27/2023" + self.months = months + self.cord_y = 0 + self.cord_x = 0 + self.old_cord_x = 0 + self.selected_cord = (0, 0) + self.extract_date(self.date) + + #self.depug=Label(text="entries = "+str(self.entries[self.cord_y][self.cord_x])+':',) + # -----------------------------------------------------------------------------------------------# + # ------------------------------------ handle month names ---------------------------------------# + # -----------------------------------------------------------------------------------------------# + if len(self.months[self.current_month-1] ) < 9 : ### 9 == the tallest month letters + mon_text = self.months[self.current_month-1] + ' '* (9-len(self.months[self.current_month-1] )) + else : + mon_text = self.months[self.current_month-1] + # -----------------------------------------------------------------------------------------------# + # -----------------------------------------------------------------------------------------------# + # -----------------------------------------------------------------------------------------------# + + self.month_label = Label(text=mon_text,width=len(mon_text)) + self.year_label = Label(text=str(self.current_year),width=len(str(self.current_year))) + + + self.container =HSplit(children=[ + VSplit([ + Button(text='<',left_symbol='',right_symbol='',width=1), + DynamicContainer(lambda: self.month_label), + Button(text='>',left_symbol='',right_symbol='',width=1), + Window(width=2,char=' '), + Button(text='<',left_symbol='',right_symbol='',width=1), + DynamicContainer(lambda: self.year_label), + Button(text='>',left_symbol='',right_symbol='',width=1 ) + ],style="class:date-picker-monthandyear",padding=1), ### Month and year window style + #DynamicContainer(lambda: self.depug), + + Window( + content=FormattedTextControl( + text=self._get_calender_text, + ), + height=5, + cursorline=False, + # width=D(), #15, + style="class:date-picker-day", ### days window style + right_margins=[ScrollbarMargin(display_arrows=True),], + wrap_lines=True, + + ), + Window( + content=FormattedTextControl( + text=self._get_time_text, + focusable=True, + ), + height=2, + cursorline=False, + # width=D(), #15, + style="class:date-picker-time", ### time window style + right_margins=[ScrollbarMargin(display_arrows=True),], + wrap_lines=True + ), + ]) + + def digitalize ( + self, + number:int + )-> str: + if len(str(number)) == 1 : + return '0'+ str(number) + else : + return str(number) + + def _get_time_text(self)-> AnyFormattedText: + + result = [] + time_line = [] + + hours = self.digitalize(self.hours) + minuts = self.digitalize(self.minuts) + seconds = self.digitalize(self.seconds) + + hours_list = [hours,minuts,seconds] + + time_line.append(HTML('<{}> H : M : S '.format(cli_style.date_picker_TimeTitle,cli_style.date_picker_TimeTitle))) + time_line.append("\n") + + for i, time_element in enumerate(hours_list): + if i >= 2: + space_, colon_ = 0, '' + elif i == 0 : + space_, colon_ = 7, ':' + else: + space_, colon_ = 0, ':' + + if i == self.cord_x and not self.change_date: + time_line.append(HTML('{}'.format(' '*space_,cli_style.date_picker_TimeSelected, self.adjust_sizes(time_element),cli_style.date_picker_TimeSelected, colon_))) + else : + time_line.append(HTML('{}<{}>{}{}'.format(' '*space_, cli_style.date_picker_Time ,self.adjust_sizes(time_element), cli_style.date_picker_Time, colon_))) + + result= (time_line) + + + return merge_formatted_text(result) + + def extract_date( + self, + date:str + )-> None: #"11/27/2023" + ### this function is for the init date >> passed date from `data` + + day = int(date.split('/')[1]) + self.current_month = int(date.split('/')[0]) + self.current_year = int(date.split('/')[-1]) + + month_week_list = calendar.monthcalendar(int(self.current_year), int(self.current_month)) + + self.entries = month_week_list + + dum_week = [] + for week in self.entries: + dum_week = week + try : + day_index = week.index(day) + break + except: + day_index = 0 + week_index = self.entries.index(dum_week) + self.cord_y = week_index + self.cord_x = day_index + self.selected_cord = (self.cord_x, self.cord_y) + + def adjust_sizes( + self, + day:str + )-> str: + if str(day) != '0': + if len(str(day)) <=1: + return ' '+str(day) + else : + return ' '+str(day) + else: + return ' ' + + def _get_calender_text(self)-> AnyFormattedText: + result = [] + week_line = [] + for i, week in enumerate(self.entries): + for day in range(len(week)): + if i == self.cord_y and day == self.cord_x and self.change_date: + week_line.append(HTML(''.format(cli_style.date_picker_calenderSelected,self.adjust_sizes(week[day]),cli_style.date_picker_calenderSelected))) + elif i == self.selected_cord[1] and day == self.selected_cord[0] and not self.change_date: + week_line.append(HTML('<{}>{}'.format(cli_style.date_picker_calender_prevSelected, self.adjust_sizes(week[day]), cli_style.date_picker_calender_prevSelected))) + else: + week_line.append(HTML('<{}>{}'.format(cli_style.date_picker_calenderNSelected, self.adjust_sizes(week[day]), cli_style.date_picker_calenderNSelected))) + + result= (week_line) + + result.append("\n") + + return merge_formatted_text(result) + + def adjust_month( + self, + day:int, + i:int, + )-> None: + if self.change_date: + if i == 1 and self.current_month == 12: + return + if i == -1 and self.current_month == 1: + return + + self.current_month += i + if len(self.months[self.current_month-1] ) < 9 : + mon_text = self.months[self.current_month-1] + ' '* (9-len(self.months[self.current_month-1] )) + else : + mon_text = self.months[self.current_month-1] + self.month_label = Label(text=mon_text,width=len(mon_text)) + current_date = str(self.current_month)+'/'+str(day) + '/'+str(self.current_year) + self.extract_date(current_date) + + def inc_month( + self, + day:int, + )-> None: + self.adjust_month(day, 1) + + def dec_month( + self, + day:int, + )-> None: + self.adjust_month(day, -1) + + def adjust_year( + self, + day:int, + i:int, + )-> None: + self.current_year += i + self.year_label = Label(text=str(self.current_year),width=len(str(self.current_year))) + current_date = str(day)+'/'+str(self.current_month) + '/'+str(self.current_year) + self.extract_date(current_date)# 20/2/1997 + + def inc_year( + self, + day:int, + )-> None: + self.adjust_year(day, 1) + + def dec_year( + self, + day:int + )-> None: + self.adjust_year(day, -1) + + def adjust_time( + self, + i:int, + )-> None: + if self.cord_x ==0 : + self.hours +=i + elif self.cord_x ==1 : + self.minuts +=i + elif self.cord_x ==2 : + self.seconds +=i + + self.hours = abs(self.hours % 24) + self.minuts = abs(self.minuts % 60) + self.seconds = abs(self.seconds % 60) + + def up(self)-> None: + if self.change_date: + if self.cord_y == 0 or int(self.entries[self.cord_y-1][self.cord_x]) == 0: + self.dec_month(day=1) + else: + self.cord_y = (self.cord_y - 1)# % 5 + #self.depug=Label(text="entries = "+str(self.entries[self.cord_y][self.cord_x])+':',) + self.selected_cord = (self.cord_x, self.cord_y) + else: + self.adjust_time(1) + + def down(self)-> None: + if self.change_date: + if self.cord_y == 4 or int(self.entries[self.cord_y+1][self.cord_x]) == 0: + self.inc_month(day=28) + else: + self.cord_y = (self.cord_y + 1)# % 5 + #self.depug=Label(text="entries = "+str(self.entries[self.cord_y][self.cord_x])+':',) + self.selected_cord = (self.cord_x, self.cord_y) + else: + self.adjust_time(-1) + + def right(self)-> None: + if self.change_date: + if self.cord_x == 6 or int(self.entries[self.cord_y][self.cord_x+1]) == 0: + self.inc_year(day=7) + else : + self.cord_x = (self.cord_x + 1) #% 7 + self.selected_cord = (self.cord_x, self.cord_y) + else: + if self.cord_x >= 2 : + pass + else : + self.cord_x = (self.cord_x + 1) #% 7 + + def left(self)-> None: + if self.change_date: + if self.cord_x == 0 or int(self.entries[self.cord_y][self.cord_x-1]) == 0: + self.dec_year(day=1) + else: + self.cord_x = (self.cord_x - 1)# % 7 + self.depug=Label(text="cord_y = "+str(self.cord_y)+':',) + self.selected_cord = (self.cord_x, self.cord_y) + self.date_changed = True + else: + if self.cord_x <=0 : + pass + else : + self.cord_x = (self.cord_x - 1) #% 7 + + def next(self)-> None: + self.change_date = not self.change_date + + if not self.change_date: + self.old_cord_x = self.cord_x + self.cord_x = 0 + else: + self.cord_x = self.old_cord_x + + + def __pt_container__(self)-> Dialog: + return self.container + +class DateSelectWidget: + """This is a Dape Picker widget to select exact time and date + """ + def __init__( + self, + parent, + value:str, + ) -> Window: + # ex: value = "2023-11-27T14:05:35" + """init for DateSelectWidget + + Args: + parent (widget): This is the parent widget for the dialog, to access `Pageup` and `Pagedown` + value (str): string time stamp value like "2023-11-27T14:05:35" + """ + self.parent = parent + self.months = [calendar.month_name[i] for i in range(1,13)] + + if value: + self.text = value + ts = time.strptime(value[:19], "%Y-%m-%dT%H:%M:%S") # "2023-11-27" + self.date = time.strftime("%m/%d/%Y", ts) # "11/27/2023" + self.hours = int(time.strftime("%H",ts)) + self.minuts =int(time.strftime("%M",ts)) + self.seconds = int(time.strftime("%S",ts)) + else: + today = datetime.date.today() + self.date = str(today.month) +'/' +str(today.day) +'/'+str(today.year) ## '11/27/2023' ## + self.text = "Enter to Select" + now = datetime.datetime.now() + self.hours = int(now.strftime("%H")) + self.minuts =int(now.strftime("%M")) + self.seconds = int(now.strftime("%S")) + + self.value = str(value) + + self.dropdown = True + self.window = Window( + content=FormattedTextControl( + text=self._get_text, + focusable=True, + key_bindings=self._get_key_bindings(), + ), height= 10) #D()) #5 ## large sized enties get >> (window too small) + + self.select_box = JansSelectDate(date=self.date,months=self.months,mytime=[self.hours,self.minuts,self.seconds] ) + self.select_box_float = Float(content=self.select_box, xcursor=True, ycursor=True) + + @property + def value(self): + """Getter for the value property + + Returns: + str: The selected value + """ + if self.text != "Enter to Select": + return self.text + + @value.setter + def value( + self, + value:str, + )-> None: + #passed_value = self.value + self._value = self.value + + def make_time( + self, + text:str, + )-> None: + """extract time from the text to increase or decrease + + Args: + text (str): the text that appear on the wigdet + """ + ts = time.strptime(text[:19], "%Y-%m-%dT%H:%M:%S") # "2023-11-27" + years =int(time.strftime("%Y",ts)) + months = int(time.strftime("%m",ts)) + days = int(time.strftime("%d",ts)) + self.hours = int(time.strftime("%H",ts)) -7 ## it start from 0 to 23 + self.minuts =int(time.strftime("%M",ts)) + self.seconds = int(time.strftime("%S",ts)) + + t = (years, months,days,self.hours,self.minuts,self.seconds,0,0,0) ## the up increment + t = time.mktime(t) + self.text= (time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(t))) + + def _get_text(self)-> AnyFormattedText: + """To get The selected value + + Returns: + str: The selected value + """ + + if get_app().layout.current_window is self.window: + return HTML('> <'.format('#00FF00', self.text)) + return '> {} <'.format(self.text) + + def _get_key_bindings(self)-> KeyBindingsBase: + """All key binding for the Dialog with Navigation bar + + Returns: + KeyBindings: The method according to the binding key + """ + + kb = KeyBindings() + + def _focus_next(event): + focus_next(event) + + def _focus_pre(event): + focus_previous(event) + + @kb.add("enter") + def _enter(event) -> None: + if self.select_box_float not in get_app().layout.container.floats: + get_app().layout.container.floats.append( self.select_box_float) + else: + years = int(self.select_box.current_year) + months =int(self.select_box.current_month) + days = int(self.select_box.entries[self.select_box.cord_y][self.select_box.cord_x] ) + + t = (years, months,days,(self.select_box.hours-8),self.select_box.minuts,self.select_box.seconds,0,0,0) ## the up increment + t = time.mktime(t) + self.text= (time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(t))) + get_app().layout.container.floats.remove(self.select_box_float) + + @kb.add("up") + def _up(event): + if self.select_box_float in get_app().layout.container.floats: + self.select_box.up() + + @kb.add("down") + def _down(event): + if self.select_box_float in get_app().layout.container.floats: + self.select_box.down() + + @kb.add("right") + def _right(event): + if self.select_box_float in get_app().layout.container.floats: + self.select_box.right() + + @kb.add("left") + def _left(event): + if self.select_box_float in get_app().layout.container.floats: + self.select_box.left() + + # @kb.add("+") + # def _plus(event): + # if self.select_box_float not in get_app().layout.container.floats: + # self.make_time(self.text) + + # @kb.add("-") + # def _minus(event): + # if self.select_box_float not in get_app().layout.container.floats: + # self.make_time(self.text) + + @kb.add("tab") + def _tab(event): + if self.select_box_float in get_app().layout.container.floats: + self.select_box.next() + else : + _focus_next(event) + + @kb.add("s-tab") + def _tab(event): + if self.select_box_float in get_app().layout.container.floats: + self.select_box.next() + else : + _focus_pre(event) + + + @kb.add("escape") + def _escape(event): + if self.select_box_float in get_app().layout.container.floats: + app = get_app() + app.layout.container.floats.remove(self.select_box_float) + + + @kb.add("pageup", eager=True) + def _pageup(event): + if self.select_box_float in get_app().layout.container.floats: + _escape(event) + self.parent.dialog.navbar.go_up() + else : + app = get_app() + self.parent.dialog.navbar.go_up() + app.layout.focus(self.parent.dialog.navbar) + + @kb.add("pagedown", eager=True) + def _pagedown(event): + if self.select_box_float in get_app().layout.container.floats: + _escape(event) + self.parent.dialog.navbar.go_down() + else : + app = get_app() + self.parent.dialog.navbar.go_down() + app.layout.focus(self.parent.dialog.navbar) + + return kb + + def __pt_container__(self)-> Window: + return self.window diff --git a/jans-cli-tui/cli_tui/wui_components/jans_dialog.py b/jans-cli-tui/cli_tui/wui_components/jans_dialog.py new file mode 100755 index 00000000000..2c5c220b12c --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_dialog.py @@ -0,0 +1,64 @@ +from prompt_toolkit.widgets import ( + Button, + Dialog, +) +from typing import Optional, Sequence, Union +from prompt_toolkit.layout.containers import ( + AnyContainer, +) +from prompt_toolkit.layout.dimension import AnyDimension +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.layout.containers import HSplit + + +class JansDialog(): + """NOt Used + """ + def __init__( + self, + only_view: Optional[bool] = False, + height: AnyDimension = None, + width: AnyDimension = None, + title: Optional[str]= '', + button_functions: Optional[list] = [], + entries_list: Optional[list] = [], + entries_color: str="#00ff44", + )-> Dialog: + + self.entries_list = entries_list + self.button_functions = button_functions + self.entries_color = entries_color + self.title = title + self.height = height + self.width =width + self.only_view=only_view + self.create_window() + + def create_window(self)-> None: + ### get max title len + max_title_str = self.entries_list[0][1] # list is not empty + for x in self.entries_list: + if len(x[1]) > len(max_title_str): + max_title_str = x[1] + + max_data_str = 41 ## TODO TO BE Dynamic + self.dialog = Dialog( + title=str(self.title), + body=HSplit( + [ + self.entries_list[i][0]for i in range(len(self.entries_list)) + ],height=self.height, + width= self.width + ), + buttons=[ + Button( + text=str(self.button_functions[k][1]), + handler=self.button_functions[k][0], + ) for k in range(len(self.button_functions)) + ], + with_background=False, + ) +#--------------------------------------------------------------------------------------# + def __pt_container__(self)-> Dialog: + return self.dialog + diff --git a/jans-cli-tui/cli_tui/wui_components/jans_dialog_with_nav.py b/jans-cli-tui/cli_tui/wui_components/jans_dialog_with_nav.py new file mode 100755 index 00000000000..3d3aafc1314 --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_dialog_with_nav.py @@ -0,0 +1,137 @@ +from shutil import get_terminal_size + + +from prompt_toolkit.layout.containers import ( + HSplit, + VSplit, + Window, +) +from prompt_toolkit.widgets import ( + Button, + Dialog, + VerticalLine, +) +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout import ScrollablePane +from prompt_toolkit.application.current import get_app +from typing import Optional, Sequence, Union +from prompt_toolkit.layout.containers import ( + AnyContainer, +) +from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase +from prompt_toolkit.layout.dimension import AnyDimension +from prompt_toolkit.formatted_text import AnyFormattedText + +class JansDialogWithNav(): + """This is a custom dialog Widget with side Navigation Bar (Used for Client/Scope dialogs) + """ + def __init__( + self, + content: AnyContainer, + width: AnyDimension, + navbar: AnyContainer=None, + height: AnyDimension= None, + title: Optional[str]= '', + button_functions: Optional[list] = [], + )-> Dialog: + + """init for JansDialogWithNav + + Args: + content (OrderedDict): All tabs content orderd in Dict + height (int, optional): Only if custom hieght is needed. Defaults to None. + width (int, optional): Only if custom width is needed. Defaults to None. + title (str, optional): The main title of the dialog. Defaults to None. + button_functions (list, optional): Dialog main buttons with their handlers. Defaults to []. + navbar (widget, optional): The Navigation bar widget can be Vertical (Side) Navigation bar or horizontal Navigation bar . Defaults to None. + + Examples: + self.dialog = JansDialogWithNav( + title=title, ## Dialog Title + navbar=self.side_nav_bar, ## Nav Bar widget + content=DynamicContainer(lambda: self.tabs[self.left_nav]), + button_functions=[ + (save, "Save"), ## Button Handler , Button Name + (cancel, "Cancel") ## Button Handler , Button Name + ], + height=10, ## Fixed Height + width=30, ## Fixed Width + ) + """ + self.navbar = navbar + self.button_functions = button_functions + self.title = title + self.height = height + self.width =width + self.content = content + self.create_window() + + def create_window(self)-> None: + """This method creat the dialog it self + Todo: + * Change `max_data_str` to be dynamic + """ + max_data_str = 30 ## TODO TO BE Dynamic + wwidth, wheight = get_terminal_size() + + height = 19 if wheight <= 30 else wheight - 11 + + self.dialog = Dialog( + title=self.title, + body=VSplit([ + VSplit([ + HSplit([ + self.navbar + ], width= (max_data_str )), + VerticalLine(), + + ]) if self.navbar else VSplit([]), + VSplit([ + ScrollablePane(content=self.content, height=height,display_arrows=False), + ],key_bindings=self.get_nav_bar_key_bindings()) + ], width=self.width, padding=1), + + buttons=[ + Button( + text=str(self.button_functions[k][1]), + handler=self.button_functions[k][0], + ) for k in range(len(self.button_functions)) + ], + with_background=False, + ) + + def get_nav_bar_key_bindings(self)-> KeyBindingsBase: + """All key binding for the Dialog with Navigation bar + + Returns: + KeyBindings: The method according to the binding key + """ + kb = KeyBindings() + + + @kb.add('pageup', eager=True) ### eager neglect any other keybinding + def _go_pageup(event) -> None: + if self.navbar : + app = get_app() + self.navbar.go_up() + app.layout.focus(self.navbar) + + @kb.add('pagedown', eager=True) + def _go_pagedown(event) -> None: + if self.navbar: + app = get_app() + self.navbar.go_down() + app.layout.focus(self.navbar) + + @kb.add('f2', eager=True) + def _go_up(event) -> None: + if self.button_functions: + for k in range(len(self.button_functions)): + if str(self.button_functions[k][1]).lower() == 'save': + self.button_functions[k][0]() + + return kb + + def __pt_container__(self)-> Dialog: + return self.dialog + 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 new file mode 100755 index 00000000000..4ef4406bdac --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_drop_down.py @@ -0,0 +1,269 @@ + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout.containers import Float, HSplit, Window +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.formatted_text import HTML, merge_formatted_text +from prompt_toolkit.layout.margins import ScrollbarMargin +from prompt_toolkit.key_binding.bindings.focus import focus_next, focus_previous +from prompt_toolkit.layout.dimension import D +from prompt_toolkit.layout.containers import ( + AnyContainer, +) +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase + +from prompt_toolkit.layout.dimension import AnyDimension +from typing import Optional, Sequence, Union +from typing import TypeVar, Callable + +import cli_style + +class JansSelectBox: + """_summary_ + """ + + def __init__( + self, + values: Optional[list] = [], + value: Optional[str] = '', + height: AnyDimension= 4, + rotatable_up: Optional[bool] = True, + rotatable_down: Optional[bool] = True, + ) -> HSplit: + """_summary_ + + Args: + values (list, optional): _description_. Defaults to []. + value (_type_, optional): _description_. Defaults to None. + height (int, optional): _description_. Defaults to 4. + rotatable_up (bool, optional): _description_. Defaults to True. + rotatable_down (bool, optional): _description_. Defaults to True. + """ + + self.values = values + self.values_flag = (values[0],values[-1]) + self.set_value(value) + # --------------------------------------------------- # + self.height = min(len(self.values), height) + self.rotatable_up = rotatable_up + self.rotatable_down = rotatable_down + # --------------------------------------------------- # + + self.container = HSplit(children=[Window( + content=FormattedTextControl( + text=self._get_formatted_text, + focusable=True, + ), + height=self.height, + cursorline=False, + width=D(), # 15, + style='bg:#4D4D4D', + right_margins=[ScrollbarMargin(display_arrows=True), ], + wrap_lines=True, + allow_scroll_beyond_bottom=True, + )]) + + def set_value( + self, + value:str + )-> None: + """_summary_ + + Args: + value (_type_): _description_ + """ + self.value = value + + for i, val in enumerate(self.values): + if val[0] == value: + self.selected_line = i + break + else: + self.selected_line = 0 + + def _get_formatted_text(self)-> AnyFormattedText: + """_summary_ + + Returns: + _type_: _description_ + """ + result = [] + for i, entry in enumerate(self.values): + if i == self.selected_line: + result.append( + HTML(''.format(cli_style.drop_down_itemSelect, entry[1]))) + else: + result.append(HTML('{}'.format(entry[1]))) + result.append("\n") + + return merge_formatted_text(result) + + def shift( + self, + seq:str, + n:int, + )-> str: + """_summary_ + + Args: + seq (_type_): _description_ + n (_type_): _description_ + + Returns: + _type_: _description_ + """ + return seq[n:]+seq[:n] + + def up(self)-> None: + """_summary_ + """ + if self.selected_line == 0: + if self.rotatable_up and self.values[self.selected_line] == self.values_flag[0]: + pass + else: + self.values = self.shift(self.values, -1) + else: + self.selected_line = (self.selected_line - 1) % (self.height) + + self.set_value(self.values[self.selected_line][0]) + + def down(self)-> None: + """_summary_ + """ + if self.selected_line + 1 == (self.height): + if self.rotatable_down and self.values[self.selected_line] == self.values_flag[-1]: + pass + else: + self.values = self.shift(self.values, 1) + else: + self.selected_line = (self.selected_line + 1) % (self.height) + + self.set_value(self.values[self.selected_line][0]) + + def __pt_container__(self)-> HSplit: + return self.container + + +class DropDownWidget: + """This is a Combobox widget (drop down) to select single from multi choices + """ + + def __init__( + self, + values: Optional[list] = [], + value: Optional[str] = '', + on_value_changed: Callable= None, + )->Window: + """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. + + Examples: + widget=DropDownWidget( + values=[('client_secret_basic', 'client_secret_basic'), ('client_secret_post', 'client_secret_post'), ('client_secret_jwt', 'client_secret_jwt'), ('private_key_jwt', 'private_key_jwt')], + value=self.data.get('tokenEndpointAuthMethodsSupported')) + """ + self.values = values + self.on_value_changed = on_value_changed + values.insert(0, (None, 'Select One')) + for val in values: + if val[0] == value: + self.text = val[1] + break + else: + self.text = self.values[0][1] if self.values else "Enter to Select" + + self.dropdown = True + self.window = Window( + content=FormattedTextControl( + text=self._get_text, + focusable=True, + key_bindings=self._get_key_bindings(), + ), height=D()) # 5 ## large sized enties get >> (window too small) + + self.select_box = JansSelectBox( + values=self.values, value=value, rotatable_down=True, rotatable_up=True, height=4) + self.select_box_float = Float( + content=self.select_box, xcursor=True, ycursor=True) + + @property + def value(self)-> str: + """Getter for the value property + + Returns: + str: The selected value + """ + return self.select_box.value + + @value.setter + def value( + self, + value:str, + )-> None: + self.select_box.set_value(value) + if self.on_value_changed: + self.on_value_changed(value) + + def _get_text(self)-> AnyFormattedText: + """To get The selected value + + Returns: + str: The selected value + """ + if get_app().layout.current_window is self.window: + return HTML('> <'.format(cli_style.drop_down_hover, self.text)) + return '> {} <'.format(self.text) + + def _get_key_bindings(self)-> KeyBindingsBase: + """All key binding for the Dialog with Navigation bar + + Returns: + KeyBindings: The method according to the binding key + """ + kb = KeyBindings() + + def _focus_next(event): + focus_next(event) + + def _focus_previous(event): + focus_previous(event) + + @kb.add("enter") + def _enter(event) -> None: + if self.select_box_float not in get_app().layout.container.floats: + get_app().layout.container.floats.append(self.select_box_float) + else: + self.text = self.select_box.values[self.select_box.selected_line][1] + get_app().layout.container.floats.remove(self.select_box_float) + + self.value = self.select_box.value + + @kb.add('up') + def _up(event): + self.select_box.up() + + @kb.add('down') + def _down(event): + self.select_box.down() + + @kb.add('escape') + @kb.add('tab') + def _(event): + if self.select_box_float in get_app().layout.container.floats: + get_app().layout.container.floats.remove(self.select_box_float) + + _focus_next(event) + + @kb.add('s-tab') + def _(event): + if self.select_box_float in get_app().layout.container.floats: + get_app().layout.container.floats.remove(self.select_box_float) + + _focus_previous(event) + + return kb + + def __pt_container__(self)-> Window: + return self.window diff --git a/jans-cli-tui/cli_tui/wui_components/jans_message_dialog.py b/jans-cli-tui/cli_tui/wui_components/jans_message_dialog.py new file mode 100755 index 00000000000..fb2bf5e62a1 --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_message_dialog.py @@ -0,0 +1,76 @@ +from functools import partial + +from prompt_toolkit.widgets import Button, Dialog, Label +from prompt_toolkit.application.current import get_app +from prompt_toolkit.layout.dimension import D +from typing import Optional, Sequence, Union +from prompt_toolkit.layout.containers import ( + AnyContainer, +) +from prompt_toolkit.layout.dimension import AnyDimension +from prompt_toolkit.formatted_text import AnyFormattedText +from utils.multi_lang import _ + +class JansMessageDialog: + """This is a Dialog to show Message + """ + def __init__( + self, + title: AnyFormattedText, + body: AnyContainer, + buttons:Optional[Sequence[Button]] = [], + focus_on_exit:AnyContainer =None + )-> Dialog: + """init for JansMessageDialog + + Args: + title (str): The title of the Dialog message + body (widget): Widget to be displayed with the dialog (Usually Label) + buttons (list, optional): Dialog main buttons with their handlers. Defaults to []. + focus_on_exit (widget, optional): Move the focus on exit. Defaults to None. + + Examples: + buttons = [Button("OK", handler=my_method] + dialog = JansMessageDialog(title="my title", body=HSplit([Label(message)]), buttons=buttons) + """ + self.result = None + self.me = None + self.focus_on_exit = focus_on_exit + if not buttons: + buttons = [_("OK")] + + def exit_me(result, handler): + if handler: + handler() + self.result = result + app = get_app() + + if self.me in app.root_layout.floats: + app.root_layout.floats.remove(self.me) + + try: + app.layout.focus(self.focus_on_exit) + except: + pass + + blist = [] + + for button in buttons: + if isinstance(button, str): + button = Button(text=button) + button.handler = partial(exit_me, button.text, button.handler) + blist.append(button) + + self.dialog = Dialog( + title=title, + body=body, + buttons=blist, + width=D(preferred=80), + modal=True, + ) + + app = get_app() + app.layout.focus(self.dialog) + + def __pt_container__(self)-> Dialog: + return self.dialog diff --git a/jans-cli-tui/cli_tui/wui_components/jans_nav_bar.py b/jans-cli-tui/cli_tui/wui_components/jans_nav_bar.py new file mode 100755 index 00000000000..04e21b7f0f4 --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_nav_bar.py @@ -0,0 +1,174 @@ +import re +import os + +from typing import TypeVar, Callable, Optional, Sequence, Union + +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase +from prompt_toolkit.layout.containers import Window +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.formatted_text import HTML, merge_formatted_text + + +import cli_style + + +shortcut_re = re.compile(r'\[(.*?)\]') + +class JansNavBar(): + """This is a horizontal Navigation bar Widget used in Main screen ('clients', 'scopes', 'keys', 'defaults', 'properties', 'logging') + """ + def __init__( + self, + myparent, + entries: list, + selection_changed: Callable, + select: int= 0, + jans_name: Optional[str] = '', + last_to_right: Optional[bool] = False, + ) -> Window: + + """init for JansNavBar + + Args: + myparent (widget): This is the parent widget for the dialog, to caluclate the size + entries (_type_): _description_ + selection_changed (_type_): _description_ + select (int, optional): _description_. Defaults to 0. + bgcolor (str, optional): _description_. Defaults to '#00ff44'. + last_to_right (bool, optional): move last item to rightmost. + Examples: + self.oauth_navbar = JansNavBar( + self, + entries=[('clients', 'Clients'), ('scopes', 'Scopes'), ('keys', 'Keys'), ('defaults', 'Defaults'), ('properties', 'Properties'), ('logging', 'Logging')], + selection_changed=self.oauth_nav_selection_changed, + select=0, + bgcolor='#66d9ef' + ) + """ + self.myparent = myparent + self.navbar_entries = entries + self.cur_navbar_selection = select + self.selection_changed = selection_changed + self.jans_name = jans_name + self.last_to_right = last_to_right + self.cur_tab = entries[self.cur_navbar_selection][0] + self.create_window() + + def create_window(self)-> None: + """This method creat the Navigation Bar it self + """ + self.nav_window = Window( + content=FormattedTextControl( + text=self.get_navbar_entries, + focusable=True, + key_bindings=self.get_nav_bar_key_bindings(), + ), + height=1, + cursorline=False, + ) + + + def _set_tab_for_view(self, view, ev): + for i, entry in enumerate(view.navbar_entries): + re_search = shortcut_re.search(entry[1]) + if re_search and re_search.group(1).lower() == ev.data: + view.cur_navbar_selection = i + try: + self.myparent.layout.focus(view.nav_window) + except: + pass + view._set_selection() + return True + + + + def _go_tab(self, ev)-> None: + + if self.myparent.layout.container.floats: + return + # first set main navbar tab + if not self._set_tab_for_view(self.myparent.nav_bar, ev): + # then set sub navbar + cur_plugin = self.myparent.nav_bar.cur_navbar_selection + try: ## i couldnt access the plugin content from here + cur_view = self.myparent._plugins[cur_plugin].nav_bar + self._set_tab_for_view(cur_view, ev) + except: + pass + + def add_key_binding( + self, + shorcut_key:str, + )-> None: + r = os.urandom(3).hex() + for binding in self.myparent.bindings.bindings: + if len(binding.keys) == 2 and binding.keys[0].value == 'escape' and binding.keys[1].lower() == shorcut_key: + return + self.myparent.bindings.add('escape', shorcut_key.lower())(self._go_tab) + + + def get_navbar_entries(self)-> AnyFormattedText: + """Get all selective entries + + Returns: + merge_formatted_text: Merge (Concatenate) several pieces of formatted text together. + """ + + result = [] + nitems = len(self.navbar_entries) + total_text_lenght = 0 + + for i, entry in enumerate(self.navbar_entries): + display_text = entry[1] + re_search = shortcut_re.search(display_text) + if re_search: + sc, ec = re_search.span() + shorcut_key = re_search.group(1) + display_text = display_text[:sc]+ '' +display_text[ec:] + self.add_key_binding(shorcut_key.lower()) + + total_text_lenght += len(entry[1].replace('[','').replace(']','')) + if i == self.cur_navbar_selection: + result.append(HTML(''.format(cli_style.sub_navbar_selected_bgcolor, cli_style.sub_navbar_selected_fgcolor, display_text))) + else: + result.append(HTML('{}'.format(display_text))) + if self.last_to_right and i+2 == nitems: + screen_width = self.myparent.output.get_size().columns + remaining_space = (screen_width - total_text_lenght - len(self.navbar_entries[-1][1].replace('[','').replace(']','')) - 2) + sep_space = ' ' * remaining_space + else: + sep_space = ' ' + total_text_lenght += len(sep_space) + result.append(sep_space) + + return merge_formatted_text(result) + + + def _set_selection(self)-> None: + + if self.selection_changed: + self.selection_changed(self.navbar_entries[self.cur_navbar_selection][0]) + + def get_nav_bar_key_bindings(self)-> KeyBindingsBase: + """All key binding for the Dialog with Navigation bar + + Returns: + KeyBindings: The method according to the binding key + """ + kb = KeyBindings() + + @kb.add('left') + def _go_up(event) -> None: + self.cur_navbar_selection = (self.cur_navbar_selection - 1) % len(self.navbar_entries) + self._set_selection() + + @kb.add('right') + def _go_up(event) -> None: + self.cur_navbar_selection = (self.cur_navbar_selection + 1) % len(self.navbar_entries) + self._set_selection() + + + + return kb diff --git a/jans-cli-tui/cli_tui/wui_components/jans_side_nav_bar.py b/jans-cli-tui/cli_tui/wui_components/jans_side_nav_bar.py new file mode 100755 index 00000000000..c5918fcf591 --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_side_nav_bar.py @@ -0,0 +1,145 @@ +from prompt_toolkit.layout.containers import ( + HSplit, + Window, + FloatContainer, + Dimension +) +from prompt_toolkit.formatted_text import merge_formatted_text +from prompt_toolkit.layout.controls import FormattedTextControl +from prompt_toolkit.key_binding import KeyBindings +from typing import TypeVar, Callable +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase + +class JansSideNavBar(): + """This is a Vertical Navigation bar Widget with one value used in clients/scopes dialogs + """ + def __init__( + self, + entries: list, + selection_changed: Callable, + save_handler: Callable= None, + myparent=None, + select: int= 0, + entries_color: str= "#00ff44", + ) -> FloatContainer: + """init for JansSideNavBar + + Args: + parent (widget): This is the parent widget for the dialog, to caluclate the size + entries (list): List of all navigation headers names + selection_changed (method): Method to be invoked when selection is changed + select (int, optional): The first value to be selected. Defaults to 0. + entries_color (str, optional): Color for entries. Defaults to '#00ff44'. + + Examples: + self.side_nav_bar = JansSideNavBar(myparent=self.myparent, + entries=list(self.tabs.keys()), + selection_changed=(self.client_dialog_nav_selection_changed) , + select=0, + entries_color='#2600ff') + """ + if myparent : + self.myparent = myparent # ListBox parent class + self.navbar_entries = entries # ListBox entries + self.cur_navbar_selection = select # ListBox initial selection + self.entries_color = entries_color + self.save_handler=save_handler + self.cur_tab = entries[self.cur_navbar_selection][0] + self.selection_changed = selection_changed + self.create_window() + + def create_window(self)-> None: + """This method creat the dialog it self + Todo: + * Change `width` to be dynamic + """ + self.side_nav = FloatContainer( + content=HSplit([ + Window( + content=FormattedTextControl( + text=self.get_navbar_entries, + focusable=True, + key_bindings=self.get_nav_bar_key_bindings(), + style=self.entries_color, + + ), + style='class:select-box', + height=Dimension(preferred=len( + self.navbar_entries)*2, max=len(self.navbar_entries)*2+1), + cursorline=True, + width= self.get_data_width() + ), + ] + ), floats=[] + ) + + def get_data_width(self)-> int: + """get the largest title lenght + + Returns: + int: the max title lenght + """ + return len(max(self.navbar_entries, key=len)) + + def get_navbar_entries(self)-> AnyFormattedText: + """Get all selective entries + + Returns: + merge_formatted_text: Merge (Concatenate) several pieces of formatted text together. + """ + + result = [] + for i, entry in enumerate(self.navbar_entries): + if i == self.cur_navbar_selection: + result.append([('[SetCursorPosition]', '')]) + result.append(entry) + result.append('\n') + result.append('\n') + return merge_formatted_text(result) + + def update_selection(self)-> None: + """Update the selected tab and pass the current tab name to the selection_changed handler + """ + self.cur_tab = self.navbar_entries[self.cur_navbar_selection] + self.selection_changed(self.cur_tab) + + def go_up(self)-> None: + self.cur_navbar_selection = ( + self.cur_navbar_selection - 1) % len(self.navbar_entries) + self.update_selection() + + def go_down(self)-> None: + self.cur_navbar_selection = ( + self.cur_navbar_selection + 1) % len(self.navbar_entries) + self.update_selection() + + def get_nav_bar_key_bindings(self)-> KeyBindingsBase: + """All key binding for the Dialog with Navigation bar + + Returns: + KeyBindings: The method according to the binding key + """ + kb = KeyBindings() + + @kb.add('up') + def _go_up(event) -> None: + self.cur_navbar_selection = ( + self.cur_navbar_selection - 1) % len(self.navbar_entries) + self.update_selection() + + @kb.add('down') + def _go_down(event) -> None: + self.cur_navbar_selection = ( + self.cur_navbar_selection + 1) % len(self.navbar_entries) + self.update_selection() + + @kb.add('f2', eager=True) + def _go_up(event) -> None: + if self.save_handler: + self.save_handler() + + return kb + + def __pt_container__(self)-> FloatContainer: + return self.side_nav diff --git a/jans-cli-tui/cli_tui/wui_components/jans_spinner.py b/jans-cli-tui/cli_tui/wui_components/jans_spinner.py new file mode 100644 index 00000000000..455f4a262db --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_spinner.py @@ -0,0 +1,51 @@ +from prompt_toolkit.formatted_text import merge_formatted_text +from prompt_toolkit.key_binding import KeyBindings +from prompt_toolkit.layout import FormattedTextControl, Window +from typing import Optional, Sequence, Union +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase +class Spinner: + def __init__(self, + value: Optional[int]= 1, + min_value: Optional[int]= 1, + max_value: Optional[int]= 100, + style: Optional[str]= 'bg:#cccccc fg:blue', + ) -> Window: + + self.value = value + self.min_value = min_value + self.max_value = max_value + self.window = Window( + content=FormattedTextControl( + text=self._get_formatted_text, + focusable=True, + key_bindings=self._get_key_bindings(), + ), + style=style, + height=1, + width=len(str(self.max_value))+3, + cursorline=False, + ) + + def _get_formatted_text(self) -> AnyFormattedText: + spacing = len(str(self.max_value))+1 + result = [str(self.value).rjust(spacing) + ' ↕'] + return merge_formatted_text(result) + + def _get_key_bindings(self) -> KeyBindingsBase: + kb = KeyBindings() + + @kb.add("up") + def _go_left(event) -> None: + if self.value > self.min_value: + self.value -= 1 + + @kb.add("down") + def _go_right(event) -> None: + if self.value < self.max_value: + self.value += 1 + + return kb + + def __pt_container__(self) -> Window: + return self.window 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 new file mode 100644 index 00000000000..191a137055c --- /dev/null +++ b/jans-cli-tui/cli_tui/wui_components/jans_vetrical_nav.py @@ -0,0 +1,322 @@ +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 typing import Tuple, TypeVar, Callable +from prompt_toolkit.layout.dimension import AnyDimension +from typing import Optional, Sequence, Union +from prompt_toolkit.formatted_text import AnyFormattedText +from prompt_toolkit.key_binding.key_bindings import KeyBindings, KeyBindingsBase + +class JansVerticalNav(): + """This is a Vertical Navigation bar Widget with many values used in / + """ + def __init__( + self, + myparent, + headers: list, + on_display: Callable= None, + selectes: Optional[int]= 0, + on_enter: Callable= None, + get_help: Tuple= None, + on_delete: Callable= None, + all_data: Optional[list]= [], + preferred_size: Optional[list]= [], + data: Optional[list]= [], + headerColor: Optional[str]= "green", + entriesColor: Optional[str]= "white", + underline_headings: Optional[bool]= True, + max_width: AnyDimension = None, + jans_name: Optional[str]= '', + max_height: AnyDimension = None, + )->FloatContainer : + """init for JansVerticalNav + + Args: + parent (widget): This is the parent widget for the dialog, to caluclate the size + headers (List): List of all navigation headers names + selectes (int): The first value to be selected. + on_enter (Method): Method to be called when '' key is pressed + on_display (Method): Method to be called when '' key is pressed + on_delete (Method, optional): Method to be called when '' key is pressed. Defaults to None. + all_data (List, optional): All Data to be used with `on_enter` and `on_display`. Defaults to None. + preferred_size (list, optional): List of the max desired width for the columns contents. Defaults to []. + data (List, optional): Data to be displayed. Defaults to None. + headerColor (str, optional): Color for the Headers. Defaults to 'green'. + entriesColor (str, optional): Color for the Entries. Defaults to 'white'. + underline_headings (str, optional): Put a line under headings. + max_width (int, optional): Maximum width of container. + jans_name (str, optional): Widget name + max_height (int, optional): Maximum hegight of container + + Examples: + clients = JansVerticalNav( + myparent=self, + headers=['Client ID', 'Client Name', 'Grant Types', 'Subject Type'], + preferred_size= [0,0,30,0], + data=data, + on_enter=self.edit_client_dialog, + on_display=self.data_display_dialog, + on_delete=self.delete_client, + # selection_changed=self.data_selection_changed, + selectes=0, + headerColor='green', + entriesColor='white', + all_data=result + ) + Todo: + * Needs refactor + """ + self.myparent = myparent # ListBox parent class + self.headers = headers # ListBox headers + self.selectes = selectes # ListBox initial selection + self.max_width = max_width + self.data = data # ListBox Data (Can be renderable ?!!! #TODO ) + self.jans_name = jans_name + self.preferred_size = preferred_size + self.headerColor = headerColor + self.entriesColor = entriesColor + self.max_height = max_height + + self.on_enter = on_enter + self.on_delete = on_delete + self.on_display = on_display + 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 + + self.handle_header_spaces() + self.create_window() + + + def view_data( + self, + data:list + ) -> list: + result = [] + for i, entry in enumerate(data): ## entry = ['1800.6c5faa', 'Jans Config Api Client', 'authorization_code,refresh_...', 'Reference] + mod_entry = [] + for col in range(len(entry)) : + if self.preferred_size[col] == 0: + mod_entry.append(entry[col]) + else : + if self.preferred_size[col] >= len(str(entry[col])): + mod_entry.append(entry[col]) + else : + mod_entry.append(entry[col][:self.preferred_size[col]]+'...') + + result.append(mod_entry) + + return result + + def create_window(self) -> None: + """This method creat the dialog it self + """ + self.container_content = [ + Window( + content=FormattedTextControl( + text=self._get_head_text, + focusable=False, + key_bindings=self._get_key_bindings(), + style=self.headerColor, + ), + style='class:select-box', + height=D(preferred=1, max=1), + cursorline=False, + ), + Window( + content=FormattedTextControl( + text=self._get_formatted_text, + focusable=True, + key_bindings=self._get_key_bindings(), + style=self.entriesColor, + ), + style='class:select-box', + height=D(preferred=len(self.data), max=len(self.data)), + cursorline=True, + right_margins=[ScrollbarMargin(display_arrows=True), ], + ), + ] + + 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=[], + ) + + def handle_header_spaces(self) -> None: + """Make header evenlly spaced + """ + + data = self.view_data(self.data) + self.spaces = [] + data_length_list = [] + for row in data: + column_length_list = [] + for col in row: + column_length_list.append(len(str(col))) + data_length_list.append(column_length_list) + + + if data_length_list: + tmp_dict = {} + for num in range(len(data_length_list[0])): + tmp_dict[num] = [] + + for k in range(len(data_length_list)): + for i in range(len(data_length_list[k])): + tmp_dict[i].append(data_length_list[k][i]) + + for i in tmp_dict: + self.spaces.append(max(tmp_dict[i])) + + for i, space in enumerate(self.spaces): + if space < len(self.headers[i]): + self.spaces[i] = space + (len(self.headers[i]) - space) + else: + self.spaces = [len(header) + 2 for header in self.headers] + + self.spaces[-1] = self.myparent.output.get_size()[1] - sum(self.spaces) + sum(len(s) for s in self.headers) ## handle last head spaces (add space to the end of ter. width to remove the white line) + + def get_spaced_data(self) -> list: + """Make entries evenlly spaced + """ + data = self.view_data(self.data) + + spaced_data = [] + for d in data: + spaced_line_list = [] + for i, space in enumerate(self.spaces): + spaced_line_list.append(str(d[i]) + ' ' * (space - len(str(d[i])))) + spaced_data.append(spaced_line_list) + + return spaced_data + + def _get_head_text(self) -> AnyFormattedText: + """Get all headers entries + + Returns: + merge_formatted_text: Merge (Concatenate) several pieces of formatted text together. + """ + + result = [] + y = '' + for k in range(len(self.headers)): + y += self.headers[k] + ' ' * \ + (self.spaces[k] - len(self.headers[k]) + 3) + result.append(y) + + return merge_formatted_text(result) + + def _get_formatted_text(self) -> AnyFormattedText: + """Get all selective entries + + Returns: + merge_formatted_text: Merge (Concatenate) several pieces of formatted text together. + """ + + result = [] + spaced_data = self.get_spaced_data() + for i, entry in enumerate(spaced_data): ## entry = ['1800.6c5faa', 'Jans Config Api Client', 'authorization_code,refresh_...', 'Reference] + if i == self.selectes: + result.append([('[SetCursorPosition]', '')]) + + result.append(' '.join(entry)) + result.append('\n') + + return merge_formatted_text(result) + + + def remove_item( + self, + item: list, + ) -> None: + self.data.remove(item) + self.handle_header_spaces() + if self.max_height: + self.container_content[-1].height = self.max_height if self.max_height else len(self.data) + + def add_item( + self, + item: list, + ) -> None: + self.data.append(item) + self.handle_header_spaces() + self.container_content[-1].height = self.max_height if self.max_height else len(self.data) + + def replace_item( + self, + item_index: int, + item: list, + ) -> None: + self.data[item_index] = item + self.handle_header_spaces() + + def _get_key_bindings(self) -> KeyBindingsBase: + """All key binding for the Dialog with Navigation bar + + Returns: + KeyBindings: The method according to the binding key + """ + kb = KeyBindings() + + @kb.add('up') + def _go_up(event) -> None: + if not self.data: + return + self.selectes = (self.selectes - 1) % len(self.data) + + if self.get_help : + self.get_help(data=self.data[self.selectes],scheme=self.scheme) + + @kb.add('down') + def _go_up(event) -> None: + if not self.data: + return + self.selectes = (self.selectes + 1) % len(self.data) + if self.get_help : + self.get_help(data=self.data[self.selectes],scheme=self.scheme) + + @kb.add('enter') + def _(event): + if not self.data: + return + size = self.myparent.output.get_size() + if self.on_enter : + self.on_enter(passed=self.data[self.selectes], event=event, size=size, data=self.all_data[self.selectes], selected=self.selectes, jans_name=self.jans_name) + + + @kb.add('d') + def _(event): + if not self.data: + return + + size = self.myparent.output.get_size() + self.on_display( + selected=self.data[self.selectes], + headers=self.headers, + event=event, + size=size, + data=self.all_data[self.selectes]) + + + @kb.add('delete') + 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) + + return kb + + def __pt_container__(self) ->FloatContainer: + return self.container diff --git a/jans-cli-tui/docs/build.md b/jans-cli-tui/docs/build.md new file mode 100644 index 00000000000..17aa6076052 --- /dev/null +++ b/jans-cli-tui/docs/build.md @@ -0,0 +1,23 @@ +#### Build +``` +pip3 install shiv +wget https://github.com/JanssenProject/jans/archive/refs/heads/jans-cli-tui-works.zip +unzip jans-cli-tui-works.zip +cd jans-jans-cli-tui-works/jans-cli-tui/ +make zipapp +``` + +### Execute + +``` +./config-cli-tui.pyz +``` + +It will ask credentials unless you have no ~/.config/jans-cli.ini. Login to Jans server and get +credentials: +``` +cat /opt/jans/jans-setup/setup.properties.last | grep role +role_based_client_encoded_pw=4jnkODv3KRV6xNm1oGQ8+g\=\= +role_based_client_id=2000.eac308d1-95e3-4e38-87cf-1532af310a9e +role_based_client_pw=GnEkCqg4Vsks +``` \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/Gallery/cli.md b/jans-cli-tui/docs/docs/Gallery/cli.md new file mode 100755 index 00000000000..6281a210e0f --- /dev/null +++ b/jans-cli-tui/docs/docs/Gallery/cli.md @@ -0,0 +1,12 @@ +# CLI Gallarey + +
+ ![device Authorization](https://github.com/JanssenProject/jans/blob/main/docs/assets/device-flow-4.png?raw=true){align=center} +
device Authorization
+
+ +
+ ![device Authorization 2](https://github.com/JanssenProject/jans/blob/main/docs/assets/device-flow-2.png?raw=true){align=center} +
device Authorization 2
+
+ diff --git a/jans-cli-tui/docs/docs/Gallery/gallery.md b/jans-cli-tui/docs/docs/Gallery/gallery.md new file mode 100755 index 00000000000..d8f880993df --- /dev/null +++ b/jans-cli-tui/docs/docs/Gallery/gallery.md @@ -0,0 +1,26 @@ +# Gallarey +gallery for all possibilities and future improvment for TUI + +
+ ![Image title](https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_transparent_630px_182px.png){align=center} +
Janssen
+
+ +
+ ![jans-cli-interactive-mode](https://raw.githubusercontent.com/JanssenProject/jans/main/docs/assets/gif-jans-cli-interactive-mode-04232022.gif) +
jans-cli-interactive-mode
+
+ + +
+ ![auth-cli-main-menu](https://github.com/JanssenProject/jans/blob/main/docs/assets/image-howto-mod-auth-cli-main-menu-04292022.png?raw=true) +
auth-cli-main-menu
+
+ +
+ ![using-jans-cli-comp](https://github.com/JanssenProject/jans/blob/main/docs/assets/image-using-jans-cli-comp-04222022.png?raw=true) +
using-jans-cli-comp
+
+ +- [TUI](/gluu-4/docs/Gallery/tui/) +- [CLI](/gluu-4/docs/Gallery/cli/) diff --git a/jans-cli-tui/docs/docs/Gallery/tui.md b/jans-cli-tui/docs/docs/Gallery/tui.md new file mode 100755 index 00000000000..349cd0f2cc0 --- /dev/null +++ b/jans-cli-tui/docs/docs/Gallery/tui.md @@ -0,0 +1 @@ +### demonstrating the possibilities of jans-tui diff --git a/jans-cli-tui/docs/docs/getting_started/installation/dynamic-download.md b/jans-cli-tui/docs/docs/getting_started/installation/dynamic-download.md new file mode 100644 index 00000000000..ec9bce78211 --- /dev/null +++ b/jans-cli-tui/docs/docs/getting_started/installation/dynamic-download.md @@ -0,0 +1,29 @@ +# Install Janssen Server using Dynamic Download + +Dynamic download installs the latest development version of Janssen Server. This installation method is suitable for setting up development environments. + +## System Requirements + +System should meet [minimum VM system requirements](vm-requirements.md) + +## Supported Linux Distributions +- Enterprise Linux 8 (CentOS 8 and Red Hat 8) +- Ubuntu 20 +- SUSE 15 + +## Installation Steps + +1. Download installer +``` +curl https://raw.githubusercontent.com/JanssenProject/jans/main/jans-linux-setup/jans_setup/install.py > install.py +``` + +2. Execute installer +``` +python3 install.py +``` + +3. Uninstalling Janssen Server +``` +python3 install.py -uninstall +``` \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/getting_started/installation/rhel.md b/jans-cli-tui/docs/docs/getting_started/installation/rhel.md new file mode 100644 index 00000000000..a5a1b2e2c28 --- /dev/null +++ b/jans-cli-tui/docs/docs/getting_started/installation/rhel.md @@ -0,0 +1,30 @@ +# Install Janssen Server using Enterprise Linux Package + +## Supported versions +- Red Hat 8 +- CentOS 8 + +!!! note + SELinux should be disabled + +## System Requirements + +System should meet [minimum VM system requirements](vm-requirements.md) + +## Installation Steps + +``` +yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm +``` +``` +yum module enable mod_auth_openidc +``` +``` +yum install curl +``` +``` +yum install -y https:$(curl -s -L https://api.github.com/repos/JanssenProject/jans/releases/latest | egrep -o '/.*el8.x86_64.rpm' | head -n 1) +``` +``` +sudo python3 /opt/jans/jans-setup/setup.py +``` \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/getting_started/installation/suse.md b/jans-cli-tui/docs/docs/getting_started/installation/suse.md new file mode 100644 index 00000000000..3aa8157129d --- /dev/null +++ b/jans-cli-tui/docs/docs/getting_started/installation/suse.md @@ -0,0 +1,23 @@ +# Install Janssen Server using SUSE Linux Package + +## Supported versions +- SUSE 15 + +!!! note + SELinux should be disabled + +## System Requirements + +System should meet [minimum VM system requirements](vm-requirements.md) + +## Installation Steps + +``` +zypper install curl +``` +``` +zypper --no-gpg-checks install -y https:$(curl -s -L https://api.github.com/repos/JanssenProject/jans/releases/latest | egrep -o '/.*suse15.x86_64.rpm' | head -n 1) +``` +``` +sudo python3 /opt/jans/jans-setup/setup.py +``` \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/getting_started/installation/ubuntu.md b/jans-cli-tui/docs/docs/getting_started/installation/ubuntu.md new file mode 100644 index 00000000000..8b281bef51f --- /dev/null +++ b/jans-cli-tui/docs/docs/getting_started/installation/ubuntu.md @@ -0,0 +1,26 @@ +# Install Janssen Server using Ubuntu Linux Package + +## Supported Versions +- Ubuntu 20.04 + +!!! note + SELinux should be disabled + +## System Requirements + +System should meet [minimum VM system requirements](vm-requirements.md) + +## Installation Steps + +``` +apt install wget curl +``` +``` +wget http:$(curl -s -L https://api.github.com/repos/JanssenProject/jans/releases/latest | egrep -o '/.*ubuntu20.04_amd64.deb' | head -n 1) -O /tmp/jans.ubuntu20.04_amd64.deb +``` +``` +apt install -y /tmp/jans.ubuntu20.04_amd64.deb +``` +``` +sudo python3 /opt/jans/jans-setup/setup.py +``` diff --git a/jans-cli-tui/docs/docs/getting_started/installation/vm-requirements.md b/jans-cli-tui/docs/docs/getting_started/installation/vm-requirements.md new file mode 100644 index 00000000000..066d2eb041c --- /dev/null +++ b/jans-cli-tui/docs/docs/getting_started/installation/vm-requirements.md @@ -0,0 +1,8 @@ +# VM System Requirements + +Janssen Server needs below-mentioned minimal resources on VM. + +## System Requirements +- 4 GB RAM +- 2 CPU +- 20 GB Disk diff --git a/jans-cli-tui/docs/docs/home/about.md b/jans-cli-tui/docs/docs/home/about.md new file mode 100755 index 00000000000..6f97087ab3b --- /dev/null +++ b/jans-cli-tui/docs/docs/home/about.md @@ -0,0 +1,2 @@ +### About TUI +# About description \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/home/index.md b/jans-cli-tui/docs/docs/home/index.md new file mode 100755 index 00000000000..68066208b4f --- /dev/null +++ b/jans-cli-tui/docs/docs/home/index.md @@ -0,0 +1,10 @@ +# "Janssen Project - cloud native identity and access management platform" + +
+ ![Image title](https://github.com/JanssenProject/jans/raw/main/docs/assets/logo/janssen_project_transparent_630px_182px.png) +
Janssen
+
+ +## Welcome to the Janssen Project + +Janssen enables organizations to build a scalable centralized authentication and authorization service using free open source software. The components of the project include client and server implementations of the OAuth, OpenID Connect, SCIM and FIDO standards. diff --git a/jans-cli-tui/docs/docs/home/janssen_modules.md b/jans-cli-tui/docs/docs/home/janssen_modules.md new file mode 100644 index 00000000000..71e50abd64b --- /dev/null +++ b/jans-cli-tui/docs/docs/home/janssen_modules.md @@ -0,0 +1,23 @@ + +## Janssen Modules + +Janssen is not a big monolith--it's a lot of services working together. Whether you deploy Janssen to a Kubernetes cluster, or you are a developer running everything on one server, it's important to understand the different parts. + +1. **[jans-auth-server](/gluu-4/docs/plugins/oauth/oauth/)**: This component is the OAuth Authorization Server, the OpenID Connect Provider, the UMA Authorization Server--this is the main Internet facing component of Janssen. It's the service that returns tokens, JWT's and identity assertions. This service must be Internet facing. + +1. **[jans-fido2](/gluu-4/docs/plugins/fido/fido/)**: This component provides the server side endpoints to enroll and validate devices that use FIDO. It provides both FIDO U2F (register, authenticate) and FIDO 2 (attestation, assertion) endpoints. This service must be internet facing. + +1. **[jans-config-api](/gluu-4/docs/plugins/config_api/config_api/)**: The API to configure the auth-server and other components is consolidated in this component. This service should not be Internet-facing. + +1. **[jans-scim](/gluu-4/docs/plugins/scim/scim/)**: [SCIM](http://www.simplecloud.info/) is JSON/REST API to manage user data. Use it to add, edit and update user information. This service should not be Internet facing. + +1. **[jans-cli](jans-cli)**: This module is a command line interface for configuring the Janssen software, providing both interactive and simple single line + options for configuration. + +1. **[jans-client-api](/gluu-4/docs/plugins/client_api/client_api/)**: Middleware API to help application developers call an OAuth, OpenID or UMA server. You may wonder why this is necessary. It makes it easier for client developers to use OpenID signing and encryption features, without becoming crypto experts. This API provides some high level endpoints to do some of the heavy lifting. + +1. **[jans-core](jans-core)**: This library has code that is shared across several janssen projects. You will most likely need this project when you build other Janssen components. + +1. **[jans-orm](jans-orm)**: This is the library for persistence and caching implemenations in Janssen. Currently LDAP and Couchbase are supported. RDBMS is coming soon. + +1. **[Agama](agama)**: Agama module offers an alternative way to build authentication flows in Janssen Server. With Agama, flows are coded in a DSL (domain specific language) designed for the sole purpose of writing web flows. diff --git a/jans-cli-tui/docs/docs/plugins/client_api/client_api.md b/jans-cli-tui/docs/docs/plugins/client_api/client_api.md new file mode 100755 index 00000000000..e719802fd0c --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/client_api/client_api.md @@ -0,0 +1 @@ +::: plugins.050_client_api.main \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/plugins/config_api/config_api.md b/jans-cli-tui/docs/docs/plugins/config_api/config_api.md new file mode 100755 index 00000000000..eaed174de6d --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/config_api/config_api.md @@ -0,0 +1 @@ +::: plugins.040_config_api.main \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/plugins/fido/fido.md b/jans-cli-tui/docs/docs/plugins/fido/fido.md new file mode 100755 index 00000000000..5c30baa3f6b --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/fido/fido.md @@ -0,0 +1 @@ +::: plugins.020_fido.main \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/plugins/oauth/edit_client_dialog.md b/jans-cli-tui/docs/docs/plugins/oauth/edit_client_dialog.md new file mode 100755 index 00000000000..1b901865e20 --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/oauth/edit_client_dialog.md @@ -0,0 +1 @@ +::: plugins.010_oxauth.edit_client_dialog \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/plugins/oauth/edit_scope_dialog.md b/jans-cli-tui/docs/docs/plugins/oauth/edit_scope_dialog.md new file mode 100755 index 00000000000..61a675cd85d --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/oauth/edit_scope_dialog.md @@ -0,0 +1 @@ +::: plugins.010_oxauth.edit_scope_dialog \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/plugins/oauth/edit_uma_dialog.md b/jans-cli-tui/docs/docs/plugins/oauth/edit_uma_dialog.md new file mode 100755 index 00000000000..edccbaca4a4 --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/oauth/edit_uma_dialog.md @@ -0,0 +1 @@ +::: plugins.010_oxauth.view_uma_dialog \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/plugins/oauth/oauth.md b/jans-cli-tui/docs/docs/plugins/oauth/oauth.md new file mode 100755 index 00000000000..747de3264aa --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/oauth/oauth.md @@ -0,0 +1 @@ +::: plugins.010_oxauth.main \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/plugins/plugins.md b/jans-cli-tui/docs/docs/plugins/plugins.md new file mode 100755 index 00000000000..a2c55b521eb --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/plugins.md @@ -0,0 +1,11 @@ +# plugins +#### There are six main plugins in the TUI and user may add other plugins +---------------------------- +### The Main Plugins are +- [Open Authorization (OAuth)](/gluu-4/docs/plugins/oauth/oauth/) +- [Fast IDentity Online (FIDO)](/gluu-4/docs/plugins/fido/fido/) +- [ System for Cross-domain Identity Management (SCIM)](/gluu-4/docs/plugins/scim/scim/) +- [Config API (Config API)](/gluu-4/docs/plugins/config_api/config_api/) +- [Client API (Client API)](/gluu-4/docs/plugins/client_api/client_api/) +- [Scripts (Scripts)](/gluu-4/docs/plugins/scripts/scripts/) + diff --git a/jans-cli-tui/docs/docs/plugins/scim/scim.md b/jans-cli-tui/docs/docs/plugins/scim/scim.md new file mode 100755 index 00000000000..7ede220362b --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/scim/scim.md @@ -0,0 +1 @@ +::: plugins.030_scim.main \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/plugins/scripts/scripts.md b/jans-cli-tui/docs/docs/plugins/scripts/scripts.md new file mode 100755 index 00000000000..6cb17b690ac --- /dev/null +++ b/jans-cli-tui/docs/docs/plugins/scripts/scripts.md @@ -0,0 +1 @@ +::: plugins.060_scripts.main \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/stylesheets/extra.css b/jans-cli-tui/docs/docs/stylesheets/extra.css new file mode 100755 index 00000000000..50bed088c99 --- /dev/null +++ b/jans-cli-tui/docs/docs/stylesheets/extra.css @@ -0,0 +1,5 @@ +:root > * { + --md-primary-fg-color: #EE0F0F; + --md-primary-fg-color--light: #ECB7B7; + --md-primary-fg-color--dark: #90030C; + } \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/wui_components/jans_cli_dialog.md b/jans-cli-tui/docs/docs/wui_components/jans_cli_dialog.md new file mode 100755 index 00000000000..04392f123ad --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/jans_cli_dialog.md @@ -0,0 +1,4 @@ +### JansGDialog +::: wui_components.jans_cli_dialog.JansGDialog + + diff --git a/jans-cli-tui/docs/docs/wui_components/jans_data_picker.md b/jans-cli-tui/docs/docs/wui_components/jans_data_picker.md new file mode 100755 index 00000000000..0fb98b7bec5 --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/jans_data_picker.md @@ -0,0 +1,2 @@ +### DateSelectWidget +::: wui_components.jans_data_picker.DateSelectWidget \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/wui_components/jans_dialog.md b/jans-cli-tui/docs/docs/wui_components/jans_dialog.md new file mode 100755 index 00000000000..2026900a141 --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/jans_dialog.md @@ -0,0 +1,2 @@ +### JansDialog +::: wui_components.jans_dialog.JansDialog \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/wui_components/jans_dialog_with_nav.md b/jans-cli-tui/docs/docs/wui_components/jans_dialog_with_nav.md new file mode 100755 index 00000000000..e33e4d79185 --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/jans_dialog_with_nav.md @@ -0,0 +1,2 @@ +### JansDialogWithNav +::: wui_components.jans_dialog_with_nav.JansDialogWithNav \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/wui_components/jans_drop_down.md b/jans-cli-tui/docs/docs/wui_components/jans_drop_down.md new file mode 100755 index 00000000000..c22d9867542 --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/jans_drop_down.md @@ -0,0 +1,2 @@ +### DropDownWidget +::: wui_components.jans_drop_down.DropDownWidget \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/wui_components/jans_message_dialog.md b/jans-cli-tui/docs/docs/wui_components/jans_message_dialog.md new file mode 100755 index 00000000000..5e6d5b44d8c --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/jans_message_dialog.md @@ -0,0 +1,2 @@ +### JansMessageDialog +::: wui_components.jans_message_dialog.JansMessageDialog \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/wui_components/jans_nav_bar.md b/jans-cli-tui/docs/docs/wui_components/jans_nav_bar.md new file mode 100755 index 00000000000..a5f9869af28 --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/jans_nav_bar.md @@ -0,0 +1,2 @@ +### JansNavBar +::: wui_components.jans_nav_bar.JansNavBar \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/wui_components/jans_side_nav_bar.md b/jans-cli-tui/docs/docs/wui_components/jans_side_nav_bar.md new file mode 100755 index 00000000000..f2e03ed8812 --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/jans_side_nav_bar.md @@ -0,0 +1,2 @@ +### JansSideNavBar +::: wui_components.jans_side_nav_bar.JansSideNavBar \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/wui_components/jans_vetrical_nav.md b/jans-cli-tui/docs/docs/wui_components/jans_vetrical_nav.md new file mode 100755 index 00000000000..41701329574 --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/jans_vetrical_nav.md @@ -0,0 +1,2 @@ +### JansVerticalNav +::: wui_components.jans_vetrical_nav.JansVerticalNav \ No newline at end of file diff --git a/jans-cli-tui/docs/docs/wui_components/wui_components.md b/jans-cli-tui/docs/docs/wui_components/wui_components.md new file mode 100755 index 00000000000..8ae69e78b5e --- /dev/null +++ b/jans-cli-tui/docs/docs/wui_components/wui_components.md @@ -0,0 +1,45 @@ +# Components + +## Content + +This Sction contains all the wui components (Classes, attr and methods) + +**there are diffrent types such as** +### - Dialogs components +- [cli dialog (jans_cli_dialog)](/gluu-4/docs/wui_components/jans_cli_dialog) +- [dialog with navigation bar (dialog_with_nav)](/gluu-4/docs/wui_components/jans_dialog_with_nav) +- [message dialog (jans_message_dialog)](/gluu-4/docs/wui_components/jans_message_dialog) + +### - Navigation bar components + +- [Main navigation bar (jans_nav_bar)](/gluu-4/docs/wui_components/jans_nav_bar) +- [Horizontal navigation bar (jans_side_nav_bar)](/gluu-4/docs/wui_components/jans_side_nav_bar) +- [vetrical navigation bar (jans_vetrical_nav)](/gluu-4/docs/wui_components/jans_vetrical_nav) + + +### - Custom components + +- [data picker widget (jans_data_picker)](/gluu-4/docs/wui_components/jans_data_picker) +- [drop-down widget (jans_drop_down)](/gluu-4/docs/wui_components/jans_drop_down) + + + \ No newline at end of file diff --git a/jans-cli-tui/docs/img/favicon.ico b/jans-cli-tui/docs/img/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..3879ed15cc8d08da2f38516349c596923f1d0f00 GIT binary patch literal 15406 zcmeHO33OCtmX6305D-LEa6|2Jw2y1swmt3MZMRFStsWiiaUAMPw006k24bb?R!wunT6xMhv9Jb!o%Oe_xvx8;y4eE zbrxak!9U=myoa#m;LAv@dJDO=A7E|%uMrl1F5XPN2Ib9h9PjZT4!7GtLit>5-1i*k z*bk4}rE7RR2C|MU$NSm$qN+8G@7cIC$NH@=^rlq5jfBbtq}v$yy`S$`9i3R3`C~r2 z2EO-=mAQ|!rK6#1Kh_sK4a0DrJTJ!|YTkvIvgx|FlL+P?LgRZm_h9>>KcmZ152w3V z`6la{S3mOW*CF@lhu_$*Z>}AOrebsPzhiaoFR?V^HmuqE2-^CNc9aNE(@S4TQ`VsG8YXzne+wvyLKuc32vaIP;7{GNYPW`Qo!D!=XF8r9^5+c2=9 z;3;%j>$zq>|8`&{_vmo-1sbj2iQKmo{{cO=M#{&DO-0Y+^P(43rt+J}b^o!?1M*NN z@VMR7kH`4F0~M{w_`38}?s*08Wz;nfIxV&2&x3rA@(FM`Ts>Hq{{*_ohm|?^QPv%* zqwy8d$f#MVG<{Y4pWI(sfYCs0M>b{n8g=$T)OGAN>Ezx;KhmjtUlsomO^XvcL4|Wmu4QHOiV}`M2=h)NSFP?5(Cf6RYzmU+Q5|{bqB1nD5B%UmrAS^U3T~ zBK!N@)J+FUn!ZNNp-4EKRgH>Ne*Y?B&zXfKDNZFU8MH;e8)(@lU6_220o9 z{5_Ey<=Ia?a~m$|jSCK=oAy|Zik4X9)~!R@(YKLUH3y01Z}K@08MRAMQ2!Z!Pe6Nb z1)N5Y%1U%c<{Ip!wniT)_SQ?kn^XG{ebR3*H|bJ@C!CA0n30&Bcp(<1U4s=lw_#P@ zoz(Svu{8T8L?jJCc-&A-iyw-pgp0AO^mWc-r*Z_g7k`?lq3T;aT)VBe245X~1+x;z zAtF8$%d_r8e8o(ZHtj^arHVeFOZO(a=yck#kNkL(^n}HcKHhVaCgT(O@6@uzusPa6 zXXw9Fz|S7O<8nFZBUf_W3o$d{LhLMw#PP0D7%r#UpxPU4iGopD4X1(B>V*i8AC8E) z^B6OprETmaeVw!i2j3A}koSFlIN;Kp+e7(UY2U9RB6cJ`D|nv1=BVzmpT4M{{PWog zg~|C0{^mq=YXW9)-33YiNZ*kI@xii>e#T6APL3ggSEkS9Tu$oP&ayCsCyqc$)e`<~ z)BT!$!1Uj;4ZXORe$MUSJFBvPj#)|LHExN&Roj<6tN#Aa`xd@QdjLSp4Y z@=kbtdU=_?%s?Gub6DJP#2lVRx!L{r6L_Zsv~x|t6dsjfKiBKQI~h0O!<>gs#dITR*wtO|Bn`#_j_gBlP+h^eyYt77f}%YWX6hRL2q zX_r5ueT$xnjs&Lnq;JV{@nxx1ixH7{KFT{{$@3PtXu~eU0=ugNJcUDvyPJ9cRX!cd~9IPJY-NGaw)DCI|H(4O~lNbyVUc zex5FWVEW+_8h z>~La-bgnP4MlgrLSX-_#elzK6V(O+7{z>@8arK>rm_{F%LVRn|#`%KbgS<}J9c8_Pqc*{e3L49f*t{gN&m~RaZ&1E+9P0uie0W zau`Y)w~>`zUogn!WgeH)g|!8bVom;o#2FI54dxBqcrW{BcsJu#*qohkIvmKXS%K+s z<8ZKP3+Z%muM!6g*j#N`kbFH>=iE=TU`{IjVvv{c(8+vi9_@S=bxHC_Co%mt<~@I^wBe^hfh`Kfbskdg3HTiaR`)_;MWb>NddeSm;B@FM9{xO1Xjl<58t+klR-s zI>6W$M!ee6Q-Av^-|)e9ceZ1Qt!uN`@z2uU$!Z3ENa-u zeT*U|%cQL`FB$|U5dQCG&K{j|1?s!98P6wUdFCDHwI5gC$p4*K-@7Rf>2Old4mNJb z9QurRiCwB%Q(&jge3bhLR_5LVr=9ET*wfeozm~P7e)2)~{aBjwV=PR+77J5vVD3=k z)nn?i=n~)azhs>FN5RBWhgp^NpyJ<8^Pfdg!{@}#<55_W9)RLXmC`VDC7Dd+2ALv-?|8!H0w{wDZZ>M(gnS26FsiWqz(W+aWnyre5NSMTRK zIv2=C|BK&yxPC2Te^}fwtjPKqI*7N`_Xu{D`3G}}PS96+3^(IsE3&FrVOjQVh}b;_ zpBFw)y|9_~aO!+qUvN-n#R_aHd``!jy5{Bgsqdeb772!ui*P^GP1%z{du+8FLQ(CP z=(dQ@_S0eCJo0|O9Zl_}q>+A;HpCZwzkX*=@dc^_r_Lzy6}rT}L{Ig}&)@TPy1Kys zm+z%c!V+!B?|A>8sFQpz`XatZZt+oqGen2IHYPDwv5$Pu&*Fcm$8P#87h{pb-9z8p z!)Kp9U5o<;e-oc=-ke9~6#qk{w!t*q(>}huBu?6yg_LtP5LQ z4a`}KaG+r`k}IR|<$+gNvwRAx^M8f+bMM9bS--%0nZM-c`>=ZNgIHhqB)-`H67%dC z$f;V1s+M?k^i^_y-I{m1IJf2p0b*4!+t9e8JQqJEH2|U8iXQ86t^H@#EW`S|Cz8|}`e5?j#eCFm??prVKBTfn@NxdbT00A4o+q(J zYG+F_Zzf+KXN~Jm%roa9`^bB&VXbHF=u4Ef>}LHoPHSCh)${Rw&RrVU!kMp!C!E7N z3GWzluDOi=+2tR??Ii7t$rMD8lv5WO8>L1l@k4U?E=vRJcHzVWSCg-ya z9wi1{r8uRXb;w>zEAe>Xe>;= z1#yRFp}Bh>>_IQFs@ z+UKm|Jd%e<|A$z?oJjpeF!np3FT!(M1L>TDxrvvM?h#m)c{^(oi_zS3Nb$Kr+CAj0 z;0(ny15A?G&U{IH5aloR_yXpNb5bUd?xC8$Z!3O{d1M;%{Wi|UypDU+e3?%&eqg3x zKS*ph1bZ_-*}U%+;^r~Rv(1ISC%=moKg(Jk@eh1Yv6cE!|22hvzQeg(%w1ahim)R6 zc1)wK%#OQ^xb72T`%YqdsYlW$xTvq<*VI;}t{g085V>;>`d%0L)oX9V*3wt#8$vNV z`4ZaVVq&ol>Wb(yX_lKgiO|RoyuIS~F-u~B@PT;T?y?}6wmdC16w?#VqfM`Y&FJF3 zSi@&7PbQf2h@TX`%Po2s)Ft%Gn#}uXV`)`OxzI}i&Z|1^BiQ#m zWigFK!V5dvtqqu;e4XY(Ulje$&^+d*giiTSp1ggvV*O0|9hB>q z{V%IOU!C)7^g5fV?_!6-`@!p^;sDA^YS8QRp2gIJA^3Rj6O2jCv~|%B-oZE(pFBh_ zVd`BU=aPP))k(dk|8THgSke>&>2b_UzM8%vgZrUxR2!vGgW#9a&pA6;1AIH>X2yz8 z#2%i-`PiH1d}mBQZA#+5)JDo!t6h-z1H`iT-N2rH9^=Ns^y@J#X#}<%3|E~O4Ca%S zj|GEyDz2l_K+#E$)>2r{$*EhRdO0#H2U|I1^2P;{Q~PftBA{vX+1`KpO5$EQY=C}0 zTF&I7=ywV0(~@_*!Q9~u)~cp5?}$#GqBvI0%FKu#g(UjD4&vgvt^#a2@B-~+6y8m{ zo3)s7%@I|H)E@l}lJwJ0N<4Wl^M2OAMHE?6Z6AFxUgRLSOnZg;bkX`4o|g%a31zSIMfxiCu#_H&Nq+p7P|g06#$Ao@~ADpwrQbPPap1J1b%+&Ck#n!4Z(HEcf=oj}YeHWL* z$zFImW+h!jJsQRHemg0%?}L8ok?04{GyRok^`_GQOCIB-k1}mU^vjpe_F@P7BjIwM zD1IUem=DTX4_{qbb76_k+^>x3wl?z|$3uu9j^51ryOTIn`R|kc*~&u3dvhki5g!*k z!uT}|`=`f@L${-n>j>W|1L~~!LmPXJ8An$z z7LUd9?Ay7IJGBnAx$q_C$UI+4AEx<^`gjxXNj;#tEtUP3%P~E96jCeSRg9&+i2IPW z&+6iEk-40$t?$ah%=k-~cic#Rwwrun%ybhwO1+@5=b*;une0auH*ICDDI1H}-;y3t zYV~5~>)eC*ct3zFIls-pTx{>t2;*5c>8EvBj`2O)8P?anxmyqE7d{o#Z=i1+j!*KR zrcO(J!>r4QU+rc+Xk*dSnk#&|?_U_dTG%V=Me8pqEjLIDP3D%kE-{mZf z&2B>)`8$(6+UTUqP}vsCx|3b~qu>JRp-!Q-vNoH3 zi*~;*_X(|miB4)SD=2zTep!5hoc(e+tXRJ1r__Z|l#)Ll&MlZrd`ks;!1GhCX8-#< z#Fb7rW$)#gEg<$U*yqzT;LwJ7HvER>C&n@}4v^M;C+U6j>CMC_P=%JvxULFhj_ z{ww|T1>Lr4%#N9?wbX{5Jklt8a-ga60MBIJLA@G=^*o2%XJ`M9@xZ6zGG27Ak3H+S z@+jiF3-DIj4JbOcmG;o5^GHq8aCp#TX~f2Y=h)Xj59DkG>1~J=BR^h}ad% zbI5n_98V4TVyB%Q!P>0fFrE!%9&jsj{r$A#Zqm;+eJU<^lk$`LYqzykedW~MW7$8y zia2#6@oX#2!K`r#zvZ05?8Hlmi7rMd^BT9kbTjV9j%QCO zn*9r&TUY7MX-PfL$}>V)n4fwD zG5Y1&+b~!s6}+VM3#JU>68d|IGZ*u$$Ml48$f{Y1tfNaXGxkF5Rm7G>l1E({n?$F$ z5N|*JpR$(pLhPP!DkOCwp=s)gNVMdpzY=qtwH!|VrOvvqP> z@ud=zPh!j|vwQ!PSCnPKGmqlPd5KqGR`NvbD0!7NgG-pVpNB75PweSyqkRg_XT0(G zYj1oFG`Y;Dev`6tlCK?>YT}-kcqaHF&BY{#D`d>5>&(NNyvK;^LXpFN&miZFWSsC* z_n`am5_;-F!av^P-wfs_a^5L2>3l?_jMQ8sg1xxS`~Qt+S*u7pv5(p-d86Y*zH-al zfiC_=K1xkk`%m<3;+y15->R&;v_23?om`dk3#{As7-c+K^ZeSjz22C~@wx|D<23f* zrNgXeS$N)N26K3xyJVgbPMrJKl9zc-qK-Dl7$iBO=1ha&i*Cp7J<_4_pnc0ZtXAfB z$yIapY^TJrX*}O7xq-wS`F9)^J24CQeS&`KY9M z`Z%fC%f8Kh1X$ZgKXtU7`nilc8I?GR{}v~jKAitfM|&eu>kyy7dCn$ADjkydO8rE5 zCTBaXJU?eeOLsYIyBnC3zRDcoeyquO49z^-FZ~(KBZTw-AS(tPonor;E+&-+anx-!2kdN literal 0 HcmV?d00001 diff --git a/jans-cli-tui/docs/img/logo.png b/jans-cli-tui/docs/img/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..d925cd28c4147fef1cb64c13304e195e6b0bb07a GIT binary patch literal 4373 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 HCZGNf0%Zst literal 0 HcmV?d00001 diff --git a/jans-cli-tui/docs/remote_install.md b/jans-cli-tui/docs/remote_install.md new file mode 100644 index 00000000000..fe5843bf31e --- /dev/null +++ b/jans-cli-tui/docs/remote_install.md @@ -0,0 +1,24 @@ +Install Jans-TUI with pip from GitHub +-------------------------------------- +``` +pip3 install https://github.com/JanssenProject/jans/archive/refs/heads/jans-cli-tui-works.zip#subdirectory=jans-cli-tui +``` + +Execute: + +``` +config-cli-tui +``` + +Get Credidentials for CLI +------------------------- +On Jans server + +``` +# cat /opt/jans/jans-setup/setup.properties.last | grep role +role_based_client_encoded_pw=dDpwNN3lv94JF+ibgVFT7A\=\= +role_based_client_id=2000.076aa5d9-fa8d-42a0-90d2-b83b5ea535d5 +role_based_client_pw=mrF8tcBd6m9Q +``` + +`role_based_client_id` wile go **Client ID** and `role_based_client_pw` will go **Client Secret** diff --git a/jans-cli-tui/run_test.py b/jans-cli-tui/run_test.py new file mode 100644 index 00000000000..00aa77985b5 --- /dev/null +++ b/jans-cli-tui/run_test.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +""" +""" +import unittest +from test import test_drop_down +from test import test_getTitledCheckBoxList, test_drop_down, test_date_picker, test_getTitledText, test_getTitledCheckBox, test_getTitledRadioButton +from mock import patch + + +class Test_DropDownWidget(unittest.TestCase): + def test_drop_down1(self): ### existing value + with patch('test.prompt', return_value = ('HS1',[('HS1','HS1'),('HS2','HS2'),])) as prompt : ## return_value >> value, values + self.assertEqual(test_drop_down(), True) + prompt.assert_called_once_with('input') + + def test_drop_down2(self): ### non-existing value + with patch('test.prompt', return_value = ('HS5',[('HS1','HS1'),('HS2','HS2'),])) as prompt : + self.assertEqual(test_drop_down(), True) + prompt.assert_called_once_with('input') + + def test_drop_down3(self): ### wrong type value + with patch('test.prompt', return_value = (5.2,[('HS1','HS1'),('HS2','HS2'),])) as prompt : + self.assertEqual(test_drop_down(), True) + prompt.assert_called_once_with('input') + +class Test_DateSelectWidget(unittest.TestCase): + def test_date_picker1(self): ### valid value + with patch('test.prompt', return_value = ('2022-10-08T06:18:20',self)) as prompt : ## return_value >> value, parent + self.assertEqual(test_date_picker(), True) + prompt.assert_called_once_with('input') + + def test_date_picker2(self): ### non-valid value >>>>>>>>>>>>>>>>>>>>>>>>>>> ## TODO FALID + with patch('test.prompt', return_value = ('dumy string',self)) as prompt : ## return_value >> value, parent + self.assertEqual(test_date_picker(), False) + prompt.assert_called_once_with('input') + + def test_date_picker3(self): ### wrong type value >>>>>>>>>>>>>>>>>>>>>>>>>>> ## TODO FALID + with patch('test.prompt', return_value = (5,self)) as prompt : ## return_value >> value, parent + self.assertEqual(test_date_picker(), False) + prompt.assert_called_once_with('input') + +class Test_GetTitledText(unittest.TestCase): + def test_getTitledText1(self): ### valid values + with patch('test.prompt', return_value = ("Title","value",1,False,0)) as prompt : ## return_value >> title, value, height, read_only, width + self.assertEqual(test_getTitledText(), True) + prompt.assert_called_once_with('input') + + def test_getTitledText2(self): ### valid values with hight=3, and read_only + with patch('test.prompt', return_value = ("Title","value",3,True,2)) as prompt : ## return_value >> title, value, height, read_only, width + self.assertEqual(test_getTitledText(), True) + prompt.assert_called_once_with('input') + + + def test_getTitledText3(self): ### non-valid title >>>>>>>>>>>>>>>>>>>>>>>>>>> ## TODO FALID + with patch('test.prompt', return_value = (5,"value",1,False,0)) as prompt : ## return_value >> title, value, height, read_only, width + self.assertEqual(test_getTitledText(), True) + prompt.assert_called_once_with('input') + + + def test_getTitledText4(self): ### non-valid value >>>>>>>>>>>>>>>>>>>>>>>>>>> ## TODO FALID + with patch('test.prompt', return_value = ("Title",2.2,1,False,0)) as prompt : ## return_value >> title, value, height, read_only, width + self.assertEqual(test_getTitledText(), True) + prompt.assert_called_once_with('input') + + + def test_getTitledText5(self): ### non-valid height + with patch('test.prompt', return_value = ("Title","value",2.2,False,0)) as prompt : ## return_value >> title, value, height, read_only, width + self.assertEqual(test_getTitledText(), True) + prompt.assert_called_once_with('input') + + def test_getTitledText6(self): ### non-valid width + with patch('test.prompt', return_value = ("Title","value",1,False,4.5)) as prompt : ## return_value >> title, value, height, read_only, width + self.assertEqual(test_getTitledText(), True) + prompt.assert_called_once_with('input') + +class Test_GetTitledCheckBoxList(unittest.TestCase): + def test_getTitledCheckBoxList1(self): ### valid values + with patch('test.prompt', return_value = ("Title",['value1','value2'],[('value1', 'value1'), ('value2', 'value2'), ('value3', 'value3')])) as prompt : ## return_value >> title,values,current_values + self.assertEqual(test_getTitledCheckBoxList(), True) + prompt.assert_called_once_with('input') + + def test_getTitledCheckBoxList2(self): ### invalid selected values + with patch('test.prompt', return_value = ("Title",['value5','value6'],[('value1', 'value1'), ('value2', 'value2'), ('value3', 'value3')])) as prompt : ## return_value >> title,values,current_values + self.assertEqual(test_getTitledCheckBoxList(), True) + prompt.assert_called_once_with('input') + + def test_getTitledCheckBoxList3(self): ### invalid selected values type ### in-valid values >>>>>>>>>>>>>>>>>>>>>>>>>>> ##FALID + with patch('test.prompt', return_value = ("Title",[5,6],[('value1', 'value1'), ('value2', 'value2'), ('value3', 'value3')])) as prompt : ## return_value >> title,values,current_values + self.assertEqual(test_getTitledCheckBoxList(), True) + prompt.assert_called_once_with('input') + +class Test_GetTitledCheckBox(unittest.TestCase): + def test_getTitledCheckBox1(self): ### valid values + with patch('test.prompt', return_value = ("Title",False)) as prompt : ## return_value >> title,checked + self.assertEqual(test_getTitledCheckBox(), True) + prompt.assert_called_once_with('input') + + def test_getTitledCheckBox2(self): ### valid values + with patch('test.prompt', return_value = ("Title",False)) as prompt : ## return_value >> title,checked + self.assertEqual(test_getTitledCheckBox(), True) + prompt.assert_called_once_with('input') + + def test_getTitledCheckBox3(self): ### valid values type + with patch('test.prompt', return_value = ("Title",'False')) as prompt : ## return_value >> title,checked + self.assertEqual(test_getTitledCheckBox(), False) + prompt.assert_called_once_with('input') + +class Test_GetTitledRadioButton(unittest.TestCase): + def test_getTitledRadioButton1(self): ### valid values + with patch('test.prompt', return_value = ( "title",[('value1', 'value1'),('value2', 'value2')],'value1')) as prompt : ## return_value >> title,values,current_value + self.assertEqual(test_getTitledRadioButton(), True) + prompt.assert_called_once_with('input') + + def test_getTitledRadioButton2(self): ### in-valid values >>>>>>>>>>>>>>>>>>>>>>>>>>> ## TODO FALID + with patch('test.prompt', return_value = ( "title",[('value1', 'value1'),('value2', 'value2')],'value5')) as prompt : ## return_value >> title,values,current_value + self.assertEqual(test_getTitledRadioButton(), True) + prompt.assert_called_once_with('input') + + def test_getTitledRadioButton3(self): ### in-valid values >>>>>>>>>>>>>>>>>>>>>>>>>>> ## TODO FALID + with patch('test.prompt', return_value = ( "title",[('value1', 'value1'),('value2', 'value2')],5)) as prompt : ## return_value >> title,values,current_value + self.assertEqual(test_getTitledRadioButton(), True) + prompt.assert_called_once_with('input') + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/jans-cli-tui/setup.py b/jans-cli-tui/setup.py new file mode 100644 index 00000000000..a4a97efce48 --- /dev/null +++ b/jans-cli-tui/setup.py @@ -0,0 +1,88 @@ +""" + License terms and conditions for Janssen: + https://www.apache.org/licenses/LICENSE-2.0 +""" + +import codecs +import os +import re +from setuptools import setup +from setuptools import find_packages +from setuptools.command.install import install +from urllib.request import urlretrieve + +class PostInstallCommand(install): + """Post-installation for installation mode.""" + def run(self): + install.run(self) + yaml_dir = os.path.join(self.install_lib, 'cli_tui/cli/ops/jca') + if not os.path.exists(yaml_dir): + os.makedirs(yaml_dir, exist_ok=True) + + print("downloding", 'jans-config-api-swagger-auto.yaml') + + urlretrieve( + 'https://raw.githubusercontent.com/JanssenProject/jans/main/jans-config-api/docs/jans-config-api-swagger-auto.yaml', + os.path.join(yaml_dir, 'jans-config-api-swagger-auto.yaml') + ) + + for plugin_yaml_file in ('fido2-plugin-swagger.yaml', 'jans-admin-ui-plugin-swagger.yaml', 'scim-plugin-swagger.yaml', 'user-mgt-plugin-swagger.yaml'): + print("downloding", plugin_yaml_file) + urlretrieve( + 'https://raw.githubusercontent.com/JanssenProject/jans/main/jans-config-api/plugins/docs/' + plugin_yaml_file, + os.path.join(yaml_dir, 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: + version_file = f.read() + version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", + version_file, re.M) + if version_match: + return version_match.group(1) + raise RuntimeError("Unable to find version string.") + +setup( + name="jans-cli-tui", + version=find_version("cli_tui", "version.py"), + url="", + copyright="Copyright 2021, Janssen", + license="Apache 2.0 ", + author="Janssen", + author_email="", + maintainer="", + status="Dev", + description="", + long_description=__doc__, + packages=find_packages(), + package_data={'': ['*.yaml', '.enabled']}, + zip_safe=False, + install_requires=[ + "ruamel.yaml>=0.16.5", + "PyJWT==2.3.0", + "pygments", + "prompt_toolkit", + "requests", + "urllib3", + "pyDes", + ], + classifiers=[ + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache 2.0 License", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python", + "Programming Language :: Python :: 3s", + "Programming Language :: Python :: 3.6", + ], + include_package_data=True, + + entry_points={ + "console_scripts": [ + "config-cli-tui=cli_tui.jans_cli_tui:run" + ], + }, + cmdclass={ + 'install': PostInstallCommand, + }, +) diff --git a/jans-cli-tui/test.py b/jans-cli-tui/test.py new file mode 100644 index 00000000000..b5c8f8ae9f4 --- /dev/null +++ b/jans-cli-tui/test.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +""" +import importlib +import sys +sys.path.append("./cli_tui/") +jans_main = importlib.import_module("jans-cli-tui") + +from cli_tui.wui_components.jans_drop_down import DropDownWidget +from cli_tui.wui_components.jans_data_picker import DateSelectWidget +from prompt_toolkit import prompt + + + + +#---------------------------------------------------------------------------# +#------------------------ Test Custom Widget ------------------------------# +#---------------------------------------------------------------------------# + +def _wid_to_text_wid(wid) -> str: + return wid.text +#---------------------------------------------------------------------------# +#------------------------ test getTitledText ------------------------------# +#---------------------------------------------------------------------------# +def test_getTitledText(): + title, value, height, read_only, width = prompt('input') + wid = jans_main.JansCliApp().getTitledText( + title=title, + name='spontaneousScopes', + height=height, + value=value, + read_only=read_only, + width=width + ) + return wid.get_children()[0].content.text() + wid.get_children()[1].content.buffer.text == title + ': '+value ## dont remove this space + +#---------------------------------------------------------------------------# +#---------------------- test getTitledCheckBoxList -------------------------# +#---------------------------------------------------------------------------# +def test_getTitledCheckBoxList(): + title, current_values, values = prompt('input') + wid = jans_main.JansCliApp().getTitledCheckBoxList( + title=title, + name=title, + values=values, + current_values=current_values, + ), + selected= [] + x = wid[0].get_children()[1].content.text() + final=[] + for i in x: + if 'class:checkbox ' in i[0] or '*' in i[1]: ## dont remove this space + final.append(i) + + for k in range(len(final)): + if '*' in final[k][1] : + if 'class:checkbox ' in final[k+1][0]: ## dont remove this space + selected.append(final[k+1][1]) + + check_title = (wid[0].get_children()[0].content.text() == title +': ') ## dont remove this space + check_values = set(selected).issubset(current_values) + + if check_title and check_values : + return True + else: + return False + +#---------------------------------------------------------------------------# +#---------------------- test getTitledCheckBox -----------------------------# +#---------------------------------------------------------------------------# +def test_getTitledCheckBox(): + title, checked = prompt('input') + wid = jans_main.JansCliApp().getTitledCheckBox( + title=title, + name=title, + checked=checked + ), + + x = wid[0].get_children()[1].content.text() + wid_checked=False + for i in x: + if '*' in i[1]: + checked=True + break + + check_title = (wid[0].get_children()[0].content.text() == title +': ') + check_values = checked == wid_checked + + if check_title and check_values : + return True + else: + return False + +#---------------------------------------------------------------------------# +#---------------------- test getTitledRadioButton --------------------------# +#---------------------------------------------------------------------------# +def test_getTitledRadioButton(): + title, values, current_value = prompt('input') + wid = jans_main.JansCliApp().getTitledRadioButton( + title=title, + name=title, + values=values, + current_value=current_value, + ), + selected= [] + x = wid[0].get_children()[1].content.text() + final=[] + for i in x: + if 'class:radio ' in i[0] or '*' in i[1]: ## dont remove this space + final.append(i) + + for k in range(len(final)): + if '*' in final[k][1] : + if 'class:radio ' in final[k+1][0]: ## dont remove this space + selected.append(final[k+1][1]) + + check_title = (wid[0].get_children()[0].content.text() == title +': ') ## dont remove this space + check_values = current_value == selected[0] + + if check_title and check_values : + return True + else: + return False + +#---------------------------------------------------------------------------# +#------------------------ test drop_down -----------------------------------# +#---------------------------------------------------------------------------# +def test_drop_down(): + value, values = prompt('input') + wid = DropDownWidget(value=value,values=values) + if [item for item in values if value in item]: + return _wid_to_text_wid(wid) == value + else: + return _wid_to_text_wid(wid) == 'Select One' + +#---------------------------------------------------------------------------# +#------------------------ test date_picker ---------------------------------# +#---------------------------------------------------------------------------# +def test_date_picker(): + value, parent = prompt('input') + wid = DateSelectWidget(value=value,parent=parent) + print(_wid_to_text_wid(wid)) + return _wid_to_text_wid(wid) == value + +#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# +#---------------------------------------------------------------------------# diff --git a/jans-cli-tui/wrapper_test.py b/jans-cli-tui/wrapper_test.py new file mode 100755 index 00000000000..412f168ba46 --- /dev/null +++ b/jans-cli-tui/wrapper_test.py @@ -0,0 +1,185 @@ +import os +import json +from cli_tui.cli import config_cli +test_client = config_cli.client_id if config_cli.test_client else None +config_cli.debug = True +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, + test_client=test_client + ) + +#print(config_cli.host, config_cli.client_id, config_cli.client_secret, config_cli.access_token, cli_object.use_test_client) +status = cli_object.check_connection() + +print(status) +""" +response = cli_object.get_device_verification_code() + +result = response.json() +print(result) + +input() + +cli_object.get_jwt_access_token(result) + +print(result) +""" + +client_data = { + "displayName": "Test 3", + "clientSecret": "TopSecret", + "redirectUris": ["https://www.jans.io/cb"], + "applicationType": "web", + "grantTypes": ["implicit", "refresh_token"] +} +""" +result = cli_object.process_command_by_id( + operation_id='post-oauth-openid-clients', + url_suffix='', + endpoint_args='', + data_fn='', + data=client_data + ) + + +result = cli_object.process_command_by_id( + operation_id='delete-oauth-openid-clients-by-inum', + url_suffix='inum:7710112a-ce34-445b-8d85-fd18bec56ce5', + endpoint_args='', + data_fn='', + data={} + ) + +result = cli_object.process_command_by_id( + operation_id='get-oauth-openid-clients', + url_suffix='', + endpoint_args='', + data_fn='', + data={} + ) + +""" + + +# endpoint_args ='pattern:2B29' ### limit the responce and wrong pattern +data = {} +inum = '40a48740-4892-4fce-b30f-81c5c45670f4' ## this user has 3 UMA +result = cli_object.process_command_by_id( + operation_id='get-all-attribute', + url_suffix='', + endpoint_args="pattern:3B47\,3692\,E7BC\,11AA", + data_fn=None, + data={} + ) + +x = result.json() +print(x) +# print(len(x["entries"])) +# print(x["entries"]) +# for i in x["entries"]: +# if i['inum'] == '08E2': +# print(i['claimName']) + + + +# endpoint_args ='limit:1,pattern:hasdaeat' +# endpoint_args ='limit:5,startIndex:0,pattern:sagasg' + +# result = cli_object.process_command_by_id( +# operation_id='get-oauth-openid-clients', +# url_suffix='', +# endpoint_args=endpoint_args, +# data_fn=None, +# data={} +# ) +# print(result.text) + + + + + + + + + +''' +[ +{"dn":"jansId=5635e18b-67b9-4997-a786-a6b2cdb84355,ou=resources,ou=uma,o=jans", +"id":"5635e18b-67b9-4997-a786-a6b2cdb84355", +"name":"[GET] /document", +"scopes":["inum=40a48740-4892-4fce-b30f-81c5c45670f4,ou=scopes,o=jans"], +"clients":["inum=a5f45938-97d6-408b-965a-229afb0552fa,ou=clients,o=jans"], +"creationDate":"2022-09-15T09:03:14", +"expirationDate":"2022-10-05T09:03:14", +"deletable":true}, + +{"dn":"jansId=9b473b72-496b-4414-828c-a4d2bebca97b,ou=resources,ou=uma,o=jans" +,"id":"9b473b72-496b-4414-828c-a4d2bebca97b", +"name":"[GET] /photo", +"scopes":["inum=40a48740-4892-4fce-b30f-81c5c45670f4,ou=scopes,o=jans"], +"clients":["inum=a5f45938-97d6-408b-965a-229afb0552fa,ou=clients,o=jans"], +"creationDate":"2022-09-15T09:03:13", +"expirationDate":"2022-10-05T09:03:13", +"deletable":true} +] +''' + + +''' +[ +{"dn":"inum=1800.671CF9,ou=scopes,o=jans","inum":"1800.671CF9","displayName":"Config API scope https://jans.io/oauth/config/database/couchbase.readonly","id":"https://jans.io/oauth/config/database/couchbase.readonly","description":"View Couchbase database information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.671CF9,ou=scopes,o=jans"}, +{"dn":"inum=F0C4,ou=scopes,o=jans","inum":"F0C4","displayName":"authenticate_openid_connect","id":"openid","description":"Authenticate using OpenID Connect.","scopeType":"openid","defaultScope":true,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=F0C4,ou=scopes,o=jans"}, +{"dn":"inum=43F1,ou=scopes,o=jans","inum":"43F1","displayName":"view_profile","id":"profile","description":"View your basic profile info.","scopeType":"openid","claims":["inum=2B29,ou=attributes,o=jans","inum=0C85,ou=attributes,o=jans","inum=B4B0,ou=attributes,o=jans","inum=A0E8,ou=attributes,o=jans","inum=5EC6,ou=attributes,o=jans","inum=B52A,ou=attributes,o=jans","inum=64A0,ou=attributes,o=jans","inum=EC3A,ou=attributes,o=jans","inum=3B47,ou=attributes,o=jans","inum=3692,ou=attributes,o=jans","inum=98FC,ou=attributes,o=jans","inum=A901,ou=attributes,o=jans","inum=36D9,ou=attributes,o=jans","inum=BE64,ou=attributes,o=jans","inum=6493,ou=attributes,o=jans","inum=4CF1,ou=attributes,o=jans","inum=29DA,ou=attributes,o=jans"],"defaultScope":false,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=43F1,ou=scopes,o=jans"}, +{"dn":"inum=1800.A92509,ou=scopes,o=jans","inum":"1800.A92509","displayName":"Config API scope https://jans.io/oauth/config/attributes.write","id":"https://jans.io/oauth/config/attributes.write","description":"Manage attribute related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.A92509,ou=scopes,o=jans"}, +{"dn":"inum=C4F5,ou=scopes,o=jans","inum":"C4F5","displayName":"view_user_permissions_roles","id":"permission","description":"View your user permission and roles.","scopeType":"dynamic","defaultScope":true,"dynamicScopeScripts":["inum=CB5B-3211,ou=scripts,o=jans"],"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=C4F5,ou=scopes,o=jans"}, +{"dn":"inum=1200.2B53F9,ou=scopes,o=jans","inum":"1200.2B53F9","displayName":"Scim users.read","id":"https://jans.io/scim/users.read","description":"Query user resources","scopeType":"oauth","attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=1200.2B53F9,ou=scopes,o=jans"}, +{"dn":"inum=1800.642032,ou=scopes,o=jans","inum":"1800.642032","displayName":"Config API scope https://jans.io/oauth/config/fido2.write","id":"https://jans.io/oauth/config/fido2.write","description":"Manage FIDO2 related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.642032,ou=scopes,o=jans"}, +{"dn":"inum=1800.831F68,ou=scopes,o=jans","inum":"1800.831F68","displayName":"Config API scope https://jans.io/oauth/config/acrs.write","id":"https://jans.io/oauth/config/acrs.write","description":"Manage ACRS related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.831F68,ou=scopes,o=jans"}, +{"dn":"inum=1800.2BFAC6,ou=scopes,o=jans","inum":"1800.2BFAC6","displayName":"Config API scope https://jans.io/oauth/jans-auth-server/config/properties.write","id":"https://jans.io/oauth/jans-auth-server/config/properties.write","description":"Manage Auth Server properties related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.2BFAC6,ou=scopes,o=jans"}, +{"dn":"inum=1800.A9D2B6,ou=scopes,o=jans","inum":"1800.A9D2B6","displayName":"Config API scope https://jans.io/oauth/config/database/ldap.readonly","id":"https://jans.io/oauth/config/database/ldap.readonly","description":"View LDAP database related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.A9D2B6,ou=scopes,o=jans"}, +{"dn":"inum=1800.7DA888,ou=scopes,o=jans","inum":"1800.7DA888","displayName":"Config API scope https://jans.io/oauth/config/acrs.readonly","id":"https://jans.io/oauth/config/acrs.readonly","description":"View ACRS related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.7DA888,ou=scopes,o=jans"}, +{"dn":"inum=1800.E99751,ou=scopes,o=jans","inum":"1800.E99751","displayName":"Config API scope https://jans.io/oauth/config/scripts.write","id":"https://jans.io/oauth/config/scripts.write","description":"Manage scripts related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.E99751,ou=scopes,o=jans"}, +{"dn":"inum=1800.EB8C51,ou=scopes,o=jans","inum":"1800.EB8C51","displayName":"Config API scope https://jans.io/oauth/config/attributes.readonly","id":"https://jans.io/oauth/config/attributes.readonly","description":"View attribute related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.EB8C51,ou=scopes,o=jans"}, +{"dn":"inum=1800.6B63B0,ou=scopes,o=jans","inum":"1800.6B63B0","displayName":"Config API scope https://jans.io/oauth/config/smtp.delete","id":"https://jans.io/oauth/config/smtp.delete","description":"Delete SMTP related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.6B63B0,ou=scopes,o=jans"}, +{"dn":"inum=6D99,ou=scopes,o=jans","inum":"6D99","displayName":"UMA Protection","id":"uma_protection","description":"Obtain UMA PAT.","scopeType":"openid","defaultScope":true,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=6D99,ou=scopes,o=jans"}, +{"dn":"inum=C4F7,ou=scopes,o=jans","inum":"C4F7","id":"jans_stat","description":"This scope is required for calling Statistic Endpoint","scopeType":"openid","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=C4F7,ou=scopes,o=jans"}, +{"dn":"inum=1800.AA3FFB,ou=scopes,o=jans","inum":"1800.AA3FFB","displayName":"Config API scope https://jans.io/oauth/config/uma/resources.readonly","id":"https://jans.io/oauth/config/uma/resources.readonly","description":"View UMA Resource related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.AA3FFB,ou=scopes,o=jans"}, +{"dn":"inum=1800.5E2668,ou=scopes,o=jans","inum":"1800.5E2668","displayName":"Config API scope https://jans.io/oauth/config/logging.write","id":"https://jans.io/oauth/config/logging.write","description":"Manage logging related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.5E2668,ou=scopes,o=jans"}, +{"dn":"inum=1800.E9EE2A,ou=scopes,o=jans","inum":"1800.E9EE2A","displayName":"Config API scope https://jans.io/oauth/config/openid/clients.readonly","id":"https://jans.io/oauth/config/openid/clients.readonly","description":"View clients related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.E9EE2A,ou=scopes,o=jans"}, +{"dn":"inum=1800.5D8461,ou=scopes,o=jans","inum":"1800.5D8461","displayName":"Config API scope https://jans.io/oauth/config/scopes.write","id":"https://jans.io/oauth/config/scopes.write","description":"Manage scope related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.5D8461,ou=scopes,o=jans"}, +{"dn":"inum=D491,ou=scopes,o=jans","inum":"D491","displayName":"view_phone_number","id":"phone","description":"View your phone number.","scopeType":"openid","claims":["inum=B17A,ou=attributes,o=jans","inum=0C18,ou=attributes,o=jans"],"defaultScope":false,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=D491,ou=scopes,o=jans"}, +{"dn":"inum=C17A,ou=scopes,o=jans","inum":"C17A","displayName":"view_address","id":"address","description":"View your address.","scopeType":"openid","claims":["inum=27DB,ou=attributes,o=jans","inum=2A3D,ou=attributes,o=jans","inum=6609,ou=attributes,o=jans","inum=6EEB,ou=attributes,o=jans","inum=BCE8,ou=attributes,o=jans","inum=D90B,ou=attributes,o=jans","inum=E6B8,ou=attributes,o=jans","inum=E999,ou=attributes,o=jans"],"defaultScope":false,"groupClaims":true,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=C17A,ou=scopes,o=jans"}, +{"dn":"inum=1800.31BA7D,ou=scopes,o=jans","inum":"1800.31BA7D","displayName":"Config API scope https://jans.io/oauth/config/attributes.delete","id":"https://jans.io/oauth/config/attributes.delete","description":"Delete attribute related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.31BA7D,ou=scopes,o=jans"}, +{"dn":"inum=1200.BAC91C,ou=scopes,o=jans","inum":"1200.BAC91C","displayName":"Scim users.write","id":"https://jans.io/scim/users.write","description":"Modify user resources","scopeType":"oauth","attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=1200.BAC91C,ou=scopes,o=jans"}, +{"dn":"inum=1800.F0B5D0,ou=scopes,o=jans","inum":"1800.F0B5D0","displayName":"Config API scope https://jans.io/oauth/config/database/couchbase.delete","id":"https://jans.io/oauth/config/database/couchbase.delete","description":"Delete Couchbase database related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.F0B5D0,ou=scopes,o=jans"}, +{"dn":"inum=1800.2928DA,ou=scopes,o=jans","inum":"1800.2928DA","displayName":"Config API scope https://jans.io/oauth/config/database/couchbase.write","id":"https://jans.io/oauth/config/database/couchbase.write","description":"Manage Couchbase database related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.2928DA,ou=scopes,o=jans"}, +{"dn":"inum=1800.7382A8,ou=scopes,o=jans","inum":"1800.7382A8","displayName":"Config API scope https://jans.io/oauth/config/cache.readonly","id":"https://jans.io/oauth/config/cache.readonly","description":"View cache related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.7382A8,ou=scopes,o=jans"}, +{"dn":"inum=8A01,ou=scopes,o=jans","inum":"8A01","displayName":"view_mobile_phone_number","id":"mobile_phone","description":"View your mobile phone number.","scopeType":"openid","claims":["inum=6DA6,ou=attributes,o=jans"],"defaultScope":false,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=8A01,ou=scopes,o=jans"}, +{"dn":"inum=7D90,ou=scopes,o=jans","inum":"7D90","displayName":"revoke_session","id":"revoke_session","description":"revoke_session scope which is required to be able call /revoke_session endpoint","scopeType":"openid","defaultScope":false,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=7D90,ou=scopes,o=jans"}, +{"dn":"inum=10B2,ou=scopes,o=jans","inum":"10B2","displayName":"view_username","id":"user_name","description":"View your local username in the Janssen Server.","scopeType":"openid","claims":["inum=42E0,ou=attributes,o=jans"],"defaultScope":false,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=10B2,ou=scopes,o=jans"}, +{"dn":"inum=764C,ou=scopes,o=jans","inum":"764C","displayName":"view_email_address","id":"email","description":"View your email address.","scopeType":"openid","claims":["inum=8F88,ou=attributes,o=jans","inum=CAE3,ou=attributes,o=jans"],"defaultScope":false,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=764C,ou=scopes,o=jans"}, +{"dn":"inum=1800.F4D6ED,ou=scopes,o=jans","inum":"1800.F4D6ED","displayName":"Config API scope https://jans.io/oauth/config/openid/clients.delete","id":"https://jans.io/oauth/config/openid/clients.delete","description":"Delete clients related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.F4D6ED,ou=scopes,o=jans"}, +{"dn":"inum=1800.F74870,ou=scopes,o=jans","inum":"1800.F74870","displayName":"Config API scope https://jans.io/oauth/config/scripts.readonly","id":"https://jans.io/oauth/config/scripts.readonly","description":"View cache scripts information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.F74870,ou=scopes,o=jans"}, +{"dn":"inum=1800.26E74A,ou=scopes,o=jans","inum":"1800.26E74A","displayName":"Config API scope https://jans.io/oauth/config/jwks.write","id":"https://jans.io/oauth/config/jwks.write","description":"Manage JWKS related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.26E74A,ou=scopes,o=jans"}, +{"dn":"inum=1800.238093,ou=scopes,o=jans","inum":"1800.238093","displayName":"Config API scope https://jans.io/oauth/config/cache.write","id":"https://jans.io/oauth/config/cache.write","description":"Manage cache related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.238093,ou=scopes,o=jans"}, +{"dn":"inum=341A,ou=scopes,o=jans","inum":"341A","displayName":"view_client","id":"clientinfo","description":"View the client info.","scopeType":"openid","claims":["inum=2B29,ou=attributes,o=jans","inum=29DA,ou=attributes,o=jans"],"defaultScope":false,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=341A,ou=scopes,o=jans"}, +{"dn":"inum=1800.E3A23B,ou=scopes,o=jans","inum":"1800.E3A23B","displayName":"Config API scope https://jans.io/oauth/config/jwks.readonly","id":"https://jans.io/oauth/config/jwks.readonly","description":"View JWKS related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.E3A23B,ou=scopes,o=jans"}, +{"dn":"inum=1800.9A234E,ou=scopes,o=jans","inum":"1800.9A234E","displayName":"Config API scope https://jans.io/oauth/config/logging.readonly","id":"https://jans.io/oauth/config/logging.readonly","description":"View logging related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.9A234E,ou=scopes,o=jans"}, +{"dn":"inum=1800.C86963,ou=scopes,o=jans","inum":"1800.C86963","displayName":"Config API scope https://jans.io/oauth/config/fido2.readonly","id":"https://jans.io/oauth/config/fido2.readonly","description":"View FIDO2 related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.C86963,ou=scopes,o=jans"}, +{"dn":"inum=1800.63BC87,ou=scopes,o=jans","inum":"1800.63BC87","displayName":"Config API scope https://jans.io/oauth/config/smtp.readonly","id":"https://jans.io/oauth/config/smtp.readonly","description":"View SMTP related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.63BC87,ou=scopes,o=jans"}, +{"dn":"inum=1800.1101C3,ou=scopes,o=jans","inum":"1800.1101C3","displayName":"Config API scope https://jans.io/oauth/config/database/ldap.write","id":"https://jans.io/oauth/config/database/ldap.write","description":"Manage LDAP database related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.1101C3,ou=scopes,o=jans"}, +{"dn":"inum=1800.487E8C,ou=scopes,o=jans","inum":"1800.487E8C","displayName":"Config API scope https://jans.io/oauth/config/database/ldap.delete","id":"https://jans.io/oauth/config/database/ldap.delete","description":"Delete LDAP database related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.487E8C,ou=scopes,o=jans"}, +{"dn":"inum=1800.8BA54D,ou=scopes,o=jans","inum":"1800.8BA54D","displayName":"Config API scope https://jans.io/oauth/config/scripts.delete","id":"https://jans.io/oauth/config/scripts.delete","description":"Delete scripts related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.8BA54D,ou=scopes,o=jans"}, +{"dn":"inum=6D90,ou=scopes,o=jans","inum":"6D90","displayName":"jans_client_api","id":"jans_client_api","description":"jans_client_api scope which is required to call jans_client_api API","scopeType":"openid","defaultScope":true,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=6D90,ou=scopes,o=jans"}, +{"dn":"inum=1800.FA84EE,ou=scopes,o=jans","inum":"1800.FA84EE","displayName":"Config API scope https://jans.io/oauth/config/scopes.delete","id":"https://jans.io/oauth/config/scopes.delete","description":"Delete scope related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.FA84EE,ou=scopes,o=jans"}, +{"dn":"inum=1800.41F22A,ou=scopes,o=jans","inum":"1800.41F22A","displayName":"Config API scope https://jans.io/oauth/config/smtp.write","id":"https://jans.io/oauth/config/smtp.write","description":"Manage SMTP related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.41F22A,ou=scopes,o=jans"}, +{"dn":"inum=1800.C95FD0,ou=scopes,o=jans","inum":"1800.C95FD0","displayName":"Config API scope https://jans.io/oauth/jans-auth-server/config/properties.readonly","id":"https://jans.io/oauth/jans-auth-server/config/properties.readonly","description":"View Auth Server properties related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.C95FD0,ou=scopes,o=jans"}, +{"dn":"inum=1800.55DAE0,ou=scopes,o=jans","inum":"1800.55DAE0","displayName":"Config API scope https://jans.io/oauth/config/openid/clients.write","id":"https://jans.io/oauth/config/openid/clients.write","description":"Manage clients related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:14","umaType":false,"baseDn":"inum=1800.55DAE0,ou=scopes,o=jans"}, +{"dn":"inum=1800.446159,ou=scopes,o=jans","inum":"1800.446159","displayName":"Config API scope https://jans.io/oauth/config/scopes.readonly","id":"https://jans.io/oauth/config/scopes.readonly","description":"View scope related information","scopeType":"oauth","defaultScope":false,"attributes":{"showInConfigurationEndpoint":false},"creationDate":"2022-09-21T11:15:15","umaType":false,"baseDn":"inum=1800.446159,ou=scopes,o=jans"}, +{"dn":"inum=C4F6,ou=scopes,o=jans","inum":"C4F6","displayName":"refresh_token","id":"offline_access","description":"This scope value requests that an OAuth 2.0 Refresh Token be issued.","scopeType":"openid","defaultScope":true,"attributes":{"showInConfigurationEndpoint":true},"creationDate":"2022-09-24T07:07:19","umaType":false,"baseDn":"inum=C4F6,ou=scopes,o=jans"}] + +''' +