xenfra 0.3.0__py3-none-any.whl → 0.3.2__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.
- xenfra/commands/auth.py +2 -2
- xenfra/commands/auth_device.py +7 -2
- xenfra/utils/auth.py +97 -21
- {xenfra-0.3.0.dist-info → xenfra-0.3.2.dist-info}/METADATA +1 -1
- {xenfra-0.3.0.dist-info → xenfra-0.3.2.dist-info}/RECORD +7 -7
- {xenfra-0.3.0.dist-info → xenfra-0.3.2.dist-info}/WHEEL +0 -0
- {xenfra-0.3.0.dist-info → xenfra-0.3.2.dist-info}/entry_points.txt +0 -0
xenfra/commands/auth.py
CHANGED
|
@@ -128,8 +128,8 @@ def whoami(token):
|
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
console.print("[bold green]Logged in as:[/bold green]")
|
|
131
|
-
console.print(f"
|
|
132
|
-
console.print(f"
|
|
131
|
+
console.print(f" Email: {claims.get('sub', 'N/A')}")
|
|
132
|
+
console.print(f" User ID: {claims.get('user_id', 'N/A')}")
|
|
133
133
|
|
|
134
134
|
if token:
|
|
135
135
|
console.print(f"\n[dim]Access Token:[/dim]\n{access_token}")
|
xenfra/commands/auth_device.py
CHANGED
|
@@ -105,13 +105,18 @@ def device_login():
|
|
|
105
105
|
access_token = token_data["access_token"]
|
|
106
106
|
refresh_token = token_data.get("refresh_token")
|
|
107
107
|
|
|
108
|
-
# Store tokens
|
|
108
|
+
# Store tokens (keyring or file fallback)
|
|
109
109
|
try:
|
|
110
110
|
keyring.set_password(SERVICE_ID, "access_token", access_token)
|
|
111
111
|
if refresh_token:
|
|
112
112
|
keyring.set_password(SERVICE_ID, "refresh_token", refresh_token)
|
|
113
113
|
except keyring.errors.KeyringError as e:
|
|
114
|
-
console.print(f"[
|
|
114
|
+
console.print(f"[dim]Keyring unavailable, using file storage: {e}[/dim]")
|
|
115
|
+
# Fallback to file storage
|
|
116
|
+
from ..utils.auth import _set_token_to_file
|
|
117
|
+
_set_token_to_file("access_token", access_token)
|
|
118
|
+
if refresh_token:
|
|
119
|
+
_set_token_to_file("refresh_token", refresh_token)
|
|
115
120
|
|
|
116
121
|
console.print()
|
|
117
122
|
console.print("[bold green]✓ Successfully authenticated![/bold green]")
|
xenfra/utils/auth.py
CHANGED
|
@@ -3,7 +3,10 @@ Authentication utilities for Xenfra CLI.
|
|
|
3
3
|
Handles OAuth2 PKCE flow and token management.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
6
8
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
9
|
+
from pathlib import Path
|
|
7
10
|
from urllib.parse import parse_qs, urlparse
|
|
8
11
|
|
|
9
12
|
import httpx
|
|
@@ -24,6 +27,9 @@ console = Console()
|
|
|
24
27
|
API_BASE_URL = validate_and_get_api_url()
|
|
25
28
|
SERVICE_ID = "xenfra"
|
|
26
29
|
|
|
30
|
+
# Fallback file-based token storage (for Windows if keyring fails)
|
|
31
|
+
TOKEN_FILE = Path.home() / ".xenfra" / "tokens.json"
|
|
32
|
+
|
|
27
33
|
# CLI OAuth2 Configuration
|
|
28
34
|
CLI_CLIENT_ID = "xenfra-cli"
|
|
29
35
|
CLI_REDIRECT_PATH = "/auth/callback"
|
|
@@ -118,6 +124,62 @@ def _refresh_token_with_retry(refresh_token: str) -> dict:
|
|
|
118
124
|
return token_data
|
|
119
125
|
|
|
120
126
|
|
|
127
|
+
def _get_token_from_file(key: str) -> str | None:
|
|
128
|
+
"""Fallback: Get token from file storage (for Windows if keyring fails)."""
|
|
129
|
+
try:
|
|
130
|
+
if TOKEN_FILE.exists():
|
|
131
|
+
with open(TOKEN_FILE, "r") as f:
|
|
132
|
+
tokens = json.load(f)
|
|
133
|
+
return tokens.get(key)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _set_token_to_file(key: str, value: str):
|
|
140
|
+
"""Fallback: Save token to file storage (for Windows if keyring fails)."""
|
|
141
|
+
try:
|
|
142
|
+
TOKEN_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
|
|
144
|
+
# Load existing tokens
|
|
145
|
+
tokens = {}
|
|
146
|
+
if TOKEN_FILE.exists():
|
|
147
|
+
with open(TOKEN_FILE, "r") as f:
|
|
148
|
+
tokens = json.load(f)
|
|
149
|
+
|
|
150
|
+
# Update token
|
|
151
|
+
tokens[key] = value
|
|
152
|
+
|
|
153
|
+
# Save (with restrictive permissions on Unix-like systems)
|
|
154
|
+
with open(TOKEN_FILE, "w") as f:
|
|
155
|
+
json.dump(tokens, f)
|
|
156
|
+
|
|
157
|
+
# Set file permissions (owner read/write only)
|
|
158
|
+
if os.name != "nt": # Not Windows
|
|
159
|
+
TOKEN_FILE.chmod(0o600)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
console.print(f"[yellow]Warning: Could not save token to file: {e}[/yellow]")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _delete_token_from_file(key: str):
|
|
165
|
+
"""Fallback: Delete token from file storage."""
|
|
166
|
+
try:
|
|
167
|
+
if TOKEN_FILE.exists():
|
|
168
|
+
with open(TOKEN_FILE, "r") as f:
|
|
169
|
+
tokens = json.load(f)
|
|
170
|
+
|
|
171
|
+
if key in tokens:
|
|
172
|
+
del tokens[key]
|
|
173
|
+
|
|
174
|
+
if tokens:
|
|
175
|
+
with open(TOKEN_FILE, "w") as f:
|
|
176
|
+
json.dump(tokens, f)
|
|
177
|
+
else:
|
|
178
|
+
TOKEN_FILE.unlink() # Delete file if empty
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
|
|
121
183
|
def get_auth_token() -> str | None:
|
|
122
184
|
"""
|
|
123
185
|
Retrieve a valid access token, refreshing it if necessary.
|
|
@@ -125,12 +187,19 @@ def get_auth_token() -> str | None:
|
|
|
125
187
|
Returns:
|
|
126
188
|
Valid access token or None if not authenticated
|
|
127
189
|
"""
|
|
190
|
+
# Try keyring first
|
|
191
|
+
access_token = None
|
|
192
|
+
refresh_token = None
|
|
193
|
+
use_file_fallback = False
|
|
194
|
+
|
|
128
195
|
try:
|
|
129
196
|
access_token = keyring.get_password(SERVICE_ID, "access_token")
|
|
130
197
|
refresh_token = keyring.get_password(SERVICE_ID, "refresh_token")
|
|
131
198
|
except keyring.errors.KeyringError as e:
|
|
132
|
-
console.print(f"[
|
|
133
|
-
|
|
199
|
+
console.print(f"[dim]Keyring unavailable, using file storage: {e}[/dim]")
|
|
200
|
+
use_file_fallback = True
|
|
201
|
+
access_token = _get_token_from_file("access_token")
|
|
202
|
+
refresh_token = _get_token_from_file("refresh_token")
|
|
134
203
|
|
|
135
204
|
if not access_token:
|
|
136
205
|
return None
|
|
@@ -173,18 +242,24 @@ def get_auth_token() -> str | None:
|
|
|
173
242
|
new_refresh_token = token_data.get("refresh_token")
|
|
174
243
|
|
|
175
244
|
if new_access_token:
|
|
176
|
-
|
|
177
|
-
|
|
245
|
+
# Save tokens (keyring or file fallback)
|
|
246
|
+
if use_file_fallback:
|
|
247
|
+
_set_token_to_file("access_token", new_access_token)
|
|
178
248
|
if new_refresh_token:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
249
|
+
_set_token_to_file("refresh_token", new_refresh_token)
|
|
250
|
+
else:
|
|
251
|
+
try:
|
|
252
|
+
keyring.set_password(SERVICE_ID, "access_token", new_access_token)
|
|
253
|
+
if new_refresh_token:
|
|
254
|
+
keyring.set_password(SERVICE_ID, "refresh_token", new_refresh_token)
|
|
255
|
+
except keyring.errors.KeyringError as e:
|
|
256
|
+
console.print(f"[dim]Keyring failed, falling back to file storage: {e}[/dim]")
|
|
257
|
+
_set_token_to_file("access_token", new_access_token)
|
|
258
|
+
if new_refresh_token:
|
|
259
|
+
_set_token_to_file("refresh_token", new_refresh_token)
|
|
260
|
+
|
|
261
|
+
console.print("[bold green]Token refreshed successfully.[/bold green]")
|
|
262
|
+
return new_access_token
|
|
188
263
|
else:
|
|
189
264
|
console.print("[bold red]Failed to get new access token.[/bold red]")
|
|
190
265
|
return None
|
|
@@ -214,11 +289,7 @@ def get_auth_token() -> str | None:
|
|
|
214
289
|
)
|
|
215
290
|
|
|
216
291
|
# Clear tokens on refresh failure
|
|
217
|
-
|
|
218
|
-
keyring.delete_password(SERVICE_ID, "access_token")
|
|
219
|
-
keyring.delete_password(SERVICE_ID, "refresh_token")
|
|
220
|
-
except keyring.errors.KeyringError:
|
|
221
|
-
pass # Ignore errors when clearing
|
|
292
|
+
clear_tokens()
|
|
222
293
|
return None
|
|
223
294
|
except ValueError as e:
|
|
224
295
|
console.print(f"[bold red]Token refresh failed: {e}[/bold red]")
|
|
@@ -233,11 +304,16 @@ def get_auth_token() -> str | None:
|
|
|
233
304
|
|
|
234
305
|
|
|
235
306
|
def clear_tokens():
|
|
236
|
-
"""Clear stored access and refresh tokens."""
|
|
307
|
+
"""Clear stored access and refresh tokens (from both keyring and file)."""
|
|
308
|
+
# Clear from keyring
|
|
237
309
|
try:
|
|
238
310
|
keyring.delete_password(SERVICE_ID, "access_token")
|
|
239
311
|
keyring.delete_password(SERVICE_ID, "refresh_token")
|
|
240
312
|
except keyring.errors.PasswordDeleteError:
|
|
241
313
|
pass # Tokens already cleared
|
|
242
|
-
except keyring.errors.KeyringError
|
|
243
|
-
|
|
314
|
+
except keyring.errors.KeyringError:
|
|
315
|
+
pass # Keyring not available, that's OK
|
|
316
|
+
|
|
317
|
+
# Clear from file storage (fallback)
|
|
318
|
+
_delete_token_from_file("access_token")
|
|
319
|
+
_delete_token_from_file("refresh_token")
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
xenfra/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
xenfra/commands/__init__.py,sha256=kTTwVnTvoxikyPUhQiyTAbnw4PYafktuE1----TqQoA,43
|
|
3
|
-
xenfra/commands/auth.py,sha256=
|
|
4
|
-
xenfra/commands/auth_device.py,sha256=
|
|
3
|
+
xenfra/commands/auth.py,sha256=V6azS3ohhuEufHS1j34W2If08cCfYUUeDYApGSfGedg,4118
|
|
4
|
+
xenfra/commands/auth_device.py,sha256=caD2UdveEZtAFjgjmnA-l5bjbbPONFjXJXgeJN7mhbk,6710
|
|
5
5
|
xenfra/commands/deployments.py,sha256=bI6d9lVYPhCDoQE3nke7s80MUQ75ILPqiEGhcbXDExo,28609
|
|
6
6
|
xenfra/commands/intelligence.py,sha256=1lw1Pw6deyC52JmpK8YAVFuydAC7cvg_2qiRnw8B8Yc,13884
|
|
7
7
|
xenfra/commands/projects.py,sha256=SAxF_pOr95K6uz35U-zENptKndKxJNZn6bcD45PHcpI,6696
|
|
8
8
|
xenfra/commands/security_cmd.py,sha256=EI5sjX2lcMxgMH-LCFmPVkc9YqadOrcoSgTiKknkVRY,7327
|
|
9
9
|
xenfra/main.py,sha256=2EPPuIdxjhW-I-e-Mc0i2ayeLaSJdmzddNThkXq7B7c,2033
|
|
10
10
|
xenfra/utils/__init__.py,sha256=4ZRYkrb--vzoXjBHG8zRxz2jCXNGtAoKNtkyu2WRI2A,45
|
|
11
|
-
xenfra/utils/auth.py,sha256=
|
|
11
|
+
xenfra/utils/auth.py,sha256=4sjcuFwiqnSTBBiE9JAX36qLpZsrn6H-v_633Pj8ia8,11534
|
|
12
12
|
xenfra/utils/codebase.py,sha256=57GthXOvOQnUHiDwIHqxK6hNUGWlWf6Nfs3T8647Wrc,4144
|
|
13
13
|
xenfra/utils/config.py,sha256=F2zedd3JXP7TBdul0u8b4NVx-C1N6Hq4sH5szyWim6M,11947
|
|
14
14
|
xenfra/utils/security.py,sha256=EA8CIPLt8Y-QP5uZ7c5NuC6ZLRV1aZS8NapS9ix_vok,11479
|
|
15
15
|
xenfra/utils/validation.py,sha256=cvuL_AEFJ2oCoP0abCqoOIABOwz79Gkf-jh_dcFIQlM,6912
|
|
16
|
-
xenfra-0.3.
|
|
17
|
-
xenfra-0.3.
|
|
18
|
-
xenfra-0.3.
|
|
19
|
-
xenfra-0.3.
|
|
16
|
+
xenfra-0.3.2.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
|
|
17
|
+
xenfra-0.3.2.dist-info/entry_points.txt,sha256=a_2cGhYK__X6eW05Ba8uB6RIM_61c2sHtXsPY8N0mic,45
|
|
18
|
+
xenfra-0.3.2.dist-info/METADATA,sha256=5u2CxbFjkXvlMqnf06WIN5cr-7iBBDDf-3aCu6zjVcU,3834
|
|
19
|
+
xenfra-0.3.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|