kstlib 0.0.1a0__py3-none-any.whl → 1.0.0__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.
- kstlib/__init__.py +266 -1
- kstlib/__main__.py +16 -0
- kstlib/alerts/__init__.py +110 -0
- kstlib/alerts/channels/__init__.py +36 -0
- kstlib/alerts/channels/base.py +197 -0
- kstlib/alerts/channels/email.py +227 -0
- kstlib/alerts/channels/slack.py +389 -0
- kstlib/alerts/exceptions.py +72 -0
- kstlib/alerts/manager.py +651 -0
- kstlib/alerts/models.py +142 -0
- kstlib/alerts/throttle.py +263 -0
- kstlib/auth/__init__.py +139 -0
- kstlib/auth/callback.py +399 -0
- kstlib/auth/config.py +502 -0
- kstlib/auth/errors.py +127 -0
- kstlib/auth/models.py +316 -0
- kstlib/auth/providers/__init__.py +14 -0
- kstlib/auth/providers/base.py +393 -0
- kstlib/auth/providers/oauth2.py +645 -0
- kstlib/auth/providers/oidc.py +821 -0
- kstlib/auth/session.py +338 -0
- kstlib/auth/token.py +482 -0
- kstlib/cache/__init__.py +50 -0
- kstlib/cache/decorator.py +261 -0
- kstlib/cache/strategies.py +516 -0
- kstlib/cli/__init__.py +8 -0
- kstlib/cli/app.py +195 -0
- kstlib/cli/commands/__init__.py +5 -0
- kstlib/cli/commands/auth/__init__.py +39 -0
- kstlib/cli/commands/auth/common.py +122 -0
- kstlib/cli/commands/auth/login.py +325 -0
- kstlib/cli/commands/auth/logout.py +74 -0
- kstlib/cli/commands/auth/providers.py +57 -0
- kstlib/cli/commands/auth/status.py +291 -0
- kstlib/cli/commands/auth/token.py +199 -0
- kstlib/cli/commands/auth/whoami.py +106 -0
- kstlib/cli/commands/config.py +89 -0
- kstlib/cli/commands/ops/__init__.py +39 -0
- kstlib/cli/commands/ops/attach.py +49 -0
- kstlib/cli/commands/ops/common.py +269 -0
- kstlib/cli/commands/ops/list_sessions.py +252 -0
- kstlib/cli/commands/ops/logs.py +49 -0
- kstlib/cli/commands/ops/start.py +98 -0
- kstlib/cli/commands/ops/status.py +138 -0
- kstlib/cli/commands/ops/stop.py +60 -0
- kstlib/cli/commands/rapi/__init__.py +60 -0
- kstlib/cli/commands/rapi/call.py +341 -0
- kstlib/cli/commands/rapi/list.py +99 -0
- kstlib/cli/commands/rapi/show.py +206 -0
- kstlib/cli/commands/secrets/__init__.py +35 -0
- kstlib/cli/commands/secrets/common.py +425 -0
- kstlib/cli/commands/secrets/decrypt.py +88 -0
- kstlib/cli/commands/secrets/doctor.py +743 -0
- kstlib/cli/commands/secrets/encrypt.py +242 -0
- kstlib/cli/commands/secrets/shred.py +96 -0
- kstlib/cli/common.py +86 -0
- kstlib/config/__init__.py +76 -0
- kstlib/config/exceptions.py +110 -0
- kstlib/config/export.py +225 -0
- kstlib/config/loader.py +963 -0
- kstlib/config/sops.py +287 -0
- kstlib/db/__init__.py +54 -0
- kstlib/db/aiosqlcipher.py +137 -0
- kstlib/db/cipher.py +112 -0
- kstlib/db/database.py +367 -0
- kstlib/db/exceptions.py +25 -0
- kstlib/db/pool.py +302 -0
- kstlib/helpers/__init__.py +35 -0
- kstlib/helpers/exceptions.py +11 -0
- kstlib/helpers/time_trigger.py +396 -0
- kstlib/kstlib.conf.yml +890 -0
- kstlib/limits.py +963 -0
- kstlib/logging/__init__.py +108 -0
- kstlib/logging/manager.py +633 -0
- kstlib/mail/__init__.py +42 -0
- kstlib/mail/builder.py +626 -0
- kstlib/mail/exceptions.py +27 -0
- kstlib/mail/filesystem.py +248 -0
- kstlib/mail/transport.py +224 -0
- kstlib/mail/transports/__init__.py +19 -0
- kstlib/mail/transports/gmail.py +268 -0
- kstlib/mail/transports/resend.py +324 -0
- kstlib/mail/transports/smtp.py +326 -0
- kstlib/meta.py +72 -0
- kstlib/metrics/__init__.py +88 -0
- kstlib/metrics/decorators.py +1090 -0
- kstlib/metrics/exceptions.py +14 -0
- kstlib/monitoring/__init__.py +116 -0
- kstlib/monitoring/_styles.py +163 -0
- kstlib/monitoring/cell.py +57 -0
- kstlib/monitoring/config.py +424 -0
- kstlib/monitoring/delivery.py +579 -0
- kstlib/monitoring/exceptions.py +63 -0
- kstlib/monitoring/image.py +220 -0
- kstlib/monitoring/kv.py +79 -0
- kstlib/monitoring/list.py +69 -0
- kstlib/monitoring/metric.py +88 -0
- kstlib/monitoring/monitoring.py +341 -0
- kstlib/monitoring/renderer.py +139 -0
- kstlib/monitoring/service.py +392 -0
- kstlib/monitoring/table.py +129 -0
- kstlib/monitoring/types.py +56 -0
- kstlib/ops/__init__.py +86 -0
- kstlib/ops/base.py +148 -0
- kstlib/ops/container.py +577 -0
- kstlib/ops/exceptions.py +209 -0
- kstlib/ops/manager.py +407 -0
- kstlib/ops/models.py +176 -0
- kstlib/ops/tmux.py +372 -0
- kstlib/ops/validators.py +287 -0
- kstlib/py.typed +0 -0
- kstlib/rapi/__init__.py +118 -0
- kstlib/rapi/client.py +875 -0
- kstlib/rapi/config.py +861 -0
- kstlib/rapi/credentials.py +887 -0
- kstlib/rapi/exceptions.py +213 -0
- kstlib/resilience/__init__.py +101 -0
- kstlib/resilience/circuit_breaker.py +440 -0
- kstlib/resilience/exceptions.py +95 -0
- kstlib/resilience/heartbeat.py +491 -0
- kstlib/resilience/rate_limiter.py +506 -0
- kstlib/resilience/shutdown.py +417 -0
- kstlib/resilience/watchdog.py +637 -0
- kstlib/secrets/__init__.py +29 -0
- kstlib/secrets/exceptions.py +19 -0
- kstlib/secrets/models.py +62 -0
- kstlib/secrets/providers/__init__.py +79 -0
- kstlib/secrets/providers/base.py +58 -0
- kstlib/secrets/providers/environment.py +66 -0
- kstlib/secrets/providers/keyring.py +107 -0
- kstlib/secrets/providers/kms.py +223 -0
- kstlib/secrets/providers/kwargs.py +101 -0
- kstlib/secrets/providers/sops.py +209 -0
- kstlib/secrets/resolver.py +221 -0
- kstlib/secrets/sensitive.py +130 -0
- kstlib/secure/__init__.py +23 -0
- kstlib/secure/fs.py +194 -0
- kstlib/secure/permissions.py +70 -0
- kstlib/ssl.py +347 -0
- kstlib/ui/__init__.py +23 -0
- kstlib/ui/exceptions.py +26 -0
- kstlib/ui/panels.py +484 -0
- kstlib/ui/spinner.py +864 -0
- kstlib/ui/tables.py +382 -0
- kstlib/utils/__init__.py +48 -0
- kstlib/utils/dict.py +36 -0
- kstlib/utils/formatting.py +338 -0
- kstlib/utils/http_trace.py +237 -0
- kstlib/utils/lazy.py +49 -0
- kstlib/utils/secure_delete.py +205 -0
- kstlib/utils/serialization.py +247 -0
- kstlib/utils/text.py +56 -0
- kstlib/utils/validators.py +124 -0
- kstlib/websocket/__init__.py +97 -0
- kstlib/websocket/exceptions.py +214 -0
- kstlib/websocket/manager.py +1102 -0
- kstlib/websocket/models.py +361 -0
- kstlib-1.0.0.dist-info/METADATA +201 -0
- kstlib-1.0.0.dist-info/RECORD +163 -0
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.dist-info}/WHEEL +1 -1
- kstlib-1.0.0.dist-info/entry_points.txt +2 -0
- kstlib-1.0.0.dist-info/licenses/LICENSE.md +9 -0
- kstlib-0.0.1a0.dist-info/METADATA +0 -29
- kstlib-0.0.1a0.dist-info/RECORD +0 -6
- kstlib-0.0.1a0.dist-info/licenses/LICENSE.md +0 -5
- {kstlib-0.0.1a0.dist-info → kstlib-1.0.0.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
|
+
]
|