1use crate::agent::AgentConfig;
2use crate::assertion::Assertion;
3use crate::test_case::TestCase;
4use crate::TestCaseBuilder;
5
6pub struct RbacMatrix {
8 roles: Vec<(String, Vec<String>)>,
9}
10
11impl RbacMatrix {
12 pub fn new() -> Self {
13 Self { roles: vec![] }
14 }
15
16 pub fn role(mut self, name: &str, tools: &[&str]) -> Self {
18 self.roles.push((
19 name.to_string(),
20 tools.iter().map(|s| s.to_string()).collect(),
21 ));
22 self
23 }
24
25 pub fn tools_for(&self, role: &str) -> Vec<&str> {
27 self.roles
28 .iter()
29 .find(|(r, _)| r == role)
30 .map(|(_, tools)| tools.iter().map(|s| s.as_str()).collect())
31 .unwrap_or_default()
32 }
33
34 pub fn all_tools(&self) -> Vec<&str> {
36 let mut all: Vec<&str> = self
37 .roles
38 .iter()
39 .flat_map(|(_, tools)| tools.iter().map(|s| s.as_str()))
40 .collect();
41 all.sort();
42 all.dedup();
43 all
44 }
45
46 pub fn config_for_role(&self, role: &str) -> AgentConfig {
48 AgentConfig::new(serde_json::json!({"role": role}))
49 }
50
51 pub fn generate_allowlist_tests(&self, user_message: &str) -> Vec<TestCase> {
53 let mut tests = Vec::new();
54
55 for (role, role_tools) in &self.roles {
56 let all = self.all_tools();
57 let forbidden: Vec<&str> = all
58 .iter()
59 .filter(|t| !role_tools.iter().any(|rt| rt.as_str() == **t))
60 .copied()
61 .collect();
62
63 let mut builder = TestCaseBuilder::new(
64 format!("rbac-allowlist-{}", role.to_lowercase()),
65 user_message,
66 )
67 .name(format!("{} — tools within allowlist", role))
68 .tags(&["rbac", "security"])
69 .config(self.config_for_role(role))
70 .expect_tools_within_allowlist();
71
72 if !forbidden.is_empty() {
73 builder = builder.forbid_tools(&forbidden);
74 }
75
76 tests.push(builder.build());
77 }
78
79 tests
80 }
81
82 pub fn generate_scenario_tests(
85 &self,
86 id: &str,
87 msg: &str,
88 role_assertions: Vec<(&str, Vec<Assertion>)>,
89 ) -> Vec<TestCase> {
90 let mut tests = Vec::new();
91
92 for (role, assertions) in role_assertions {
93 let mut tc = TestCase {
94 id: format!("rbac-{}-{}", id, role.to_lowercase()),
95 name: Some(format!("{} — {} scenario", role, id)),
96 user_message: msg.to_string(),
97 config: self.config_for_role(role),
98 assertions,
99 tags: vec!["rbac".to_string(), "security".to_string()],
100 retries: 0,
101 consensus_runs: None,
102 consensus_required: None,
103 timeout: None,
104 };
105 tc.assertions.push(Assertion::ExpectToolsWithinAllowlist);
107 tests.push(tc);
108 }
109
110 tests
111 }
112
113 pub fn generate_injection_tests(
115 &self,
116 payloads: &[(&str, &str)],
117 ) -> Vec<TestCase> {
118 let mut tests = Vec::new();
119
120 for (role, _) in &self.roles {
121 for (payload_id, payload_msg) in payloads {
122 tests.push(
123 TestCaseBuilder::new(
124 format!(
125 "rbac-injection-{}-{}",
126 role.to_lowercase(),
127 payload_id
128 ),
129 *payload_msg,
130 )
131 .name(format!("{} — injection: {}", role, payload_id))
132 .tags(&["rbac", "security", "injection"])
133 .config(self.config_for_role(role))
134 .expect_tools_within_allowlist()
135 .build(),
136 );
137 }
138 }
139
140 tests
141 }
142}
143
144impl Default for RbacMatrix {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150pub fn forbid_tools(tools: &[&str]) -> Assertion {
154 Assertion::ForbidTools(tools.iter().map(|s| s.to_string()).collect())
155}
156
157pub fn expect_tools(tools: &[&str]) -> Assertion {
159 Assertion::ExpectTools(tools.iter().map(|s| s.to_string()).collect())
160}