skyplatform-iam 1.1.0__tar.gz → 1.2.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skyplatform-iam
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: SkyPlatform IAM认证SDK,提供FastAPI中间件和认证路由
5
5
  Project-URL: Homepage, https://github.com/xinmayoujiang12621/agenterra_iam
6
6
  Project-URL: Documentation, https://skyplatform-iam.readthedocs.io/
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "skyplatform-iam"
7
- version = "1.1.0"
7
+ version = "1.2.0"
8
8
  authors = [
9
9
  { name="x9", email="xuanxienanxunmobao@gmail.com" },
10
10
  ]
@@ -17,7 +17,7 @@ from .exceptions import (
17
17
  NetworkError
18
18
  )
19
19
 
20
- __version__ = "1.0.0"
20
+ __version__ = "1.2.0"
21
21
  __author__ = "x9"
22
22
  __description__ = "SkyPlatform IAM认证SDK,提供FastAPI中间件和IAM服务连接功能"
23
23
 
@@ -1,14 +1,18 @@
1
1
  """
2
2
  SkyPlatform IAM SDK 配置模块
3
3
  """
4
+ import logging
4
5
  import os
5
6
  import fnmatch
6
- from typing import Optional, List
7
+ from typing import List, Optional, Dict
8
+
9
+ import jwt
7
10
  from pydantic import BaseModel, Field
8
11
  from dotenv import load_dotenv
9
12
 
10
13
  # 加载环境变量
11
14
  load_dotenv()
15
+ logger = logging.getLogger(__name__)
12
16
 
13
17
 
14
18
  class AuthConfig(BaseModel):
@@ -20,6 +24,7 @@ class AuthConfig(BaseModel):
20
24
  agenterra_iam_host: str
21
25
  server_name: str
22
26
  access_key: str
27
+ machine_token: str
23
28
 
24
29
  # Token配置
25
30
  token_header: str = "Authorization"
@@ -44,6 +49,7 @@ class AuthConfig(BaseModel):
44
49
  server_name=os.environ.get('AGENTERRA_SERVER_NAME', ''),
45
50
  access_key=os.environ.get('AGENTERRA_ACCESS_KEY', ''),
46
51
  enable_debug=os.environ.get('AGENTERRA_ENABLE_DEBUG', 'false').lower() == 'true',
52
+ machine_token=os.environ.get('MACHINE_TOKEN', ''),
47
53
  whitelist_paths=[] # 初始化空的白名单路径列表
48
54
  )
49
55
 
@@ -63,15 +69,15 @@ class AuthConfig(BaseModel):
63
69
  """
64
70
  if not path:
65
71
  return path
66
-
72
+
67
73
  # 确保路径以 / 开头
68
74
  if not path.startswith('/'):
69
75
  path = '/' + path
70
-
76
+
71
77
  # 移除重复的斜杠
72
78
  while '//' in path:
73
79
  path = path.replace('//', '/')
74
-
80
+
75
81
  return path
76
82
 
77
83
  def add_whitelist_path(self, path: str) -> None:
@@ -80,7 +86,7 @@ class AuthConfig(BaseModel):
80
86
  """
81
87
  if not path:
82
88
  return
83
-
89
+
84
90
  normalized_path = self._normalize_path(path)
85
91
  if normalized_path not in self.whitelist_paths:
86
92
  self.whitelist_paths.append(normalized_path)
@@ -98,7 +104,7 @@ class AuthConfig(BaseModel):
98
104
  """
99
105
  if not path:
100
106
  return
101
-
107
+
102
108
  normalized_path = self._normalize_path(path)
103
109
  if normalized_path in self.whitelist_paths:
104
110
  self.whitelist_paths.remove(normalized_path)
@@ -121,12 +127,27 @@ class AuthConfig(BaseModel):
121
127
  """
122
128
  if not path:
123
129
  return False
124
-
130
+
125
131
  normalized_path = self._normalize_path(path)
