Protect Your Business Data: A Practical Guide to Roles and Permissions in .NET
Decide who can see and change what inside your business software, with roles, least privilege, record-level access and audit logs.

In this article28
In most business systems, the biggest data risk isn't a stranger on the internet. It's an account that can do more than it should: a cashier who can delete invoices, a sales rep who can export every customer, a former employee whose login still works. Access control decides who can see and do what, and it's one of the decisions that's hardest to fix after the software is built. This guide explains roles and permissions in business software, how to design them around real work, and how to enforce them properly in a .NET application.
Two different questions: who are you, and what may you do?
Authentication confirms identity: the user proves who they are with a password, a passkey, a one-time code or a company sign-in. Authorisation decides what that identified user may access or change. A system can have excellent login security and still leak data if authorisation is weak. Broken access control sits at the top of the OWASP Top 10 list of web application security risks, which says a lot about how often this goes wrong.
Multi-factor authentication (MFA) strengthens the first question by making stolen passwords less useful, especially for administrators. It doesn't answer the second question at all. An attacker who gets in through any account inherits exactly what that account is allowed to do, which is why permissions matter even when login is strong.
Users, roles and permissions
- User: one real person with their own account. Never share accounts; without individual users there is no accountability.
- Permission: one specific action on one type of data, such as "view invoices", "approve refunds" or "export customers".
- Role: a named bundle of permissions matching a job, such as Cashier, Branch Manager or Accountant. Users get roles; roles get permissions.
This model is called role-based access control (RBAC). Its advantage is manageability: when a new accountant joins, you assign the Accountant role instead of ticking forty boxes, and when the accountant's duties change, you change the role once for everyone.
Least privilege as the default
The principle of least privilege means each person and each system account gets only the access needed for their work, and nothing "just in case". It limits the damage of mistakes, misuse and compromised accounts. In practice, new roles start with no permissions and gain them deliberately.
A practical role structure
Every business is different, but most systems start from a structure like this and refine it:
| Role | Typical permissions | Typical restrictions |
|---|---|---|
| Super Admin | System settings, users and roles, integrations | Very few people; not used for daily work; every action logged |
| Manager | Approve discounts and refunds, view reports for their scope, manage their team's records | Limited to their department or branch; can't change system settings |
| Accountant | Invoices, payments, financial reports | Can't change prices or stock quantities directly |
| Employee / Operator | Create and update the records their job requires | No deletions, no exports, no access to other teams' data |
| Auditor (read-only) | View records and logs within scope | Can't create, edit or delete anything |
Keep the Super Admin role for administration. Managers who also administer the system should have two accounts: one for daily work and one, protected with MFA, for administration.
When roles alone aren't enough
Department and branch scope
Two branch managers have the same role but should see different data. Scope adds a second dimension: the role says what you can do, the scope says where. Model branch, department or region as part of the user's profile and filter every query by it.
Record-level access
Sometimes access depends on the specific record: a sales rep may edit only their own leads, or a document is visible only to its project team. This is record-level (or resource-based) authorisation, and it must be checked for every record requested, not only when a list is displayed. A classic flaw is a URL like /invoices/1043 that returns any invoice to any logged-in user who changes the number.
Approvals and separation of duties
Separation of duties means no single person can complete a sensitive process alone. The person who creates a supplier shouldn't approve payments to that supplier; the person who requests a large discount shouldn't approve it. Build approval steps into the workflow, with thresholds such as "refunds above a set amount need a manager", and prevent users from approving their own requests.
Sensitive fields and exports
Some data deserves extra protection even inside an allowed record: cost prices, salaries, national ID numbers, bank details. Field-level rules can hide or mask these from roles that don't need them. Treat bulk export as its own permission. Viewing one customer at a time and downloading the entire customer list are very different risks.
Enforcement: where security actually happens
Hiding a button is not security
Hiding the "Delete" button from a cashier improves the interface, but it doesn't protect anything. Anyone can send a request directly to the server with a browser's developer tools or a simple script. Every permission must be enforced on the server, for every request, regardless of what the interface shows. The interface should reflect permissions; the server must enforce them.
APIs need the same rules
Mobile apps, integrations and front-end frameworks talk to the server through APIs. Each API endpoint needs the same authorisation as the screen that uses it, including record-level checks. Integration accounts should have their own narrowly scoped permissions, as described in our guide to API integration for business.
Implementing permissions in .NET
ASP.NET Core has a mature authorisation framework that supports several approaches:
- Role-based checks such as restricting an endpoint to the Manager role. They're simple, but scattering role names across code makes later changes painful.
- Policy-based authorisation: named policies backed by requirements and handlers. Code asks "does this user satisfy the Invoices.Approve policy?" and the policy decides how.
- Resource-based authorisation through
IAuthorizationService, which evaluates a policy against a specific record, for example "same branch" or "owner of this lead".
A scalable pattern for business systems is to treat permissions as the unit the code checks, and roles as data that administrators manage. The code never asks "is this user a Manager?"; it asks "may this user approve invoices?". For example:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Invoices.Approve", policy =>
policy.RequireClaim("permission", "invoices.approve"));
});
app.MapPost("/invoices/{id}/approve", ApproveInvoice)
.RequireAuthorization("Invoices.Approve");
Here the user's permissions, loaded from their roles, are issued as claims at sign-in or resolved per request, and the record-level rule (same branch, not your own request) is checked inside the handler with IAuthorizationService. When the business creates a new role, administrators combine existing permissions in the admin screen and no code changes. Identity can come from ASP.NET Core Identity for self-contained systems or from a company identity provider such as Microsoft Entra ID; the authorisation design stays the same.
Architecture choices that scale
- Define permissions in one place, with clear names, and generate admin screens from that list.
- Apply authorisation in the application layer that all screens and APIs share, so no path bypasses it.
- Filter queries by scope at the data-access level, so lists never include records the user can't see.
- Decide how quickly permission changes must take effect, and refresh or revoke sessions accordingly.
- Write automated tests for "must be denied" cases, not only "must work" cases.
Audit logs and the access lifecycle
What to log
Record who did what and when for sensitive actions: logins, failed logins, approvals, deletions, exports, price changes and, importantly, permission changes. A log that shows "user X was given the Accountant role by user Y" is essential for investigating problems. Protect logs from being edited by the same people they record.
Joiners, movers and leavers
- Joiners: new accounts get a defined role, not a copy of a colleague's account with accumulated extras.
- Movers: when someone changes job, remove the old role's access, don't just add the new one.
- Leavers: disable accounts on the last working day and end active sessions. Disable rather than delete, so history and audit records keep their meaning.
- Periodic review: managers confirm their team's access every quarter or half-year.
Example: a distribution company
A distributor runs sales, a warehouse and accounting in one .NET system across two branches. Sales reps create orders and see only their own customers. Branch managers approve discounts above a threshold and see their branch's reports. Warehouse staff confirm picking and receiving but can't see prices. Accountants record payments but can't edit orders after invoicing. Exports are limited to managers and logged. The owner reviews a monthly report of permission changes and disabled accounts. None of these rules is complex alone; together they prevent the most common internal data problems.
Common mistakes
- Giving everyone admin access "for now" to speed up launch.
- Relying on hidden menus and buttons instead of server checks.
- Checking permissions on lists but not on individual record requests.
- Hard-coding role names throughout the code, so every new role needs a developer.
- Shared accounts at counters or in departments.
- No log of permission changes and no process for leavers.
Design permissions before development
Before any code is written, build a simple permission matrix: rows are actions (view, create, edit, delete, approve, export) for each type of data, columns are roles, and each cell says yes, no or "own records / own branch only". Review it with department heads. It's far cheaper to change a spreadsheet than a deployed system, and the matrix becomes the specification and the test plan.
Frequently asked questions
How many roles should we have?
As few as reflect genuinely different jobs. If you have nearly as many roles as users, permissions are probably being granted person by person instead of by job.
Can administrators create new roles without a developer?
Yes, if the system checks permissions rather than role names. Administrators then combine existing permissions into new roles from the admin panel.
Is MFA enough to protect our data?
No. MFA makes account takeover harder, but it doesn't limit what an account can do once signed in. You need both strong authentication and well-designed authorisation.
Can permissions be added to an existing system?
Usually, but it costs more than designing them upfront, because every screen, query and API must be reviewed. Start with the most sensitive actions such as deletions, approvals and exports.
Should the business owner have full access?
The owner should be able to see everything relevant, but daily use is safer with a normal account, keeping the Super Admin account for administration and protecting it with MFA.
Conclusion
Good access control is a business design exercise first and a technical one second. Define roles around real jobs, start from least privilege, add scope and record-level rules where needed, separate sensitive duties, log permission changes, and enforce everything on the server. In .NET, policy- and permission-based authorisation lets these rules grow with the business without rewriting code.
Plan Access Control for Your Business System
If you're planning a new .NET system or worried about who can access what in your current one, start with a permission matrix and a review of the sensitive actions. Explore C# and .NET application development, or discuss your system's access requirements.



Comments
No comments yet. Share your thoughts or questions.
Leave a comment
Comments are reviewed before they appear.