skyplatform-iam 1.0.0__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,814 @@
1
+ import os
2
+ import requests
3
+ import logging
4
+ import traceback
5
+ import copy
6
+ from dotenv import load_dotenv
7
+ from enum import Enum
8
+
9
+ # 加载环境变量
10
+ load_dotenv()
11
+
12
+
13
+ class CredentialTypeEnum(str, Enum):
14
+ """凭证类型枚举,与后端API保持一致"""
15
+ USERNAME = "username"
16
+ EMAIL = "email"
17
+ PHONE = "phone"
18
+ WECHAT_OPENID = "wechat_openid"
19
+
20
+
21
+ class ConnectAgenterraIam(object):
22
+ def __init__(self, logger_name="skyplatform_iam", log_level=logging.INFO):
23
+ """
24
+ 初始化AgenterraIAM连接器
25
+
26
+ 参数:
27
+ - logger_name: 日志记录器名称
28
+ - log_level: 日志级别
29
+ """
30
+ # 配置日志记录器
31
+ self.logger = logging.getLogger(logger_name)
32
+ if not self.logger.handlers:
33
+ handler = logging.StreamHandler()
34
+ formatter = logging.Formatter(
35
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
36
+ )
37
+ handler.setFormatter(formatter)
38
+ self.logger.addHandler(handler)
39
+ self.logger.setLevel(log_level)
40
+
41
+ # 从环境变量读取配置,提供默认值以确保向后兼容
42
+ self.agenterra_iam_host = os.environ.get('AGENTERRA_IAM_HOST')
43
+ self.server_name = os.environ.get('AGENTERRA_SERVER_NAME')
44
+ self.access_key = os.environ.get('AGENTERRA_ACCESS_KEY')
45
+
46
+ self.logger.info(f"初始化AgenterraIAM连接器 - Host: {self.agenterra_iam_host}, Server: {self._mask_sensitive(self.server_name)}")
47
+
48
+ self.headers = {
49
+ "Content-Type": "application/json",
50
+ "SERVER-AK": self.server_name,
51
+ "SERVER-SK": self.access_key
52
+ }
53
+ self.body = {
54
+ "server_name": self.server_name,
55
+ "access_key": self.access_key
56
+ }
57
+
58
+ def _mask_sensitive(self, value, mask_char="*", show_chars=4):
59
+ """
60
+ 脱敏处理敏感信息
61
+
62
+ 参数:
63
+ - value: 要脱敏的值
64
+ - mask_char: 脱敏字符
65
+ - show_chars: 显示的字符数量
66
+
67
+ 返回: 脱敏后的字符串
68
+ """
69
+ if not value or not isinstance(value, str):
70
+ return str(value) if value else "None"
71
+
72
+ if len(value) <= show_chars:
73
+ return mask_char * len(value)
74
+
75
+ return value[:show_chars] + mask_char * (len(value) - show_chars)
76
+
77
+ def _sanitize_log_data(self, data):
78
+ """
79
+ 清理日志数据,脱敏敏感信息
80
+
81
+ 参数:
82
+ - data: 要清理的数据(字典或其他类型)
83
+
84
+ 返回: 清理后的数据
85
+ """
86
+ if not isinstance(data, dict):
87
+ return data
88
+
89
+ # 需要脱敏的字段列表
90
+ sensitive_fields = [
91
+ 'password', 'access_key', 'token', 'refresh_token',
92
+ 'SERVER-SK', 'new_password', 'server_sk'
93
+ ]
94
+
95
+ sanitized = copy.deepcopy(data)
96
+
97
+ for key, value in sanitized.items():
98
+ if key.lower() in [field.lower() for field in sensitive_fields]:
99
+ sanitized[key] = self._mask_sensitive(str(value))
100
+ elif isinstance(value, dict):
101
+ sanitized[key] = self._sanitize_log_data(value)
102
+
103
+ return sanitized
104
+
105
+ def _log_request(self, method_name, url, headers, body):
106
+ """记录请求信息"""
107
+ sanitized_headers = self._sanitize_log_data(headers)
108
+ sanitized_body = self._sanitize_log_data(body)
109
+
110
+ self.logger.info(f"[{method_name}] 发送请求 - URL: {url}")
111
+ self.logger.info(f"[{method_name}] 请求头: {sanitized_headers}")
112
+ self.logger.info(f"[{method_name}] 请求体: {sanitized_body}")
113
+
114
+ def _log_response(self, method_name, response):
115
+ """记录响应信息"""
116
+ try:
117
+ response_data = response.json() if response.content else {}
118
+ sanitized_response = self._sanitize_log_data(response_data)
119
+ self.logger.info(f"[{method_name}] 响应状态码: {response.status_code}")
120
+ self.logger.info(f"[{method_name}] 响应内容: {sanitized_response}")
121
+ except Exception as e:
122
+ self.logger.info(f"[{method_name}] 响应状态码: {response.status_code}")
123
+ self.logger.info(f"[{method_name}] 响应内容解析失败: {str(e)}")
124
+
125
+ def register(self, cred_type=None, cred_value=None, password=None, nickname=None, avatar_url=None,
126
+ username=None, phone=None):
127
+ """
128
+ 注册用户时,同步至iam
129
+
130
+ 新参数格式(推荐使用):
131
+ - cred_type: 凭证类型 (CredentialTypeEnum: username, email, phone, wechat_openid)
132
+ - cred_value: 凭证值
133
+ - password: 用户密码(可选)
134
+ - nickname: 用户昵称(可选)
135
+ - avatar_url: 头像URL(可选)
136
+
137
+ 旧参数格式(向后兼容):
138
+ - username: 要注册的用户名
139
+ - phone: 手机号码(可选)
140
+ - password: 用户密码
141
+ - nickname: 用户昵称(可选)
142
+
143
+ 返回:
144
+ - 成功: 返回包含用户信息的字典
145
+ - 失败: 返回False
146
+ """
147
+ method_name = "register"
148
+ self.logger.info(f"[{method_name}] 开始用户注册 - cred_type: {cred_type}, cred_value: {self._mask_sensitive(str(cred_value))}")
149
+
150
+ try:
151
+ # 参数映射:支持旧的调用方式
152
+ if cred_type is None and cred_value is None:
153
+ if username:
154
+ cred_type = CredentialTypeEnum.USERNAME
155
+ cred_value = username
156
+ self.logger.debug(f"[{method_name}] 使用旧参数格式 - username: {self._mask_sensitive(username)}")
157
+ elif phone:
158
+ cred_type = CredentialTypeEnum.PHONE
159
+ cred_value = phone
160
+ self.logger.debug(f"[{method_name}] 使用旧参数格式 - phone: {self._mask_sensitive(phone)}")
161
+ else:
162
+ raise ValueError("必须提供 cred_type+cred_value 或 username/phone")
163
+
164
+ # 验证凭证类型
165
+ if isinstance(cred_type, str):
166
+ cred_type = CredentialTypeEnum(cred_type)
167
+
168
+ body = {
169
+ "server_name": self.server_name,
170
+ "access_key": self.access_key,
171
+ "cred_type": cred_type.value,
172
+ "cred_value": cred_value
173
+ }
174
+
175
+ # 添加可选参数
176
+ if password:
177
+ body["password"] = password
178
+ if nickname:
179
+ body["nickname"] = nickname
180
+ if avatar_url:
181
+ body["avatar_url"] = avatar_url
182
+
183
+ uri = "/api/v2/service/register"
184
+ url = self.agenterra_iam_host + uri
185
+
186
+ # 记录请求信息
187
+ self._log_request(method_name, url, self.headers, body)
188
+
189
+ response = requests.post(
190
+ url=url,
191
+ headers=self.headers,
192
+ json=body,
193
+ verify=False
194
+ )
195
+
196
+ # 记录响应信息
197
+ self._log_response(method_name, response)
198
+
199
+ if response.status_code == 200:
200
+ result = response.json()
201
+ if result.get("success"):
202
+ self.logger.info(f"[{method_name}] 用户注册成功")
203
+ return result.get("data")
204
+ else:
205
+ self.logger.warning(f"[{method_name}] 用户注册失败 - 响应: {result}")
206
+ else:
207
+ self.logger.warning(f"[{method_name}] 用户注册失败 - 状态码: {response.status_code}")
208
+
209
+ return False
210
+ except Exception as e:
211
+ self.logger.error(f"[{method_name}] 注册请求异常: {str(e)}")
212
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
213
+ return False
214
+
215
+ def login_with_password(self, cred_type=None, cred_value=None, password=None, ip_address=None, user_agent=None,
216
+ username=None):
217
+ """
218
+ 账号密码登陆时,同步至iam,由iam签发token
219
+
220
+ 新参数格式(推荐使用):
221
+ - cred_type: 凭证类型 (CredentialTypeEnum: username, email, phone, wechat_openid)
222
+ - cred_value: 凭证值
223
+ - password: 用户密码
224
+ - ip_address: IP地址(可选)
225
+ - user_agent: 用户代理(可选)
226
+
227
+ 旧参数格式(向后兼容):
228
+ - username: 用户名
229
+ - password: 用户密码
230
+ """
231
+ method_name = "login_with_password"
232
+ self.logger.info(f"[{method_name}] 开始密码登录 - cred_type: {cred_type}, cred_value: {self._mask_sensitive(str(cred_value))}")
233
+
234
+ try:
235
+ # 参数映射:支持旧的调用方式
236
+ if cred_type is None and cred_value is None:
237
+ if username:
238
+ cred_type = CredentialTypeEnum.USERNAME
239
+ cred_value = username
240
+ self.logger.debug(f"[{method_name}] 使用旧参数格式 - username: {self._mask_sensitive(username)}")
241
+ else:
242
+ raise ValueError("必须提供 cred_type+cred_value 或 username")
243
+
244
+ # 验证凭证类型
245
+ if isinstance(cred_type, str):
246
+ cred_type = CredentialTypeEnum(cred_type)
247
+
248
+ body = {
249
+ "server_name": self.server_name,
250
+ "access_key": self.access_key,
251
+ "cred_type": cred_type.value,
252
+ "cred_value": cred_value,
253
+ "password": password
254
+ }
255
+
256
+ # 添加可选参数
257
+ if ip_address:
258
+ body["ip_address"] = ip_address
259
+ if user_agent:
260
+ body["user_agent"] = user_agent
261
+
262
+ uri = "/api/v2/service/login"
263
+ url = self.agenterra_iam_host + uri
264
+
265
+ # 记录请求信息
266
+ self._log_request(method_name, url, self.headers, body)
267
+
268
+ response = requests.post(
269
+ url=url,
270
+ headers=self.headers,
271
+ json=body,
272
+ verify=False
273
+ )
274
+
275
+ # 记录响应信息
276
+ self._log_response(method_name, response)
277
+
278
+ if response.status_code == 200:
279
+ self.logger.info(f"[{method_name}] 密码登录成功")
280
+ return response
281
+ else:
282
+ self.logger.warning(f"[{method_name}] 密码登录失败 - 状态码: {response.status_code}")
283
+
284
+ return False
285
+ except Exception as e:
286
+ self.logger.error(f"[{method_name}] 密码登录请求异常: {str(e)}")
287
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
288
+ return False
289
+
290
+ def login_without_password(self, cred_type=None, cred_value=None, ip_address=None, user_agent=None,
291
+ username=None):
292
+ """
293
+ 短信验证码登陆时,机机接口请求token
294
+
295
+ 新参数格式(推荐使用):
296
+ - cred_type: 凭证类型 (CredentialTypeEnum: username, email, phone, wechat_openid)
297
+ - cred_value: 凭证值
298
+ - ip_address: IP地址(可选)
299
+ - user_agent: 用户代理(可选)
300
+
301
+ 旧参数格式(向后兼容):
302
+ - username: 用户名
303
+
304
+ 返回: response对象或False
305
+ """
306
+ method_name = "login_without_password"
307
+ self.logger.info(f"[{method_name}] 开始免密登录 - cred_type: {cred_type}, cred_value: {self._mask_sensitive(str(cred_value))}")
308
+
309
+ try:
310
+ # 参数映射:支持旧的调用方式
311
+ if cred_type is None and cred_value is None:
312
+ if username:
313
+ cred_type = CredentialTypeEnum.USERNAME
314
+ cred_value = username
315
+ self.logger.debug(f"[{method_name}] 使用旧参数格式 - username: {self._mask_sensitive(username)}")
316
+ else:
317
+ raise ValueError("必须提供 cred_type+cred_value 或 username")
318
+
319
+ # 验证凭证类型
320
+ if isinstance(cred_type, str):
321
+ cred_type = CredentialTypeEnum(cred_type)
322
+
323
+ body = {
324
+ "server_name": self.server_name,
325
+ "access_key": self.access_key,
326
+ "cred_type": cred_type.value,
327
+ "cred_value": cred_value
328
+ }
329
+
330
+ # 添加可选参数
331
+ if ip_address:
332
+ body["ip_address"] = ip_address
333
+ if user_agent:
334
+ body["user_agent"] = user_agent
335
+
336
+ uri = "/api/v2/service/login_without_password"
337
+ url = self.agenterra_iam_host + uri
338
+
339
+ # 记录请求信息
340
+ self._log_request(method_name, url, self.headers, body)
341
+
342
+ response = requests.post(
343
+ url=url,
344
+ headers=self.headers,
345
+ json=body,
346
+ verify=False
347
+ )
348
+
349
+ # 记录响应信息
350
+ self._log_response(method_name, response)
351
+
352
+ if response.status_code == 200:
353
+ self.logger.info(f"[{method_name}] 免密登录成功")
354
+ return response
355
+ else:
356
+ self.logger.warning(f"[{method_name}] 免密登录失败 - 状态码: {response.status_code}")
357
+
358
+ return False
359
+ except Exception as e:
360
+ self.logger.error(f"[{method_name}] 免密登录请求异常: {str(e)}")
361
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
362
+ return False
363
+
364
+ def logout(self, token):
365
+ """
366
+ 用户登出
367
+ server_name: 服务名称
368
+ username: 用户名
369
+ """
370
+ method_name = "logout"
371
+ self.logger.info(f"[{method_name}] 开始用户登出 - token: {self._mask_sensitive(token)}")
372
+
373
+ try:
374
+ body = {
375
+ "server_name": self.server_name,
376
+ "access_key": self.access_key,
377
+ "token": token
378
+ }
379
+ uri = "/api/v2/service/logout"
380
+ url = self.agenterra_iam_host + uri
381
+
382
+ # 记录请求信息
383
+ self._log_request(method_name, url, self.headers, body)
384
+
385
+ response = requests.post(
386
+ url=url,
387
+ headers=self.headers,
388
+ json=body,
389
+ verify=False
390
+ )
391
+
392
+ # 记录响应信息
393
+ self._log_response(method_name, response)
394
+
395
+ if response.status_code == 200:
396
+ self.logger.info(f"[{method_name}] 用户登出成功")
397
+ return True
398
+ else:
399
+ self.logger.warning(f"[{method_name}] 用户登出失败 - 状态码: {response.status_code}")
400
+
401
+ return False
402
+ except Exception as e:
403
+ self.logger.error(f"[{method_name}] 登出请求异常: {str(e)}")
404
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
405
+ return False
406
+
407
+ def verify_token(self, token, api, method, server_ak="", server_sk=""):
408
+ """
409
+ 请求iam进行鉴权
410
+ server_name: 服务名称
411
+ token: 用户token
412
+ api: 当前访问的API路径
413
+
414
+ 返回:
415
+ - 成功且有权限: 返回用户信息字典
416
+ - 成功但无权限: 抛出403异常
417
+ - token无效或其他错误: 返回None
418
+ """
419
+ method_name = "verify_token"
420
+ self.logger.info(f"[{method_name}] 开始token验证 - api: {api}, method: {method}, token: {self._mask_sensitive(token)}")
421
+
422
+ try:
423
+ body = {
424
+ "server_name": self.server_name,
425
+ "access_key": self.access_key,
426
+ "token": token,
427
+ "api": api,
428
+ "method": method,
429
+ "server_ak": server_ak,
430
+ "server_sk": server_sk,
431
+ }
432
+ uri = "/api/v2/service/verify"
433
+ url = self.agenterra_iam_host + uri
434
+
435
+ # 记录请求信息
436
+ self._log_request(method_name, url, self.headers, body)
437
+
438
+ response = requests.post(
439
+ url=url,
440
+ headers=self.headers,
441
+ json=body,
442
+ verify=False
443
+ )
444
+
445
+ # 记录响应信息
446
+ self._log_response(method_name, response)
447
+
448
+ if response.status_code == 200:
449
+ result = response.json()
450
+ # 检查响应格式
451
+ if result.get("success") and result.get("valid"):
452
+ if result.get("has_permission"):
453
+ # 有权限时返回用户信息
454
+ user_info = {
455
+ "user_id": result.get("user_id"),
456
+ "username": result.get("username"),
457
+ "session_id": result.get("session_id"),
458
+ "microservice": result.get("microservice"),
459
+ "is_whitelist": result.get("is_whitelist", False)
460
+ }
461
+ self.logger.info(f"[{method_name}] token验证成功,用户有权限 - user_id: {user_info.get('user_id')}")
462
+ return user_info
463
+ else:
464
+ # token有效但无权限,抛出403异常
465
+ self.logger.warning(f"[{method_name}] token有效但用户无权限访问API: {api}")
466
+ from fastapi import HTTPException, status
467
+ raise HTTPException(
468
+ status_code=status.HTTP_403_FORBIDDEN,
469
+ detail=result.get("message", "用户无权限访问此API")
470
+ )
471
+ else:
472
+ self.logger.warning(f"[{method_name}] token验证失败 - success: {result.get('success')}, valid: {result.get('valid')}")
473
+
474
+ elif response.status_code == 403:
475
+ result = response.json()
476
+ # 处理403响应
477
+ self.logger.warning(f"[{method_name}] 收到403响应 - {result.get('message', '用户无权限访问此API')}")
478
+ from fastapi import HTTPException, status
479
+ raise HTTPException(
480
+ status_code=status.HTTP_403_FORBIDDEN,
481
+ detail=result.get("message", "用户无权限访问此API")
482
+ )
483
+ else:
484
+ self.logger.warning(f"[{method_name}] token验证失败 - 状态码: {response.status_code}")
485
+
486
+ return None
487
+ except HTTPException:
488
+ # 重新抛出HTTP异常
489
+ raise
490
+ except Exception as e:
491
+ self.logger.error(f"[{method_name}] Token验证异常: {str(e)}")
492
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
493
+ return None
494
+
495
+ def reset_password(self, user_id=None, new_password=None, username=None, password=None):
496
+ """
497
+ 重置密码
498
+
499
+ 新参数格式(推荐使用):
500
+ - user_id: 用户ID
501
+ - new_password: 新密码
502
+
503
+ 旧参数格式(向后兼容):
504
+ - username: 用户名
505
+ - password: 新密码
506
+ """
507
+ method_name = "reset_password"
508
+ self.logger.info(f"[{method_name}] 开始重置密码 - user_id: {user_id}")
509
+
510
+ # 记录旧参数格式的使用
511
+ if username or password:
512
+ self.logger.debug(f"[{method_name}] 检测到旧参数格式 - username: {username}")
513
+
514
+ try:
515
+ # 参数映射:支持旧的调用方式
516
+ if user_id is None and new_password is None:
517
+ if username and password:
518
+ # 旧版本使用username,但后端需要user_id
519
+ # 这里需要先通过其他方式获取user_id,或者提示用户使用新格式
520
+ raise ValueError("旧版本参数已废弃,请使用 user_id 和 new_password 参数")
521
+ else:
522
+ raise ValueError("必须提供 user_id 和 new_password")
523
+
524
+ body = {
525
+ "server_name": self.server_name,
526
+ "access_key": self.access_key,
527
+ "user_id": user_id,
528
+ "new_password": new_password
529
+ }
530
+ uri = "/api/v2/service/reset_password"
531
+ url = self.agenterra_iam_host + uri
532
+
533
+ # 记录请求信息
534
+ self._log_request(method_name, url, self.headers, body)
535
+
536
+ response = requests.post(
537
+ url=url,
538
+ headers=self.headers,
539
+ json=body,
540
+ verify=False
541
+ )
542
+
543
+ # 记录响应信息
544
+ self._log_response(method_name, response)
545
+
546
+ if response.status_code == 200:
547
+ self.logger.info(f"[{method_name}] 密码重置成功")
548
+ return True
549
+ else:
550
+ self.logger.warning(f"[{method_name}] 密码重置失败 - 状态码: {response.status_code}")
551
+
552
+ return False
553
+ except Exception as e:
554
+ self.logger.error(f"[{method_name}] 重置密码请求异常: {str(e)}")
555
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
556
+ return False
557
+
558
+ def refresh_token(self, refresh_token):
559
+ """
560
+ 机机接口:刷新用户令牌
561
+
562
+ 参数:
563
+ - refresh_token: 刷新令牌
564
+
565
+ 返回: response对象或False
566
+ """
567
+ method_name = "refresh_token"
568
+ self.logger.info(f"[{method_name}] 开始刷新令牌 - refresh_token: {self._mask_sensitive(refresh_token)}")
569
+
570
+ try:
571
+ body = {
572
+ "server_name": self.server_name,
573
+ "access_key": self.access_key,
574
+ "refresh_token": refresh_token
575
+ }
576
+ uri = "/api/v2/service/refresh_token"
577
+ url = self.agenterra_iam_host + uri
578
+
579
+ # 记录请求信息
580
+ self._log_request(method_name, url, self.headers, body)
581
+
582
+ response = requests.post(
583
+ url=url,
584
+ headers=self.headers,
585
+ json=body,
586
+ verify=False
587
+ )
588
+
589
+ # 记录响应信息
590
+ self._log_response(method_name, response)
591
+
592
+ if response.status_code == 200:
593
+ self.logger.info(f"[{method_name}] 令牌刷新成功")
594
+ return response
595
+ else:
596
+ self.logger.warning(f"[{method_name}] 令牌刷新失败 - 状态码: {response.status_code}")
597
+
598
+ return False
599
+ except Exception as e:
600
+ self.logger.error(f"[{method_name}] 刷新令牌请求异常: {str(e)}")
601
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
602
+ return False
603
+
604
+ def assign_role_to_user(self, user_id, role_id):
605
+ """
606
+ 机机接口:给指定用户授予指定角色
607
+
608
+ 参数:
609
+ - user_id: 用户ID
610
+ - role_id: 角色ID
611
+
612
+ 返回: 成功返回True,失败返回False
613
+ """
614
+ method_name = "assign_role_to_user"
615
+ self.logger.info(f"[{method_name}] 开始角色分配 - user_id: {user_id}, role_id: {role_id}")
616
+
617
+ try:
618
+ body = {
619
+ "server_name": self.server_name,
620
+ "access_key": self.access_key,
621
+ "user_id": user_id,
622
+ "role_id": role_id
623
+ }
624
+ uri = "/api/v2/service/assign_role"
625
+ url = self.agenterra_iam_host + uri
626
+
627
+ # 记录请求信息
628
+ self._log_request(method_name, url, self.headers, body)
629
+
630
+ response = requests.post(
631
+ url=url,
632
+ headers=self.headers,
633
+ json=body,
634
+ verify=False
635
+ )
636
+
637
+ # 记录响应信息
638
+ self._log_response(method_name, response)
639
+
640
+ if response.status_code == 200:
641
+ self.logger.info(f"[{method_name}] 角色分配成功")
642
+ return True
643
+ else:
644
+ self.logger.warning(f"[{method_name}] 角色分配失败 - 状态码: {response.status_code}")
645
+
646
+ return False
647
+ except Exception as e:
648
+ self.logger.error(f"[{method_name}] 角色分配请求异常: {str(e)}")
649
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
650
+ return False
651
+
652
+ def get_userinfo_by_token(self, token):
653
+ """
654
+ 账号密码登陆时,同步至iam,由iam签发token
655
+ """
656
+ method_name = "get_userinfo_by_token"
657
+ self.logger.info(f"[{method_name}] 开始获取用户信息 - token: {self._mask_sensitive(token)}")
658
+
659
+ try:
660
+ body = {
661
+ "server_name": self.server_name,
662
+ "access_key": self.access_key,
663
+ "token": token,
664
+ }
665
+ uri = "/api/v2/service/token"
666
+ url = self.agenterra_iam_host + uri
667
+
668
+ # 记录请求信息
669
+ self._log_request(method_name, url, self.headers, body)
670
+
671
+ response = requests.post(
672
+ url=url,
673
+ headers=self.headers,
674
+ json=body,
675
+ verify=False
676
+ )
677
+
678
+ # 记录响应信息
679
+ self._log_response(method_name, response)
680
+
681
+ if response.status_code == 200:
682
+ self.logger.info(f"[{method_name}] 获取用户信息成功")
683
+ return response
684
+ else:
685
+ self.logger.warning(f"[{method_name}] 获取用户信息失败 - 状态码: {response.status_code}")
686
+
687
+ return False
688
+ except Exception as e:
689
+ self.logger.error(f"[{method_name}] 获取用户信息请求异常: {str(e)}")
690
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
691
+ return False
692
+
693
+ def merge_credential(self, target_user_id, cred_type, cred_value, merge_reason=None):
694
+ """
695
+ 机机接口:凭证合并
696
+
697
+ 为第三方服务提供凭证合并功能,处理用户绑定新凭证时的账号合并场景。
698
+ 例如用户先用账号密码注册,后续又绑定手机号时的账号合并需求。
699
+
700
+ 参数:
701
+ - target_user_id: 目标用户ID(保留的用户)
702
+ - cred_type: 要绑定的凭证类型 (CredentialTypeEnum: username, email, phone, wechat_openid)
703
+ - cred_value: 要绑定的凭证值
704
+ - merge_reason: 合并原因(可选)
705
+
706
+ 返回:
707
+ - 成功: 返回响应对象
708
+ - 失败: 返回False
709
+ """
710
+ method_name = "merge_credential"
711
+ self.logger.info(f"[{method_name}] 开始凭证合并 - target_user_id: {target_user_id}, cred_type: {cred_type}, cred_value: {self._mask_sensitive(cred_value)}")
712
+
713
+ try:
714
+ # 验证凭证类型
715
+ if isinstance(cred_type, str):
716
+ cred_type = CredentialTypeEnum(cred_type)
717
+
718
+ body = {
719
+ "server_name": self.server_name,
720
+ "access_key": self.access_key,
721
+ "target_user_id": target_user_id,
722
+ "cred_type": cred_type.value,
723
+ "cred_value": cred_value
724
+ }
725
+
726
+ # 添加可选参数
727
+ if merge_reason:
728
+ body["merge_reason"] = merge_reason
729
+ self.logger.debug(f"[{method_name}] 合并原因: {merge_reason}")
730
+
731
+ uri = "/api/v2/service/merge_credential"
732
+ url = self.agenterra_iam_host + uri
733
+
734
+ # 记录请求信息
735
+ self._log_request(method_name, url, self.headers, body)
736
+
737
+ response = requests.post(
738
+ url=url,
739
+ headers=self.headers,
740
+ json=body,
741
+ verify=False
742
+ )
743
+
744
+ # 记录响应信息
745
+ self._log_response(method_name, response)
746
+
747
+ if response.status_code == 200:
748
+ self.logger.info(f"[{method_name}] 凭证合并成功")
749
+ return response
750
+ else:
751
+ self.logger.warning(f"[{method_name}] 凭证合并失败 - 状态码: {response.status_code}")
752
+
753
+ return False
754
+ except Exception as e:
755
+ self.logger.error(f"[{method_name}] 凭证合并请求异常: {str(e)}")
756
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
757
+ return False
758
+
759
+ def get_user_by_credential(self, cred_type, cred_value):
760
+ """
761
+ 机机接口:通过凭证获取用户信息
762
+
763
+ 为第三方服务提供通过用户名或手机号等认证凭据获取用户信息的功能。
764
+
765
+ 参数:
766
+ - cred_type: 凭证类型 (CredentialTypeEnum: username, email, phone, wechat_openid)
767
+ - cred_value: 凭证值
768
+
769
+ 返回:
770
+ - 成功: 返回响应对象
771
+ - 失败: 返回False
772
+ """
773
+ method_name = "get_user_by_credential"
774
+ self.logger.info(f"[{method_name}] 开始获取用户信息 - cred_type: {cred_type}, cred_value: {self._mask_sensitive(cred_value)}")
775
+
776
+ try:
777
+ # 验证凭证类型
778
+ if isinstance(cred_type, str):
779
+ cred_type = CredentialTypeEnum(cred_type)
780
+
781
+ body = {
782
+ "server_name": self.server_name,
783
+ "access_key": self.access_key,
784
+ "cred_type": cred_type.value,
785
+ "cred_value": cred_value
786
+ }
787
+
788
+ uri = "/api/v2/service/get_user_by_credential"
789
+ url = self.agenterra_iam_host + uri
790
+
791
+ # 记录请求信息
792
+ self._log_request(method_name, url, self.headers, body)
793
+
794
+ response = requests.post(
795
+ url=url,
796
+ headers=self.headers,
797
+ json=body,
798
+ verify=False
799
+ )
800
+
801
+ # 记录响应信息
802
+ self._log_response(method_name, response)
803
+
804
+ if response.status_code == 200:
805
+ self.logger.info(f"[{method_name}] 获取用户信息成功")
806
+ return response
807
+ else:
808
+ self.logger.warning(f"[{method_name}] 获取用户信息失败 - 状态码: {response.status_code}")
809
+
810
+ return False
811
+ except Exception as e:
812
+ self.logger.error(f"[{method_name}] 获取用户信息请求异常: {str(e)}")
813
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
814
+ return False