126
-
132
+
127
133
  for whitelist_path in self.whitelist_paths:
128
134
  # 支持通配符匹配
129
135
  if fnmatch.fnmatch(normalized_path, whitelist_path):
130
136
  return True
131
-
137
+
132
138
  return False
139
+
140
+
141
+ def decode_jwt_token(token: str) -> Optional[Dict]:
142
+ """直接解析JWT token获取payload"""
143
+ try:
144
+ # 不验证签名,只解析payload(因为token已经通过verify_token验证过)
145
+ decoded_payload = jwt.decode(token, options={"verify_signature": False})
146
+ logger.debug(f"JWT token解析成功: {decoded_payload}")
147
+ return decoded_payload
148
+ except jwt.InvalidTokenError as e:
149
+ logger.error(f"JWT token解析失败: {str(e)}")
150
+ return None
151
+ except Exception as e:
152
+ logger.error(f"JWT token解析异常: {str(e)}")
153
+ return None
@@ -5,6 +5,8 @@ import copy
5
5
  from enum import Enum
6
6
  from fastapi import HTTPException, status
7
7
 
8
+ from .config import decode_jwt_token
9
+
8
10
 
9
11
  class CredentialTypeEnum(str, Enum):
10
12
  """凭证类型枚举,与后端API保持一致"""
@@ -17,7 +19,7 @@ class CredentialTypeEnum(str, Enum):
17
19
  class ConnectAgenterraIam(object):
18
20
  _instance = None
19
21
  _initialized = False
20
-
22
+
21
23
  def __new__(cls, config=None, logger_name="skyplatform_iam", log_level=logging.INFO):
22
24
  """
23
25
  单例模式实现
@@ -26,11 +28,11 @@ class ConnectAgenterraIam(object):
26
28
  if cls._instance is None:
27
29
  cls._instance = super(ConnectAgenterraIam, cls).__new__(cls)
28
30
  return cls._instance
29
-
31
+
30
32
  def __init__(self, config=None, logger_name="skyplatform_iam", log_level=logging.INFO):
31
33
  """
32
34
  初始化AgenterraIAM连接器
33
-
35
+
34
36
  参数:
35
37
  - config: AuthConfig配置对象,如果为None则从环境变量读取
36
38
  - logger_name: 日志记录器名称
@@ -39,7 +41,7 @@ class ConnectAgenterraIam(object):
39
41
  # 防止重复初始化
40
42
  if self._initialized:
41
43
  return
42
-
44
+
43
45
  # 配置日志记录器
44
46
  self.logger = logging.getLogger(logger_name)
45
47
  if not self.logger.handlers:
@@ -50,16 +52,17 @@ class ConnectAgenterraIam(object):
50
52
  handler.setFormatter(formatter)
51
53
  self.logger.addHandler(handler)
52
54
  self.logger.setLevel(log_level)
53
-
55
+
54
56
  # 必须传入config参数,不再支持从环境变量读取
55
57
  if config is None:
56
58
  raise ValueError("必须传入AuthConfig配置对象,不再支持从环境变量读取配置")
57
-
59
+
58
60
  self.agenterra_iam_host = config.agenterra_iam_host
59
61
  self.server_name = config.server_name
60
62
  self.access_key = config.access_key
63
+ self.machine_token = config.machine_token
61
64
  self.logger.info("使用传入的AuthConfig配置")
62
-
65
+
63
66
  # 验证必要的配置
64
67
  if not self.agenterra_iam_host:
65
68
  self.logger.warning("AGENTERRA_IAM_HOST 配置未设置")
@@ -67,9 +70,10 @@ class ConnectAgenterraIam(object):
67
70
  self.logger.warning("AGENTERRA_SERVER_NAME 配置未设置")
68
71
  if not self.access_key:
69
72
  self.logger.warning("AGENTERRA_ACCESS_KEY 配置未设置")
