@@ -49,13 +49,30 @@ const OBJECTS = [
4949 message : RULE_MESSAGE , condition : "record.line.kind == 'secret'" ,
5050 } ] ,
5151 } ,
52+ {
53+ // The same rule on an object the organization wall does not cover.
54+ name : 'qa_note' ,
55+ label : 'Note' ,
56+ sharingModel : 'public_read_write' ,
57+ tenancy : { enabled : false } ,
58+ fields : {
59+ id : { name : 'id' , type : 'text' , primaryKey : true } ,
60+ name : { name : 'name' , type : 'text' } ,
61+ line : { name : 'line' , type : 'lookup' , reference : 'qa_line' } ,
62+ } ,
63+ validations : [ {
64+ name : 'no_secret_line' , type : 'script' , severity : 'error' ,
65+ message : RULE_MESSAGE , condition : "record.line.kind == 'secret'" ,
66+ } ] ,
67+ } ,
5268] ;
5369
5470const MEMBER : PermissionSet = {
5571 name : 'member_default' ,
5672 label : 'Member' ,
5773 objects : {
5874 qa_inspection : { allowRead : true , allowCreate : true , allowEdit : true } ,
75+ qa_note : { allowRead : true , allowCreate : true , allowEdit : true } ,
5976 qa_line : { allowRead : true } ,
6077 } ,
6178} as unknown as PermissionSet ;
@@ -77,7 +94,7 @@ afterEach(async () => {
7794} ) ;
7895
7996/** `kind` is org Y's row's value; org X holds one `secret` line of its own. */
80- async function boot ( kind : 'secret' | 'public' , posture ?: 'group' ) {
97+ async function boot ( kind : 'secret' | 'public' , posture ?: 'group' | 'isolated' ) {
8198 const driver = new SqlDriver ( { client : 'better-sqlite3' , connection : { filename : ':memory:' } , useNullAsDefault : true } ) ;
8299 const engine = new ObjectQL ( ) ;
83100 engine . registerDriver ( driver as never , true ) ;
@@ -129,31 +146,34 @@ async function boot(kind: 'secret' | 'public', posture?: 'group') {
129146 return rows ;
130147 } ) ;
131148
132- const stored = async ( name : string ) => ( await table ( 'qa_inspection' ) . where ( { name } ) . select ( 'id' ) ) . length ;
149+ const stored = async ( object : string , name : string ) => ( await table ( object ) . where ( { name } ) . select ( 'id' ) ) . length ;
133150 return { engine, readsOfLine, stored, table } ;
134151}
135152
136153/** Everything the caller sees of one write and one preview naming `line`. */
137- async function observe ( kind : 'secret' | 'public' , line : string , caller : object = CALLER , posture ?: 'group' ) {
154+ async function observe (
155+ kind : 'secret' | 'public' , line : string , caller : object = CALLER ,
156+ posture ?: 'group' | 'isolated' , object = 'qa_inspection' ,
157+ ) {
138158 const h = await boot ( kind , posture ) ;
139159 const refusal = await h . engine
140- . insert ( 'qa_inspection' , { name : 'probe' , line } , { context : caller } as never )
160+ . insert ( object , { name : 'probe' , line } , { context : caller } as never )
141161 . then ( ( ) => null , ( e : { code ?: string ; message ?: string } ) => ( { code : e . code , message : e . message } ) ) ;
142162 const preview = await h . engine . validate (
143- 'qa_inspection' , { name : 'probe' , line } , { mode : 'insert' , context : caller } as never ,
163+ object , { name : 'probe' , line } , { mode : 'insert' , context : caller } as never ,
144164 ) ;
145- // Under `group` , the by-id UPDATE door too: org X's own inspection repointed at `line`.
165+ // Given a posture , the by-id UPDATE door too: a seeded row the caller may edit, repointed at `line`.
146166 let update : unknown ;
147167 if ( posture ) {
148- await h . table ( 'qa_inspection' ) . insert ( [ { id : 'insp_x ' , name : 'seed' , organization_id : 'org_x' } ] ) ;
168+ await h . table ( object ) . insert ( [ { id : 'row_x ' , name : 'seed' , ... ( object === 'qa_inspection' ? { organization_id : 'org_x' } : { } ) } ] ) ;
149169 update = await h . engine
150- . update ( 'qa_inspection' , { line } , { where : { id : 'insp_x ' } , context : caller } as never )
170+ . update ( object , { line } , { where : { id : 'row_x ' } , context : caller } as never )
151171 . then ( ( ) => 'committed' , ( e : { code ?: string ; message ?: string } ) => ( { code : e . code , message : e . message } ) ) ;
152172 }
153173 return {
154174 seen : {
155175 refusal,
156- committed : await h . stored ( 'probe' ) ,
176+ committed : await h . stored ( object , 'probe' ) ,
157177 preview : { valid : preview . results ?. [ 0 ] ?. valid , errors : preview . results ?. [ 0 ] ?. errors ?. map ( ( e ) => e . message ) } ,
158178 update,
159179 } ,
@@ -213,3 +233,35 @@ describe('#18682 — under `group`, a member with no active organization reads n
213233 expect ( own . readsOfLine . map ( ( rows ) => rows . length ) ) . toEqual ( [ 1 , 1 , 1 ] ) ;
214234 } ) ;
215235} ) ;
236+
237+ /**
238+ * Under `isolated` the organization wall refuses an org-less writer only on an
239+ * object it covers. On an object declared `tenancy: { enabled: false }` the
240+ * write is admitted, and its rule's related read has no `tenantId` to scope by:
241+ * such a caller gets no related read at all.
242+ */
243+ describe ( '#18682 — under `isolated`, an org-less writer of an unwalled object reads nothing related' , ( ) => {
244+ const ORGLESS_USER = { userId : 'u_x' , positions : [ ] , permissions : [ ] , posture : 'MEMBER' } ;
245+
246+ it ( 'a reference to org Y’s row ends identically whether that row is secret or public' , async ( ) => {
247+ const secret = await observe ( 'secret' , 'line_y' , ORGLESS_USER , 'isolated' , 'qa_note' ) ;
248+ const open = await observe ( 'public' , 'line_y' , ORGLESS_USER , 'isolated' , 'qa_note' ) ;
249+
250+ expect ( open . seen ) . toEqual ( secret . seen ) ;
251+ expect ( secret . seen . refusal ?. code ) . toBe ( 'VALIDATION_FAILED' ) ;
252+ expect ( secret . seen . committed ) . toBe ( 0 ) ;
253+ expect ( secret . seen . preview . valid ) . toBe ( false ) ;
254+ expect ( secret . seen . update ) . toMatchObject ( { code : 'VALIDATION_FAILED' } ) ;
255+ expect ( secret . readsOfLine ) . toEqual ( [ ] ) ;
256+ expect ( open . readsOfLine ) . toEqual ( [ ] ) ;
257+ } ) ;
258+
259+ it ( 'CONTROL: WITH an active organization, org X’s own secret line reaches the rule on every door' , async ( ) => {
260+ const own = await observe ( 'public' , 'line_x' , { ...ORGLESS_USER , tenantId : 'org_x' } , 'isolated' , 'qa_note' ) ;
261+
262+ expect ( own . seen . refusal ) . toEqual ( { code : 'VALIDATION_FAILED' , message : RULE_MESSAGE } ) ;
263+ expect ( own . seen . preview ) . toEqual ( { valid : false , errors : [ RULE_MESSAGE ] } ) ;
264+ expect ( own . seen . update ) . toEqual ( { code : 'VALIDATION_FAILED' , message : RULE_MESSAGE } ) ;
265+ expect ( own . readsOfLine . map ( ( rows ) => rows . length ) ) . toEqual ( [ 1 , 1 , 1 ] ) ;
266+ } ) ;
267+ } ) ;
0 commit comments