Wise Hustlers — Digital Product & App Development Studio Logo
Get Consultation
By Wise Hustler Admin9/1/20269 min read

Permissions That Don't Lie: RBAC and Immutable Audit Trails in an Industrial ERP

Permissions That Don't Lie: RBAC and Immutable Audit Trails in an Industrial ERP

# Permissions That Don't Lie: RBAC and Immutable Audit Trails in an Industrial ERP

TL;DR: RBAC access control in an industrial ERP only protects you for as long as exception grants are reviewed with the same discipline used to create them — and the database, not the application, has to be the last line of defence against rewriting history.

Anyone who builds systems for upstream operations, EPC contractors, or oilfield services in Angola has heard the sentence "we just need this for a week, until we close inventory." That is exactly where RBAC access control starts to fail — not in the design, but in the upkeep. Wise Hustlers builds and operates its own energy-sector ERP. This article draws on that to describe how to structure roles, permissions, and grants so they survive years of one-off exceptions without turning into Swiss cheese — and how to design an audit table that the database itself refuses to alter.

Roles, permissions, and grants: three things people conflate

The reference standard for RBAC — INCITS 359, formalised from the original work of David Ferraiolo and Rick Kuhn at NIST in 1992 — defines a role as "a job function within the context of an organization with some associated semantics regarding the authority and responsibility conferred on the user assigned to the role" (ANSI Blog). In practice, on an industrial ERP, it pays to keep three layers separate and never merge them:

  • Permission — the atomic unit: can approve purchase requisitions up to $50,000, can close an accounting period, can edit well master data. A permission doesn't know who holds it; it only describes an action on a resource.
  • Role — a named set of permissions that maps to a real function in the organisation: Production Engineer, Financial Controller, Field HSE Coordinator. The role is the unit you assign to people, not the individual permission — this is what makes RBAC manageable: changing what a Financial Controller can do changes it for everyone holding that role, without touching individual accounts.
  • Grant — the concrete link between a user (or group) and a role, usually with a scope: this business unit, this project, this period. This is where the time dimension enters — and where most systems fail, because they treat a grant as permanent by default.

The most common confusion we see in legacy systems is permissions attached directly to individual users, with no role in between. It works fine for the first six months. Two years in, nobody knows why user jsilva has write access to the treasury module, because that grant never went through a role review — it was a one-off request that stuck around.

The central failure mode: the exception nobody revokes

This is the most important point in this article, and it is systematically underestimated: RBAC does not fail from a lack of rules — it fails because a rule stops reflecting reality and nobody notices.

The pattern is always the same. Someone needs broader access for a legitimate, temporary reason: covering an absence, resolving an incident, closing a late fiscal period. An exception grant is created — a user given a wider role than normal, or a rule that bypasses a usual validation ("skip manager approval for requisitions below X, just for this campaign"). The exception solves the immediate problem. And then it stays.

Nobody revokes it because revoking requires someone to remember — and the system, as designed, doesn't force anyone to. The exception doesn't show up as an active alarm; it shows up as a quiet row in a permissions table nobody reopens. Two years later, an auditor (internal, from ANPG, or from an international partner on a joint operation) asks why a service account has unlimited financial approval access. The answer, invariably, is "that was set up for a project that ended a while ago."

The central point is this: a stale exception list is not neutral — it is an active vulnerability that looks like a control. The guard is still standing there, but has stopped looking. The grant is still in the table, the access report still "passes," the compliance checkbox is still ticked — and yet the real protection is gone, because the exception silently disarmed it. This is consistent with what OWASP documents as the most prevalent security failure category in applications: Broken Access Control (A01:2021) was found in 94% of applications tested, with more reported occurrences than any other Top 10 category (OWASP) — not because systems lack access control, but because the control that exists degrades without anyone noticing.

The practical mitigation we apply in an industrial ERP has three components, none of them optional:

1. Every exception grant has a mandatory expiration date at the schema level — not an optional field someone fills in if they remember, but a NOT NULL column with a short default (7 to 30 days, depending on the exception type).

2. Expiration is automatic, not requested. A scheduled job revokes the grant at midnight on the expiry date, with no human step required. If it's still needed, someone has to actively renew it — the burden sits on keeping access, not on removing it.

