kaqing 1.98.15__py3-none-any.whl → 2.0.145__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.
Potentially problematic release.
This version of kaqing might be problematic. Click here for more details.
- adam/app_session.py +1 -1
- adam/apps.py +2 -2
- adam/batch.py +30 -31
- adam/checks/check_utils.py +4 -4
- adam/checks/compactionstats.py +1 -1
- adam/checks/cpu.py +2 -2
- adam/checks/disk.py +1 -1
- adam/checks/gossip.py +1 -1
- adam/checks/memory.py +3 -3
- adam/checks/status.py +1 -1
- adam/commands/alter_tables.py +81 -0
- adam/commands/app.py +3 -3
- adam/commands/app_ping.py +2 -2
- adam/commands/audit/audit.py +86 -0
- adam/commands/audit/audit_repair_tables.py +77 -0
- adam/commands/audit/audit_run.py +58 -0
- adam/commands/audit/show_last10.py +51 -0
- adam/commands/audit/show_slow10.py +50 -0
- adam/commands/audit/show_top10.py +48 -0
- adam/commands/audit/utils_show_top10.py +59 -0
- adam/commands/bash/bash.py +133 -0
- adam/commands/bash/bash_completer.py +93 -0
- adam/commands/cat.py +56 -0
- adam/commands/cd.py +12 -82
- adam/commands/check.py +6 -0
- adam/commands/cli_commands.py +3 -3
- adam/commands/code.py +60 -0
- adam/commands/command.py +48 -12
- adam/commands/commands_utils.py +4 -5
- adam/commands/cql/cql_completions.py +28 -0
- adam/commands/cql/cql_utils.py +209 -0
- adam/commands/{cqlsh.py → cql/cqlsh.py} +15 -10
- adam/commands/deploy/__init__.py +0 -0
- adam/commands/{frontend → deploy}/code_start.py +1 -1
- adam/commands/{frontend → deploy}/code_stop.py +1 -1
- adam/commands/{frontend → deploy}/code_utils.py +2 -2
- adam/commands/deploy/deploy.py +48 -0
- adam/commands/deploy/deploy_frontend.py +52 -0
- adam/commands/deploy/deploy_pg_agent.py +38 -0
- adam/commands/deploy/deploy_pod.py +110 -0
- adam/commands/deploy/deploy_utils.py +29 -0
- adam/commands/deploy/undeploy.py +48 -0
- adam/commands/deploy/undeploy_frontend.py +41 -0
- adam/commands/deploy/undeploy_pg_agent.py +42 -0
- adam/commands/deploy/undeploy_pod.py +51 -0
- adam/commands/devices/__init__.py +0 -0
- adam/commands/devices/device.py +27 -0
- adam/commands/devices/device_app.py +146 -0
- adam/commands/devices/device_auit_log.py +43 -0
- adam/commands/devices/device_cass.py +145 -0
- adam/commands/devices/device_export.py +86 -0
- adam/commands/devices/device_postgres.py +109 -0
- adam/commands/devices/devices.py +25 -0
- adam/commands/export/__init__.py +0 -0
- adam/commands/export/clean_up_export_session.py +53 -0
- adam/commands/{frontend/teardown_frontend.py → export/clean_up_export_sessions.py} +9 -11
- adam/commands/export/drop_export_database.py +58 -0
- adam/commands/export/drop_export_databases.py +46 -0
- adam/commands/export/export.py +83 -0
- adam/commands/export/export_databases.py +170 -0
- adam/commands/export/export_select.py +85 -0
- adam/commands/export/export_select_x.py +54 -0
- adam/commands/export/export_use.py +55 -0
- adam/commands/export/exporter.py +364 -0
- adam/commands/export/import_session.py +68 -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 +63 -0
- adam/commands/export/show_export_databases.py +39 -0
- adam/commands/export/show_export_session.py +51 -0
- adam/commands/export/show_export_sessions.py +47 -0
- adam/commands/export/utils_export.py +291 -0
- adam/commands/help.py +12 -7
- adam/commands/issues.py +6 -0
- adam/commands/kubectl.py +41 -0
- adam/commands/login.py +9 -5
- adam/commands/logs.py +2 -1
- adam/commands/ls.py +4 -107
- adam/commands/medusa/medusa.py +2 -26
- adam/commands/medusa/medusa_backup.py +2 -2
- adam/commands/medusa/medusa_restore.py +3 -4
- adam/commands/medusa/medusa_show_backupjobs.py +4 -3
- adam/commands/medusa/medusa_show_restorejobs.py +3 -3
- adam/commands/nodetool.py +9 -4
- adam/commands/param_set.py +1 -1
- adam/commands/postgres/postgres.py +42 -43
- adam/commands/postgres/postgres_context.py +248 -0
- adam/commands/postgres/postgres_preview.py +0 -1
- adam/commands/postgres/postgres_utils.py +31 -0
- adam/commands/postgres/psql_completions.py +10 -0
- adam/commands/preview_table.py +18 -40
- adam/commands/pwd.py +2 -28
- adam/commands/reaper/reaper.py +4 -24
- adam/commands/reaper/reaper_restart.py +1 -1
- adam/commands/reaper/reaper_session.py +2 -2
- adam/commands/repair/repair.py +3 -27
- adam/commands/repair/repair_log.py +1 -1
- adam/commands/repair/repair_run.py +2 -2
- adam/commands/repair/repair_scan.py +2 -7
- adam/commands/repair/repair_stop.py +1 -1
- adam/commands/report.py +6 -0
- adam/commands/restart.py +2 -2
- adam/commands/rollout.py +1 -1
- adam/commands/shell.py +33 -0
- adam/commands/show/show.py +11 -26
- adam/commands/show/show_app_actions.py +3 -0
- adam/commands/show/show_app_id.py +1 -1
- adam/commands/show/show_app_queues.py +3 -2
- adam/commands/show/show_cassandra_status.py +3 -3
- adam/commands/show/show_cassandra_version.py +3 -3
- adam/commands/show/show_commands.py +4 -1
- adam/commands/show/show_host.py +33 -0
- adam/commands/show/show_login.py +3 -0
- adam/commands/show/show_processes.py +1 -1
- adam/commands/show/show_repairs.py +2 -2
- adam/commands/show/show_storage.py +1 -1
- adam/commands/watch.py +1 -1
- adam/config.py +16 -3
- adam/embedded_params.py +1 -1
- adam/pod_exec_result.py +10 -2
- adam/repl.py +132 -117
- adam/repl_commands.py +62 -18
- adam/repl_state.py +276 -55
- adam/sql/__init__.py +0 -0
- adam/sql/sql_completer.py +120 -0
- adam/sql/sql_state_machine.py +617 -0
- adam/sql/term_completer.py +76 -0
- adam/sso/authenticator.py +1 -1
- adam/sso/authn_ad.py +36 -56
- adam/sso/authn_okta.py +6 -32
- adam/sso/cred_cache.py +1 -1
- adam/sso/idp.py +74 -9
- adam/sso/idp_login.py +2 -2
- adam/sso/idp_session.py +10 -7
- adam/utils.py +85 -4
- adam/utils_athena.py +145 -0
- adam/utils_audits.py +102 -0
- adam/utils_k8s/__init__.py +0 -0
- adam/utils_k8s/app_clusters.py +33 -0
- adam/utils_k8s/app_pods.py +31 -0
- adam/{k8s_utils → utils_k8s}/cassandra_clusters.py +6 -21
- adam/{k8s_utils → utils_k8s}/cassandra_nodes.py +12 -5
- adam/utils_k8s/config_maps.py +34 -0
- adam/utils_k8s/deployment.py +56 -0
- adam/{k8s_utils → utils_k8s}/jobs.py +1 -1
- adam/{k8s_utils → utils_k8s}/kube_context.py +1 -1
- adam/utils_k8s/pods.py +342 -0
- adam/{k8s_utils → utils_k8s}/secrets.py +4 -0
- adam/utils_k8s/service_accounts.py +169 -0
- adam/{k8s_utils → utils_k8s}/statefulsets.py +5 -4
- adam/{k8s_utils → utils_k8s}/volumes.py +9 -0
- 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 +101 -0
- adam/version.py +1 -1
- {kaqing-1.98.15.dist-info → kaqing-2.0.145.dist-info}/METADATA +1 -1
- kaqing-2.0.145.dist-info/RECORD +227 -0
- adam/commands/bash.py +0 -87
- adam/commands/cql_utils.py +0 -53
- adam/commands/devices.py +0 -89
- adam/commands/frontend/setup.py +0 -60
- adam/commands/frontend/setup_frontend.py +0 -58
- adam/commands/frontend/teardown.py +0 -61
- adam/commands/postgres/postgres_session.py +0 -225
- adam/commands/user_entry.py +0 -77
- adam/k8s_utils/pods.py +0 -211
- kaqing-1.98.15.dist-info/RECORD +0 -160
- /adam/commands/{frontend → audit}/__init__.py +0 -0
- /adam/{k8s_utils → commands/bash}/__init__.py +0 -0
- /adam/{medusa_show_restorejobs.py → commands/cql/__init__.py} +0 -0
- /adam/{k8s_utils → utils_k8s}/custom_resources.py +0 -0
- /adam/{k8s_utils → utils_k8s}/ingresses.py +0 -0
- /adam/{k8s_utils → utils_k8s}/services.py +0 -0
- {kaqing-1.98.15.dist-info → kaqing-2.0.145.dist-info}/WHEEL +0 -0
- {kaqing-1.98.15.dist-info → kaqing-2.0.145.dist-info}/entry_points.txt +0 -0
- {kaqing-1.98.15.dist-info → kaqing-2.0.145.dist-info}/top_level.txt +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/authenticator.py
CHANGED
|
@@ -9,7 +9,7 @@ class Authenticator:
|
|
|
9
9
|
return None
|
|
10
10
|
|
|
11
11
|
@abstractmethod
|
|
12
|
-
def authenticate(self, idp_uri: str, app_host: str, username: str, password: str) -> IdpLogin:
|
|
12
|
+
def authenticate(self, idp_uri: str, app_host: str, username: str, password: str, verify: bool = True) -> IdpLogin:
|
|
13
13
|
pass
|
|
14
14
|
|
|
15
15
|
def extract(self, form: str, pattern: re.Pattern):
|
adam/sso/authn_ad.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import base64
|
|
2
1
|
import json
|
|
3
2
|
import re
|
|
3
|
+
import traceback
|
|
4
|
+
import jwt
|
|
4
5
|
import requests
|
|
5
6
|
from urllib.parse import urlparse, parse_qs
|
|
6
7
|
|
|
7
8
|
from adam.log import Log
|
|
8
9
|
from adam.sso.authenticator import Authenticator
|
|
9
10
|
from adam.sso.id_token import IdToken
|
|
10
|
-
|
|
11
11
|
from .idp_login import IdpLogin
|
|
12
12
|
from adam.config import Config
|
|
13
13
|
|
|
@@ -24,7 +24,7 @@ class AdAuthenticator(Authenticator):
|
|
|
24
24
|
|
|
25
25
|
return cls.instance
|
|
26
26
|
|
|
27
|
-
def authenticate(self, idp_uri: str, app_host: str, username: str, password: str) -> IdpLogin:
|
|
27
|
+
def authenticate(self, idp_uri: str, app_host: str, username: str, password: str, verify: bool) -> IdpLogin:
|
|
28
28
|
parsed_url = urlparse(idp_uri)
|
|
29
29
|
query_string = parsed_url.query
|
|
30
30
|
params = parse_qs(query_string)
|
|
@@ -84,6 +84,9 @@ class AdAuthenticator(Authenticator):
|
|
|
84
84
|
if not id_token:
|
|
85
85
|
raise AdException('Invalid username/password.')
|
|
86
86
|
|
|
87
|
+
if not verify:
|
|
88
|
+
return IdpLogin(redirect_url, id_token, state_token, username, idp_uri=idp_uri, id_token_obj=None, session=session)
|
|
89
|
+
|
|
87
90
|
parsed = self.parse_id_token(id_token)
|
|
88
91
|
roles = parsed.groups
|
|
89
92
|
roles.append(username)
|
|
@@ -134,56 +137,33 @@ class AdAuthenticator(Authenticator):
|
|
|
134
137
|
return []
|
|
135
138
|
|
|
136
139
|
def parse_id_token(self, id_token: str) -> IdToken:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
# 'c4fbb32c-9892-4eb3-a829-b4eaaf71b4ef'
|
|
168
|
-
# ],
|
|
169
|
-
# 'name': 'Sean Ahn',
|
|
170
|
-
# 'nonce': 'V7DzmHtmhu3X3tzZmC55vMQ2WJtqXnW6wJpYb3Kfud8',
|
|
171
|
-
# 'oid': '380029a1-0643-4764-b3b8-9bc02630af41',
|
|
172
|
-
# 'preferred_username': 'sean.ahn@c3.ai',
|
|
173
|
-
# 'rh': '1.ATcAmnetU-eTXEi6IKyCkNclK6iU_wAKaxVHmOCVSQAS2Bj1AJU3AA.',
|
|
174
|
-
# 'sid': '007dea79-a65c-7911-5142-8ea1b9faa41a',
|
|
175
|
-
# 'sub': 'd-iCznXBDo3HEV7UCGalCVFIG47dQl_SFCaQtN2yVQI',
|
|
176
|
-
# 'tid': '53ad779a-93e7-485c-ba20-ac8290d7252b',
|
|
177
|
-
# 'uti': 'b7gHorgqqEOuuPDIcSgHAQ',
|
|
178
|
-
# 'ver': '2.0'
|
|
179
|
-
# }
|
|
180
|
-
|
|
181
|
-
return IdToken(
|
|
182
|
-
data,
|
|
183
|
-
data['email'],
|
|
184
|
-
data['name'],
|
|
185
|
-
groups=data['groups'] if 'groups' in data else [],
|
|
186
|
-
iat=data['iat'] if 'iat' in data else 0,
|
|
187
|
-
nbf=data['nbf'] if 'nbf' in data else 0,
|
|
188
|
-
exp=data['exp'] if 'exp' in data else 0
|
|
189
|
-
)
|
|
140
|
+
jwks_url = Config().get('idps.ad.jwks-uri', '')
|
|
141
|
+
try:
|
|
142
|
+
jwks_client = jwt.PyJWKClient(jwks_url, cache_jwk_set=True, lifespan=360)
|
|
143
|
+
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
|
|
144
|
+
data = jwt.decode(
|
|
145
|
+
id_token,
|
|
146
|
+
signing_key.key,
|
|
147
|
+
algorithms=["RS256"],
|
|
148
|
+
options={
|
|
149
|
+
"verify_signature": True,
|
|
150
|
+
"verify_exp": False,
|
|
151
|
+
"verify_nbf": True,
|
|
152
|
+
"verify_iat": True,
|
|
153
|
+
"verify_aud": False,
|
|
154
|
+
"verify_iss": False,
|
|
155
|
+
},
|
|
156
|
+
)
|
|
157
|
+
return IdToken(
|
|
158
|
+
data,
|
|
159
|
+
data['email'],
|
|
160
|
+
data['name'],
|
|
161
|
+
groups=data['groups'] if 'groups' in data else [],
|
|
162
|
+
iat=data['iat'] if 'iat' in data else 0,
|
|
163
|
+
nbf=data['nbf'] if 'nbf' in data else 0,
|
|
164
|
+
exp=data['exp'] if 'exp' in data else 0
|
|
165
|
+
)
|
|
166
|
+
except:
|
|
167
|
+
Config().debug(traceback.format_exc())
|
|
168
|
+
|
|
169
|
+
return None
|
adam/sso/authn_okta.py
CHANGED
|
@@ -23,7 +23,7 @@ class OktaAuthenticator(Authenticator):
|
|
|
23
23
|
|
|
24
24
|
return cls.instance
|
|
25
25
|
|
|
26
|
-
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:
|
|
27
27
|
parsed_url = urlparse(idp_uri)
|
|
28
28
|
query_string = parsed_url.query
|
|
29
29
|
params = parse_qs(query_string)
|
|
@@ -53,7 +53,6 @@ class OktaAuthenticator(Authenticator):
|
|
|
53
53
|
auth_response = response.json()
|
|
54
54
|
|
|
55
55
|
if 'sessionToken' not in auth_response:
|
|
56
|
-
print('password', password, auth_response)
|
|
57
56
|
raise OktaException('Invalid username/password.')
|
|
58
57
|
|
|
59
58
|
session_token = auth_response['sessionToken']
|
|
@@ -72,6 +71,10 @@ class OktaAuthenticator(Authenticator):
|
|
|
72
71
|
|
|
73
72
|
raise OktaException('Invalid username/password.')
|
|
74
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)
|
|
77
|
+
|
|
75
78
|
if group := Config().get('app.login.admin-group', '{host}/C3.ClusterAdmin').replace('{host}', app_host):
|
|
76
79
|
parsed = OktaAuthenticator.parse_id_token(okta_host, id_token)
|
|
77
80
|
if group not in parsed.groups:
|
|
@@ -80,34 +83,6 @@ class OktaAuthenticator(Authenticator):
|
|
|
80
83
|
log2(f'{username} is not a member of {group}.')
|
|
81
84
|
|
|
82
85
|
raise OktaException("You are not part of admin group.")
|
|
83
|
-
# print(parsed)
|
|
84
|
-
# {
|
|
85
|
-
# 'sub': '00u1fii2azgmh46dE1d8',
|
|
86
|
-
# 'name': 'Sean Ahn',
|
|
87
|
-
# 'locale': 'US',
|
|
88
|
-
# 'email': 'sean.ahn@c3iot.com',
|
|
89
|
-
# 'ver': 1,
|
|
90
|
-
# 'iss': 'https://c3energy.okta.com',
|
|
91
|
-
# 'aud': 'azops88.c3.ai',
|
|
92
|
-
# 'iat': 1756136559,
|
|
93
|
-
# 'exp': 1756140159,
|
|
94
|
-
# 'jti': 'ID.fIjNa_A-uYo-mKMqpN_CLPqgQUnDIHeBV4WGa-Q_Qx8',
|
|
95
|
-
# 'amr': [
|
|
96
|
-
# 'pwd'
|
|
97
|
-
# ],
|
|
98
|
-
# 'idp': '00onsxj5mmBGYLRURHTH',
|
|
99
|
-
# 'nonce': 'kRpFTGiyUGg6uWfdCUkS3tGI4wrroCz3MTaeI_wTILE',
|
|
100
|
-
# 'preferred_username': 'sean.ahn@c3iot.com',
|
|
101
|
-
# 'given_name': 'Seung',
|
|
102
|
-
# 'family_name': 'Ahn',
|
|
103
|
-
# 'zoneinfo': 'America/Los_Angeles',
|
|
104
|
-
# 'updated_at': 1755201182,
|
|
105
|
-
# 'email_verified': False,
|
|
106
|
-
# 'auth_time': 1756136558,
|
|
107
|
-
# 'groups': [
|
|
108
|
-
# 'azops88.c3.ai/C3.ClusterAdmin'
|
|
109
|
-
# ]
|
|
110
|
-
# }
|
|
111
86
|
|
|
112
87
|
return IdpLogin(redirect_url, id_token, state_token, username, idp_uri=idp_uri, id_token_obj=parsed, session=session)
|
|
113
88
|
|
|
@@ -119,8 +94,7 @@ class OktaAuthenticator(Authenticator):
|
|
|
119
94
|
|
|
120
95
|
return None
|
|
121
96
|
|
|
122
|
-
|
|
123
|
-
jwks_url = f"{okta_auth_server}/v1/keys"
|
|
97
|
+
jwks_url = Config().get('idps.okta.jwks-uri', 'https://c3energy.okta.com/oauth2/v1/keys')
|
|
124
98
|
try:
|
|
125
99
|
jwks_client = jwt.PyJWKClient(jwks_url, cache_jwk_set=True, lifespan=360)
|
|
126
100
|
signing_key = jwks_client.get_signing_key_from_jwt(id_token)
|
adam/sso/cred_cache.py
CHANGED
adam/sso/idp.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import base64
|
|
1
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
|
|
2
13
|
|
|
3
14
|
from .cred_cache import CredCache
|
|
4
15
|
from .idp_session import IdpSession
|
|
5
16
|
from .idp_login import IdpLogin
|
|
6
17
|
from adam.config import Config
|
|
7
|
-
from adam.utils import log
|
|
18
|
+
from adam.utils import log, log2
|
|
19
|
+
|
|
20
|
+
T = TypeVar('T')
|
|
8
21
|
|
|
9
22
|
class Idp:
|
|
10
23
|
ctrl_c_entered = False
|
|
@@ -15,13 +28,23 @@ class Idp:
|
|
|
15
28
|
|
|
16
29
|
return cls.instance
|
|
17
30
|
|
|
18
|
-
def login(app_host: str, username: str = None, idp_uri: str = None, forced = False, use_token_from_env = True, use_cached_creds = True) -> IdpLogin:
|
|
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:
|
|
19
32
|
session: IdpSession = IdpSession.create(username, app_host, app_host, idp_uri=idp_uri)
|
|
20
33
|
|
|
21
34
|
if use_token_from_env:
|
|
22
35
|
if l0 := session.login_from_env_var():
|
|
23
36
|
return l0
|
|
24
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
|
+
|
|
25
48
|
r: IdpLogin = None
|
|
26
49
|
try:
|
|
27
50
|
if username:
|
|
@@ -31,8 +54,13 @@ class Idp:
|
|
|
31
54
|
if Idp.ctrl_c_entered:
|
|
32
55
|
Idp.ctrl_c_entered = False
|
|
33
56
|
|
|
34
|
-
default_user =
|
|
35
|
-
|
|
57
|
+
default_user: str = None
|
|
58
|
+
if use_cached_creds:
|
|
59
|
+
default_user = CredCache().get_username()
|
|
60
|
+
Config().debug(f'User read from cache: {default_user}')
|
|
61
|
+
|
|
62
|
+
if from_env := os.getenv('USERNAME'):
|
|
63
|
+
default_user = from_env
|
|
36
64
|
if default_user and default_user != username:
|
|
37
65
|
session = IdpSession.create(default_user, app_host, app_host)
|
|
38
66
|
|
|
@@ -61,18 +89,55 @@ class Idp:
|
|
|
61
89
|
if forced:
|
|
62
90
|
password = default_pass
|
|
63
91
|
else:
|
|
64
|
-
password = getpass.getpass(f'Password(default ********): ') or default_pass
|
|
92
|
+
password = Idp.with_no_ican(lambda: getpass.getpass(f'Password(default ********): ') or default_pass)
|
|
65
93
|
else:
|
|
66
|
-
password = getpass.getpass(f'Password: ')
|
|
94
|
+
password = Idp.with_no_ican(lambda: getpass.getpass(f'Password: '))
|
|
67
95
|
|
|
68
96
|
if username and password:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
72
107
|
finally:
|
|
73
108
|
if r and Config().get('app.login.cache-creds', True):
|
|
74
109
|
CredCache().cache(username, password)
|
|
75
110
|
elif username and Config().get('app.login.cache-username', True):
|
|
76
111
|
CredCache().cache(username)
|
|
77
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
|
+
Config().debug(traceback.format_exc())
|
|
141
|
+
pass
|
|
142
|
+
|
|
78
143
|
return None
|
adam/sso/idp_login.py
CHANGED
|
@@ -23,7 +23,7 @@ class IdpLogin:
|
|
|
23
23
|
j['id_token'],
|
|
24
24
|
j['state'],
|
|
25
25
|
idp_uri=j['idp_uri'] if 'idp_uri' in j else None,
|
|
26
|
-
id_token_obj=IdToken.from_dict(j['id_token_obj']))
|
|
26
|
+
id_token_obj=IdToken.from_dict(j['id_token_obj']) if 'id_token_obj' in j else None)
|
|
27
27
|
|
|
28
28
|
def ser(self):
|
|
29
29
|
return base64.b64encode(json.dumps({
|
|
@@ -31,7 +31,7 @@ class IdpLogin:
|
|
|
31
31
|
'id_token': self.id_token,
|
|
32
32
|
'state': self.state,
|
|
33
33
|
'idp_uri': self.idp_uri,
|
|
34
|
-
'id_token_obj': self.id_token_obj.to_dict()
|
|
34
|
+
'id_token_obj': self.id_token_obj.to_dict() if self.id_token_obj else None
|
|
35
35
|
}).encode('utf-8')).decode('utf-8')
|
|
36
36
|
|
|
37
37
|
def create_from_idp_uri(idp_uri: str):
|
adam/sso/idp_session.py
CHANGED
|
@@ -36,17 +36,20 @@ class IdpSession:
|
|
|
36
36
|
|
|
37
37
|
def login_from_env_var(self) -> IdpLogin:
|
|
38
38
|
if idp_token := os.getenv('IDP_TOKEN'):
|
|
39
|
-
|
|
40
|
-
l1: IdpLogin = self.get_idp_login()
|
|
41
|
-
# if l0.app_login_url == l1.app_login_url:
|
|
42
|
-
if l0.state != 'EMPTY':
|
|
43
|
-
return l0
|
|
39
|
+
return self.login_from_token(idp_token)
|
|
44
40
|
|
|
45
|
-
|
|
41
|
+
return None
|
|
46
42
|
|
|
43
|
+
def login_from_token(self, idp_token: str) -> IdpLogin:
|
|
44
|
+
l0: IdpLogin = IdpLogin.deser(idp_token)
|
|
45
|
+
l1: IdpLogin = self.get_idp_login()
|
|
46
|
+
# if l0.app_login_url == l1.app_login_url:
|
|
47
|
+
if l0.state != 'EMPTY':
|
|
47
48
|
return l0
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
l0.state = l1.state
|
|
51
|
+
|
|
52
|
+
return l0
|
|
50
53
|
|
|
51
54
|
def get_idp_login(self) -> IdpLogin:
|
|
52
55
|
return IdpLogin.create_from_idp_uri(self.idp_uri)
|
adam/utils.py
CHANGED
|
@@ -9,7 +9,8 @@ import os
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
import random
|
|
11
11
|
import string
|
|
12
|
-
|
|
12
|
+
import threading
|
|
13
|
+
from typing import Callable
|
|
13
14
|
from dateutil import parser
|
|
14
15
|
import subprocess
|
|
15
16
|
import sys
|
|
@@ -19,6 +20,8 @@ import yaml
|
|
|
19
20
|
|
|
20
21
|
from . import __version__
|
|
21
22
|
|
|
23
|
+
is_debug_holder = [lambda: False]
|
|
24
|
+
|
|
22
25
|
def to_tabular(lines: str, header: str = None, dashed_line = False):
|
|
23
26
|
return lines_to_tabular(lines.split('\n'), header, dashed_line)
|
|
24
27
|
|
|
@@ -70,15 +73,21 @@ def epoch(timestamp_string: str):
|
|
|
70
73
|
return parser.parse(timestamp_string).timestamp()
|
|
71
74
|
|
|
72
75
|
def log(s = None):
|
|
76
|
+
if not loggable():
|
|
77
|
+
return
|
|
78
|
+
|
|
73
79
|
# want to print empty line for False or empty collection
|
|
74
80
|
if s == None:
|
|
75
81
|
print()
|
|
76
82
|
else:
|
|
77
83
|
click.echo(s)
|
|
78
84
|
|
|
79
|
-
def log2(s = None):
|
|
85
|
+
def log2(s = None, nl = True):
|
|
86
|
+
if not loggable():
|
|
87
|
+
return
|
|
88
|
+
|
|
80
89
|
if s:
|
|
81
|
-
click.echo(s, err=True)
|
|
90
|
+
click.echo(s, err=True, nl=nl)
|
|
82
91
|
else:
|
|
83
92
|
print(file=sys.stderr)
|
|
84
93
|
|
|
@@ -125,6 +134,19 @@ def deep_merge_dicts(dict1, dict2):
|
|
|
125
134
|
merged_dict[key] = value
|
|
126
135
|
return merged_dict
|
|
127
136
|
|
|
137
|
+
def deep_sort_dict(d):
|
|
138
|
+
"""
|
|
139
|
+
Recursively sorts a dictionary by its keys, and any nested lists by their elements.
|
|
140
|
+
"""
|
|
141
|
+
if not isinstance(d, (dict, list)):
|
|
142
|
+
return d
|
|
143
|
+
|
|
144
|
+
if isinstance(d, dict):
|
|
145
|
+
return {k: deep_sort_dict(d[k]) for k in sorted(d)}
|
|
146
|
+
|
|
147
|
+
if isinstance(d, list):
|
|
148
|
+
return sorted([deep_sort_dict(item) for item in d])
|
|
149
|
+
|
|
128
150
|
def get_deep_keys(d, current_path=""):
|
|
129
151
|
"""
|
|
130
152
|
Recursively collects all combined keys (paths) from a deep dictionary.
|
|
@@ -228,4 +250,63 @@ def copy_config_file(rel_path: str, module: str, suffix: str = '.yaml', show_out
|
|
|
228
250
|
return path
|
|
229
251
|
|
|
230
252
|
def idp_token_from_env():
|
|
231
|
-
return os.getenv('IDP_TOKEN')
|
|
253
|
+
return os.getenv('IDP_TOKEN')
|
|
254
|
+
|
|
255
|
+
def is_lambda(func):
|
|
256
|
+
return callable(func) and hasattr(func, '__name__') and func.__name__ == '<lambda>'
|
|
257
|
+
|
|
258
|
+
class Ing:
|
|
259
|
+
state = threading.local()
|
|
260
|
+
|
|
261
|
+
def __init__(self, msg: str, suppress_log=False):
|
|
262
|
+
self.msg = msg
|
|
263
|
+
self.suppress_log = suppress_log
|
|
264
|
+
self.nested = False
|
|
265
|
+
|
|
266
|
+
def __enter__(self):
|
|
267
|
+
if not hasattr(Ing.state, 'ing_cnt'):
|
|
268
|
+
Ing.state.ing_cnt = 0
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
if not Ing.state.ing_cnt:
|
|
272
|
+
if not self.suppress_log and not is_debug_holder[0]():
|
|
273
|
+
log2(f'{self.msg}...', nl=False)
|
|
274
|
+
|
|
275
|
+
return None
|
|
276
|
+
finally:
|
|
277
|
+
Ing.state.ing_cnt += 1
|
|
278
|
+
|
|
279
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
280
|
+
Ing.state.ing_cnt -= 1
|
|
281
|
+
if not Ing.state.ing_cnt:
|
|
282
|
+
if not self.suppress_log and not is_debug_holder[0]():
|
|
283
|
+
log2(' OK')
|
|
284
|
+
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
def ing(msg: str, body: Callable[[], None]=None, suppress_log=False):
|
|
288
|
+
if not body:
|
|
289
|
+
return Ing(msg, suppress_log=suppress_log)
|
|
290
|
+
|
|
291
|
+
r = None
|
|
292
|
+
|
|
293
|
+
if not hasattr(Ing.state, 'ing_cnt'):
|
|
294
|
+
Ing.state.ing_cnt = 0
|
|
295
|
+
|
|
296
|
+
if not Ing.state.ing_cnt:
|
|
297
|
+
if not suppress_log and not is_debug_holder[0]():
|
|
298
|
+
log2(f'{msg}...', nl=False)
|
|
299
|
+
|
|
300
|
+
Ing.state.ing_cnt += 1
|
|
301
|
+
try:
|
|
302
|
+
r = body()
|
|
303
|
+
finally:
|
|
304
|
+
Ing.state.ing_cnt -= 1
|
|
305
|
+
if not Ing.state.ing_cnt:
|
|
306
|
+
if not suppress_log and not is_debug_holder[0]():
|
|
307
|
+
log2(' OK')
|
|
308
|
+
|
|
309
|
+
return r
|
|
310
|
+
|
|
311
|
+
def loggable():
|
|
312
|
+
return is_debug_holder[0]() or not hasattr(Ing.state, 'ing_cnt') or not Ing.state.ing_cnt
|