skyplatform-iam 1.0.1__py3-none-any.whl → 1.0.5__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/config.py CHANGED
@@ -3,18 +3,21 @@ SkyPlatform IAM SDK 配置模块
3
3
  """
4
4
  import os
5
5
  import fnmatch
6
- from typing import Optional, List
7
- from pydantic import BaseModel, Field
6
+ import logging
7
+ from typing import Optional, List, Dict, Any
8
+ from pydantic import BaseModel, Field, validator
8
9
  from dotenv import load_dotenv
9
10
 
10
11
  # 加载环境变量
11
12
  load_dotenv()
12
13
 
14
+ logger = logging.getLogger(__name__)
15
+
13
16
 
14
17
  class AuthConfig(BaseModel):
15
18
  """
16
19
  认证配置类
17
- 支持环境变量和代码配置
20
+ 支持环境变量和代码配置,增强配置验证和管理功能
18
21
  """
19
22
  # IAM服务配置
20
23
  agenterra_iam_host: str
@@ -30,32 +33,184 @@ class AuthConfig(BaseModel):
30
33
 
31
34
  # 白名单路径配置(实例变量)
32
35
  whitelist_paths: List[str] = Field(default_factory=list)
36
+
37
+ # 连接配置
38
+ timeout: int = 30
39
+ max_retries: int = 3
40
+
41
+ # 缓存配置
42
+ enable_cache: bool = True
43
+ cache_ttl: int = 300 # 5分钟
33
44
 
34
45
  class Config:
35
- env_prefix = "AGENTERRA_"
46
+ env_prefix = "SKYPLATFORM_"
47
+ validate_assignment = True
48
+
49
+ @validator('agenterra_iam_host')
50
+ def validate_iam_host(cls, v):
51
+ """验证IAM主机地址"""
52
+ if not v:
53
+ raise ValueError("agenterra_iam_host不能为空")
54
+ if not (v.startswith('http://') or v.startswith('https://')):
55
+ raise ValueError("agenterra_iam_host必须以http://或https://开头")
56
+ return v.rstrip('/') # 移除末尾的斜杠
57
+
58
+ @validator('server_name')
59
+ def validate_server_name(cls, v):
60
+ """验证服务名称"""
61
+ if not v or not v.strip():
62
+ raise ValueError("server_name不能为空")
63
+ return v.strip()
64
+
65
+ @validator('access_key')
66
+ def validate_access_key(cls, v):
67
+ """验证访问密钥"""
68
+ if not v or not v.strip():
69
+ raise ValueError("access_key不能为空")
70
+ if len(v.strip()) < 8:
71
+ raise ValueError("access_key长度不能少于8个字符")
72
+ return v.strip()
73
+
74
+ @validator('timeout')
75
+ def validate_timeout(cls, v):
76
+ """验证超时时间"""
77
+ if v <= 0:
78
+ raise ValueError("timeout必须大于0")
79
+ return v
80
+
81
+ @validator('max_retries')
82
+ def validate_max_retries(cls, v):
83
+ """验证最大重试次数"""
84
+ if v < 0:
85
+ raise ValueError("max_retries不能小于0")
86
+ return v
87
+
88
+ @validator('cache_ttl')
89
+ def validate_cache_ttl(cls, v):
90
+ """验证缓存TTL"""
91
+ if v <= 0:
92
+ raise ValueError("cache_ttl必须大于0")
93
+ return v
36
94
 
37
95
  @classmethod
38
- def from_env(cls) -> "AuthConfig":
96
+ def from_env(cls, prefix: str = "SKYPLATFORM_") -> "AuthConfig":
39
97
  """
40
98
  从环境变量创建配置
