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.
Files changed (308) hide show
  1. adam/__init__.py +1 -0
  2. adam/app_session.py +182 -0
  3. {walker → adam}/apps.py +8 -24
  4. {walker → adam}/batch.py +54 -97
  5. {walker → adam}/checks/check.py +3 -3
  6. {walker → adam}/checks/check_result.py +1 -1
  7. adam/checks/check_utils.py +65 -0
  8. {walker → adam}/checks/compactionstats.py +6 -6
  9. {walker → adam}/checks/cpu.py +14 -8
  10. adam/checks/cpu_metrics.py +52 -0
  11. {walker → adam}/checks/disk.py +6 -6
  12. {walker → adam}/checks/gossip.py +5 -5
  13. {walker → adam}/checks/memory.py +7 -7
  14. {walker → adam}/checks/status.py +5 -5
  15. {walker → adam}/cli.py +3 -3
  16. {walker → adam}/columns/column.py +1 -1
  17. adam/columns/columns.py +45 -0
  18. {walker → adam}/columns/compactions.py +5 -5
  19. {walker → adam}/columns/cpu.py +6 -4
  20. adam/columns/cpu_metrics.py +22 -0
  21. {walker → adam}/columns/dir_data.py +3 -3
  22. {walker → adam}/columns/dir_snapshots.py +3 -3
  23. {walker → adam}/columns/gossip.py +5 -5
  24. {walker → adam}/columns/host_id.py +3 -3
  25. {walker → adam}/columns/memory.py +3 -3
  26. {walker → adam}/columns/node_address.py +3 -3
  27. {walker → adam}/columns/node_load.py +3 -3
  28. {walker → adam}/columns/node_owns.py +3 -3
  29. {walker → adam}/columns/node_status.py +3 -3
  30. {walker → adam}/columns/node_tokens.py +3 -3
  31. {walker → adam}/columns/node_utils.py +2 -2
  32. {walker → adam}/columns/pod_name.py +2 -2
  33. {walker → adam}/columns/volume_cassandra.py +4 -4
  34. {walker → adam}/columns/volume_root.py +3 -3
  35. adam/commands/__init__.py +15 -0
  36. adam/commands/alter_tables.py +81 -0
  37. adam/commands/app_cmd.py +38 -0
  38. {walker → adam}/commands/app_ping.py +10 -16
  39. adam/commands/audit/audit.py +84 -0
  40. adam/commands/audit/audit_repair_tables.py +74 -0
  41. adam/commands/audit/audit_run.py +50 -0
  42. adam/commands/audit/show_last10.py +48 -0
  43. adam/commands/audit/show_slow10.py +47 -0
  44. adam/commands/audit/show_top10.py +45 -0
  45. adam/commands/audit/utils_show_top10.py +59 -0
  46. adam/commands/bash/__init__.py +5 -0
  47. adam/commands/bash/bash.py +36 -0
  48. adam/commands/bash/bash_completer.py +93 -0
  49. adam/commands/bash/utils_bash.py +16 -0
  50. adam/commands/cat.py +50 -0
  51. adam/commands/cd.py +43 -0
  52. adam/commands/check.py +73 -0
  53. {walker → adam}/commands/cli_commands.py +7 -8
  54. adam/commands/code.py +57 -0
  55. adam/commands/command.py +190 -0
  56. {walker → adam}/commands/command_helpers.py +1 -1
  57. {walker → adam}/commands/commands_utils.py +15 -25
  58. adam/commands/cp.py +89 -0
  59. adam/commands/cql/cql_completions.py +33 -0
  60. {walker/commands → adam/commands/cql}/cqlsh.py +20 -35
  61. adam/commands/cql/utils_cql.py +343 -0
  62. {walker/commands/frontend → adam/commands/deploy}/code_start.py +11 -14
  63. adam/commands/deploy/code_stop.py +40 -0
  64. {walker/commands/frontend → adam/commands/deploy}/code_utils.py +7 -9
  65. adam/commands/deploy/deploy.py +25 -0
  66. adam/commands/deploy/deploy_frontend.py +49 -0
  67. adam/commands/deploy/deploy_pg_agent.py +35 -0
  68. adam/commands/deploy/deploy_pod.py +108 -0
  69. adam/commands/deploy/deploy_utils.py +29 -0
  70. adam/commands/deploy/undeploy.py +25 -0
  71. adam/commands/deploy/undeploy_frontend.py +38 -0
  72. adam/commands/deploy/undeploy_pg_agent.py +39 -0
  73. adam/commands/deploy/undeploy_pod.py +48 -0
  74. adam/commands/devices/device.py +118 -0
  75. adam/commands/devices/device_app.py +173 -0
  76. adam/commands/devices/device_auit_log.py +49 -0
  77. adam/commands/devices/device_cass.py +185 -0
  78. adam/commands/devices/device_export.py +86 -0
  79. adam/commands/devices/device_postgres.py +144 -0
  80. adam/commands/devices/devices.py +25 -0
  81. {walker → adam}/commands/exit.py +3 -6
  82. adam/commands/export/clean_up_all_export_sessions.py +37 -0
  83. adam/commands/export/clean_up_export_sessions.py +51 -0
  84. adam/commands/export/drop_export_database.py +55 -0
  85. adam/commands/export/drop_export_databases.py +43 -0
  86. adam/commands/export/export.py +53 -0
  87. adam/commands/export/export_databases.py +170 -0
  88. adam/commands/export/export_handlers.py +71 -0
  89. adam/commands/export/export_select.py +81 -0
  90. adam/commands/export/export_select_x.py +54 -0
  91. adam/commands/export/export_use.py +52 -0
  92. adam/commands/export/exporter.py +352 -0
  93. adam/commands/export/import_session.py +40 -0
  94. adam/commands/export/importer.py +67 -0
  95. adam/commands/export/importer_athena.py +80 -0
  96. adam/commands/export/importer_sqlite.py +47 -0
  97. adam/commands/export/show_column_counts.py +54 -0
  98. adam/commands/export/show_export_databases.py +36 -0
  99. adam/commands/export/show_export_session.py +48 -0
  100. adam/commands/export/show_export_sessions.py +44 -0
  101. adam/commands/export/utils_export.py +314 -0
  102. {walker → adam}/commands/help.py +17 -12
  103. adam/commands/intermediate_command.py +49 -0
  104. adam/commands/issues.py +43 -0
  105. adam/commands/kubectl.py +38 -0
  106. adam/commands/login.py +70 -0
  107. {walker → adam}/commands/logs.py +8 -10
  108. adam/commands/ls.py +41 -0
  109. adam/commands/medusa/medusa.py +27 -0
  110. adam/commands/medusa/medusa_backup.py +57 -0
  111. adam/commands/medusa/medusa_restore.py +83 -0
  112. adam/commands/medusa/medusa_show_backupjobs.py +51 -0
  113. adam/commands/medusa/medusa_show_restorejobs.py +47 -0
  114. {walker → adam}/commands/nodetool.py +17 -21
  115. {walker → adam}/commands/param_get.py +15 -16
  116. adam/commands/param_set.py +43 -0
  117. adam/commands/postgres/postgres.py +104 -0
  118. adam/commands/postgres/postgres_context.py +274 -0
  119. {walker → adam}/commands/postgres/postgres_ls.py +7 -11
  120. {walker → adam}/commands/postgres/postgres_preview.py +8 -13
  121. adam/commands/postgres/psql_completions.py +10 -0
  122. adam/commands/postgres/utils_postgres.py +66 -0
  123. adam/commands/preview_table.py +37 -0
  124. adam/commands/pwd.py +47 -0
  125. adam/commands/reaper/reaper.py +35 -0
  126. adam/commands/reaper/reaper_forward.py +93 -0
  127. adam/commands/reaper/reaper_forward_session.py +6 -0
  128. {walker → adam}/commands/reaper/reaper_forward_stop.py +13 -19
  129. {walker → adam}/commands/reaper/reaper_restart.py +10 -17
  130. adam/commands/reaper/reaper_run_abort.py +46 -0
  131. adam/commands/reaper/reaper_runs.py +82 -0
  132. adam/commands/reaper/reaper_runs_abort.py +63 -0
  133. adam/commands/reaper/reaper_schedule_activate.py +45 -0
  134. adam/commands/reaper/reaper_schedule_start.py +45 -0
  135. adam/commands/reaper/reaper_schedule_stop.py +45 -0
  136. {walker → adam}/commands/reaper/reaper_schedules.py +6 -16
  137. {walker → adam}/commands/reaper/reaper_status.py +11 -19
  138. adam/commands/reaper/utils_reaper.py +196 -0
  139. adam/commands/repair/repair.py +26 -0
  140. {walker → adam}/commands/repair/repair_log.py +7 -10
  141. adam/commands/repair/repair_run.py +70 -0
  142. adam/commands/repair/repair_scan.py +71 -0
  143. {walker → adam}/commands/repair/repair_stop.py +8 -11
  144. adam/commands/report.py +61 -0
  145. adam/commands/restart.py +60 -0
  146. {walker → adam}/commands/rollout.py +25 -30
  147. adam/commands/shell.py +34 -0
  148. adam/commands/show/show.py +39 -0
  149. walker/commands/show/show_version.py → adam/commands/show/show_adam.py +14 -10
  150. adam/commands/show/show_app_actions.py +57 -0
  151. {walker → adam}/commands/show/show_app_id.py +12 -15
  152. {walker → adam}/commands/show/show_app_queues.py +9 -12
  153. adam/commands/show/show_cassandra_repairs.py +38 -0
  154. adam/commands/show/show_cassandra_status.py +124 -0
  155. {walker → adam}/commands/show/show_cassandra_version.py +6 -16
  156. adam/commands/show/show_commands.py +59 -0
  157. walker/commands/show/show_storage.py → adam/commands/show/show_host.py +11 -13
  158. adam/commands/show/show_login.py +62 -0
  159. {walker → adam}/commands/show/show_params.py +4 -4
  160. adam/commands/show/show_processes.py +51 -0
  161. adam/commands/show/show_storage.py +42 -0
  162. adam/commands/watch.py +82 -0
  163. {walker → adam}/config.py +10 -22
  164. {walker → adam}/embedded_apps.py +1 -1
  165. adam/embedded_params.py +2 -0
  166. adam/log.py +47 -0
  167. {walker → adam}/pod_exec_result.py +10 -2
  168. adam/repl.py +182 -0
  169. adam/repl_commands.py +124 -0
  170. adam/repl_state.py +458 -0
  171. adam/sql/__init__.py +0 -0
  172. adam/sql/sql_completer.py +120 -0
  173. adam/sql/sql_state_machine.py +618 -0
  174. adam/sql/term_completer.py +76 -0
  175. adam/sso/__init__.py +0 -0
  176. {walker → adam}/sso/authenticator.py +5 -1
  177. adam/sso/authn_ad.py +170 -0
  178. {walker → adam}/sso/authn_okta.py +39 -22
  179. adam/sso/cred_cache.py +60 -0
  180. adam/sso/id_token.py +23 -0
  181. adam/sso/idp.py +143 -0
  182. adam/sso/idp_login.py +50 -0
  183. adam/sso/idp_session.py +55 -0
  184. adam/sso/sso_config.py +63 -0
  185. adam/utils.py +679 -0
  186. adam/utils_app.py +98 -0
  187. adam/utils_athena.py +145 -0
  188. adam/utils_audits.py +106 -0
  189. adam/utils_issues.py +32 -0
  190. adam/utils_k8s/__init__.py +0 -0
  191. adam/utils_k8s/app_clusters.py +28 -0
  192. adam/utils_k8s/app_pods.py +33 -0
  193. adam/utils_k8s/cassandra_clusters.py +36 -0
  194. adam/utils_k8s/cassandra_nodes.py +33 -0
  195. adam/utils_k8s/config_maps.py +34 -0
  196. {walker/k8s_utils → adam/utils_k8s}/custom_resources.py +7 -2
  197. adam/utils_k8s/deployment.py +56 -0
  198. {walker/k8s_utils → adam/utils_k8s}/ingresses.py +3 -4
  199. {walker/k8s_utils → adam/utils_k8s}/jobs.py +3 -3
  200. adam/utils_k8s/k8s.py +87 -0
  201. {walker/k8s_utils → adam/utils_k8s}/kube_context.py +4 -4
  202. adam/utils_k8s/pods.py +290 -0
  203. {walker/k8s_utils → adam/utils_k8s}/secrets.py +8 -4
  204. adam/utils_k8s/service_accounts.py +170 -0
  205. {walker/k8s_utils → adam/utils_k8s}/services.py +3 -4
  206. {walker/k8s_utils → adam/utils_k8s}/statefulsets.py +6 -16
  207. {walker/k8s_utils → adam/utils_k8s}/volumes.py +10 -1
  208. adam/utils_net.py +24 -0
  209. adam/utils_repl/__init__.py +0 -0
  210. adam/utils_repl/automata_completer.py +48 -0
  211. adam/utils_repl/repl_completer.py +46 -0
  212. adam/utils_repl/state_machine.py +173 -0
  213. adam/utils_sqlite.py +109 -0
  214. adam/version.py +5 -0
  215. {kaqing-1.77.0.dist-info → kaqing-2.0.171.dist-info}/METADATA +1 -1
  216. kaqing-2.0.171.dist-info/RECORD +236 -0
  217. kaqing-2.0.171.dist-info/entry_points.txt +3 -0
  218. kaqing-2.0.171.dist-info/top_level.txt +1 -0
  219. kaqing-1.77.0.dist-info/RECORD +0 -159
  220. kaqing-1.77.0.dist-info/entry_points.txt +0 -3
  221. kaqing-1.77.0.dist-info/top_level.txt +0 -1
  222. walker/__init__.py +0 -3
  223. walker/app_session.py +0 -168
  224. walker/checks/check_utils.py +0 -97
  225. walker/columns/columns.py +0 -43
  226. walker/commands/add_user.py +0 -68
  227. walker/commands/app.py +0 -67
  228. walker/commands/bash.py +0 -87
  229. walker/commands/cd.py +0 -115
  230. walker/commands/check.py +0 -68
  231. walker/commands/command.py +0 -104
  232. walker/commands/cp.py +0 -95
  233. walker/commands/cql_utils.py +0 -53
  234. walker/commands/devices.py +0 -89
  235. walker/commands/frontend/code_stop.py +0 -57
  236. walker/commands/frontend/setup.py +0 -60
  237. walker/commands/frontend/setup_frontend.py +0 -58
  238. walker/commands/frontend/teardown.py +0 -61
  239. walker/commands/frontend/teardown_frontend.py +0 -42
  240. walker/commands/issues.py +0 -69
  241. walker/commands/login.py +0 -72
  242. walker/commands/ls.py +0 -145
  243. walker/commands/medusa/medusa.py +0 -69
  244. walker/commands/medusa/medusa_backup.py +0 -61
  245. walker/commands/medusa/medusa_restore.py +0 -86
  246. walker/commands/medusa/medusa_show_backupjobs.py +0 -52
  247. walker/commands/medusa/medusa_show_restorejobs.py +0 -52
  248. walker/commands/param_set.py +0 -44
  249. walker/commands/postgres/postgres.py +0 -113
  250. walker/commands/postgres/postgres_session.py +0 -225
  251. walker/commands/preview_table.py +0 -98
  252. walker/commands/processes.py +0 -53
  253. walker/commands/pwd.py +0 -64
  254. walker/commands/reaper/reaper.py +0 -78
  255. walker/commands/reaper/reaper_forward.py +0 -100
  256. walker/commands/reaper/reaper_run_abort.py +0 -65
  257. walker/commands/reaper/reaper_runs.py +0 -97
  258. walker/commands/reaper/reaper_runs_abort.py +0 -83
  259. walker/commands/reaper/reaper_schedule_activate.py +0 -64
  260. walker/commands/reaper/reaper_schedule_start.py +0 -64
  261. walker/commands/reaper/reaper_schedule_stop.py +0 -64
  262. walker/commands/reaper/reaper_session.py +0 -159
  263. walker/commands/repair/repair.py +0 -68
  264. walker/commands/repair/repair_run.py +0 -72
  265. walker/commands/repair/repair_scan.py +0 -79
  266. walker/commands/report.py +0 -57
  267. walker/commands/restart.py +0 -61
  268. walker/commands/show/show.py +0 -72
  269. walker/commands/show/show_app_actions.py +0 -53
  270. walker/commands/show/show_cassandra_status.py +0 -35
  271. walker/commands/show/show_commands.py +0 -58
  272. walker/commands/show/show_processes.py +0 -35
  273. walker/commands/show/show_repairs.py +0 -47
  274. walker/commands/status.py +0 -128
  275. walker/commands/storage.py +0 -52
  276. walker/commands/user_entry.py +0 -69
  277. walker/commands/watch.py +0 -85
  278. walker/embedded_params.py +0 -2
  279. walker/k8s_utils/cassandra_clusters.py +0 -48
  280. walker/k8s_utils/cassandra_nodes.py +0 -26
  281. walker/k8s_utils/pods.py +0 -211
  282. walker/repl.py +0 -165
  283. walker/repl_commands.py +0 -58
  284. walker/repl_state.py +0 -211
  285. walker/sso/authn_ad.py +0 -94
  286. walker/sso/idp.py +0 -150
  287. walker/sso/idp_login.py +0 -29
  288. walker/sso/sso_config.py +0 -45
  289. walker/utils.py +0 -194
  290. walker/version.py +0 -5
  291. {walker → adam}/checks/__init__.py +0 -0
  292. {walker → adam}/checks/check_context.py +0 -0
  293. {walker → adam}/checks/issue.py +0 -0
  294. {walker → adam}/cli_group.py +0 -0
  295. {walker → adam}/columns/__init__.py +0 -0
  296. {walker/commands → adam/commands/audit}/__init__.py +0 -0
  297. {walker/commands/frontend → adam/commands/cql}/__init__.py +0 -0
  298. {walker/commands/medusa → adam/commands/deploy}/__init__.py +0 -0
  299. {walker/commands/postgres → adam/commands/devices}/__init__.py +0 -0
  300. {walker/commands/reaper → adam/commands/export}/__init__.py +0 -0
  301. {walker/commands/repair → adam/commands/medusa}/__init__.py +0 -0
  302. {walker → adam}/commands/nodetool_commands.py +0 -0
  303. {walker/commands/show → adam/commands/postgres}/__init__.py +0 -0
  304. {walker/k8s_utils → adam/commands/reaper}/__init__.py +0 -0
  305. {walker/sso → adam/commands/repair}/__init__.py +0 -0
  306. /walker/medusa_show_restorejobs.py → /adam/commands/show/__init__.py +0 -0
  307. {walker → adam}/repl_session.py +0 -0
  308. {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 authenticate(self, idp_uri: str, app_host: str, username: str, password: str) -> IdpLogin:
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 walker.sso.authenticator import Authenticator
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 walker.config import Config
10
- from walker.utils import log2
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
- url = f"https://{okta_host}/api/v1/authn"
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(url, headers=headers, data=json.dumps(payload))
45
- if Config().is_debug():
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
- return None
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
- if Config().is_debug():
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
- return None
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
- if group not in OktaAuthenticator.get_groups(okta_host, id_token):
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
- return None
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 get_groups(idp_host, id_token) -> list[str]:
80
- groups: list[str] = []
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 groups
95
+ return None
86
96
 
87
- okta_auth_server = f"https://{idp_host}/oauth2"
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 data['groups']
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 groups
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('.', '')