70
-
71
- self.logger.info(f"初始化AgenterraIAM连接器 - Host: {self.agenterra_iam_host}, Server: {self._mask_sensitive(self.server_name)}")
72
-
73
+
74
+ self.logger.info(
75
+ f"初始化AgenterraIAM连接器 - Host: {self.agenterra_iam_host}, Server: {self._mask_sensitive(self.server_name)}")
76
+
73
77
  self.headers = {
74
78
  "Content-Type": "application/json",
75
79
  "SERVER-AK": self.server_name,
@@ -77,9 +81,10 @@ class ConnectAgenterraIam(object):
77
81
  }
78
82
  self.body = {
79
83
  "server_name": self.server_name,
80
- "access_key": self.access_key
84
+ "access_key": self.access_key,
85
+ "machine_token": self.machine_token
81
86
  }
82
-
87
+
83
88
  # 标记为已初始化
84
89
  self._initialized = True
85
90
 
@@ -87,20 +92,20 @@ class ConnectAgenterraIam(object):
87
92
  """
88
93
  重新加载配置
89
94
  用于在运行时更新配置
90
-
95
+
91
96
  参数:
92
97
  - config: AuthConfig配置对象
93
98
  """
94
99
  if config is None:
95
100
  raise ValueError("必须传入AuthConfig配置对象")
96
-
101
+
97
102
  self.logger.info("重新加载配置")
98
-
103
+
99
104
  # 更新配置
100
105
  self.agenterra_iam_host = config.agenterra_iam_host
101
106
  self.server_name = config.server_name
102
107
  self.access_key = config.access_key
103
-
108
+
104
109
  # 验证必要的配置
105
110
  if not self.agenterra_iam_host:
106
111
  self.logger.warning("AGENTERRA_IAM_HOST 配置未设置")
@@ -108,7 +113,7 @@ class ConnectAgenterraIam(object):
108
113
  self.logger.warning("AGENTERRA_SERVER_NAME 配置未设置")
109
114
  if not self.access_key:
110
115
  self.logger.warning("AGENTERRA_ACCESS_KEY 配置未设置")
111
-
116
+
112
117
  # 更新headers和body
113
118
  self.headers = {
114
119
  "Content-Type": "application/json",
@@ -117,63 +122,65 @@ class ConnectAgenterraIam(object):
117
122
  }
118
123
  self.body = {
119
124
  "server_name": self.server_name,
120
- "access_key": self.access_key
125
+ "access_key": self.access_key,
126
+ "machine_token": self.machine_token,
121
127
  }
122
-
123
- self.logger.info(f"配置重新加载完成 - Host: {self.agenterra_iam_host}, Server: {self._mask_sensitive(self.server_name)}")
128
+
129
+ self.logger.info(
130
+ f"配置重新加载完成 - Host: {self.agenterra_iam_host}, Server: {self._mask_sensitive(self.server_name)}")
124
131
 
125
132
  def _mask_sensitive(self, value, mask_char="*", show_chars=4):
126
133
  """
127
134
  脱敏处理敏感信息
128
-
135
+
129
136
  参数:
130
137
  - value: 要脱敏的值
131
138
  - mask_char: 脱敏字符
132
139
  - show_chars: 显示的字符数量
133
-
140
+
134
141
  返回: 脱敏后的字符串
135
142
  """
136
143
  if not value or not isinstance(value, str):
137
144
  return str(value) if value else "None"
138
-
145
+
139
146
  if len(value) <= show_chars:
140
147
  return mask_char * len(value)
141
-
148
+
142
149
  return value[:show_chars] + mask_char * (len(value) - show_chars)
143
150
 
144
151
  def _sanitize_log_data(self, data):
145
152
  """
146
153
  清理日志数据,脱敏敏感信息
147
-
154
+
148
155
  参数:
149
156
  - data: 要清理的数据(字典或其他类型)
150
-
157
+
151
158
  返回: 清理后的数据
152
159
  """
