sso-nebus 0.1.0__py3-none-any.whl → 0.1.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.
sso_nebus/__init__.py CHANGED
@@ -6,9 +6,9 @@ SSO Nebus Client - Python клиент для взаимодействия с MS
6
6
  - ServiceClient: для микросервисного взаимодействия (Client Credentials)
7
7
  """
8
8
 
9
- from user_client import UserClient
10
- from service_client import ServiceClient
11
- from exceptions import (
9
+ from .user_client import UserClient
10
+ from .service_client import ServiceClient
11
+ from .exceptions import (
12
12
  SSOClientError,
13
13
  AuthenticationError,
14
14
  AuthorizationError,
sso_nebus/base.py CHANGED
@@ -2,18 +2,20 @@
2
2
 
3
3
  from typing import Optional, Dict, Any
4
4
  from urllib.parse import urljoin
5
+ from abc import ABC, abstractmethod
5
6
 
6
7
  import aiohttp
7
8
  from aiohttp import ClientSession, ClientResponse
8
9
 
9
- from exceptions import (
10
+ from .exceptions import (
10
11
  APIError,
11
12
  AuthenticationError,
12
13
  AuthorizationError,
14
+ TokenError,
13
15
  )
14
16
 
15
17
 
16
- class BaseClient:
18
+ class BaseClient(ABC):
17
19
  """Базовый класс для всех SSO клиентов"""
18
20
 
19
21
  def __init__(
@@ -22,6 +24,7 @@ class BaseClient:
22
24
  api_version: str = "v1",
23
25
  timeout: int = 30,
24
26
  session: Optional[ClientSession] = None,
27
+ auto_refresh_token: bool = True,
25
28
  ):
26
29
  """
27
30
  Инициализация базового клиента
@@ -31,12 +34,15 @@ class BaseClient:
31
34
  api_version: Версия API (по умолчанию "v1")
32
35
  timeout: Таймаут запросов в секундах
33
36
  session: Опциональная aiohttp сессия (если не указана, создается новая)
37
+ auto_refresh_token: Автоматически обновлять токен при получении 401 ошибки
34
38
  """
35
39
  self.base_url = base_url.rstrip("/")
36
40
  self.api_version = api_version
37
41
  self.timeout = aiohttp.ClientTimeout(total=timeout)
38
42
  self._session = session
39
43
  self._own_session = session is None
44
+ self.auto_refresh_token = auto_refresh_token
45
+ self._refreshing = False # Флаг для предотвращения рекурсивных обновлений
40
46
 
41
47
  @property
42
48
  def api_base_url(self) -> str:
@@ -80,6 +86,21 @@ class BaseClient:
80
86
  headers["Authorization"] = f"Bearer {access_token}"
81
87
  return headers
82
88
 
89
+ @abstractmethod
90
+ async def _refresh_token(self) -> None:
91
+ """
92
+ Абстрактный метод для обновления токена.
93
+ Должен быть реализован в дочерних классах.
94
+ """
95
+ pass
96
+
97
+ def _get_access_token(self) -> Optional[str]:
98
+ """
99
+ Получить текущий access token.
100
+ Должен быть переопределен в дочерних классах.
101
+ """
102
+ return None
103
+
83
104
  async def _handle_response(self, response: ClientResponse) -> Dict[str, Any]:
84
105
  """
85
106
  Обработать HTTP ответ
@@ -125,9 +146,10 @@ class BaseClient:
125
146
  json_data: Optional[Dict[str, Any]] = None,
126
147
  form_data: Optional[Dict[str, Any]] = None,
127
148
  params: Optional[Dict[str, Any]] = None,
149
+ retry_on_401: bool = True,
128
150
  ) -> Dict[str, Any]:
129
151
  """
130
- Выполнить HTTP запрос
152
+ Выполнить HTTP запрос с автоматическим обновлением токена при 401
131
153
 
132
154
  Args:
133
155
  method: HTTP метод (GET, POST, etc.)
@@ -136,10 +158,15 @@ class BaseClient:
136
158
  json_data: JSON данные для тела запроса
137
159
  form_data: Form data для тела запроса
138
160
  params: Query параметры
161
+ retry_on_401: Повторить запрос после обновления токена при 401
139
162
 
140
163
  Returns:
141
164
  Распарсенный JSON ответ
142
165
  """
166
+ # Используем сохраненный токен, если не передан явно
167
+ if access_token is None:
168
+ access_token = self._get_access_token()
169
+
143
170
  url = self._build_url(endpoint)
144
171
  headers = self._get_headers(access_token)
145
172
 
@@ -155,6 +182,40 @@ class BaseClient:
155
182
  data=form_data,
156
183
  params=params,
157
184
  ) as response:
