YHVH CYRUS ENTERPRISES PTE. LTD. UEN 202240171D  ·  Singapore
YHVH Cyrus Enterprises Contact YHVH

Software Development

Audit Trails and Permission Models for Internal Business Applications

An audit trail records what people did; a permission model decides what they may do. Both are cheap to design in early and very hard to retrofit.

Most internal business applications start life with two security features: a login screen and a single "admin" checkbox. That is enough for a pilot. It stops being enough the first time someone asks one of two questions: "Who changed this record, and why?" or "Why could that person see this at all?"

Both questions are about the same underlying design: a permission model that decides what each person may do, and an audit trail that records what they actually did. This article sets out how to design both for internal operational software such as approval systems, case management tools and finance workflows.

What an audit trail is, and what it is not

NIST's glossary defines an audit trail as a chronological record that reconstructs and examines the sequence of activities surrounding an operation, procedure or event "from inception to final result". The important word is reconstructs. An audit trail is not a debug log, and it is not the "last modified by" column on a table. It is a record complete enough that someone who was not present can work out what happened, in what order, and who was responsible.

That sets a practical test. Pick any important record in your system, such as a payment approval, a customer credit limit or a contract status, and ask whether you could answer the following from stored data alone:

  • Who created it, and when?
  • Every change to it: the old value, the new value, who made the change and when.
  • Who approved it, under what authority, and whether that person was entitled to approve it at the time.
  • Who viewed it, if the record is sensitive enough that access itself matters.

If the answer to any of these is "we would have to ask around", the system does not have an audit trail.

What to record for each event

The OWASP Logging Cheat Sheet puts the requirement simply: application logs must record "when, where, who and what" for each event. For an internal business application, that translates into a small, consistent event structure:

  • When. A server-side timestamp in a single, explicit time zone format. Never rely on the client's clock.
  • Who. The authenticated user identity, and, where someone acts on another's behalf, both identities. Service accounts and scheduled jobs are actors too and need their own identities rather than borrowing a human's.
  • What. The action (created, updated, approved, exported, deleted), the object type and identifier, and for updates, the fields that changed with their before and after values.
  • Where. The entry point (screen, API endpoint or integration) and the application version.
  • Why, where it exists. A reason code or comment for consequential actions such as overrides, reversals and manual adjustments. This is the field auditors ask for most and systems capture least.

The same cheat sheet is equally clear about what should usually stay out of logs: passwords, session identifiers, access tokens, encryption keys, payment card data and sensitive personal data should be removed, masked, hashed or encrypted rather than recorded directly. An audit trail that copies full customer records into every event creates a second, less protected copy of your most sensitive data. Record the identifier and the changed fields, and apply the same data protection thinking you apply to the main database.

Write audit events at the application layer, in the language of the business process, and in the same transaction as the change they describe. A database log can tell you a row was updated; only the application knows that a manager approved a payment while covering for a colleague.

Protecting the audit trail itself

An audit trail that administrators can edit is an opinion, not evidence. OWASP's guidance on protecting logs at rest recommends building in tamper detection so you know if a record has been modified or deleted, copying log data to read-only storage as soon as possible, recording and monitoring all access to the logs, and restricting and periodically reviewing the privileges to read them.

In practice, for an internal application this usually means:

  • Audit records are append-only. The application has no code path to update or delete them, and the database account the application uses cannot either.
  • Audit data is copied regularly to separate storage that application administrators do not control.
  • Reading the audit trail is itself an audited action, and the list of people who can do it is short and reviewed.
  • Retention is decided deliberately, balancing legal and contractual requirements against the risk of holding data longer than needed.

Designing the permission model

If the audit trail records what happened, the permission model decides what is allowed to happen. Three principles from the security literature do most of the work.

Least privilege

NIST defines least privilege as restricting the access privileges of users, and processes acting on their behalf, to the minimum necessary to accomplish assigned tasks. OWASP's Authorization Cheat Sheet adds a useful nuance: least privilege must be applied horizontally as well as vertically. Seniority is not a permission.

Deny by default

The same OWASP guidance recommends that an application deny access by default: when no rule explicitly permits a request, the answer is no. A new record type, report or API endpoint added in a hurry should be invisible until someone decides who may use it, not visible to everyone until someone remembers to restrict it.

Separation of duties

NIST's glossary describes separation of duty as the principle that no user should be given enough privileges to misuse the system on their own, with the classic example that the person authorizing a paycheck should not also be the one who prepares it. It notes this can be enforced statically, by defining roles that cannot be held by the same person, or dynamically at the time of access, as with a two-person rule where the second approver must be a different user from the first.

Dynamic enforcement is usually what internal applications need. It is not enough that "requester" and "approver" are different roles if one person holds both; the system should check, at the moment of approval, that the approver is not the person who raised the request.

Roles, attributes or relationships?

