Open Closed

Question about the User permissions module. #10818


User avatar
0
bpdeveloper created

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.

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

2 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • 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:

      1. Model your domain entities with scope columns (e.g., CompanyId, OfficeId, StoreId on the business entities that must be protected).
      2. Maintain mapping tables like you already have (UserCompany, UserOffice, UserStore).
      3. 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).
    • Implementation outline (recommended “selected context” approach):

      1. Create an interface like IHasCompany / IHasOffice / IHasStore on entities that must be filtered:
    public interface IHasCompany { Guid CompanyId { get; } }
         public interface IHasOffice  { Guid OfficeId  { get; } }
         public interface IHasStore   { Guid StoreId   { get; } }
    
    1. 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).
    2. In your AbpDbContext, override ShouldFilterEntity + CreateFilterExpression and combine expressions to enforce:
      • entity.CompanyId == CurrentCompanyId
      • entity.OfficeId == CurrentOfficeId
      • entity.StoreId == CurrentStoreId This makes the restriction automatically apply to all repository queries (including those feeding dropdowns and APIs).
    3. Expose an application service endpoint like CurrentScopeAppService.ChangeCompany/ChangeOffice/ChangeStore that:
      • Checks the user’s membership table (UserCompany etc.) 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).
    4. Add middleware (after UseAuthorization) that loads the saved “current scope” and calls CurrentScopeProvider.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/UserStore filtered by CurrentUser.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.
    • 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)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    Yes, this is achievable, and your existing UserCompany / UserOffice / UserStore tables 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 your DbContext, 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 UserCompany mapping 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 DbContext class (replace int with 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 / IHasStore with your UserOffice / UserStore tables.

    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 IDataFilter service: 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 / StoreId against the current user's memberships, otherwise a client can bypass the dropdown and post an id directly.
    • The generated SQL is a parameterized EXISTS subquery against UserCompanies, so add a unique index on UserCompany (UserId, CompanyId) (same for the office/store tables).
    • The membership tables must be mapped in the same DbContext as the filtered entities. In a modular solution, apply the same overrides in each module DbContext that 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 extra EntityTypeBuilder<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)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 21, 2026, 06:18
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.