185
+ # Если получили 401 и включен авто-рефреш, пытаемся обновить токен
186
+ if (
187
+ response.status == 401
188
+ and self.auto_refresh_token
189
+ and retry_on_401
190
+ and not self._refreshing
191
+ and access_token
192
+ ):
193
+ try:
194
+ # Пытаемся обновить токен
195
+ self._refreshing = True
196
+ await self._refresh_token()
197
+ # Получаем новый токен
198
+ new_token = self._get_access_token()
199
+ if new_token and new_token != access_token:
200
+ # Повторяем запрос с новым токеном
201
+ headers = self._get_headers(new_token)
202
+ if form_data:
203
+ headers.pop("Content-Type", None)
204
+ async with self.session.request(
205
+ method=method,
206
+ url=url,
207
+ headers=headers,
208
+ json=json_data,
209
+ data=form_data,
210
+ params=params,
211
+ ) as retry_response:
212
+ return await self._handle_response(retry_response)
213
+ except Exception:
214
+ # Если обновление не удалось, пробрасываем оригинальную ошибку
215
+ pass
216
+ finally:
217
+ self._refreshing = False
218
+
158
219
  return await self._handle_response(response)
159
220
 
160
221
  async def get(
@@ -162,9 +223,16 @@ class BaseClient:
162
223
  endpoint: str,
163
224
  access_token: Optional[str] = None,
164
225
  params: Optional[Dict[str, Any]] = None,
226
+ use_auto_refresh: bool = True,
165
227
  ) -> Dict[str, Any]:
166
228
  """Выполнить GET запрос"""
167
- return await self._request("GET", endpoint, access_token=access_token, params=params)
229
+ return await self._request(
230
+ "GET",
231
+ endpoint,
232
+ access_token=access_token,
233
+ params=params,
234
+ retry_on_401=use_auto_refresh,
235
+ )
168
236
 
169
237
  async def post(
170
238
  self,
@@ -173,6 +241,7 @@ class BaseClient:
173
241
  json_data: Optional[Dict[str, Any]] = None,
174
242
  form_data: Optional[Dict[str, Any]] = None,
175
243
  params: Optional[Dict[str, Any]] = None,
244
+ use_auto_refresh: bool = True,
176
245
  ) -> Dict[str, Any]:
177
246
  """Выполнить POST запрос"""
178
247
  return await self._request(
@@ -182,6 +251,7 @@ class BaseClient:
182
251
  json_data=json_data,
183
252
  form_data=form_data,
184
253
  params=params,
254
+ retry_on_401=use_auto_refresh,
185
255
  )
186
256
 
187
257
  async def put(
@@ -190,15 +260,359 @@ class BaseClient:
190
260
  access_token: Optional[str] = None,
191
261
  json_data: Optional[Dict[str, Any]] = None,
192
262
  params: Optional[Dict[str, Any]] = None,
263
+ use_auto_refresh: bool = True,
193
264
  ) -> Dict[str, Any]:
194
265
  """Выполнить PUT запрос"""
195
- return await self._request("PUT", endpoint, access_token=access_token, json_data=json_data, params=params)
266
+ return await self._request(
267
+ "PUT",
268
+ endpoint,
269
+ access_token=access_token,
270
+ json_data=json_data,
271
+ params=params,
272
+ retry_on_401=use_auto_refresh,
273
+ )
196
274
 
197
275
  async def delete(
198
276
  self,
199
277
  endpoint: str,
200
278
  access_token: Optional[str] = None,
201
279
  params: Optional[Dict[str, Any]] = None,
280
+ use_auto_refresh: bool = True,
202
281
  ) -> Dict[str, Any]:
203
282
  """Выполнить DELETE запрос"""
