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.

Files changed (180) hide show
  1. adam/app_session.py +1 -1
  2. adam/apps.py +2 -2
  3. adam/batch.py +30 -31
  4. adam/checks/check_utils.py +4 -4
  5. adam/checks/compactionstats.py +1 -1
  6. adam/checks/cpu.py +2 -2
  7. adam/checks/disk.py +1 -1
  8. adam/checks/gossip.py +1 -1
  9. adam/checks/memory.py +3 -3
  10. adam/checks/status.py +1 -1
  11. adam/commands/alter_tables.py +81 -0
  12. adam/commands/app.py +3 -3
  13. adam/commands/app_ping.py +2 -2
  14. adam/commands/audit/audit.py +86 -0
  15. adam/commands/audit/audit_repair_tables.py +77 -0
  16. adam/commands/audit/audit_run.py +58 -0
  17. adam/commands/audit/show_last10.py +51 -0
  18. adam/commands/audit/show_slow10.py +50 -0
  19. adam/commands/audit/show_top10.py +48 -0
  20. adam/commands/audit/utils_show_top10.py +59 -0
  21. adam/commands/bash/bash.py +133 -0
  22. adam/commands/bash/bash_completer.py +93 -0
  23. adam/commands/cat.py +56 -0
  24. adam/commands/cd.py +12 -82
  25. adam/commands/check.py +6 -0
  26. adam/commands/cli_commands.py +3 -3
  27. adam/commands/code.py +60 -0
  28. adam/commands/command.py +48 -12
  29. adam/commands/commands_utils.py +4 -5
  30. adam/commands/cql/cql_completions.py +28 -0
  31. adam/commands/cql/cql_utils.py +209 -0
  32. adam/commands/{cqlsh.py → cql/cqlsh.py} +15 -10
  33. adam/commands/deploy/__init__.py +0 -0
  34. adam/commands/{frontend → deploy}/code_start.py +1 -1
  35. adam/commands/{frontend → deploy}/code_stop.py +1 -1
  36. adam/commands/{frontend → deploy}/code_utils.py +2 -2
  37. adam/commands/deploy/deploy.py +48 -0
  38. adam/commands/deploy/deploy_frontend.py +52 -0
  39. adam/commands/deploy/deploy_pg_agent.py +38 -0
  40. adam/commands/deploy/deploy_pod.py +110 -0
  41. adam/commands/deploy/deploy_utils.py +29 -0
  42. adam/commands/deploy/undeploy.py +48 -0
  43. adam/commands/deploy/undeploy_frontend.py +41 -0
  44. adam/commands/deploy/undeploy_pg_agent.py +42 -0
  45. adam/commands/deploy/undeploy_pod.py +51 -0
  46. adam/commands/devices/__init__.py +0 -0
  47. adam/commands/devices/device.py +27 -0
  48. adam/commands/devices/device_app.py +146 -0
  49. adam/commands/devices/device_auit_log.py +43 -0
  50. adam/commands/devices/device_cass.py +145 -0
  51. adam/commands/devices/device_export.py +86 -0
  52. adam/commands/devices/device_postgres.py +109 -0
  53. adam/commands/devices/devices.py +25 -0
  54. adam/commands/export/__init__.py +0 -0
  55. adam/commands/export/clean_up_export_session.py +53 -0
  56. adam/commands/{frontend/teardown_frontend.py → export/clean_up_export_sessions.py} +9 -11
  57. adam/commands/export/drop_export_database.py +58 -0
  58. adam/commands/export/drop_export_databases.py +46 -0
  59. adam/commands/export/export.py +83 -0
  60. adam/commands/export/export_databases.py +170 -0
  61. adam/commands/export/export_select.py +85 -0
  62. adam/commands/export/export_select_x.py +54 -0
  63. adam/commands/export/export_use.py +55 -0
  64. adam/commands/export/exporter.py +364 -0
  65. adam/commands/export/import_session.py +68 -0
  66. adam/commands/export/importer.py +67 -0
  67. adam/commands/export/importer_athena.py +80 -0
  68. adam/commands/export/importer_sqlite.py +47 -0
  69. adam/commands/export/show_column_counts.py +63 -0
  70. adam/commands/export/show_export_databases.py +39 -0
  71. adam/commands/export/show_export_session.py +51 -0
  72. adam/commands/export/show_export_sessions.py +47 -0
  73. adam/commands/export/utils_export.py +291 -0
  74. adam/commands/help.py +12 -7
  75. adam/commands/issues.py +6 -0
  76. adam/commands/kubectl.py +41 -0
  77. adam/commands/login.py +9 -5
  78. adam/commands/logs.py +2 -1
  79. adam/commands/ls.py +4 -107
  80. adam/commands/medusa/medusa.py +2 -26
  81. adam/commands/medusa/medusa_backup.py +2 -2
  82. adam/commands/medusa/medusa_restore.py +3 -4
  83. adam/commands/medusa/medusa_show_backupjobs.py +4 -3
  84. adam/commands/medusa/medusa_show_restorejobs.py +3 -3
  85. adam/commands/nodetool.py +9 -4
  86. adam/commands/param_set.py +1 -1
  87. adam/commands/postgres/postgres.py +42 -43
  88. adam/commands/postgres/postgres_context.py +248 -0
  89. adam/commands/postgres/postgres_preview.py +0 -1
  90. adam/commands/postgres/postgres_utils.py +31 -0
  91. adam/commands/postgres/psql_completions.py +10 -0
  92. adam/commands/preview_table.py +18 -40
  93. adam/commands/pwd.py +2 -28
  94. adam/commands/reaper/reaper.py +4 -24
  95. adam/commands/reaper/reaper_restart.py +1 -1
  96. adam/commands/reaper/reaper_session.py +2 -2
  97. adam/commands/repair/repair.py +3 -27
  98. adam/commands/repair/repair_log.py +1 -1
  99. adam/commands/repair/repair_run.py +2 -2
  100. adam/commands/repair/repair_scan.py +2 -7
  101. adam/commands/repair/repair_stop.py +1 -1
  102. adam/commands/report.py +6 -0
  103. adam/commands/restart.py +2 -2
  104. adam/commands/rollout.py +1 -1
  105. adam/commands/shell.py +33 -0
  106. adam/commands/show/show.py +11 -26
  107. adam/commands/show/show_app_actions.py +3 -0
  108. adam/commands/show/show_app_id.py +1 -1
  109. adam/commands/show/show_app_queues.py +3 -2
  110. adam/commands/show/show_cassandra_status.py +3 -3
  111. adam/commands/show/show_cassandra_version.py +3 -3
  112. adam/commands/show/show_commands.py +4 -1
  113. adam/commands/show/show_host.py +33 -0
  114. adam/commands/show/show_login.py +3 -0
  115. adam/commands/show/show_processes.py +1 -1
  116. adam/commands/show/show_repairs.py +2 -2
  117. adam/commands/show/show_storage.py +1 -1
  118. adam/commands/watch.py +1 -1
  119. adam/config.py +16 -3
  120. adam/embedded_params.py +1 -1
  121. adam/pod_exec_result.py +10 -2
  122. adam/repl.py +132 -117
  123. adam/repl_commands.py +62 -18
  124. adam/repl_state.py +276 -55
  125. adam/sql/__init__.py +0 -0
  126. adam/sql/sql_completer.py +120 -0
  127. adam/sql/sql_state_machine.py +617 -0
  128. adam/sql/term_completer.py +76 -0
  129. adam/sso/authenticator.py +1 -1
  130. adam/sso/authn_ad.py +36 -56
  131. adam/sso/authn_okta.py +6 -32
  132. adam/sso/cred_cache.py +1 -1
  133. adam/sso/idp.py +74 -9
  134. adam/sso/idp_login.py +2 -2
  135. adam/sso/idp_session.py +10 -7
  136. adam/utils.py +85 -4
  137. adam/utils_athena.py +145 -0
  138. adam/utils_audits.py +102 -0
  139. adam/utils_k8s/__init__.py +0 -0
  140. adam/utils_k8s/app_clusters.py +33 -0
  141. adam/utils_k8s/app_pods.py +31 -0
  142. adam/{k8s_utils → utils_k8s}/cassandra_clusters.py +6 -21
  143. adam/{k8s_utils → utils_k8s}/cassandra_nodes.py +12 -5
  144. adam/utils_k8s/config_maps.py +34 -0
  145. adam/utils_k8s/deployment.py +56 -0
  146. adam/{k8s_utils → utils_k8s}/jobs.py +1 -1
  147. adam/{k8s_utils → utils_k8s}/kube_context.py +1 -1
  148. adam/utils_k8s/pods.py +342 -0
  149. adam/{k8s_utils → utils_k8s}/secrets.py +4 -0
  150. adam/utils_k8s/service_accounts.py +169 -0
  151. adam/{k8s_utils → utils_k8s}/statefulsets.py +5 -4
  152. adam/{k8s_utils → utils_k8s}/volumes.py +9 -0
  153. adam/utils_net.py +24 -0
  154. adam/utils_repl/__init__.py +0 -0
  155. adam/utils_repl/automata_completer.py +48 -0
  156. adam/utils_repl/repl_completer.py +46 -0
  157. adam/utils_repl/state_machine.py +173 -0
  158. adam/utils_sqlite.py +101 -0
  159. adam/version.py +1 -1
  160. {kaqing-1.98.15.dist-info → kaqing-2.0.145.dist-info}/METADATA +1 -1
  161. kaqing-2.0.145.dist-info/RECORD +227 -0
  162. adam/commands/bash.py +0 -87
  163. adam/commands/cql_utils.py +0 -53
  164. adam/commands/devices.py +0 -89
  165. adam/commands/frontend/setup.py +0 -60
  166. adam/commands/frontend/setup_frontend.py +0 -58
  167. adam/commands/frontend/teardown.py +0 -61
  168. adam/commands/postgres/postgres_session.py +0 -225
  169. adam/commands/user_entry.py +0 -77
  170. adam/k8s_utils/pods.py +0 -211
  171. kaqing-1.98.15.dist-info/RECORD +0 -160
  172. /adam/commands/{frontend → audit}/__init__.py +0 -0
  173. /adam/{k8s_utils → commands/bash}/__init__.py +0 -0
  174. /adam/{medusa_show_restorejobs.py → commands/cql/__init__.py} +0 -0
  175. /adam/{k8s_utils → utils_k8s}/custom_resources.py +0 -0
  176. /adam/{k8s_utils → utils_k8s}/ingresses.py +0 -0
  177. /adam/{k8s_utils → utils_k8s}/services.py +0 -0
  178. {kaqing-1.98.15.dist-info → kaqing-2.0.145.dist-info}/WHEEL +0 -0
  179. {kaqing-1.98.15.dist-info → kaqing-2.0.145.dist-info}/entry_points.txt +0 -0
  180. {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
- def decode_jwt_part(encoded_part):
138
- missing_padding = len(encoded_part) % 4
139
- if missing_padding:
140
- encoded_part += '=' * (4 - missing_padding)
141
- decoded_bytes = base64.urlsafe_b64decode(encoded_part)
142
- return json.loads(decoded_bytes.decode('utf-8'))
143
-
144
- parts = id_token.split('.')
145
- # header = decode_jwt_part(parts[0])
146
- data = decode_jwt_part(parts[1])
147
- # print('SEAN', payload)
148
- # {
149
- # 'aud': '00ff94a8-6b0a-4715-98e0-95490012d818',
150
- # 'iss': 'https://login.microsoftonline.com/53ad779a-93e7-485c-ba20-ac8290d7252b/v2.0',
151
- # 'iat': 1756138348,
152
- # 'nbf': 1756138348,
153
- # 'exp': 1756142248,
154
- # 'cc': 'CmCuOndNgXFbP/Vor0kv9fqd32LOv7kSHKStVrGPTXXnlPlSET3g4z23XjVZJW37F2Yy8d45MzZ6xA/XNbYGE3BHYAZFhfDKOp0ZbWZysqa2zqD3lpyXxnlEpzWkFY1SlDgSBWMzLmFpGhIKEFbS9ukejKFBi0QKCcPHh6MiEgoQb7gHorgqqEOuuPDIcSgHATICTkE4AUIJCQDvjjtx391I',
155
- # 'email': 'sean.ahn@c3.ai',
156
- # 'groups': [
157
- # 'e58ef97f-8622-47cc-93cd-0ec8e3df2b4e',
158
- # '290214ec-2eaa-47bb-802c-70c2535bb7e7',
159
- # '55cd2e92-c40d-4646-b837-c6fb6406013b',
160
- # '9b715aa3-ec6c-44ad-be0c-a1d95045526d',
161
- # '19a38c19-e4c3-4d7b-8bb0-4afe7f502a51',
162
- # '5d891ce3-02d8-4d34-9748-9e711a3d54c5',
163
- # '0626cb36-106c-4ab3-adcf-7ee8e3f05584',
164
- # '6dfdfa3e-b225-4a74-a534-1cc963f78e08',
165
- # 'bfdcca4e-7212-4e73-9b8a-eecfd3a7797d',
166
- # '6d050845-61c4-4072-acb1-9662d0c9faa0',
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
- okta_auth_server = f"https://{idp_host}/oauth2"
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
@@ -4,7 +4,7 @@ import traceback
4
4
  from dotenv import load_dotenv
5
5
 
6
6
  from adam.config import Config
7
- from adam.k8s_utils.kube_context import KubeContext
7
+ from adam.utils_k8s.kube_context import KubeContext
8
8
 
9
9
  class CredCache:
10
10
  # the singleton pattern
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 = CredCache().get_username() if use_cached_creds else None
35
- Config().debug(f'User read from cache: {default_user} with use_cached: {use_cached_creds}')
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
- r = session.authenticator.authenticate(session.idp_uri, app_host, username, password)
70
-
71
- return r
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
- l0: IdpLogin = IdpLogin.deser(idp_token)
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
- l0.state = l1.state
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
- return None
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
- from typing import cast
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