import {BindingScope, Getter, inject, injectable} from '@loopback/core'; import { AnyObject, DataObject, Filter, FilterExcludingWhere, Where, WhereBuilder, } from '@loopback/repository'; import {HttpErrors} from '@loopback/rest'; import {AuthenticationBindings} from 'loopback4-authentication'; import {TenantType} from '../enums'; import {TenantUtilitiesErrorKeys} from '../error-keys'; import {TenantUtilitiesBindings} from '../keys'; import { EntityWithTenantId, ITenantGuard, ITenantUtilitiesConfig, UserInToken, } from '../types'; @injectable({scope: BindingScope.TRANSIENT}) export class TenantGuardService< T extends EntityWithTenantId, ID, > implements ITenantGuard { constructor( @inject.getter(AuthenticationBindings.CURRENT_USER) public readonly getCurrentUser: Getter, @inject(TenantUtilitiesBindings.Config, {optional: true}) private readonly config?: ITenantUtilitiesConfig, ) {} async skipTenantGuard(): Promise { const user = await this.getCurrentUser(); return ( user.tenantType === TenantType.MASTER || (this.config?.useSingleTenant ?? false) ); } find(filter?: Filter): Promise> { return this.addTenantToFilter(filter); } async findOne(filter?: Filter): Promise> { return this.addTenantToFilter(filter); } async findById(id: ID, filter?: FilterExcludingWhere): Promise> { return this.addTenantToFilter(filter, id); } async count(where?: Where): Promise> { return this.addTenantToWhere(where); } async exists(id: ID): Promise> { return this.addTenantToWhere(undefined, id); } create(data: DataObject): Promise> { return this.addTenantId(data); } createAll | T>(data: R[]): Promise { return this.addTenantIDMultiple(data); } save(entity: T): Promise { return this.addTenantId(entity); } async replaceById( id: ID, data: DataObject, ): Promise<{data: DataObject; where: Where}> { return this.updateById(id, data); } async updateById | T>( id: ID, data: R, ): Promise<{data: R; where: Where}> { await this.checkTenantId(data); return this.addTenantToWhere(undefined, id).then(where => ({ where, data, })); } async update(data: T): Promise<{data: T; where: Where}> { await this.checkTenantId(data); return this.addTenantToWhere(undefined, data.getId()).then(newWhere => ({ where: newWhere, data, })); } async updateAll( data: DataObject, where?: Where, ): Promise<{data: DataObject; where: Where}> { await this.checkTenantId(data); return this.addTenantToWhere(where).then(newWhere => ({ where: newWhere, data, })); } deleteById(id: ID): Promise> { return this.addTenantToWhere(undefined, id); } async delete(entity: T): Promise<{where: Where; entity: T}> { await this.checkTenantId(entity); return this.addTenantToWhere(undefined, entity.getId()).then(where => ({ where, entity, })); } async deleteAll(where?: Where): Promise> { return this.addTenantToWhere(where); } private async checkTenantId(data: T | DataObject): Promise { const user = await this.getCurrentUser(); if (!user.tenantId) { throw new HttpErrors.Unauthorized( TenantUtilitiesErrorKeys.TenantIdMissing, ); } if (data.tenantId && data.tenantId !== user.tenantId) { throw new HttpErrors.Forbidden( TenantUtilitiesErrorKeys.TenantIdDoesNotMatch, ); } } private async addTenantId | T>( entity: S, ): Promise { const user = await this.getCurrentUser(); await this.checkTenantId(entity); entity.tenantId = user.tenantId; return entity; } private async addTenantIDMultiple | T>( entities: R[], ): Promise { const user = await this.getCurrentUser(); const tenantId = user.tenantId; if (tenantId) { entities.forEach(entity => { if (!entity.tenantId) { entity.tenantId = tenantId; } else if (entity.tenantId !== tenantId) { throw new HttpErrors.Forbidden( `${TenantUtilitiesErrorKeys.TenantIdDoesNotMatch}: ${entity.tenantId}`, ); } else { // do nothing } }); return entities; } throw new HttpErrors.Unauthorized(TenantUtilitiesErrorKeys.TenantIdMissing); } private async addTenantToWhere(where?: Where, id?: ID): Promise> { const user = await this.getCurrentUser(); if (user.tenantId) { return this.buildWhere(user, where, id); } throw new HttpErrors.Unauthorized(TenantUtilitiesErrorKeys.TenantIdMissing); } private async addTenantToFilter( filter?: Filter, id?: ID, ): Promise> { const user = await this.getCurrentUser(); if (user.tenantId) { return { ...filter, where: this.buildWhere(user, filter?.where, id), }; } throw new HttpErrors.Unauthorized(TenantUtilitiesErrorKeys.TenantIdMissing); } buildWhere(user: UserInToken, where?: Where, id?: ID): Where { const whereBuilder = new WhereBuilder(); const extraFilter: AnyObject = { tenantId: user.tenantId, }; if (id) { extraFilter.id = id; } if (!where) { return extraFilter as Where; } whereBuilder.and([where, extraFilter].filter(w => !!w)); return whereBuilder.build(); } }