99
+
100
+ Args:
101
+ prefix: 环境变量前缀,默认为SKYPLATFORM_
102
+
103
+ Returns:
104
+ AuthConfig: 配置实例
105
+
106
+ Raises:
107
+ ValueError: 配置验证失败
41
108
  """
42
- return cls(
43
- agenterra_iam_host=os.environ.get('AGENTERRA_IAM_HOST', ''),
44
- server_name=os.environ.get('AGENTERRA_SERVER_NAME', ''),
45
- access_key=os.environ.get('AGENTERRA_ACCESS_KEY', ''),
46
- enable_debug=os.environ.get('AGENTERRA_ENABLE_DEBUG', 'false').lower() == 'true',
47
- whitelist_paths=[] # 初始化空的白名单路径列表
109
+ logger.info(f"从环境变量加载配置,前缀: {prefix}")
110
+
111
+ # 支持多种环境变量前缀(向后兼容)
112
+ def get_env_value(key: str, default: str = '') -> str:
113
+ # 优先使用新前缀
114
+ value = os.environ.get(f"{prefix}{key}", '')
115
+ if not value:
116
+ # 回退到旧前缀
117
+ value = os.environ.get(f"AGENTERRA_{key}", default)
118
+ return value
119
+
120
+ # 解析白名单路径
121
+ whitelist_paths_str = get_env_value('WHITELIST_PATHS', '')
122
+ whitelist_paths = []
123
+ if whitelist_paths_str:
124
+ whitelist_paths = [path.strip() for path in whitelist_paths_str.split(',') if path.strip()]
125
+
126
+ config = cls(
127
+ agenterra_iam_host=get_env_value('IAM_HOST'),
128
+ server_name=get_env_value('SERVER_NAME'),
129
+ access_key=get_env_value('ACCESS_KEY'),
130
+ enable_debug=get_env_value('ENABLE_DEBUG', 'false').lower() == 'true',
131
+ whitelist_paths=whitelist_paths,
132
+ timeout=int(get_env_value('TIMEOUT', '30')),
133
+ max_retries=int(get_env_value('MAX_RETRIES', '3')),
134
+ enable_cache=get_env_value('ENABLE_CACHE', 'true').lower() == 'true',
135
+ cache_ttl=int(get_env_value('CACHE_TTL', '300'))
48
136
  )
137
+
138
+ logger.info(f"配置加载完成: server_name={config.server_name}, "
139
+ f"iam_host={config.agenterra_iam_host}, "
140
+ f"whitelist_paths_count={len(config.whitelist_paths)}")
141
+
142
+ return config
143
+
144
+ def validate_config(self) -> None:
145
+ """
146
+ 验证配置完整性
147
+
148
+ Raises:
149
+ ValueError: 配置验证失败
150
+ """
151
+ logger.debug("开始验证配置完整性")
152
+
153
+ # Pydantic会自动调用validator,这里只需要检查业务逻辑
154
+ if not self.agenterra_iam_host:
155
+ raise ValueError("agenterra_iam_host不能为空")
156
+ if not self.server_name:
157
+ raise ValueError("server_name不能为空")
158
+ if not self.access_key:
159
+ raise ValueError("access_key不能为空")
160
+
161
+ logger.info("配置验证通过")
49
162
 
50
- def validate_config(self) -> bool:
163
+ def merge_config(self, other: "AuthConfig") -> "AuthConfig":
164
+ """
165
+ 合并配置,other的非空值会覆盖当前配置
166
+
167
+ Args:
168
+ other: 要合并的配置
169
+
170
+ Returns:
171
+ AuthConfig: 合并后的新配置实例
51
172
  """
52
- 验证配置是否完整
173
+ logger.debug("开始合并配置")
174
+
175
+ # 获取当前配置的字典表示
176
+ current_dict = self.dict()
177
+ other_dict = other.dict()
178
+
179
+ # 合并配置
180
+ merged_dict = current_dict.copy()
181
+ for key, value in other_dict.items():
182
+ if key == 'whitelist_paths':
183
+ # 白名单路径需要合并而不是覆盖
184
+ merged_paths = list(set(current_dict[key] + value))
185
+ merged_dict[key] = merged_paths
186
+ elif value: # 只有非空值才覆盖
187
+ merged_dict[key] = value
188
+
189
+ logger.debug(f"配置合并完成,合并后的配置: {merged_dict}")
190
+ return AuthConfig(**merged_dict)
191
+
192
+ def to_dict(self) -> Dict[str, Any]:
193
+ """
194
+ 转换为字典格式
195
+
196
+ Returns:
197
+ Dict: 配置字典
198
+ """
199
+ return self.dict()
200
+
201
+ def copy_with_updates(self, **updates) -> "AuthConfig":
202
+ """
203
+ 创建配置副本并更新指定字段
204
+
205
+ Args:
206
+ **updates: 要更新的字段
207
+
208
+ Returns:
209
+ AuthConfig: 更新后的配置副本
53
210
  """
54
- required_fields = ['agenterra_iam_host', 'server_name', 'access_key']
55
- for field in required_fields:
56
- if not getattr(self, field):
57
- raise ValueError(f"配置项 {field} 不能为空")
58
- return True
211
+ config_dict = self.dict()
212
+ config_dict.update(updates)
213
+ return AuthConfig(**config_dict)
59
214
 
60
215
  def _normalize_path(self, path: str) -> str:
61
216
  """
