@@ -13,6 +13,9 @@ import {
1313 dismissAudienceBindingSuggestion ,
1414 SuggestionNotFoundError ,
1515 SuggestionStateError ,
16+ readAcceptedDefaultPermissionSetsOption ,
17+ refuseAcceptedDefaultsWithoutWall ,
18+ type DeploymentAcceptance ,
1619 type SuggestionDeps ,
1720} from './suggested-audience-bindings' ;
1821import { declareEveryoneBaseline } from './builtin-positions' ;
@@ -763,3 +766,175 @@ describe('[ADR-0086 D3] a set\'s owner is its binding — a body `packageId` on
763766 ] ) ;
764767 } ) ;
765768} ) ;
769+
770+ // ── [ADR-0131 D5] Under a wall, the deployment-level acceptance may be host
771+ // configuration — `acceptedDefaultPermissionSets` on the security plugin's
772+ // options. No organization owns anything for it (ADR-0131 §1.5), and the
773+ // `single` posture's record is untouched: every block above runs with no
774+ // `DeploymentAcceptance`, which is read as that record.
775+ describe ( '[ADR-0131 D5] the walled posture: deployment-level acceptance is host configuration' , ( ) => {
776+ const OPTION = 'acceptedDefaultPermissionSets' ;
777+ /** Two organizations, neither with slug `default`: no record owner (a walled multi-organization deployment). */
778+ const noRecordOwner = ( ql : any ) => {
779+ ql . tables . sys_organization = [ { id : 'org_a' , slug : 'a' } , { id : 'org_b' , slug : 'b' } ] ;
780+ return ql ;
781+ } ;
782+ const walled = ( declared ?: readonly string [ ] ) : DeploymentAcceptance => ( {
783+ posture : 'isolated' ,
784+ ...( declared !== undefined ? { acceptedDefaultPermissionSets : declared } : { } ) ,
785+ } ) ;
786+ /** The security plugin's composition under a wall: baseline + whatever the derivation accepts. */
787+ const walledDeps = ( ql : any , declared ?: readonly string [ ] ) : SuggestionDeps => ( {
788+ ql,
789+ posture : ( ) => 'isolated' ,
790+ acceptedDefaultPermissionSets : declared ,
791+ rederiveEveryone : async ( ) => {
792+ const { accepted } = await readAcceptedEveryoneSets ( ql , undefined , walled ( declared ) ) ;
793+ declareEveryoneBaseline ( ql . registry , SECURITY_PACKAGE , [ ...new Set ( [ ...BASELINE , ...accepted ] ) ] ) ;
794+ } ,
795+ } ) ;
796+ const capture = ( ) => {
797+ const warns : string [ ] = [ ] ;
798+ return { warns, logger : { warn : ( m : string ) => { warns . push ( m ) ; } , info : ( ) => undefined } } ;
799+ } ;
800+
801+ it ( 'a declared EMPTY list: the boot reconcile neither throws nor warns, everyone is the baseline, and the door refuses naming the option' , async ( ) => {
802+ const ql = noRecordOwner ( makeQl ( [ CRM_PACKAGE ] ) ) ;
803+ const { warns, logger } = capture ( ) ;
804+
805+ // The two `(non-fatal)` warn sites in the security plugin log exactly when this rejects.
806+ await expect ( reconcileAudienceBindingSuggestions ( ql , logger , walled ( [ ] ) ) ) . resolves . toMatchObject ( { created : 0 , unread : 0 } ) ;
807+ expect ( warns ) . toEqual ( [ ] ) ;
808+ expect ( rows ( ql ) ) . toHaveLength ( 0 ) ;
809+
810+ const deps = walledDeps ( ql , [ ] ) ;
811+ await deps . rederiveEveryone ( ) ;
812+ expect ( everyoneSets ( ql ) ) . toEqual ( BASELINE ) ;
813+ expect ( await readAcceptedEveryoneSets ( ql , undefined , walled ( [ ] ) ) ) . toEqual ( { accepted : [ ] , withheld : [ ] } ) ;
814+
815+ for ( const door of [
816+ confirmAudienceBindingSuggestion ( deps , OPERATOR_CTX , 'sug_any' ) ,
817+ dismissAudienceBindingSuggestion ( deps , OPERATOR_CTX , 'sug_any' ) ,
818+ listAudienceBindingSuggestions ( deps , OPERATOR_CTX , { } ) ,
819+ ] ) {
820+ const refusal = door . catch ( ( e : unknown ) => e ) ;
821+ await expect ( refusal ) . resolves . toMatchObject ( { code : 'SUGGESTION_STATE' , statusCode : 409 } ) ;
822+ const message = String ( ( ( await refusal ) as Error ) . message ) ;
823+ expect ( message ) . toMatch ( / ^ T h i s d e p l o y m e n t r u n s t h e ' i s o l a t e d ' t e n a n c y p o s t u r e a n d d e c l a r e s i t s a c c e p t e d p a c k a g e d e f a u l t s / ) ;
824+ expect ( message ) . toContain ( `\`${ OPTION } \`` ) ;
825+ expect ( message ) . not . toContain ( 'create the Default Organization' ) ;
826+ }
827+ // The deployment authority still answers first: a non-operator is a 403, not a 409.
828+ await expect ( confirmAudienceBindingSuggestion ( deps , ADMIN_CTX , 'sug_any' ) )
829+ . rejects . toMatchObject ( { code : 'PERMISSION_DENIED' , statusCode : 403 } ) ;
830+ } ) ;
831+
832+ it ( 'a declared set is granted through everyone — and only through the declaration (anti-vacuity: absent before)' , async ( ) => {
833+ const ql = noRecordOwner ( makeQl ( [ CRM_PACKAGE ] ) ) ;
834+ expect ( everyoneSets ( ql ) ) . not . toContain ( 'crm_readonly' ) ;
835+
836+ const derived = await readAcceptedEveryoneSets ( ql , undefined , walled ( [ 'crm_readonly' ] ) ) ;
837+ expect ( derived ) . toEqual ( { accepted : [ 'crm_readonly' ] , withheld : [ ] } ) ;
838+
839+ await walledDeps ( ql , [ 'crm_readonly' ] ) . rederiveEveryone ( ) ;
840+ expect ( everyoneSets ( ql ) ) . toEqual ( [ ...BASELINE , 'crm_readonly' ] ) ;
841+ expect ( ql . registry . getItem ( 'position' , 'everyone' ) . _packageId ) . toBe ( SECURITY_PACKAGE ) ;
842+ expect ( rows ( ql ) ) . toHaveLength ( 0 ) ;
843+ } ) ;
844+
845+ it ( 'a declared name passes the everyone anchor\'s high-risk gate — a forbidden set is withheld as forbidden, never granted (ADR-0090 D7)' , async ( ) => {
846+ const evil = {
847+ enabled : true ,
848+ manifest : {
849+ id : 'com.example.evil' ,
850+ permissions : [ { name : 'evil_default' , isDefault : true , objects : { '*' : { viewAllRecords : true } } } ] ,
851+ } ,
852+ } ;
853+ const ql = noRecordOwner ( makeQl ( [ evil ] ) ) ;
854+ const derived = await readAcceptedEveryoneSets ( ql , undefined , walled ( [ 'evil_default' ] ) ) ;
855+ expect ( derived . accepted ) . toEqual ( [ ] ) ;
856+ expect ( derived . withheld ) . toEqual ( [
857+ expect . objectContaining ( { set : 'evil_default' , forbidden : true , source : 'host-configuration' , packageId : 'com.example.evil' } ) ,
858+ ] ) ;
859+ await walledDeps ( ql , [ 'evil_default' ] ) . rederiveEveryone ( ) ;
860+ expect ( everyoneSets ( ql ) ) . toEqual ( BASELINE ) ;
861+ } ) ;
862+
863+ it ( 'a declared name the catalog does not serve as a package\'s isDefault set is withheld with its reason, as an accepted row is' , async ( ) => {
864+ const unowned = { enabled : true , manifest : { permissions : [ { name : 'loose_default' , isDefault : true , objects : { } } ] } } ;
865+ const ql = noRecordOwner ( makeQl ( [ CRM_PACKAGE , unowned ] ) ) ;
866+ const derived = await readAcceptedEveryoneSets (
867+ ql , undefined , walled ( [ 'no_such_set' , 'crm_admin' , 'loose_default' , 'crm_readonly' ] ) ,
868+ ) ;
869+ expect ( derived . accepted ) . toEqual ( [ 'crm_readonly' ] ) ;
870+ expect ( derived . withheld ) . toEqual ( [
871+ { set : 'no_such_set' , packageId : '' , forbidden : false , source : 'host-configuration' ,
872+ reason : 'the security catalog serves no set of that name (package uninstalled or disabled)' } ,
873+ { set : 'crm_admin' , packageId : 'com.example.crm' , forbidden : false , source : 'host-configuration' ,
874+ reason : 'the set no longer declares isDefault: true' } ,
875+ { set : 'loose_default' , packageId : '' , forbidden : false , source : 'host-configuration' ,
876+ reason : "the set is declared by 'no package' now" } ,
877+ ] ) ;
878+ } ) ;
879+
880+ it ( 'declared, the record is not read even where an organization could own it — one source, never two' , async ( ) => {
881+ const ql = makeQl ( [ CRM_PACKAGE ] ) ; // a `default` organization exists
882+ const deps = makeDeps ( ql ) ;
883+ const { suggestions } = await listAudienceBindingSuggestions ( deps , ADMIN_CTX , { } ) ;
884+ await confirmAudienceBindingSuggestion ( deps , ADMIN_CTX , suggestions [ 0 ] . id ) ;
885+ expect ( ( await readAcceptedEveryoneSets ( ql ) ) . accepted ) . toEqual ( [ 'crm_readonly' ] ) ;
886+
887+ expect ( await readAcceptedEveryoneSets ( ql , undefined , walled ( [ ] ) ) ) . toEqual ( { accepted : [ ] , withheld : [ ] } ) ;
888+ // The sync writes nothing; the rows it no longer reads are reported once, naming the option.
889+ const { warns, logger } = capture ( ) ;
890+ const before = JSON . stringify ( rows ( ql ) ) ;
891+ await expect ( reconcileAudienceBindingSuggestions ( ql , logger , walled ( [ ] ) ) ) . resolves . toMatchObject ( { created : 0 , unread : 1 } ) ;
892+ expect ( JSON . stringify ( rows ( ql ) ) ) . toBe ( before ) ;
893+ expect ( warns ) . toHaveLength ( 1 ) ;
894+ expect ( warns [ 0 ] ) . toContain ( `\`${ OPTION } \`` ) ;
895+ } ) ;
896+
897+ it ( 'a walled boot with NO declaration keeps today\'s behaviour, and the refusal names the option, not the Default Organization' , async ( ) => {
898+ const ql = noRecordOwner ( makeQl ( [ CRM_PACKAGE ] ) ) ;
899+ const sync = reconcileAudienceBindingSuggestions ( ql , undefined , walled ( ) ) . catch ( ( e : unknown ) => e ) ;
900+ await expect ( sync ) . resolves . toBeInstanceOf ( SuggestionStateError ) ;
901+ await expect ( sync ) . resolves . toMatchObject ( { code : 'SUGGESTION_STATE' , statusCode : 409 } ) ;
902+ const message = String ( ( ( await sync ) as Error ) . message ) ;
903+ expect ( message ) . toMatch ( / ^ T h e a u d i e n c e - b i n d i n g a c c e p t a n c e i s d e p l o y m e n t - l e v e l , a n d t h i s d e p l o y m e n t r u n s t h e ' i s o l a t e d ' t e n a n c y p o s t u r e w i t h n o o r g a n i z a t i o n t h a t c a n o w n t h e r e c o r d / ) ;
904+ expect ( message ) . toContain ( `\`${ OPTION } \`` ) ;
905+ expect ( message ) . toContain ( 'ADR-0131 §1.5' ) ;
906+ expect ( message ) . not . toContain ( 'create the Default Organization' ) ;
907+
908+ const door = confirmAudienceBindingSuggestion ( walledDeps ( ql ) , OPERATOR_CTX , 'sug_any' ) . catch ( ( e : unknown ) => e ) ;
909+ await expect ( door ) . resolves . toMatchObject ( { code : 'SUGGESTION_STATE' , statusCode : 409 } ) ;
910+ expect ( String ( ( ( await door ) as Error ) . message ) ) . toBe ( message ) ;
911+ expect ( everyoneSets ( ql ) ) . toEqual ( BASELINE ) ;
912+ } ) ;
913+
914+ it ( 'under `single` the refusal keeps D5\'s Default-Organization text, and the posture never reads a declaration' , async ( ) => {
915+ for ( const deployment of [ undefined , { posture : 'single' as const } , { posture : 'single' as const , acceptedDefaultPermissionSets : [ 'crm_readonly' ] } ] ) {
916+ const ql = noRecordOwner ( makeQl ( [ CRM_PACKAGE ] ) ) ;
917+ const sync = syncAudienceBindingSuggestions ( ql , undefined , deployment ) . catch ( ( e : unknown ) => e ) ;
918+ await expect ( sync ) . resolves . toMatchObject ( { code : 'SUGGESTION_STATE' , statusCode : 409 } ) ;
919+ const message = String ( ( ( await sync ) as Error ) . message ) ;
920+ expect ( message ) . toMatch ( / ^ T h e a u d i e n c e - b i n d i n g a c c e p t a n c e i s r e c o r d e d o n c e p e r d e p l o y m e n t a n d o w n e d b y t h e D e f a u l t O r g a n i z a t i o n / ) ;
921+ expect ( message ) . toContain ( "Remedy: create the Default Organization (slug 'default')" ) ;
922+ expect ( ( await readAcceptedEveryoneSets ( ql , undefined , deployment ) ) . accepted ) . toEqual ( [ ] ) ;
923+ }
924+ } ) ;
925+
926+ it ( 'the option is a closed list of names — anything else is refused at construction, and `single` refuses a declaration' , ( ) => {
927+ expect ( readAcceptedDefaultPermissionSetsOption ( undefined ) ) . toBeUndefined ( ) ;
928+ expect ( readAcceptedDefaultPermissionSetsOption ( [ ] ) ) . toEqual ( [ ] ) ;
929+ expect ( readAcceptedDefaultPermissionSetsOption ( [ 'a' , 'b' , 'a' ] ) ) . toEqual ( [ 'a' , 'b' ] ) ;
930+ for ( const bad of [ null , 'crm_readonly' , [ '' ] , [ ' ' ] , [ 1 ] , { } ] ) {
931+ expect ( ( ) => readAcceptedDefaultPermissionSetsOption ( bad ) ) . toThrow ( / ` a c c e p t e d D e f a u l t P e r m i s s i o n S e t s ` m u s t b e a l i s t o f p e r m i s s i o n - s e t n a m e s / ) ;
932+ }
933+ expect ( ( ) => refuseAcceptedDefaultsWithoutWall ( 'single' , [ ] ) ) . toThrow (
934+ / ` a c c e p t e d D e f a u l t P e r m i s s i o n S e t s ` i s d e c l a r e d , a n d t h i s d e p l o y m e n t r u n s t h e ' s i n g l e ' t e n a n c y p o s t u r e , w h i c h d o e s n o t r e a d i t / ,
935+ ) ;
936+ expect ( ( ) => refuseAcceptedDefaultsWithoutWall ( 'single' , undefined ) ) . not . toThrow ( ) ;
937+ expect ( ( ) => refuseAcceptedDefaultsWithoutWall ( 'isolated' , [ ] ) ) . not . toThrow ( ) ;
938+ expect ( ( ) => refuseAcceptedDefaultsWithoutWall ( 'group' , [ 'crm_readonly' ] ) ) . not . toThrow ( ) ;
939+ } ) ;
940+ } ) ;
0 commit comments