العودة إلى المدونة
Custom Software

RBAC Explained: How to Design User Roles and Permissions

What is authorization in software and how do you design role-based access control (RBAC)? Roles versus permissions, the permission matrix, record-level access, audit logs and common mistakes.

YetkilendirmeRBACKullanıcı RolleriGüvenlikÖzel Yazılım

Authorization is the layer that decides which screens a signed-in user can see, which records they can open and which actions they can perform. Authentication answers "who are you"; authorization answers "what are you allowed to do". Role-based access control (RBAC) is the most common model: permissions are attached to roles rather than to individual users, and users are assigned roles. Done right, a new employee is set up in a minute and a leaver is switched off with one click. Done wrong, the "only managers should see this" conditions patched onto every screen turn, within a few months, into a structure nobody understands.

Authentication versus authorization

The two are often confused because both are associated with the login screen. Authentication proves the user is who they claim to be: a password, an SMS verification code, single sign-on (SSO). Authorization draws the boundaries for a user whose identity has already been proven. Authentication happens once; authorization is asked again on every request and every record. That is why a large share of security flaws appear not at the login screen but after it, where the question "should this user really see this record" is skipped.

What is role-based access control (RBAC)?

RBAC has three parts: users, roles and permissions. A permission is the smallest unit of authority and is named as "which action on what": view invoice, create invoice, cancel invoice. A role is a bundle of permissions that make sense together: accountant, sales representative, warehouse lead. A user is assigned a role and automatically gains its permissions. The critical rule is this: code never asks "is this user an admin", it asks "does this user have the cancel-invoice permission". Role names can change and new roles can be added, but because the code is written at permission level it never needs to be touched.

  • User: a real person who signs in, or an integration account.
  • Role: the counterpart of a job in the business (sales, accounting, warehouse, manager).
  • Permission: the right to a single action (view order, approve order, edit price).
  • Assignment: giving a user one or more roles; permissions flow through the role.
  • Scope: which branch, region or department the role applies to.
The most expensive mistake in authorization is attaching permissions to people instead of roles. Three months later nobody can answer "why can Ayşe see this screen", and the access of an employee who left stays in the system.

How to design the role and permission structure

Design starts with a table, not with code: the permission matrix. Roles in the rows, permissions in the columns, yes/no in the cells. The business fills this table in, not the software company, because the answer to "who approves a refund" is a business rule. When the matrix is delivered as part of the project brief, there are no screen-by-screen debates during development. A few principles make a good matrix:

  • Actions first, roles second: list every action the system can perform, then build roles from that list; the other way round, roles end up shaped around people.
  • Least privilege: each role starts with the narrowest permission set its job needs and grows as needs appear; nobody gets "see everything" by default.
  • Viewing and editing are separate permissions: not everyone who sees a report needs to edit the record.
  • Approvals are separate permissions: the person who creates an order should not be the one who approves it (separation of duties).
  • Five to ten roles are enough for most businesses: if you have twenty, you have probably modelled people instead of permissions.

Screen-level access is not enough: record-level access

RBAC says "a sales representative can view orders", but which orders? In most business systems the answer is "only their own customers" or "only their own branch". This is called record-level or row-level access, and RBAC alone does not solve it. The solution is to add scope next to the role: sales role plus Ankara region. The server adds that scope as a filter to every query. The filter must be applied on the server, not in the interface; a button hidden in the UI is bypassed by typing a record id straight into the address bar. The IDOR vulnerability we describe in our mobile app security post comes precisely from skipping this check. In B2B dealer systems, a dealer seeing only its own orders relies on the same mechanism.

What does an authorization check look like in code?

The example below shows the permission check written against a permission name rather than a role name, and the scope filter added to the query. The check lives in one function; screens and API endpoints call it instead of writing their own conditions.

type Permission = 'order:view' | 'order:approve' | 'price:edit';

type User = {
  id: string;
  roles: string[];
  branchIds: string[];
};

const rolePermissions: Record<string, Permission[]> = {
  sales: ['order:view'],
  salesManager: ['order:view', 'order:approve', 'price:edit'],
};

export const can = (user: User, permission: Permission) =>
  user.roles.some(role => rolePermissions[role]?.includes(permission));

export const approveOrder = async (user: User, orderId: string) => {
  if (!can(user, 'order:approve')) {
    throw new Error('FORBIDDEN');
  }
  const order = await db.order.findFirst({
    where: { id: orderId, branchId: { in: user.branchIds } },
  });
  if (!order) {
    throw new Error('NOT_FOUND');
  }
  return db.order.update({
    where: { id: order.id },
    data: { status: 'approved', approvedBy: user.id },
  });
};

Note two details: for a record outside the user’s scope the response is "not found" rather than "forbidden", so the existence of the record is not leaked. The approving user’s id is written onto the record; that is the beginning of the audit log covered in the next section.

Why an audit log is mandatory

Granting access is only half the job; recording who did what and when is the other half. An audit log stores critical actions such as price changes, refund approvals and role assignments together with the user, the time, the old value and the new value. It is not just a security tool; it is the strongest evidence in a business dispute. In systems that hold personal data, logging access is also part of KVKK compliance: when asked which employee viewed which customer record and when, the system must be able to answer. The audit log has to be immutable, so that even the most senior administrator cannot erase their own trail.

Common mistakes

  • Starting with a single "admin" and a single "user": in month two, when someone says "accounting should not see everything", every screen is reopened.
  • Checking permissions only in the interface: a hidden menu is bypassed with a direct API request.
  • Hard-coding role names: every "if role === manager" breaks when the role is renamed.
  • Forgetting scope: two branch managers with the same role see each other’s data.
  • Having no offboarding flow: the account is never closed, only the password is changed, and integration keys stay live.
  • Using the superuser for daily work: an account that can do everything can also break everything.

Off-the-shelf or custom-built?

In packaged software the role structure is fixed; if the business’s approval chain does not match the package, the process is bent to fit the software. In the off-the-shelf versus custom software decision, the permission structure is often the deciding criterion: in multi-branch systems, multi-level approval flows or systems opened to external users such as dealers and customers, the access model is the product itself. In our corporate solutions we deliver the role and permission design during analysis, together with the permission matrix, and the code is generated from that table.

Conclusion

Authorization is not a security feature bolted on later; it is the backbone of every multi-user system. Attach permissions to roles, write code against permission names, filter scope on the server and put critical actions in an audit log. With these four rules in place, opening a new branch or defining a new job becomes a setting in the admin panel rather than a code change. If you are planning a system with a complex permission structure, see our custom software service or get a quote.

Let's Build Your Project

Get a free consultation for your website, mobile app, or corporate software project.

Get a Free QuoteExplore our Custom Software service