@@ -1,13 +1,9 @@
1
- import os
2
1
  import requests
3
2
  import logging
4
3
  import traceback
5
4
  import copy
6
- from dotenv import load_dotenv
7
5
  from enum import Enum
8
-
9
- # 加载环境变量
10
- load_dotenv()
6
+ from fastapi import HTTPException, status
11
7
 
12
8
 
13
9
  class CredentialTypeEnum(str, Enum):
@@ -19,14 +15,31 @@ class CredentialTypeEnum(str, Enum):
19
15
 
20
16
 
21
17
  class ConnectAgenterraIam(object):
22
- def __init__(self, logger_name="skyplatform_iam", log_level=logging.INFO):
18
+ _instance = None
19
+ _initialized = False
20
+
21
+ def __new__(cls, config=None, logger_name="skyplatform_iam", log_level=logging.INFO):
22
+ """
23
+ 单例模式实现
24
+ 确保整个应用中只有一个ConnectAgenterraIam实例
25
+ """
26
+ if cls._instance is None:
27
+ cls._instance = super(ConnectAgenterraIam, cls).__new__(cls)
28
+ return cls._instance
29
+
30
+ def __init__(self, config=None, logger_name="skyplatform_iam", log_level=logging.INFO):
23
31
  """
24
32
  初始化AgenterraIAM连接器
25
33
 
26
34
  参数:
35
+ - config: AuthConfig配置对象,如果为None则从环境变量读取
27
36
  - logger_name: 日志记录器名称
28
37
  - log_level: 日志级别
29
38
  """
39
+ # 防止重复初始化
40
+ if self._initialized:
41
+ return
42
+
30
43
  # 配置日志记录器
31
44
  self.logger = logging.getLogger(logger_name)
32
45
  if not self.logger.handlers:
@@ -38,10 +51,22 @@ class ConnectAgenterraIam(object):
38
51
  self.logger.addHandler(handler)
39
52
  self.logger.setLevel(log_level)
40
53
 
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')
54
+ # 必须传入config参数,不再支持从环境变量读取
55
+ if config is None:
56
+ raise ValueError("必须传入AuthConfig配置对象,不再支持从环境变量读取配置")
57
+
58
+ self.agenterra_iam_host = config.agenterra_iam_host
59
+ self.server_name = config.server_name
60
+ self.access_key = config.access_key
61
+ self.logger.info("使用传入的AuthConfig配置")
62
+
63
+ # 验证必要的配置
64
+ if not self.agenterra_iam_host:
65
+ self.logger.warning("AGENTERRA_IAM_HOST 配置未设置")
66
+ if not self.server_name:
67
+ self.logger.warning("AGENTERRA_SERVER_NAME 配置未设置")
68
+ if not self.access_key:
69
+ self.logger.warning("AGENTERRA_ACCESS_KEY 配置未设置")
45
70
 