204
- return await self._request("DELETE", endpoint, access_token=access_token, params=params)
283
+ return await self._request(
284
+ "DELETE",
285
+ endpoint,
286
+ access_token=access_token,
287
+ params=params,
288
+ retry_on_401=use_auto_refresh,
289
+ )
290
+
291
+ # ========== Админские методы ==========
292
+
293
+ # Пользователи
294
+ async def create_user(
295
+ self,
296
+ login: str,
297
+ email: str,
298
+ password: str,
299
+ name: str,
300
+ surname: str,
301
+ lastname: Optional[str] = None,
302
+ access_token: Optional[str] = None,
303
+ ) -> Dict[str, Any]:
304
+ """Создать нового пользователя (требует sso.admin.create)"""
305
+ json_data = {
306
+ "login": login,
307
+ "email": email,
308
+ "password": password,
309
+ "name": name,
310
+ "surname": surname,
311
+ }
312
+ if lastname:
313
+ json_data["lastname"] = lastname
314
+ return await self.post("admin/users", access_token=access_token, json_data=json_data)
315
+
316
+ async def get_users(
317
+ self,
318
+ skip: int = 0,
319
+ limit: int = 100,
320
+ access_token: Optional[str] = None,
321
+ ) -> Dict[str, Any]:
322
+ """Получить список пользователей (требует sso.admin.read)"""
323
+ return await self.get(
324
+ "admin/users",
325
+ access_token=access_token,
326
+ params={"skip": skip, "limit": limit},
327
+ )
328
+
329
+ async def get_user(self, user_id: int, access_token: Optional[str] = None) -> Dict[str, Any]:
330
+ """Получить пользователя по ID (требует sso.admin.read)"""
331
+ return await self.get(f"admin/users/{user_id}", access_token=access_token)
332
+
333
+ async def update_user(
334
+ self,
335
+ user_id: int,
336
+ access_token: Optional[str] = None,
337
+ **kwargs,
338
+ ) -> Dict[str, Any]:
339
+ """Обновить пользователя (требует sso.admin.edit)"""
340
+ return await self.put(f"admin/users/{user_id}", access_token=access_token, json_data=kwargs)
341
+
342
+ async def delete_user(self, user_id: int, access_token: Optional[str] = None) -> Dict[str, Any]:
343
+ """Удалить пользователя (требует sso.admin.delete)"""
344
+ return await self.delete(f"admin/users/{user_id}", access_token=access_token)
345
+
346
+ # Роли
347
+ async def create_role(
348
+ self,
349
+ name: str,
350
+ display_name: str,
351
+ description: Optional[str] = None,
352
+ client_id: Optional[str] = None,
353
+ access_token: Optional[str] = None,
354
+ ) -> Dict[str, Any]:
355
+ """Создать новую роль (требует sso.admin.create)"""
356
+ json_data = {"name": name, "display_name": display_name}
357
+ if description:
358
+ json_data["description"] = description
359
+ if client_id:
360
+ json_data["client_id"] = client_id
361
+ return await self.post("admin/roles", access_token=access_token, json_data=json_data)
362
+
363
+ async def get_roles(
364
+ self,
365
+ skip: int = 0,
366
+ limit: int = 100,
367
+ client_id: Optional[str] = None,
368
+ access_token: Optional[str] = None,
369
+ ) -> Dict[str, Any]:
370
+ """Получить список ролей (требует sso.admin.read)"""
371
+ params = {"skip": skip, "limit": limit}
372
+ if client_id:
373
+ params["client_id"] = client_id
374
+ return await self.get("admin/roles", access_token=access_token, params=params)
375
+
376
+ async def get_role(self, role_id: int, access_token: Optional[str] = None) -> Dict[str, Any]:
377
+ """Получить роль по ID (требует sso.admin.read)"""
378
+ return await self.get(f"admin/roles/{role_id}", access_token=access_token)
379
+
380
+ async def update_role(
381
+ self,
382
+ role_id: int,
383
+ access_token: Optional[str] = None,
384
+ **kwargs,
385
+ ) -> Dict[str, Any]:
386
+ """Обновить роль (требует sso.admin.edit)"""
387
+ return await self.put(f"admin/roles/{role_id}", access_token=access_token, json_data=kwargs)
388
+
389
+ async def delete_role(self, role_id: int, access_token: Optional[str] = None) -> Dict[str, Any]:
390
+ """Удалить роль (требует sso.admin.delete)"""
391
+ return await self.delete(f"admin/roles/{role_id}", access_token=access_token)
392
+
393
+ # Разрешения (Scopes)
394
+ async def create_scope(
395
+ self,
396
+ name: str,
397
+ service_name: str,
398
+ resource: str,
399
+ action: str,
400
+ description: Optional[str] = None,
401
+ access_token: Optional[str] = None,
402
+ ) -> Dict[str, Any]:
403
+ """Создать новое разрешение (требует sso.admin.create)"""
404
+ json_data = {
405
+ "name": name,
406
+ "service_name": service_name,
407
+ "resource": resource,
408
+ "action": action,
409
+ }
410
+ if description:
411
+ json_data["description"] = description
412
+ return await self.post("admin/scopes", access_token=access_token, json_data=json_data)
413
+
414
+ async def get_scopes(
415
+ self,
416
+ skip: int = 0,
417
+ limit: int = 100,
418
+ access_token: Optional[str] = None,
419
+ ) -> Dict[str, Any]:
420
+ """Получить список разрешений (требует sso.admin.read)"""
421
+ return await self.get(
422
+ "admin/scopes",
423
+ access_token=access_token,
424
+ params={"skip": skip, "limit": limit},
425
+ )
426
+
427
+ async def get_scope(self, scope_id: int, access_token: Optional[str] = None) -> Dict[str, Any]:
428
+ """Получить разрешение по ID (требует sso.admin.read)"""
429
+ return await self.get(f"admin/scopes/{scope_id}", access_token=access_token)
430
+
431
+ async def update_scope(
432
+ self,
433
+ scope_id: int,
434
+ access_token: Optional[str] = None,
435
+ **kwargs,
436
+ ) -> Dict[str, Any]:
437
+ """Обновить разрешение (требует sso.admin.edit)"""
438
+ return await self.put(f"admin/scopes/{scope_id}", access_token=access_token, json_data=kwargs)
439
+
440
+ async def delete_scope(self, scope_id: int, access_token: Optional[str] = None) -> Dict[str, Any]:
441
+ """Удалить разрешение (требует sso.admin.delete)"""
442
+ return await self.delete(f"admin/scopes/{scope_id}", access_token=access_token)
443
+
444
+ # Микросервисы (Clients)
445
+ async def create_client(
446
+ self,
447
+ service_name: str,
448
+ access_token: Optional[str] = None,
449
+ ) -> Dict[str, Any]:
450
+ """Создать новый микросервис (требует sso.admin.create)"""
451
+ return await self.post("admin/clients", access_token=access_token, json_data={"service_name": service_name})
452
+
453
+ async def get_clients(
454
+ self,
455
+ skip: int = 0,
456
+ limit: int = 100,
457
+ access_token: Optional[str] = None,
458
+ ) -> Dict[str, Any]:
459
+ """Получить список клиентов (требует sso.admin.read)"""
460
+ return await self.get(
461
+ "admin/clients",
462
+ access_token=access_token,
463
+ params={"skip": skip, "limit": limit},
464
+ )
465
+
466
+ async def get_client(self, client_id: str, access_token: Optional[str] = None) -> Dict[str, Any]:
467
+ """Получить клиента по ID (требует sso.admin.read)"""
468
+ return await self.get(f"admin/clients/{client_id}", access_token=access_token)
469
+
470
+ async def update_client(
471
+ self,
472
+ client_id: str,
473
+ access_token: Optional[str] = None,
474
+ **kwargs,
475
+ ) -> Dict[str, Any]:
476
+ """Обновить клиента (требует sso.admin.edit)"""
477
+ return await self.put(f"admin/clients/{client_id}", access_token=access_token, json_data=kwargs)
478
+
479
+ async def assign_scopes_to_client(
480
+ self,
481
+ client_id: str,
482
+ scope_ids: list[int],
483
+ access_token: Optional[str] = None,
484
+ ) -> Dict[str, Any]:
485
+ """Назначить разрешения клиенту (требует sso.admin.edit)"""
486
+ return await self.post(
487
+ f"admin/clients/{client_id}/scopes",
488
+ access_token=access_token,
489
+ json_data={"scope_ids": scope_ids},
490
+ )
491
+
492
+ async def rotate_client_secret(
493
+ self,
494
+ client_id: str,
495
+ access_token: Optional[str] = None,
496
+ ) -> Dict[str, Any]:
497
+ """Ротация client_secret (требует sso.admin.edit)"""
498
+ return await self.post(f"admin/clients/{client_id}/rotate-secret", access_token=access_token)
499
+
500
+ # Назначение ролей пользователям
501
+ async def assign_role_to_user(
502
+ self,
503
+ user_id: int,
504
+ role_id: int,
505
+ access_token: Optional[str] = None,
506
+ ) -> Dict[str, Any]:
507
+ """Назначить роль пользователю (требует sso.admin.create)"""
508
+ return await self.post(
509
+ f"admin/user-roles/{user_id}/roles",
510
+ access_token=access_token,
511
+ json_data={"role_id": role_id},
512
+ )
513
+
514
+ async def revoke_role_from_user(
515
+ self,
516
+ user_id: int,
517
+ role_id: int,
518
+ access_token: Optional[str] = None,
519
+ ) -> Dict[str, Any]:
520
+ """Отозвать роль у пользователя (требует sso.admin.delete)"""
521
+ return await self.delete(
522
+ f"admin/user-roles/{user_id}/roles/{role_id}",
523
+ access_token=access_token,
524
+ )
525
+
526
+ async def get_user_roles(
527
+ self,
528
+ user_id: int,
529
+ access_token: Optional[str] = None,
530
+ ) -> Dict[str, Any]:
531
+ """Получить роли пользователя (требует sso.admin.read)"""
532
+ return await self.get(f"admin/user-roles/{user_id}/roles", access_token=access_token)
533
+
534
+ async def get_user_scopes(
535
+ self,
536
+ user_id: int,
537
+ access_token: Optional[str] = None,
538
+ ) -> Dict[str, Any]:
539
+ """Получить разрешения пользователя (требует sso.admin.read)"""
540
+ return await self.get(f"admin/user-roles/{user_id}/scopes", access_token=access_token)
541
+
542
+ async def get_users_with_roles(
543
+ self,
544
+ skip: int = 0,
545
+ limit: int = 100,
546
+ access_token: Optional[str] = None,
547
+ ) -> Dict[str, Any]:
548
+ """Получить список пользователей с их ролями (требует sso.admin.read)"""
549
+ return await self.get(
550
+ "admin/user-roles",
551
+ access_token=access_token,
552
+ params={"skip": skip, "limit": limit},
553
+ )
554
+
555
+ # Логи
556
+ async def get_role_logs(
557
+ self,
558
+ skip: int = 0,
559
+ limit: int = 100,
560
+ user_id: Optional[int] = None,
561
+ search: Optional[str] = None,
562
+ access_token: Optional[str] = None,
563
+ ) -> Dict[str, Any]:
564
+ """Получить логи действий с ролями (требует sso.admin.read)"""
565
+ params = {"skip": skip, "limit": limit}
566
+ if user_id:
567
+ params["user_id"] = user_id
568
+ if search:
569
+ params["search"] = search
570
+ return await self.get("logs/role", access_token=access_token, params=params)
571
+
572
+ async def get_user_logs(
573
+ self,
574
+ skip: int = 0,
575
+ limit: int = 100,
576
+ user_id: Optional[int] = None,
577
+ search: Optional[str] = None,
578
+ access_token: Optional[str] = None,
579
+ ) -> Dict[str, Any]:
580
+ """Получить логи действий с пользователями (требует sso.admin.read)"""
581
+ params = {"skip": skip, "limit": limit}
582
+ if user_id:
583
+ params["user_id"] = user_id
584
+ if search:
585
+ params["search"] = search
586
+ return await self.get("logs/user", access_token=access_token, params=params)
587
+
588
+ async def get_auth_logs(
589
+ self,
590
+ skip: int = 0,
591
+ limit: int = 100,
592
+ user_id: Optional[int] = None,
593
+ search: Optional[str] = None,
594
+ access_token: Optional[str] = None,
595
+ ) -> Dict[str, Any]:
596
+ """Получить логи действий авторизации (требует sso.admin.read)"""
597
+ params = {"skip": skip, "limit": limit}
598
+ if user_id:
599
+ params["user_id"] = user_id
600
+ if search:
601
+ params["search"] = search
602
+ return await self.get("logs/auth", access_token=access_token, params=params)
603
+
604
+ async def get_service_logs(
605
+ self,
606
+ skip: int = 0,
607
+ limit: int = 100,
608
+ user_id: Optional[int] = None,
609
+ search: Optional[str] = None,
610
+ access_token: Optional[str] = None,
611
+ ) -> Dict[str, Any]:
612
+ """Получить логи действий сервисов (требует sso.admin.read)"""
613
+ params = {"skip": skip, "limit": limit}
614
+ if user_id:
615
+ params["user_id"] = user_id
616
+ if search:
617
+ params["search"] = search
618
+ return await self.get("logs/service", access_token=access_token, params=params)
@@ -0,0 +1,53 @@
1
+ """Пример использования UserClient"""
2
+
3
+ import asyncio
4
+
5
+ from user_client import UserClient
6
+
7
+
8
+ async def main():
9
+ # Создаем клиент
10
+ client = UserClient(
11
+ base_url="http://localhost:8000",
12
+ client_id="your_client_id"
13
+ )
14
+
15
+ try:
16
+ # Полный цикл авторизации
17
+ token_response = await client.full_auth_flow(
18
+ login="admin",
19
+ password="SecretPassword123!",
20
+ scope="sso.admin.read sso.admin.create",
21
+ )
22
+
23
+ print(f"Access token: {token_response.access_token[:50]}...")
24
+ print(
25
+ f"Refresh token: {token_response.refresh_token[:50] if token_response.refresh_token else None}...")
26
+ print(f"Expires in: {token_response.expires_in} seconds")
27
+
28
+ # Получаем информацию о пользователе
29
+ user_info = await client.get_current_user()
30
+ print(f"\nПользователь: {user_info.name} {user_info.surname}")
31
+ print(f"Email: {user_info.email}")
32
+ print(f"Scopes: {user_info.scopes}")
33
+
34
+ # Обновляем токен
35
+ new_token = await client.refresh_access_token()
36
+ print(
37
+ f"\nНовый access token получен: {new_token.access_token[:50]}...")
38
+
39
+ # Получаем список доступных сервисов
40
+ services = await client.get_available_services()
41
+ print(f"\nДоступно сервисов: {len(services.services)}")
42
+ for service in services.services:
43
+ print(f" - {service.name} ({service.client_id})")
44
+
45
+ except Exception as e:
46
+ print(f"Ошибка: {e}")
47
+
48
+ finally:
49
+ await client.close()
50
+
51
+
52
+ if __name__ == "__main__":
53
+ asyncio.run(main())
@@ -16,8 +16,8 @@ async def main():
16
16
  try:
