@@ -118,6 +118,66 @@ impl Default for CommentOnWorkItemConfig {
118118 }
119119}
120120
121+ /// Validate that a work item is allowed by the configured target policy.
122+ ///
123+ /// Returns `Ok(None)` when the work item is allowed, `Ok(Some(msg))` when it is
124+ /// rejected by policy (the caller should return `ExecutionResult::failure(msg)`),
125+ /// or `Err(…)` on an unexpected infrastructure failure.
126+ async fn validate_target_policy (
127+ target : & CommentTarget ,
128+ client : & reqwest:: Client ,
129+ org_url : & str ,
130+ project : & str ,
131+ token : & str ,
132+ work_item_id : i64 ,
133+ ) -> anyhow:: Result < Option < String > > {
134+ match target. allows_id ( work_item_id) {
135+ Some ( true ) => {
136+ debug ! ( "Work item #{} allowed by target policy" , work_item_id) ;
137+ Ok ( None )
138+ }
139+ Some ( false ) => Ok ( Some ( format ! (
140+ "Work item #{} is not in the allowed target set" ,
141+ work_item_id
142+ ) ) ) ,
143+ None => {
144+ // Area path validation — `allows_id` returns `None` only for
145+ // `StringTarget(s != "*")`, so `area_path_prefix` is always `Some` here.
146+ let prefix = target
147+ . area_path_prefix ( )
148+ . expect ( "allows_id returned None but area_path_prefix is also None" ) ;
149+ debug ! (
150+ "Validating area path for work item #{} against prefix '{}'" ,
151+ work_item_id, prefix
152+ ) ;
153+ match get_work_item_area_path ( client, org_url, project, token, work_item_id) . await {
154+ Ok ( area_path) => {
155+ // ADO area paths are case-insensitive and use backslash separators.
156+ // Require the match to land on a path boundary so that prefix "4x4"
157+ // doesn't accidentally match "4x4Production".
158+ let ap = area_path. to_lowercase ( ) ;
159+ let pf = prefix. to_lowercase ( ) ;
160+ let is_match =
161+ ap == pf || ( ap. starts_with ( & * pf) && ap[ pf. len ( ) ..] . starts_with ( '\\' ) ) ;
162+ if is_match {
163+ debug ! ( "Area path '{}' validated against '{}'" , area_path, prefix) ;
164+ Ok ( None )
165+ } else {
166+ Ok ( Some ( format ! (
167+ "Work item #{} has area path '{}' which is not under allowed prefix '{}'" ,
168+ work_item_id, area_path, prefix
169+ ) ) )
170+ }
171+ }
172+ Err ( e) => Ok ( Some ( format ! (
173+ "Failed to validate area path for work item #{}: {}" ,
174+ work_item_id, e
175+ ) ) ) ,
176+ }
177+ }
178+ }
179+ }
180+
121181/// Fetch a work item's area path from the ADO API
122182async fn get_work_item_area_path (
123183 client : & reqwest:: Client ,
@@ -216,57 +276,11 @@ impl Executor for CommentOnWorkItemResult {
216276 let client = reqwest:: Client :: new ( ) ;
217277
218278 // Validate work item ID against target policy
219- match target. allows_id ( self . work_item_id ) {
220- Some ( true ) => {
221- debug ! ( "Work item #{} allowed by target policy" , self . work_item_id) ;
222- }
223- Some ( false ) => {
224- return Ok ( ExecutionResult :: failure ( format ! (
225- "Work item #{} is not in the allowed target set" ,
226- self . work_item_id
227- ) ) ) ;
228- }
229- None => {
230- // Area path validation — need to fetch the work item.
231- // Invariant: allows_id returns None only for StringTarget(s != "*"),
232- // and area_path_prefix returns Some for exactly that case.
233- let prefix = match target. area_path_prefix ( ) {
234- Some ( p) => p,
235- None => {
236- unreachable ! ( "allows_id returned None but area_path_prefix is also None" )
237- }
238- } ;
239- debug ! (
240- "Validating area path for work item #{} against prefix '{}'" ,
241- self . work_item_id, prefix
242- ) ;
243- match get_work_item_area_path ( & client, org_url, project, token, self . work_item_id )
244- . await
245- {
246- Ok ( area_path) => {
247- // ADO area paths are case-insensitive and use backslash separators.
248- // Require the match to land on a path boundary so that prefix "4x4"
249- // doesn't accidentally match "4x4Production".
250- let ap = area_path. to_lowercase ( ) ;
251- let pf = prefix. to_lowercase ( ) ;
252- let is_match =
253- ap == pf || ( ap. starts_with ( & * pf) && ap[ pf. len ( ) ..] . starts_with ( '\\' ) ) ;
254- if !is_match {
255- return Ok ( ExecutionResult :: failure ( format ! (
256- "Work item #{} has area path '{}' which is not under allowed prefix '{}'" ,
257- self . work_item_id, area_path, prefix
258- ) ) ) ;
259- }
260- debug ! ( "Area path '{}' validated against '{}'" , area_path, prefix) ;
261- }
262- Err ( e) => {
263- return Ok ( ExecutionResult :: failure ( format ! (
264- "Failed to validate area path for work item #{}: {}" ,
265- self . work_item_id, e
266- ) ) ) ;
267- }
268- }
269- }
279+ if let Some ( rejection_msg) =
280+ validate_target_policy ( target, & client, org_url, project, token, self . work_item_id )
281+ . await ?
282+ {
283+ return Ok ( ExecutionResult :: failure ( rejection_msg) ) ;
270284 }
271285
272286 // Build the Azure DevOps REST API URL for adding a comment
0 commit comments