access-control-async 0.2.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.
- access_control/__init__.py +12 -0
- access_control/exceptions.py +18 -0
- access_control/file_backed_resource_manager.py +195 -0
- access_control/resource_manager.py +540 -0
- access_control/utils.py +97 -0
- access_control_async-0.2.5.dist-info/METADATA +181 -0
- access_control_async-0.2.5.dist-info/RECORD +10 -0
- access_control_async-0.2.5.dist-info/WHEEL +5 -0
- access_control_async-0.2.5.dist-info/licenses/LICENSE +21 -0
- access_control_async-0.2.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
异常
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class ResourceManagerError(Exception):
|
|
6
|
+
"""
|
|
7
|
+
与 ResourceManager 有关的错误。
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
class PermissionInsufficient(ResourceManagerError):
|
|
11
|
+
"""
|
|
12
|
+
所使用的控制/访问码的权限不足(控制/访问码不存在)。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
class CodeExistError(ResourceManagerError):
|
|
16
|
+
"""
|
|
17
|
+
若控制码/访问码重复。
|
|
18
|
+
"""
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
class: FileBackedResourceManager
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable, Coroutine
|
|
7
|
+
import os
|
|
8
|
+
import pickle
|
|
9
|
+
from typing import Generic, Optional, override
|
|
10
|
+
from uuid import uuid4
|
|
11
|
+
|
|
12
|
+
import aiofiles
|
|
13
|
+
import aiofiles.os
|
|
14
|
+
from pathvalidate import ValidationError, is_valid_filename
|
|
15
|
+
|
|
16
|
+
from .utils import to_async
|
|
17
|
+
from .exceptions import PermissionInsufficient
|
|
18
|
+
from .resource_manager import (
|
|
19
|
+
ResourceManager,
|
|
20
|
+
ControlCode, AccessCode, Resource,
|
|
21
|
+
ControlCodeGen, ControlCodeGenAsync, AccessCodeGen, AccessCodeGenAsync,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def uuid4_str():
|
|
26
|
+
"""生成一个标准格式的随机UUID字符串"""
|
|
27
|
+
return str(uuid4())
|
|
28
|
+
|
|
29
|
+
# 资源与二进制数据转换函数
|
|
30
|
+
DumpsFunction = Callable[[Resource], bytes]
|
|
31
|
+
LoadsFunction = Callable[[bytes], Resource]
|
|
32
|
+
DumpsFunctionAsync = Callable[[Resource], Coroutine[None, None, bytes]]
|
|
33
|
+
LoadsFunctionAsync = Callable[[bytes], Coroutine[None, None, Resource]]
|
|
34
|
+
|
|
35
|
+
# 文件名生成函数
|
|
36
|
+
FileName = str
|
|
37
|
+
FileNameGen = Callable[[Optional[Resource], Optional[ControlCode]], FileName]
|
|
38
|
+
FileNameGenAsync = Callable[[Optional[Resource], Optional[ControlCode]], Coroutine[None, None, FileName]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FileBackedResourceManager(
|
|
42
|
+
Generic[Resource, ControlCode, AccessCode],
|
|
43
|
+
ResourceManager[FileName, ControlCode, AccessCode]
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Subclass of ResourceManager.
|
|
47
|
+
|
|
48
|
+
Store resources in disk files.
|
|
49
|
+
|
|
50
|
+
If control/access codes, codes generators and filename generator are all pickle serializable,
|
|
51
|
+
then the FileBackedResourceManager object can also be.
|
|
52
|
+
"""
|
|
53
|
+
def __init__(
|
|
54
|
+
self,
|
|
55
|
+
data_dir_path: str,
|
|
56
|
+
dumps_function: Optional[DumpsFunction | DumpsFunctionAsync] = None,
|
|
57
|
+
loads_function: Optional[LoadsFunction | LoadsFunctionAsync] = None,
|
|
58
|
+
file_name_gen: Optional[FileNameGen | FileNameGenAsync] = None,
|
|
59
|
+
*,
|
|
60
|
+
control_code_gen: Optional[ControlCodeGen | ControlCodeGenAsync] = None,
|
|
61
|
+
access_code_gen: Optional[AccessCodeGen | AccessCodeGenAsync] = None,
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
初始化一个 FileBackedResourceManager 对象。
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
data_dir_path (str): 存储资源文件的目录。
|
|
68
|
+
dumps_function (Optional[DumpsFunction | DumpsFunctionAsync]): 将资源转换为字节串的函数,
|
|
69
|
+
默认为 pickle.dumps 。
|
|
70
|
+
loads_function (Optional[LoadsFunction | LoadsFunctionAsync]): 将字节串解码为资源的函数,
|
|
71
|
+
默认为 pickle.loads 。
|
|
72
|
+
file_name_gen (Optional[FileNameGen | FileNameGenAsync]): 根据资源内容(可选)和控制码(可选)生成文件名的函数。
|
|
73
|
+
control_code_gen (Optional[ControlCodeGen | ControlCodeGenAsync]): 根据资源内容(可选)生成控制码的函数。
|
|
74
|
+
access_code_gen (Optional[AccessCodeGen | AccessCodeGenAsync]): 根据资源内容(可选)、控制码(可选)、
|
|
75
|
+
父级访问码(可选)生成访问码的函数。
|
|
76
|
+
"""
|
|
77
|
+
super().__init__(control_code_gen, access_code_gen)
|
|
78
|
+
|
|
79
|
+
# 文件存放路径
|
|
80
|
+
self.data_dir_path = data_dir_path
|
|
81
|
+
|
|
82
|
+
# 数据转换函数
|
|
83
|
+
if not dumps_function:
|
|
84
|
+
dumps_function = pickle.dumps
|
|
85
|
+
|
|
86
|
+
if not loads_function:
|
|
87
|
+
loads_function = pickle.loads
|
|
88
|
+
|
|
89
|
+
self.dumps_async: DumpsFunctionAsync = to_async(dumps_function)
|
|
90
|
+
self.loads_async: LoadsFunctionAsync = to_async(loads_function)
|
|
91
|
+
|
|
92
|
+
# 文件名生成函数
|
|
93
|
+
if not file_name_gen:
|
|
94
|
+
file_name_gen = uuid4_str
|
|
95
|
+
self._file_name_gen_raw_async: FileNameGenAsync = to_async(file_name_gen)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _get_path(self, file_name: str) -> str:
|
|
99
|
+
"""
|
|
100
|
+
获取文件存储的路径。
|
|
101
|
+
"""
|
|
102
|
+
return os.path.join(self.data_dir_path, file_name)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
async def file_name_gen_async(
|
|
106
|
+
self,
|
|
107
|
+
resource: Optional[Resource] = None,
|
|
108
|
+
control_code: Optional[ControlCode] = None
|
|
109
|
+
) -> FileName:
|
|
110
|
+
"""
|
|
111
|
+
包装了生成文件名的函数,使其能够接受接受 resource, control_code 参数。
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
return await self._file_name_gen_raw_async(resource, control_code)
|
|
115
|
+
except TypeError:
|
|
116
|
+
try:
|
|
117
|
+
return await self._file_name_gen_raw_async(resource)
|
|
118
|
+
except TypeError:
|
|
119
|
+
return await self._file_name_gen_raw_async()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ==== 重载父类方法 ====
|
|
123
|
+
|
|
124
|
+
@override
|
|
125
|
+
async def _get_resource_async(self, control_code: ControlCode) -> Resource:
|
|
126
|
+
"""
|
|
127
|
+
返回资源。
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
code (Code): 控制码或访问码。
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Resource: control_code 对应的资源。
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
FileNotFoundError: 如果文件没有找到。
|
|
137
|
+
PermissionInsufficient: 如果 control_code 不是控制码。
|
|
138
|
+
"""
|
|
139
|
+
# 获取文件路径
|
|
140
|
+
file_name = await super()._get_resource_async(control_code)
|
|
141
|
+
file_path = self._get_path(file_name)
|
|
142
|
+
|
|
143
|
+
# 读取文件
|
|
144
|
+
async with aiofiles.open(file_path, mode="rb") as file:
|
|
145
|
+
content = await file.read()
|
|
146
|
+
return await self.loads_async(content)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@override
|
|
150
|
+
async def _set_resource_async(self, control_code: ControlCode, resource: Resource) -> None:
|
|
151
|
+
"""
|
|
152
|
+
记录资源。
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
control_code (Code): 控制码。
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
PermissionInsufficient: 如果 file_name_gen 生成了重复的文件名。
|
|
159
|
+
ValidationError: 如果 file_name_gen 生成了非法的文件名。
|
|
160
|
+
"""
|
|
161
|
+
# 生成文件名
|
|
162
|
+
file_name = await self.file_name_gen_async(resource=resource, control_code=control_code)
|
|
163
|
+
if not is_valid_filename(file_name):
|
|
164
|
+
raise ValidationError(f"Invalid file name: {file_name}")
|
|
165
|
+
if file_name in self._control_resource_map.values():
|
|
166
|
+
raise PermissionInsufficient("Duplicate file name")
|
|
167
|
+
|
|
168
|
+
# 写入文件
|
|
169
|
+
await aiofiles.os.makedirs(self.data_dir_path, exist_ok=True)
|
|
170
|
+
file_path = self._get_path(file_name)
|
|
171
|
+
content = await self.dumps_async(resource)
|
|
172
|
+
async with aiofiles.open(file_path, mode="wb") as file:
|
|
173
|
+
await file.write(content)
|
|
174
|
+
|
|
175
|
+
# 记录
|
|
176
|
+
await super()._set_resource_async(control_code, file_name)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@override
|
|
180
|
+
async def _delete_resource_async(self, control_code: ControlCode) -> None:
|
|
181
|
+
"""
|
|
182
|
+
删除资源。
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
control_code (Code): 控制码。
|
|
186
|
+
"""
|
|
187
|
+
file_name = await super()._get_resource_async(control_code)
|
|
188
|
+
|
|
189
|
+
# 删除记录
|
|
190
|
+
await super()._delete_resource_async(control_code)
|
|
191
|
+
|
|
192
|
+
# 删除文件
|
|
193
|
+
file_path = self._get_path(file_name)
|
|
194
|
+
if await aiofiles.os.path.isfile(file_path):
|
|
195
|
+
await aiofiles.os.remove(file_path)
|
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""
|
|
2
|
+
class: ResourceManager
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Coroutine, Hashable, Callable
|
|
7
|
+
import functools
|
|
8
|
+
from typing import Dict, Generic, List, Optional, TypeVar, Union
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from treelib import Tree
|
|
12
|
+
from treelib.exceptions import NodeIDAbsentError
|
|
13
|
+
|
|
14
|
+
from .utils import to_async, AsyncRLock
|
|
15
|
+
from .exceptions import CodeExistError, PermissionInsufficient, ResourceManagerError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# 资源类型
|
|
19
|
+
Resource = TypeVar("Resource")
|
|
20
|
+
|
|
21
|
+
# 控制码类型,一般是字符串或整数
|
|
22
|
+
ControlCode = TypeVar("ControlCode", bound=Hashable)
|
|
23
|
+
|
|
24
|
+
# 访问码类型,一般是字符串或整数
|
|
25
|
+
AccessCode = TypeVar("AccessCode", bound=Hashable)
|
|
26
|
+
|
|
27
|
+
# 控制码和访问码的联合类型
|
|
28
|
+
Code = Union[ControlCode, AccessCode]
|
|
29
|
+
|
|
30
|
+
# 生成控制码的函数类型
|
|
31
|
+
ControlCodeGen = Callable[[Optional[Resource]], ControlCode]
|
|
32
|
+
|
|
33
|
+
# 生成访问码的函数类型
|
|
34
|
+
AccessCodeGen = Callable[
|
|
35
|
+
[Optional[Resource], Optional[ControlCode], Optional[AccessCode]],
|
|
36
|
+
AccessCode
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
# 生成控制码的异步函数类型
|
|
40
|
+
ControlCodeGenAsync = Callable[[Optional[Resource]], Coroutine[None, None, ControlCode]]
|
|
41
|
+
|
|
42
|
+
# 生成访问码的异步函数类型
|
|
43
|
+
AccessCodeGenAsync = Callable[
|
|
44
|
+
[Optional[Resource], Optional[ControlCode], Optional[AccessCode]],
|
|
45
|
+
Coroutine[None, None, AccessCode]
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def async_locked(method: Callable) -> Callable:
|
|
51
|
+
"""装饰器:为异步方法自动加上 RLock"""
|
|
52
|
+
@functools.wraps(method)
|
|
53
|
+
async def wrapper(self, *args, **kwargs):
|
|
54
|
+
async with self._lock_async:
|
|
55
|
+
return await method(self, *args, **kwargs)
|
|
56
|
+
return wrapper
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ResourceManager(Generic[Resource, ControlCode, AccessCode]):
|
|
61
|
+
"""
|
|
62
|
+
Implementing access control for resources.
|
|
63
|
+
|
|
64
|
+
Resources can be any Python object.
|
|
65
|
+
|
|
66
|
+
A single resource can have multiple access codes, but only one control code.
|
|
67
|
+
|
|
68
|
+
If control/access codes, codes generators, and resources are all pickle serializable,
|
|
69
|
+
then the ResourceManager object can also be.
|
|
70
|
+
"""
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
control_code_gen: Optional[ControlCodeGen | ControlCodeGenAsync] = None,
|
|
74
|
+
access_code_gen: Optional[AccessCodeGen | AccessCodeGenAsync] = None,
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
初始化一个 ResourceManager 对象。
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
control_code_gen (Optional[ControlCodeGen | ControlCodeGenAsync]): 根据资源内容(可选)生成控制码的函数。
|
|
81
|
+
access_code_gen (Optional[AccessCodeGen | AccessCodeGenAsync]): 根据资源内容(可选)、控制码(可选)、
|
|
82
|
+
父级访问码(可选)生成访问码的函数。
|
|
83
|
+
"""
|
|
84
|
+
# 控制码、访问码生成函数
|
|
85
|
+
if not control_code_gen:
|
|
86
|
+
control_code_gen = uuid4
|
|
87
|
+
|
|
88
|
+
if not access_code_gen:
|
|
89
|
+
access_code_gen = control_code_gen
|
|
90
|
+
|
|
91
|
+
self._control_code_gen_raw_async: ControlCodeGenAsync = to_async(control_code_gen)
|
|
92
|
+
self._access_code_gen_raw_async: AccessCodeGenAsync = to_async(access_code_gen)
|
|
93
|
+
|
|
94
|
+
# 存储控制码和资源的关系:control_code -> resource
|
|
95
|
+
self._control_resource_map: Dict[ControlCode, Resource] = {}
|
|
96
|
+
|
|
97
|
+
# 存储访问码和控制码的关系:access_code -> control_code
|
|
98
|
+
self._access_control_map: Dict[AccessCode, ControlCode] = {}
|
|
99
|
+
|
|
100
|
+
# 存储资源的访问码树:control_code -> code_tree
|
|
101
|
+
self._control_code_tree_map: Dict[ControlCode, Tree] = {}
|
|
102
|
+
|
|
103
|
+
# 异步锁,用于线程安全
|
|
104
|
+
self._lock_async = AsyncRLock()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def __len__(self) -> int:
|
|
108
|
+
"""
|
|
109
|
+
返回资源数量。
|
|
110
|
+
"""
|
|
111
|
+
return len(self._control_resource_map)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ==== 子类可能需要重载的方法 ====
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
async def _get_control_code_async(self, code: Code) -> ControlCode:
|
|
118
|
+
"""
|
|
119
|
+
返回 code 的控制码。
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
code (Code): 控制码或访问码。
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
ControlCode: code 对应的控制码。
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
PermissionInsufficient: 如果 code 不存在。
|
|
129
|
+
"""
|
|
130
|
+
if await self.is_control_code_async(code):
|
|
131
|
+
return code
|
|
132
|
+
if await self.is_access_code_async(code):
|
|
133
|
+
return self._access_control_map[code]
|
|
134
|
+
raise PermissionInsufficient("Code not found")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
async def _get_tree_async(self, code: Code) -> Tree:
|
|
138
|
+
"""
|
|
139
|
+
返回 code 的访问控制树。
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
code (Code): 控制码或访问码。
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Tree: code 所在的访问控制树。
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
PermissionInsufficient: 如果 code 不存在。
|
|
149
|
+
ResourceManagerError: 内部错误:找不到访问树。
|
|
150
|
+
"""
|
|
151
|
+
control_code = await self._get_control_code_async(code)
|
|
152
|
+
code_tree = self._control_code_tree_map.get(control_code)
|
|
153
|
+
if not code_tree:
|
|
154
|
+
raise ResourceManagerError("Code tree not found")
|
|
155
|
+
return code_tree
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
async def _access_async(
|
|
159
|
+
self,
|
|
160
|
+
control_code: ControlCode,
|
|
161
|
+
parent_code: Optional[Code] = None,
|
|
162
|
+
child_code: Optional[AccessCode] = None
|
|
163
|
+
) -> AccessCode:
|
|
164
|
+
"""
|
|
165
|
+
生成一个访问码。
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
control_code (ControlCode): 控制码。
|
|
169
|
+
parent_code (Optional[Code]): 分享者。
|
|
170
|
+
child_code (Optional[AccessCode]): 自定义的访问码。若缺省,则由 access_code_gen 生成。
|
|
171
|
+
|
|
172
|
+
Raises:
|
|
173
|
+
CodeExistError: 如果提供的 child_code 已存在,或 access_code_gen 生成了重复的访问码。
|
|
174
|
+
PermissionInsufficient: 如果不存在 control_code 。
|
|
175
|
+
"""
|
|
176
|
+
if not await self.is_control_code_async(control_code):
|
|
177
|
+
raise PermissionInsufficient("Control code not found")
|
|
178
|
+
|
|
179
|
+
# 生成访问码
|
|
180
|
+
access_code = child_code or await self.access_code_gen_async(
|
|
181
|
+
await self.get_async(control_code),
|
|
182
|
+
control_code,
|
|
183
|
+
parent_code
|
|
184
|
+
)
|
|
185
|
+
if await self.is_access_code_async(access_code):
|
|
186
|
+
raise CodeExistError("Access code already exists")
|
|
187
|
+
|
|
188
|
+
self._access_control_map[access_code] = control_code
|
|
189
|
+
|
|
190
|
+
if not parent_code:
|
|
191
|
+
parent_code = control_code
|
|
192
|
+
|
|
193
|
+
# 将访问码添加到访问码树中
|
|
194
|
+
code_tree = self._control_code_tree_map[control_code]
|
|
195
|
+
code_tree.create_node(identifier=access_code, parent=parent_code)
|
|
196
|
+
|
|
197
|
+
return access_code
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def _create_tree_async(self, control_code: ControlCode) -> Tree:
|
|
201
|
+
"""
|
|
202
|
+
创建一棵访问树。
|
|
203
|
+
"""
|
|
204
|
+
code_tree = Tree()
|
|
205
|
+
code_tree.create_node(identifier=control_code)
|
|
206
|
+
self._control_code_tree_map[control_code] = code_tree
|
|
207
|
+
return code_tree
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def _get_resource_async(self, control_code: ControlCode) -> Resource:
|
|
211
|
+
"""
|
|
212
|
+
返回资源。
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
code (Code): 控制码或访问码。
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Resource: control_code 对应的资源。
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
PermissionInsufficient: 如果 control_code 不是控制码。
|
|
222
|
+
"""
|
|
223
|
+
return self._control_resource_map[control_code]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
async def _set_resource_async(self, control_code: ControlCode, resource: Resource) -> None:
|
|
227
|
+
"""
|
|
228
|
+
记录资源。
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
control_code (Code): 控制码。
|
|
232
|
+
"""
|
|
233
|
+
self._control_resource_map[control_code] = resource
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
async def _delete_resource_async(self, control_code: ControlCode) -> None:
|
|
237
|
+
"""
|
|
238
|
+
删除资源。
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
control_code (Code): 控制码。
|
|
242
|
+
"""
|
|
243
|
+
self._control_resource_map.pop(control_code, None)
|
|
244
|
+
self._control_code_tree_map.pop(control_code, None)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ==== 子类应该不用重载的方法 ====
|
|
248
|
+
|
|
249
|
+
@async_locked
|
|
250
|
+
async def control_code_gen_async(self, resource: Optional[Resource] = None) -> ControlCode:
|
|
251
|
+
"""
|
|
252
|
+
包装了生成控制码的函数,使其能够接受接受 resource 参数。
|
|
253
|
+
"""
|
|
254
|
+
try:
|
|
255
|
+
return await self._control_code_gen_raw_async(resource)
|
|
256
|
+
except TypeError:
|
|
257
|
+
return await self._control_code_gen_raw_async()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@async_locked
|
|
261
|
+
async def access_code_gen_async(
|
|
262
|
+
self,
|
|
263
|
+
resource: Optional[Resource] = None,
|
|
264
|
+
control_code: Optional[ControlCode] = None,
|
|
265
|
+
parent_access_code: Optional[AccessCode] = None
|
|
266
|
+
) -> AccessCode:
|
|
267
|
+
"""
|
|
268
|
+
包装生成访问码的异步函数,使其能够接受 resource、control_code、parent_access_code 参数。
|
|
269
|
+
"""
|
|
270
|
+
try:
|
|
271
|
+
return await self._access_code_gen_raw_async(resource, control_code, parent_access_code)
|
|
272
|
+
except TypeError:
|
|
273
|
+
try:
|
|
274
|
+
return await self._access_code_gen_raw_async(resource, control_code)
|
|
275
|
+
except TypeError:
|
|
276
|
+
try:
|
|
277
|
+
return await self._access_code_gen_raw_async(resource)
|
|
278
|
+
except TypeError:
|
|
279
|
+
return await self._access_code_gen_raw_async()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@async_locked
|
|
283
|
+
async def is_control_code_async(self, code: Code) -> bool:
|
|
284
|
+
"""
|
|
285
|
+
判断一个 code 是否是控制码。
|
|
286
|
+
"""
|
|
287
|
+
return code in self._control_resource_map
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@async_locked
|
|
291
|
+
async def is_access_code_async(self, code: Code) -> bool:
|
|
292
|
+
"""
|
|
293
|
+
判断一个 code 是否是访问码。
|
|
294
|
+
"""
|
|
295
|
+
return code in self._access_control_map
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@async_locked
|
|
299
|
+
async def create_async(
|
|
300
|
+
self,
|
|
301
|
+
resource: Resource,
|
|
302
|
+
control_code: Optional[ControlCode] = None
|
|
303
|
+
) -> ControlCode:
|
|
304
|
+
"""
|
|
305
|
+
创建一个资源。
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
resource (Resource): 资源。
|
|
309
|
+
control_code (Optional[ControlCode]): 自定义的控制码。若缺省,则由 control_code_gen 生成。
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
CodeExistError: 如果提供的 control_code 已存在,或 control_code_gen 生成了重复的控制码。
|
|
313
|
+
"""
|
|
314
|
+
# 生成控制码
|
|
315
|
+
control_code = control_code or await self.control_code_gen_async(resource)
|
|
316
|
+
if await self.is_control_code_async(control_code):
|
|
317
|
+
raise CodeExistError("Control code already exists")
|
|
318
|
+
|
|
319
|
+
# 记录资源
|
|
320
|
+
await self._set_resource_async(control_code, resource)
|
|
321
|
+
|
|
322
|
+
# 初始化 code_tree
|
|
323
|
+
await self._create_tree_async(control_code)
|
|
324
|
+
|
|
325
|
+
return control_code
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@async_locked
|
|
329
|
+
async def replace_async(self, control_code: ControlCode, new_resource: Resource) -> None:
|
|
330
|
+
"""
|
|
331
|
+
替换一项资源。
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
control_code (ControlCode): 被操作的控制码。
|
|
335
|
+
new_resource (Resource): 新的资源。
|
|
336
|
+
"""
|
|
337
|
+
if not await self.is_control_code_async(control_code):
|
|
338
|
+
raise PermissionInsufficient("Control code not found")
|
|
339
|
+
await self._set_resource_async(control_code, new_resource)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@async_locked
|
|
343
|
+
async def delete_async(self, control_code: ControlCode) -> None:
|
|
344
|
+
"""
|
|
345
|
+
删除一项资源及其控制码、所有访问码。
|
|
346
|
+
"""
|
|
347
|
+
if not await self.is_control_code_async(control_code):
|
|
348
|
+
raise PermissionInsufficient("Control code not found")
|
|
349
|
+
|
|
350
|
+
# 获取该资源对应的访问码树
|
|
351
|
+
code_tree = self._control_code_tree_map.get(control_code)
|
|
352
|
+
if code_tree:
|
|
353
|
+
# 删除所有访问码映射
|
|
354
|
+
for node in code_tree.all_nodes():
|
|
355
|
+
if node.identifier != control_code:
|
|
356
|
+
self._access_control_map.pop(node.identifier, None)
|
|
357
|
+
|
|
358
|
+
# 删除资源映射
|
|
359
|
+
await self._delete_resource_async(control_code)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@async_locked
|
|
363
|
+
async def get_async(self, code: Code) -> Resource:
|
|
364
|
+
"""
|
|
365
|
+
获取一项资源。
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
code (Code): 控制码或访问码。
|
|
369
|
+
|
|
370
|
+
Raises:
|
|
371
|
+
PermissionInsufficient: 如果 code 不存在。
|
|
372
|
+
"""
|
|
373
|
+
control_code = await self._get_control_code_async(code)
|
|
374
|
+
return await self._get_resource_async(control_code)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@async_locked
|
|
378
|
+
async def share_async(self, parent_code: Code, child_code: Optional[AccessCode] = None) -> AccessCode:
|
|
379
|
+
"""
|
|
380
|
+
生成一个新的访问码。
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
parent_code (Optional[Code]): 分享者。
|
|
384
|
+
child_code (Optional[AccessCode]): 自定义的访问码。若缺省,则由 access_code_gen 生成。
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
CodeExistError: 如果提供的 child_code 已存在,或 access_code_gen 生成了重复的访问码 。
|
|
388
|
+
PermissionInsufficient: 如果不存在 parent_code 。
|
|
389
|
+
"""
|
|
390
|
+
control_code = await self._get_control_code_async(parent_code)
|
|
391
|
+
return await self._access_async(
|
|
392
|
+
control_code=control_code,
|
|
393
|
+
parent_code=parent_code,
|
|
394
|
+
child_code=child_code
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@async_locked
|
|
399
|
+
async def revoke_async(self, ancestor_code: Code, descendant_code: AccessCode) -> None:
|
|
400
|
+
"""
|
|
401
|
+
撤销一个访问码及其所有子访问码,不能撤销自己。
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
ancestor_code (Code): 撤销发起者。
|
|
405
|
+
descendant_code (Code): 被撤销者。
|
|
406
|
+
|
|
407
|
+
Raises:
|
|
408
|
+
PermissionInsufficient:
|
|
409
|
+
- 如果不存在 ancestor_code 或 descendant_code 。
|
|
410
|
+
- 如果 descendant_code 不是 ancestor_code 的后代。
|
|
411
|
+
|
|
412
|
+
ResourceManagerError: 内部错误:找不到访问树。
|
|
413
|
+
"""
|
|
414
|
+
# 获取访问码树
|
|
415
|
+
code_tree = await self._get_tree_async(ancestor_code)
|
|
416
|
+
|
|
417
|
+
# 检查 ancestor_code 是否是 descendant_code 的祖先节点(TreeLib 的实现已保证自己不是自己的祖先)
|
|
418
|
+
if (descendant_code not in code_tree) or (not code_tree.is_ancestor(ancestor_code, descendant_code)):
|
|
419
|
+
raise PermissionInsufficient("Cannot revoke: not ancestor of the descendant code")
|
|
420
|
+
|
|
421
|
+
# 获取删除的树节点(descendant_code 及其子树)
|
|
422
|
+
subtree = code_tree.remove_subtree(descendant_code)
|
|
423
|
+
subtree_nodes = subtree.all_nodes()
|
|
424
|
+
|
|
425
|
+
# 删除访问码映射
|
|
426
|
+
for node in subtree_nodes:
|
|
427
|
+
self._access_control_map.pop(node.identifier, None)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@async_locked
|
|
431
|
+
async def get_access_codes_async(self, code: Code) -> List[AccessCode]:
|
|
432
|
+
"""
|
|
433
|
+
查看 code 的所有后代。
|
|
434
|
+
|
|
435
|
+
Raises:
|
|
436
|
+
PermissionInsufficient: 如果 code 不存在。
|
|
437
|
+
ResourceManagerError: 内部错误:找不到访问树。
|
|
438
|
+
"""
|
|
439
|
+
# 获取访问码树
|
|
440
|
+
code_tree = await self._get_tree_async(code)
|
|
441
|
+
|
|
442
|
+
# 直接获取整个子树,然后排除根节点自身
|
|
443
|
+
try:
|
|
444
|
+
subtree = code_tree.subtree(code)
|
|
445
|
+
except NodeIDAbsentError:
|
|
446
|
+
# 如果 code 不在树中(可能已被删除),返回空列表
|
|
447
|
+
return []
|
|
448
|
+
|
|
449
|
+
return [
|
|
450
|
+
node.identifier
|
|
451
|
+
for node in subtree.all_nodes()
|
|
452
|
+
if node.identifier != code
|
|
453
|
+
]
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@async_locked
|
|
457
|
+
async def transfer_async(
|
|
458
|
+
self,
|
|
459
|
+
old_control_code: ControlCode,
|
|
460
|
+
new_control_code: ControlCode
|
|
461
|
+
) -> None:
|
|
462
|
+
"""
|
|
463
|
+
转让资源所有者。
|
|
464
|
+
|
|
465
|
+
原控制码及其访问树被删除,新控制码的资源被修改为原控制码的资源。
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
old_control_code (ControlCode): 原控制码。
|
|
469
|
+
new_control_code (ControlCode): 新控制码。
|
|
470
|
+
"""
|
|
471
|
+
if not await self.is_control_code_async(old_control_code):
|
|
472
|
+
raise PermissionInsufficient("Old control code not found")
|
|
473
|
+
if not await self.is_control_code_async(new_control_code):
|
|
474
|
+
raise PermissionInsufficient("New control code not found")
|
|
475
|
+
if old_control_code == new_control_code:
|
|
476
|
+
return
|
|
477
|
+
|
|
478
|
+
# 获取原资源
|
|
479
|
+
resource = await self.get_async(old_control_code)
|
|
480
|
+
|
|
481
|
+
# 将资源转移到新控制码
|
|
482
|
+
await self.replace_async(new_control_code, resource)
|
|
483
|
+
|
|
484
|
+
# 移除原访问码树
|
|
485
|
+
await self.delete_async(old_control_code)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
# ==== 异步方法转同步 ====
|
|
489
|
+
|
|
490
|
+
def control_code_gen(self, resource: Optional[Resource] = None) -> ControlCode:
|
|
491
|
+
""" Synchronous version of `.control_code_gen_async`. """
|
|
492
|
+
return asyncio.run(self.control_code_gen_async(resource))
|
|
493
|
+
|
|
494
|
+
def access_code_gen(
|
|
495
|
+
self, resource: Optional[Resource] = None,
|
|
496
|
+
control_code: Optional[ControlCode] = None,
|
|
497
|
+
parent_access_code: Optional[AccessCode] = None
|
|
498
|
+
) -> AccessCode:
|
|
499
|
+
""" Synchronous version of `.access_code_gen_async`. """
|
|
500
|
+
return asyncio.run(self.access_code_gen_async(resource, control_code, parent_access_code))
|
|
501
|
+
|
|
502
|
+
def is_control_code(self, code: Code) -> bool:
|
|
503
|
+
""" Synchronous version of `.is_control_code_async`. """
|
|
504
|
+
return asyncio.run(self.is_control_code_async(code))
|
|
505
|
+
|
|
506
|
+
def is_access_code(self, code: Code) -> bool:
|
|
507
|
+
""" Synchronous version of `.is_access_code_async`. """
|
|
508
|
+
return asyncio.run(self.is_access_code_async(code))
|
|
509
|
+
|
|
510
|
+
def create(self, resource: Resource, control_code: Optional[ControlCode] = None) -> ControlCode:
|
|
511
|
+
""" Synchronous version of `.create_async`. """
|
|
512
|
+
return asyncio.run(self.create_async(resource, control_code))
|
|
513
|
+
|
|
514
|
+
def replace(self, control_code: ControlCode, new_resource: Resource) -> None:
|
|
515
|
+
""" Synchronous version of `.replace_async`. """
|
|
516
|
+
return asyncio.run(self.replace_async(control_code, new_resource))
|
|
517
|
+
|
|
518
|
+
def delete(self, control_code: ControlCode) -> None:
|
|
519
|
+
""" Synchronous version of `.delete_async`. """
|
|
520
|
+
return asyncio.run(self.delete_async(control_code))
|
|
521
|
+
|
|
522
|
+
def get(self, code: Code) -> Resource:
|
|
523
|
+
""" Synchronous version of `.get_async`. """
|
|
524
|
+
return asyncio.run(self.get_async(code))
|
|
525
|
+
|
|
526
|
+
def share(self, parent_code: Code, child_code: Optional[AccessCode] = None) -> AccessCode:
|
|
527
|
+
""" Synchronous version of `.share_async`. """
|
|
528
|
+
return asyncio.run(self.share_async(parent_code, child_code))
|
|
529
|
+
|
|
530
|
+
def revoke(self, ancestor_code: Code, descendant_code: AccessCode) -> None:
|
|
531
|
+
""" Synchronous version of `.revoke_async`. """
|
|
532
|
+
return asyncio.run(self.revoke_async(ancestor_code, descendant_code))
|
|
533
|
+
|
|
534
|
+
def get_access_codes(self, code: Code) -> List[AccessCode]:
|
|
535
|
+
""" Synchronous version of `.get_access_codes_async`. """
|
|
536
|
+
return asyncio.run(self.get_access_codes_async(code))
|
|
537
|
+
|
|
538
|
+
def transfer(self, old_control_code: ControlCode, new_control_code: ControlCode) -> None:
|
|
539
|
+
""" Synchronous version of `.transfer_async`. """
|
|
540
|
+
return asyncio.run(self.transfer_async(old_control_code, new_control_code))
|
access_control/utils.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
提供了一些与同步、异步函数转换有关的工厂函数。
|
|
3
|
+
|
|
4
|
+
Pickle 友好。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from functools import partial
|
|
9
|
+
from inspect import isawaitable
|
|
10
|
+
from typing import Any, Awaitable, AsyncContextManager, Callable, ContextManager, Optional
|
|
11
|
+
|
|
12
|
+
# 同步函数类型
|
|
13
|
+
SyncFunction = Callable[..., Any]
|
|
14
|
+
|
|
15
|
+
# 异步函数类型
|
|
16
|
+
AsyncFunction = Callable[..., Awaitable[Any]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def _run_func_async(func: Callable, *args, **kwargs):
|
|
20
|
+
"""
|
|
21
|
+
异步运行一个同步或异步函数。
|
|
22
|
+
"""
|
|
23
|
+
result = func(*args, **kwargs)
|
|
24
|
+
if isawaitable(result):
|
|
25
|
+
return await result
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
def to_async(func: Callable) -> AsyncFunction:
|
|
29
|
+
"""
|
|
30
|
+
把同步或异步函数转换为异步函数。
|
|
31
|
+
"""
|
|
32
|
+
return partial(_run_func_async, func)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _run_func_sync(func: Callable, *args, **kwargs):
|
|
36
|
+
"""
|
|
37
|
+
同步运行一个同步或异步函数。
|
|
38
|
+
"""
|
|
39
|
+
result = func(*args, **kwargs)
|
|
40
|
+
if isawaitable(result):
|
|
41
|
+
return asyncio.run(result)
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
def to_sync(func: Callable) -> SyncFunction:
|
|
45
|
+
"""
|
|
46
|
+
把同步或异步函数转换为同步函数。
|
|
47
|
+
"""
|
|
48
|
+
return partial(_run_func_sync, func)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def run_within_context(context: ContextManager, func: SyncFunction, *args, **kwargs):
|
|
52
|
+
"""
|
|
53
|
+
将函数 func 放在上下文里执行。
|
|
54
|
+
"""
|
|
55
|
+
with context:
|
|
56
|
+
return func(*args, **kwargs)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def run_within_context_async(context: AsyncContextManager, func: AsyncFunction, *args, **kwargs):
|
|
60
|
+
"""
|
|
61
|
+
Asynchronous version of `run_within_context`.
|
|
62
|
+
"""
|
|
63
|
+
async with context:
|
|
64
|
+
return await func(*args, **kwargs)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AsyncRLock:
|
|
68
|
+
"""简单的异步可重入锁,兼容 Python 3.10"""
|
|
69
|
+
def __init__(self):
|
|
70
|
+
self._lock = asyncio.Lock()
|
|
71
|
+
self._owner: Optional[int] = None # 持有锁的 task id
|
|
72
|
+
self._count = 0
|
|
73
|
+
|
|
74
|
+
async def acquire(self):
|
|
75
|
+
current_task = id(asyncio.current_task())
|
|
76
|
+
if self._owner == current_task:
|
|
77
|
+
self._count += 1
|
|
78
|
+
return True
|
|
79
|
+
await self._lock.acquire()
|
|
80
|
+
self._owner = current_task
|
|
81
|
+
self._count = 1
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def release(self):
|
|
85
|
+
if self._owner != id(asyncio.current_task()):
|
|
86
|
+
raise RuntimeError("Cannot release a lock that is not held by the current task")
|
|
87
|
+
self._count -= 1
|
|
88
|
+
if self._count == 0:
|
|
89
|
+
self._owner = None
|
|
90
|
+
self._lock.release()
|
|
91
|
+
|
|
92
|
+
async def __aenter__(self):
|
|
93
|
+
await self.acquire()
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
97
|
+
self.release()
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: access-control-async
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: A brief description of your access control library
|
|
5
|
+
Author-email: Jerry <wujr24@m.fudan.edu.cn>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Jerry-Wu-GitHub/access-control
|
|
8
|
+
Project-URL: Repository, https://github.com/Jerry-Wu-GitHub/access-control.git
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: treelib
|
|
16
|
+
Requires-Dist: aiofiles
|
|
17
|
+
Requires-Dist: pathvalidate
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# Access Control
|
|
21
|
+
|
|
22
|
+
一个轻量级的 Python 资源访问控制库,支持基于控制码(Control Code)和访问码(Access Code)的细粒度权限管理,并提供可选的磁盘持久化能力。
|
|
23
|
+
|
|
24
|
+
## 特性
|
|
25
|
+
|
|
26
|
+
- **两级授权模型**:每个资源拥有唯一的控制码(所有者权限)和多个访问码(分享权限)
|
|
27
|
+
- **树形访问结构**:访问码可继续衍生子访问码,形成层级分享关系
|
|
28
|
+
- **同步与异步双接口**:所有方法均提供 `*_async` 异步版本和同步版本
|
|
29
|
+
- **可插拔的码生成器**:支持自定义控制码/访问码生成逻辑
|
|
30
|
+
- **磁盘持久化**:`FileBackedResourceManager` 可将资源序列化保存到文件系统
|
|
31
|
+
- **Pickle 友好**:管理器对象可序列化(需保证内部对象可 Pickle)
|
|
32
|
+
|
|
33
|
+
## 安装
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install access-control-async
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
或者直接从源码安装:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git clone https://github.com/Jerry-Wu-GitHub/access-control.git
|
|
43
|
+
cd access-control
|
|
44
|
+
pip install .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 快速开始
|
|
48
|
+
|
|
49
|
+
### 基本用法(内存存储)
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from access_control import ResourceManager
|
|
53
|
+
|
|
54
|
+
# 创建管理器(控制码和访问码默认使用 uuid4)
|
|
55
|
+
manager = ResourceManager()
|
|
56
|
+
|
|
57
|
+
# 创建一个资源
|
|
58
|
+
resource = {"data": "secret message"}
|
|
59
|
+
control_code = manager.create(resource) # 返回控制码,如 "3a6f2b1c..."
|
|
60
|
+
|
|
61
|
+
# 基于控制码分享访问码
|
|
62
|
+
access_code1 = manager.share(control_code)
|
|
63
|
+
access_code2 = manager.share(control_code)
|
|
64
|
+
|
|
65
|
+
# 从访问码获取资源
|
|
66
|
+
retrieved = manager.get(access_code1)
|
|
67
|
+
assert retrieved == resource
|
|
68
|
+
|
|
69
|
+
# 查看某个码的所有后代访问码
|
|
70
|
+
codes = manager.get_access_codes(control_code) # [access_code1, access_code2]
|
|
71
|
+
|
|
72
|
+
# 撤销一个访问码及其所有子访问码
|
|
73
|
+
manager.revoke(control_code, access_code1)
|
|
74
|
+
|
|
75
|
+
# 删除整个资源(包括所有访问码)
|
|
76
|
+
manager.delete(control_code)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 异步接口
|
|
80
|
+
|
|
81
|
+
所有操作都提供 `*_async` 异步版本,便于集成到异步应用中:
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
import asyncio
|
|
85
|
+
from access_control import ResourceManager
|
|
86
|
+
|
|
87
|
+
async def main():
|
|
88
|
+
manager = ResourceManager()
|
|
89
|
+
control_code = await manager.create_async({"data": "async test"})
|
|
90
|
+
access_code = await manager.share_async(control_code)
|
|
91
|
+
resource = await manager.get_async(access_code)
|
|
92
|
+
print(resource)
|
|
93
|
+
|
|
94
|
+
asyncio.run(main())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 使用文件持久化
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
import asyncio
|
|
101
|
+
from access_control import FileBackedResourceManager
|
|
102
|
+
|
|
103
|
+
async def main():
|
|
104
|
+
# 资源将保存在 ./data 目录下
|
|
105
|
+
manager = FileBackedResourceManager("./data")
|
|
106
|
+
|
|
107
|
+
# 创建资源(自动序列化到磁盘)
|
|
108
|
+
resource = {"user": "alice", "score": 100}
|
|
109
|
+
control_code = await manager.create_async(resource)
|
|
110
|
+
|
|
111
|
+
# 即使重启程序,只要 data 目录存在,即可恢复
|
|
112
|
+
new_manager = FileBackedResourceManager("./data")
|
|
113
|
+
same_resource = await new_manager.get_async(control_code)
|
|
114
|
+
print(same_resource)
|
|
115
|
+
|
|
116
|
+
asyncio.run(main())
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 自定义码生成器
|
|
120
|
+
|
|
121
|
+
你可以传入自己的生成函数(同步或异步均可):
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import hashlib
|
|
125
|
+
from access_control import ResourceManager
|
|
126
|
+
|
|
127
|
+
def my_control_code_gen(resource):
|
|
128
|
+
# 根据资源内容生成控制码
|
|
129
|
+
return hashlib.md5(str(resource).encode()).hexdigest()[:8]
|
|
130
|
+
|
|
131
|
+
def my_access_code_gen(resource, control_code, parent_code):
|
|
132
|
+
# 自定义访问码生成逻辑
|
|
133
|
+
return f"share_{control_code}_{parent_code}"
|
|
134
|
+
|
|
135
|
+
manager = ResourceManager(
|
|
136
|
+
control_code_gen=my_control_code_gen,
|
|
137
|
+
access_code_gen=my_access_code_gen
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
control = manager.create({"foo": "bar"})
|
|
141
|
+
print(control) # 输出类似 "d3b07384"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## API 概览
|
|
145
|
+
|
|
146
|
+
### `ResourceManager[Resource, ControlCode, AccessCode]`
|
|
147
|
+
|
|
148
|
+
主要方法(均提供同步版本和 `*_async` 异步版本):
|
|
149
|
+
|
|
150
|
+
| 方法 | 描述 |
|
|
151
|
+
|------|------|
|
|
152
|
+
| `create(resource, control_code=None)` | 创建资源,返回控制码 |
|
|
153
|
+
| `get(code)` | 通过控制码或访问码获取资源 |
|
|
154
|
+
| `share(parent_code, child_code=None)` | 生成新的访问码 |
|
|
155
|
+
| `revoke(ancestor_code, descendant_code)` | 撤销一个访问码及其所有子访问码 |
|
|
156
|
+
| `get_access_codes(code)` | 返回某个码的所有后代访问码 |
|
|
157
|
+
| `replace(control_code, new_resource)` | 替换控制码对应的资源 |
|
|
158
|
+
| `transfer(old_control, new_control)` | 将资源所有权转移给另一个控制码 |
|
|
159
|
+
| `delete(control_code)` | 删除资源及其所有关联码 |
|
|
160
|
+
| `is_control_code(code)` / `is_access_code(code)` | 判断码的类型 |
|
|
161
|
+
|
|
162
|
+
### `FileBackedResourceManager`
|
|
163
|
+
|
|
164
|
+
继承自 `ResourceManager`,额外接受以下构造参数:
|
|
165
|
+
|
|
166
|
+
- `data_dir_path`: 存储文件的目录路径
|
|
167
|
+
- `dumps_function` / `loads_function`: 自定义序列化函数(默认 `pickle.dumps` / `pickle.loads`)
|
|
168
|
+
- `file_name_gen`: 根据资源和控制码生成文件名的函数(默认使用 `uuid4`)
|
|
169
|
+
|
|
170
|
+
所有资源的存取均自动读写磁盘文件。
|
|
171
|
+
|
|
172
|
+
## 依赖
|
|
173
|
+
|
|
174
|
+
- Python >= 3.10
|
|
175
|
+
- `treelib` – 用于维护访问码树结构
|
|
176
|
+
- `aiofiles` – 异步文件操作
|
|
177
|
+
- `pathvalidate` – 校验文件名的合法性
|
|
178
|
+
|
|
179
|
+
## 许可证
|
|
180
|
+
|
|
181
|
+
本项目使用 [MIT 许可证](LICENSE)。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
access_control/__init__.py,sha256=ignBnuu2uQQiQIJnIcLTPHLpbfVoQ6abXcN1XgIZKzI,234
|
|
2
|
+
access_control/exceptions.py,sha256=frKL4gKgisnGwnlZlqKUJeBFCQvtLdPN428Obr4HkkM,380
|
|
3
|
+
access_control/file_backed_resource_manager.py,sha256=0Mb16KL5_qlnA4gFG-j7RhONuNDS3c-s9NmcHb2Q2UQ,7012
|
|
4
|
+
access_control/resource_manager.py,sha256=nDr5g5pHRqnNbMAhKvbIXInizZe2B3gHAjrrHq7RzIg,19151
|
|
5
|
+
access_control/utils.py,sha256=aL8lDKE4ynVIZ6wVV1B2vE28UN0aK2rgZiBux381Fms,2684
|
|
6
|
+
access_control_async-0.2.5.dist-info/licenses/LICENSE,sha256=errzu9jbnGCiEZ50jZxag9tkLf5opFvlVTDC8yfYlQA,1081
|
|
7
|
+
access_control_async-0.2.5.dist-info/METADATA,sha256=d3LcuRFrl366JuWhK626RpbVV12z7nEkUk_L1hKQNQc,5841
|
|
8
|
+
access_control_async-0.2.5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
access_control_async-0.2.5.dist-info/top_level.txt,sha256=Ydmx4mMJO0arjcPAL3zqz9yMQjF69QogVQtoQJEeOok,15
|
|
10
|
+
access_control_async-0.2.5.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jerry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
access_control
|