17
17
  # Полный цикл авторизации
18
18
  token_response = await client.full_auth_flow(
19
- login="user@example.com",
20
- password="password123",
19
+ login="user",
20
+ password="SimplePassword123!",
21
21
  scope="sso.admin.read sso.admin.create",
22
22
  )
23
23
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  from typing import Optional
4
4
 
5
- from base import BaseClient
6
- from models import TokenResponse, UserInfo
7
- from exceptions import TokenError
5
+ from .base import BaseClient
6
+ from .models import TokenResponse, UserInfo
7
+ from .exceptions import TokenError
8
8
 
9
9
 
10
10
  class ServiceClient(BaseClient):
@@ -18,6 +18,8 @@ class ServiceClient(BaseClient):
18
18
  api_version: str = "v1",
19
19
  timeout: int = 30,
20
20
  session=None,
21
+ auto_refresh_token: bool = True,
22
+ default_scope: Optional[str] = None,
21
23
  ):
22
24
  """
23
25
  Инициализация клиента для микросервисов
@@ -29,11 +31,14 @@ class ServiceClient(BaseClient):
29
31
  api_version: Версия API
30
32
  timeout: Таймаут запросов
31
33
  session: Опциональная aiohttp сессия
34
+ auto_refresh_token: Автоматически обновлять токен при получении 401 ошибки
35
+ default_scope: Scope по умолчанию для автоматического получения токена
32
36
  """
