Coverage for tests/test_security.py: 100%

1"""

2安全模块测试

3

4测试覆盖：

5- 权限配置加载

6- 路径访问检查

7- 命令安全检查

8- 权限继承

9"""

10 import pytest

11 from pathlib import Path

12 from qrclaw.security import (

13 SecurityManager,

14 AgentPermission,

15 PermissionConfig,

16 _get_effective_agent_id,

17 DANGEROUS_COMMANDS,

18 PROTECTED_PATHS,

19 PERMISSION_DENIED_SUFFIX,

20)

21

22

23 class TestPermissionModel:

24 """测试权限模型"""

25

26 def test_agent_permission_defaults(self):

27 """测试默认权限配置"""

28 perm = AgentPermission()

29

30 assert perm.access == "scoped"

31 assert perm.allow_paths == []

32

33 def test_agent_permission_full(self):

34 """测试完全访问权限"""

35 perm = AgentPermission(access="full")

36

37 assert perm.access == "full"

38

39 def test_permission_config_defaults(self):

40 """测试默认全局配置"""

41 config = PermissionConfig()

42

43 assert config.default_policy == "restricted"

44 assert config.agents == {}

45

46

47 class TestSecurityManager:

48 """测试安全管理器"""

49

50 def test_singleton_pattern(self):

51 """测试单例模式"""

52 manager1 = SecurityManager()

53 manager2 = SecurityManager()

54

55 assert manager1 is manager2

56

57 def test_get_permission_default_agent(self):

58 """测试获取 default agent 权限"""

59 manager = SecurityManager()

60 perm = manager.get_permission("default")

61

62 # default agent 应该有 full access

63 assert perm.access == "full"

64

65 def test_get_permission_unknown_agent(self):

66 """测试获取未知 agent 权限"""

67 manager = SecurityManager()

68 perm = manager.get_permission("unknown_agent")

69

70 # 未知 agent 应该使用默认策略

71 assert perm.access == "scoped"

72

73

74 class TestPathSafety:

75 """测试路径安全检查"""

76

77 def test_check_path_in_workspace(self, temp_dir):

78 """测试工作区内的路径"""

79 manager = SecurityManager()

80

81 # scoped agent 访问 workspace 内的文件应该放行

82 manager._check_path_safety(

83 path_str=str(temp_dir / "test.txt"),

84 perm=AgentPermission(access="scoped"),

85 workspace_root=temp_dir,

86 tool_name="read_file",

87 agent_id="test_agent"

88 )

89 # 不抛异常 = 通过

90

91 def test_check_path_outside_workspace(self, temp_dir):

92 """测试工作区外的路径"""

93 manager = SecurityManager()

94

95 # scoped agent 访问 workspace 外的文件应该拒绝

96 with pytest.raises(PermissionError) as exc_info:

97 manager._check_path_safety(

98 path_str="/tmp/outside.txt",

99 perm=AgentPermission(access="scoped"),

100 workspace_root=temp_dir,

101 tool_name="read_file",

102 agent_id="test_agent"

103 )

104

105 assert "无权访问" in str(exc_info.value)

106

107 def test_check_path_whitelist(self, temp_dir):

108 """测试白名单路径"""

109 manager = SecurityManager()

110

111 # 允许访问 /tmp

112 perm = AgentPermission(

113 access="scoped",

114 allow_paths=["/tmp"]

115 )

116

117 # 应该放行

118 manager._check_path_safety(

119 path_str="/tmp/allowed.txt",

120 perm=perm,

121 workspace_root=temp_dir,

122 tool_name="read_file",

123 agent_id="test_agent"

124 )

125

126 def test_check_path_readonly_write_outside(self, temp_dir):

127 """测试只读权限在白名单外写入"""

128 manager = SecurityManager()

129

130 # 白名单路径

131 perm = AgentPermission(access="readonly", allow_paths=["/tmp"])

132

133 # 只读权限尝试写入白名单路径应该拒绝

134 with pytest.raises(PermissionError) as exc_info:

135 manager._check_path_safety(

136 path_str="/tmp/test.txt",

137 perm=perm,

138 workspace_root=temp_dir,

139 tool_name="write_file",

140 agent_id="test_agent"

141 )

142

143 assert "只读" in str(exc_info.value)

144

145 def test_check_path_readonly_read(self, temp_dir):

146 """测试只读权限读取"""

147 manager = SecurityManager()

148

149 perm = AgentPermission(access="readonly")

150

151 # 只读权限读取应该放行

152 manager._check_path_safety(

153 path_str=str(temp_dir / "test.txt"),

154 perm=perm,

155 workspace_root=temp_dir,

156 tool_name="read_file",

157 agent_id="test_agent"

158 )

