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:
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
| Column | Type | Description |
|---|
| EntityId | int | Unique identifier for a domain entity (e.g., Employee, Invoice) |
| EntityName | nvarchar(200) | |
table: PermissionRole
| RoleId | int | Role key |
| RoleName | nvarchar(100) | |
table: PermissionRowRule
| Column | Type | Description |
|---|
| RowRuleId | bigint | |
| EntityId | int | |
| RoleId | int | |
| Condition | nvarchar(max) | SQL condition e.g., DepartmentId = @UserDepartmentId |
table: PermissionColumnRule
| Column | Type | Description |
|---|
| ColumnRuleId | bigint | |
| EntityId | int | |
| RoleId | int | |
| ColumnName | nvarchar(200) | |
| PermissionType | tinyint | 1=Show, 2=Hide, 3=Mask |
| MaskFormat | nvarchar(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:
65800 → xxxxx
Phone Mask:
9876543210 → 98******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:
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
API must enforce rules, Angular enforcement is optional.
Never send hidden fields to the browser.
Rule engine must be cached for performance.
Row rules should use parameterised queries.
Use audit logging for every access and change.
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
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
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