3. Every grant active for more than 90 days shows up in a recurring review report, visible to the role owner and the security team — not hidden in a table only opened during an external audit.

This is, at its core, access control applied with the same discipline as the identity and vulnerability management work described in more detail on the Wise Hustlers cybersecurity page — well-designed RBAC is a security component of the system, not an admin form.

Immutable audit trails: when the application layer isn't enough

A well-designed RBAC system answers "who can do this." An audit trail answers the much less comfortable question, "who did this, when, and what was it before." In the modules where this weighs most — capital expenditure approval, production allocation, maintenance work orders — auditing is not a nice-to-have compliance requirement; it is the only mechanism that lets you reconstruct a financial or operational decision months later.

The most common mistake is relying solely on the application layer to guarantee audit records aren't altered: a service that writes to an audit_log table and assumes that, because only that service writes there, the records are protected. This breaks the moment someone with direct database access — an administrator, a poorly written migration, a data-fix script run under pressure — "just fixes this one record" directly in production. From that point on, the audit trail isn't proof anymore; it's an opinion.

The correct alternative is to make the database itself refuse the change, regardless of who asks:

-- Revoke UPDATE and DELETE from the application role on the audit table
REVOKE UPDATE, DELETE ON audit_log FROM app_role;
GRANT INSERT, SELECT ON audit_log TO app_role;

-- Reinforce with a trigger, to cover accounts with broader privileges
CREATE OR REPLACE FUNCTION audit_log_no_mutation()
RETURNS TRIGGER AS $$
BEGIN
  RAISE EXCEPTION 'audit_log is append-only: UPDATE/DELETE not allowed';
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER audit_log_block_update
  BEFORE UPDATE OR DELETE ON audit_log
  FOR EACH ROW EXECUTE FUNCTION audit_log_no_mutation();

This pattern — database-level privileges combined with a trigger that explicitly rejects UPDATE/DELETE — is established practice for audit tables in PostgreSQL: "the simplest rule is: never update or delete audit rows, only insert" (PostgreSQL wiki, Audit trigger). It's worth being honest about what this guarantees and what it doesn't: blocking edits by privilege is not the same as making edits cryptographically detectable — any account with superuser privileges, or direct disk access, can still alter history (AppMaster). That's why, in contexts that demand stronger proof (litigation, arbitration between block partners, an ANPG inspection of a local-content process), the next step is hash chaining — each record includes the hash of the previous record, making any later alteration mathematically detectable, even by someone with superuser access.

For Angolan operators, this design serves a second purpose: the personal data protection legal framework (Lei n.º 22/11, overseen by the Agência de Protecção de Dados) requires security and accountability practices around the processing of personal data, and an audit trail the database itself protects is the most direct way to demonstrate — rather than merely claim — that access to and changes on sensitive data were recorded with integrity.

A practical checklist

ElementQuestion it should answerWhere it usually fails
RoleWhat permission set maps to this function?Overly generic roles ("Admin") that accumulate everything
PermissionWhat concrete action on what resource?Vague permissions covering entire modules
GrantWho holds this role, with what scope, until when?No expiration date; never reviewed
ExceptionIs this broadened grant still needed?Created for an incident, never revoked
AuditWho changed this, and can the record be altered?Trusting the application alone; no database-level REVOKE

FAQ

Is RBAC enough, or do I need ABAC (attribute-based access control)?

For most modules of an industrial ERP, scoped RBAC (by business unit, project, or cost centre) covers it well. ABAC — decisions based on dynamic attributes like location, time, or asset classification — pays off when business rules depend on context that changes frequently, for example in field HSE. Many mature systems combine both: roles for structure, attributes for contextual exceptions.

Doesn't an append-only audit table grow the database indefinitely?

Yes, and that's intentional — audit history should never be deleted for disk space. The correct answer is date-based partitioning and cold-archiving (to cheaper storage) of old partitions, never deleting active records.

Who should be allowed to create an exception grant?

The same role that can revoke it, never a different one — otherwise you create an asymmetry where it's easy to grant and hard to revoke, which is exactly the pattern that produces forgotten exceptions.

Does this apply to smaller systems, or only large-scale ERPs?

The principle applies to any system holding sensitive financial or operational data. Scale changes the complexity of the role structure, not the need for automatic exception expiration or database-protected auditing.

Sources