Introduction

Enterprise applications often deal with sensitive business data such as salary details, vendor rates, contract values, credit limits, discounts, customer confidential fields, employee performance, and audit-sensitive information.
A common requirement is:

This leads to the need for a Dynamic Row & Column Permission System, also known as field-level security or attribute-level access control.

In this article, you will learn how to implement a production-ready, metadata-driven, rule-based permission engine using:

You will also understand row-level filtering, column-level masking, dynamic UI generation, and rule-driven API responses.

What problem are we solving?

Traditional RBAC (Role-Based Access Control) allows you to define:

But this is not enough in real-life enterprise software.
For example:

A Manager should see salary fields for employees of Department A but not Department B.
A Sales Executive can view only their assigned customers and only basic fields like Name, Phone, Email.
A Finance Officer can view all fields including invoices, outstanding amounts, and financial remarks.

Static code-based rules become impossible to maintain.

We need:

Architecture overview

Our solution follows three layers:

1. Rule Definition Layer
Admins configure rules in a database table.
Rules define which roles can access which rows and which columns.

2. Resolution Layer
At runtime, rules are resolved based on the current user, module, entity, and action.

3. Enforcement Layer

Workflow diagram (conceptual)

           +---------------------------+
           |         Angular UI        |
           |  Table, Forms, Masked UI  |
           +------------+--------------+
                        |
                        v
             Request for Data (User)
                        |
                        v
           +----------------------------+
           |        .NET API            |
           | Permission Resolution      |
           | Row Filter + Column Mask   |
           +------------+---------------+
                        |
                        v
           +----------------------------+
           |       Database Rules       |
           | Row Rules + Column Rules   |
           +----------------------------+
                        |
                        v
                 Filtered Result
                        |
                        v
              Returned to Angular

Flowchart: rule resolution

Start
  |
  v
User Makes Request
  |
  v
Fetch User Roles
  |
  v
Load Permission Rules for Entity
  |
  +--> No rules found? Allow all (default)
  |
  v
Apply Row-Level Rules
  |
  v
Apply Column-Level Rules
  |
  v
Apply Masking Rules
  |
  v
Return Filtered and Masked Data
  |
  v
End

Designing rule metadata (sql server)

table: PermissionEntity

ColumnTypeDescription
EntityIdintUnique identifier for a domain entity (e.g., Employee, Invoice)
EntityNamenvarchar(200)

table: PermissionRole

| RoleId | int | Role key |
| RoleName | nvarchar(100) | |

table: PermissionRowRule

ColumnTypeDescription
RowRuleIdbigint
EntityIdint
RoleIdint
Conditionnvarchar(max)SQL condition e.g., DepartmentId = @UserDepartmentId

table: PermissionColumnRule

ColumnTypeDescription
ColumnRuleIdbigint
EntityIdint
RoleIdint
ColumnNamenvarchar(200)
PermissionTypetinyint1=Show, 2=Hide, 3=Mask
MaskFormatnvarchar(50)Optional masking rule

table: UserRole

| UserId | bigint | |
| RoleId | int | |

How row permissions work

Row permissions rely on SQL-level conditions.

Examples:

Salesperson:

OwnerUserId = @UserId

Manager:

DepartmentId = @UserDepartmentId

Finance Admin:

1=1   // Full access

How column permissions work

Examples:

Hide Salary for non-HR roles:

ColumnName = 'Salary'PermissionType = Hide

Mask Employee Email for non-admin:

MaskFormat = '[email protected]'

Resolving rules (backend)

fetching applicable rules

public async Task<ResolvedPermissions> ResolveAsync(long userId, string entity)
{
    var roles = await _repo.GetUserRoles(userId);
    var entityId = await _repo.GetEntityId(entity);

    var rowRules = await _repo.GetRowRules(entityId, roles);
    var columnRules = await _repo.GetColumnRules(entityId, roles);

    return new ResolvedPermissions
    {
        RowRules = rowRules,
        ColumnRules = columnRules
    };
}

Applying row rules

public IQueryable<T> ApplyRowRules<T>(IQueryable<T> query, List<RowRule> rules, UserContext user)
{
    foreach (var rule in rules)
    {
        var sql = rule.Condition
                      .Replace("@UserId", user.UserId.ToString())
                      .Replace("@UserDepartmentId", user.DepartmentId.ToString());

        query = query.Where(sql);
    }

    return query;
}

Applying column rules before returning json

public object ApplyColumnRules(object data, List<ColumnRule> rules)
{
    var dict = JObject.FromObject(data);

    foreach (var rule in rules)
    {
        if (rule.PermissionType == PermissionType.Hide)
        {
            dict.Remove(rule.ColumnName);
        }
        else if (rule.PermissionType == PermissionType.Mask)
        {
            dict[rule.ColumnName] = rule.MaskFormat;
        }
    }

    return dict;
}

Angular front-end enforcement

Dynamic column configuration

Angular must hide/mask columns based on metadata returned from the API.

API returns:

{"data": [ ... ],"columnPermissions": {
    "Salary": "HIDE",
    "Email": "MASK",
    "Address": "SHOW"}}

Angular smart-table component

applyColumnPermissions() {
  for (const col of this.columns) {
    const permission = this.columnPermissions[col.field];

    if (permission === 'HIDE') {
      col.hidden = true;
    }

    if (permission === 'MASK') {
      col.mask = true;
    }
  }
}

Template logic

<td *ngIf="!col.hidden">
  <ng-container *ngIf="!col.mask">
    {{ row[col.field] }}
  </ng-container>
  <ng-container *ngIf="col.mask">
    ****** 
  </ng-container>
</td>

Masking patterns

Common patterns:

Integrating with .net middleware

Use middleware to pre-resolve permissions for each request.

public class PermissionMiddleware
{
    private readonly RequestDelegate _next;

    public PermissionMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context, IPermissionService service)
    {
        var userId = context.User.GetUserId();
        var entity = context.Request.GetEntity();

        var permissions = await service.ResolveAsync(userId, entity);

        context.Items["Permissions"] = permissions;

        await _next(context);
    }
}

Securing update and delete actions

Backend must check:

Validating patch operations

public void ValidateUpdate(object oldData, object newData, List<ColumnRule> rules)
{
    foreach (var rule in rules)
    {
        if (rule.PermissionType == PermissionType.Hide)
        {
            // Treat hidden columns as read-only
            if (!JToken.DeepEquals(oldData[rule.ColumnName], newData[rule.ColumnName]))
                throw new UnauthorizedAccessException($"{rule.ColumnName} is read-only.");
        }
    }
}

Security best practices

  1. API must enforce rules, Angular enforcement is optional.

  2. Never send hidden fields to the browser.

  3. Rule engine must be cached for performance.

  4. Row rules should use parameterised queries.

  5. Use audit logging for every access and change.

  6. Masking on backend is more secure than UI masking.

Performance optimisations

Real-world examples

HRMS System

Sales CRM

ERP Procurement

Testing strategy

Backend tests

Frontend tests

Integration tests

Summary

A Dynamic Row & Column Permission System is a critical enterprise requirement.
It provides:

This article demonstrated a complete, production-ready solution using: