@@ -474,3 +474,136 @@ describe('DelegatedAdminGate — self-service delegation of duty (ADR-0091 D3)',
474474 . rejects . toThrow ( / a u d i e n c e a n c h o r / ) ;
475475 } ) ;
476476} ) ;
477+
478+ // ── [cloud#830 follow-up] Self-delegation anchor containment ────────────────
479+ //
480+ // cloud#830 (C1 position-anchor) made sys_user_position.business_unit_id
481+ // visibility LOAD-BEARING: it is the readScope depth anchor, so a
482+ // `unit`/`unit_and_below` holder sees the owner set rooted at that BU (and, for
483+ // unit_and_below, its whole subtree). The self-delegation (D3) path stamped the
484+ // anchor without any subtree/source constraint, so a holder of a delegatable
485+ // non-admin position anchored at a LOW BU could delegate it to a co-conspirator
486+ // with an ANCESTOR/arbitrary-high anchor — leaking that BU's whole subtree,
487+ // beyond the delegator's own range (lateral visibility escalation). The fix
488+ // requires the delegated anchor to fall inside the delegator's OWN effective
489+ // anchor for the position (same spirit as the D12 delegated-admin subtree
490+ // check), fail-closed.
491+ //
492+ // Topology:
493+ // hq (bu_hq)
494+ // ├── east (bu_east) ← u_boss's own approver anchor
495+ // │ └── east_sales (bu_es)
496+ // └── west (bu_west)
497+ function makeAnchoredDelegationHarness ( nowMs = T0 ) {
498+ const tables : Record < string , any [ ] > = {
499+ sys_business_unit : [
500+ { id : 'bu_hq' , name : 'hq' , parent_business_unit_id : null } ,
501+ { id : 'bu_east' , name : 'east' , parent_business_unit_id : 'bu_hq' } ,
502+ { id : 'bu_es' , name : 'east_sales' , parent_business_unit_id : 'bu_east' } ,
503+ { id : 'bu_west' , name : 'west' , parent_business_unit_id : 'bu_hq' } ,
504+ ] ,
505+ sys_position : [ { id : 'p_appr' , name : 'approver' , delegatable : true } ] ,
506+ sys_permission_set : [ { id : 's_appr' , name : 'approve_set' } ] ,
507+ sys_position_permission_set : [ { id : 'b_appr' , position_id : 'p_appr' , permission_set_id : 's_appr' } ] ,
508+ // u_boss holds approver DIRECTLY, anchored at east.
509+ sys_user_position : [ { id : 'ha' , user_id : 'u_boss' , position : 'approver' , business_unit_id : 'bu_east' } ] ,
510+ sys_business_unit_member : [ { id : 'm_boss' , user_id : 'u_boss' , business_unit_id : 'bu_es' } ] ,
511+ sys_user : [ { id : 'u_boss' } , { id : 'u_deleg' } ] ,
512+ } ;
513+ const matches = ( row : any , where : any ) : boolean =>
514+ Object . entries ( where ?? { } ) . every ( ( [ k , v ] ) => {
515+ if ( v && typeof v === 'object' && Array . isArray ( ( v as any ) . $in ) ) return ( v as any ) . $in . includes ( row [ k ] ) ;
516+ return row [ k ] === v ;
517+ } ) ;
518+ const ql = {
519+ tables,
520+ async find ( object : string , opts : any ) {
521+ const rows = ( tables [ object ] ?? [ ] ) . filter ( ( r ) => matches ( r , opts ?. where ) ) ;
522+ return typeof opts ?. limit === 'number' ? rows . slice ( 0 , opts . limit ) : rows ;
523+ } ,
524+ async findOne ( object : string , opts : any ) {
525+ return ( tables [ object ] ?? [ ] ) . filter ( ( r ) => matches ( r , opts ?. where ) ) [ 0 ] ?? null ;
526+ } ,
527+ } as any ;
528+ const gate = new DelegatedAdminGate ( {
529+ ql,
530+ resolveSets : async ( ) => [ { name : 'member_default' , objects : { } } ] ,
531+ now : ( ) => nowMs ,
532+ } ) ;
533+ const delegate = ( userId : string , row : any ) =>
534+ gate . assert ( { object : 'sys_user_position' , operation : 'insert' , data : row , context : { userId, positions : [ ] } } ) ;
535+ const base = ( extra : any ) => ( {
536+ user_id : 'u_deleg' , position : 'approver' , delegated_from : 'u_boss' ,
537+ valid_until : iso ( nowMs + 5 * DAY ) , reason : 'coverage' , ...extra ,
538+ } ) ;
539+ return { gate, ql, tables, delegate, base } ;
540+ }
541+
542+ describe ( 'DelegatedAdminGate — self-delegation anchor containment (cloud#830 follow-up)' , ( ) => {
543+ it ( 'anchor equal to the delegator\'s own anchor (east) is allowed' , async ( ) => {
544+ const d = makeAnchoredDelegationHarness ( ) ;
545+ await expect ( d . delegate ( 'u_boss' , d . base ( { business_unit_id : 'bu_east' } ) ) ) . resolves . toBeUndefined ( ) ;
546+ } ) ;
547+
548+ it ( 'anchor inside the delegator\'s own subtree (east_sales) is allowed — narrowing' , async ( ) => {
549+ const d = makeAnchoredDelegationHarness ( ) ;
550+ await expect ( d . delegate ( 'u_boss' , d . base ( { business_unit_id : 'bu_es' } ) ) ) . resolves . toBeUndefined ( ) ;
551+ } ) ;
552+
553+ it ( 'anchor at an ANCESTOR BU (hq) is DENIED — a delegation may not widen visibility' , async ( ) => {
554+ const d = makeAnchoredDelegationHarness ( ) ;
555+ await expect ( d . delegate ( 'u_boss' , d . base ( { business_unit_id : 'bu_hq' } ) ) )
556+ . rejects . toThrow ( / o n l y n a r r o w s | n e v e r w i d e n / ) ;
557+ } ) ;
558+
559+ it ( 'anchor at an UNRELATED sibling BU (west) is DENIED — outside your own effective anchor' , async ( ) => {
560+ const d = makeAnchoredDelegationHarness ( ) ;
561+ await expect ( d . delegate ( 'u_boss' , d . base ( { business_unit_id : 'bu_west' } ) ) )
562+ . rejects . toThrow ( / o u t s i d e y o u r o w n e f f e c t i v e a n c h o r / ) ;
563+ } ) ;
564+
565+ it ( 'an unanchored delegation row keeps prior behavior (no anchor to widen)' , async ( ) => {
566+ const d = makeAnchoredDelegationHarness ( ) ;
567+ await expect ( d . delegate ( 'u_boss' , d . base ( { } ) ) ) . resolves . toBeUndefined ( ) ;
568+ } ) ;
569+
570+ it ( 'mutual delegation cannot cross ranges: neither direction may hand out the other\'s BU' , async ( ) => {
571+ // u_boss anchored east may not delegate a west anchor…
572+ const d = makeAnchoredDelegationHarness ( ) ;
573+ await expect ( d . delegate ( 'u_boss' , d . base ( { business_unit_id : 'bu_west' } ) ) )
574+ . rejects . toThrow ( / o u t s i d e y o u r o w n e f f e c t i v e a n c h o r / ) ;
575+ // …and a would-be west holder cannot reach east either (no east holding to source it).
576+ d . tables . sys_user_position . push ( { id : 'hb' , user_id : 'u_deleg' , position : 'approver' , business_unit_id : 'bu_west' } ) ;
577+ await expect ( d . delegate ( 'u_deleg' , { user_id : 'u_boss' , position : 'approver' , delegated_from : 'u_deleg' , valid_until : iso ( T0 + 5 * DAY ) , reason : 'x' , business_unit_id : 'bu_east' } ) )
578+ . rejects . toThrow ( / o u t s i d e y o u r o w n e f f e c t i v e a n c h o r / ) ;
579+ } ) ;
580+
581+ it ( 'when the delegator holds the position UNANCHORED, the anchor is bounded by their MEMBER BU' , async ( ) => {
582+ const d = makeAnchoredDelegationHarness ( ) ;
583+ // Drop the anchor on u_boss's own holding; boss is a member of east_sales.
584+ d . tables . sys_user_position . find ( ( r : any ) => r . id === 'ha' ) . business_unit_id = null ;
585+ // Member-BU (east_sales) or below is allowed…
586+ await expect ( d . delegate ( 'u_boss' , d . base ( { business_unit_id : 'bu_es' } ) ) ) . resolves . toBeUndefined ( ) ;
587+ // …but the parent (east) — above the member BU — is not.
588+ await expect ( d . delegate ( 'u_boss' , d . base ( { business_unit_id : 'bu_east' } ) ) )
589+ . rejects . toThrow ( / o u t s i d e y o u r o w n e f f e c t i v e a n c h o r / ) ;
590+ } ) ;
591+
592+ it ( 'fail-closed: an anchor that cannot be validated (delegator has no resolvable range) is refused' , async ( ) => {
593+ const d = makeAnchoredDelegationHarness ( ) ;
594+ // Boss holds approver unanchored AND has no BU membership → no provable range.
595+ d . tables . sys_user_position . find ( ( r : any ) => r . id === 'ha' ) . business_unit_id = null ;
596+ d . tables . sys_business_unit_member . length = 0 ;
597+ await expect ( d . delegate ( 'u_boss' , d . base ( { business_unit_id : 'bu_es' } ) ) )
598+ . rejects . toThrow ( / c a n n o t b e v a l i d a t e d / ) ;
599+ } ) ;
600+
601+ it ( 'a holding acquired VIA delegation cannot source an anchored re-delegation (chains stay cut)' , async ( ) => {
602+ const d = makeAnchoredDelegationHarness ( ) ;
603+ // u_relay holds approver only via delegation, anchored east.
604+ d . tables . sys_user . push ( { id : 'u_relay' } ) ;
605+ d . tables . sys_user_position . push ( { id : 'hd' , user_id : 'u_relay' , position : 'approver' , business_unit_id : 'bu_east' , delegated_from : 'u_boss' , valid_until : iso ( T0 + 20 * DAY ) } ) ;
606+ await expect ( d . delegate ( 'u_relay' , { user_id : 'u_deleg' , position : 'approver' , delegated_from : 'u_relay' , valid_until : iso ( T0 + 5 * DAY ) , reason : 'x' , business_unit_id : 'bu_es' } ) )
607+ . rejects . toThrow ( / o n l y v i a d e l e g a t i o n / ) ;
608+ } ) ;
609+ } ) ;
0 commit comments