返回 CodeWhale
authorization_order.rs
根目录 / crates / execpolicy / tests / authorization_order.rs
1 use codewhale_execpolicy::{
2 AskForApproval, ExecPolicyContext, ExecPolicyEngine, PermissionAction, Ruleset, ToolAskRule,
3 };
4
5 struct AuthorizationCase {
6 name: &'static str,
7 engine: ExecPolicyEngine,
8 command: &'static str,
9 approval: AskForApproval,
10 expected_allow: bool,
11 expected_approval: bool,
12 expected_phase: &'static str,
13 expected_action: Option<PermissionAction>,
14 expected_rule: Option<&'static str>,
15 }
16
17 fn command_rule(command: &str, action: PermissionAction) -> ToolAskRule {
18 ToolAskRule {
19 action,
20 ..ToolAskRule::exec_shell(command)
21 }
22 }
23
24 fn tool_rule(action: PermissionAction) -> ToolAskRule {
25 ToolAskRule {
26 action,
27 ..ToolAskRule::new("exec_shell")
28 }
29 }
30
31 #[test]
32 fn authorization_order_contract_matches_documented_precedence() {
33 let cases = vec![
34 AuthorizationCase {
35 name: "hard denied prefix beats typed allow",
36 engine: ExecPolicyEngine::with_rulesets(vec![
37 Ruleset::user(vec![], vec!["cargo publish".to_string()])
38 .with_ask_rules(vec![command_rule("cargo publish", PermissionAction::Allow)]),
39 ]),
40 command: "cargo publish --dry-run",
41 approval: AskForApproval::OnRequest,
42 expected_allow: false,
43 expected_approval: false,
44 expected_phase: "forbidden",
45 expected_action: None,
46 expected_rule: Some("cargo publish"),
47 },
48 AuthorizationCase {
49 name: "higher typed layer beats lower action and specificity",
50 engine: ExecPolicyEngine::with_rulesets(vec![
51 Ruleset::agent(vec![], vec![]).with_ask_rules(vec![command_rule(
52 "cargo test --workspace",
53 PermissionAction::Deny,
54 )]),
55 Ruleset::user(vec![], vec![])
56 .with_ask_rules(vec![command_rule("cargo test", PermissionAction::Allow)]),
57 ]),
58 command: "cargo test --workspace",
59 approval: AskForApproval::OnRequest,
60 expected_allow: true,
61 expected_approval: false,
62 expected_phase: "allowed",
63 expected_action: Some(PermissionAction::Allow),
64 expected_rule: Some("tool=exec_shell command=cargo test"),
65 },
66 AuthorizationCase {
67 name: "typed action beats specificity inside one layer",
68 engine: ExecPolicyEngine::with_rulesets(vec![
69 Ruleset::user(vec![], vec![]).with_ask_rules(vec![
70 tool_rule(PermissionAction::Deny),
71 command_rule("cargo test --workspace", PermissionAction::Allow),
72 ]),
73 ]),
74 command: "cargo test --workspace",
75 approval: AskForApproval::OnRequest,
76 expected_allow: false,
77 expected_approval: false,
78 expected_phase: "forbidden",
79 expected_action: Some(PermissionAction::Deny),
80 expected_rule: Some("tool=exec_shell"),
81 },
82 AuthorizationCase {
83 name: "specificity breaks a same-layer same-action tie",
84 engine: ExecPolicyEngine::with_rulesets(vec![
85 Ruleset::user(vec![], vec![]).with_ask_rules(vec![
86 tool_rule(PermissionAction::Allow),
87 command_rule("cargo test", PermissionAction::Allow),
88 ]),
89 ]),
90 command: "cargo test --workspace",
91 approval: AskForApproval::OnRequest,
92 expected_allow: true,
93 expected_approval: false,
94 expected_phase: "allowed",
95 expected_action: Some(PermissionAction::Allow),
96 expected_rule: Some("tool=exec_shell command=cargo test"),
97 },
98 AuthorizationCase {
99 name: "typed ask beats a trusted prefix",
100 engine: ExecPolicyEngine::with_rulesets(vec![
101 Ruleset::user(vec!["cargo test".to_string()], vec![])
102 .with_ask_rules(vec![command_rule("cargo test", PermissionAction::Ask)]),
103 ]),
104 command: "cargo test --workspace",
105 approval: AskForApproval::UnlessTrusted,
106 expected_allow: true,
107 expected_approval: true,
108 expected_phase: "needs_approval",
109 expected_action: Some(PermissionAction::Ask),
110 expected_rule: Some("tool=exec_shell command=cargo test"),
111 },
112 AuthorizationCase {
113 name: "typed ask fails closed when prompts are forbidden",
114 engine: ExecPolicyEngine::with_rulesets(vec![
115 Ruleset::user(vec![], vec![])
116 .with_ask_rules(vec![command_rule("cargo test", PermissionAction::Ask)]),
117 ]),
118 command: "cargo test --workspace",
119 approval: AskForApproval::Never,
120 expected_allow: false,
121 expected_approval: false,
122 expected_phase: "forbidden",
123 expected_action: Some(PermissionAction::Ask),
124 expected_rule: Some("tool=exec_shell command=cargo test"),
125 },
126 AuthorizationCase {
127 name: "trusted prefix feeds the approval fallback",
128 engine: ExecPolicyEngine::with_rulesets(vec![Ruleset::user(
129 vec!["cargo test".to_string()],
130 vec![],
131 )]),
132 command: "cargo test --workspace",
133 approval: AskForApproval::UnlessTrusted,
134 expected_allow: true,
135 expected_approval: false,
136 expected_phase: "allowed",
137 expected_action: None,
138 expected_rule: Some("cargo test"),
139 },
140 ];
141
142 for case in cases {
143 let decision = case
144 .engine
145 .check(ExecPolicyContext {
146 command: case.command,
147 cwd: "/workspace",
148 tool: Some("exec_shell"),
149 path: None,
150 ask_for_approval: case.approval,
151 sandbox_mode: Some("workspace-write"),
152 })
153 .unwrap_or_else(|error| panic!("{}: policy check failed: {error}", case.name));
154
155 assert_eq!(decision.allow, case.expected_allow, "{}: allow", case.name);
156 assert_eq!(
157 decision.requires_approval, case.expected_approval,
158 "{}: requires_approval",
159 case.name
160 );
161 assert_eq!(
162 decision.requirement.phase(),
163 case.expected_phase,
164 "{}: phase",
165 case.name
166 );
167 assert_eq!(
168 decision.matched_action, case.expected_action,
169 "{}: matched action",
170 case.name
171 );
172 assert_eq!(
173 decision.matched_rule.as_deref(),
174 case.expected_rule,
175 "{}: matched rule",
176 case.name
177 );
178 }
179 }
180
180 lines RUST