keyrunes-python-sdk 0.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.
- keyrunes_python_sdk-0.0.1.dist-info/LICENSE +661 -0
- keyrunes_python_sdk-0.0.1.dist-info/METADATA +880 -0
- keyrunes_python_sdk-0.0.1.dist-info/RECORD +10 -0
- keyrunes_python_sdk-0.0.1.dist-info/WHEEL +4 -0
- keyrunes_sdk/__init__.py +52 -0
- keyrunes_sdk/client.py +565 -0
- keyrunes_sdk/config.py +203 -0
- keyrunes_sdk/decorators.py +248 -0
- keyrunes_sdk/exceptions.py +43 -0
- keyrunes_sdk/models.py +100 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
keyrunes_sdk/__init__.py,sha256=YS_9hZGg-EgFJagZHkOox5d4SoLRCEr8AaxUaNdWyis,1085
|
|
2
|
+
keyrunes_sdk/client.py,sha256=eNIj7iS1WuePzXsZUq06f7PDxrsDgG8nY_CGKH5AVzk,17922
|
|
3
|
+
keyrunes_sdk/config.py,sha256=KkkFKkBOx7wlK393f7o90yTOQQlG81qYaapCsia8sFs,5577
|
|
4
|
+
keyrunes_sdk/decorators.py,sha256=b_AYuGbcGYa_RXtN6O32-HY6jiyOIw-xjgvHg01ZEWY,8197
|
|
5
|
+
keyrunes_sdk/exceptions.py,sha256=LU9I1jcMoIQtupBeoSCGwozaYgPtSW9LEF5AVR0qdFI,721
|
|
6
|
+
keyrunes_sdk/models.py,sha256=v5-JbEuPmgoSRK9i3wY9YrXcTeMfQ2n-bJCnmrXVP6Y,3211
|
|
7
|
+
keyrunes_python_sdk-0.0.1.dist-info/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
|
|
8
|
+
keyrunes_python_sdk-0.0.1.dist-info/METADATA,sha256=dd4O6IEkxR6vv2G4Sxb8lpppLugkgo_OGSTayBUeCO4,23272
|
|
9
|
+
keyrunes_python_sdk-0.0.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
10
|
+
keyrunes_python_sdk-0.0.1.dist-info/RECORD,,
|
keyrunes_sdk/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Keyrunes SDK - Python client for Keyrunes Authorization System."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from keyrunes_sdk.client import KeyrunesClient
|
|
6
|
+
from keyrunes_sdk.config import (
|
|
7
|
+
clear_global_client,
|
|
8
|
+
configure,
|
|
9
|
+
get_config,
|
|
10
|
+
get_global_client,
|
|
11
|
+
)
|
|
12
|
+
from keyrunes_sdk.decorators import require_admin, require_group
|
|
13
|
+
from keyrunes_sdk.exceptions import (
|
|
14
|
+
AuthenticationError,
|
|
15
|
+
AuthorizationError,
|
|
16
|
+
GroupNotFoundError,
|
|
17
|
+
KeyrunesError,
|
|
18
|
+
NetworkError,
|
|
19
|
+
UserNotFoundError,
|
|
20
|
+
)
|
|
21
|
+
from keyrunes_sdk.models import (
|
|
22
|
+
AdminRegistration,
|
|
23
|
+
Group,
|
|
24
|
+
GroupCheck,
|
|
25
|
+
LoginCredentials,
|
|
26
|
+
Token,
|
|
27
|
+
User,
|
|
28
|
+
UserRegistration,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"KeyrunesClient",
|
|
33
|
+
"configure",
|
|
34
|
+
"get_global_client",
|
|
35
|
+
"clear_global_client",
|
|
36
|
+
"get_config",
|
|
37
|
+
"require_group",
|
|
38
|
+
"require_admin",
|
|
39
|
+
"KeyrunesError",
|
|
40
|
+
"AuthenticationError",
|
|
41
|
+
"AuthorizationError",
|
|
42
|
+
"GroupNotFoundError",
|
|
43
|
+
"UserNotFoundError",
|
|
44
|
+
"NetworkError",
|
|
45
|
+
"User",
|
|
46
|
+
"Group",
|
|
47
|
+
"Token",
|
|
48
|
+
"UserRegistration",
|
|
49
|
+
"AdminRegistration",
|
|
50
|
+
"LoginCredentials",
|
|
51
|
+
"GroupCheck",
|
|
52
|
+
]
|
keyrunes_sdk/client.py
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"""Keyrunes API Client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from urllib.parse import urljoin
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import jwt
|
|
8
|
+
|
|
9
|
+
from keyrunes_sdk.exceptions import (
|
|
10
|
+
AuthenticationError,
|
|
11
|
+
AuthorizationError,
|
|
12
|
+
GroupNotFoundError,
|
|
13
|
+
NetworkError,
|
|
14
|
+
UserNotFoundError,
|
|
15
|
+
)
|
|
16
|
+
from keyrunes_sdk.models import (
|
|
17
|
+
AdminRegistration,
|
|
18
|
+
GroupCheck,
|
|
19
|
+
LoginCredentials,
|
|
20
|
+
Token,
|
|
21
|
+
User,
|
|
22
|
+
UserRegistration,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class KeyrunesClient:
|
|
27
|
+
"""
|
|
28
|
+
Client for interacting with Keyrunes Authorization System.
|
|
29
|
+
|
|
30
|
+
This client provides methods for:
|
|
31
|
+
- User authentication (login)
|
|
32
|
+
- User registration
|
|
33
|
+
- Admin registration
|
|
34
|
+
- Group membership verification
|
|
35
|
+
- Authorization checks
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
base_url: Base URL of the Keyrunes API
|
|
39
|
+
(e.g., "https://keyrunes.example.com")
|
|
40
|
+
api_key: Optional API key for authentication
|
|
41
|
+
timeout: Request timeout in seconds (default: 30)
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
45
|
+
>>> token = client.login("user@example.com", "password123")
|
|
46
|
+
>>> print(token.access_token)
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
base_url: str,
|
|
52
|
+
api_key: Optional[str] = None,
|
|
53
|
+
timeout: int = 30,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Initialize Keyrunes client."""
|
|
56
|
+
self.base_url = base_url.rstrip("/")
|
|
57
|
+
self.api_key = api_key
|
|
58
|
+
self.timeout = timeout
|
|
59
|
+
self._token: Optional[str] = None
|
|
60
|
+
self._token_data: Optional[Dict[str, Any]] = None
|
|
61
|
+
self._client = httpx.Client(timeout=timeout)
|
|
62
|
+
|
|
63
|
+
if api_key:
|
|
64
|
+
self._client.headers.update({"X-API-Key": api_key})
|
|
65
|
+
|
|
66
|
+
def _make_request(
|
|
67
|
+
self,
|
|
68
|
+
method: str,
|
|
69
|
+
endpoint: str,
|
|
70
|
+
data: Optional[Dict[str, Any]] = None,
|
|
71
|
+
params: Optional[Dict[str, Any]] = None,
|
|
72
|
+
use_auth: bool = True,
|
|
73
|
+
) -> Dict[str, Any]:
|
|
74
|
+
"""
|
|
75
|
+
Make HTTP request to Keyrunes API.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
method: HTTP method (GET, POST, etc.)
|
|
79
|
+
endpoint: API endpoint
|
|
80
|
+
data: Request body data
|
|
81
|
+
params: Query parameters
|
|
82
|
+
use_auth: Whether to include authentication header
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Response JSON data
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
NetworkError: If request fails
|
|
89
|
+
AuthenticationError: If authentication fails (401)
|
|
90
|
+
AuthorizationError: If authorization fails (403)
|
|
91
|
+
"""
|
|
92
|
+
url = urljoin(self.base_url + "/", endpoint.lstrip("/"))
|
|
93
|
+
headers = {}
|
|
94
|
+
|
|
95
|
+
if use_auth and self._token:
|
|
96
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
response = self._client.request(
|
|
100
|
+
method=method,
|
|
101
|
+
url=url,
|
|
102
|
+
json=data,
|
|
103
|
+
params=params,
|
|
104
|
+
headers=headers,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if response.status_code == 401:
|
|
108
|
+
raise AuthenticationError(
|
|
109
|
+
"Authentication failed. Invalid credentials or token."
|
|
110
|
+
)
|
|
111
|
+
elif response.status_code == 403:
|
|
112
|
+
raise AuthorizationError(
|
|
113
|
+
"Authorization denied. Insufficient permissions."
|
|
114
|
+
)
|
|
115
|
+
elif response.status_code == 404:
|
|
116
|
+
raise UserNotFoundError("Resource not found.")
|
|
117
|
+
elif response.status_code >= 400:
|
|
118
|
+
try:
|
|
119
|
+
error_data = response.json()
|
|
120
|
+
error_msg = error_data.get("error", response.text)
|
|
121
|
+
except (ValueError, TypeError):
|
|
122
|
+
error_msg = (
|
|
123
|
+
response.text or f"HTTP {response.status_code} error"
|
|
124
|
+
)
|
|
125
|
+
raise NetworkError(f"Request failed: {error_msg}")
|
|
126
|
+
|
|
127
|
+
result: Dict[str, Any] = response.json()
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
except httpx.RequestError as e:
|
|
131
|
+
raise NetworkError(f"Network request failed: {str(e)}")
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _normalize_user(data: Dict[str, Any]) -> User:
|
|
135
|
+
"""Convert API user payload to SDK User model."""
|
|
136
|
+
if not data:
|
|
137
|
+
raise NetworkError("Empty user payload received from server.")
|
|
138
|
+
|
|
139
|
+
normalized: Dict[str, Any] = {}
|
|
140
|
+
normalized["id"] = str(
|
|
141
|
+
data.get("id") or data.get("user_id") or data.get("external_id")
|
|
142
|
+
)
|
|
143
|
+
normalized["username"] = data.get("username", "")
|
|
144
|
+
normalized["email"] = data.get("email", "")
|
|
145
|
+
normalized["groups"] = data.get("groups", []) or []
|
|
146
|
+
normalized["attributes"] = data.get("attributes", {})
|
|
147
|
+
normalized["is_active"] = data.get("is_active", True)
|
|
148
|
+
groups = normalized["groups"]
|
|
149
|
+
is_admin_flag = data.get("is_admin", False)
|
|
150
|
+
has_admin_group = "admins" in groups or "superadmin" in groups
|
|
151
|
+
has_admin_in_name = any("admin" in str(g).lower() for g in groups)
|
|
152
|
+
normalized["is_admin"] = (
|
|
153
|
+
is_admin_flag or has_admin_group or has_admin_in_name
|
|
154
|
+
)
|
|
155
|
+
return User(**normalized)
|
|
156
|
+
|
|
157
|
+
def _parse_token_response(self, payload: Dict[str, Any]) -> Token:
|
|
158
|
+
"""
|
|
159
|
+
Accept both legacy and current API token responses.
|
|
160
|
+
|
|
161
|
+
- New API: {"token": "...", "user": {...},
|
|
162
|
+
"requires_password_change": false}
|
|
163
|
+
- Legacy: {access_token, token_type, expires_in,
|
|
164
|
+
refresh_token, user}
|
|
165
|
+
"""
|
|
166
|
+
if "access_token" in payload:
|
|
167
|
+
user = payload.get("user")
|
|
168
|
+
if isinstance(user, dict):
|
|
169
|
+
payload["user"] = self._normalize_user(user)
|
|
170
|
+
return Token(**payload)
|
|
171
|
+
|
|
172
|
+
token_value = payload.get("token")
|
|
173
|
+
if not token_value:
|
|
174
|
+
raise AuthenticationError(
|
|
175
|
+
"No token returned by authentication endpoint."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
user_payload = payload.get("user")
|
|
179
|
+
user_model = (
|
|
180
|
+
self._normalize_user(user_payload)
|
|
181
|
+
if isinstance(user_payload, dict)
|
|
182
|
+
else None
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
return Token(
|
|
186
|
+
access_token=token_value,
|
|
187
|
+
token_type="bearer",
|
|
188
|
+
expires_in=payload.get("expires_in"),
|
|
189
|
+
refresh_token=payload.get("refresh_token"),
|
|
190
|
+
user=user_model,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def login(self, username: str, password: str) -> Token:
|
|
194
|
+
"""
|
|
195
|
+
Authenticate user and obtain access token.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
username: Username or email
|
|
199
|
+
password: User password
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Token object containing access token and user info
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
AuthenticationError: If login fails
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
209
|
+
>>> token = client.login("user@example.com", "password123")
|
|
210
|
+
>>> client.set_token(token.access_token)
|
|
211
|
+
"""
|
|
212
|
+
credentials = LoginCredentials(identity=username, password=password)
|
|
213
|
+
response = self._make_request(
|
|
214
|
+
"POST",
|
|
215
|
+
"/api/login",
|
|
216
|
+
data=credentials.model_dump(),
|
|
217
|
+
use_auth=False,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
token = self._parse_token_response(response)
|
|
221
|
+
self._token = token.access_token
|
|
222
|
+
try:
|
|
223
|
+
self._token_data = jwt.decode(
|
|
224
|
+
token.access_token, options={"verify_signature": False}
|
|
225
|
+
)
|
|
226
|
+
except Exception:
|
|
227
|
+
self._token_data = None
|
|
228
|
+
return token
|
|
229
|
+
|
|
230
|
+
def register_user(
|
|
231
|
+
self, username: str, email: str, password: str, **attributes: Any
|
|
232
|
+
) -> User:
|
|
233
|
+
"""
|
|
234
|
+
Register a new user.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
username: Username (3-50 characters)
|
|
238
|
+
email: User email address
|
|
239
|
+
password: Password (minimum 8 characters)
|
|
240
|
+
**attributes: Additional user attributes
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Created User object
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
AuthenticationError: If registration fails
|
|
247
|
+
|
|
248
|
+
Example:
|
|
249
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
250
|
+
>>> user = client.register_user(
|
|
251
|
+
... username="newuser",
|
|
252
|
+
... email="newuser@example.com",
|
|
253
|
+
... password="securepass123",
|
|
254
|
+
... department="Engineering"
|
|
255
|
+
... )
|
|
256
|
+
"""
|
|
257
|
+
registration = UserRegistration(
|
|
258
|
+
username=username,
|
|
259
|
+
email=email,
|
|
260
|
+
password=password,
|
|
261
|
+
attributes=attributes,
|
|
262
|
+
)
|
|
263
|
+
response = self._make_request(
|
|
264
|
+
"POST",
|
|
265
|
+
"/api/register",
|
|
266
|
+
data=registration.model_dump(),
|
|
267
|
+
use_auth=False,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
user_payload = (
|
|
271
|
+
response.get("user") if isinstance(response, dict) else None
|
|
272
|
+
)
|
|
273
|
+
if not user_payload:
|
|
274
|
+
raise NetworkError(
|
|
275
|
+
"Unexpected response format for user registration."
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return self._normalize_user(user_payload)
|
|
279
|
+
|
|
280
|
+
def register_admin(
|
|
281
|
+
self,
|
|
282
|
+
username: str,
|
|
283
|
+
email: str,
|
|
284
|
+
password: str,
|
|
285
|
+
admin_key: str,
|
|
286
|
+
**attributes: Any,
|
|
287
|
+
) -> User:
|
|
288
|
+
"""
|
|
289
|
+
Register a new admin user.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
username: Username (3-50 characters)
|
|
293
|
+
email: Admin email address
|
|
294
|
+
password: Password (minimum 8 characters)
|
|
295
|
+
admin_key: Admin registration key
|
|
296
|
+
**attributes: Additional user attributes
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
Created User object with admin privileges
|
|
300
|
+
|
|
301
|
+
Raises:
|
|
302
|
+
AuthenticationError: If registration fails
|
|
303
|
+
AuthorizationError: If admin key is invalid
|
|
304
|
+
|
|
305
|
+
Example:
|
|
306
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
307
|
+
>>> admin = client.register_admin(
|
|
308
|
+
... username="adminuser",
|
|
309
|
+
... email="admin@example.com",
|
|
310
|
+
... password="securepass123",
|
|
311
|
+
... admin_key="secret-admin-key"
|
|
312
|
+
... )
|
|
313
|
+
"""
|
|
314
|
+
registration = AdminRegistration(
|
|
315
|
+
username=username,
|
|
316
|
+
email=email,
|
|
317
|
+
password=password,
|
|
318
|
+
admin_key=admin_key,
|
|
319
|
+
attributes=attributes,
|
|
320
|
+
)
|
|
321
|
+
response = self._make_request(
|
|
322
|
+
"POST",
|
|
323
|
+
"/api/register",
|
|
324
|
+
data=registration.model_dump(),
|
|
325
|
+
use_auth=False,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
user_payload = (
|
|
329
|
+
response.get("user") if isinstance(response, dict) else None
|
|
330
|
+
)
|
|
331
|
+
if not user_payload:
|
|
332
|
+
raise NetworkError(
|
|
333
|
+
"Unexpected response format for admin registration."
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
return self._normalize_user(user_payload)
|
|
337
|
+
|
|
338
|
+
def has_group(self, user_id: str, group_id: str) -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Check if a user belongs to a specific group.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
user_id: User ID to check
|
|
344
|
+
group_id: Group ID to verify membership
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
True if user belongs to the group, False otherwise
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
AuthenticationError: If not authenticated
|
|
351
|
+
GroupNotFoundError: If group doesn't exist
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
355
|
+
>>> client.login("user@example.com", "password")
|
|
356
|
+
>>> has_access = client.has_group("user123", "admins")
|
|
357
|
+
>>> if has_access:
|
|
358
|
+
... print("User has admin access")
|
|
359
|
+
"""
|
|
360
|
+
if not self._token:
|
|
361
|
+
raise AuthenticationError("Not authenticated. Please login first.")
|
|
362
|
+
|
|
363
|
+
token_user_id = (
|
|
364
|
+
str(self._token_data.get("sub", "")) if self._token_data else None
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
if token_user_id and str(user_id) == token_user_id:
|
|
368
|
+
groups = (
|
|
369
|
+
self._token_data.get("groups", []) if self._token_data else []
|
|
370
|
+
)
|
|
371
|
+
return group_id in groups
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
response = self._make_request(
|
|
375
|
+
"GET",
|
|
376
|
+
f"/api/users/{user_id}/groups/{group_id}",
|
|
377
|
+
)
|
|
378
|
+
check = GroupCheck(**response)
|
|
379
|
+
return check.has_access
|
|
380
|
+
except UserNotFoundError:
|
|
381
|
+
if token_user_id and str(user_id) == token_user_id:
|
|
382
|
+
groups = (
|
|
383
|
+
self._token_data.get("groups", [])
|
|
384
|
+
if self._token_data
|
|
385
|
+
else []
|
|
386
|
+
)
|
|
387
|
+
return group_id in groups
|
|
388
|
+
raise GroupNotFoundError(
|
|
389
|
+
f"Group '{group_id}' not found or user not in group"
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def get_user(self, user_id: str) -> User:
|
|
393
|
+
"""
|
|
394
|
+
Get user information by ID.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
user_id: User ID
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
User object
|
|
401
|
+
|
|
402
|
+
Raises:
|
|
403
|
+
AuthenticationError: If not authenticated
|
|
404
|
+
UserNotFoundError: If user doesn't exist
|
|
405
|
+
|
|
406
|
+
Example:
|
|
407
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
408
|
+
>>> client.login("user@example.com", "password")
|
|
409
|
+
>>> user = client.get_user("user123")
|
|
410
|
+
>>> print(user.username)
|
|
411
|
+
"""
|
|
412
|
+
if not self._token:
|
|
413
|
+
raise AuthenticationError("Not authenticated. Please login first.")
|
|
414
|
+
|
|
415
|
+
token_user_id = (
|
|
416
|
+
str(self._token_data.get("sub", "")) if self._token_data else None
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
if token_user_id and str(user_id) == token_user_id and self._token_data:
|
|
420
|
+
token_data = self._token_data
|
|
421
|
+
user_data = {
|
|
422
|
+
"id": str(token_data.get("sub", "")),
|
|
423
|
+
"username": token_data.get("username", ""),
|
|
424
|
+
"email": token_data.get("email", ""),
|
|
425
|
+
"groups": token_data.get("groups", []),
|
|
426
|
+
}
|
|
427
|
+
return self._normalize_user(user_data)
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
response = self._make_request("GET", f"/api/users/{user_id}")
|
|
431
|
+
return self._normalize_user(response)
|
|
432
|
+
except UserNotFoundError:
|
|
433
|
+
if (
|
|
434
|
+
token_user_id
|
|
435
|
+
and str(user_id) == token_user_id
|
|
436
|
+
and self._token_data
|
|
437
|
+
):
|
|
438
|
+
token_data = self._token_data
|
|
439
|
+
user_data = {
|
|
440
|
+
"id": str(token_data.get("sub", "")),
|
|
441
|
+
"username": token_data.get("username", ""),
|
|
442
|
+
"email": token_data.get("email", ""),
|
|
443
|
+
"groups": token_data.get("groups", []),
|
|
444
|
+
}
|
|
445
|
+
return self._normalize_user(user_data)
|
|
446
|
+
raise
|
|
447
|
+
|
|
448
|
+
def get_current_user(self) -> User:
|
|
449
|
+
"""
|
|
450
|
+
Get currently authenticated user information.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
User object for authenticated user
|
|
454
|
+
|
|
455
|
+
Raises:
|
|
456
|
+
AuthenticationError: If not authenticated
|
|
457
|
+
|
|
458
|
+
Example:
|
|
459
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
460
|
+
>>> client.login("user@example.com", "password")
|
|
461
|
+
>>> me = client.get_current_user()
|
|
462
|
+
>>> print(f"Logged in as: {me.username}")
|
|
463
|
+
"""
|
|
464
|
+
if not self._token:
|
|
465
|
+
raise AuthenticationError("Not authenticated. Please login first.")
|
|
466
|
+
|
|
467
|
+
if self._token_data:
|
|
468
|
+
token_data = self._token_data
|
|
469
|
+
user_data = {
|
|
470
|
+
"id": str(token_data.get("sub", "")),
|
|
471
|
+
"username": token_data.get("username", ""),
|
|
472
|
+
"email": token_data.get("email", ""),
|
|
473
|
+
"groups": token_data.get("groups", []),
|
|
474
|
+
}
|
|
475
|
+
return self._normalize_user(user_data)
|
|
476
|
+
|
|
477
|
+
try:
|
|
478
|
+
response = self._make_request("GET", "/api/users/me")
|
|
479
|
+
return self._normalize_user(response)
|
|
480
|
+
except UserNotFoundError:
|
|
481
|
+
if self._token_data:
|
|
482
|
+
token_data = self._token_data
|
|
483
|
+
user_data = {
|
|
484
|
+
"id": str(token_data.get("sub", "")),
|
|
485
|
+
"username": token_data.get("username", ""),
|
|
486
|
+
"email": token_data.get("email", ""),
|
|
487
|
+
"groups": token_data.get("groups", []),
|
|
488
|
+
}
|
|
489
|
+
return self._normalize_user(user_data)
|
|
490
|
+
raise
|
|
491
|
+
|
|
492
|
+
def get_user_groups(self, user_id: Optional[str] = None) -> List[str]:
|
|
493
|
+
"""
|
|
494
|
+
Get list of groups for a user.
|
|
495
|
+
|
|
496
|
+
Args:
|
|
497
|
+
user_id: User ID (if None, uses current user)
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
List of group IDs
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
AuthenticationError: If not authenticated
|
|
504
|
+
|
|
505
|
+
Example:
|
|
506
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
507
|
+
>>> client.login("user@example.com", "password")
|
|
508
|
+
>>> groups = client.get_user_groups()
|
|
509
|
+
>>> print(f"User belongs to: {groups}")
|
|
510
|
+
"""
|
|
511
|
+
if user_id:
|
|
512
|
+
user = self.get_user(user_id)
|
|
513
|
+
else:
|
|
514
|
+
user = self.get_current_user()
|
|
515
|
+
|
|
516
|
+
return user.groups
|
|
517
|
+
|
|
518
|
+
def set_token(self, token: str) -> None:
|
|
519
|
+
"""
|
|
520
|
+
Set authentication token for subsequent requests.
|
|
521
|
+
|
|
522
|
+
Args:
|
|
523
|
+
token: JWT access token
|
|
524
|
+
|
|
525
|
+
Example:
|
|
526
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
527
|
+
>>> client.set_token("eyJhbGciOiJIUzI1NiIs...")
|
|
528
|
+
"""
|
|
529
|
+
self._token = token
|
|
530
|
+
try:
|
|
531
|
+
self._token_data = jwt.decode(
|
|
532
|
+
token, options={"verify_signature": False}
|
|
533
|
+
)
|
|
534
|
+
except Exception:
|
|
535
|
+
self._token_data = None
|
|
536
|
+
|
|
537
|
+
def clear_token(self) -> None:
|
|
538
|
+
"""
|
|
539
|
+
Clear authentication token.
|
|
540
|
+
|
|
541
|
+
Example:
|
|
542
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
543
|
+
>>> client.clear_token()
|
|
544
|
+
"""
|
|
545
|
+
self._token = None
|
|
546
|
+
self._token_data = None
|
|
547
|
+
|
|
548
|
+
def close(self) -> None:
|
|
549
|
+
"""
|
|
550
|
+
Close HTTP client.
|
|
551
|
+
|
|
552
|
+
Example:
|
|
553
|
+
>>> client = KeyrunesClient("https://keyrunes.example.com")
|
|
554
|
+
>>> # ... use client ...
|
|
555
|
+
>>> client.close()
|
|
556
|
+
"""
|
|
557
|
+
self._client.close()
|
|
558
|
+
|
|
559
|
+
def __enter__(self) -> "KeyrunesClient":
|
|
560
|
+
"""Context manager entry."""
|
|
561
|
+
return self
|
|
562
|
+
|
|
563
|
+
def __exit__(self, *args: Any) -> None:
|
|
564
|
+
"""Context manager exit."""
|
|
565
|
+
self.close()
|