tsk-cli 0.1.0__tar.gz → 0.1.5__tar.gz
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.
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/PKG-INFO +1 -1
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/pyproject.toml +1 -1
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/api.py +52 -10
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli.egg-info/PKG-INFO +1 -1
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/README.md +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/setup.cfg +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/__init__.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/auth.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/config.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/list_cmd.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/main.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/pull.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/push.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/resolve.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/set_status.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli/show.py +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli.egg-info/SOURCES.txt +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli.egg-info/dependency_links.txt +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli.egg-info/entry_points.txt +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli.egg-info/requires.txt +0 -0
- {tsk_cli-0.1.0 → tsk_cli-0.1.5}/tsk_cli.egg-info/top_level.txt +0 -0
|
@@ -5,11 +5,12 @@ HTTP client that uses stored session cookies to talk to the TSK backend.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import sys
|
|
8
|
+
from urllib.parse import urlparse
|
|
8
9
|
|
|
9
10
|
import click
|
|
10
11
|
import requests
|
|
11
12
|
|
|
12
|
-
from .config import get_auth, get_server_url
|
|
13
|
+
from .config import get_auth, get_server_url, save_auth
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def create_session() -> requests.Session:
|
|
@@ -23,9 +24,31 @@ def create_session() -> requests.Session:
|
|
|
23
24
|
if csrftoken:
|
|
24
25
|
s.cookies.set("csrftoken", csrftoken)
|
|
25
26
|
s.headers["X-CSRFToken"] = csrftoken
|
|
27
|
+
|
|
28
|
+
# Django's CSRF middleware requires Origin (or Referer on HTTPS) for unsafe
|
|
29
|
+
# methods. Browsers send these automatically; urllib3/requests does not.
|
|
30
|
+
base = get_server_url().rstrip("/")
|
|
31
|
+
parsed = urlparse(base)
|
|
32
|
+
if parsed.scheme in ("http", "https") and parsed.netloc:
|
|
33
|
+
origin = f"{parsed.scheme}://{parsed.netloc}"
|
|
34
|
+
s.headers.setdefault("Origin", origin)
|
|
35
|
+
s.headers.setdefault("Referer", f"{origin}/")
|
|
36
|
+
|
|
26
37
|
return s
|
|
27
38
|
|
|
28
39
|
|
|
40
|
+
def _persist_csrf_from_response(resp: requests.Response) -> None:
|
|
41
|
+
"""If the server rotated csrftoken, keep ~/.config/tsk/auth.json in sync."""
|
|
42
|
+
new = resp.cookies.get("csrftoken")
|
|
43
|
+
if not new:
|
|
44
|
+
return
|
|
45
|
+
auth = get_auth()
|
|
46
|
+
sid = auth.get("sessionid", "")
|
|
47
|
+
if not sid:
|
|
48
|
+
return
|
|
49
|
+
save_auth(sessionid=sid, csrftoken=new)
|
|
50
|
+
|
|
51
|
+
|
|
29
52
|
def require_auth() -> tuple[requests.Session, str]:
|
|
30
53
|
"""
|
|
31
54
|
Return (session, server_url) or exit with an error message
|
|
@@ -42,14 +65,35 @@ def require_auth() -> tuple[requests.Session, str]:
|
|
|
42
65
|
return session, server_url
|
|
43
66
|
|
|
44
67
|
|
|
68
|
+
def _check_auth_response(resp: requests.Response) -> None:
|
|
69
|
+
"""Exit with a helpful message on 401/403."""
|
|
70
|
+
if resp.status_code == 401:
|
|
71
|
+
click.echo("Session expired. Run: tsk login", err=True)
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
if resp.status_code == 403:
|
|
74
|
+
detail = ""
|
|
75
|
+
try:
|
|
76
|
+
detail = resp.json().get("detail", "")
|
|
77
|
+
except Exception:
|
|
78
|
+
pass
|
|
79
|
+
if "CSRF" in detail:
|
|
80
|
+
click.echo("CSRF check failed. Run: tsk login", err=True)
|
|
81
|
+
else:
|
|
82
|
+
click.echo(
|
|
83
|
+
f"Permission denied ({detail or 'no details'}). "
|
|
84
|
+
"You may need a higher org role, or run: tsk login",
|
|
85
|
+
err=True,
|
|
86
|
+
)
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
|
|
45
90
|
def api_get(path: str) -> dict | list:
|
|
46
91
|
"""GET an API endpoint. Exits on auth failure."""
|
|
47
92
|
session, server_url = require_auth()
|
|
48
93
|
resp = session.get(f"{server_url}{path}")
|
|
49
|
-
|
|
50
|
-
click.echo("Session expired. Run: tsk login", err=True)
|
|
51
|
-
sys.exit(1)
|
|
94
|
+
_check_auth_response(resp)
|
|
52
95
|
resp.raise_for_status()
|
|
96
|
+
_persist_csrf_from_response(resp)
|
|
53
97
|
return resp.json()
|
|
54
98
|
|
|
55
99
|
|
|
@@ -57,10 +101,9 @@ def api_post(path: str, json_data: dict) -> dict:
|
|
|
57
101
|
"""POST to an API endpoint. Exits on auth failure."""
|
|
58
102
|
session, server_url = require_auth()
|
|
59
103
|
resp = session.post(f"{server_url}{path}", json=json_data)
|
|
60
|
-
|
|
61
|
-
click.echo("Session expired. Run: tsk login", err=True)
|
|
62
|
-
sys.exit(1)
|
|
104
|
+
_check_auth_response(resp)
|
|
63
105
|
resp.raise_for_status()
|
|
106
|
+
_persist_csrf_from_response(resp)
|
|
64
107
|
return resp.json()
|
|
65
108
|
|
|
66
109
|
|
|
@@ -68,8 +111,7 @@ def api_patch(path: str, json_data: dict) -> dict:
|
|
|
68
111
|
"""PATCH an API endpoint. Exits on auth failure."""
|
|
69
112
|
session, server_url = require_auth()
|
|
70
113
|
resp = session.patch(f"{server_url}{path}", json=json_data)
|
|
71
|
-
|
|
72
|
-
click.echo("Session expired. Run: tsk login", err=True)
|
|
73
|
-
sys.exit(1)
|
|
114
|
+
_check_auth_response(resp)
|
|
74
115
|
resp.raise_for_status()
|
|
116
|
+
_persist_csrf_from_response(resp)
|
|
75
117
|
return resp.json()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|