153
160
  if not isinstance(data, dict):
154
161
  return data
155
-
162
+
156
163
  # 需要脱敏的字段列表
157
164
  sensitive_fields = [
158
- 'password', 'access_key', 'token', 'refresh_token',
165
+ 'password', 'access_key', 'token', 'refresh_token',
159
166
  'SERVER-SK', 'new_password', 'server_sk'
160
167
  ]
161
-
168
+
162
169
  sanitized = copy.deepcopy(data)
163
-
170
+
164
171
  for key, value in sanitized.items():
165
172
  if key.lower() in [field.lower() for field in sensitive_fields]:
166
173
  sanitized[key] = self._mask_sensitive(str(value))
167
174
  elif isinstance(value, dict):
168
175
  sanitized[key] = self._sanitize_log_data(value)
169
-
176
+
170
177
  return sanitized
171
178
 
172
179
  def _log_request(self, method_name, url, headers, body):
173
180
  """记录请求信息"""
174
181
  sanitized_headers = self._sanitize_log_data(headers)
175
182
  sanitized_body = self._sanitize_log_data(body)
176
-
183
+
177
184
  self.logger.info(f"[{method_name}] 发送请求 - URL: {url}")
178
185
  self.logger.info(f"[{method_name}] 请求头: {sanitized_headers}")
179
186
  self.logger.info(f"[{method_name}] 请求体: {sanitized_body}")
@@ -346,6 +353,11 @@ class ConnectAgenterraIam(object):
346
353
 
347
354
  if response.status_code == 200:
348
355
  self.logger.info(f"[{method_name}] 密码登录成功")
356
+ data = response.json()["data"]
357
+ access_token = data["access_token"]
358
+ payload = decode_jwt_token(access_token)
359
+ self.machine_token = payload["machine_access_token"]
360
+
349
361
  return response
350
362
  else:
351
363
  self.logger.warning(f"[{method_name}] 密码登录失败 - 状态码: {response.status_code}")
@@ -452,9 +464,12 @@ class ConnectAgenterraIam(object):
452
464
  # 记录请求信息
453
465
  self._log_request(method_name, url, self.headers, body)
454
466
 
467
+ headers = self.headers.copy()
468
+ headers['MACHINE-TOKEN'] = self.machine_token
469
+
455
470
  response = requests.post(
456
471
  url=url,
457
- headers=self.headers,
472
+ headers=headers,
458
473
  json=body,
459
474
  verify=False
460
475
  )
@@ -474,7 +489,7 @@ class ConnectAgenterraIam(object):
474
489
  self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
475
490
  return False
476
491
 
