ASP.NET Core  

Dynamic Row & Column Permission System | Field-Level Security and Rule Engine for Angular + .NET Applications

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:

  • Some users can view all rows.

  • Some users should see only their own rows.

  • Some users can see only columns A and B but not C.

  • Some users should see masked values instead of real values.

  • Some users should only be able to update certain fields.

  • The rules must change without redeploying the application.

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:

  • Angular (front-end enforcement)

  • .NET Core (backend enforcement)

  • SQL Server (metadata store and rule definitions)

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:

  • Admin

  • Manager

  • User

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:

  • A dynamic, database-driven permission model

  • No code changes needed when rules change

  • Real-time enforcement at both UI and API layers

  • Metadata-driven column masking and hiding

  • Rule engine capable of row and column filtering

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

  • API filters rows and columns before sending data

  • Angular hides or masks columns

  • Updates are validated using rule engine

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:

  • Email Mask:
    [email protected]j***@example.com

  • Salary Mask:
    65800xxxxx

  • Phone Mask:
    987654321098******10

  • Conditional Mask
    Mask only if user is not owner.

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:

  • Is user allowed to update this row?

  • Is user allowed to change this column?

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

  • Cache user roles

  • Cache resolved entity-level rules

  • Precompile rule conditions

  • Use Expression Trees instead of SQL strings for filtering

  • Paginate filtered results

Real-world examples

HRMS System

  • HR can view full employee profile

  • Manager can view only name, department, attendance

  • Employee can view only self

Sales CRM

  • Salesperson sees only own customers

  • Manager sees region-wise customers

  • Finance user sees credit limits and financial comments

  • Sales cannot see financial fields

ERP Procurement

  • Vendor pricing visible only to purchase team

  • Inventory team sees only stock-related columns

Testing strategy

Backend tests

  • Rule resolution for each role

  • Row filtering correctness

  • Column masking correctness

  • Update permissions

  • Blocked operations

Frontend tests

  • Column hiding logic

  • Masking logic

  • Dynamic table generation

Integration tests

  • API-level rule enforcement

  • Real user roles & real data scenarios

Summary

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

  • Fine-grained access control

  • Secure data exposure

  • Metadata-driven rule configuration

  • Flexible business policies without code changes

  • Field-level masking and hiding

  • Runtime rule enforcement across UI and API layers

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

  • Angular for dynamic UI enforcement

  • ASP.NET Core for backend rule application

  • SQL Server for centralized metadata rules

  • Rule engine for field-level and row-level access