huace-aigc-auth-client 1.1.7__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.
@@ -0,0 +1,625 @@
1
+ """
2
+ AIGC Auth Legacy System Adapter
3
+
4
+ 提供旧系统接入支持,包括:
5
+ 1. 字段映射配置
6
+ 2. 用户数据同步
7
+ 3. 密码处理策略
8
+ 4. Webhook 推送支持
9
+ """
10
+
11
+ import os
12
+ import hmac
13
+ import hashlib
14
+ import json
15
+ import logging
16
+ import requests
17
+ from typing import Optional, List, Dict, Any, Callable, TypeVar, Generic
18
+ from dataclasses import dataclass, field
19
+ from enum import Enum
20
+ from abc import ABC, abstractmethod
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ class PasswordMode(Enum):
26
+ """密码处理模式"""
27
+ UNIFIED = "unified" # 统一初始密码
28
+ CUSTOM_MAPPING = "custom_mapping" # 自定义映射函数
29
+
30
+
31
+ class SyncDirection(Enum):
32
+ """同步方向"""
33
+ AUTH_TO_LEGACY = "auth_to_legacy" # aigc-auth → 旧系统
34
+ LEGACY_TO_AUTH = "legacy_to_auth" # 旧系统 → aigc-auth
35
+ BIDIRECTIONAL = "bidirectional" # 双向同步
36
+
37
+
38
+ @dataclass
39
+ class FieldMapping:
40
+ """字段映射配置"""
41
+ auth_field: str # aigc-auth 字段名
42
+ legacy_field: str # 旧系统字段名
43
+ transform_to_legacy: Optional[Callable[[Any], Any]] = None # auth → legacy 转换函数
44
+ transform_to_auth: Optional[Callable[[Any], Any]] = None # legacy → auth 转换函数
45
+ required: bool = False # 是否必填
46
+ default_value: Any = None # 默认值
47
+
48
+
49
+ @dataclass
50
+ class SyncConfig:
51
+ """同步配置"""
52
+ # 基本配置
53
+ enabled: bool = True
54
+ direction: SyncDirection = SyncDirection.AUTH_TO_LEGACY
55
+
56
+ # 字段映射
57
+ field_mappings: List[FieldMapping] = field(default_factory=list)
58
+
59
+ # 唯一标识字段(用于匹配用户)
60
+ unique_field: str = "username"
61
+
62
+ # 密码处理
63
+ password_mode: PasswordMode = PasswordMode.UNIFIED
64
+ unified_password: str = "Abc@123456" # 统一初始密码
65
+ password_mapper: Optional[Callable[[Dict], str]] = None # 自定义密码映射函数(接收用户数据字典)
66
+ password_is_hashed: bool = False # password_mapper 返回的是否已经是加密后的密码
67
+
68
+ # Webhook 配置
69
+ webhook_enabled: bool = False
70
+ webhook_url: Optional[str] = None
71
+ webhook_secret: Optional[str] = None
72
+ webhook_retry_count: int = 3
73
+ webhook_timeout: int = 10
74
+
75
+
76
+ @dataclass
77
+ class LegacyUserData:
78
+ """旧系统用户数据"""
79
+ data: Dict[str, Any]
80
+
81
+ def get(self, key: str, default: Any = None) -> Any:
82
+ return self.data.get(key, default)
83
+
84
+ def to_dict(self) -> Dict[str, Any]:
85
+ return self.data.copy()
86
+
87
+
88
+ @dataclass
89
+ class SyncResult:
90
+ """同步结果"""
91
+ success: bool
92
+ user_id: Optional[int] = None
93
+ auth_user_id: Optional[int] = None
94
+ legacy_user_id: Optional[Any] = None
95
+ message: str = ""
96
+ errors: List[str] = field(default_factory=list)
97
+
98
+
99
+ class LegacySystemAdapter(ABC):
100
+ """
101
+ 旧系统适配器抽象基类
102
+
103
+ 接入系统需要继承此类并实现以下方法:
104
+ - get_user_by_unique_field: 通过唯一字段获取用户
105
+ - create_user: 创建用户
106
+ - update_user: 更新用户(可选)
107
+ - get_all_users: 获取所有用户(用于初始化同步)
108
+ - handle_webhook: 处理 webhook 通知(可选,异步方法)
109
+ """
110
+
111
+ def __init__(self, sync_config: SyncConfig, auth_client=None):
112
+ """
113
+ 初始化适配器
114
+
115
+ Args:
116
+ sync_config: 同步配置
117
+ auth_client: AigcAuthClient 实例(可选,用于批量同步)
118
+ """
119
+ self.config = sync_config
120
+ self.auth_client = auth_client
121
+
122
+ @abstractmethod
123
+ def get_user_by_unique_field(self, value: Any) -> Optional[LegacyUserData]:
124
+ """通过唯一字段获取旧系统用户"""
125
+ pass
126
+
127
+ @abstractmethod
128
+ def create_user(self, user_data: Dict[str, Any]) -> Optional[Any]:
129
+ """在旧系统创建用户,返回用户ID"""
130
+ pass
131
+
132
+ def update_user(self, unique_value: Any, user_data: Dict[str, Any]) -> bool:
133
+ """更新旧系统用户(可选实现)"""
134
+ return False
135
+
136
+ def get_all_users(self) -> List[LegacyUserData]:
137
+ """获取所有旧系统用户(用于初始化同步)"""
138
+ return []
139
+
140
+ @abstractmethod
141
+ async def get_user_by_username_async(self, username: str) -> Optional[LegacyUserData]:
142
+ """异步获取用户(子类必须实现)
143
+
144
+ Args:
145
+ username: 用户名
146
+
147
+ Returns:
148
+ Optional[LegacyUserData]: 用户数据,不存在返回 None
149
+ """
150
+ raise NotImplementedError("Subclass must implement get_user_by_username_async method")
151
+
152
+ @abstractmethod
153
+ async def _create_user_async(self, user_data: Dict[str, Any]) -> Optional[Any]:
154
+ """异步创建用户(子类必须实现)
155
+
156
+ Args:
157
+ user_data: 用户数据字典
158
+
159
+ Returns:
160
+ Optional[Any]: 创建的用户 ID 或其他标识
161
+ """
162
+ raise NotImplementedError("Subclass must implement _create_user_async method")
163
+
164
+ @abstractmethod
165
+ async def _update_user_async(self, username: str, user_data: Dict[str, Any]) -> bool:
166
+ """异步更新用户(子类必须实现)
167
+
168
+ Args:
169
+ username: 用户名
170
+ user_data: 要更新的用户数据
171
+
172
+ Returns:
173
+ bool: 更新成功返回 True
174
+ """
175
+ raise NotImplementedError("Subclass must implement _update_user_async method")
176
+
177
+ @abstractmethod
178
+ async def _delete_user_async(self, username: str) -> bool:
179
+ """异步删除用户(子类必须实现)
180
+
181
+ Args:
182
+ username: 用户名
183
+
184
+ Returns:
185
+ bool: 删除成功返回 True
186
+ """
187
+ raise NotImplementedError("Subclass must implement _delete_user_async method")
188
+
189
+ @abstractmethod
190
+ async def get_all_users_async(self) -> List[LegacyUserData]:
191
+ """获取所有用户(子类必须实现,用于批量同步)
192
+
193
+ Returns:
194
+ List[LegacyUserData]: 所有用户数据列表
195
+ """
196
+ raise NotImplementedError("Subclass must implement get_all_users_async method")
197
+
198
+ async def upsert_user_async(self, user_data: Dict[str, Any], auth_data: Dict[str, Any] = None) -> Dict[str, Any]:
199
+ """异步创建或更新用户(存在则更新,不存在则新增)
200
+
201
+ 这是一个默认实现,子类可以选择性覆盖以优化性能。
202
+
203
+ Args:
204
+ user_data: 用户数据字典(必须包含 username)
205
+ auth_data: 鉴权系统用户数据字典(可选)
206
+ Returns:
207
+ Dict: 操作结果 {"created": bool, "user_id": Any}
208
+ """
209
+ username = user_data.get("username")
210
+ if not username:
211
+ raise ValueError("username is required for upsert operation")
212
+
213
+ # 检查用户是否存在
214
+ existing = await self.get_user_by_username_async(username)
215
+
216
+ if existing:
217
+ # 用户存在,执行更新
218
+ if not auth_data or auth_data.get("updatedFields") is None or len(auth_data.get("updatedFields")) == 0:
219
+ # 如果没有提供 auth_data 或 updatedFields,则不更新
220
+ return {"created": False, "user_id": existing.get("id")}
221
+ await self._update_user_async(username, user_data)
222
+ return {"created": False, "user_id": existing.get("id")}
223
+ else:
224
+ # 用户不存在,执行创建
225
+ user_id = await self._create_user_async(user_data)
226
+ return {"created": True, "user_id": user_id}
227
+
228
+ async def batch_sync_to_auth(self) -> Dict[str, Any]:
229
+ """批量同步旧系统用户到 aigc-auth(默认实现)
230
+
231
+ 子类可以选择性覆盖此方法以自定义同步逻辑。
232
+
233
+ Returns:
234
+ Dict: 同步结果统计
235
+ """
236
+ if not self.auth_client:
237
+ raise ValueError("auth_client is required for batch_sync_to_auth. Please provide it in constructor.")
238
+
239
+ users = await self.get_all_users_async()
240
+
241
+ results = {
242
+ "total": len(users),
243
+ "success": 0,
244
+ "failed": 0,
245
+ "skipped": 0,
246
+ "errors": []
247
+ }
248
+
249
+ for user in users:
250
+ try:
251
+ auth_data = self.transform_legacy_to_auth(user)
252
+
253
+ # 获取密码(支持新的元组返回格式)
254
+ password_result = self.get_password_for_sync(user)
255
+ if isinstance(password_result, tuple):
256
+ password, is_hashed = password_result
257
+ else:
258
+ password, is_hashed = password_result, False
259
+
260
+ # 根据是否已加密选择不同的字段
261
+ if is_hashed:
262
+ auth_data["password_hashed"] = password
263
+ else:
264
+ auth_data["password"] = password
265
+
266
+ result = self.auth_client.sync_user_to_auth(auth_data)
267
+
268
+ if result.get("success"):
269
+ if result.get("created"):
270
+ results["success"] += 1
271
+ else:
272
+ results["skipped"] += 1
273
+ else:
274
+ results["failed"] += 1
275
+ results["errors"].append({
276
+ "user": user.get("username"),
277
+ "error": result.get("message")
278
+ })
279
+ except Exception as e:
280
+ results["failed"] += 1
281
+ results["errors"].append({
282
+ "user": user.get("username"),
283
+ "error": str(e)
284
+ })
285
+
286
+ return results
287
+
288
+ async def handle_webhook(self, event: str, data: Dict[str, Any]) -> Dict[str, Any]:
289
+ """
290
+ 处理来自 aigc-auth 的 webhook 通知(默认实现)
291
+
292
+ 子类可以选择性覆盖此方法以自定义 webhook 处理逻辑。
293
+
294
+ Args:
295
+ event: 事件类型,如 "user.created", "user.updated", "user.deleted"
296
+ data: 事件数据(用户信息)
297
+
298
+ Returns:
299
+ Dict: 处理结果
300
+ """
301
+ if event == "user.created" or event == "user.updated" or event == "user.login":
302
+ # 转换数据格式
303
+ legacy_data = self.transform_auth_to_legacy(data)
304
+
305
+ # 获取密码
306
+ password_result = self.get_password_for_sync()
307
+ if isinstance(password_result, tuple):
308
+ password, is_hashed = password_result
309
+ else:
310
+ password, is_hashed = password_result, False
311
+ legacy_data["password"] = password
312
+
313
+ # 创建或更新用户
314
+ result = await self.upsert_user_async(legacy_data)
315
+
316
+ return {
317
+ "success": True,
318
+ "message": "User created" if result["created"] else "User updated",
319
+ "created": result["created"],
320
+ "user_id": result["user_id"]
321
+ }
322
+
323
+ elif event == "user.deleted":
324
+ # 禁用用户而不是删除
325
+ username = data.get("username")
326
+ if not username:
327
+ logger.error("username is required for user.deleted event")
328
+ return {"success": False, "message": "username is required for user.deleted event"}
329
+ await self._delete_user_async(username)
330
+
331
+ return {"success": True, "message": "User disabled"}
332
+
333
+ elif event == "user.init_sync_auth":
334
+ # 初始化同步:批量同步旧系统用户到 aigc-auth
335
+ if not self.auth_client:
336
+ return {"success": False, "message": "auth_client is required for init_sync_auth event. Please provide it in constructor."}
337
+
338
+ results = await self.batch_sync_to_auth()
339
+
340
+ return {
341
+ "success": True,
342
+ "message": f"Batch sync completed: {results['success']} created, {results['skipped']} skipped, {results['failed']} failed",
343
+ "results": results
344
+ }
345
+
346
+ return {"success": True, "message": "Event ignored"}
347
+
348
+ def transform_auth_to_legacy(self, auth_user: Dict[str, Any]) -> Dict[str, Any]:
349
+ """将 aigc-auth 用户数据转换为旧系统格式"""
350
+ result = {}
351
+
352
+ for mapping in self.config.field_mappings:
353
+ auth_value = auth_user.get(mapping.auth_field)
354
+
355
+ if auth_value is None:
356
+ if mapping.required and mapping.default_value is None:
357
+ raise ValueError(f"Required field '{mapping.auth_field}' is missing")
358
+ auth_value = mapping.default_value
359
+
360
+ if mapping.transform_to_legacy:
361
+ auth_value = mapping.transform_to_legacy(auth_value)
362
+
363
+ if auth_value is not None:
364
+ result[mapping.legacy_field] = auth_value
365
+
366
+ return result
367
+
368
+ def transform_legacy_to_auth(self, legacy_user: LegacyUserData) -> Dict[str, Any]:
369
+ """将旧系统用户数据转换为 aigc-auth 格式"""
370
+ result = {}
371
+
372
+ for mapping in self.config.field_mappings:
373
+ legacy_value = legacy_user.get(mapping.legacy_field)
374
+
375
+ if legacy_value is None:
376
+ if mapping.required and mapping.default_value is None:
377
+ raise ValueError(f"Required field '{mapping.legacy_field}' is missing")
378
+ legacy_value = mapping.default_value
379
+
380
+ if mapping.transform_to_auth:
381
+ legacy_value = mapping.transform_to_auth(legacy_value)
382
+
383
+ if legacy_value is not None:
384
+ result[mapping.auth_field] = legacy_value
385
+
386
+ return result
387
+
388
+ def get_password_for_sync(self, legacy_user: Optional[LegacyUserData] = None) -> tuple:
389
+ """获取同步时使用的密码
390
+
391
+ Returns:
392
+ tuple: (password, is_hashed)
393
+ - password: 密码字符串
394
+ - is_hashed: 是否已经是加密后的密码
395
+ """
396
+ if self.config.password_mode == PasswordMode.UNIFIED:
397
+ return (self.config.unified_password, False)
398
+ elif self.config.password_mode == PasswordMode.CUSTOM_MAPPING:
399
+ if self.config.password_mapper and legacy_user:
400
+ password = self.config.password_mapper(legacy_user.data)
401
+ return (password, self.config.password_is_hashed)
402
+ return (self.config.unified_password, False)
403
+ return (self.config.unified_password, False)
404
+
405
+
406
+ class WebhookSender:
407
+ """Webhook 发送器"""
408
+
409
+ def __init__(self, config: SyncConfig):
410
+ self.config = config
411
+
412
+ def generate_signature(self, payload: str) -> str:
413
+ """生成 HMAC-SHA256 签名"""
414
+ if not self.config.webhook_secret:
415
+ return ""
416
+
417
+ return hmac.new(
418
+ self.config.webhook_secret.encode('utf-8'),
419
+ payload.encode('utf-8'),
420
+ hashlib.sha256
421
+ ).hexdigest()
422
+
423
+ def send(self, event_type: str, data: Dict[str, Any]) -> bool:
424
+ """发送 webhook 通知"""
425
+ if not self.config.webhook_enabled or not self.config.webhook_url:
426
+ return False
427
+
428
+ payload = json.dumps({
429
+ "event": event_type,
430
+ "data": data
431
+ }, ensure_ascii=False)
432
+
433
+ signature = self.generate_signature(payload)
434
+
435
+ headers = {
436
+ "Content-Type": "application/json",
437
+ "X-Webhook-Signature": signature,
438
+ "X-Webhook-Event": event_type
439
+ }
440
+
441
+ for attempt in range(self.config.webhook_retry_count):
442
+ try:
443
+ response = requests.post(
444
+ self.config.webhook_url,
445
+ data=payload,
446
+ headers=headers,
447
+ timeout=self.config.webhook_timeout
448
+ )
449
+
450
+ if response.status_code == 200:
451
+ logger.info(f"Webhook sent successfully: {event_type}")
452
+ return True
453
+ else:
454
+ logger.warning(f"Webhook failed with status {response.status_code}: {response.text}")
455
+
456
+ except requests.RequestException as e:
457
+ logger.error(f"Webhook request failed (attempt {attempt + 1}): {e}")
458
+
459
+ return False
460
+
461
+
462
+ class UserSyncService:
463
+ """
464
+ 用户同步服务
465
+
466
+ 提供以下功能:
467
+ 1. 登录时自动同步(auth → legacy)
468
+ 2. 初始化批量同步(legacy → auth)
469
+ 3. Webhook 增量推送
470
+ """
471
+
472
+ def __init__(
473
+ self,
474
+ auth_client, # AigcAuthClient
475
+ legacy_adapter: LegacySystemAdapter
476
+ ):
477
+ self.auth_client = auth_client
478
+ self.adapter = legacy_adapter
479
+ self.config = legacy_adapter.config
480
+ self.webhook_sender = WebhookSender(self.config)
481
+
482
+ # 确保 adapter 也持有 auth_client 引用
483
+ if not self.adapter.auth_client:
484
+ self.adapter.auth_client = auth_client
485
+
486
+ def _user_info_to_dict(self, user_info) -> Dict[str, Any]:
487
+ """将 UserInfo 对象转换为字典"""
488
+ return {
489
+ "id": user_info.id,
490
+ "username": user_info.username,
491
+ "nickname": user_info.nickname,
492
+ "email": user_info.email,
493
+ "phone": user_info.phone,
494
+ "avatar": user_info.avatar,
495
+ "roles": user_info.roles,
496
+ "permissions": user_info.permissions,
497
+ "department": user_info.department,
498
+ "company": user_info.company,
499
+ "is_admin": user_info.is_admin,
500
+ "status": user_info.status,
501
+ }
502
+
503
+ async def sync_on_login_async(self, auth_user_info) -> Dict[str, Any]:
504
+ """
505
+ 登录时异步同步用户到旧系统
506
+
507
+ 当用户通过 aigc-auth 登录成功后调用,
508
+ 如果旧系统没有该用户则自动创建。
509
+
510
+ Args:
511
+ auth_user_info: aigc-auth 返回的 UserInfo 对象
512
+
513
+ Returns:
514
+ Dict: 同步结果
515
+ """
516
+ if not self.config.enabled:
517
+ return {"success": True, "message": "Sync disabled"}
518
+
519
+ # 转换用户数据
520
+ auth_data = self._user_info_to_dict(auth_user_info)
521
+ legacy_data = self.adapter.transform_auth_to_legacy(auth_data)
522
+
523
+ # 获取密码(支持新的元组返回格式)
524
+ password_result = self.adapter.get_password_for_sync()
525
+ if isinstance(password_result, tuple):
526
+ password, is_hashed = password_result
527
+ else:
528
+ password, is_hashed = password_result, False
529
+ legacy_data["password"] = password
530
+
531
+ # 使用 upsert 方法(存在则更新,不存在则创建)
532
+ result = await self.adapter.upsert_user_async(legacy_data)
533
+
534
+ return {
535
+ "success": True,
536
+ "auth_user_id": auth_user_info.id,
537
+ "legacy_user_id": result["user_id"],
538
+ "created": result["created"],
539
+ "message": "User created" if result["created"] else "User updated"
540
+ }
541
+
542
+ async def batch_sync_to_auth_async(self) -> Dict[str, Any]:
543
+ """
544
+ 异步批量同步旧系统用户到 aigc-auth
545
+
546
+ 直接委托给 adapter 的默认实现
547
+
548
+ Returns:
549
+ Dict: 同步结果统计
550
+ """
551
+ return await self.adapter.batch_sync_to_auth()
552
+
553
+ # ============ 预设字段映射 ============
554
+
555
+ def create_default_field_mappings() -> List[FieldMapping]:
556
+ """创建默认字段映射配置(通用基础映射)"""
557
+ return [
558
+ FieldMapping(
559
+ auth_field="username",
560
+ legacy_field="username",
561
+ required=True
562
+ ),
563
+ FieldMapping(
564
+ auth_field="email",
565
+ legacy_field="email"
566
+ ),
567
+ FieldMapping(
568
+ auth_field="nickname",
569
+ legacy_field="nickname"
570
+ ),
571
+ FieldMapping(
572
+ auth_field="phone",
573
+ legacy_field="phone"
574
+ ),
575
+ FieldMapping(
576
+ auth_field="avatar",
577
+ legacy_field="avatar"
578
+ ),
579
+ FieldMapping(
580
+ auth_field="company",
581
+ legacy_field="company"
582
+ ),
583
+ FieldMapping(
584
+ auth_field="department",
585
+ legacy_field="department"
586
+ ),
587
+ ]
588
+
589
+
590
+ # ============ 便捷函数 ============
591
+
592
+ def create_sync_config(
593
+ field_mappings: List[FieldMapping] = None,
594
+ password_mode: PasswordMode = PasswordMode.UNIFIED,
595
+ unified_password: str = "Abc@123456",
596
+ webhook_url: Optional[str] = None,
597
+ webhook_secret: Optional[str] = None,
598
+ direction: SyncDirection = SyncDirection.AUTH_TO_LEGACY,
599
+ **kwargs
600
+ ) -> SyncConfig:
601
+ """
602
+ 创建同步配置的便捷函数
603
+
604
+ Args:
605
+ field_mappings: 字段映射列表,默认使用通用映射
606
+ password_mode: 密码处理模式
607
+ unified_password: 统一初始密码
608
+ webhook_url: Webhook 接收地址
609
+ webhook_secret: Webhook 签名密钥
610
+ direction: 同步方向
611
+
612
+ Returns:
613
+ SyncConfig: 同步配置对象
614
+ """
615
+ return SyncConfig(
616
+ enabled=True,
617
+ direction=direction,
618
+ field_mappings=field_mappings or create_default_field_mappings(),
619
+ password_mode=password_mode,
620
+ unified_password=unified_password,
621
+ webhook_enabled=bool(webhook_url),
622
+ webhook_url=webhook_url,
623
+ webhook_secret=webhook_secret,
624
+ **kwargs
625
+ )