kaqing 1.77.0__py3-none-any.whl → 2.0.171__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adam/__init__.py +1 -0
- adam/app_session.py +182 -0
- {walker → adam}/apps.py +8 -24
- {walker → adam}/batch.py +54 -97
- {walker → adam}/checks/check.py +3 -3
- {walker → adam}/checks/check_result.py +1 -1
- adam/checks/check_utils.py +65 -0
- {walker → adam}/checks/compactionstats.py +6 -6
- {walker → adam}/checks/cpu.py +14 -8
- adam/checks/cpu_metrics.py +52 -0
- {walker → adam}/checks/disk.py +6 -6
- {walker → adam}/checks/gossip.py +5 -5
- {walker → adam}/checks/memory.py +7 -7
- {walker → adam}/checks/status.py +5 -5
- {walker → adam}/cli.py +3 -3
- {walker → adam}/columns/column.py +1 -1
- adam/columns/columns.py +45 -0
- {walker → adam}/columns/compactions.py +5 -5
- {walker → adam}/columns/cpu.py +6 -4
- adam/columns/cpu_metrics.py +22 -0
- {walker → adam}/columns/dir_data.py +3 -3
- {walker → adam}/columns/dir_snapshots.py +3 -3
- {walker → adam}/columns/gossip.py +5 -5
- {walker → adam}/columns/host_id.py +3 -3
- {walker → adam}/columns/memory.py +3 -3
- {walker → adam}/columns/node_address.py +3 -3
- {walker → adam}/columns/node_load.py +3 -3
- {walker → adam}/columns/node_owns.py +3 -3
- {walker → adam}/columns/node_status.py +3 -3
- {walker → adam}/columns/node_tokens.py +3 -3
- {walker → adam}/columns/node_utils.py +2 -2
- {walker → adam}/columns/pod_name.py +2 -2
- {walker → adam}/columns/volume_cassandra.py +4 -4
- {walker → adam}/columns/volume_root.py +3 -3
- adam/commands/__init__.py +15 -0
- adam/commands/alter_tables.py +81 -0
- adam/commands/app_cmd.py +38 -0
- {walker → adam}/commands/app_ping.py +10 -16
- adam/commands/audit/audit.py +84 -0
- adam/commands/audit/audit_repair_tables.py +74 -0
- adam/commands/audit/audit_run.py +50 -0
- adam/commands/audit/show_last10.py +48 -0
- adam/commands/audit/show_slow10.py +47 -0
- adam/commands/audit/show_top10.py +45 -0
- adam/commands/audit/utils_show_top10.py +59 -0
- adam/commands/bash/__init__.py +5 -0
- adam/commands/bash/bash.py +36 -0
- adam/commands/bash/bash_completer.py +93 -0
- adam/commands/bash/utils_bash.py +16 -0
- adam/commands/cat.py +50 -0
- adam/commands/cd.py +43 -0
- adam/commands/check.py +73 -0
- {walker → adam}/commands/cli_commands.py +7 -8
- adam/commands/code.py +57 -0
- adam/commands/command.py +190 -0
- {walker → adam}/commands/command_helpers.py +1 -1
- {walker → adam}/commands/commands_utils.py +15 -25
- adam/commands/cp.py +89 -0
- adam/commands/cql/cql_completions.py +33 -0
- {walker/commands → adam/commands/cql}/cqlsh.py +20 -35
- adam/commands/cql/utils_cql.py +343 -0
- {walker/commands/frontend → adam/commands/deploy}/code_start.py +11 -14
- adam/commands/deploy/code_stop.py +40 -0
- {walker/commands/frontend → adam/commands/deploy}/code_utils.py +7 -9
- adam/commands/deploy/deploy.py +25 -0
- adam/commands/deploy/deploy_frontend.py +49 -0
- adam/commands/deploy/deploy_pg_agent.py +35 -0
- adam/commands/deploy/deploy_pod.py +108 -0
- adam/commands/deploy/deploy_utils.py +29 -0
- adam/commands/deploy/undeploy.py +25 -0
- adam/commands/deploy/undeploy_frontend.py +38 -0
- adam/commands/deploy/undeploy_pg_agent.py +39 -0
- adam/commands/deploy/undeploy_pod.py +48 -0
- adam/commands/devices/device.py +118 -0
- adam/commands/devices/device_app.py +173 -0
- adam/commands/devices/device_auit_log.py +49 -0
- adam/commands/devices/device_cass.py +185 -0
- adam/commands/devices/device_export.py +86 -0
- adam/commands/devices/device_postgres.py +144 -0
- adam/commands/devices/devices.py +25 -0
- {walker → adam}/commands/exit.py +3 -6
- adam/commands/export/clean_up_all_export_sessions.py +37 -0
- adam/commands/export/clean_up_export_sessions.py +51 -0
- adam/commands/export/drop_export_database.py +55 -0
- adam/commands/export/drop_export_databases.py +43 -0
- adam/commands/export/export.py +53 -0
- adam/commands/export/export_databases.py +170 -0
- adam/commands/export/export_handlers.py +71 -0
- adam/commands/export/export_select.py +81 -0
- adam/commands/export/export_select_x.py +54 -0
- adam/commands/export/export_use.py +52 -0
- adam/commands/export/exporter.py +352 -0
- adam/commands/export/import_session.py +40 -0
- adam/commands/export/importer.py +67 -0
- adam/commands/export/importer_athena.py +80 -0
- adam/commands/export/importer_sqlite.py +47 -0
- adam/commands/export/show_column_counts.py +54 -0
- adam/commands/export/show_export_databases.py +36 -0
- adam/commands/export/show_export_session.py +48 -0
- adam/commands/export/show_export_sessions.py +44 -0
- adam/commands/export/utils_export.py +314 -0
- {walker → adam}/commands/help.py +17 -12
- adam/commands/intermediate_command.py +49 -0
- adam/commands/issues.py +43 -0
- adam/commands/kubectl.py +38 -0
- adam/commands/login.py +70 -0
- {walker → adam}/commands/logs.py +8 -10
- adam/commands/ls.py +41 -0
- adam/commands/medusa/medusa.py +27 -0
- adam/commands/medusa/medusa_backup.py +57 -0
- adam/commands/medusa/medusa_restore.py +83 -0
- adam/commands/medusa/medusa_show_backupjobs.py +51 -0
- adam/commands/medusa/medusa_show_restorejobs.py +47 -0
- {walker → adam}/commands/nodetool.py +17 -21
- {walker → adam}/commands/param_get.py +15 -16
- adam/commands/param_set.py +43 -0
- adam/commands/postgres/postgres.py +104 -0
- adam/commands/postgres/postgres_context.py +274 -0
- {walker → adam}/commands/postgres/postgres_ls.py +7 -11
- {walker → adam}/commands/postgres/postgres_preview.py +8 -13
- adam/commands/postgres/psql_completions.py +10 -0
- adam/commands/postgres/utils_postgres.py +66 -0
- adam/commands/preview_table.py +37 -0
- adam/commands/pwd.py +47 -0
- adam/commands/reaper/reaper.py +35 -0
- adam/commands/reaper/reaper_forward.py +93 -0
- adam/commands/reaper/reaper_forward_session.py +6 -0
- {walker → adam}/commands/reaper/reaper_forward_stop.py +13 -19
- {walker → adam}/commands/reaper/reaper_restart.py +10 -17
- adam/commands/reaper/reaper_run_abort.py +46 -0
- adam/commands/reaper/reaper_runs.py +82 -0
- adam/commands/reaper/reaper_runs_abort.py +63 -0
- adam/commands/reaper/reaper_schedule_activate.py +45 -0
- adam/commands/reaper/reaper_schedule_start.py +45 -0
- adam/commands/reaper/reaper_schedule_stop.py +45 -0
- {walker → adam}/commands/reaper/reaper_schedules.py +6 -16
- {walker → adam}/commands/reaper/reaper_status.py +11 -19
- adam/commands/reaper/utils_reaper.py +196 -0
- adam/commands/repair/repair.py +26 -0
- {walker → adam}/commands/repair/repair_log.py +7 -10
- adam/commands/repair/repair_run.py +70 -0
- adam/commands/repair/repair_scan.py +71 -0
- {walker → adam}/commands/repair/repair_stop.py +8 -11
- adam/commands/report.py +61 -0
- adam/commands/restart.py +60 -0
- {walker → adam}/commands/rollout.py +25 -30
- adam/commands/shell.py +34 -0
- adam/commands/show/show.py +39 -0
- walker/commands/show/show_version.py → adam/commands/show/show_adam.py +14 -10
- adam/commands/show/show_app_actions.py +57 -0
- {walker → adam}/commands/show/show_app_id.py +12 -15
- {walker → adam}/commands/show/show_app_queues.py +9 -12
- adam/commands/show/show_cassandra_repairs.py +38 -0
- adam/commands/show/show_cassandra_status.py +124 -0
- {walker → adam}/commands/show/show_cassandra_version.py +6 -16
- adam/commands/show/show_commands.py +59 -0
- walker/commands/show/show_storage.py → adam/commands/show/show_host.py +11 -13
- adam/commands/show/show_login.py +62 -0
- {walker → adam}/commands/show/show_params.py +4 -4
- adam/commands/show/show_processes.py +51 -0
- adam/commands/show/show_storage.py +42 -0
- adam/commands/watch.py +82 -0
- {walker → adam}/config.py +10 -22
- {walker → adam}/embedded_apps.py +1 -1
- adam/embedded_params.py +2 -0
- adam/log.py +47 -0
- {walker → adam}/pod_exec_result.py +10 -2
- adam/repl.py +182 -0
- adam/repl_commands.py +124 -0
- adam/repl_state.py +458 -0
- adam/sql/__init__.py +0 -0
- adam/sql/sql_completer.py +120 -0
- adam/sql/sql_state_machine.py +618 -0
- adam/sql/term_completer.py +76 -0
- adam/sso/__init__.py +0 -0
- {walker → adam}/sso/authenticator.py +5 -1
- adam/sso/authn_ad.py +170 -0
- {walker → adam}/sso/authn_okta.py +39 -22
- adam/sso/cred_cache.py +60 -0
- adam/sso/id_token.py +23 -0
- adam/sso/idp.py +143 -0
- adam/sso/idp_login.py +50 -0
- adam/sso/idp_session.py +55 -0
- adam/sso/sso_config.py +63 -0
- adam/utils.py +679 -0
- adam/utils_app.py +98 -0
- adam/utils_athena.py +145 -0
- adam/utils_audits.py +106 -0
- adam/utils_issues.py +32 -0
- adam/utils_k8s/__init__.py +0 -0
- adam/utils_k8s/app_clusters.py +28 -0
- adam/utils_k8s/app_pods.py +33 -0
- adam/utils_k8s/cassandra_clusters.py +36 -0
- adam/utils_k8s/cassandra_nodes.py +33 -0
- adam/utils_k8s/config_maps.py +34 -0
- {walker/k8s_utils → adam/utils_k8s}/custom_resources.py +7 -2
- adam/utils_k8s/deployment.py +56 -0
- {walker/k8s_utils → adam/utils_k8s}/ingresses.py +3 -4
- {walker/k8s_utils → adam/utils_k8s}/jobs.py +3 -3
- adam/utils_k8s/k8s.py +87 -0
- {walker/k8s_utils → adam/utils_k8s}/kube_context.py +4 -4
- adam/utils_k8s/pods.py +290 -0
- {walker/k8s_utils → adam/utils_k8s}/secrets.py +8 -4
- adam/utils_k8s/service_accounts.py +170 -0
- {walker/k8s_utils → adam/utils_k8s}/services.py +3 -4
- {walker/k8s_utils → adam/utils_k8s}/statefulsets.py +6 -16
- {walker/k8s_utils → adam/utils_k8s}/volumes.py +10 -1
- adam/utils_net.py +24 -0
- adam/utils_repl/__init__.py +0 -0
- adam/utils_repl/automata_completer.py +48 -0
- adam/utils_repl/repl_completer.py +46 -0
- adam/utils_repl/state_machine.py +173 -0
- adam/utils_sqlite.py +109 -0
- adam/version.py +5 -0
- {kaqing-1.77.0.dist-info → kaqing-2.0.171.dist-info}/METADATA +1 -1
- kaqing-2.0.171.dist-info/RECORD +236 -0
- kaqing-2.0.171.dist-info/entry_points.txt +3 -0
- kaqing-2.0.171.dist-info/top_level.txt +1 -0
- kaqing-1.77.0.dist-info/RECORD +0 -159
- kaqing-1.77.0.dist-info/entry_points.txt +0 -3
- kaqing-1.77.0.dist-info/top_level.txt +0 -1
- walker/__init__.py +0 -3
- walker/app_session.py +0 -168
- walker/checks/check_utils.py +0 -97
- walker/columns/columns.py +0 -43
- walker/commands/add_user.py +0 -68
- walker/commands/app.py +0 -67
- walker/commands/bash.py +0 -87
- walker/commands/cd.py +0 -115
- walker/commands/check.py +0 -68
- walker/commands/command.py +0 -104
- walker/commands/cp.py +0 -95
- walker/commands/cql_utils.py +0 -53
- walker/commands/devices.py +0 -89
- walker/commands/frontend/code_stop.py +0 -57
- walker/commands/frontend/setup.py +0 -60
- walker/commands/frontend/setup_frontend.py +0 -58
- walker/commands/frontend/teardown.py +0 -61
- walker/commands/frontend/teardown_frontend.py +0 -42
- walker/commands/issues.py +0 -69
- walker/commands/login.py +0 -72
- walker/commands/ls.py +0 -145
- walker/commands/medusa/medusa.py +0 -69
- walker/commands/medusa/medusa_backup.py +0 -61
- walker/commands/medusa/medusa_restore.py +0 -86
- walker/commands/medusa/medusa_show_backupjobs.py +0 -52
- walker/commands/medusa/medusa_show_restorejobs.py +0 -52
- walker/commands/param_set.py +0 -44
- walker/commands/postgres/postgres.py +0 -113
- walker/commands/postgres/postgres_session.py +0 -225
- walker/commands/preview_table.py +0 -98
- walker/commands/processes.py +0 -53
- walker/commands/pwd.py +0 -64
- walker/commands/reaper/reaper.py +0 -78
- walker/commands/reaper/reaper_forward.py +0 -100
- walker/commands/reaper/reaper_run_abort.py +0 -65
- walker/commands/reaper/reaper_runs.py +0 -97
- walker/commands/reaper/reaper_runs_abort.py +0 -83
- walker/commands/reaper/reaper_schedule_activate.py +0 -64
- walker/commands/reaper/reaper_schedule_start.py +0 -64
- walker/commands/reaper/reaper_schedule_stop.py +0 -64
- walker/commands/reaper/reaper_session.py +0 -159
- walker/commands/repair/repair.py +0 -68
- walker/commands/repair/repair_run.py +0 -72
- walker/commands/repair/repair_scan.py +0 -79
- walker/commands/report.py +0 -57
- walker/commands/restart.py +0 -61
- walker/commands/show/show.py +0 -72
- walker/commands/show/show_app_actions.py +0 -53
- walker/commands/show/show_cassandra_status.py +0 -35
- walker/commands/show/show_commands.py +0 -58
- walker/commands/show/show_processes.py +0 -35
- walker/commands/show/show_repairs.py +0 -47
- walker/commands/status.py +0 -128
- walker/commands/storage.py +0 -52
- walker/commands/user_entry.py +0 -69
- walker/commands/watch.py +0 -85
- walker/embedded_params.py +0 -2
- walker/k8s_utils/cassandra_clusters.py +0 -48
- walker/k8s_utils/cassandra_nodes.py +0 -26
- walker/k8s_utils/pods.py +0 -211
- walker/repl.py +0 -165
- walker/repl_commands.py +0 -58
- walker/repl_state.py +0 -211
- walker/sso/authn_ad.py +0 -94
- walker/sso/idp.py +0 -150
- walker/sso/idp_login.py +0 -29
- walker/sso/sso_config.py +0 -45
- walker/utils.py +0 -194
- walker/version.py +0 -5
- {walker → adam}/checks/__init__.py +0 -0
- {walker → adam}/checks/check_context.py +0 -0
- {walker → adam}/checks/issue.py +0 -0
- {walker → adam}/cli_group.py +0 -0
- {walker → adam}/columns/__init__.py +0 -0
- {walker/commands → adam/commands/audit}/__init__.py +0 -0
- {walker/commands/frontend → adam/commands/cql}/__init__.py +0 -0
- {walker/commands/medusa → adam/commands/deploy}/__init__.py +0 -0
- {walker/commands/postgres → adam/commands/devices}/__init__.py +0 -0
- {walker/commands/reaper → adam/commands/export}/__init__.py +0 -0
- {walker/commands/repair → adam/commands/medusa}/__init__.py +0 -0
- {walker → adam}/commands/nodetool_commands.py +0 -0
- {walker/commands/show → adam/commands/postgres}/__init__.py +0 -0
- {walker/k8s_utils → adam/commands/reaper}/__init__.py +0 -0
- {walker/sso → adam/commands/repair}/__init__.py +0 -0
- /walker/medusa_show_restorejobs.py → /adam/commands/show/__init__.py +0 -0
- {walker → adam}/repl_session.py +0 -0
- {kaqing-1.77.0.dist-info → kaqing-2.0.171.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from typing import Callable, Iterable, List, Mapping, Optional, Pattern, Union
|
|
2
|
+
|
|
3
|
+
from prompt_toolkit.completion import CompleteEvent, Completion, WordCompleter
|
|
4
|
+
from prompt_toolkit.document import Document
|
|
5
|
+
from prompt_toolkit.formatted_text import AnyFormattedText
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"TermCompleter",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
class TermCompleter(WordCompleter):
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
words: Union[List[str], Callable[[], List[str]]],
|
|
15
|
+
ignore_case: bool = False,
|
|
16
|
+
display_dict: Optional[Mapping[str, AnyFormattedText]] = None,
|
|
17
|
+
meta_dict: Optional[Mapping[str, AnyFormattedText]] = None,
|
|
18
|
+
WORD: bool = False,
|
|
19
|
+
sentence: bool = False,
|
|
20
|
+
match_middle: bool = False,
|
|
21
|
+
pattern: Optional[Pattern[str]] = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
super().__init__(words, ignore_case, display_dict, meta_dict, WORD, sentence, match_middle, pattern)
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return ','.join(self.words)
|
|
27
|
+
|
|
28
|
+
def get_completions(
|
|
29
|
+
self, document: Document, complete_event: CompleteEvent
|
|
30
|
+
) -> Iterable[Completion]:
|
|
31
|
+
# Get list of words.
|
|
32
|
+
words = self.words
|
|
33
|
+
if callable(words):
|
|
34
|
+
words = words()
|
|
35
|
+
|
|
36
|
+
# Get word/text before cursor.
|
|
37
|
+
if self.sentence:
|
|
38
|
+
word_before_cursor = document.text_before_cursor
|
|
39
|
+
else:
|
|
40
|
+
word_before_cursor = document.get_word_before_cursor(
|
|
41
|
+
WORD=self.WORD, pattern=self.pattern
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if self.ignore_case:
|
|
45
|
+
word_before_cursor = word_before_cursor.lower()
|
|
46
|
+
|
|
47
|
+
def word_matches(word: str) -> bool:
|
|
48
|
+
"""True when the word before the cursor matches."""
|
|
49
|
+
if self.ignore_case:
|
|
50
|
+
word = word.lower()
|
|
51
|
+
|
|
52
|
+
if self.match_middle:
|
|
53
|
+
return word_before_cursor in word
|
|
54
|
+
else:
|
|
55
|
+
return word.startswith(word_before_cursor)
|
|
56
|
+
|
|
57
|
+
for a in words:
|
|
58
|
+
if word_before_cursor in ['(', ',', '=', 'in', "',"]:
|
|
59
|
+
display = self.display_dict.get(a, a)
|
|
60
|
+
display_meta = self.meta_dict.get(a, "")
|
|
61
|
+
yield Completion(
|
|
62
|
+
a,
|
|
63
|
+
0,
|
|
64
|
+
display=display,
|
|
65
|
+
display_meta=display_meta,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if word_matches(a):
|
|
69
|
+
display = self.display_dict.get(a, a)
|
|
70
|
+
display_meta = self.meta_dict.get(a, "")
|
|
71
|
+
yield Completion(
|
|
72
|
+
a,
|
|
73
|
+
-len(word_before_cursor),
|
|
74
|
+
display=display,
|
|
75
|
+
display_meta=display_meta,
|
|
76
|
+
)
|
adam/sso/__init__.py
ADDED
|
File without changes
|
|
@@ -5,7 +5,11 @@ from .idp_login import IdpLogin
|
|
|
5
5
|
|
|
6
6
|
class Authenticator:
|
|
7
7
|
@abstractmethod
|
|
8
|
-
def
|
|
8
|
+
def name(self) -> str:
|
|
9
|
+
return None
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def authenticate(self, idp_uri: str, app_host: str, username: str, password: str, verify: bool = True) -> IdpLogin:
|
|
9
13
|
pass
|
|
10
14
|
|
|
11
15
|
def extract(self, form: str, pattern: re.Pattern):
|
adam/sso/authn_ad.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import traceback
|
|
4
|
+
import jwt
|
|
5
|
+
import requests
|
|
6
|
+
from urllib.parse import urlparse, parse_qs
|
|
7
|
+
|
|
8
|
+
from adam.log import Log
|
|
9
|
+
from adam.sso.authenticator import Authenticator
|
|
10
|
+
from adam.sso.id_token import IdToken
|
|
11
|
+
from adam.utils import debug
|
|
12
|
+
from .idp_login import IdpLogin
|
|
13
|
+
from adam.config import Config
|
|
14
|
+
|
|
15
|
+
class AdException(Exception):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
class AdAuthenticator(Authenticator):
|
|
19
|
+
def name(self) -> str:
|
|
20
|
+
return 'ActiveDirectory'
|
|
21
|
+
|
|
22
|
+
# the singleton pattern
|
|
23
|
+
def __new__(cls, *args, **kwargs):
|
|
24
|
+
if not hasattr(cls, 'instance'): cls.instance = super(AdAuthenticator, cls).__new__(cls)
|
|
25
|
+
|
|
26
|
+
return cls.instance
|
|
27
|
+
|
|
28
|
+
def authenticate(self, idp_uri: str, app_host: str, username: str, password: str, verify: bool) -> IdpLogin:
|
|
29
|
+
parsed_url = urlparse(idp_uri)
|
|
30
|
+
query_string = parsed_url.query
|
|
31
|
+
params = parse_qs(query_string)
|
|
32
|
+
state_token = params.get('state', [''])[0]
|
|
33
|
+
redirect_url = params.get('redirect_uri', [''])[0]
|
|
34
|
+
|
|
35
|
+
session = requests.Session()
|
|
36
|
+
r = session.get(idp_uri)
|
|
37
|
+
debug(f'{r.status_code} {idp_uri}')
|
|
38
|
+
|
|
39
|
+
config = self.validate_and_return_config(r)
|
|
40
|
+
|
|
41
|
+
groups = re.match(r'(https://.*?/.*?)/.*', idp_uri)
|
|
42
|
+
if not groups:
|
|
43
|
+
raise AdException('Incorrect idp_uri configuration.')
|
|
44
|
+
|
|
45
|
+
login_uri = f'{groups[1]}/login'
|
|
46
|
+
body = {
|
|
47
|
+
'login': username,
|
|
48
|
+
'passwd': password,
|
|
49
|
+
'ctx': config['sCtx'],
|
|
50
|
+
'hpgrequestid': config['sessionId'],
|
|
51
|
+
'flowToken': config['sFT']
|
|
52
|
+
}
|
|
53
|
+
r = session.post(login_uri, data=body, headers={
|
|
54
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
55
|
+
})
|
|
56
|
+
debug(f'{r.status_code} {login_uri}')
|
|
57
|
+
|
|
58
|
+
config = self.validate_and_return_config(r)
|
|
59
|
+
|
|
60
|
+
groups = re.match(r'(https://.*?)/.*', idp_uri)
|
|
61
|
+
if not groups:
|
|
62
|
+
raise AdException('Incorrect idp_uri configuration.')
|
|
63
|
+
|
|
64
|
+
kmsi_uri = f'{groups[1]}/kmsi'
|
|
65
|
+
body = {
|
|
66
|
+
'ctx': config['sCtx'],
|
|
67
|
+
'hpgrequestid': config['sessionId'],
|
|
68
|
+
'flowToken': config['sFT'],
|
|
69
|
+
}
|
|
70
|
+
r = session.post(kmsi_uri, data=body, headers={
|
|
71
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
72
|
+
})
|
|
73
|
+
debug(f'{r.status_code} {kmsi_uri}')
|
|
74
|
+
|
|
75
|
+
if (config := self.extract_config_object(r.text)):
|
|
76
|
+
if 'sErrorCode' in config and config['sErrorCode'] == '50058':
|
|
77
|
+
raise AdException('Invalid username/password.')
|
|
78
|
+
elif 'strServiceExceptionMessage' in config:
|
|
79
|
+
raise AdException(config['strServiceExceptionMessage'])
|
|
80
|
+
else:
|
|
81
|
+
Log.log_to_file(config)
|
|
82
|
+
raise AdException('Unknown err.')
|
|
83
|
+
|
|
84
|
+
id_token = self.extract(r.text, r'.*name=\"id_token\" value=\"(.*?)\".*')
|
|
85
|
+
if not id_token:
|
|
86
|
+
raise AdException('Invalid username/password.')
|
|
87
|
+
|
|
88
|
+
if not verify:
|
|
89
|
+
return IdpLogin(redirect_url, id_token, state_token, username, idp_uri=idp_uri, id_token_obj=None, session=session)
|
|
90
|
+
|
|
91
|
+
parsed = self.parse_id_token(id_token)
|
|
92
|
+
roles = parsed.groups
|
|
93
|
+
roles.append(username)
|
|
94
|
+
whitelisted = self.whitelisted_members()
|
|
95
|
+
|
|
96
|
+
for role in roles:
|
|
97
|
+
if role in whitelisted:
|
|
98
|
+
return IdpLogin(redirect_url, id_token, state_token, username, idp_uri=idp_uri, id_token_obj=parsed, session=session)
|
|
99
|
+
|
|
100
|
+
contact = Config().get('idps.ad.contact', 'Please contact ted.tran@c3.ai.')
|
|
101
|
+
raise AdException(f'{username} is not whitelisted. {contact}')
|
|
102
|
+
|
|
103
|
+
def validate_and_return_config(self, r: requests.Response):
|
|
104
|
+
if r.status_code < 200 or r.status_code >= 300:
|
|
105
|
+
debug(r.text)
|
|
106
|
+
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
return self.extract_config_object(r.text)
|
|
110
|
+
|
|
111
|
+
def extract_config_object(self, text: str):
|
|
112
|
+
for line in text.split('\n'):
|
|
113
|
+
groups = re.match(r'.*\$Config=\s*(\{.*)', line)
|
|
114
|
+
if groups:
|
|
115
|
+
js = groups[1].replace(';', '')
|
|
116
|
+
config = json.loads(js)
|
|
117
|
+
|
|
118
|
+
return config
|
|
119
|
+
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def whitelisted_members(self) -> list[str]:
|
|
123
|
+
members_f = Config().get('idps.ad.whitelist-file', '/kaqing/members')
|
|
124
|
+
try:
|
|
125
|
+
with open(members_f, 'r') as file:
|
|
126
|
+
lines = file.readlines()
|
|
127
|
+
lines = [line.strip() for line in lines]
|
|
128
|
+
|
|
129
|
+
def is_non_comment(line: str):
|
|
130
|
+
return not line.startswith('#')
|
|
131
|
+
|
|
132
|
+
lines = list(filter(is_non_comment, lines))
|
|
133
|
+
|
|
134
|
+
return [line.split('#')[0].strip(' ') for line in lines]
|
|
135
|
+
except FileNotFoundError:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
def parse_id_token(self, id_token: str) -> IdToken:
|
|
141
|
+
jwks_url = Config().get('idps.ad.jwks-uri', '')
|
|
142
|
+
try:
|
|
143
|
+
jwks_client = jwt.PyJWKClient(jwks_url, cache_jwk_set=True, lifespan=360)
|
|
144
|
+
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
|
|
145
|
+
data = jwt.decode(
|
|
146
|
+
id_token,
|
|
147
|
+
signing_key.key,
|
|
148
|
+
algorithms=["RS256"],
|
|
149
|
+
options={
|
|
150
|
+
"verify_signature": True,
|
|
151
|
+
"verify_exp": False,
|
|
152
|
+
"verify_nbf": True,
|
|
153
|
+
"verify_iat": True,
|
|
154
|
+
"verify_aud": False,
|
|
155
|
+
"verify_iss": False,
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
return IdToken(
|
|
159
|
+
data,
|
|
160
|
+
data['email'],
|
|
161
|
+
data['name'],
|
|
162
|
+
groups=data['groups'] if 'groups' in data else [],
|
|
163
|
+
iat=data['iat'] if 'iat' in data else 0,
|
|
164
|
+
nbf=data['nbf'] if 'nbf' in data else 0,
|
|
165
|
+
exp=data['exp'] if 'exp' in data else 0
|
|
166
|
+
)
|
|
167
|
+
except:
|
|
168
|
+
debug(traceback.format_exc())
|
|
169
|
+
|
|
170
|
+
return None
|
|
@@ -3,20 +3,27 @@ import jwt
|
|
|
3
3
|
import requests
|
|
4
4
|
from urllib.parse import urlparse, parse_qs, unquote
|
|
5
5
|
|
|
6
|
-
from
|
|
6
|
+
from adam.sso.authenticator import Authenticator
|
|
7
|
+
from adam.sso.id_token import IdToken
|
|
7
8
|
|
|
8
9
|
from .idp_login import IdpLogin
|
|
9
|
-
from
|
|
10
|
-
from
|
|
10
|
+
from adam.config import Config
|
|
11
|
+
from adam.utils import debug, log2
|
|
12
|
+
|
|
13
|
+
class OktaException(Exception):
|
|
14
|
+
pass
|
|
11
15
|
|
|
12
16
|
class OktaAuthenticator(Authenticator):
|
|
17
|
+
def name(self) -> str:
|
|
18
|
+
return 'Okta'
|
|
19
|
+
|
|
13
20
|
# the singleton pattern
|
|
14
21
|
def __new__(cls, *args, **kwargs):
|
|
15
22
|
if not hasattr(cls, 'instance'): cls.instance = super(OktaAuthenticator, cls).__new__(cls)
|
|
16
23
|
|
|
17
24
|
return cls.instance
|
|
18
25
|
|
|
19
|
-
def authenticate(self, idp_uri: str, app_host: str, username: str, password: str) -> IdpLogin:
|
|
26
|
+
def authenticate(self, idp_uri: str, app_host: str, username: str, password: str, verify: bool) -> IdpLogin:
|
|
20
27
|
parsed_url = urlparse(idp_uri)
|
|
21
28
|
query_string = parsed_url.query
|
|
22
29
|
params = parse_qs(query_string)
|
|
@@ -25,7 +32,7 @@ class OktaAuthenticator(Authenticator):
|
|
|
25
32
|
|
|
26
33
|
okta_host = parsed_url.hostname
|
|
27
34
|
|
|
28
|
-
|
|
35
|
+
authn_uri = f"https://{okta_host}/api/v1/authn"
|
|
29
36
|
payload = {
|
|
30
37
|
"username": username,
|
|
31
38
|
"password": password,
|
|
@@ -41,20 +48,18 @@ class OktaAuthenticator(Authenticator):
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
session = requests.Session()
|
|
44
|
-
response = session.post(
|
|
45
|
-
|
|
46
|
-
log2(f'{response.status_code} {url}')
|
|
51
|
+
response = session.post(authn_uri, headers=headers, data=json.dumps(payload))
|
|
52
|
+
debug(f'{response.status_code} {authn_uri}')
|
|
47
53
|
auth_response = response.json()
|
|
48
54
|
|
|
49
55
|
if 'sessionToken' not in auth_response:
|
|
50
|
-
|
|
56
|
+
raise OktaException('Invalid username/password.')
|
|
51
57
|
|
|
52
58
|
session_token = auth_response['sessionToken']
|
|
53
59
|
|
|
54
60
|
url = f'{idp_uri}&sessionToken={session_token}'
|
|
55
61
|
r = session.get(url)
|
|
56
|
-
|
|
57
|
-
log2(f'{r.status_code} {url}')
|
|
62
|
+
debug(f'{r.status_code} {url}')
|
|
58
63
|
|
|
59
64
|
id_token = OktaAuthenticator().extract(r.text, r'.*name=\"id_token\" value=\"(.*?)\".*')
|
|
60
65
|
if not id_token:
|
|
@@ -64,28 +69,32 @@ class OktaAuthenticator(Authenticator):
|
|
|
64
69
|
else:
|
|
65
70
|
log2('id_token not found\n' + r.text)
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
raise OktaException('Invalid username/password.')
|
|
73
|
+
|
|
74
|
+
if not verify:
|
|
75
|
+
# just relay id_token, it will be verified by SP
|
|
76
|
+
return IdpLogin(redirect_url, id_token, state_token, username, idp_uri=idp_uri, id_token_obj=None, session=session)
|
|
68
77
|
|
|
69
78
|
if group := Config().get('app.login.admin-group', '{host}/C3.ClusterAdmin').replace('{host}', app_host):
|
|
70
|
-
|
|
79
|
+
parsed = OktaAuthenticator.parse_id_token(okta_host, id_token)
|
|
80
|
+
if group not in parsed.groups:
|
|
71
81
|
tks = group.split('/')
|
|
72
82
|
group = tks[len(tks) - 1]
|
|
73
83
|
log2(f'{username} is not a member of {group}.')
|
|
74
84
|
|
|
75
|
-
|
|
85
|
+
raise OktaException("You are not part of admin group.")
|
|
76
86
|
|
|
77
|
-
return IdpLogin(redirect_url, id_token, state_token, username, session=session)
|
|
87
|
+
return IdpLogin(redirect_url, id_token, state_token, username, idp_uri=idp_uri, id_token_obj=parsed, session=session)
|
|
78
88
|
|
|
79
|
-
def
|
|
80
|
-
|
|
89
|
+
def parse_id_token(idp_host, id_token) -> IdToken:
|
|
90
|
+
data: dict[str, any] = []
|
|
81
91
|
|
|
82
92
|
if not jwt.algorithms.has_crypto:
|
|
83
93
|
log2("No crypto support for JWT, please install the cryptography dependency")
|
|
84
94
|
|
|
85
|
-
return
|
|
95
|
+
return None
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
jwks_url = f"{okta_auth_server}/v1/keys"
|
|
97
|
+
jwks_url = Config().get('idps.okta.jwks-uri', 'https://c3energy.okta.com/oauth2/v1/keys')
|
|
89
98
|
try:
|
|
90
99
|
jwks_client = jwt.PyJWKClient(jwks_url, cache_jwk_set=True, lifespan=360)
|
|
91
100
|
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
|
|
@@ -103,8 +112,16 @@ class OktaAuthenticator(Authenticator):
|
|
|
103
112
|
},
|
|
104
113
|
)
|
|
105
114
|
|
|
106
|
-
return
|
|
115
|
+
return IdToken(
|
|
116
|
+
data,
|
|
117
|
+
data['email'],
|
|
118
|
+
data['name'],
|
|
119
|
+
groups=data['groups'] if 'groups' in data else [],
|
|
120
|
+
iat=data['iat'] if 'iat' in data else 0,
|
|
121
|
+
nbf=data['nbf'] if 'nbf' in data else 0,
|
|
122
|
+
exp=data['exp'] if 'exp' in data else 0
|
|
123
|
+
)
|
|
107
124
|
except:
|
|
108
125
|
pass
|
|
109
126
|
|
|
110
|
-
return
|
|
127
|
+
return None
|
adam/sso/cred_cache.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import traceback
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
from adam.config import Config
|
|
7
|
+
from adam.utils import debug
|
|
8
|
+
from adam.utils_k8s.kube_context import KubeContext
|
|
9
|
+
|
|
10
|
+
class CredCache:
|
|
11
|
+
# the singleton pattern
|
|
12
|
+
def __new__(cls, *args, **kwargs):
|
|
13
|
+
if not hasattr(cls, 'instance'): cls.instance = super(CredCache, cls).__new__(cls)
|
|
14
|
+
|
|
15
|
+
return cls.instance
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
if not hasattr(self, 'env_f'):
|
|
19
|
+
self.dir = f'{Path.home()}/.kaqing'
|
|
20
|
+
self.env_f = f'{self.dir}/.credentials'
|
|
21
|
+
# immutable - cannot reload with different file content
|
|
22
|
+
load_dotenv(dotenv_path=self.env_f)
|
|
23
|
+
|
|
24
|
+
self.overrides: dict[str, str] = {}
|
|
25
|
+
|
|
26
|
+
def get_username(self):
|
|
27
|
+
return self.overrides['IDP_USERNAME'] if 'IDP_USERNAME' in self.overrides else self.get('IDP_USERNAME')
|
|
28
|
+
|
|
29
|
+
def get_password(self):
|
|
30
|
+
return self.overrides['IDP_PASSWORD'] if 'IDP_PASSWORD' in self.overrides else self.get('IDP_PASSWORD')
|
|
31
|
+
|
|
32
|
+
def get(self, key: str) -> str:
|
|
33
|
+
return os.getenv(key)
|
|
34
|
+
|
|
35
|
+
def cache(self, username: str, password: str = None):
|
|
36
|
+
if os.path.exists(self.env_f):
|
|
37
|
+
with open(self.env_f, 'w') as file:
|
|
38
|
+
try:
|
|
39
|
+
file.truncate()
|
|
40
|
+
except:
|
|
41
|
+
debug(traceback.format_exc())
|
|
42
|
+
|
|
43
|
+
updated = []
|
|
44
|
+
updated.append(f'IDP_USERNAME={username}')
|
|
45
|
+
if not KubeContext.in_cluster() and password:
|
|
46
|
+
# do not store password to the .credentials file when in Kubernetes pod
|
|
47
|
+
updated.append(f'IDP_PASSWORD={password}')
|
|
48
|
+
|
|
49
|
+
if updated:
|
|
50
|
+
if not os.path.exists(self.env_f):
|
|
51
|
+
os.makedirs(self.dir, exist_ok=True)
|
|
52
|
+
with open(self.env_f, 'w') as file:
|
|
53
|
+
file.write('\n'.join(updated))
|
|
54
|
+
|
|
55
|
+
if username:
|
|
56
|
+
self.overrides['IDP_USERNAME'] = username
|
|
57
|
+
if password:
|
|
58
|
+
self.overrides['IDP_PASSWORD'] = password
|
|
59
|
+
|
|
60
|
+
debug(f'Cached username: {username}, password: {password}, try load: {self.get_username()}')
|
adam/sso/id_token.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class IdToken:
|
|
2
|
+
def __init__(self, data: dict[str, any], email: str, username: str, groups: list[str] = [], iat: int = 0, nbf: int = 0, exp: int = 0):
|
|
3
|
+
self.data = data
|
|
4
|
+
self.email = email
|
|
5
|
+
self.username = username
|
|
6
|
+
self.groups = groups
|
|
7
|
+
self.iat = iat
|
|
8
|
+
self.nbf = nbf
|
|
9
|
+
self.exp = exp
|
|
10
|
+
|
|
11
|
+
def from_dict(j: dict[str, any]):
|
|
12
|
+
return IdToken(
|
|
13
|
+
j['data'],
|
|
14
|
+
j['email'],
|
|
15
|
+
j['username'],
|
|
16
|
+
groups=j['groups'] if 'groups' in j else None,
|
|
17
|
+
iat=j['iat'] if 'iat' in j else None,
|
|
18
|
+
nbf=j['nbf'] if 'nbf' in j else None,
|
|
19
|
+
exp=j['exp'] if 'exp' in j else None
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def to_dict(self):
|
|
23
|
+
return self.__dict__
|
adam/sso/idp.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import getpass
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import termios
|
|
6
|
+
import traceback
|
|
7
|
+
from typing import Callable, TypeVar
|
|
8
|
+
import requests
|
|
9
|
+
from kubernetes import config
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from adam.utils_k8s.secrets import Secrets
|
|
13
|
+
|
|
14
|
+
from .cred_cache import CredCache
|
|
15
|
+
from .idp_session import IdpSession
|
|
16
|
+
from .idp_login import IdpLogin
|
|
17
|
+
from adam.config import Config
|
|
18
|
+
from adam.utils import debug, log
|
|
19
|
+
|
|
20
|
+
T = TypeVar('T')
|
|
21
|
+
|
|
22
|
+
class Idp:
|
|
23
|
+
ctrl_c_entered = False
|
|
24
|
+
|
|
25
|
+
# the singleton pattern
|
|
26
|
+
def __new__(cls, *args, **kwargs):
|
|
27
|
+
if not hasattr(cls, 'instance'): cls.instance = super(Idp, cls).__new__(cls)
|
|
28
|
+
|
|
29
|
+
return cls.instance
|
|
30
|
+
|
|
31
|
+
def login(app_host: str, username: str = None, idp_uri: str = None, forced = False, use_token_from_env = True, use_cached_creds = True, verify = True) -> IdpLogin:
|
|
32
|
+
session: IdpSession = IdpSession.create(username, app_host, app_host, idp_uri=idp_uri)
|
|
33
|
+
|
|
34
|
+
if use_token_from_env:
|
|
35
|
+
if l0 := session.login_from_env_var():
|
|
36
|
+
return l0
|
|
37
|
+
|
|
38
|
+
if port := os.getenv("SERVER_PORT"):
|
|
39
|
+
token_server = Config().get('app.login.token-server-url', 'http://localhost:{port}').replace('{port}', port)
|
|
40
|
+
res: requests.Response = requests.get(token_server)
|
|
41
|
+
if res.status_code == 200 and res.text:
|
|
42
|
+
try:
|
|
43
|
+
# may fail if the idp token is not complete
|
|
44
|
+
return session.login_from_token(res.text)
|
|
45
|
+
except:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
r: IdpLogin = None
|
|
49
|
+
try:
|
|
50
|
+
if username:
|
|
51
|
+
log(f'{session.idp_host()} login: {username}')
|
|
52
|
+
|
|
53
|
+
while not username or Idp.ctrl_c_entered:
|
|
54
|
+
if Idp.ctrl_c_entered:
|
|
55
|
+
Idp.ctrl_c_entered = False
|
|
56
|
+
|
|
57
|
+
default_user: str = None
|
|
58
|
+
if use_cached_creds:
|
|
59
|
+
default_user = CredCache().get_username()
|
|
60
|
+
debug(f'User read from cache: {default_user}')
|
|
61
|
+
|
|
62
|
+
if from_env := os.getenv('USERNAME'):
|
|
63
|
+
default_user = from_env
|
|
64
|
+
if default_user and default_user != username:
|
|
65
|
+
session = IdpSession.create(default_user, app_host, app_host)
|
|
66
|
+
|
|
67
|
+
if forced:
|
|
68
|
+
username = default_user
|
|
69
|
+
else:
|
|
70
|
+
username = input(f'{session.idp_host()} login(default {default_user}): ') or default_user
|
|
71
|
+
else:
|
|
72
|
+
username = input(f'{session.idp_host()} login: ')
|
|
73
|
+
|
|
74
|
+
session2: IdpSession = IdpSession.create(username, app_host, app_host)
|
|
75
|
+
if session.idp_host() != session2.idp_host():
|
|
76
|
+
session = session2
|
|
77
|
+
|
|
78
|
+
log(f'Switching to {session.idp_host()}...')
|
|
79
|
+
log()
|
|
80
|
+
log(f'{session.idp_host()} login: {username}')
|
|
81
|
+
|
|
82
|
+
password = None
|
|
83
|
+
while password == None or Idp.ctrl_c_entered: # exit the while loop even if password is empty string
|
|
84
|
+
if Idp.ctrl_c_entered:
|
|
85
|
+
Idp.ctrl_c_entered = False
|
|
86
|
+
|
|
87
|
+
default_pass = CredCache().get_password() if use_cached_creds else None
|
|
88
|
+
if default_pass:
|
|
89
|
+
if forced:
|
|
90
|
+
password = default_pass
|
|
91
|
+
else:
|
|
92
|
+
password = Idp.with_no_ican(lambda: getpass.getpass(f'Password(default ********): ') or default_pass)
|
|
93
|
+
else:
|
|
94
|
+
password = Idp.with_no_ican(lambda: getpass.getpass(f'Password: '))
|
|
95
|
+
|
|
96
|
+
if username and password:
|
|
97
|
+
# if uploading kubeconfig file fails many times, you will be locked out
|
|
98
|
+
# kubeconfig file content has first char as tab or length of bigger than 128
|
|
99
|
+
if password[0] == '\t' or len(password) > Config().get('app.login.password-max-length', 128):
|
|
100
|
+
if r := Idp.try_kubeconfig(username, password):
|
|
101
|
+
log(f"You're signed in as {username}")
|
|
102
|
+
return r
|
|
103
|
+
else:
|
|
104
|
+
if r := session.authenticator.authenticate(session.idp_uri, app_host, username, password, verify=verify):
|
|
105
|
+
log(f"You're signed in as {username}")
|
|
106
|
+
return r
|
|
107
|
+
finally:
|
|
108
|
+
if r and Config().get('app.login.cache-creds', True):
|
|
109
|
+
CredCache().cache(username, password)
|
|
110
|
+
elif username and Config().get('app.login.cache-username', True):
|
|
111
|
+
CredCache().cache(username)
|
|
112
|
+
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
def with_no_ican(body: Callable[[], T]) -> T:
|
|
116
|
+
# override 4096 character limit with OS terminal - stty -noican
|
|
117
|
+
fd = sys.stdin.fileno()
|
|
118
|
+
old = termios.tcgetattr(fd)
|
|
119
|
+
new = termios.tcgetattr(fd)
|
|
120
|
+
new[3] = new[3] & ~termios.ICANON
|
|
121
|
+
try:
|
|
122
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, new)
|
|
123
|
+
return body()
|
|
124
|
+
finally:
|
|
125
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old)
|
|
126
|
+
|
|
127
|
+
def try_kubeconfig(username: str, kubeconfig: str):
|
|
128
|
+
try:
|
|
129
|
+
if kubeconfig[0] == '\t':
|
|
130
|
+
kubeconfig = kubeconfig[1:]
|
|
131
|
+
kubeconfig_string = base64.b64decode(kubeconfig.encode('ascii') + b'==').decode('utf-8')
|
|
132
|
+
if kubeconfig_string.startswith('apiVersion: '):
|
|
133
|
+
kubeconfig_dict = yaml.safe_load(kubeconfig_string)
|
|
134
|
+
config.kube_config.load_kube_config_from_dict(kubeconfig_dict)
|
|
135
|
+
# test if you can list Kubernetes secretes with the given kubeconfig file
|
|
136
|
+
Secrets.list_secrets(os.getenv('NAMESPACE'))
|
|
137
|
+
|
|
138
|
+
return IdpLogin(None, None, None, username)
|
|
139
|
+
except:
|
|
140
|
+
debug(traceback.format_exc())
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
return None
|
adam/sso/idp_login.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
from urllib.parse import parse_qs, urlparse
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from adam.sso.id_token import IdToken
|
|
7
|
+
|
|
8
|
+
class IdpLogin:
|
|
9
|
+
def __init__(self, app_login_url: str, id_token: str, state: str, user: str = None, idp_uri: str = None, id_token_obj: IdToken = None, session: requests.Session = None):
|
|
10
|
+
self.app_login_url = app_login_url
|
|
11
|
+
self.id_token = id_token
|
|
12
|
+
self.state = state
|
|
13
|
+
self.user = user
|
|
14
|
+
self.idp_uri = idp_uri
|
|
15
|
+
self.id_token_obj = id_token_obj
|
|
16
|
+
self.session = session
|
|
17
|
+
|
|
18
|
+
def deser(idp_token: str):
|
|
19
|
+
j = json.loads(base64.b64decode(idp_token.encode('utf-8')))
|
|
20
|
+
|
|
21
|
+
return IdpLogin(
|
|
22
|
+
j['r'],
|
|
23
|
+
j['id_token'],
|
|
24
|
+
j['state'],
|
|
25
|
+
idp_uri=j['idp_uri'] if 'idp_uri' in j else None,
|
|
26
|
+
id_token_obj=IdToken.from_dict(j['id_token_obj']) if 'id_token_obj' in j else None)
|
|
27
|
+
|
|
28
|
+
def ser(self):
|
|
29
|
+
return base64.b64encode(json.dumps({
|
|
30
|
+
'r': self.app_login_url,
|
|
31
|
+
'id_token': self.id_token,
|
|
32
|
+
'state': self.state,
|
|
33
|
+
'idp_uri': self.idp_uri,
|
|
34
|
+
'id_token_obj': self.id_token_obj.to_dict() if self.id_token_obj else None
|
|
35
|
+
}).encode('utf-8')).decode('utf-8')
|
|
36
|
+
|
|
37
|
+
def create_from_idp_uri(idp_uri: str):
|
|
38
|
+
parsed_url = urlparse(idp_uri)
|
|
39
|
+
query_string = parsed_url.query
|
|
40
|
+
params = parse_qs(query_string)
|
|
41
|
+
state_token = params.get('state', [''])[0]
|
|
42
|
+
redirect_url = params.get('redirect_uri', [''])[0]
|
|
43
|
+
|
|
44
|
+
return IdpLogin(app_login_url=redirect_url, id_token=None, state=state_token, idp_uri=idp_uri)
|
|
45
|
+
|
|
46
|
+
def shell_user(self):
|
|
47
|
+
if not self.user:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
return self.user.split('@')[0].replace('.', '')
|