477
- def verify_token(self, token, api, method, server_ak="", server_sk=""):
492
+ def verify_token(self, token, api, method, server_ak="", server_sk="", machine_token=""):
478
493
  """
479
494
  请求iam进行鉴权
480
495
  server_name: 服务名称
@@ -494,6 +509,7 @@ class ConnectAgenterraIam(object):
494
509
  body = {
495
510
  "server_name": self.server_name,
496
511
  "access_key": self.access_key,
512
+ "machine_token": machine_token if machine_token else self.machine_token,
497
513
  "token": token,
498
514
  "api": api,
499
515
  "method": method,
@@ -600,6 +616,7 @@ class ConnectAgenterraIam(object):
600
616
  body = {
601
617
  "server_name": self.server_name,
602
618
  "access_key": self.access_key,
619
+ "machine_token": self.machine_token,
603
620
  "user_id": user_id,
604
621
  "new_password": new_password
605
622
  }
@@ -647,6 +664,7 @@ class ConnectAgenterraIam(object):
647
664
  body = {
648
665
  "server_name": self.server_name,
649
666
  "access_key": self.access_key,
667
+ "machine_token": self.machine_token,
650
668
  "refresh_token": refresh_token
651
669
  }
652
670
  uri = "/api/v2/service/refresh_token"
@@ -694,6 +712,7 @@ class ConnectAgenterraIam(object):
694
712
  body = {
695
713
  "server_name": self.server_name,
696
714
  "access_key": self.access_key,
715
+ "machine_token": self.machine_token,
697
716
  "user_id": user_id,
698
717
  "role_id": role_id
699
718
  }
@@ -736,6 +755,7 @@ class ConnectAgenterraIam(object):
736
755
  body = {
737
756
  "server_name": self.server_name,
738
757
  "access_key": self.access_key,
758
+ "machine_token": self.machine_token,
739
759
  "token": token,
740
760
  }
741
761
  uri = "/api/v2/service/token"
@@ -788,6 +808,7 @@ class ConnectAgenterraIam(object):
788
808
  body = {
789
809
  "server_name": self.server_name,
790
810
  "access_key": self.access_key,
811
+ "machine_token": self.machine_token,
791
812
  "user_id": user_id,
792
813
  "config_name": config_name
793
814
  }
@@ -844,6 +865,7 @@ class ConnectAgenterraIam(object):
844
865
  body = {
845
866
  "server_name": self.server_name,
846
867
  "access_key": self.access_key,
868
+ "machine_token": self.machine_token,
847
869
  "user_id": user_id
848
870
  }
849
871
 
@@ -896,6 +918,7 @@ class ConnectAgenterraIam(object):
896
918
  body = {
897
919
  "server_name": self.server_name,
898
920
  "access_key": self.access_key,
921
+ "machine_token": self.machine_token,
899
922
  "user_id": user_id,
900
923
  "config_name": config_name
901
924
  }
@@ -957,6 +980,7 @@ class ConnectAgenterraIam(object):
957
980
  body = {
958
981
  "server_name": self.server_name,
959
982
  "access_key": self.access_key,
983
+ "machine_token": self.machine_token,
960
984
  "target_user_id": target_user_id,
961
985
  "cred_type": cred_type.value,
962
986
  "cred_value": cred_value
@@ -1021,6 +1045,7 @@ class ConnectAgenterraIam(object):
1021
1045
  body = {
1022
1046
  "server_name": self.server_name,
1023
1047
  "access_key": self.access_key,
1048
+ "machine_token": self.machine_token,
1024
1049
  "cred_type": cred_type.value,
1025
1050
  "cred_value": cred_value
1026
1051
  }
@@ -66,7 +66,14 @@ class AuthMiddleware(BaseHTTPMiddleware):
66
66
  try:
67
67
  # 获取请求路径
68
68
  api_path = request.url.path
69
-
69
+ method = request.method
70
+
71
+ # 检查是否为机机接口鉴权 来自其他服务
72
+ server_ak = request.headers.get('SERVER-AK')
73
+ server_sk = request.headers.get('SERVER-SK')
74
+ # 检查是否为人机接口鉴权 来自前端
75
+ # token = request.headers.get('Authorization')
76
+
70
77
  # 首先检查路径是否在本地白名单中
71
78
  if self.is_path_whitelisted(api_path):
72
79
  logger.info(f"路径 {api_path} 在本地白名单中,跳过认证直接允许访问")
@@ -80,6 +87,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
80
87
 
81
88
  # 提取Token(可能为空,白名单接口不需要token)
82
89
  token = self._extract_token(request)
90
+ machine_token = self._extract_machine_token(request)
83
91
 
84
92
  # 验证Token和权限(即使token为空也要调用IAM验证,因为可能是白名单接口)
85
93
  user_info = await self._verify_token_and_permission(request, token)
@@ -97,6 +105,11 @@ class AuthMiddleware(BaseHTTPMiddleware):
97
105
  request.state.authenticated = False
98
106
  request.state.is_whitelist = True
99
107
  else:
108
+ if machine_token:
109
+ payload = jwt.decode(machine_token, algorithms=["HS256"], options={"verify_signature": False})
110
+ user_id = payload.get("sub", None)
111
+ request.state.user_id = user_id
112
+
100
113
  # 正常认证接口,设置用户信息
101
114
  request.state.user = user_info
102
115
  request.state.authenticated = True
@@ -142,15 +155,29 @@ class AuthMiddleware(BaseHTTPMiddleware):
142
155
  # 从Authorization头提取
143
156
  auth_header = request.headers.get(self.config.token_header)
144
157
  if auth_header and auth_header.startswith(self.config.token_prefix):
158
+ print("has auth_header: ", auth_header)
145
159
  return auth_header[len(self.config.token_prefix):].strip()
146
160
 
147
161
  # 从查询参数提取(备选方案)
148
162
  token = request.query_params.get("token")
149
163
  if token:
164
+ print("has token: ", token)
150
165
  return token
151
166
 
152
167
  return None
153
168
 
169
+ def _extract_machine_token(self, request: Request) -> Optional[str]:
170
+ """
171
+ 从请求中提取Token
172
+ """
173
+ # 从Authorization头提取
174
+ machine_token = request.headers.get("MACHINE-TOKEN")
175
+ if machine_token:
176
+ print("has MACHINE-TOKEN': ", machine_token)
177
+ return machine_token
178
+
179
+ return None
180
+
154
181
  async def _verify_token_and_permission(self, request: Request, token: Optional[str]) -> Optional[Dict[str, Any]]:
155
182
  """
