Back to ER Diagram
RBAC Logic

RBAC & Permissions Logic

Complete documentation of Role-Based Access Control implementation with user-level overrides, permission resolution, and ALLOW/DENY logic for the ProKure system.

9 Tables Involved
Schema v2.3
Admin Module

System Overview

The ProKure RBAC system uses a hybrid model combining role-based permissions with user-level overrides. This allows both standardized role assignment and granular per-user customization.

admin.users
admin.user_roles
admin.roles
admin.role_permissions
admin.user_permissions
admin.permissions
Final Permissions

Tables Involved

admin.users

  • Core user table with authentication credentials
  • Links to company, department, and reporting hierarchy
  • Primary entity for permission resolution

admin.roles

  • Role definitions (PR_CREATOR, RFQ_MANAGER, etc.)
  • Can be system-wide or company-specific
  • is_system_role flag prevents deletion

admin.user_roles

  • Junction table: assigns roles to users
  • Tracks who assigned and when
  • Users can have multiple roles

admin.permissions

  • Master list of all system permissions
  • Format: MODULE.ACTION (e.g., PR.CREATE, RFQ.APPROVE)
  • Developer-managed, not editable via UI

admin.role_permissions

  • Junction table: assigns permissions to roles
  • All entries are implicit ALLOW
  • Admin-managed via role configuration UI

admin.user_permissions (NEW)

  • User-level permission overrides
  • grant_type: 'ALLOW' or 'DENY'
  • Enables "PR_CREATOR minus EDIT" scenarios

Permission Resolution Logic

When checking if a user has a specific permission, the system follows this priority order:

1

Check User-Level DENY

First, check if the user has an explicit DENY for this permission in admin.user_permissions. If found, immediately return FALSE - DENY always wins.

SELECT 1 FROM admin.user_permissions
WHERE user_id = @userId
  AND permission_id = (SELECT id FROM admin.permissions WHERE permission_code = 'PR.EDIT')
  AND grant_type = 'DENY'
  AND is_active = true;
2

Check User-Level ALLOW

If no DENY found, check if the user has an explicit ALLOW override. This grants permissions beyond their roles.

SELECT 1 FROM admin.user_permissions
WHERE user_id = @userId
  AND permission_id = (SELECT id FROM admin.permissions WHERE permission_code = 'PR.EDIT')
  AND grant_type = 'ALLOW'
  AND is_active = true;
3

Check Role-Level Permissions

If no user-level override exists, aggregate permissions from all assigned roles.

SELECT DISTINCT p.permission_code
FROM admin.user_roles ur
JOIN admin.role_permissions rp ON ur.role_id = rp.role_id
JOIN admin.permissions p ON rp.permission_id = p.id
WHERE ur.user_id = @userId
  AND ur.is_active = true
  AND p.is_active = true;
4

Default: DENY

If no permission is found at any level, access is denied. This follows the principle of least privilege.

Priority Order

Priority Source Type Effect
1 (Highest) admin.user_permissions DENY Immediately blocks access
2 admin.user_permissions ALLOW Grants access (overrides roles)
3 admin.role_permissions Role ALLOW Grants access from assigned roles
4 (Default) No entry found Implicit DENY Access denied by default

Key Principle

DENY always wins over ALLOW at any level. This follows standard security practices where explicit restrictions cannot be bypassed by broader permissions.

Example Scenario

Requirement: User "John" should have PR_CREATOR role but without EDIT permission.

1

Assign PR_CREATOR Role

PR_CREATOR role includes: PR.CREATE, PR.EDIT, PR.VIEW, PR.DELETE

INSERT INTO admin.user_roles (user_id, role_id, assigned_by, is_active)
VALUES (@johnId, @prCreatorRoleId, @adminId, true);
2

Add DENY Override for PR.EDIT

Create a user-level DENY to block the EDIT permission.

INSERT INTO admin.user_permissions (user_id, permission_id, grant_type, granted_by, is_active)
VALUES (
    @johnId,
    (SELECT id FROM admin.permissions WHERE permission_code = 'PR.EDIT'),
    'DENY',
    @adminId,
    true
);
3

Resulting Permissions

John's final effective permissions:

Permission From Role User Override Final Result
PR.CREATE ALLOW - ALLOW
PR.EDIT ALLOW DENY DENY
PR.VIEW ALLOW - ALLOW
PR.DELETE ALLOW - ALLOW

API Implementation (.NET)

The permission check logic implemented in the .NET API layer:

public async Task<bool> HasPermissionAsync(int userId, string permissionCode)
{
    // Step 1: Check user-level DENY (highest priority)
    var userDeny = await _context.UserPermissions
        .AnyAsync(up => up.UserId == userId
            && up.Permission.PermissionCode == permissionCode
            && up.GrantType == "DENY"
            && up.IsActive);

    if (userDeny) return false;  // DENY wins immediately

    // Step 2: Check user-level ALLOW
    var userAllow = await _context.UserPermissions
        .AnyAsync(up => up.UserId == userId
            && up.Permission.PermissionCode == permissionCode
            && up.GrantType == "ALLOW"
            && up.IsActive);

    if (userAllow) return true;

    // Step 3: Check role-level permissions
    var roleAllow = await _context.UserRoles
        .Where(ur => ur.UserId == userId && ur.IsActive)
        .SelectMany(ur => ur.Role.RolePermissions)
        .AnyAsync(rp => rp.Permission.PermissionCode == permissionCode
            && rp.Permission.IsActive);

    return roleAllow;
}

Login Response

At login, the API resolves all permissions and returns only the final ALLOWED permissions to Angular. The frontend never needs to handle DENY logic - it simply works with the list of granted permissions.

Angular Frontend Integration

The frontend receives pre-resolved permissions and uses them for UI control:

// AuthService - stores permissions from login response
@Injectable({ providedIn: 'root' })
export class AuthService {
    private permissions: string[] = [];

    setPermissions(permissions: string[]) {
        this.permissions = permissions;
    }

    hasPermission(code: string): boolean {
        return this.permissions.includes(code);
    }

    hasAnyPermission(codes: string[]): boolean {
        return codes.some(code => this.permissions.includes(code));
    }
}

// Usage in component template
<button *ngIf="auth.hasPermission('PR.CREATE')">Create PR</button>
<button *ngIf="auth.hasPermission('PR.EDIT')">Edit PR</button>  <!-- Hidden for John -->

Standard Permission Codes

Permission codes follow the pattern: MODULE.ACTION

Module Actions Example Codes
PR (Purchase Request) CREATE, VIEW, EDIT, DELETE, APPROVE PR.CREATE, PR.APPROVE
RFQ (Request for Quote) CREATE, VIEW, EDIT, PUBLISH, COMPARE RFQ.PUBLISH, RFQ.COMPARE
PO (Purchase Order) CREATE, VIEW, EDIT, APPROVE, CANCEL PO.APPROVE, PO.CANCEL
VENDOR CREATE, VIEW, EDIT, APPROVE, BLACKLIST VENDOR.APPROVE, VENDOR.BLACKLIST
GRN (Goods Receipt) CREATE, VIEW, EDIT, CONFIRM GRN.CREATE, GRN.CONFIRM
INVOICE CREATE, VIEW, APPROVE, PAY INVOICE.APPROVE, INVOICE.PAY
ADMIN USER_MANAGE, ROLE_MANAGE, CONFIG ADMIN.USER_MANAGE, ADMIN.CONFIG