kstlib 0.0.1a0__py3-none-any.whl → 1.0.1__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 (166) hide show
  1. kstlib/__init__.py +266 -1
  2. kstlib/__main__.py +16 -0
  3. kstlib/alerts/__init__.py +110 -0
  4. kstlib/alerts/channels/__init__.py +36 -0
  5. kstlib/alerts/channels/base.py +197 -0
  6. kstlib/alerts/channels/email.py +227 -0
  7. kstlib/alerts/channels/slack.py +389 -0
  8. kstlib/alerts/exceptions.py +72 -0
  9. kstlib/alerts/manager.py +651 -0
  10. kstlib/alerts/models.py +142 -0
  11. kstlib/alerts/throttle.py +263 -0
  12. kstlib/auth/__init__.py +139 -0
  13. kstlib/auth/callback.py +399 -0
  14. kstlib/auth/config.py +502 -0
  15. kstlib/auth/errors.py +127 -0
  16. kstlib/auth/models.py +316 -0
  17. kstlib/auth/providers/__init__.py +14 -0
  18. kstlib/auth/providers/base.py +393 -0
  19. kstlib/auth/providers/oauth2.py +645 -0
  20. kstlib/auth/providers/oidc.py +821 -0
  21. kstlib/auth/session.py +338 -0
  22. kstlib/auth/token.py +482 -0
  23. kstlib/cache/__init__.py +50 -0
  24. kstlib/cache/decorator.py +261 -0
  25. kstlib/cache/strategies.py +516 -0
  26. kstlib/cli/__init__.py +8 -0
  27. kstlib/cli/app.py +195 -0
  28. kstlib/cli/commands/__init__.py +5 -0
  29. kstlib/cli/commands/auth/__init__.py +39 -0
  30. kstlib/cli/commands/auth/common.py +122 -0
  31. kstlib/cli/commands/auth/login.py +325 -0
  32. kstlib/cli/commands/auth/logout.py +74 -0
  33. kstlib/cli/commands/auth/providers.py +57 -0
  34. kstlib/cli/commands/auth/status.py +291 -0
  35. kstlib/cli/commands/auth/token.py +199 -0
  36. kstlib/cli/commands/auth/whoami.py +106 -0
  37. kstlib/cli/commands/config.py +89 -0
  38. kstlib/cli/commands/ops/__init__.py +39 -0
  39. kstlib/cli/commands/ops/attach.py +49 -0
  40. kstlib/cli/commands/ops/common.py +269 -0
  41. kstlib/cli/commands/ops/list_sessions.py +252 -0
  42. kstlib/cli/commands/ops/logs.py +49 -0
  43. kstlib/cli/commands/ops/start.py +98 -0
  44. kstlib/cli/commands/ops/status.py +138 -0
  45. kstlib/cli/commands/ops/stop.py +60 -0
  46. kstlib/cli/commands/rapi/__init__.py +60 -0
  47. kstlib/cli/commands/rapi/call.py +341 -0
  48. kstlib/cli/commands/rapi/list.py +99 -0
  49. kstlib/cli/commands/rapi/show.py +206 -0
  50. kstlib/cli/commands/secrets/__init__.py +35 -0
  51. kstlib/cli/commands/secrets/common.py +425 -0
  52. kstlib/cli/commands/secrets/decrypt.py +88 -0
  53. kstlib/cli/commands/secrets/doctor.py +743 -0
  54. kstlib/cli/commands/secrets/encrypt.py +242 -0
  55. kstlib/cli/commands/secrets/shred.py +96 -0
  56. kstlib/cli/common.py +86 -0
  57. kstlib/config/__init__.py +76 -0
  58. kstlib/config/exceptions.py +110 -0
  59. kstlib/config/export.py +225 -0
  60. kstlib/config/loader.py +963 -0
  61. kstlib/config/sops.py +287 -0
  62. kstlib/db/__init__.py +54 -0
  63. kstlib/db/aiosqlcipher.py +137 -0
  64. kstlib/db/cipher.py +112 -0
  65. kstlib/db/database.py +367 -0
  66. kstlib/db/exceptions.py +25 -0
  67. kstlib/db/pool.py +302 -0
  68. kstlib/helpers/__init__.py +35 -0
  69. kstlib/helpers/exceptions.py +11 -0
  70. kstlib/helpers/time_trigger.py +396 -0
  71. kstlib/kstlib.conf.yml +890 -0
  72. kstlib/limits.py +963 -0
  73. kstlib/logging/__init__.py +108 -0
  74. kstlib/logging/manager.py +633 -0
  75. kstlib/mail/__init__.py +42 -0
  76. kstlib/mail/builder.py +626 -0
  77. kstlib/mail/exceptions.py +27 -0
  78. kstlib/mail/filesystem.py +248 -0
  79. kstlib/mail/transport.py +224 -0
  80. kstlib/mail/transports/__init__.py +19 -0
  81. kstlib/mail/transports/gmail.py +268 -0
  82. kstlib/mail/transports/resend.py +324 -0
  83. kstlib/mail/transports/smtp.py +326 -0
  84. kstlib/meta.py +72 -0
  85. kstlib/metrics/__init__.py +88 -0
  86. kstlib/metrics/decorators.py +1090 -0
  87. kstlib/metrics/exceptions.py +14 -0
  88. kstlib/monitoring/__init__.py +116 -0
  89. kstlib/monitoring/_styles.py +163 -0
  90. kstlib/monitoring/cell.py +57 -0
  91. kstlib/monitoring/config.py +424 -0
  92. kstlib/monitoring/delivery.py +579 -0
  93. kstlib/monitoring/exceptions.py +63 -0
  94. kstlib/monitoring/image.py +220 -0
  95. kstlib/monitoring/kv.py +79 -0
  96. kstlib/monitoring/list.py +69 -0
  97. kstlib/monitoring/metric.py +88 -0
  98. kstlib/monitoring/monitoring.py +341 -0
  99. kstlib/monitoring/renderer.py +139 -0
  100. kstlib/monitoring/service.py +392 -0
  101. kstlib/monitoring/table.py +129 -0
  102. kstlib/monitoring/types.py +56 -0
  103. kstlib/ops/__init__.py +86 -0
  104. kstlib/ops/base.py +148 -0
  105. kstlib/ops/container.py +577 -0
  106. kstlib/ops/exceptions.py +209 -0
  107. kstlib/ops/manager.py +407 -0
  108. kstlib/ops/models.py +176 -0
  109. kstlib/ops/tmux.py +372 -0
  110. kstlib/ops/validators.py +287 -0
  111. kstlib/py.typed +0 -0
  112. kstlib/rapi/__init__.py +118 -0
  113. kstlib/rapi/client.py +875 -0
  114. kstlib/rapi/config.py +861 -0
  115. kstlib/rapi/credentials.py +887 -0
  116. kstlib/rapi/exceptions.py +213 -0
  117. kstlib/resilience/__init__.py +101 -0
  118. kstlib/resilience/circuit_breaker.py +440 -0
  119. kstlib/resilience/exceptions.py +95 -0
  120. kstlib/resilience/heartbeat.py +491 -0
  121. kstlib/resilience/rate_limiter.py +506 -0
  122. kstlib/resilience/shutdown.py +417 -0
  123. kstlib/resilience/watchdog.py +637 -0
  124. kstlib/secrets/__init__.py +29 -0
  125. kstlib/secrets/exceptions.py +19 -0
  126. kstlib/secrets/models.py +62 -0
  127. kstlib/secrets/providers/__init__.py +79 -0
  128. kstlib/secrets/providers/base.py +58 -0
  129. kstlib/secrets/providers/environment.py +66 -0
  130. kstlib/secrets/providers/keyring.py +107 -0
  131. kstlib/secrets/providers/kms.py +223 -0
  132. kstlib/secrets/providers/kwargs.py +101 -0
  133. kstlib/secrets/providers/sops.py +209 -0
  134. kstlib/secrets/resolver.py +221 -0
  135. kstlib/secrets/sensitive.py +130 -0
  136. kstlib/secure/__init__.py +23 -0
  137. kstlib/secure/fs.py +194 -0
  138. kstlib/secure/permissions.py +70 -0
  139. kstlib/ssl.py +347 -0
  140. kstlib/ui/__init__.py +23 -0
  141. kstlib/ui/exceptions.py +26 -0
  142. kstlib/ui/panels.py +484 -0
  143. kstlib/ui/spinner.py +864 -0
  144. kstlib/ui/tables.py +382 -0
  145. kstlib/utils/__init__.py +48 -0
  146. kstlib/utils/dict.py +36 -0
  147. kstlib/utils/formatting.py +338 -0
  148. kstlib/utils/http_trace.py +237 -0
  149. kstlib/utils/lazy.py +49 -0
  150. kstlib/utils/secure_delete.py +205 -0
  151. kstlib/utils/serialization.py +247 -0
  152. kstlib/utils/text.py +56 -0
  153. kstlib/utils/validators.py +124 -0
  154. kstlib/websocket/__init__.py +97 -0
  155. kstlib/websocket/exceptions.py +214 -0
  156. kstlib/websocket/manager.py +1102 -0
  157. kstlib/websocket/models.py +361 -0
  158. kstlib-1.0.1.dist-info/METADATA +201 -0
  159. kstlib-1.0.1.dist-info/RECORD +163 -0
  160. {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/WHEEL +1 -1
  161. kstlib-1.0.1.dist-info/entry_points.txt +2 -0
  162. kstlib-1.0.1.dist-info/licenses/LICENSE.md +9 -0
  163. kstlib-0.0.1a0.dist-info/METADATA +0 -29
  164. kstlib-0.0.1a0.dist-info/RECORD +0 -6
  165. kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
  166. {kstlib-0.0.1a0.dist-info → kstlib-1.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,393 @@
1
+ """Abstract base class for authentication providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from typing_extensions import Self
10
+
11
+ from kstlib.logging import TRACE_LEVEL, get_logger
12
+ from kstlib.ssl import validate_ca_bundle_path, validate_ssl_verify
13
+
14
+ if TYPE_CHECKING:
15
+ import types
16
+
17
+ from kstlib.auth.models import AuthFlow, PreflightReport, Token
18
+ from kstlib.auth.token import AbstractTokenStorage
19
+
20
+ logger = get_logger(__name__)
21
+
22
+
23
+ @dataclass
24
+ class AuthProviderConfig: # pylint: disable=too-many-instance-attributes
25
+ """Configuration for an authentication provider.
26
+
27
+ Supports three modes:
28
+
29
+ 1. **Auto discovery** (OIDC): Only ``issuer`` provided, endpoints discovered via
30
+ ``.well-known/openid-configuration``.
31
+
32
+ 2. **Hybrid mode** (OIDC): ``issuer`` + some explicit endpoints. Discovery fills
33
+ missing endpoints, explicit ones take precedence.
34
+
35
+ 3. **Full manual** (OAuth2/OIDC): No ``issuer``, all endpoints explicit.
36
+ No discovery attempted.
37
+
38
+ Attributes:
39
+ client_id: OAuth2 client identifier.
40
+ client_secret: Optional client secret (not needed for public clients with PKCE).
41
+ authorize_url: Authorization endpoint URL.
42
+ token_url: Token endpoint URL.
43
+ revoke_url: Optional token revocation endpoint.
44
+ userinfo_url: Optional UserInfo endpoint URL.
45
+ jwks_uri: JWKS endpoint for ID token signature verification.
46
+ end_session_endpoint: Logout/end session endpoint.
47
+ issuer: OIDC issuer URL (enables discovery).
48
+ scopes: List of OAuth2 scopes to request.
49
+ redirect_uri: Callback URI for authorization code flow.
50
+ pkce: Enable PKCE extension (default True for OIDC).
51
+ discovery_ttl: Cache TTL for OIDC discovery document (seconds).
52
+ headers: Custom HTTP headers to send with all IDP requests.
53
+ ssl_verify: Enable SSL certificate verification (default True).
54
+ Set to False only for development with self-signed certificates.
55
+ ssl_ca_bundle: Path to custom CA bundle file for corporate PKI.
56
+ If provided, ssl_verify is implicitly True.
57
+ extra: Additional provider-specific configuration.
58
+
59
+ Example:
60
+ Auto discovery (Keycloak, Auth0, etc.)::
61
+
62
+ AuthProviderConfig(
63
+ client_id="my-app",
64
+ issuer="http://localhost:8080/realms/test",
65
+ )
66
+
67
+ Hybrid mode (discovery + override)::
68
+
69
+ AuthProviderConfig(
70
+ client_id="my-app",
71
+ issuer="https://idp.corp.local",
72
+ end_session_endpoint="https://idp.corp.local/custom/logout", # Override
73
+ )
74
+
75
+ Full manual (legacy IDP without discovery)::
76
+
77
+ AuthProviderConfig(
78
+ client_id="my-app",
79
+ authorize_url="https://old-idp.local/auth",
80
+ token_url="https://old-idp.local/token",
81
+ jwks_uri="https://old-idp.local/certs",
82
+ )
83
+ """
84
+
85
+ client_id: str
86
+ client_secret: str | None = None
87
+ authorize_url: str | None = None
88
+ token_url: str | None = None
89
+ revoke_url: str | None = None
90
+ userinfo_url: str | None = None
91
+ jwks_uri: str | None = None
92
+ end_session_endpoint: str | None = None
93
+ issuer: str | None = None
94
+ scopes: list[str] = field(default_factory=lambda: ["openid"])
95
+ redirect_uri: str = "http://127.0.0.1:8400/callback"
96
+ pkce: bool = True
97
+ discovery_ttl: int = 3600
98
+ headers: dict[str, str] = field(default_factory=dict)
99
+ ssl_verify: bool = True
100
+ ssl_ca_bundle: str | None = None
101
+ extra: dict[str, Any] = field(default_factory=dict)
102
+
103
+ def __post_init__(self) -> None:
104
+ """Validate configuration."""
105
+ if not self.issuer and not (self.authorize_url and self.token_url):
106
+ msg = "Either 'issuer' (OIDC with discovery) or both 'authorize_url' and 'token_url' (manual) required"
107
+ raise ValueError(msg)
108
+
109
+ # SSL/TLS validation (delegated to kstlib.ssl for DRY)
110
+ validate_ssl_verify(self.ssl_verify)
111
+
112
+ if self.ssl_ca_bundle is not None:
113
+ validated_path = validate_ca_bundle_path(self.ssl_ca_bundle)
114
+ object.__setattr__(self, "ssl_ca_bundle", validated_path)
115
+
116
+ @property
117
+ def has_explicit_endpoints(self) -> bool:
118
+ """Check if any endpoints are explicitly configured."""
119
+ return any(
120
+ [
121
+ self.authorize_url,
122
+ self.token_url,
123
+ self.userinfo_url,
124
+ self.jwks_uri,
125
+ self.end_session_endpoint,
126
+ self.revoke_url,
127
+ ]
128
+ )
129
+
130
+
131
+ class AbstractAuthProvider(ABC):
132
+ """Abstract base class for OAuth2/OIDC authentication providers.
133
+
134
+ Subclasses must implement the abstract methods to handle the specific
135
+ authentication flow (OAuth2, OIDC, etc.).
136
+
137
+ Attributes:
138
+ name: Provider identifier (matches config key).
139
+ config: Provider configuration.
140
+ token_storage: Storage backend for tokens.
141
+ """
142
+
143
+ def __init__(
144
+ self,
145
+ name: str,
146
+ config: AuthProviderConfig,
147
+ token_storage: AbstractTokenStorage,
148
+ ) -> None:
149
+ """Initialize the provider.
150
+
151
+ Args:
152
+ name: Provider identifier.
153
+ config: Provider configuration.
154
+ token_storage: Token storage backend.
155
+ """
156
+ self.name = name
157
+ self.config = config
158
+ self.token_storage = token_storage
159
+ self._current_token: Token | None = None
160
+
161
+ # ─────────────────────────────────────────────────────────────────────────
162
+ # Properties
163
+ # ─────────────────────────────────────────────────────────────────────────
164
+
165
+ @property
166
+ def is_authenticated(self) -> bool:
167
+ """Check if a valid (non-expired) token is available."""
168
+ token = self.get_token(auto_refresh=False)
169
+ return token is not None and not token.is_expired
170
+
171
+ @property
172
+ @abstractmethod
173
+ def flow(self) -> AuthFlow:
174
+ """Return the OAuth2 flow used by this provider."""
175
+
176
+ # ─────────────────────────────────────────────────────────────────────────
177
+ # Authorization flow (abstract)
178
+ # ─────────────────────────────────────────────────────────────────────────
179
+
180
+ @abstractmethod
181
+ def get_authorization_url(self, state: str | None = None) -> tuple[str, str]:
182
+ """Generate the authorization URL for the user to visit.
183
+
184
+ Args:
185
+ state: Optional state parameter. Generated if not provided.
186
+
187
+ Returns:
188
+ Tuple of (authorization_url, state).
189
+ """
190
+
191
+ @abstractmethod
192
+ def exchange_code(
193
+ self,
194
+ code: str,
195
+ state: str,
196
+ *,
197
+ code_verifier: str | None = None,
198
+ ) -> Token:
199
+ """Exchange an authorization code for tokens.
200
+
201
+ Args:
202
+ code: Authorization code from callback.
203
+ state: State parameter for CSRF validation.
204
+ code_verifier: PKCE code verifier (required if PKCE was used).
205
+
206
+ Returns:
207
+ Token object with access_token, refresh_token, etc.
208
+
209
+ Raises:
210
+ TokenExchangeError: If the exchange fails.
211
+ """
212
+
213
+ @abstractmethod
214
+ def refresh(self, token: Token | None = None) -> Token:
215
+ """Refresh an expired token.
216
+
217
+ Args:
218
+ token: Token to refresh. Uses stored token if not provided.
219
+
220
+ Returns:
221
+ New Token object.
222
+
223
+ Raises:
224
+ TokenRefreshError: If refresh fails or no refresh_token available.
225
+ """
226
+
227
+ @abstractmethod
228
+ def revoke(self, token: Token | None = None) -> bool:
229
+ """Revoke a token at the authorization server.
230
+
231
+ Args:
232
+ token: Token to revoke. Uses stored token if not provided.
233
+
234
+ Returns:
235
+ True if revoked successfully, False if revocation not supported.
236
+ """
237
+
238
+ # ─────────────────────────────────────────────────────────────────────────
239
+ # Token management
240
+ # ─────────────────────────────────────────────────────────────────────────
241
+
242
+ def get_token(self, *, auto_refresh: bool = True) -> Token | None:
243
+ """Get the current token, optionally refreshing if expired.
244
+
245
+ Args:
246
+ auto_refresh: If True and token is expired, attempt refresh.
247
+
248
+ Returns:
249
+ Token if available, None otherwise.
250
+ """
251
+ if self._current_token is None:
252
+ if logger.isEnabledFor(TRACE_LEVEL):
253
+ logger.log(TRACE_LEVEL, "[AUTH] Loading token from storage for '%s'", self.name)
254
+ self._current_token = self.token_storage.load(self.name)
255
+
256
+ if self._current_token is None:
257
+ if logger.isEnabledFor(TRACE_LEVEL):
258
+ logger.log(TRACE_LEVEL, "[AUTH] No token found for '%s'", self.name)
259
+ return None
260
+
261
+ if self._current_token.should_refresh and auto_refresh:
262
+ if self._current_token.is_refreshable:
263
+ if logger.isEnabledFor(TRACE_LEVEL):
264
+ logger.log(TRACE_LEVEL, "[AUTH] Token needs refresh for '%s'", self.name)
265
+ try:
266
+ self._current_token = self.refresh(self._current_token)
267
+ self.token_storage.save(self.name, self._current_token)
268
+ if logger.isEnabledFor(TRACE_LEVEL):
269
+ logger.log(TRACE_LEVEL, "[AUTH] Token refreshed successfully for '%s'", self.name)
270
+ except Exception as e: # pylint: disable=broad-exception-caught
271
+ # Best-effort refresh, return potentially expired token
272
+ # Clean warning for user, full traceback in DEBUG only
273
+ logger.warning(
274
+ "Token refresh failed for '%s': %s. Using cached token.",
275
+ self.name,
276
+ e,
277
+ )
278
+ logger.debug("Token refresh traceback:", exc_info=True)
279
+ else:
280
+ logger.debug("Token expired and not refreshable for provider '%s'", self.name)
281
+
282
+ return self._current_token
283
+
284
+ def save_token(self, token: Token) -> None:
285
+ """Save a token to storage.
286
+
287
+ Args:
288
+ token: Token to save.
289
+ """
290
+ self._current_token = token
291
+ self.token_storage.save(self.name, token)
292
+
293
+ def clear_token(self) -> None:
294
+ """Clear the current token from memory and storage."""
295
+ self._current_token = None
296
+ self.token_storage.delete(self.name)
297
+
298
+ # ─────────────────────────────────────────────────────────────────────────
299
+ # Preflight validation
300
+ # ─────────────────────────────────────────────────────────────────────────
301
+
302
+ @abstractmethod
303
+ def preflight(self) -> PreflightReport:
304
+ """Run preflight validation checks.
305
+
306
+ Returns:
307
+ PreflightReport with results for each validation step.
308
+ """
309
+
310
+ # ─────────────────────────────────────────────────────────────────────────
311
+ # Context manager support
312
+ # ─────────────────────────────────────────────────────────────────────────
313
+
314
+ def __enter__(self) -> Self:
315
+ """Enter context manager."""
316
+ return self
317
+
318
+ def __exit__(
319
+ self,
320
+ exc_type: type[BaseException] | None,
321
+ exc_val: BaseException | None,
322
+ exc_tb: types.TracebackType | None,
323
+ ) -> None:
324
+ """Exit context manager - clear sensitive data from memory."""
325
+ self._current_token = None
326
+
327
+
328
+ # ─────────────────────────────────────────────────────────────────────────────
329
+ # Helper for from_config factory pattern
330
+ # ─────────────────────────────────────────────────────────────────────────────
331
+
332
+
333
+ def load_provider_from_config(
334
+ provider_name: str,
335
+ allowed_types: tuple[str, ...],
336
+ type_label: str,
337
+ config: dict[str, Any] | None = None,
338
+ **overrides: Any,
339
+ ) -> tuple[AuthProviderConfig, AbstractTokenStorage]:
340
+ """Load provider configuration and token storage from config file.
341
+
342
+ This helper factorizes the common logic for OAuth2Provider.from_config()
343
+ and OIDCProvider.from_config().
344
+
345
+ Args:
346
+ provider_name: Name of the provider in config.
347
+ allowed_types: Tuple of allowed provider type strings (e.g., ("oidc", "openid")).
348
+ type_label: Human-readable type label for error messages (e.g., "oidc").
349
+ config: Optional explicit config dict.
350
+ **overrides: Direct overrides for provider config.
351
+
352
+ Returns:
353
+ Tuple of (AuthProviderConfig, AbstractTokenStorage).
354
+
355
+ Raises:
356
+ ConfigurationError: If provider not found or type mismatch.
357
+ """
358
+ from kstlib.auth.config import (
359
+ build_provider_config,
360
+ get_provider_config,
361
+ get_token_storage_from_config,
362
+ )
363
+ from kstlib.auth.errors import ConfigurationError
364
+
365
+ # Validate provider exists
366
+ provider_cfg = get_provider_config(provider_name, config=config)
367
+ if provider_cfg is None:
368
+ msg = f"Provider '{provider_name}' not found in auth.providers config"
369
+ raise ConfigurationError(msg)
370
+
371
+ # Verify provider type matches
372
+ provider_type = provider_cfg.get("type", allowed_types[0]).lower()
373
+ if provider_type not in allowed_types:
374
+ msg = f"Provider '{provider_name}' has type '{provider_type}', expected '{type_label}'"
375
+ raise ConfigurationError(msg)
376
+
377
+ # Build AuthProviderConfig
378
+ auth_config = build_provider_config(provider_name, config=config, **overrides)
379
+
380
+ # Get token storage
381
+ token_storage = get_token_storage_from_config(
382
+ provider_name=provider_name,
383
+ config=config,
384
+ )
385
+
386
+ return auth_config, token_storage
387
+
388
+
389
+ __all__ = [
390
+ "AbstractAuthProvider",
391
+ "AuthProviderConfig",
392
+ "load_provider_from_config",
393
+ ]