Most internal applications start with role-based access control (RBAC): permissions are attached to roles such as "Viewer", "Editor" and "Approver", and users inherit them by holding a role. RBAC is easy to explain and easy to review, which is a genuine advantage.

Its weakness shows when "approvers can approve" becomes "approvers can approve requests from their own department, up to their limit, unless they raised the request". Pure RBAC handles that by multiplying roles until nobody understands them. OWASP's Authorization Cheat Sheet recommends preferring attribute-based (ABAC) and relationship-based (ReBAC) access control over RBAC, and stresses that the choice has implications for the whole development life cycle and should be made as early as possible.

A pragmatic approach for most internal systems is a hybrid:

  • Use roles for the coarse question of which functions a person can reach at all.
  • Use attributes of the user and the record, such as department, region, amount and status, for the fine-grained decision on a specific record.
  • Use relationships, such as "is the requester", "is the assigned owner" or "is the manager of the requester", wherever separation of duties or ownership matters.

Whatever the model, keep the rules in one place. Authorization logic scattered across screens, API handlers and reports is where inconsistencies hide.

Enforce on the server, on every request

Hiding a button is a user interface choice, not a control. OWASP's guidance is that permissions should be validated on every request, whatever its source, because an attacker only needs to find one way in; checking correctly on most requests is not enough. Two specific failure patterns account for a large share of real-world problems in internal tools:

  • Guessable identifiers. If a user can change /requests/1041 to /requests/1042 and see someone else's record, the application is checking whether the user is logged in, not whether they may see this record. Every lookup must check the relationship between the user and the specific object.
  • Side doors. Exports, bulk actions, search results, attachments, notifications and reporting endpoints often bypass the checks applied to the main screens. They need the same rules.

OWASP also recommends automated unit and integration tests for authorization logic. A simple and effective pattern is a permissions matrix: for each role and each important action, a test that asserts both what should succeed and what should be refused. Refusal tests are the ones teams forget.

Administration, delegation and emergency access

The permission model is only as good as the process for changing it. Three situations deserve explicit design:

  • Granting and removing access. Changes to roles and permissions are themselves audit events, recording who granted what to whom and when. Joiners, movers and leavers should be driven from your identity directory where possible, so that someone who changes department loses their old access automatically.
  • Delegation. When an approver is on leave, their authority should be delegated explicitly and for a fixed period, with the audit trail recording that the delegate acted under delegation. Sharing a login is the alternative people reach for when delegation is missing.
  • Emergency access. Sometimes an engineer genuinely needs elevated access to fix a production problem. Make that a defined, time-limited, logged procedure that someone reviews afterwards, rather than a permanent superuser account.

A periodic access review closes the loop, and the audit trail makes it evidence-based: it shows who has actually used a permission, which is often a shorter list than who holds it.

Where AI components fit

Internal applications increasingly include AI features that draft, classify or recommend. The same rules apply to them, with one addition: the audit trail should distinguish between an action taken by a person and one proposed or taken by an automated component. If a model suggests an approval and a person accepts it, record both. If an agent acts automatically, it needs its own identity and a permission scope no wider than the task requires. Our articles on human oversight in autonomous AI systems and AI governance for enterprise software cover the oversight side of this in more detail.

A short implementation checklist

  1. Decide the audit event structure (when, who, what, where, why) before building the first screen, and write events in the same transaction as the change.
  2. Keep secrets and sensitive personal data out of audit events; record identifiers and changed fields instead.
  3. Make audit storage append-only, copy it off the application's own storage, and audit access to it.
  4. Choose the permission model early: roles for coarse access, attributes and relationships for record-level decisions.
  5. Deny by default, check on the server for every request, and check the user's relationship to each specific record.
  6. Enforce separation of duties at the moment of action, not only in role definitions.
  7. Write tests for what each role must not be able to do.
  8. Design delegation and emergency access as explicit, time-limited, logged procedures.
  9. Review access periodically, using the audit trail as evidence.

These decisions belong in the first release, as the article on scoping the first release of enterprise software argues: they are cheap to build in and very hard to reconstruct later. More on how we approach this in our own engineering is on the software development page.

Common questions

Is an audit trail the same as application logging?

No. Application and infrastructure logs help engineers operate and debug a system. An audit trail is a business record, written at the application layer in the language of the process, that lets someone reconstruct who did what to which record and why. The two can share infrastructure, but they have different audiences, retention and protection requirements.

Who should be able to read the audit trail?

As few people as practical, typically the system owner, internal audit and security. Access to the audit trail should itself be logged and reviewed periodically, and nobody who can read it should be able to alter it.

References

  1. OWASP Foundation. Logging Cheat Sheet. OWASP Cheat Sheet Series. Sections on event attributes, data to exclude and protection.
  2. OWASP Foundation. Authorization Cheat Sheet. OWASP Cheat Sheet Series.
  3. NIST Computer Security Resource Center. Glossary: audit trail, least privilege, separation of duty.