Skip to content

Permissions

The admin dashboard uses react-admin RBAC permissions loaded from the backend to control UI visibility.

Permission Format

Permissions are loaded from GET /api/v1/users/me as an array:

interface Permission {
  action: string | string[];
  resource: string;
  type?: "deny";
  record?: { id: string };
}

Examples:

[
  {"action": "list", "resource": "borrowers"},
  {"action": ["show", "edit"], "resource": "loans"},
  {"action": "*", "resource": "*"},
  {"type": "deny", "action": "show", "resource": "borrowers", "record": {"ssn": true}}
]

hasPermission() Utility

The core permission-checking function:

function hasPermission(
  permissions: Permission[] | undefined,
  action: string,
  resource: string,
): boolean

Logic:

  1. Returns false if permissions is undefined
  2. Iterates through permissions, skipping any with type: "deny"
  3. Checks if the permission's action matches (supports "*" wildcard)
  4. Checks if the permission's resource matches (supports "*" wildcard)
  5. Returns true if any permission matches

Superadmins receive {"action": "*", "resource": "*"} which matches everything.

The sidebar menu uses permissions to conditionally render items:

function Menu() {
  const { permissions } = usePermissions<Permission[]>();
  const can = (resource: string) =>
    canAccessResource(permissions, resource);

  return (
    <RaMenu>
      {can("borrowers") && (
        <RaMenu.ResourceItem name="borrowers" />
      )}
      {can("loans") && (
        <RaMenu.ResourceItem name="loans" />
      )}
    </RaMenu>
  );
}

The helper canAccessResource() checks for either "list" or "show" action on the resource.

Resource Screen Gating

Within resource screens, permissions control action buttons and form fields:

function LoanShow() {
  const { permissions } = usePermissions<Permission[]>();

  return (
    <Show>
      <SimpleShowLayout>
        <TextField source="loan_number" />
        {hasPermission(permissions, "approve", "loans") && (
          <ActionButton action="approve" label="Approve" />
        )}
      </SimpleShowLayout>
    </Show>
  );
}

Permission Sources

The backend merges three sources into the final permission array:

  1. Tenant role mapping --- Role-based defaults from common/rbac.py
  2. Django auth permissions --- Group and Permission model assignments
  3. Object-level grants --- django-guardian per-object access

Role Hierarchy

Role Access Level
viewer Read-only access to all resources
collector Viewer + collection actions, cases
loan_officer Collector + loan origination, servicing, borrower management
admin Loan officer + user management, configuration, GL operations
superadmin Full access (wildcard *)

Field-Level Deny

SSN access is denied for all roles via a deny permission:

{"type": "deny", "action": "show", "resource": "borrowers", "record": {"ssn": true}}

This prevents the SSN field from being displayed in borrower detail views.

See Also