156
183
  验证Token和权限
@@ -163,6 +190,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
163
190
  # 从请求头获取服务认证信息(可选)
164
191
  server_ak = request.headers.get("SERVER-AK", "")
165
192
  server_sk = request.headers.get("SERVER-SK", "")
193
+ machine_token = request.headers.get("MACHINE-TOKEN", "")
166
194
 
167
195
  # 调用IAM验证接口(即使token为空也要调用,因为可能是白名单接口)
168
196
  user_info = self.iam_client.verify_token(
@@ -170,7 +198,8 @@ class AuthMiddleware(BaseHTTPMiddleware):
170
198
  api=api_path,
171
199
  method=method,
172
200
  server_ak=server_ak,
173
- server_sk=server_sk
201
+ server_sk=server_sk,
202
+ machine_token=machine_token
174
203
  )
175
204
 
176
205
  return user_info
@@ -233,22 +262,24 @@ class AuthService:
233
262
  """验证token和权限"""
234
263
  # 通过token, server_ak, server_sk判断是否有权限
235
264
  api_path = request.url.path
236
-
265
+
237
266
  # 首先检查路径是否在白名单中
238
267
  if self.is_path_whitelisted(api_path):
239
268
  logger.info(f"路径 {api_path} 在白名单中,跳过IAM鉴权")
240
269
  return True
241
-
270
+
242
271
  credentials: HTTPAuthorizationCredentials = await self.security(request)
243
272
  method = request.method
244
273
 
245
274
  server_ak = request.headers.get("SERVER-AK", "")
246
275
  server_sk = request.headers.get("SERVER-SK", "")
276
+ machine_token = request.headers.get("MACHINE-TOKEN", "")
277
+ print("248 machine_token:", machine_token)
247
278
 
248
279
  token = ""
249
280
  if credentials is not None:
250
281
  token = credentials.credentials
251
- user_info_by_iam = self.iam_client.verify_token(token, api_path, method, server_ak, server_sk)
282
+ user_info_by_iam = self.iam_client.verify_token(token, api_path, method, server_ak, server_sk, machine_token)
252
283
  if user_info_by_iam:
253
284
  return True
254
285
  return False
@@ -264,7 +295,7 @@ class AuthService:
264
295
  credentials: HTTPAuthorizationCredentials = await self.security(request)
265
296
  if not credentials:
266
297
  return None
267
-
298
+
268
299
  token = credentials.credentials
269
300
 
270
301
  # 直接解析JWT token获取payload