@@ -559,15 +559,26 @@ const SHARING_READER = { userId: 'u_reader', positions: ['qa_pos'], permissions:
559559const SHARING_WRITER = { userId : 'u_writer' , positions : [ 'qa_pos' ] , permissions : [ 'qa_line_writer' ] , posture : 'MEMBER' } ;
560560/** Read and write depth `own`: the control. */
561561const SHARING_OWN = { userId : 'u_own' , positions : [ 'qa_pos' ] , permissions : [ 'qa_line_own' ] , posture : 'MEMBER' } ;
562+ /**
563+ * Read depth `org`, write depth `own`, and an app-authored row-level `update`
564+ * and `delete` policy admitting every line not named `unnamed`
565+ * ({@link bootSharing}'s `authored` rows): record sharing refuses this writer
566+ * every line owned by someone else, and the by-id write gate defers that
567+ * refusal to the policy.
568+ */
569+ const AUTHORED_WRITER = { userId : 'u_authored' , positions : [ 'qa_pos' ] , permissions : [ 'qa_line_authored' ] , posture : 'MEMBER' } ;
562570
563571/**
564572 * A `private`-OWD object with an `owner_id`, the real `SharingService` as the
565573 * kernel's `sharing` service and its middleware on the engine after this
566574 * plugin's, as the platform boots them. Row `l_other` is owned by nobody the
567575 * rig explains. `faultReadFilter` makes the sharing service's read filter
568- * reject, as a share store that cannot be read does.
576+ * reject, as a share store that cannot be read does. `authored` adds
577+ * {@link AUTHORED_WRITER}'s set and three lines: `l_theirs` (owned by
578+ * someone else, admitted by the policy), `l_unnamed` (owned by someone else,
579+ * not admitted) and `l_mine` (owned by the writer).
569580 */
570- async function bootSharing ( opts : { faultReadFilter ?: boolean } = { } ) {
581+ async function bootSharing ( opts : { faultReadFilter ?: boolean ; authored ?: boolean } = { } ) {
571582 const LINE = `qa_parity_line_${ next ( ) } ` ;
572583 const engine = new ObjectQL ( ) ;
573584 engine . registerDriver (
@@ -601,6 +612,13 @@ async function bootSharing(opts: { faultReadFilter?: boolean } = {}) {
601612 { id : 'l_other' , name : 'owned by someone else' , owner_id : 'u_somebody' } ,
602613 { id : 'l_own' , name : 'owned by the control' , owner_id : 'u_own' } ,
603614 ] , SYS ) ;
615+ if ( opts . authored ) {
616+ await engine . insert ( LINE , [
617+ { id : 'l_theirs' , name : 'theirs' , owner_id : 'u_somebody' } ,
618+ { id : 'l_unnamed' , name : 'unnamed' , owner_id : 'u_somebody' } ,
619+ { id : 'l_mine' , name : 'mine' , owner_id : AUTHORED_WRITER . userId } ,
620+ ] , SYS ) ;
621+ }
604622
605623 const sets = [
606624 MEMBER_DEFAULT ,
@@ -613,6 +631,18 @@ async function bootSharing(opts: { faultReadFilter?: boolean } = {}) {
613631 name : 'qa_line_own' ,
614632 objects : { [ LINE ] : { allowRead : true , allowEdit : true , readScope : 'own' , writeScope : 'own' } } ,
615633 } ) ,
634+ ...( opts . authored
635+ ? [ PermissionSetSchema . parse ( {
636+ name : 'qa_line_authored' ,
637+ objects : {
638+ [ LINE ] : { allowRead : true , allowEdit : true , allowDelete : true , allowTransfer : true , readScope : 'org' , writeScope : 'own' } ,
639+ } ,
640+ rowLevelSecurity : [
641+ { name : 'qa_line_named_updates' , object : LINE , operation : 'update' , using : "name != 'unnamed'" } ,
642+ { name : 'qa_line_named_deletes' , object : LINE , operation : 'delete' , using : "name != 'unnamed'" } ,
643+ ] ,
644+ } ) ]
645+ : [ ] ) ,
616646 ] ;
617647 let security : unknown ;
618648 let sharing : SharingService | undefined ;
@@ -645,11 +675,21 @@ async function bootSharing(opts: { faultReadFilter?: boolean } = {}) {
645675 engine . registerMiddleware ( buildSharingMiddleware ( sharing , ctx . logger ) as never , { object : '*' } ) ;
646676
647677 return {
648- explain : ( caller : object , operation : 'read' | 'update' , recordId : string ) =>
678+ explain : ( caller : object , operation : 'read' | 'update' | 'delete' | 'transfer' , recordId : string ) =>
649679 explained ( plugin . explainAccessForCaller ( { object : LINE , operation, recordId } , { ...caller } ) ) ,
650680 find : ( caller : object , id : string ) => rowsOf ( engine . find ( LINE , { where : { id } , context : { ...caller } } as never ) ) ,
651681 update : ( caller : object , id : string ) =>
652682 landed ( engine . update ( LINE , { name : 'renamed' } , { where : { id } , context : { ...caller } } as never ) ) ,
683+ remove : ( caller : object , id : string ) => landed ( engine . delete ( LINE , { where : { id } , context : { ...caller } } as never ) ) ,
684+ /** A transfer is a by-id update of `owner_id`, gated by the transfer grant. */
685+ transfer : ( caller : object , id : string ) =>
686+ landed ( engine . update ( LINE , { owner_id : 'u_recipient' } , { where : { id } , context : { ...caller } } as never ) ) ,
687+ /**
688+ * Record sharing's tri-state update verdict, asked as the `sys_attachment`
689+ * and `sys_comment` parent gates ask it before anything else: the caller's
690+ * envelope with no operation-private key.
691+ */
692+ checkEdit : ( caller : object , id : string ) => sharing ! . checkEdit ( LINE , id , { ...caller } as never ) ,
653693 teardown : async ( ) => {
654694 try { await engine . destroy ( ) ; } catch { /* noop */ }
655695 releaseAsyncContexts ( engine , plugin ) ;
@@ -700,7 +740,7 @@ const withRls = (predicate: string, f: (rig: RlsRig) => Promise<RowFaces>, opts:
700740 try { return { ...( await f ( rig ) ) , teardown : rig . teardown } ; } catch ( e ) { await rig . teardown ( ) ; throw e ; }
701741} ;
702742const withSharing = (
703- opts : { faultReadFilter ?: boolean } ,
743+ opts : { faultReadFilter ?: boolean ; authored ?: boolean } ,
704744 f : ( rig : Awaited < ReturnType < typeof bootSharing > > ) => Promise < RowFaces > ,
705745) : RowRun => async ( ) => {
706746 const rig = await bootSharing ( opts ) ;
@@ -935,6 +975,48 @@ const TABLE: Row[] = [
935975 explain : await r . explain ( SHARING_OWN , 'update' , 'l_other' ) , enforce : await r . update ( SHARING_OWN , 'l_other' ) ,
936976 } ) ) ,
937977 } ,
978+ // #24647: the by-id write gate defers record sharing's refusal to an
979+ // app-authored row-level write policy that admits the row (ADR-0105 D3).
980+ {
981+ card : '#24647' , shape : 'private OWD, an `own` writer whose authored update policy admits a row owned by someone else, update' ,
982+ position : 'record.visible' ,
983+ enforced : 'admitted' ,
984+ run : withSharing ( { authored : true } , async ( r ) => ( {
985+ explain : await r . explain ( AUTHORED_WRITER , 'update' , 'l_theirs' ) , enforce : await r . update ( AUTHORED_WRITER , 'l_theirs' ) ,
986+ } ) ) ,
987+ } ,
988+ {
989+ card : '#24647' , shape : 'private OWD, an `own` writer whose authored delete policy admits a row owned by someone else, delete' ,
990+ position : 'record.visible' ,
991+ enforced : 'admitted' ,
992+ run : withSharing ( { authored : true } , async ( r ) => ( {
993+ explain : await r . explain ( AUTHORED_WRITER , 'delete' , 'l_theirs' ) , enforce : await r . remove ( AUTHORED_WRITER , 'l_theirs' ) ,
994+ } ) ) ,
995+ } ,
996+ {
997+ card : '#24647 control' , shape : 'private OWD, the same writer, a row owned by someone else that no authored policy admits, update' ,
998+ position : 'record.visible' ,
999+ enforced : REFUSED_DENIED ,
1000+ run : withSharing ( { authored : true } , async ( r ) => ( {
1001+ explain : await r . explain ( AUTHORED_WRITER , 'update' , 'l_unnamed' ) , enforce : await r . update ( AUTHORED_WRITER , 'l_unnamed' ) ,
1002+ } ) ) ,
1003+ } ,
1004+ {
1005+ card : '#24647 control' , shape : 'private OWD, the same writer, a row owned by someone else that no authored policy admits, delete' ,
1006+ position : 'record.visible' ,
1007+ enforced : REFUSED_DENIED ,
1008+ run : withSharing ( { authored : true } , async ( r ) => ( {
1009+ explain : await r . explain ( AUTHORED_WRITER , 'delete' , 'l_unnamed' ) , enforce : await r . remove ( AUTHORED_WRITER , 'l_unnamed' ) ,
1010+ } ) ) ,
1011+ } ,
1012+ {
1013+ card : '#24647 probe' , shape : 'private OWD, the same writer, a row owned by someone else the policy admits, transfer' ,
1014+ position : 'record.visible' ,
1015+ enforced : 'admitted' ,
1016+ run : withSharing ( { authored : true } , async ( r ) => ( {
1017+ explain : await r . explain ( AUTHORED_WRITER , 'transfer' , 'l_theirs' ) , enforce : await r . transfer ( AUTHORED_WRITER , 'l_theirs' ) ,
1018+ } ) ) ,
1019+ } ,
9381020 // #20002: a dependency enforcement shares with explain throws.
9391021 {
9401022 card : '#20002' , shape : 'the sharing read filter throws, the caller\'s own row, read' , position : 'record.visible' ,
@@ -999,3 +1081,58 @@ describe('security.explain answers what enforcement does — the enumeration', (
9991081 } , 60_000 ) ;
10001082 }
10011083} ) ;
1084+
1085+ /**
1086+ * #24647's row, beyond the verdict the table compares: what the record story
1087+ * names, and what the `sys_attachment` and `sys_comment` parent gates answer.
1088+ *
1089+ * Both gates ask record sharing's `checkEdit` FIRST, with the caller's envelope
1090+ * and no operation-private key, refuse on its `deny` without asking anything
1091+ * further, and ask explain's record-grained `update` verdict only once that
1092+ * step admits (each gate's own parity table pins it: its `sharing deny` row
1093+ * refuses with explain never called, though the double would admit). So on a
1094+ * row the deferral admits, record sharing's own `deny` still decides both
1095+ * gates: explain admitting it cannot widen either one.
1096+ */
1097+ describe ( 'an app-authored row-level write policy the by-id write gate defers to (ADR-0105 D3)' , ( ) => {
1098+ it ( 'the update story names the deferral: record sharing excludes the row, row-level security admits it' , async ( ) => {
1099+ const rig = await bootSharing ( { authored : true } ) ;
1100+ try {
1101+ const x = await rig . explain ( AUTHORED_WRITER , 'update' , 'l_theirs' ) ;
1102+ expect ( x . kind , describeExplained ( x ) ) . toBe ( 'decision' ) ;
1103+ if ( x . kind !== 'decision' ) return ;
1104+ expect ( x . decision . record ) . toEqual ( { recordId : 'l_theirs' , visible : true , decidedBy : 'rls' } ) ;
1105+ const sharingRecord = x . decision . layers . find ( ( l ) => l . layer === 'sharing' ) ?. record ;
1106+ expect ( sharingRecord ?. outcome ) . toBe ( 'excluded' ) ;
1107+ expect ( sharingRecord ?. detail ) . toMatch ( / a p p - a u t h o r e d r o w - l e v e l u p d a t e p o l i c y a d m i t s t h i s r e c o r d / ) ;
1108+ expect ( x . decision . layers . find ( ( l ) => l . layer === 'rls' ) ?. record ?. outcome ) . toBe ( 'admitted' ) ;
1109+ } finally {
1110+ await rig . teardown ( ) ;
1111+ }
1112+ } , 60_000 ) ;
1113+
1114+ it ( "the parent gates' first question, record sharing, still refuses the row explain now admits" , async ( ) => {
1115+ const rig = await bootSharing ( { authored : true } ) ;
1116+ try {
1117+ expect ( await rig . checkEdit ( AUTHORED_WRITER , 'l_theirs' ) ) . toBe ( 'deny' ) ;
1118+ const x = await rig . explain ( AUTHORED_WRITER , 'update' , 'l_theirs' ) ;
1119+ expect ( x . kind === 'decision' ? x . decision . record ?. visible : x . kind , describeExplained ( x ) ) . toBe ( true ) ;
1120+ expect ( await rig . update ( AUTHORED_WRITER , 'l_theirs' ) ) . toEqual ( { kind : 'admitted' } ) ;
1121+ } finally {
1122+ await rig . teardown ( ) ;
1123+ }
1124+ } , 60_000 ) ;
1125+
1126+ it ( 'the positive control: a row record sharing admits is admitted by the first question, by explain and by the door' , async ( ) => {
1127+ const rig = await bootSharing ( { authored : true } ) ;
1128+ try {
1129+ expect ( await rig . checkEdit ( AUTHORED_WRITER , 'l_mine' ) ) . toBe ( 'allow' ) ;
1130+ const x = await rig . explain ( AUTHORED_WRITER , 'update' , 'l_mine' ) ;
1131+ expect ( x . kind === 'decision' ? x . decision . record : x . kind , describeExplained ( x ) )
1132+ . toEqual ( { recordId : 'l_mine' , visible : true , decidedBy : 'rls' } ) ;
1133+ expect ( await rig . update ( AUTHORED_WRITER , 'l_mine' ) ) . toEqual ( { kind : 'admitted' } ) ;
1134+ } finally {
1135+ await rig . teardown ( ) ;
1136+ }
1137+ } , 60_000 ) ;
1138+ } ) ;
0 commit comments