46
71
  self.logger.info(f"初始化AgenterraIAM连接器 - Host: {self.agenterra_iam_host}, Server: {self._mask_sensitive(self.server_name)}")
47
72
 
@@ -54,6 +79,95 @@ class ConnectAgenterraIam(object):
54
79
  "server_name": self.server_name,
55
80
  "access_key": self.access_key
56
81
  }
82
+
83
+ # 标记为已初始化
84
+ self._initialized = True
85
+
86
+ @classmethod
87
+ def get_instance(cls):
88
+ """
89
+ 获取已初始化的ConnectAgenterraIam单例实例
90
+
91
+ 如果实例尚未初始化,会抛出异常提示用户先进行配置
92
+ 这个方法通常在用户已经通过setup_auth()进行配置后使用
93
+
94
+ 返回:
95
+ - ConnectAgenterraIam: 已初始化的单例实例
96
+
97
+ 异常:
98
+ - RuntimeError: 当实例未初始化时抛出
99
+ """
100
+ if cls._instance is None or not cls._initialized:
101
+ raise RuntimeError(
102
+ "ConnectAgenterraIam实例尚未初始化。请先使用以下方式之一进行配置:\n"
103
+ "1. 使用setup_auth()进行一键配置\n"
104
+ "2. 手动创建ConnectAgenterraIam实例:ConnectAgenterraIam(config=your_config)"
105
+ )
106
+ return cls._instance
107
+
108
+ @classmethod
109
+ def get_instance_lazy(cls):
110
+ """
111
+ 获取已初始化的单例实例(延迟模式)
112
+
113
+ 此方法支持延迟初始化,当实例未初始化时返回 None 而不是抛出异常。
114
+ 适用于在模块导入时需要获取实例但可能尚未初始化的场景。
115
+
116
+ 返回:
117
+ - ConnectAgenterraIam: 已初始化的单例实例,如果未初始化则返回 None
118
+ """
119
+ if cls._instance is None or not cls._initialized:
120
+ return None
121
+ return cls._instance
122
+
123
+ @classmethod
124
+ def is_initialized(cls):
125
+ """
126
+ 检查单例实例是否已初始化
127
+
128
+ 返回:
129
+ - bool: 如果实例已初始化返回 True,否则返回 False
130
+ """
131
+ return cls._instance is not None and cls._initialized
132
+
133
+ def reload_config(self, config):
134
+ """
135
+ 重新加载配置
136
+ 用于在运行时更新配置
137
+
138
+ 参数:
139
+ - config: AuthConfig配置对象
140
+ """
141
+ if config is None:
142
+ raise ValueError("必须传入AuthConfig配置对象")
143
+
144
+ self.logger.info("重新加载配置")
145
+
146
+ # 更新配置
147
+ self.agenterra_iam_host = config.agenterra_iam_host
148
+ self.server_name = config.server_name
149
+ self.access_key = config.access_key
150
+
151
+ # 验证必要的配置
152
+ if not self.agenterra_iam_host:
153
+ self.logger.warning("AGENTERRA_IAM_HOST 配置未设置")
154
+ if not self.server_name:
155
+ self.logger.warning("AGENTERRA_SERVER_NAME 配置未设置")
156
+ if not self.access_key:
157
+ self.logger.warning("AGENTERRA_ACCESS_KEY 配置未设置")
158
+
159
+ # 更新headers和body
160
+ self.headers = {
161
+ "Content-Type": "application/json",
162
+ "SERVER-AK": self.server_name,
163
+ "SERVER-SK": self.access_key
164
+ }
165
+ self.body = {
166
+ "server_name": self.server_name,
167
+ "access_key": self.access_key
168
+ }
169
+
170
+ self.logger.info(f"配置重新加载完成 - Host: {self.agenterra_iam_host}, Server: {self._mask_sensitive(self.server_name)}")
57
171
 
