I have a scenario where we have a User table, and OfficeMaster, companymaster, Storemaster .
User A has rights for company 1, Office 1,3 and store 2,5,7 User B has rights for company 1,2 , Office 4 and store 9,10,11 User C has rights for Company 3.4 Office 1,4 and store 7,2,5
It is different than the Multitenancy Saas option you have.
Can you give me some clarity on if this is achievable, i have USerCompany, USerStore, Useroffice tables with this data. Basically we want a user to see only the data he needs. with this we also control the Options a user can choose from dropdowns, suppose User A is entering a CustomerOrder then he only sees company 1 in the Company dropdown. He has no idea of the other companies.
This functionality is important to stop exposing all the offices from a user needs to access just his offices.something which can work in the API's too - we are on the modern modular monolith option with React.js. Is there an easy way or a difficult way to achieve this.
2 Answer(s)
-
0
Yes, this scenario is achievable in ABP without SaaS multi-tenancy. What you need is data authorization (row-level filtering), not “permission management” alone. In ABP, the most robust “easy” way is to implement custom global data filters (EF Core global query filters via ABP’s data filtering infrastructure) so all repository/app service queries automatically return only records the current user is allowed to see (works for APIs and UI dropdown sources).
A proven pattern in ABP is:
- Model your domain entities with scope columns (e.g.,
CompanyId,OfficeId,StoreIdon the business entities that must be protected). - Maintain mapping tables like you already have (
UserCompany,UserOffice,UserStore). - Provide a “current scope” provider (similar to the “CurrentOrganizationIdProvider” pattern) and apply an ABP EF Core global filter for entities that should be restricted.
- If a user can belong to multiple companies/offices/stores at the same time, you typically either:
- (A) filter by a selected current context (e.g., current company/office/store) stored per user/session (often best UX and best SQL performance), or
- (B) filter by membership lists (IN (…) subquery against
UserCompany/UserOffice/UserStore) for every query (works, but can be heavier).
- If a user can belong to multiple companies/offices/stores at the same time, you typically either:
- Model your domain entities with scope columns (e.g.,
Implementation outline (recommended “selected context” approach):
- Create an interface like
IHasCompany/IHasOffice/IHasStoreon entities that must be filtered:
- Create an interface like
public interface IHasCompany { Guid CompanyId { get; } } public interface IHasOffice { Guid OfficeId { get; } } public interface IHasStore { Guid StoreId { get; } }- Create singleton “current context” providers (AsyncLocal) similar to the verified ABP community implementation for organization units (you can do one provider per dimension, or a single provider holding all 3 ids).
- In your
AbpDbContext, overrideShouldFilterEntity+CreateFilterExpressionand combine expressions to enforce:entity.CompanyId == CurrentCompanyIdentity.OfficeId == CurrentOfficeIdentity.StoreId == CurrentStoreIdThis makes the restriction automatically apply to all repository queries (including those feeding dropdowns and APIs).
- Expose an application service endpoint like
CurrentScopeAppService.ChangeCompany/ChangeOffice/ChangeStorethat:- Checks the user’s membership table (
UserCompanyetc.) before changing, - Stores current selections (cache/db) per user,
- Your UI uses this to set the scope (and your dropdown “get companies” endpoint already returns only allowed ones).
- Checks the user’s membership table (
- Add middleware (after
UseAuthorization) that loads the saved “current scope” and callsCurrentScopeProvider.Change(...)for the request lifetime.
Alternative “membership list” approach (no selection):
- In the filter expression, instead of equality against a single “current id”, you join/exists against
UserCompany/UserOffice/UserStorefiltered byCurrentUser.Id. This can be done, but you must be careful about performance and EF translation (indexes become critical). The “current context” approach usually keeps queries simpler and faster.
- In the filter expression, instead of equality against a single “current id”, you join/exists against
Also note: Permissions in ABP control “can the user call this feature/action”, but they do not automatically restrict data rows. So you typically use:
- ABP Permissions/Authorization for actions (create/update/delete),
- ABP Data Filtering (global filters) for which records are visible.
Sources (verified)
- https://abp.io/docs/latest/framework/infrastructure/data-filtering
- https://abp.io/docs/latest/framework/fundamentals/authorization#claims-principal-factory
- https://abp.io/community/articles/i5tokpzt
This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) -
0
Hi,
Yes, this is achievable, and your existing
UserCompany/UserOffice/UserStoretables fit this well. ABP permissions control "can the user call this action", they don't filter rows. For "which rows can the user see", use ABP's data filtering system: you add a custom EF Core global query filter to yourDbContext, and regular EF Core queries on those entities (app services, APIs, dropdown lookup endpoints) apply the filter by default. It's a standard customization, not a built-in switch.Define an interface per dimension and implement it on the entities that must be restricted (don't implement it on the
UserCompanymapping entity itself, that would make the filter reference itself):public interface IHasCompany { int CompanyId { get; } } public class CustomerOrder : FullAuditedAggregateRoot<Guid>, IHasCompany { public int CompanyId { get; set; } // ... }Then add the following members to your existing
DbContextclass (replaceintwith your actual key type):using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.Users; public DbSet<Company> Companies { get; set; } public DbSet<UserCompany> UserCompanies { get; set; } protected bool IsCompanyFilterEnabled => DataFilter?.IsEnabled<IHasCompany>() ?? false; protected Guid? CurrentUserId => LazyServiceProvider?.LazyGetRequiredService<ICurrentUser>().Id; protected override bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) { if (typeof(IHasCompany).IsAssignableFrom(typeof(TEntity)) || typeof(TEntity) == typeof(Company)) { return true; } return base.ShouldFilterEntity<TEntity>(entityType); } protected override Expression<Func<TEntity, bool>>? CreateFilterExpression<TEntity>( ModelBuilder modelBuilder, EntityTypeBuilder<TEntity> entityTypeBuilder) where TEntity : class { var expression = base.CreateFilterExpression<TEntity>(modelBuilder, entityTypeBuilder); // Business entities (CustomerOrder etc.): only rows of the user's companies if (typeof(IHasCompany).IsAssignableFrom(typeof(TEntity))) { Expression<Func<TEntity, bool>> companyFilter = e => !IsCompanyFilterEnabled || (CurrentUserId != null && UserCompanies.Any(uc => uc.UserId == CurrentUserId && uc.CompanyId == EF.Property<int>(e, nameof(IHasCompany.CompanyId)))); expression = expression == null ? companyFilter : QueryFilterExpressionHelper.CombineExpressions(expression, companyFilter); } // The Company master table itself: this makes dropdowns return only allowed companies if (typeof(TEntity) == typeof(Company)) { Expression<Func<TEntity, bool>> companyLookupFilter = e => !IsCompanyFilterEnabled || (CurrentUserId != null && UserCompanies.Any(uc => uc.UserId == CurrentUserId && uc.CompanyId == EF.Property<int>(e, nameof(Company.Id)))); expression = expression == null ? companyLookupFilter : QueryFilterExpressionHelper.CombineExpressions(expression, companyLookupFilter); } return expression; }Repeat the same pattern for
IHasOffice/IHasStorewith yourUserOffice/UserStoretables.A few notes:
- The filter is fail-closed: when there is no current user (background jobs, machine-to-machine clients), queries return nothing. For admin screens that need to see everything, check the admin permission first and then disable the filters explicitly with the
IDataFilterservice:using (_dataFilter.Disable<IHasCompany>()) { ... }(disable all three filters if the query involves all dimensions). Background workers that need all data disable the filters the same way. - Global query filters protect reads only. Your create/update methods must also validate the submitted
CompanyId/OfficeId/StoreIdagainst the current user's memberships, otherwise a client can bypass the dropdown and post an id directly. - The generated SQL is a parameterized
EXISTSsubquery againstUserCompanies, so add a unique index onUserCompany (UserId, CompanyId)(same for the office/store tables). - The membership tables must be mapped in the same
DbContextas the filtered entities. In a modular solution, apply the same overrides in each moduleDbContextthat contains restricted entities. - This community article is a complete end-to-end sample of the same technique: https://abp.io/community/articles/switching-between-organization-units-i5tokpzt It uses a "current organization" variant where the user switches the active organization from a toolbar dropdown; you can borrow that idea if you ever want an explicit company/office switcher instead of showing all authorized data at once.
- The docs page below and the article show an older
CreateFilterExpression(ModelBuilder)signature; on 10.x the method has the extraEntityTypeBuilder<TEntity>parameter as above.
Docs: https://abp.io/docs/latest/framework/infrastructure/data-filtering
Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post) - The filter is fail-closed: when there is no current user (background jobs, machine-to-machine clients), queries return nothing. For admin screens that need to see everything, check the admin permission first and then disable the filters explicitly with the