skyplatform-iam 1.1.0__py3-none-any.whl → 1.2.1__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 +34 -1
- skyplatform_iam/config.py +34 -9
- skyplatform_iam/connect_agenterra_iam.py +99 -51
- skyplatform_iam/middleware.py +68 -24
- {skyplatform_iam-1.1.0.dist-info → skyplatform_iam-1.2.1.dist-info}/METADATA +1 -1
- skyplatform_iam-1.2.1.dist-info/RECORD +8 -0
- skyplatform_iam-1.1.0.dist-info/RECORD +0 -8
- {skyplatform_iam-1.1.0.dist-info → skyplatform_iam-1.2.1.dist-info}/WHEEL +0 -0
skyplatform_iam/__init__.py
CHANGED
|
@@ -17,7 +17,7 @@ from .exceptions import (
|
|
|
17
17
|
NetworkError
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
__version__ = "1.
|
|
20
|
+
__version__ = "1.2.1"
|
|
21
21
|
__author__ = "x9"
|
|
22
22
|
__description__ = "SkyPlatform IAM认证SDK,提供FastAPI中间件和IAM服务连接功能"
|
|
23
23
|
|
|
@@ -100,6 +100,9 @@ def init_skyplatform_iam(app, config: AuthConfig = None):
|
|
|
100
100
|
# 验证配置的完整性
|
|
101
101
|
config.validate_config()
|
|
102
102
|
|
|
103
|
+
# 配置SDK日志级别
|
|
104
|
+
_configure_sdk_logging(config.enable_sdk_logging)
|
|
105
|
+
|
|
103
106
|
# 初始化全局认证服务
|
|
104
107
|
setup_auth_middleware(config)
|
|
105
108
|
|
|
@@ -110,6 +113,36 @@ def init_skyplatform_iam(app, config: AuthConfig = None):
|
|
|
110
113
|
return middleware
|
|
111
114
|
|
|
112
115
|
|
|
116
|
+
def _configure_sdk_logging(enable_sdk_logging: bool):
|
|
117
|
+
"""
|
|
118
|
+
配置SDK日志级别
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
enable_sdk_logging: 是否启用SDK日志
|
|
122
|
+
"""
|
|
123
|
+
import logging
|
|
124
|
+
|
|
125
|
+
# SDK相关的logger名称列表
|
|
126
|
+
sdk_loggers = [
|
|
127
|
+
'skyplatform_iam.config',
|
|
128
|
+
'skyplatform_iam.middleware',
|
|
129
|
+
'skyplatform_iam.connect_agenterra_iam',
|
|
130
|
+
'skyplatform_iam.exceptions'
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
if enable_sdk_logging:
|
|
134
|
+
# 启用SDK日志,设置为INFO级别
|
|
135
|
+
log_level = logging.INFO
|
|
136
|
+
else:
|
|
137
|
+
# 禁用SDK日志,设置为WARNING级别,只显示警告和错误
|
|
138
|
+
log_level = logging.WARNING
|
|
139
|
+
|
|
140
|
+
# 配置所有SDK相关的logger
|
|
141
|
+
for logger_name in sdk_loggers:
|
|
142
|
+
logger = logging.getLogger(logger_name)
|
|
143
|
+
logger.setLevel(log_level)
|
|
144
|
+
|
|
145
|
+
|
|
113
146
|
def get_iam_client(config: AuthConfig = None) -> ConnectAgenterraIam:
|
|
114
147
|
"""
|
|
115
148
|
获取全局IAM客户端实例
|
skyplatform_iam/config.py
CHANGED
|
@@ -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,
|
|
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"
|
|
@@ -27,6 +32,9 @@ class AuthConfig(BaseModel):
|
|
|
27
32
|
|
|
28
33
|
# 错误处理配置
|
|
29
34
|
enable_debug: bool = False
|
|
35
|
+
|
|
36
|
+
# SDK日志控制配置
|
|
37
|
+
enable_sdk_logging: bool = True
|
|
30
38
|
|
|
31
39
|
# 白名单路径配置(实例变量)
|
|
32
40
|
whitelist_paths: List[str] = Field(default_factory=list)
|
|
@@ -44,6 +52,8 @@ class AuthConfig(BaseModel):
|
|
|
44
52
|
server_name=os.environ.get('AGENTERRA_SERVER_NAME', ''),
|
|
45
53
|
access_key=os.environ.get('AGENTERRA_ACCESS_KEY', ''),
|
|
46
54
|
enable_debug=os.environ.get('AGENTERRA_ENABLE_DEBUG', 'false').lower() == 'true',
|
|
55
|
+
enable_sdk_logging=os.environ.get('AGENTERRA_ENABLE_SDK_LOGGING', 'true').lower() == 'true',
|
|
56
|
+
machine_token=os.environ.get('MACHINE_TOKEN', ''),
|
|
47
57
|
whitelist_paths=[] # 初始化空的白名单路径列表
|
|
48
58
|
)
|
|
49
59
|
|
|
@@ -63,15 +73,15 @@ class AuthConfig(BaseModel):
|
|
|
63
73
|
"""
|
|
64
74
|
if not path:
|
|
65
75
|
return path
|
|
66
|
-
|
|
76
|
+
|
|
67
77
|
# 确保路径以 / 开头
|
|
68
78
|
if not path.startswith('/'):
|
|
69
79
|
path = '/' + path
|
|
70
|
-
|
|
80
|
+
|
|
71
81
|
# 移除重复的斜杠
|
|
72
82
|
while '//' in path:
|
|
73
83
|
path = path.replace('//', '/')
|
|
74
|
-
|
|
84
|
+
|
|
75
85
|
return path
|
|
76
86
|
|
|
77
87
|
def add_whitelist_path(self, path: str) -> None:
|
|
@@ -80,7 +90,7 @@ class AuthConfig(BaseModel):
|
|
|
80
90
|
"""
|
|
81
91
|
if not path:
|
|
82
92
|
return
|
|
83
|
-
|
|
93
|
+
|
|
84
94
|
normalized_path = self._normalize_path(path)
|
|
85
95
|
if normalized_path not in self.whitelist_paths:
|
|
86
96
|
self.whitelist_paths.append(normalized_path)
|
|
@@ -98,7 +108,7 @@ class AuthConfig(BaseModel):
|
|
|
98
108
|
"""
|
|
99
109
|
if not path:
|
|
100
110
|
return
|
|
101
|
-
|
|
111
|
+
|
|
102
112
|
normalized_path = self._normalize_path(path)
|
|
103
113
|
if normalized_path in self.whitelist_paths:
|
|
104
114
|
self.whitelist_paths.remove(normalized_path)
|
|
@@ -121,12 +131,27 @@ class AuthConfig(BaseModel):
|
|
|
121
131
|
"""
|
|
122
132
|
if not path:
|
|
123
133
|
return False
|
|
124
|
-
|
|
134
|
+
|
|
125
135
|
normalized_path = self._normalize_path(path)
|
|
126
|
-
|
|
136
|
+
|
|
127
137
|
for whitelist_path in self.whitelist_paths:
|
|
128
138
|
# 支持通配符匹配
|
|
129
139
|
if fnmatch.fnmatch(normalized_path, whitelist_path):
|
|
130
140
|
return True
|
|
131
|
-
|
|
141
|
+
|
|
132
142
|
return False
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def decode_jwt_token(token: str) -> Optional[Dict]:
|
|
146
|
+
"""直接解析JWT token获取payload"""
|
|
147
|
+
try:
|
|
148
|
+
# 不验证签名,只解析payload(因为token已经通过verify_token验证过)
|
|
149
|
+
decoded_payload = jwt.decode(token, options={"verify_signature": False})
|
|
150
|
+
logger.debug(f"JWT token解析成功: {decoded_payload}")
|
|
151
|
+
return decoded_payload
|
|
152
|
+
except jwt.InvalidTokenError as e:
|
|
153
|
+
logger.error(f"JWT token解析失败: {str(e)}")
|
|
154
|
+
return None
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.error(f"JWT token解析异常: {str(e)}")
|
|
157
|
+
return None
|
|
@@ -2,9 +2,16 @@ import requests
|
|
|
2
2
|
import logging
|
|
3
3
|
import traceback
|
|
4
4
|
import copy
|
|
5
|
+
import urllib3
|
|
5
6
|
from enum import Enum
|
|
6
7
|
from fastapi import HTTPException, status
|
|
7
8
|
|
|
9
|
+
from .config import decode_jwt_token
|
|
10
|
+
|
|
11
|
+
# 禁用 urllib3 的 InsecureRequestWarning 警告
|
|
12
|
+
# 这样可以避免在使用自定义 HTTPS 地址(可能没有有效证书)时出现警告信息
|
|
13
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
14
|
+
|
|
8
15
|
|
|
9
16
|
class CredentialTypeEnum(str, Enum):
|
|
10
17
|
"""凭证类型枚举,与后端API保持一致"""
|
|
@@ -17,7 +24,7 @@ class CredentialTypeEnum(str, Enum):
|
|
|
17
24
|
class ConnectAgenterraIam(object):
|
|
18
25
|
_instance = None
|
|
19
26
|
_initialized = False
|
|
20
|
-
|
|
27
|
+
|
|
21
28
|
def __new__(cls, config=None, logger_name="skyplatform_iam", log_level=logging.INFO):
|
|
22
29
|
"""
|
|
23
30
|
单例模式实现
|
|
@@ -26,11 +33,11 @@ class ConnectAgenterraIam(object):
|
|
|
26
33
|
if cls._instance is None:
|
|
27
34
|
cls._instance = super(ConnectAgenterraIam, cls).__new__(cls)
|
|
28
35
|
return cls._instance
|
|
29
|
-
|
|
36
|
+
|
|
30
37
|
def __init__(self, config=None, logger_name="skyplatform_iam", log_level=logging.INFO):
|
|
31
38
|
"""
|
|
32
39
|
初始化AgenterraIAM连接器
|
|
33
|
-
|
|
40
|
+
|
|
34
41
|
参数:
|
|
35
42
|
- config: AuthConfig配置对象,如果为None则从环境变量读取
|
|
36
43
|
- logger_name: 日志记录器名称
|
|
@@ -39,7 +46,22 @@ class ConnectAgenterraIam(object):
|
|
|
39
46
|
# 防止重复初始化
|
|
40
47
|
if self._initialized:
|
|
41
48
|
return
|
|
42
|
-
|
|
49
|
+
|
|
50
|
+
# 必须传入config参数,不再支持从环境变量读取
|
|
51
|
+
if config is None:
|
|
52
|
+
raise ValueError("必须传入AuthConfig配置对象,不再支持从环境变量读取配置")
|
|
53
|
+
|
|
54
|
+
# 保存配置对象,用于日志控制
|
|
55
|
+
self.config = config
|
|
56
|
+
|
|
57
|
+
# 根据配置设置日志级别
|
|
58
|
+
if hasattr(config, 'enable_sdk_logging') and not config.enable_sdk_logging:
|
|
59
|
+
# 禁用SDK日志时,设置为WARNING级别,只显示警告和错误
|
|
60
|
+
effective_log_level = logging.WARNING
|
|
61
|
+
else:
|
|
62
|
+
# 启用SDK日志时,使用传入的日志级别
|
|
63
|
+
effective_log_level = log_level
|
|
64
|
+
|
|
43
65
|
# 配置日志记录器
|
|
44
66
|
self.logger = logging.getLogger(logger_name)
|
|
45
67
|
if not self.logger.handlers:
|
|
@@ -49,17 +71,16 @@ class ConnectAgenterraIam(object):
|
|
|
49
71
|
)
|
|
50
72
|
handler.setFormatter(formatter)
|
|
51
73
|
self.logger.addHandler(handler)
|
|
52
|
-
self.logger.setLevel(
|
|
53
|
-
|
|
54
|
-
# 必须传入config参数,不再支持从环境变量读取
|
|
55
|
-
if config is None:
|
|
56
|
-
raise ValueError("必须传入AuthConfig配置对象,不再支持从环境变量读取配置")
|
|
57
|
-
|
|
74
|
+
self.logger.setLevel(effective_log_level)
|
|
75
|
+
|
|
58
76
|
self.agenterra_iam_host = config.agenterra_iam_host
|
|
59
77
|
self.server_name = config.server_name
|
|
60
78
|
self.access_key = config.access_key
|
|
61
|
-
self.
|
|
79
|
+
self.machine_token = config.machine_token
|
|
62
80
|
|
|
81
|
+
if hasattr(config, 'enable_sdk_logging') and config.enable_sdk_logging:
|
|
82
|
+
self.logger.info("使用传入的AuthConfig配置")
|
|
83
|
+
|
|
63
84
|
# 验证必要的配置
|
|
64
85
|
if not self.agenterra_iam_host:
|
|
65
86
|
self.logger.warning("AGENTERRA_IAM_HOST 配置未设置")
|
|
@@ -67,9 +88,11 @@ class ConnectAgenterraIam(object):
|
|
|
67
88
|
self.logger.warning("AGENTERRA_SERVER_NAME 配置未设置")
|
|
68
89
|
if not self.access_key:
|
|
69
90
|
self.logger.warning("AGENTERRA_ACCESS_KEY 配置未设置")
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
91
|
+
|
|
92
|
+
if hasattr(config, 'enable_sdk_logging') and config.enable_sdk_logging:
|
|
93
|
+
self.logger.info(
|
|
94
|
+
f"初始化AgenterraIAM连接器 - Host: {self.agenterra_iam_host}, Server: {self._mask_sensitive(self.server_name)}")
|
|
95
|
+
|
|
73
96
|
self.headers = {
|
|
74
97
|
"Content-Type": "application/json",
|
|
75
98
|
"SERVER-AK": self.server_name,
|
|
@@ -77,9 +100,10 @@ class ConnectAgenterraIam(object):
|
|
|
77
100
|
}
|
|
78
101
|
self.body = {
|
|
79
102
|
"server_name": self.server_name,
|
|
80
|
-
"access_key": self.access_key
|
|
103
|
+
"access_key": self.access_key,
|
|
104
|
+
"machine_token": self.machine_token
|
|
81
105
|
}
|
|
82
|
-
|
|
106
|
+
|
|
83
107
|
# 标记为已初始化
|
|
84
108
|
self._initialized = True
|
|
85
109
|
|
|
@@ -87,20 +111,20 @@ class ConnectAgenterraIam(object):
|
|
|
87
111
|
"""
|
|
88
112
|
重新加载配置
|
|
89
113
|
用于在运行时更新配置
|
|
90
|
-
|
|
114
|
+
|
|
91
115
|
参数:
|
|
92
116
|
- config: AuthConfig配置对象
|
|
93
117
|
"""
|
|
94
118
|
if config is None:
|
|
95
119
|
raise ValueError("必须传入AuthConfig配置对象")
|
|
96
|
-
|
|
120
|
+
|
|
97
121
|
self.logger.info("重新加载配置")
|
|
98
|
-
|
|
122
|
+
|
|
99
123
|
# 更新配置
|
|
100
124
|
self.agenterra_iam_host = config.agenterra_iam_host
|
|
101
125
|
self.server_name = config.server_name
|
|
102
126
|
self.access_key = config.access_key
|
|
103
|
-
|
|
127
|
+
|
|
104
128
|
# 验证必要的配置
|
|
105
129
|
if not self.agenterra_iam_host:
|
|
106
130
|
self.logger.warning("AGENTERRA_IAM_HOST 配置未设置")
|
|
@@ -108,7 +132,7 @@ class ConnectAgenterraIam(object):
|
|
|
108
132
|
self.logger.warning("AGENTERRA_SERVER_NAME 配置未设置")
|
|
109
133
|
if not self.access_key:
|
|
110
134
|
self.logger.warning("AGENTERRA_ACCESS_KEY 配置未设置")
|
|
111
|
-
|
|
135
|
+
|
|
112
136
|
# 更新headers和body
|
|
113
137
|
self.headers = {
|
|
114
138
|
"Content-Type": "application/json",
|
|
@@ -117,77 +141,83 @@ class ConnectAgenterraIam(object):
|
|
|
117
141
|
}
|
|
118
142
|
self.body = {
|
|
119
143
|
"server_name": self.server_name,
|
|
120
|
-
"access_key": self.access_key
|
|
144
|
+
"access_key": self.access_key,
|
|
145
|
+
"machine_token": self.machine_token,
|
|
121
146
|
}
|
|
122
|
-
|
|
123
|
-
self.logger.info(
|
|
147
|
+
|
|
148
|
+
self.logger.info(
|
|
149
|
+
f"配置重新加载完成 - Host: {self.agenterra_iam_host}, Server: {self._mask_sensitive(self.server_name)}")
|
|
124
150
|
|
|
125
151
|
def _mask_sensitive(self, value, mask_char="*", show_chars=4):
|
|
126
152
|
"""
|
|
127
153
|
脱敏处理敏感信息
|
|
128
|
-
|
|
154
|
+
|
|
129
155
|
参数:
|
|
130
156
|
- value: 要脱敏的值
|
|
131
157
|
- mask_char: 脱敏字符
|
|
132
158
|
- show_chars: 显示的字符数量
|
|
133
|
-
|
|
159
|
+
|
|
134
160
|
返回: 脱敏后的字符串
|
|
135
161
|
"""
|
|
136
162
|
if not value or not isinstance(value, str):
|
|
137
163
|
return str(value) if value else "None"
|
|
138
|
-
|
|
164
|
+
|
|
139
165
|
if len(value) <= show_chars:
|
|
140
166
|
return mask_char * len(value)
|
|
141
|
-
|
|
167
|
+
|
|
142
168
|
return value[:show_chars] + mask_char * (len(value) - show_chars)
|
|
143
169
|
|
|
144
170
|
def _sanitize_log_data(self, data):
|
|
145
171
|
"""
|
|
146
172
|
清理日志数据,脱敏敏感信息
|
|
147
|
-
|
|
173
|
+
|
|
148
174
|
参数:
|
|
149
175
|
- data: 要清理的数据(字典或其他类型)
|
|
150
|
-
|
|
176
|
+
|
|
151
177
|
返回: 清理后的数据
|
|
152
178
|
"""
|
|
153
179
|
if not isinstance(data, dict):
|
|
154
180
|
return data
|
|
155
|
-
|
|
181
|
+
|
|
156
182
|
# 需要脱敏的字段列表
|
|
157
183
|
sensitive_fields = [
|
|
158
|
-
'password', 'access_key', 'token', 'refresh_token',
|
|
184
|
+
'password', 'access_key', 'token', 'refresh_token',
|
|
159
185
|
'SERVER-SK', 'new_password', 'server_sk'
|
|
160
186
|
]
|
|
161
|
-
|
|
187
|
+
|
|
162
188
|
sanitized = copy.deepcopy(data)
|
|
163
|
-
|
|
189
|
+
|
|
164
190
|
for key, value in sanitized.items():
|
|
165
191
|
if key.lower() in [field.lower() for field in sensitive_fields]:
|
|
166
192
|
sanitized[key] = self._mask_sensitive(str(value))
|
|
167
193
|
elif isinstance(value, dict):
|
|
168
194
|
sanitized[key] = self._sanitize_log_data(value)
|
|
169
|
-
|
|
195
|
+
|
|
170
196
|
return sanitized
|
|
171
197
|
|
|
172
198
|
def _log_request(self, method_name, url, headers, body):
|
|
173
199
|
"""记录请求信息"""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
# 只有在启用SDK日志时才输出详细的请求信息
|
|
201
|
+
if hasattr(self, 'config') and hasattr(self.config, 'enable_sdk_logging') and self.config.enable_sdk_logging:
|
|
202
|
+
sanitized_headers = self._sanitize_log_data(headers)
|
|
203
|
+
sanitized_body = self._sanitize_log_data(body)
|
|
204
|
+
|
|
205
|
+
self.logger.info(f"[{method_name}] 发送请求 - URL: {url}")
|
|
206
|
+
self.logger.info(f"[{method_name}] 请求头: {sanitized_headers}")
|
|
207
|
+
self.logger.info(f"[{method_name}] 请求体: {sanitized_body}")
|
|
180
208
|
|
|
181
209
|
def _log_response(self, method_name, response):
|
|
182
210
|
"""记录响应信息"""
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
211
|
+
# 只有在启用SDK日志时才输出详细的响应信息
|
|
212
|
+
if hasattr(self, 'config') and hasattr(self.config, 'enable_sdk_logging') and self.config.enable_sdk_logging:
|
|
213
|
+
try:
|
|
214
|
+
response_data = response.json() if response.content else {}
|
|
215
|
+
sanitized_response = self._sanitize_log_data(response_data)
|
|
216
|
+
self.logger.info(f"[{method_name}] 响应状态码: {response.status_code}")
|
|
217
|
+
self.logger.info(f"[{method_name}] 响应内容: {sanitized_response}")
|
|
218
|
+
except Exception as e:
|
|
219
|
+
self.logger.info(f"[{method_name}] 响应状态码: {response.status_code}")
|
|
220
|
+
self.logger.info(f"[{method_name}] 响应内容解析失败: {str(e)}")
|
|
191
221
|
|
|
192
222
|
def register(self, cred_type=None, cred_value=None, password=None, nickname=None, avatar_url=None,
|
|
193
223
|
username=None, phone=None):
|
|
@@ -346,6 +376,11 @@ class ConnectAgenterraIam(object):
|
|
|
346
376
|
|
|
347
377
|
if response.status_code == 200:
|
|
348
378
|
self.logger.info(f"[{method_name}] 密码登录成功")
|
|
379
|
+
data = response.json()["data"]
|
|
380
|
+
access_token = data["access_token"]
|
|
381
|
+
payload = decode_jwt_token(access_token)
|
|
382
|
+
self.machine_token = payload["machine_access_token"]
|
|
383
|
+
|
|
349
384
|
return response
|
|
350
385
|
else:
|
|
351
386
|
self.logger.warning(f"[{method_name}] 密码登录失败 - 状态码: {response.status_code}")
|
|
@@ -452,9 +487,12 @@ class ConnectAgenterraIam(object):
|
|
|
452
487
|
# 记录请求信息
|
|
453
488
|
self._log_request(method_name, url, self.headers, body)
|
|
454
489
|
|
|
490
|
+
headers = self.headers.copy()
|
|
491
|
+
headers['MACHINE-TOKEN'] = self.machine_token
|
|
492
|
+
|
|
455
493
|
response = requests.post(
|
|
456
494
|
url=url,
|
|
457
|
-
headers=
|
|
495
|
+
headers=headers,
|
|
458
496
|
json=body,
|
|
459
497
|
verify=False
|
|
460
498
|
)
|
|
@@ -474,7 +512,7 @@ class ConnectAgenterraIam(object):
|
|
|
474
512
|
self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
|
|
475
513
|
return False
|
|
476
514
|
|
|
477
|
-
def verify_token(self, token, api, method, server_ak="", server_sk=""):
|
|
515
|
+
def verify_token(self, token, api, method, server_ak="", server_sk="", machine_token=""):
|
|
478
516
|
"""
|
|
479
517
|
请求iam进行鉴权
|
|
480
518
|
server_name: 服务名称
|
|
@@ -494,6 +532,7 @@ class ConnectAgenterraIam(object):
|
|
|
494
532
|
body = {
|
|
495
533
|
"server_name": self.server_name,
|
|
496
534
|
"access_key": self.access_key,
|
|
535
|
+
"machine_token": machine_token if machine_token else self.machine_token,
|
|
497
536
|
"token": token,
|
|
498
537
|
"api": api,
|
|
499
538
|
"method": method,
|
|
@@ -600,6 +639,7 @@ class ConnectAgenterraIam(object):
|
|
|
600
639
|
body = {
|
|
601
640
|
"server_name": self.server_name,
|
|
602
641
|
"access_key": self.access_key,
|
|
642
|
+
"machine_token": self.machine_token,
|
|
603
643
|
"user_id": user_id,
|
|
604
644
|
"new_password": new_password
|
|
605
645
|
}
|
|
@@ -647,6 +687,7 @@ class ConnectAgenterraIam(object):
|
|
|
647
687
|
body = {
|
|
648
688
|
"server_name": self.server_name,
|
|
649
689
|
"access_key": self.access_key,
|
|
690
|
+
"machine_token": self.machine_token,
|
|
650
691
|
"refresh_token": refresh_token
|
|
651
692
|
}
|
|
652
693
|
uri = "/api/v2/service/refresh_token"
|
|
@@ -694,6 +735,7 @@ class ConnectAgenterraIam(object):
|
|
|
694
735
|
body = {
|
|
695
736
|
"server_name": self.server_name,
|
|
696
737
|
"access_key": self.access_key,
|
|
738
|
+
"machine_token": self.machine_token,
|
|
697
739
|
"user_id": user_id,
|
|
698
740
|
"role_id": role_id
|
|
699
741
|
}
|
|
@@ -736,6 +778,7 @@ class ConnectAgenterraIam(object):
|
|
|
736
778
|
body = {
|
|
737
779
|
"server_name": self.server_name,
|
|
738
780
|
"access_key": self.access_key,
|
|
781
|
+
"machine_token": self.machine_token,
|
|
739
782
|
"token": token,
|
|
740
783
|
}
|
|
741
784
|
uri = "/api/v2/service/token"
|
|
@@ -788,6 +831,7 @@ class ConnectAgenterraIam(object):
|
|
|
788
831
|
body = {
|
|
789
832
|
"server_name": self.server_name,
|
|
790
833
|
"access_key": self.access_key,
|
|
834
|
+
"machine_token": self.machine_token,
|
|
791
835
|
"user_id": user_id,
|
|
792
836
|
"config_name": config_name
|
|
793
837
|
}
|
|
@@ -844,6 +888,7 @@ class ConnectAgenterraIam(object):
|
|
|
844
888
|
body = {
|
|
845
889
|
"server_name": self.server_name,
|
|
846
890
|
"access_key": self.access_key,
|
|
891
|
+
"machine_token": self.machine_token,
|
|
847
892
|
"user_id": user_id
|
|
848
893
|
}
|
|
849
894
|
|
|
@@ -896,6 +941,7 @@ class ConnectAgenterraIam(object):
|
|
|
896
941
|
body = {
|
|
897
942
|
"server_name": self.server_name,
|
|
898
943
|
"access_key": self.access_key,
|
|
944
|
+
"machine_token": self.machine_token,
|
|
899
945
|
"user_id": user_id,
|
|
900
946
|
"config_name": config_name
|
|
901
947
|
}
|
|
@@ -957,6 +1003,7 @@ class ConnectAgenterraIam(object):
|
|
|
957
1003
|
body = {
|
|
958
1004
|
"server_name": self.server_name,
|
|
959
1005
|
"access_key": self.access_key,
|
|
1006
|
+
"machine_token": self.machine_token,
|
|
960
1007
|
"target_user_id": target_user_id,
|
|
961
1008
|
"cred_type": cred_type.value,
|
|
962
1009
|
"cred_value": cred_value
|
|
@@ -1021,6 +1068,7 @@ class ConnectAgenterraIam(object):
|
|
|
1021
1068
|
body = {
|
|
1022
1069
|
"server_name": self.server_name,
|
|
1023
1070
|
"access_key": self.access_key,
|
|
1071
|
+
"machine_token": self.machine_token,
|
|
1024
1072
|
"cred_type": cred_type.value,
|
|
1025
1073
|
"cred_value": cred_value
|
|
1026
1074
|
}
|
skyplatform_iam/middleware.py
CHANGED
|
@@ -66,10 +66,18 @@ 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
|
+
if self.config.enable_sdk_logging:
|
|
80
|
+
logger.info(f"路径 {api_path} 在本地白名单中,跳过认证直接允许访问")
|
|
73
81
|
# 设置白名单标识
|
|
74
82
|
request.state.user = None
|
|
75
83
|
request.state.authenticated = False
|
|
@@ -80,6 +88,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
80
88
|
|
|
81
89
|
# 提取Token(可能为空,白名单接口不需要token)
|
|
82
90
|
token = self._extract_token(request)
|
|
91
|
+
machine_token = self._extract_machine_token(request)
|
|
83
92
|
|
|
84
93
|
# 验证Token和权限(即使token为空也要调用IAM验证,因为可能是白名单接口)
|
|
85
94
|
user_info = await self._verify_token_and_permission(request, token)
|
|
@@ -97,6 +106,16 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
97
106
|
request.state.authenticated = False
|
|
98
107
|
request.state.is_whitelist = True
|
|
99
108
|
else:
|
|
109
|
+
if machine_token:
|
|
110
|
+
try:
|
|
111
|
+
payload = jwt.decode(machine_token, algorithms=["HS256"], options={"verify_signature": False})
|
|
112
|
+
user_id = payload.get("sub", None)
|
|
113
|
+
request.state.user_id = user_id
|
|
114
|
+
except Exception as jwt_error:
|
|
115
|
+
if self.config.enable_sdk_logging:
|
|
116
|
+
logger.error(f"JWT解码失败: {str(jwt_error)}")
|
|
117
|
+
# JWT解码失败时不设置user_id,但继续处理
|
|
118
|
+
|
|
100
119
|
# 正常认证接口,设置用户信息
|
|
101
120
|
request.state.user = user_info
|
|
102
121
|
request.state.authenticated = True
|
|
@@ -125,9 +144,10 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
125
144
|
detail=e.detail
|
|
126
145
|
)
|
|
127
146
|
except Exception as e:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
if self.config.enable_sdk_logging:
|
|
148
|
+
logger.error(f"认证中间件处理异常: {str(e)}")
|
|
149
|
+
if self.config.enable_debug:
|
|
150
|
+
logger.exception("详细异常信息:")
|
|
131
151
|
|
|
132
152
|
return self._create_error_response(
|
|
133
153
|
status_code=500,
|
|
@@ -151,6 +171,17 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
151
171
|
|
|
152
172
|
return None
|
|
153
173
|
|
|
174
|
+
def _extract_machine_token(self, request: Request) -> Optional[str]:
|
|
175
|
+
"""
|
|
176
|
+
从请求中提取Token
|
|
177
|
+
"""
|
|
178
|
+
# 从Authorization头提取
|
|
179
|
+
machine_token = request.headers.get("MACHINE-TOKEN")
|
|
180
|
+
if machine_token:
|
|
181
|
+
return machine_token
|
|
182
|
+
|
|
183
|
+
return None
|
|
184
|
+
|
|
154
185
|
async def _verify_token_and_permission(self, request: Request, token: Optional[str]) -> Optional[Dict[str, Any]]:
|
|
155
186
|
"""
|
|
156
187
|
验证Token和权限
|
|
@@ -163,6 +194,7 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
163
194
|
# 从请求头获取服务认证信息(可选)
|
|
164
195
|
server_ak = request.headers.get("SERVER-AK", "")
|
|
165
196
|
server_sk = request.headers.get("SERVER-SK", "")
|
|
197
|
+
machine_token = request.headers.get("MACHINE-TOKEN", "")
|
|
166
198
|
|
|
167
199
|
# 调用IAM验证接口(即使token为空也要调用,因为可能是白名单接口)
|
|
168
200
|
user_info = self.iam_client.verify_token(
|
|
@@ -170,7 +202,8 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
170
202
|
api=api_path,
|
|
171
203
|
method=method,
|
|
172
204
|
server_ak=server_ak,
|
|
173
|
-
server_sk=server_sk
|
|
205
|
+
server_sk=server_sk,
|
|
206
|
+
machine_token=machine_token
|
|
174
207
|
)
|
|
175
208
|
|
|
176
209
|
return user_info
|
|
@@ -179,9 +212,10 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|
|
179
212
|
# 重新抛出HTTP异常
|
|
180
213
|
raise
|
|
181
214
|
except Exception as e:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
215
|
+
if self.config.enable_sdk_logging:
|
|
216
|
+
logger.error(f"Token验证异常: {str(e)}")
|
|
217
|
+
if self.config.enable_debug:
|
|
218
|
+
logger.exception("详细异常信息:")
|
|
185
219
|
return None
|
|
186
220
|
|
|
187
221
|
def _create_error_response(
|
|
@@ -233,22 +267,24 @@ class AuthService:
|
|
|
233
267
|
"""验证token和权限"""
|
|
234
268
|
# 通过token, server_ak, server_sk判断是否有权限
|
|
235
269
|
api_path = request.url.path
|
|
236
|
-
|
|
270
|
+
|
|
237
271
|
# 首先检查路径是否在白名单中
|
|
238
272
|
if self.is_path_whitelisted(api_path):
|
|
239
|
-
|
|
273
|
+
if self.auth_config.enable_sdk_logging:
|
|
274
|
+
logger.info(f"路径 {api_path} 在白名单中,跳过IAM鉴权")
|
|
240
275
|
return True
|
|
241
|
-
|
|
276
|
+
|
|
242
277
|
credentials: HTTPAuthorizationCredentials = await self.security(request)
|
|
243
278
|
method = request.method
|
|
244
279
|
|
|
245
280
|
server_ak = request.headers.get("SERVER-AK", "")
|
|
246
281
|
server_sk = request.headers.get("SERVER-SK", "")
|
|
282
|
+
machine_token = request.headers.get("MACHINE-TOKEN", "")
|
|
247
283
|
|
|
248
284
|
token = ""
|
|
249
285
|
if credentials is not None:
|
|
250
286
|
token = credentials.credentials
|
|
251
|
-
user_info_by_iam = self.iam_client.verify_token(token, api_path, method, server_ak, server_sk)
|
|
287
|
+
user_info_by_iam = self.iam_client.verify_token(token, api_path, method, server_ak, server_sk, machine_token)
|
|
252
288
|
if user_info_by_iam:
|
|
253
289
|
return True
|
|
254
290
|
return False
|
|
@@ -264,13 +300,14 @@ class AuthService:
|
|
|
264
300
|
credentials: HTTPAuthorizationCredentials = await self.security(request)
|
|
265
301
|
if not credentials:
|
|
266
302
|
return None
|
|
267
|
-
|
|
303
|
+
|
|
268
304
|
token = credentials.credentials
|
|
269
305
|
|
|
270
306
|
# 直接解析JWT token获取payload
|
|
271
307
|
payload = self.decode_jwt_token(token)
|
|
272
308
|
if not payload:
|
|
273
|
-
|
|
309
|
+
if self.auth_config.enable_sdk_logging:
|
|
310
|
+
logger.error("JWT token解析失败")
|
|
274
311
|
return None
|
|
275
312
|
|
|
276
313
|
# 从payload中提取用户信息
|
|
@@ -326,20 +363,23 @@ class AuthService:
|
|
|
326
363
|
cred_types = [cred.get("type") for cred in all_credentials]
|
|
327
364
|
cred_type_count = {cred_type: cred_types.count(cred_type) for cred_type in set(cred_types)}
|
|
328
365
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
366
|
+
if self.auth_config.enable_sdk_logging:
|
|
367
|
+
logger.info(
|
|
368
|
+
f"用户认证成功: user_id={iam_user_id}, username={username}, 凭证数量={total_credentials}, 凭证类型分布={cred_type_count}")
|
|
369
|
+
logger.debug(f"JWT payload: {payload}")
|
|
332
370
|
|
|
333
371
|
# 将用户信息添加到请求状态中
|
|
334
372
|
request.state.user = user_info
|
|
335
373
|
return user_info
|
|
336
374
|
|
|
337
375
|
except HTTPException as e:
|
|
338
|
-
|
|
376
|
+
if self.auth_config.enable_sdk_logging:
|
|
377
|
+
logger.error(f"获取当前用户信息失败: {str(e)}")
|
|
339
378
|
# 重新抛出HTTP异常(403权限不足)
|
|
340
379
|
return None
|
|
341
380
|
except Exception as e:
|
|
342
|
-
|
|
381
|
+
if self.auth_config.enable_sdk_logging:
|
|
382
|
+
logger.error(f"获取当前用户信息失败: {str(e)}")
|
|
343
383
|
return None
|
|
344
384
|
|
|
345
385
|
async def require_auth(self, request: Request) -> Dict:
|
|
@@ -371,13 +411,16 @@ class AuthService:
|
|
|
371
411
|
try:
|
|
372
412
|
# 不验证签名,只解析payload(因为token已经通过verify_token验证过)
|
|
373
413
|
decoded_payload = jwt.decode(token, options={"verify_signature": False})
|
|
374
|
-
|
|
414
|
+
if self.auth_config.enable_sdk_logging:
|
|
415
|
+
logger.debug(f"JWT token解析成功: {decoded_payload}")
|
|
375
416
|
return decoded_payload
|
|
376
417
|
except jwt.InvalidTokenError as e:
|
|
377
|
-
|
|
418
|
+
if self.auth_config.enable_sdk_logging:
|
|
419
|
+
logger.error(f"JWT token解析失败: {str(e)}")
|
|
378
420
|
return None
|
|
379
421
|
except Exception as e:
|
|
380
|
-
|
|
422
|
+
if self.auth_config.enable_sdk_logging:
|
|
423
|
+
logger.error(f"JWT token解析异常: {str(e)}")
|
|
381
424
|
return None
|
|
382
425
|
|
|
383
426
|
|
|
@@ -394,7 +437,8 @@ def setup_auth_middleware(auth_config: AuthConfig) -> None:
|
|
|
394
437
|
"""
|
|
395
438
|
global auth_service
|
|
396
439
|
auth_service = AuthService(auth_config)
|
|
397
|
-
|
|
440
|
+
if auth_config.enable_sdk_logging:
|
|
441
|
+
logger.info(f"认证中间件已配置,白名单路径数量: {len(auth_config.get_whitelist_paths())}")
|
|
398
442
|
|
|
399
443
|
|
|
400
444
|
# 便捷的依赖函数
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: skyplatform-iam
|
|
3
|
-
Version: 1.1
|
|
3
|
+
Version: 1.2.1
|
|
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/
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
skyplatform_iam/__init__.py,sha256=6ffSGMRItbBPnxgQMaoeB1oGLfXuMK3HHrspuiqyO8s,5591
|
|
2
|
+
skyplatform_iam/config.py,sha256=x3GBikMkC8-0og0Vhs6h5_nTcBXeEd8euDgVDd7IsK4,4607
|
|
3
|
+
skyplatform_iam/connect_agenterra_iam.py,sha256=t03IdWCwBP1oOF1WBTSZETB4MUKQ7zTmfvc6zXD83fg,42050
|
|
4
|
+
skyplatform_iam/exceptions.py,sha256=Rt55QIzVK1F_kn6yzKQKKakD6PZDFdPLCGaCphKKms8,2166
|
|
5
|
+
skyplatform_iam/middleware.py,sha256=jXX8_UciUhOkbK418anqtJoclNhvTQeFnguwPjv5K9E,17863
|
|
6
|
+
skyplatform_iam-1.2.1.dist-info/METADATA,sha256=HLkBft7Qp6w9NFpsHtkVJWFXLcHNZDJ_7_AaXYprZ6E,12658
|
|
7
|
+
skyplatform_iam-1.2.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
skyplatform_iam-1.2.1.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
skyplatform_iam/__init__.py,sha256=JfCqvHdzjiwkEVhgzlB45pKKs0VkxKg8yrMMQ4NpWpY,4664
|
|
2
|
-
skyplatform_iam/config.py,sha256=s4tctVpguKZv4O1Fhf7_Fo7zELNX6KYviMjkE1WPbQM,3715
|
|
3
|
-
skyplatform_iam/connect_agenterra_iam.py,sha256=9SnZkYmZ1VewzIz6a3-uOdOLzHrhPyJPIofZHgImJCM,39785
|
|
4
|
-
skyplatform_iam/exceptions.py,sha256=Rt55QIzVK1F_kn6yzKQKKakD6PZDFdPLCGaCphKKms8,2166
|
|
5
|
-
skyplatform_iam/middleware.py,sha256=XNJxvjw3O55TW-ff_uORK-C9Wy4BTAfkcnNjy1SQkx0,15721
|
|
6
|
-
skyplatform_iam-1.1.0.dist-info/METADATA,sha256=K3G-22HfmCKyzHOK_zvrpGDzez_gpEvrqon7nsvG4wg,12658
|
|
7
|
-
skyplatform_iam-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
-
skyplatform_iam-1.1.0.dist-info/RECORD,,
|
|
File without changes
|