Acumatica · Security

Acumatica Row-Level Security — Best Practices

How to design and implement row-level security in Acumatica — restricting by branch, company, customer, and the test plan that catches leaks before users do.

John Kihiu12 min read

Row-level security in Acumatica is one of those features clients assume is "just permissions" until they actually need branch A's sales reps unable to see branch B's customers, and discover restriction groups are a different mechanism entirely from the access rights they've been configuring for years. I've set this up on multi-branch and multi-entity instances often enough to have a short list of rules I don't deviate from anymore, mostly learned from watching the first implementation of each go wrong.

Access rights and restriction groups are not the same lever

Access rights (role-based, configured under Roles) control which screens and actions a user can reach. Restriction groups control which rows of a restrictable entity - Customer, Vendor, Cash Account, and a handful of others - a user can see once they're on a screen they're otherwise allowed to use. A user with full access rights to the Customers screen but no restriction group membership on a particular customer record simply won't see that row in the grid, and won't be able to select it in a selector on any other screen either. The two systems are independent and both need to be right; getting access rights configured correctly while forgetting restriction groups is the single most common reason a "locked down" instance still leaks records nobody meant to expose.

Restriction groups: assign to the entity, not the user

The workflow that actually works: create a restriction group per real-world boundary (a branch, a region, a business unit), assign customers/vendors/cash accounts to that group, then assign users or roles to the same group. Assigning restriction membership directly to individual users, one at a time, is the pattern I inherit most often on legacy instances and it is unmaintainable past a handful of users - nobody remembers to update it when someone changes teams, and six months later half the sales floor can see accounts they shouldn't.

Restriction groups apply per entity type independently

A Customer restriction group and a Cash Account restriction group are configured and assigned separately. It's easy to lock down customer visibility carefully and forget that the same user still has unrestricted visibility into every cash account in the company, because nobody thought to extend the same discipline to a different restrictable entity.

Making a custom DAC restrictable

Out of the box, restriction groups apply to a fixed set of base entities. When a client needs the same row-level model on a custom entity - a custom "Region" master or a bespoke project-tracking DAC - you implement it yourself, and BQL respects it automatically once wired correctly, the same way it respects restriction groups on base entities:

C# · Restrictable custom DAC
[PXCacheName("Regional Account")]
public class RegionalAccount : IBqlTable
{
    #region RestrictionGroupID
    [PXDBInt]
    [PXRestrictor(typeof(Where<RestrictionGroup.restrictionGroupID,
        Equal<Current<RegionalAccount.restrictionGroupID>>>),
        Messages.RestrictionViolation)]
    public int? RestrictionGroupID { get; set; }
    public abstract class restrictionGroupID : PX.Data.BQL.BqlInt.Field<restrictionGroupID> { }
    #endregion
}

The mistake I see most on hand-rolled restrictable DACs: the attribute is added, tested with an admin account (which bypasses restriction checks entirely), and shipped without ever logging in as a genuinely restricted user. Restriction bugs are invisible to the person who built the feature unless they specifically test as the audience the feature is for.

Restriction checks are not free at scale

Every BQL query against a restrictable entity gets an implicit join or subquery against the restriction group tables. On a small instance this is unmeasurable. On an instance with tens of thousands of customers and deeply nested restriction group hierarchies, I've seen this show up as a measurable tax on GI performance and REST API list calls. If a restriction-heavy screen is slow, check the actual SQL Acumatica generates before assuming the customization is at fault - sometimes the restriction join itself is the expensive part, and the fix is indexing the restriction group tables properly rather than touching the customization.

Wrapping up

Treat restriction groups as a separate security layer from access rights, assign membership to groups rather than individuals, extend the same discipline to every restrictable entity type rather than just the obvious one, and always validate as a genuinely restricted test user rather than an admin account that silently bypasses the whole mechanism. Most row-level security bugs I've been asked to fix were never bugs in the restriction logic itself - they were gaps in the assignment discipline around it.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.