socar-api 0.6.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.
- api_frame/__init__.py +16 -0
- api_frame/atomic.py +212 -0
- api_frame/auth.py +243 -0
- api_frame/cache.py +258 -0
- api_frame/exception.py +192 -0
- api_frame/field.py +68 -0
- api_frame/filebase.py +377 -0
- api_frame/filter.py +166 -0
- api_frame/handlers.py +309 -0
- api_frame/jsonapi.py +317 -0
- api_frame/meta.py +22 -0
- api_frame/query.py +660 -0
- api_frame/resource.py +1250 -0
- api_frame/responses.py +27 -0
- api_frame/router.py +232 -0
- api_frame/schema.py +686 -0
- api_frame/scope.py +127 -0
- api_frame/serializer.py +336 -0
- api_frame/url_parse.py +128 -0
- api_frame/util.py +253 -0
- socar_api-0.6.0.dist-info/METADATA +177 -0
- socar_api-0.6.0.dist-info/RECORD +24 -0
- socar_api-0.6.0.dist-info/WHEEL +5 -0
- socar_api-0.6.0.dist-info/top_level.txt +1 -0
api_frame/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
__version__ = "0.6.0"
|
|
2
|
+
|
|
3
|
+
from api_frame.auth import Auth # noqa: F401
|
|
4
|
+
from api_frame.cache import MemoryBackend, cached, set_default_backend, set_version_hook # noqa: F401
|
|
5
|
+
from api_frame.exception import ( # noqa: F401
|
|
6
|
+
AuthError,
|
|
7
|
+
JsonapiException,
|
|
8
|
+
QureyError,
|
|
9
|
+
ResourceDuplicate,
|
|
10
|
+
ResourceNotFound,
|
|
11
|
+
)
|
|
12
|
+
from api_frame.filebase import DownloadFileResource, UploadFileResource # noqa: F401
|
|
13
|
+
from api_frame.query import ArgParse, Filter, FilterAnd, FilterOr # noqa: F401
|
|
14
|
+
from api_frame.resource import BaseResource, Resource, UploadFileBaseResource # noqa: F401
|
|
15
|
+
from api_frame.schema import Relationship, SchemaBase, field_mapping # noqa: F401
|
|
16
|
+
from api_frame.scope import scope # noqa: F401
|
api_frame/atomic.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Atomic operations module extracted from resource.py.
|
|
3
|
+
|
|
4
|
+
Provides AtomicOperation dataclass and AtomicExecutor class for
|
|
5
|
+
processing JSON:API atomic operations without mutating the request.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
from fastapi import Request
|
|
15
|
+
|
|
16
|
+
from api_frame.exception import AuthError, JsonapiException, serialize_error
|
|
17
|
+
from api_frame.meta import type_registered_resources
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class AtomicOperation:
|
|
22
|
+
"""Represents a single atomic operation.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
op: The operation type ('add', 'update', 'remove')
|
|
26
|
+
target_cls: The target resource class
|
|
27
|
+
data: Operation data dict (from 'data' field)
|
|
28
|
+
ref_id: Optional reference id (for 'update' and 'remove')
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
op: Literal["add", "update", "remove"]
|
|
32
|
+
target_cls: type[Any]
|
|
33
|
+
data: dict
|
|
34
|
+
ref_id: str | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AtomicExecutor:
|
|
38
|
+
"""Executes JSON:API atomic operations.
|
|
39
|
+
|
|
40
|
+
Processes a list of atomic operations by creating resource instances
|
|
41
|
+
directly and calling their post/patch/delete methods, without
|
|
42
|
+
mutating request.scope or request._json on the original request.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, request: Request):
|
|
46
|
+
self.request = request
|
|
47
|
+
|
|
48
|
+
async def run(self, operations: list[dict]) -> dict:
|
|
49
|
+
"""Execute a list of atomic operations.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
operations: List of operation dicts from 'atomic:operations'
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
dict with 'atomic:results' key containing operation results
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
JsonapiException: On validation errors
|
|
59
|
+
AuthError: On permission errors
|
|
60
|
+
"""
|
|
61
|
+
if not operations:
|
|
62
|
+
raise JsonapiException(status_code=400, detail="atomic:operations 不能为空")
|
|
63
|
+
|
|
64
|
+
# 阶段1:解析并验证所有操作
|
|
65
|
+
resolved = await self._parse_operations(operations)
|
|
66
|
+
|
|
67
|
+
# 阶段2:在共享事务中执行所有操作
|
|
68
|
+
return await self._execute_operations(resolved)
|
|
69
|
+
|
|
70
|
+
async def _parse_operations(self, operations: list[dict]) -> list[tuple]:
|
|
71
|
+
"""Parse and validate all operations.
|
|
72
|
+
|
|
73
|
+
Returns list of (op, target_cls, op_data, ref_id) tuples.
|
|
74
|
+
"""
|
|
75
|
+
op_to_method = {"add": "POST", "update": "PATCH", "remove": "DELETE"}
|
|
76
|
+
resolved = []
|
|
77
|
+
|
|
78
|
+
for op_data in operations:
|
|
79
|
+
op = op_data.get("op")
|
|
80
|
+
if op not in ("add", "update", "remove"):
|
|
81
|
+
raise JsonapiException(status_code=400, detail=f"不支持的操作符: {op}")
|
|
82
|
+
|
|
83
|
+
if op == "add":
|
|
84
|
+
target_type = op_data.get("data", {}).get("type")
|
|
85
|
+
ref_id = None
|
|
86
|
+
else:
|
|
87
|
+
ref = op_data.get("ref", {})
|
|
88
|
+
target_type = ref.get("type")
|
|
89
|
+
ref_id = ref.get("id")
|
|
90
|
+
if not ref_id:
|
|
91
|
+
raise JsonapiException(status_code=400, detail="缺少资源 id")
|
|
92
|
+
|
|
93
|
+
if not target_type:
|
|
94
|
+
raise JsonapiException(status_code=400, detail="缺少资源类型 type")
|
|
95
|
+
|
|
96
|
+
target_cls = type_registered_resources.get(target_type)
|
|
97
|
+
if not target_cls:
|
|
98
|
+
raise JsonapiException(status_code=400, detail=f"资源类型不存在: {target_type}")
|
|
99
|
+
|
|
100
|
+
# 检查权限
|
|
101
|
+
if target_cls.Auth:
|
|
102
|
+
scope = target_cls.Auth.get_scopes(api_url=target_cls.Meta.link, method=op_to_method[op])
|
|
103
|
+
if scope is not None:
|
|
104
|
+
has_perm = await target_cls.Auth.api_auth(api_url=target_cls.Meta.link, method=op_to_method[op])
|
|
105
|
+
if not has_perm:
|
|
106
|
+
raise AuthError(detail=f"没有 {op_to_method[op]} {target_type} 的权限")
|
|
107
|
+
|
|
108
|
+
resolved.append((op, target_cls, op_data, ref_id))
|
|
109
|
+
|
|
110
|
+
return resolved
|
|
111
|
+
|
|
112
|
+
async def _execute_operations(self, resolved: list[tuple]) -> dict:
|
|
113
|
+
"""Execute validated operations with shared transaction management."""
|
|
114
|
+
shared_db = None
|
|
115
|
+
last_target_cls = None # 用于异常时的错误处理回退
|
|
116
|
+
try:
|
|
117
|
+
# 获取 session 数据源
|
|
118
|
+
session_source = next((tc for _, tc, _, _ in resolved if tc.session is not None), None)
|
|
119
|
+
|
|
120
|
+
if session_source and session_source.session:
|
|
121
|
+
shared_db = session_source.session.get()
|
|
122
|
+
try:
|
|
123
|
+
shared_db.rollback()
|
|
124
|
+
except BaseException:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
results = []
|
|
128
|
+
for op, target_cls, op_data, ref_id in resolved:
|
|
129
|
+
last_target_cls = target_cls
|
|
130
|
+
result = await self._execute_single(op, target_cls, op_data, ref_id, shared_db)
|
|
131
|
+
results.append(
|
|
132
|
+
{
|
|
133
|
+
"data": {
|
|
134
|
+
"type": target_cls.Meta.type_,
|
|
135
|
+
"id": result.id if result and hasattr(result, "id") else None,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
if shared_db:
|
|
141
|
+
shared_db.commit()
|
|
142
|
+
|
|
143
|
+
return {"atomic:results": results}
|
|
144
|
+
|
|
145
|
+
except BaseException as e:
|
|
146
|
+
if shared_db:
|
|
147
|
+
shared_db.rollback()
|
|
148
|
+
if last_target_cls is not None:
|
|
149
|
+
return await last_target_cls.handle_error(request=self.request, exc=e)
|
|
150
|
+
return await self._fallback_error(e)
|
|
151
|
+
finally:
|
|
152
|
+
if shared_db:
|
|
153
|
+
shared_db.close()
|
|
154
|
+
|
|
155
|
+
async def _execute_single(self, op: str, target_cls: type, op_data: dict, ref_id: str | None, shared_db):
|
|
156
|
+
"""Execute a single atomic operation.
|
|
157
|
+
|
|
158
|
+
Creates a resource instance and calls the appropriate handler,
|
|
159
|
+
without mutating request.scope or request._json.
|
|
160
|
+
"""
|
|
161
|
+
if op == "add":
|
|
162
|
+
body = op_data.get("data", {}).copy()
|
|
163
|
+
resource = target_cls(request=self.request, request_context={"request_body": {"data": body}})
|
|
164
|
+
# 标记为新增操作
|
|
165
|
+
resource._atomic_op = "add"
|
|
166
|
+
resource._atomic_body = body
|
|
167
|
+
if shared_db:
|
|
168
|
+
resource.db = shared_db
|
|
169
|
+
result = await resource.post()
|
|
170
|
+
if shared_db and resource.db is not shared_db:
|
|
171
|
+
logger.warning(
|
|
172
|
+
"原子操作: %s.post() 内部调用了 connect_data,已自动恢复共享事务", target_cls.Meta.type_
|
|
173
|
+
)
|
|
174
|
+
resource.db = shared_db
|
|
175
|
+
|
|
176
|
+
elif op == "update":
|
|
177
|
+
body = op_data.get("data", {}).copy()
|
|
178
|
+
body["id"] = ref_id
|
|
179
|
+
body.setdefault("type", target_cls.Meta.type_)
|
|
180
|
+
resource = target_cls(request=self.request, request_context={"request_body": {"data": body}})
|
|
181
|
+
# 标记为更新操作
|
|
182
|
+
resource._atomic_op = "update"
|
|
183
|
+
resource._atomic_body = body
|
|
184
|
+
resource._atomic_id = ref_id
|
|
185
|
+
if shared_db:
|
|
186
|
+
resource.db = shared_db
|
|
187
|
+
result = await resource.patch()
|
|
188
|
+
if shared_db and resource.db is not shared_db:
|
|
189
|
+
logger.warning(
|
|
190
|
+
"原子操作: %s.patch() 内部调用了 connect_data,已自动恢复共享事务", target_cls.Meta.type_
|
|
191
|
+
)
|
|
192
|
+
resource.db = shared_db
|
|
193
|
+
|
|
194
|
+
else: # remove
|
|
195
|
+
resource = target_cls(request=self.request)
|
|
196
|
+
resource._atomic_op = "remove"
|
|
197
|
+
resource._atomic_id = ref_id
|
|
198
|
+
if shared_db:
|
|
199
|
+
resource.db = shared_db
|
|
200
|
+
result = await resource.delete()
|
|
201
|
+
if shared_db and resource.db is not shared_db:
|
|
202
|
+
logger.warning(
|
|
203
|
+
"原子操作: %s.delete() 内部调用了 connect_data,已自动恢复共享事务", target_cls.Meta.type_
|
|
204
|
+
)
|
|
205
|
+
resource.db = shared_db
|
|
206
|
+
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
async def _fallback_error(self, exc: BaseException) -> dict:
|
|
210
|
+
"""Fallback error handler when target_cls is not available."""
|
|
211
|
+
|
|
212
|
+
return serialize_error(request=self.request, exc=exc)
|
api_frame/auth.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import logging
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
from bitarray import bitarray
|
|
8
|
+
from bitarray.util import ba2base, base2ba
|
|
9
|
+
from fastapi import Depends, Request, Security, routing
|
|
10
|
+
from fastapi.dependencies.utils import get_parameterless_sub_dependant
|
|
11
|
+
from fastapi.security import OAuth2PasswordBearer, SecurityScopes
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
from treelib import Node, Tree
|
|
14
|
+
|
|
15
|
+
from api_frame.exception import QureyError
|
|
16
|
+
from api_frame.meta import registered_resources
|
|
17
|
+
from api_frame.scope import _SCOPE_DECLARATIONS_ATTR, ScopeConfig
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ScopeTree:
|
|
21
|
+
def __init__(self, project_scopes: list[dict]) -> None:
|
|
22
|
+
self.project_scopes = project_scopes
|
|
23
|
+
self.scope_dict = {}
|
|
24
|
+
self.scope_tree = Tree()
|
|
25
|
+
|
|
26
|
+
for scope in project_scopes:
|
|
27
|
+
self.scope_dict[scope["scope"]] = scope["id"]
|
|
28
|
+
node = Node(data=scope["scope"], tag=scope["name"], identifier=scope["id"])
|
|
29
|
+
if scope["pid"] is not None:
|
|
30
|
+
self.scope_tree.add_node(node, parent=scope["pid"])
|
|
31
|
+
else:
|
|
32
|
+
self.scope_tree.add_node(node)
|
|
33
|
+
|
|
34
|
+
def scopes(self):
|
|
35
|
+
scopes = {}
|
|
36
|
+
for scope in self.project_scopes:
|
|
37
|
+
scopes.update({scope["scope"]: scope["name"]})
|
|
38
|
+
return scopes
|
|
39
|
+
|
|
40
|
+
def sub(self, tag: str) -> list:
|
|
41
|
+
sub = []
|
|
42
|
+
nodeid = self.scope_dict[tag]
|
|
43
|
+
children = self.scope_tree.children(nodeid)
|
|
44
|
+
for child in children:
|
|
45
|
+
childlist = self.sub(child.data)
|
|
46
|
+
sub.append(child.data)
|
|
47
|
+
sub = sub + childlist
|
|
48
|
+
return sub
|
|
49
|
+
|
|
50
|
+
def children(self, tags: list) -> list:
|
|
51
|
+
family = []
|
|
52
|
+
for node in tags:
|
|
53
|
+
family.append(node)
|
|
54
|
+
family = family + self.sub(node)
|
|
55
|
+
family = list(set(family))
|
|
56
|
+
return family
|
|
57
|
+
|
|
58
|
+
def to_base(self, checked: list) -> str:
|
|
59
|
+
family = []
|
|
60
|
+
for node in checked:
|
|
61
|
+
family.append(node)
|
|
62
|
+
family = family + self.sub(node)
|
|
63
|
+
family = list(set(family))
|
|
64
|
+
ba_len = math.ceil(len(self.scope_dict) / 6) * 6
|
|
65
|
+
ba = bitarray(ba_len)
|
|
66
|
+
ba.setall(0)
|
|
67
|
+
for item in self.scope_dict:
|
|
68
|
+
if item in family:
|
|
69
|
+
ba[self.scope_dict[item]] = 1
|
|
70
|
+
return str(ba2base(64, ba))
|
|
71
|
+
|
|
72
|
+
def to_sope(self, ba_str: str) -> list:
|
|
73
|
+
ba = base2ba(64, ba_str)
|
|
74
|
+
scopes = []
|
|
75
|
+
for item in self.scope_dict:
|
|
76
|
+
index = self.scope_dict[item]
|
|
77
|
+
if index <= len(ba) - 1 and ba[index]:
|
|
78
|
+
scopes.append(item)
|
|
79
|
+
return scopes
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class User(BaseModel):
|
|
83
|
+
id: str = Field(..., title="id")
|
|
84
|
+
name: str = Field(None, title="姓名")
|
|
85
|
+
headimage: str = Field(None, title="头像")
|
|
86
|
+
token: str = Field(None, title="token")
|
|
87
|
+
scope: list[str] = Field(None, title="scope")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Auth(metaclass=abc.ABCMeta):
|
|
91
|
+
"""权限验证基类"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, cert, token_url: str = None, scopes: list[dict] = None):
|
|
94
|
+
self.cert = cert
|
|
95
|
+
self.token_url = token_url
|
|
96
|
+
self.scope_tree = ScopeTree(scopes)
|
|
97
|
+
self.user = None
|
|
98
|
+
|
|
99
|
+
def _scopes(self) -> dict:
|
|
100
|
+
# 项目全部的scopes集合
|
|
101
|
+
return self.scope_tree.scopes()
|
|
102
|
+
|
|
103
|
+
def _oauth(self) -> OAuth2PasswordBearer:
|
|
104
|
+
# 权限依赖
|
|
105
|
+
oauth2_scheme = OAuth2PasswordBearer(
|
|
106
|
+
tokenUrl=self.token_url,
|
|
107
|
+
scopes=self._scopes(),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return oauth2_scheme
|
|
111
|
+
|
|
112
|
+
def scope(self, ba_str) -> list:
|
|
113
|
+
"""
|
|
114
|
+
64位的scope字符串转成列表
|
|
115
|
+
:param ba_str: 64进制字符串
|
|
116
|
+
:return: token中的scope对应的列表
|
|
117
|
+
"""
|
|
118
|
+
return self.scope_tree.to_sope(ba_str)
|
|
119
|
+
|
|
120
|
+
@abc.abstractmethod
|
|
121
|
+
async def auth(self, security_scopes: SecurityScopes, token: str) -> User:
|
|
122
|
+
"""子类必须实现,权限验证方法"""
|
|
123
|
+
raise NotImplementedError
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SecurityConfig(Auth):
|
|
127
|
+
"""权限安全配置"""
|
|
128
|
+
|
|
129
|
+
def __init__(
|
|
130
|
+
self,
|
|
131
|
+
api_scopes: dict,
|
|
132
|
+
cert: str,
|
|
133
|
+
token_url: str = None,
|
|
134
|
+
scopes: list[dict] = None,
|
|
135
|
+
strict: bool = False,
|
|
136
|
+
):
|
|
137
|
+
|
|
138
|
+
super().__init__(cert, token_url, scopes)
|
|
139
|
+
self.api_scopes = api_scopes
|
|
140
|
+
self.res = None
|
|
141
|
+
self.strict = strict
|
|
142
|
+
|
|
143
|
+
def get_scopes(self, api_url, method):
|
|
144
|
+
"""
|
|
145
|
+
获取接口方法对应scope
|
|
146
|
+
Args:
|
|
147
|
+
api_url: api链接
|
|
148
|
+
method: 方法
|
|
149
|
+
|
|
150
|
+
Returns: scope
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
api = self.api_scopes.get(api_url)
|
|
154
|
+
if not api:
|
|
155
|
+
return None
|
|
156
|
+
scope = api.get(method, None)
|
|
157
|
+
return scope
|
|
158
|
+
|
|
159
|
+
async def api_auth(self, api_url, method) -> bool:
|
|
160
|
+
api_scope = self.get_scopes(api_url=api_url, method=method)
|
|
161
|
+
if api_scope is None:
|
|
162
|
+
return True
|
|
163
|
+
if not self.user or not self.user.scope:
|
|
164
|
+
return False
|
|
165
|
+
return bool(set(api_scope).intersection(set(self.user.scope)))
|
|
166
|
+
|
|
167
|
+
def _get_auth(self, res):
|
|
168
|
+
"""
|
|
169
|
+
生成权限判断函数
|
|
170
|
+
Args:
|
|
171
|
+
res: 资源
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
async def wrapper(
|
|
178
|
+
request: Request, security_scopes: SecurityScopes, token: str = Depends(self._oauth())
|
|
179
|
+
) -> User:
|
|
180
|
+
include_param = request.query_params.get("include")
|
|
181
|
+
if include_param:
|
|
182
|
+
path_parts = request.scope.get("path").split("/")
|
|
183
|
+
for include in include_param.split(","):
|
|
184
|
+
if include.count(".") == 1:
|
|
185
|
+
rel_name, relrel_name = include.split(".")
|
|
186
|
+
has_id = "id" in request.path_params
|
|
187
|
+
if len(path_parts) >= 2 and has_id and request.path_params.get("id") == path_parts[-2]:
|
|
188
|
+
res_temp = res.rel_resources()
|
|
189
|
+
last_part = path_parts[-1]
|
|
190
|
+
if last_part not in res_temp:
|
|
191
|
+
raise QureyError(detail=f"include 参数 {rel_name} 不存在")
|
|
192
|
+
rel_res_temp = res_temp[last_part]
|
|
193
|
+
rel_resource = registered_resources.get(rel_res_temp.rel_resource)
|
|
194
|
+
rels = rel_resource.rel_resources()
|
|
195
|
+
else:
|
|
196
|
+
rels = res.rel_resources()
|
|
197
|
+
if rel_name not in rels:
|
|
198
|
+
raise QureyError(detail=f"include 参数 {rel_name} 不存在")
|
|
199
|
+
rel_res = rels[rel_name]
|
|
200
|
+
rel_resource = registered_resources.get(rel_res.rel_resource)
|
|
201
|
+
rel_rels = rel_resource.rel_resources()
|
|
202
|
+
if relrel_name not in rel_rels:
|
|
203
|
+
raise QureyError(detail=f"include 参数 {relrel_name} 不存在")
|
|
204
|
+
relrel_res = registered_resources.get(rel_rels[relrel_name].rel_resource)
|
|
205
|
+
scope = self.get_scopes(api_url=relrel_res.Meta.link, method="GET")
|
|
206
|
+
await self.auth(security_scopes=SecurityScopes(scopes=scope), token=token)
|
|
207
|
+
|
|
208
|
+
user = await self.auth(security_scopes, token)
|
|
209
|
+
self.user = user
|
|
210
|
+
return user
|
|
211
|
+
|
|
212
|
+
return wrapper
|
|
213
|
+
|
|
214
|
+
def run(self, res):
|
|
215
|
+
# Check if resource class has @scope declarations
|
|
216
|
+
scope_declarations = getattr(res, _SCOPE_DECLARATIONS_ATTR, None)
|
|
217
|
+
scope_config = None
|
|
218
|
+
if scope_declarations:
|
|
219
|
+
scope_config = ScopeConfig(api_scopes=self.api_scopes)
|
|
220
|
+
|
|
221
|
+
for route in res.route.routes:
|
|
222
|
+
api_url = route.path_format[len(res.prefix) :]
|
|
223
|
+
method = list(route.methods)[0]
|
|
224
|
+
if scope_config:
|
|
225
|
+
scopes = scope_config.get_scopes(res, api_url, method)
|
|
226
|
+
else:
|
|
227
|
+
scopes = self.get_scopes(api_url=api_url, method=method)
|
|
228
|
+
if scopes is not None:
|
|
229
|
+
if isinstance(route, routing.APIRoute):
|
|
230
|
+
route.dependant.dependencies.insert(
|
|
231
|
+
0,
|
|
232
|
+
get_parameterless_sub_dependant(
|
|
233
|
+
depends=Security(self._get_auth(res), scopes=scopes), path=route.path_format
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
msg = f"接口没有配置权限,请确认接口权限是否公开,不公开请配置权限。接口:{route}"
|
|
238
|
+
if self.strict:
|
|
239
|
+
raise RuntimeError(msg)
|
|
240
|
+
logger.warning(
|
|
241
|
+
"接口没有配置权限,请确认接口权限是否公开,不公开请配置权限。接口:%s", route
|
|
242
|
+
)
|
|
243
|
+
return res
|