@@ -56,6 +56,8 @@ export class PermissionsPlugin implements Plugin {
5656 defaultDeny : true ,
5757 permissionsDir : './permissions' ,
5858 cachePermissions : true ,
59+ tenantIsolation : false ,
60+ tenantField : '_organizationId' ,
5961 ...config ,
6062 } ;
6163
@@ -95,6 +97,11 @@ export class PermissionsPlugin implements Plugin {
9597 // Subscribe to data.* hooks for permission checking
9698 await this . setupEventListeners ( context ) ;
9799
100+ // Set up tenant isolation hooks (independent of permission checking)
101+ if ( this . config . tenantIsolation ) {
102+ await this . setupTenantIsolation ( context ) ;
103+ }
104+
98105 context . logger . info ( '[Permissions Plugin] Initialized successfully' ) ;
99106
100107 await context . trigger ( 'plugin.initialized' , { pluginId : this . name , timestamp : new Date ( ) . toISOString ( ) } ) ;
@@ -253,6 +260,70 @@ export class PermissionsPlugin implements Plugin {
253260 this . context ?. logger . info ( '[Permissions Plugin] Event listeners registered' ) ;
254261 }
255262
263+ /**
264+ * Set up tenant isolation hooks — separate from permission checking.
265+ * These hooks run independently and enforce organization-scoped data access.
266+ */
267+ private async setupTenantIsolation ( context : PluginContext ) : Promise < void > {
268+ context . hook ( 'data.beforeCreate' , async ( data : any ) => {
269+ this . applyTenantToWrite ( data ) ;
270+ } ) ;
271+
272+ context . hook ( 'data.beforeUpdate' , async ( data : any ) => {
273+ this . applyTenantToWrite ( data ) ;
274+ } ) ;
275+
276+ context . hook ( 'data.beforeDelete' , async ( data : any ) => {
277+ this . applyTenantFilter ( data ) ;
278+ } ) ;
279+
280+ context . hook ( 'data.beforeFind' , async ( data : any ) => {
281+ this . applyTenantFilter ( data ) ;
282+ } ) ;
283+
284+ context . logger . info ( `[Permissions Plugin] Tenant isolation enabled (field: ${ this . config . tenantField } )` ) ;
285+ }
286+
287+ /**
288+ * Apply tenant ID to write operations (create/update).
289+ * Stamps the tenant field on the record data so it belongs to the user's organization.
290+ */
291+ private applyTenantToWrite ( data : any ) : void {
292+ if ( ! this . config . tenantIsolation ) return ;
293+
294+ const organizationId = data . organizationId || data . metadata ?. organizationId ;
295+ if ( ! organizationId ) return ;
296+
297+ const tenantField = this . config . tenantField || '_organizationId' ;
298+
299+ // Stamp the tenant field on the record being written
300+ if ( data . doc ) {
301+ data . doc [ tenantField ] = organizationId ;
302+ }
303+ if ( data . record ) {
304+ data . record [ tenantField ] = organizationId ;
305+ }
306+ }
307+
308+ /**
309+ * Apply tenant filter to read/delete operations.
310+ * Ensures queries are automatically scoped to the user's organization.
311+ */
312+ private applyTenantFilter ( data : any ) : void {
313+ if ( ! this . config . tenantIsolation ) return ;
314+
315+ const organizationId = data . organizationId || data . metadata ?. organizationId ;
316+ if ( ! organizationId ) return ;
317+
318+ const tenantField = this . config . tenantField || '_organizationId' ;
319+
320+ // Merge tenant filter into existing query filters
321+ data . filters = {
322+ ...data . filters ,
323+ [ tenantField ] : organizationId ,
324+ } ;
325+ }
326+
256327 /**
257328 * Check permission for a data operation
258329 */
@@ -266,6 +337,7 @@ export class PermissionsPlugin implements Plugin {
266337
267338 const permissionContext : PermissionContext = {
268339 userId,
340+ organizationId : data . organizationId || data . metadata ?. organizationId ,
269341 profiles : userProfiles || [ ] ,
270342 metadata : data . metadata ,
271343 } ;
@@ -302,6 +374,7 @@ export class PermissionsPlugin implements Plugin {
302374
303375 const permissionContext : PermissionContext = {
304376 userId,
377+ organizationId : data . organizationId || data . metadata ?. organizationId ,
305378 profiles : userProfiles || [ ] ,
306379 metadata : data . metadata ,
307380 } ;
0 commit comments