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.
- skyplatform_iam/__init__.py +95 -0
- skyplatform_iam/auth_middleware.py +173 -0
- skyplatform_iam/config.py +68 -0
- skyplatform_iam/connect_agenterra_iam.py +814 -0
- skyplatform_iam/exceptions.py +71 -0
- skyplatform_iam/middleware.py +186 -0
- skyplatform_iam-1.0.0.dist-info/METADATA +262 -0
- skyplatform_iam-1.0.0.dist-info/RECORD +9 -0
- skyplatform_iam-1.0.0.dist-info/WHEEL +4 -0
|
@@ -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
|