58
172
  def _mask_sensitive(self, value, mask_char="*", show_chars=4):
59
173
  """
@@ -430,6 +544,11 @@ class ConnectAgenterraIam(object):
430
544
  "server_sk": server_sk,
431
545
  }
432
546
  uri = "/api/v2/service/verify"
547
+
548
+ # 检查agenterra_iam_host是否为None
549
+ if self.agenterra_iam_host is None:
550
+ raise ValueError("AGENTERRA_IAM_HOST 配置未设置或为空,请确保传入正确的AuthConfig对象")
551
+
433
552
  url = self.agenterra_iam_host + uri
434
553
 
435
554
  # 记录请求信息
@@ -463,7 +582,6 @@ class ConnectAgenterraIam(object):
463
582
  else:
464
583
  # token有效但无权限,抛出403异常
465
584
  self.logger.warning(f"[{method_name}] token有效但用户无权限访问API: {api}")
466
- from fastapi import HTTPException, status
467
585
  raise HTTPException(
468
586
  status_code=status.HTTP_403_FORBIDDEN,
469
587
  detail=result.get("message", "用户无权限访问此API")
@@ -475,7 +593,6 @@ class ConnectAgenterraIam(object):
475
593
  result = response.json()
476
594
  # 处理403响应
477
595
  self.logger.warning(f"[{method_name}] 收到403响应 - {result.get('message', '用户无权限访问此API')}")
478
- from fastapi import HTTPException, status
479
596
  raise HTTPException(
480
597
  status_code=status.HTTP_403_FORBIDDEN,
481
598
  detail=result.get("message", "用户无权限访问此API")
@@ -690,6 +807,168 @@ class ConnectAgenterraIam(object):
690
807
  self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
691
808
  return False
692
809
 
810
+ def add_custom_config(self, user_id, config_name, config_value=None):
811
+ """
812
+ 机机接口:添加用户自定义配置
813
+
814
+ 为指定用户添加或更新自定义属性配置。
815
+
816
+ 参数:
817
+ - user_id: 用户ID
818
+ - config_name: 配置项名称
819
+ - config_value: 配置项值(可选)
820
+
821
+ 返回:
822
+ - 成功: 返回响应对象
823
+ - 失败: 返回False
824
+ """
825
+ method_name = "add_custom_config"
826
+ self.logger.info(f"[{method_name}] 开始添加用户自定义配置 - user_id: {user_id}, config_name: {config_name}")
827
+
828
+ try:
829
+ body = {
830
+ "server_name": self.server_name,
831
+ "access_key": self.access_key,
832
+ "user_id": user_id,
833
+ "config_name": config_name
834
+ }
835
+
836
+ # 添加可选参数
837
+ if config_value is not None:
838
+ body["config_value"] = config_value
839
+
840
+ uri = "/api/v2/service/add_custom_config"
841
+ url = self.agenterra_iam_host + uri
842
+
843
+ # 记录请求信息
844
+ self._log_request(method_name, url, self.headers, body)
845
+
846
+ response = requests.post(
847
+ url=url,
848
+ headers=self.headers,
849
+ json=body,
850
+ verify=False
851
+ )
852
+
853
+ # 记录响应信息
854
+ self._log_response(method_name, response)
855
+
856
+ if response.status_code == 200:
857
+ self.logger.info(f"[{method_name}] 添加用户自定义配置成功")
858
+ return response
859
+ else:
860
+ self.logger.warning(f"[{method_name}] 添加用户自定义配置失败 - 状态码: {response.status_code}")
861
+
862
+ return False
863
+ except Exception as e:
864
+ self.logger.error(f"[{method_name}] 添加用户自定义配置请求异常: {str(e)}")
865
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
866
+ return False
867
+
868
+ def get_custom_configs(self, user_id):
869
+ """
870
+ 机机接口:获取用户自定义配置
871
+
872
+ 获取指定用户的所有自定义属性配置。
873
+
874
+ 参数:
875
+ - user_id: 用户ID
876
+
877
+ 返回:
878
+ - 成功: 返回响应对象
879
+ - 失败: 返回False
880
+ """
881
+ method_name = "get_custom_configs"
882
+ self.logger.info(f"[{method_name}] 开始获取用户自定义配置 - user_id: {user_id}")
883
+
884
+ try:
885
+ body = {
886
+ "server_name": self.server_name,
887
+ "access_key": self.access_key,
888
+ "user_id": user_id
889
+ }
890
+
891
+ uri = "/api/v2/service/get_custom_configs"
892
+ url = self.agenterra_iam_host + uri
893
+
894
+ # 记录请求信息
895
+ self._log_request(method_name, url, self.headers, body)
896
+
897
+ response = requests.post(
898
+ url=url,
899
+ headers=self.headers,
900
+ json=body,
901
+ verify=False
902
+ )
903
+
904
+ # 记录响应信息
905
+ self._log_response(method_name, response)
906
+
907
+ if response.status_code == 200:
908
+ self.logger.info(f"[{method_name}] 获取用户自定义配置成功")
909
+ return response
910
+ else:
911
+ self.logger.warning(f"[{method_name}] 获取用户自定义配置失败 - 状态码: {response.status_code}")
912
+
913
+ return False
914
+ except Exception as e:
915
+ self.logger.error(f"[{method_name}] 获取用户自定义配置请求异常: {str(e)}")
916
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
917
+ return False
918
+
919
+ def delete_custom_config(self, user_id, config_name):
920
+ """
921
+ 机机接口:删除用户自定义配置
922
+
923
+ 删除指定用户的指定自定义属性配置。
924
+
925
+ 参数:
926
+ - user_id: 用户ID
927
+ - config_name: 配置项名称
928
+
929
+ 返回:
930
+ - 成功: 返回响应对象
931
+ - 失败: 返回False
932
+ """
933
+ method_name = "delete_custom_config"
934
+ self.logger.info(f"[{method_name}] 开始删除用户自定义配置 - user_id: {user_id}, config_name: {config_name}")
935
+
936
+ try:
937
+ body = {
938
+ "server_name": self.server_name,
939
+ "access_key": self.access_key,
940
+ "user_id": user_id,
941
+ "config_name": config_name
942
+ }
943
+
944
+ uri = "/api/v2/service/delete_custom_config"
945
+ url = self.agenterra_iam_host + uri
946
+
947
+ # 记录请求信息
948
+ self._log_request(method_name, url, self.headers, body)
949
+
950
+ response = requests.post(
951
+ url=url,
952
+ headers=self.headers,
953
+ json=body,
954
+ verify=False
955
+ )
956
+
957
+ # 记录响应信息
958
+ self._log_response(method_name, response)
959
+
960
+ if response.status_code == 200:
961
+ self.logger.info(f"[{method_name}] 删除用户自定义配置成功")
962
+ return response
963
+ else:
964
+ self.logger.warning(f"[{method_name}] 删除用户自定义配置失败 - 状态码: {response.status_code}")
965
+
966
+ return False
967
+ except Exception as e:
968
+ self.logger.error(f"[{method_name}] 删除用户自定义配置请求异常: {str(e)}")
969
+ self.logger.error(f"[{method_name}] 异常堆栈: {traceback.format_exc()}")
970
+ return False
971
+
693
972
  def merge_credential(self, target_user_id, cred_type, cred_value, merge_reason=None):
694
973
  """
695
974
  机机接口:凭证合并