159

160 def test_check_protected_path(self, temp_dir):

161 """测试系统保护路径"""

162 manager = SecurityManager()

163

164 # 保护路径（如 permissions.yaml）

165 protected_path = str(PROTECTED_PATHS[0])

166

167 # 非 default agent 应该拒绝

168 with pytest.raises(PermissionError) as exc_info:

169 manager._check_path_safety(

170 path_str=protected_path,

171 perm=AgentPermission(access="full"),

172 workspace_root=temp_dir,

173 tool_name="read_file",

174 agent_id="not_default"

175 )

176

177 assert "系统配置文件" in str(exc_info.value)

178

179

180 class TestShellSafety:

181 """测试 Shell 命令安全检查"""

182

183 def test_dangerous_commands_patterns(self):

184 """测试高危命令正则"""

185 # 测试一些高危命令

186 dangerous_examples = [

187 "rm -rf /",

188 "rm -rf ~",

189 "rm -rf *",

190 "mkfs /dev/sda1",

191 "chmod 777 /",

192 ]

193

194 import re

195 for cmd in dangerous_examples:

196 # 至少匹配一个高危模式

197 matches = [re.search(pattern, cmd, re.IGNORECASE) for pattern in DANGEROUS_COMMANDS]

198 assert any(matches), f"命令 '{cmd}' 应该被识别为高危"

199

200 def test_check_shell_safety_dangerous_command(self, temp_dir):

201 """测试高危命令检查"""

202 manager = SecurityManager()

203

204 # 高危命令应该直接拒绝

205 with pytest.raises(PermissionError) as exc_info:

206 manager._check_shell_safety(

207 command="rm -rf /",

208 perm=AgentPermission(access="scoped"),

209 workspace_root=temp_dir,

210 agent_id="test_agent"

211 )

212

213 assert "高危命令" in str(exc_info.value)

214

215

216 class TestPermissionInheritance:

217 """测试权限继承"""

218

219 def test_get_effective_agent_id_no_parent(self):

220 """测试没有父 agent 的情况"""

221 # 清空父 agent 映射

222 from qrclaw.tools import spawn_agent

223 spawn_agent._parent_agent_map.clear()

224

225 result = _get_effective_agent_id("standalone_agent")

226

227 assert result == "standalone_agent"

228

229 def test_get_effective_agent_id_with_parent(self):

230 """测试有父 agent 的情况"""

231 from qrclaw.tools.spawn_agent import set_parent_agent, clear_parent_agent

232

233 # 设置父子关系

234 set_parent_agent("child_agent", "parent_agent")

235

236 # 应该返回父 agent

237 result = _get_effective_agent_id("child_agent")

238

239 # 清理

240 clear_parent_agent("child_agent")

241

242 # 由于递归查找，最终应该返回 parent_agent

243 # 但如果 parent_agent 没有父 agent，就返回 parent_agent

244 assert result == "parent_agent"

245

246

247 class TestAccessCheck:

248 """测试完整访问检查"""

249

250 def test_check_access_full_permission(self, temp_dir):

251 """测试完全权限"""

252 manager = SecurityManager()

253

254 # full access 应该放行所有操作

255 manager.check_access(

256 agent_id="default",

257 tool_name="read_file",

258 args={"path": "/any/path"},

259 workspace_root=temp_dir

260 )

261

262 def test_check_access_scoped_in_workspace(self, temp_dir):

263 """测试受限权限在 workspace 内"""

264 manager = SecurityManager()

265

266 # scoped 在 workspace 内应该放行

267 manager.check_access(

268 agent_id="test_agent",

269 tool_name="read_file",

270 args={"path": str(temp_dir / "test.txt")},

271 workspace_root=temp_dir

272 )

273

274 def test_check_access_scoped_outside_workspace(self, temp_dir):

275 """测试受限权限在 workspace 外"""

276 manager = SecurityManager()

277

278 # scoped 在 workspace 外应该拒绝

279 with pytest.raises(PermissionError):

280 manager.check_access(

281 agent_id="test_agent",

282 tool_name="read_file",

283 args={"path": "/tmp/outside.txt"},

284 workspace_root=temp_dir

285 )

286

287 def test_check_access_no_path_arg(self, temp_dir):

288 """测试无路径参数的工具"""

289 manager = SecurityManager()

290

291 # 无路径参数的工具应该跳过路径检查

292 manager.check_access(

293 agent_id="test_agent",

294 tool_name="some_tool",

295 args={},

296 workspace_root=temp_dir

297 )