33
- super().__init__(base_url, api_version, timeout, session)
37
+ super().__init__(base_url, api_version, timeout, session, auto_refresh_token)
34
38
  self.client_id = client_id
35
39
  self.client_secret = client_secret
36
40
  self._access_token: Optional[str] = None
41
+ self.default_scope = default_scope
37
42
 
38
43
  async def get_access_token(self, scope: Optional[str] = None) -> TokenResponse:
39
44
  """
@@ -90,6 +95,14 @@ class ServiceClient(BaseClient):
90
95
  """
91
96
  self._access_token = access_token
92
97
 
98
+ def _get_access_token(self) -> Optional[str]:
99
+ """Получить текущий access token (для BaseClient)"""
100
+ return self._access_token
101
+
102
+ async def _refresh_token(self) -> None:
103
+ """Обновить access token (для авто-рефреша)"""
104
+ await self.get_access_token(self.default_scope)
105
+
93
106
  def get_token(self) -> Optional[str]:
94
107
  """Получить текущий access token"""
95
108
  return self._access_token
sso_nebus/user_client.py CHANGED
@@ -5,8 +5,8 @@ import hashlib
5
5
  import base64
6
6
  import secrets
7
7
 
8
- from base import BaseClient
9
- from models import (
8
+ from .base import BaseClient
9
+ from .models import (
10
10
  PKCEParams,
11
11
  TokenResponse,
12
12
  UserInfo,
@@ -14,7 +14,7 @@ from models import (
14
14
  LoginResponse,
15
15
  ServicesList,
16
16
  )
17
- from exceptions import TokenError
17
+ from .exceptions import TokenError
18
18
 
19
19
 
20
20
  class UserClient(BaseClient):
@@ -28,6 +28,7 @@ class UserClient(BaseClient):
28
28
  api_version: str = "v1",
29
29
  timeout: int = 30,
30
30
  session=None,
31
+ auto_refresh_token: bool = True,
31
32
  ):
32
33
  """
33
34
  Инициализация клиента для пользователей
@@ -39,8 +40,9 @@ class UserClient(BaseClient):
39
40
  api_version: Версия API
40
41
  timeout: Таймаут запросов
41
42
  session: Опциональная aiohttp сессия
43
+ auto_refresh_token: Автоматически обновлять токен при получении 401 ошибки
42
44
  """
43
- super().__init__(base_url, api_version, timeout, session)
45
+ super().__init__(base_url, api_version, timeout, session, auto_refresh_token)
44
46
  self.client_id = client_id
45
47
  self.redirect_uri = redirect_uri
46
48
  self._pkce_params: Optional[PKCEParams] = None
@@ -70,7 +72,7 @@ class UserClient(BaseClient):
70
72
  Returns:
71
73
  PKCEParams с code_verifier, code_challenge и state
72
74
  """
73
- data = await self.get("auth/pkce-params")
75
+ data = await self.get("pkce-params")
74
76
  self._pkce_params = PKCEParams(**data)
75
77
  return self._pkce_params
76
78
 
@@ -116,7 +118,7 @@ class UserClient(BaseClient):
116
118
  if scope:
117
119
  params["scope"] = scope
118
120
 
119
- data = await self.get("auth/authorize", params=params)
121
+ data = await self.get("authorize", params=params)
120
122
  return AuthorizeResponse(**data)
121
123
 
122
124
  async def login(
@@ -142,7 +144,7 @@ class UserClient(BaseClient):
142
144
  "password": password,
143
145
  }
144
146
 
145
- data = await self.post("auth/login", json_data=json_data)
147
+ data = await self.post("login", json_data=json_data)
146
148
  return LoginResponse(**data)
147
149
 
148
150
  async def exchange_code_for_tokens(
@@ -180,7 +182,7 @@ class UserClient(BaseClient):
180
182
  if redirect_uri:
181
183
  form_data["redirect_uri"] = redirect_uri
182
184
 
183
- data = await self.post("auth/token", form_data=form_data)
185
+ data = await self.post("token", form_data=form_data)
184
186
  token_response = TokenResponse(**data)
185
187
 
186
188
  # Сохраняем токены
@@ -211,7 +213,7 @@ class UserClient(BaseClient):
211
213
  "client_id": self.client_id,
212
214
  }
213
215
 
214
- data = await self.post("auth/token", form_data=form_data)
216
+ data = await self.post("token", form_data=form_data)
215
217
  token_response = TokenResponse(**data)
216
218
 
217
219
  # Обновляем токены
@@ -236,7 +238,7 @@ class UserClient(BaseClient):
236
238
  raise TokenError(
237
239
  "Access token не найден. Выполните авторизацию сначала.")
238
240
 
239
- data = await self.get("auth/me", access_token=access_token)
241
+ data = await self.get("me", access_token=access_token)
240
242
  return UserInfo(**data)
241
243
 
242
244
  async def logout(self, refresh_token: Optional[str] = None) -> dict:
@@ -256,7 +258,7 @@ class UserClient(BaseClient):
256
258
 
257
259
  form_data = {"refresh_token": refresh_token}
258
260
 
259
- data = await self.post("auth/logout", form_data=form_data)
261
+ data = await self.post("logout", form_data=form_data)
260
262
 
261
263
  # Очищаем токены
262
264
  self._access_token = None
@@ -272,9 +274,19 @@ class UserClient(BaseClient):
272
274
  Returns:
273
275
  ServicesList со списком активных микросервисов
274
276
  """
275
- data = await self.get("auth/services")
277
+ data = await self.get("services")
276
278
  return ServicesList(**data)
277
279
 
280
+ def _get_access_token(self) -> Optional[str]:
281
+ """Получить текущий access token (для BaseClient)"""
282
+ return self._access_token
283
+
284
+ async def _refresh_token(self) -> None:
285
+ """Обновить access token используя refresh token (для авто-рефреша)"""
286
+ if not self._refresh_token:
287
+ raise TokenError("Refresh token не найден. Выполните авторизацию сначала.")
288
+ await self.refresh_access_token(self._refresh_token)
289
+
278
290
  def get_access_token(self) -> Optional[str]:
279
291
  """Получить текущий access token"""
280
292
  return self._access_token
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sso-nebus
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Python клиент для взаимодействия с MS Auth Service API
5
5
  License: LICENSE
6
6
  License-File: LICENSE
@@ -29,7 +29,7 @@ pip install -e .
29
29
  Или если пакет опубликован:
30
30
 
31
31
  ```bash
32
- pip install sso-nebus-client
32
+ pip install sso_nebus
33
33
  ```
34
34
 
35
35
  ## Быстрый старт
@@ -168,6 +168,54 @@ async def main():
168
168
  asyncio.run(main())
169
169
  ```
170
170
 
171
+ Пример для получения информации по пользвателю для подстановки в Depends
172
+ ```
173
+ from fastapi import FastAPI, Header, HTTPException
174
+ from typing import Optional
175
+
176
+ app = FastAPI()
177
+
178
+ sso_client = ServiceClient(
179
+ base_url="http://localhost:8000",
180
+ client_id="your_service_id",
181
+ client_secret="your_service_secret"
182
+ )
183
+
184
+ async def get_current_user(authorization: Optional[str] = Header(None)):
185
+ """
186
+ Dependency для получения текущего пользователя из токена
187
+ """
188
+ if not authorization:
189
+ raise HTTPException(status_code=401, detail="Токен не предоставлен")
190
+
191
+ # Извлекаем токен из заголовка "Bearer <token>"
192
+ try:
193
+ token = authorization.split(" ")[1]
194
+ except IndexError:
195
+ raise HTTPException(status_code=401, detail="Неверный формат токена")
196
+
197
+ try:
198
+ user_info = await sso_client.get_current_user(access_token=token)
199
+ return user_info
200
+ except AuthenticationError:
201
+ raise HTTPException(status_code=401, detail="Невалидный токен")
202
+ except Exception as e:
203
+ raise HTTPException(status_code=500, detail=f"Ошибка при проверке токена: {e}")
204
+
205
+ @app.get("/protected")
206
+ async def protected_endpoint(current_user = Depends(get_current_user)):
207
+ """
208
+ Защищенный endpoint, который требует валидный токен пользователя
209
+ """
210
+ return {
211
+ "message": f"Привет, {current_user.name} {current_user.surname}!",
212
+ "user_id": current_user.id,
213
+ "email": current_user.email,
214
+ "scopes": current_user.scopes
215
+ }
216
+
217
+ ```
218
+
171
219
  ## API Reference
172
220
 
173
221
  ### UserClient
@@ -0,0 +1,13 @@
1
+ sso_nebus/__init__.py,sha256=B7X9F074caljXhvE8Fpq1SwO-gVxzFUurOdbIkNLxp0,872
2
+ sso_nebus/base.py,sha256=4LQ7-qxxonGILktmX3uQCraolrVnWZ_kqRfKO8EOcmk,24213
3
+ sso_nebus/exceptions.py,sha256=XHrvfGGvs3eKMzkpwRxXyDngjpMSp7X7E8EUXwv26NA,1262
4
+ sso_nebus/exmples/example_admin.py,sha256=eAqkS4J5YyJneP4EPUrBlvNbGlK7rHEO39kCklt_WQM,1822
5
+ sso_nebus/exmples/example_service.py,sha256=19NNTmbaRSaWLpDQffGWNDmXkDJXdLOgD2WkV32xUu8,1778
6
+ sso_nebus/exmples/example_user.py,sha256=c_Uf36FDHIXvv_vdqsO5XvXkvu1YOUsqaTO44_8DqVM,1878
7
+ sso_nebus/models.py,sha256=0xIE5RvnxV2sAxO7_GTCes6HlHbdzGwHIjWVe1X7-ks,3024
8
+ sso_nebus/service_client.py,sha256=RUmnwVJBXfEqFfidCwHfwZo-2S_tyz_n9Ojuje7j4F4,5470
9
+ sso_nebus/user_client.py,sha256=-3pJmvZxJgyEo3moGQvZVAOhJMOIC6NuE8NgJ33Fyfs,12976
10
+ sso_nebus-0.1.2.dist-info/licenses/LICENSE,sha256=dCbOm3zpH8T7vLDC2K7QJLu-LEl2zqaSuyARbqfGsEY,1863
11
+ sso_nebus-0.1.2.dist-info/METADATA,sha256=hed0VrJyId6el7rcpUs4ecjqyh2-c3D372CXzJvaGk0,8406
12
+ sso_nebus-0.1.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
13
+ sso_nebus-0.1.2.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- sso_nebus/__init__.py,sha256=tmKUfBVcL4-AzsschcI2nFX9eWnnZfJ4SxlMSBvmF7o,869
2
- sso_nebus/base.py,sha256=DssaeMXU9kNyIiF4WllvLyLGwqSwKSXMd_rrjGoPHGc,7522
3
- sso_nebus/exceptions.py,sha256=XHrvfGGvs3eKMzkpwRxXyDngjpMSp7X7E8EUXwv26NA,1262
4
- sso_nebus/exmples/example_service.py,sha256=19NNTmbaRSaWLpDQffGWNDmXkDJXdLOgD2WkV32xUu8,1778
5
- sso_nebus/exmples/example_user.py,sha256=HXVV6FZTrnHwKoD_9OlXXlRJ9PsL9FL36bYSK7ztSNo,1883
6
- sso_nebus/models.py,sha256=0xIE5RvnxV2sAxO7_GTCes6HlHbdzGwHIjWVe1X7-ks,3024
7
- sso_nebus/service_client.py,sha256=gO6oyvu8x-r7B-T5lD65gybaQY-Ze1DYZbOXRkH-WTA,4704
8
- sso_nebus/user_client.py,sha256=X4jxWf_XeKDAlw_we552kElyIrdKf3WXgl7QTqroPrM,12269
9
- sso_nebus-0.1.0.dist-info/licenses/LICENSE,sha256=dCbOm3zpH8T7vLDC2K7QJLu-LEl2zqaSuyARbqfGsEY,1863
10
- sso_nebus-0.1.0.dist-info/METADATA,sha256=BocwVBRAyPSSXCh7vdHymr368CvEsBLy5HMN7v4Wa_I,6656
11
- sso_nebus-0.1.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
12
- sso_nebus-0.1.0.dist-info/RECORD,,