pql-test 0.1.11__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.
- pql_test/__init__.py +33 -0
- pql_test/auth/__init__.py +37 -0
- pql_test/auth/cloud_config.py +63 -0
- pql_test/auth/exceptions.py +29 -0
- pql_test/auth/factory.py +211 -0
- pql_test/auth/manager.py +124 -0
- pql_test/auth/models.py +133 -0
- pql_test/auth/store.py +101 -0
- pql_test/cli.py +1418 -0
- pql_test/runner.py +2255 -0
- pql_test-0.1.11.dist-info/METADATA +1390 -0
- pql_test-0.1.11.dist-info/RECORD +16 -0
- pql_test-0.1.11.dist-info/WHEEL +5 -0
- pql_test-0.1.11.dist-info/entry_points.txt +2 -0
- pql_test-0.1.11.dist-info/licenses/LICENSE.txt +97 -0
- pql_test-0.1.11.dist-info/top_level.txt +1 -0
pql_test/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""pql_test package — DAX test runner for PBIP semantic models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
|
|
7
|
+
# Expose main modules for easier imports
|
|
8
|
+
from pql_test.runner import (
|
|
9
|
+
Environment,
|
|
10
|
+
ModelIdentifier,
|
|
11
|
+
PqlTest,
|
|
12
|
+
TestResult,
|
|
13
|
+
TestSuite,
|
|
14
|
+
XmlaConnectionOptions,
|
|
15
|
+
parse_model_identifier,
|
|
16
|
+
retrieve_tests,
|
|
17
|
+
retrieve_test_by_name,
|
|
18
|
+
run_tests_from_model,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"__version__",
|
|
23
|
+
"Environment",
|
|
24
|
+
"ModelIdentifier",
|
|
25
|
+
"PqlTest",
|
|
26
|
+
"TestResult",
|
|
27
|
+
"TestSuite",
|
|
28
|
+
"XmlaConnectionOptions",
|
|
29
|
+
"parse_model_identifier",
|
|
30
|
+
"retrieve_tests",
|
|
31
|
+
"retrieve_test_by_name",
|
|
32
|
+
"run_tests_from_model",
|
|
33
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pql_test.auth package — Authentication management for pql-test.
|
|
3
|
+
|
|
4
|
+
This package provides:
|
|
5
|
+
- Secure credential storage using platform keyring
|
|
6
|
+
- Token acquisition via azure.identity
|
|
7
|
+
- Support for multiple authentication methods (interactive, service principal, managed identity)
|
|
8
|
+
- Backward compatibility with environment variables and CLI flags
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from pql_test.auth.cloud_config import CloudConfig
|
|
14
|
+
from pql_test.auth.exceptions import (
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
CredentialConfigError,
|
|
17
|
+
CredentialStorageError,
|
|
18
|
+
TokenAcquisitionError,
|
|
19
|
+
)
|
|
20
|
+
from pql_test.auth.factory import CredentialFactory
|
|
21
|
+
from pql_test.auth.manager import AuthenticationManager
|
|
22
|
+
from pql_test.auth.models import AuthMethod, AuthStatus, CredentialConfig
|
|
23
|
+
from pql_test.auth.store import CredentialStore
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"AuthenticationManager",
|
|
27
|
+
"CredentialConfig",
|
|
28
|
+
"AuthStatus",
|
|
29
|
+
"AuthMethod",
|
|
30
|
+
"CloudConfig",
|
|
31
|
+
"CredentialStore",
|
|
32
|
+
"CredentialFactory",
|
|
33
|
+
"AuthenticationError",
|
|
34
|
+
"CredentialStorageError",
|
|
35
|
+
"TokenAcquisitionError",
|
|
36
|
+
"CredentialConfigError",
|
|
37
|
+
]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cloud environment configuration for sovereign clouds.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CloudConfig:
|
|
9
|
+
"""Cloud environment configuration for Azure sovereign clouds."""
|
|
10
|
+
|
|
11
|
+
ENVIRONMENTS = {
|
|
12
|
+
"Public": {
|
|
13
|
+
"authority": "https://login.microsoftonline.com",
|
|
14
|
+
"resource": "https://analysis.windows.net/powerbi/api",
|
|
15
|
+
},
|
|
16
|
+
"USGov": {
|
|
17
|
+
"authority": "https://login.microsoftonline.us",
|
|
18
|
+
"resource": "https://analysis.usgovcloudapi.net/powerbi/api",
|
|
19
|
+
},
|
|
20
|
+
"USGovHigh": {
|
|
21
|
+
"authority": "https://login.microsoftonline.us",
|
|
22
|
+
"resource": "https://high.analysis.usgovcloudapi.net/powerbi/api",
|
|
23
|
+
},
|
|
24
|
+
"USGovDoD": {
|
|
25
|
+
"authority": "https://login.microsoftonline.us",
|
|
26
|
+
"resource": "https://mil.analysis.usgovcloudapi.net/powerbi/api",
|
|
27
|
+
},
|
|
28
|
+
"Germany": {
|
|
29
|
+
"authority": "https://login.microsoftonline.de",
|
|
30
|
+
"resource": "https://analysis.cloudapi.de/powerbi/api",
|
|
31
|
+
},
|
|
32
|
+
"China": {
|
|
33
|
+
"authority": "https://login.chinacloudapi.cn",
|
|
34
|
+
"resource": "https://analysis.chinacloudapi.cn/powerbi/api",
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def get_authority(self, environment: str) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Get Azure AD authority URL for environment.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
environment: Cloud environment name
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Authority URL for the specified environment (defaults to Public)
|
|
47
|
+
"""
|
|
48
|
+
env_config = self.ENVIRONMENTS.get(environment, self.ENVIRONMENTS["Public"])
|
|
49
|
+
return env_config["authority"]
|
|
50
|
+
|
|
51
|
+
def get_resource_scope(self, environment: str) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Get Power BI resource scope for environment.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
environment: Cloud environment name
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Resource scope URL with /.default suffix
|
|
60
|
+
"""
|
|
61
|
+
env_config = self.ENVIRONMENTS.get(environment, self.ENVIRONMENTS["Public"])
|
|
62
|
+
resource = env_config["resource"]
|
|
63
|
+
return f"{resource}/.default"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Exceptions for authentication operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthenticationError(Exception):
|
|
9
|
+
"""Base class for authentication errors."""
|
|
10
|
+
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CredentialStorageError(AuthenticationError):
|
|
15
|
+
"""Error storing or retrieving credentials from keyring."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TokenAcquisitionError(AuthenticationError):
|
|
21
|
+
"""Error acquiring access token from Azure AD."""
|
|
22
|
+
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class CredentialConfigError(AuthenticationError):
|
|
27
|
+
"""Invalid credential configuration."""
|
|
28
|
+
|
|
29
|
+
pass
|
pql_test/auth/factory.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Credential factory for creating azure.identity credential objects.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from azure.identity import (
|
|
12
|
+
AuthenticationRecord,
|
|
13
|
+
CertificateCredential,
|
|
14
|
+
ClientAssertionCredential,
|
|
15
|
+
ClientSecretCredential,
|
|
16
|
+
InteractiveBrowserCredential,
|
|
17
|
+
ManagedIdentityCredential,
|
|
18
|
+
TokenCachePersistenceOptions,
|
|
19
|
+
)
|
|
20
|
+
from azure.core.credentials import TokenCredential
|
|
21
|
+
HAS_AZURE_IDENTITY = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
HAS_AZURE_IDENTITY = False
|
|
24
|
+
AuthenticationRecord = None # type: ignore[assignment,misc]
|
|
25
|
+
TokenCachePersistenceOptions = None # type: ignore[assignment,misc]
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from azure.core.credentials import TokenCredential
|
|
28
|
+
|
|
29
|
+
from pql_test.auth.cloud_config import CloudConfig
|
|
30
|
+
from pql_test.auth.exceptions import CredentialConfigError
|
|
31
|
+
from pql_test.auth.models import AuthMethod, CredentialConfig
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Default interactive client_id
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
#
|
|
38
|
+
# Power BI's XMLA endpoint only accepts bearer tokens whose `appid` claim
|
|
39
|
+
# matches a client app the gateway trusts. azure-identity's
|
|
40
|
+
# InteractiveBrowserCredential defaults to the Azure CLI public client
|
|
41
|
+
# (`04b07795-…`), which Power BI XMLA rejects with
|
|
42
|
+
# `Authentication failed for all authenticators`. Verified empirically:
|
|
43
|
+
# with this client_id removed, a fresh interactive sign-in (cleared cache,
|
|
44
|
+
# no .env) fails against XMLA; with it present, it succeeds.
|
|
45
|
+
#
|
|
46
|
+
# The pql-test project registers its own multi-tenant Azure AD application
|
|
47
|
+
# (public client, delegated Power BI permissions) and ships its client_id
|
|
48
|
+
# here as the default for the INTERACTIVE auth method. End users grant
|
|
49
|
+
# consent on first sign-in and never have to register their own app.
|
|
50
|
+
#
|
|
51
|
+
# Forks / enterprises that have registered their own pql-test equivalent
|
|
52
|
+
# can override via the PQL_TEST_INTERACTIVE_CLIENT_ID env var without
|
|
53
|
+
# touching the source.
|
|
54
|
+
PQL_TEST_DEFAULT_INTERACTIVE_CLIENT_ID = "e6341730-1a0b-4a9e-9462-a435d224b5fd"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _resolve_interactive_client_id(config_client_id: str | None) -> str:
|
|
58
|
+
"""Resolution order: explicit config > env-var override > bundled default."""
|
|
59
|
+
if config_client_id:
|
|
60
|
+
return config_client_id
|
|
61
|
+
return (
|
|
62
|
+
os.environ.get("PQL_TEST_INTERACTIVE_CLIENT_ID")
|
|
63
|
+
or PQL_TEST_DEFAULT_INTERACTIVE_CLIENT_ID
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _persistent_token_cache_options(): # type: ignore[no-untyped-def]
|
|
68
|
+
"""Build ``TokenCachePersistenceOptions`` so MSAL writes tokens to the
|
|
69
|
+
platform's secure store (Windows DPAPI / macOS Keychain / Linux
|
|
70
|
+
libsecret). Without this, every CLI invocation builds a fresh credential
|
|
71
|
+
with a cold cache and triggers a new interactive sign-in — the user
|
|
72
|
+
sees a browser tab pop on every ``pql-test`` command.
|
|
73
|
+
|
|
74
|
+
Defaults to encrypted-only storage. Set
|
|
75
|
+
``PQL_TEST_ALLOW_UNENCRYPTED_TOKEN_CACHE=1`` on headless Linux without
|
|
76
|
+
libsecret to opt into unencrypted file-backed caching.
|
|
77
|
+
|
|
78
|
+
Returns ``None`` if azure-identity is missing — the caller treats that
|
|
79
|
+
as "no persistence configured" which falls back to azure-identity's
|
|
80
|
+
default in-memory cache.
|
|
81
|
+
"""
|
|
82
|
+
if TokenCachePersistenceOptions is None:
|
|
83
|
+
return None
|
|
84
|
+
allow_unencrypted = os.environ.get(
|
|
85
|
+
"PQL_TEST_ALLOW_UNENCRYPTED_TOKEN_CACHE", ""
|
|
86
|
+
).lower() in ("1", "true", "yes")
|
|
87
|
+
return TokenCachePersistenceOptions(
|
|
88
|
+
name="pql-test",
|
|
89
|
+
allow_unencrypted_storage=allow_unencrypted,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class CredentialFactory:
|
|
94
|
+
"""Factory for creating azure.identity credential objects from configuration."""
|
|
95
|
+
|
|
96
|
+
def create(
|
|
97
|
+
self, config: CredentialConfig, cloud_config: CloudConfig
|
|
98
|
+
) -> TokenCredential:
|
|
99
|
+
"""
|
|
100
|
+
Create TokenCredential from configuration.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
config: Credential configuration
|
|
104
|
+
cloud_config: Cloud environment configuration
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
TokenCredential instance
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
CredentialConfigError: If azure-identity not installed or config is invalid
|
|
111
|
+
"""
|
|
112
|
+
if not HAS_AZURE_IDENTITY:
|
|
113
|
+
raise CredentialConfigError(
|
|
114
|
+
"azure-identity is required for authentication. "
|
|
115
|
+
"Install with: pip install azure-identity"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
authority = cloud_config.get_authority(config.environment)
|
|
119
|
+
cache_opts = _persistent_token_cache_options()
|
|
120
|
+
|
|
121
|
+
if config.method == AuthMethod.INTERACTIVE:
|
|
122
|
+
# Replay the AuthenticationRecord captured on first sign-in so
|
|
123
|
+
# azure-identity can look up the cached token by homeAccountId
|
|
124
|
+
# instead of triggering a fresh browser flow. Without this,
|
|
125
|
+
# cache_persistence_options writes a cache that's never read.
|
|
126
|
+
record = None
|
|
127
|
+
if config.authentication_record_json and AuthenticationRecord is not None:
|
|
128
|
+
try:
|
|
129
|
+
record = AuthenticationRecord.deserialize(
|
|
130
|
+
config.authentication_record_json
|
|
131
|
+
)
|
|
132
|
+
except Exception: # noqa: BLE001
|
|
133
|
+
# Corrupted record — fall through to interactive prompt.
|
|
134
|
+
record = None
|
|
135
|
+
|
|
136
|
+
# When a record is supplied, azure-identity reads tenant_id,
|
|
137
|
+
# authority, and client_id from it directly — passing those
|
|
138
|
+
# kwargs ALSO causes a "got multiple values for keyword argument"
|
|
139
|
+
# TypeError inside MsalCredential.__init__. Only pass them on
|
|
140
|
+
# the first-login path where we have no record.
|
|
141
|
+
if record is not None:
|
|
142
|
+
return InteractiveBrowserCredential(
|
|
143
|
+
cache_persistence_options=cache_opts,
|
|
144
|
+
authentication_record=record,
|
|
145
|
+
)
|
|
146
|
+
return InteractiveBrowserCredential(
|
|
147
|
+
tenant_id=config.tenant_id,
|
|
148
|
+
authority=authority,
|
|
149
|
+
client_id=_resolve_interactive_client_id(config.client_id),
|
|
150
|
+
cache_persistence_options=cache_opts,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
elif config.method == AuthMethod.SERVICE_PRINCIPAL_SECRET:
|
|
154
|
+
if not all([config.tenant_id, config.client_id, config.client_secret]):
|
|
155
|
+
raise CredentialConfigError(
|
|
156
|
+
"Service principal with secret requires tenant_id, client_id, and client_secret"
|
|
157
|
+
)
|
|
158
|
+
return ClientSecretCredential(
|
|
159
|
+
tenant_id=config.tenant_id,
|
|
160
|
+
client_id=config.client_id,
|
|
161
|
+
client_secret=config.client_secret,
|
|
162
|
+
authority=authority,
|
|
163
|
+
cache_persistence_options=cache_opts,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
elif config.method == AuthMethod.SERVICE_PRINCIPAL_CERT:
|
|
167
|
+
if not all([config.tenant_id, config.client_id, config.certificate_path]):
|
|
168
|
+
raise CredentialConfigError(
|
|
169
|
+
"Service principal with certificate requires tenant_id, client_id, and certificate_path"
|
|
170
|
+
)
|
|
171
|
+
return CertificateCredential(
|
|
172
|
+
tenant_id=config.tenant_id,
|
|
173
|
+
client_id=config.client_id,
|
|
174
|
+
certificate_path=config.certificate_path,
|
|
175
|
+
password=config.certificate_password,
|
|
176
|
+
authority=authority,
|
|
177
|
+
cache_persistence_options=cache_opts,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
elif config.method == AuthMethod.SERVICE_PRINCIPAL_FEDERATED:
|
|
181
|
+
if not all([config.tenant_id, config.client_id, config.federated_token]):
|
|
182
|
+
raise CredentialConfigError(
|
|
183
|
+
"Federated credential requires tenant_id, client_id, and federated_token"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Note: azure-identity uses ClientAssertionCredential for federated tokens
|
|
187
|
+
def token_provider() -> str:
|
|
188
|
+
return config.federated_token or ""
|
|
189
|
+
|
|
190
|
+
return ClientAssertionCredential(
|
|
191
|
+
tenant_id=config.tenant_id,
|
|
192
|
+
client_id=config.client_id,
|
|
193
|
+
func=token_provider,
|
|
194
|
+
authority=authority,
|
|
195
|
+
cache_persistence_options=cache_opts,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
elif config.method == AuthMethod.MANAGED_IDENTITY:
|
|
199
|
+
if config.use_system_assigned_identity:
|
|
200
|
+
return ManagedIdentityCredential()
|
|
201
|
+
else:
|
|
202
|
+
if not config.client_id:
|
|
203
|
+
raise CredentialConfigError(
|
|
204
|
+
"User-assigned managed identity requires client_id"
|
|
205
|
+
)
|
|
206
|
+
return ManagedIdentityCredential(client_id=config.client_id)
|
|
207
|
+
|
|
208
|
+
else:
|
|
209
|
+
raise CredentialConfigError(
|
|
210
|
+
f"Unsupported authentication method: {config.method}"
|
|
211
|
+
)
|
pql_test/auth/manager.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Authentication manager — Main interface for authentication operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
from azure.core.credentials import TokenCredential
|
|
11
|
+
except ImportError:
|
|
12
|
+
TokenCredential = None # type: ignore
|
|
13
|
+
|
|
14
|
+
from pql_test.auth.cloud_config import CloudConfig
|
|
15
|
+
from pql_test.auth.exceptions import TokenAcquisitionError
|
|
16
|
+
from pql_test.auth.factory import CredentialFactory
|
|
17
|
+
from pql_test.auth.models import AuthStatus, CredentialConfig
|
|
18
|
+
from pql_test.auth.store import CredentialStore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthenticationManager:
|
|
22
|
+
"""
|
|
23
|
+
Manages authentication for pql-test.
|
|
24
|
+
|
|
25
|
+
Handles credential storage, token acquisition, and authentication state.
|
|
26
|
+
Provides a unified interface for all authentication methods.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, namespace: str = "pql-test"):
|
|
30
|
+
"""
|
|
31
|
+
Initialize authentication manager.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
namespace: Keyring namespace for credential storage
|
|
35
|
+
"""
|
|
36
|
+
self._store = CredentialStore(namespace)
|
|
37
|
+
self._factory = CredentialFactory()
|
|
38
|
+
self._cloud_config = CloudConfig()
|
|
39
|
+
|
|
40
|
+
def save_credential(self, config: CredentialConfig) -> None:
|
|
41
|
+
"""
|
|
42
|
+
Save credential configuration to secure storage.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
config: Credential configuration to store
|
|
46
|
+
|
|
47
|
+
Raises:
|
|
48
|
+
CredentialStorageError: If credential cannot be stored
|
|
49
|
+
"""
|
|
50
|
+
self._store.save(config)
|
|
51
|
+
|
|
52
|
+
def get_credential(self) -> Optional[TokenCredential]:
|
|
53
|
+
"""
|
|
54
|
+
Get credential from stored configuration.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
TokenCredential instance or None if not authenticated
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
CredentialConfigError: If stored config is invalid
|
|
61
|
+
"""
|
|
62
|
+
config = self._store.load()
|
|
63
|
+
if config is None:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
return self._factory.create(config, self._cloud_config)
|
|
67
|
+
|
|
68
|
+
def get_access_token(
|
|
69
|
+
self, scope: str = "https://analysis.windows.net/powerbi/api/.default"
|
|
70
|
+
) -> Optional[str]:
|
|
71
|
+
"""
|
|
72
|
+
Get access token for the given scope.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
scope: OAuth2 scope for the token
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Access token string or None if not authenticated
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
TokenAcquisitionError: If token acquisition fails
|
|
82
|
+
"""
|
|
83
|
+
credential = self.get_credential()
|
|
84
|
+
if credential is None:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
token_result = credential.get_token(scope)
|
|
89
|
+
return token_result.token
|
|
90
|
+
except Exception as e:
|
|
91
|
+
raise TokenAcquisitionError(
|
|
92
|
+
f"Failed to acquire access token: {e}"
|
|
93
|
+
) from e
|
|
94
|
+
|
|
95
|
+
def clear_credential(self) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Clear stored credential configuration.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
CredentialStorageError: If credential cannot be cleared
|
|
101
|
+
"""
|
|
102
|
+
self._store.delete()
|
|
103
|
+
|
|
104
|
+
def get_status(self) -> AuthStatus:
|
|
105
|
+
"""
|
|
106
|
+
Get current authentication status.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
AuthStatus with current authentication state
|
|
110
|
+
"""
|
|
111
|
+
config = self._store.load()
|
|
112
|
+
|
|
113
|
+
if config is None:
|
|
114
|
+
return AuthStatus(authenticated=False)
|
|
115
|
+
|
|
116
|
+
return AuthStatus(
|
|
117
|
+
authenticated=True,
|
|
118
|
+
method=config.method,
|
|
119
|
+
account_name=config.account_name,
|
|
120
|
+
tenant_id=config.tenant_id,
|
|
121
|
+
client_id=config.client_id,
|
|
122
|
+
environment=config.environment,
|
|
123
|
+
token_is_valid=False, # TODO: Implement token validation
|
|
124
|
+
)
|
pql_test/auth/models.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for authentication configuration and status.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuthMethod(Enum):
|
|
15
|
+
"""Supported authentication methods."""
|
|
16
|
+
|
|
17
|
+
INTERACTIVE = "interactive"
|
|
18
|
+
SERVICE_PRINCIPAL_SECRET = "service_principal_secret"
|
|
19
|
+
SERVICE_PRINCIPAL_CERT = "service_principal_cert"
|
|
20
|
+
SERVICE_PRINCIPAL_FEDERATED = "service_principal_federated"
|
|
21
|
+
MANAGED_IDENTITY = "managed_identity"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CredentialConfig:
|
|
26
|
+
"""
|
|
27
|
+
Credential configuration stored in keyring.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
method: Authentication method
|
|
31
|
+
tenant_id: Azure AD tenant ID (optional for some methods)
|
|
32
|
+
client_id: Client/application ID
|
|
33
|
+
client_secret: Client secret (service principal with secret only)
|
|
34
|
+
certificate_path: Path to certificate file (certificate auth only)
|
|
35
|
+
certificate_password: Certificate password (encrypted cert only)
|
|
36
|
+
federated_token: Federated credential token (federated auth only)
|
|
37
|
+
use_system_assigned_identity: Use system-assigned managed identity
|
|
38
|
+
environment: Cloud environment (Public, USGov, etc.)
|
|
39
|
+
account_name: Display name for status output
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
method: AuthMethod
|
|
43
|
+
tenant_id: Optional[str] = None
|
|
44
|
+
client_id: Optional[str] = None
|
|
45
|
+
client_secret: Optional[str] = None
|
|
46
|
+
certificate_path: Optional[str] = None
|
|
47
|
+
certificate_password: Optional[str] = None
|
|
48
|
+
federated_token: Optional[str] = None
|
|
49
|
+
use_system_assigned_identity: bool = False
|
|
50
|
+
environment: str = "Public"
|
|
51
|
+
account_name: Optional[str] = None
|
|
52
|
+
# Serialized azure-identity AuthenticationRecord (JSON) — captured on
|
|
53
|
+
# interactive sign-in and replayed on subsequent runs so the persistent
|
|
54
|
+
# MSAL token cache can be looked up silently instead of triggering a
|
|
55
|
+
# fresh browser flow. Only populated for AuthMethod.INTERACTIVE.
|
|
56
|
+
authentication_record_json: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict:
|
|
59
|
+
"""
|
|
60
|
+
Serialize to dictionary.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dictionary representation with AuthMethod converted to string
|
|
64
|
+
"""
|
|
65
|
+
data = asdict(self)
|
|
66
|
+
data["method"] = self.method.value
|
|
67
|
+
return data
|
|
68
|
+
|
|
69
|
+
def to_json(self) -> str:
|
|
70
|
+
"""
|
|
71
|
+
Serialize to JSON string.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
JSON string representation
|
|
75
|
+
"""
|
|
76
|
+
return json.dumps(self.to_dict())
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_dict(cls, data: dict) -> CredentialConfig:
|
|
80
|
+
"""
|
|
81
|
+
Deserialize from dictionary.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
data: Dictionary representation
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
CredentialConfig instance
|
|
88
|
+
"""
|
|
89
|
+
# Convert method string to enum
|
|
90
|
+
if "method" in data and isinstance(data["method"], str):
|
|
91
|
+
data = data.copy()
|
|
92
|
+
data["method"] = AuthMethod(data["method"])
|
|
93
|
+
return cls(**data)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def from_json(cls, json_str: str) -> CredentialConfig:
|
|
97
|
+
"""
|
|
98
|
+
Deserialize from JSON string.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
json_str: JSON string representation
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
CredentialConfig instance
|
|
105
|
+
"""
|
|
106
|
+
data = json.loads(json_str)
|
|
107
|
+
return cls.from_dict(data)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class AuthStatus:
|
|
112
|
+
"""
|
|
113
|
+
Current authentication status.
|
|
114
|
+
|
|
115
|
+
Attributes:
|
|
116
|
+
authenticated: Whether user is authenticated
|
|
117
|
+
method: Authentication method used (if authenticated)
|
|
118
|
+
account_name: Display name of authenticated account
|
|
119
|
+
tenant_id: Azure AD tenant ID
|
|
120
|
+
client_id: Client/application ID
|
|
121
|
+
environment: Cloud environment
|
|
122
|
+
token_expires_at: Token expiration timestamp
|
|
123
|
+
token_is_valid: Whether token is currently valid (not expired)
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
authenticated: bool
|
|
127
|
+
method: Optional[AuthMethod] = None
|
|
128
|
+
account_name: Optional[str] = None
|
|
129
|
+
tenant_id: Optional[str] = None
|
|
130
|
+
client_id: Optional[str] = None
|
|
131
|
+
environment: str = "Public"
|
|
132
|
+
token_expires_at: Optional[datetime] = None
|
|
133
|
+
token_is_valid: bool = False
|