@@ -95,11 +95,12 @@ describe('SecurityPlugin', () => {
9595 // multiTenant switch — single-tenant mode strips tenant policies and skips
9696 // organization_id auto-injection.
9797 // -------------------------------------------------------------------------
98- const makeMiddlewareCtx = ( overrides : { permissionSets : PermissionSet [ ] ; objectFields ?: string [ ] } ) => {
98+ const makeMiddlewareCtx = ( overrides : { permissionSets : PermissionSet [ ] ; objectFields ?: string [ ] ; schemaExtra ?: Record < string , any > } ) => {
9999 const fields : Record < string , any > = { } ;
100100 for ( const f of overrides . objectFields ?? [ 'id' , 'organization_id' , 'owner_id' , 'name' ] ) {
101101 fields [ f ] = { name : f } ;
102102 }
103+ const baseSchema : any = { name : 'task' , fields, ...( overrides . schemaExtra ?? { } ) } ;
103104 let middleware : any ;
104105 const ql = {
105106 registerMiddleware : ( mw : any ) => {
@@ -108,10 +109,10 @@ describe('SecurityPlugin', () => {
108109 // later in `start()`.
109110 if ( ! middleware ) middleware = mw ;
110111 } ,
111- getSchema : ( ) => ( { name : 'task' , fields } ) ,
112+ getSchema : ( ) => baseSchema ,
112113 } ;
113114 const metadata = {
114- get : async ( ) => ( { name : 'task' , fields } ) ,
115+ get : async ( ) => baseSchema ,
115116 list : async ( ) => overrides . permissionSets ,
116117 } ;
117118 const services : Record < string , any > = {
@@ -197,6 +198,70 @@ describe('SecurityPlugin', () => {
197198 expect ( opCtx . ast . where ) . toEqual ( { organization_id : 'org-1' } ) ;
198199 } ) ;
199200
201+ // Regression: when a schema explicitly opts out of tenancy
202+ // (`tenancy.enabled === false` — e.g. `sys_package` Marketplace catalog),
203+ // the wildcard `tenant_isolation` policy targeting `organization_id`
204+ // must be treated as "not applicable" and SKIPPED, NOT fail-closed
205+ // with RLS_DENY_FILTER. Otherwise the registry skips injecting the
206+ // tenant column (correct) but the security plugin still produces zero
207+ // rows on every read (wrong) — which silently broke the cloud
208+ // Marketplace UI.
209+ it ( 'tenancy.enabled=false — wildcard organization_id RLS is skipped, not denied' , async ( ) => {
210+ const plugin = new SecurityPlugin ( { multiTenant : true , fallbackPermissionSet : 'member_default' } ) ;
211+ const harness = makeMiddlewareCtx ( {
212+ permissionSets : [ tenantPolicySet ] ,
213+ // Catalog table without organization_id; opts out of tenancy.
214+ objectFields : [ 'id' , 'manifest_id' , 'visibility' , 'owner_org_id' ] ,
215+ schemaExtra : { tenancy : { enabled : false , strategy : 'shared' } } ,
216+ } ) ;
217+ await plugin . init ( harness . ctx ) ;
218+ await plugin . start ( harness . ctx ) ;
219+ const opCtx : any = {
220+ object : 'task' , operation : 'find' , ast : { where : undefined } ,
221+ context : { userId : 'u1' , tenantId : 'org-1' , roles : [ ] , permissions : [ ] } ,
222+ } ;
223+ await harness . run ( opCtx ) ;
224+ // No deny sentinel, no organization_id where clause: the read
225+ // passes through and the catalog row is visible to every tenant.
226+ expect ( opCtx . ast . where ) . toBeUndefined ( ) ;
227+ } ) ;
228+
229+ it ( 'tenancy.enabled=false via systemFields.tenant=false — also skipped' , async ( ) => {
230+ const plugin = new SecurityPlugin ( { multiTenant : true , fallbackPermissionSet : 'member_default' } ) ;
231+ const harness = makeMiddlewareCtx ( {
232+ permissionSets : [ tenantPolicySet ] ,
233+ objectFields : [ 'id' , 'name' ] ,
234+ schemaExtra : { systemFields : { tenant : false } } ,
235+ } ) ;
236+ await plugin . init ( harness . ctx ) ;
237+ await plugin . start ( harness . ctx ) ;
238+ const opCtx : any = {
239+ object : 'task' , operation : 'find' , ast : { where : undefined } ,
240+ context : { userId : 'u1' , tenantId : 'org-1' , roles : [ ] , permissions : [ ] } ,
241+ } ;
242+ await harness . run ( opCtx ) ;
243+ expect ( opCtx . ast . where ) . toBeUndefined ( ) ;
244+ } ) ;
245+
246+ it ( 'tenancy enabled (default) — wildcard organization_id RLS still denies when field is missing' , async ( ) => {
247+ // Sanity check: dropping the deny sentinel must remain in effect
248+ // for objects that did NOT opt out — otherwise a wildcard policy
249+ // applied to a half-migrated table would silently expose every row.
250+ const plugin = new SecurityPlugin ( { multiTenant : true , fallbackPermissionSet : 'member_default' } ) ;
251+ const harness = makeMiddlewareCtx ( {
252+ permissionSets : [ tenantPolicySet ] ,
253+ objectFields : [ 'id' , 'name' ] , // no organization_id, no opt-out
254+ } ) ;
255+ await plugin . init ( harness . ctx ) ;
256+ await plugin . start ( harness . ctx ) ;
257+ const opCtx : any = {
258+ object : 'task' , operation : 'find' , ast : { where : undefined } ,
259+ context : { userId : 'u1' , tenantId : 'org-1' , roles : [ ] , permissions : [ ] } ,
260+ } ;
261+ await harness . run ( opCtx ) ;
262+ expect ( opCtx . ast . where ) . toEqual ( RLS_DENY_FILTER ) ;
263+ } ) ;
264+
200265 // Post-resolution fallback: roles is non-empty (e.g. better-auth
201266 // sys_member.role = 'owner') but no sys_role binding maps that name to
202267 // a permission set, so resolvePermissionSets returns []. Without the
0 commit comments