The ERP Security Model
ERP authorisation is more granular than typical application security, because a single user might legitimately create purchase orders for one plant, view financials for one company code, and have no access to payroll at all.
The dimensions of ERP authorisation
WHO user, role, group
WHAT object/entity — sales order, GL account, employee record
ACTION create, read, update, delete, post, approve, release, reverse
WHERE company code, plant, sales org, cost centre, warehouse
WHICH a subset of rows — my customers, my region, my cost centre
FIELD which attributes — see the order but not the margin
WHEN time-bounded access, emergency elevation
Most application frameworks handle WHO/WHAT/ACTION. ERP needs all seven.
The layered model
User
└── Roles (one user, several roles)
└── Authorisations / permissions
└── Objects with field-level values
└── Organisational restrictions
Worked example:
Role: Purchasing Clerk — Dhaka
Object: Purchase Order
Activity: Create, Change, Display (not Delete, not Release)
Purchasing org: 1000
Purchasing group: 101, 102
Plant: DHK1, DHK2 (not CTG1)
Document type: NB (standard), not FO (framework)
Value limit: up to 500,000 BDT
Object: Vendor Master
Activity: Display only
Company code: 1000
Object: GL Account
Activity: Display
Company code: 1000
That’s one role. A real system has 50–300 roles, and designing them is a specialist job.
Role design
Approaches
| Approach | Description | Verdict |
|---|---|---|
| Job-based (single roles) | One role = one job. “AP Clerk” contains everything an AP clerk needs | Simple, but proliferates as jobs vary |
| Task-based (composite roles) | Small task roles combined into composite roles per job | Recommended. Flexible and maintainable |
| Derived roles | A master role defines the activities; derived roles vary only the org values | Essential for multi-site. One master, N sites |
| Ad hoc | Grow organically from requests | The default failure mode |
The recommended structure:
Master role: PO_CLERK_MASTER (defines all activities and objects)
├── Derived: PO_CLERK_DHAKA (org values: plant DHK1, DHK2)
├── Derived: PO_CLERK_CHATTOGRAM (org values: plant CTG1)
└── Derived: PO_CLERK_DUBAI (org values: plant DXB1)
Change an activity once in the master → propagates to all derived roles.
This is what makes a global rollout maintainable. Without derived roles, adding a country means duplicating and hand-editing dozens of roles.
Naming convention
Z_<AREA>_<FUNCTION>_<LEVEL>_<ORG>
Z_MM_BUYER_DISPLAY_ALL
Z_MM_BUYER_CREATE_DHK
Z_FI_AP_CLERK_1000
Z_SD_ORDER_ENTRY_EXPORT
Consistent naming makes the role catalogue navigable and makes SoD analysis possible. Inconsistent naming makes both impossible.
Row-level security
Restricting which records, not just which type:
Sales rep sees only their own customers:
WHERE sales_rep_id = :current_user
Plant manager sees only their plant:
WHERE plant_id IN (:user_authorised_plants)
Manager sees their team's records:
WHERE employee_id IN (SELECT id FROM employee WHERE manager_id = :current_user)
Implementations vary: SAP uses authorisation objects checked in code and structural authorisations in HR; Odoo uses record rules with domains; Frappe uses User Permissions; PostgreSQL-based systems can use Row Level Security policies.
The organisational dimension is the primary row-level filter in most ERPs. Company code, plant, sales org, and purchasing org values on the role determine which rows a user sees. Design your custom tables with those columns so the same mechanism works.
Field-level security
Sales order:
Order value — visible to all sales users
Cost / margin — visible to managers only
Customer credit limit — visible to credit team only
Employee record:
Name, department, extension — visible to all
Salary, bank details — visible to HR and the direct manager only
Health/medical — visible to occupational health only
Field-level security is more work but genuinely necessary in HR, and increasingly in sales (protecting margin data) and finance.
Critical authorisations to control tightly
| Authorisation | Risk |
|---|---|
| Post to GL directly | Bypass controls, misstate accounts |
| Change vendor bank details | Payment redirection fraud |
| Release payment runs | Direct theft |
| Create/change vendor master | Fictitious vendor fraud |
| Change customer credit limit | Bad debt exposure |
| Reverse posted documents | Cover up errors or fraud |
| Change posting periods | Post to closed periods |
| Change configuration in production | Anything |
| Debug/execute arbitrary code | Total system access |
| Table maintenance (direct edit) | Bypass all business logic |
| User administration | Grant themselves anything |
| Run OS commands / read files | System compromise |
| Change master data for pricing | Revenue leakage |
| Approve their own requests | Defeats every control |
Table maintenance and debug authorisation in production are the two most commonly over-granted and most dangerous. Debug-with-replace lets a developer change variable values at runtime, bypassing every check in the code. It should not exist in production for anyone.
Superuser and emergency access
SAP_ALL / sysadmin / Administrator
= complete access to everything
Rules:
- Not assigned to any named user in production, ever
- Firecall / break-glass account, checked out through a controlled process
- Every use: requested, approved, time-limited, fully logged, reviewed after
- Session recording where possible
- Automatic revocation after the window
This is what Privileged Access Management (CyberArk, SAP GRC Emergency Access Management, Delinea, One Identity) exists for. The auditor will ask: who has superuser access, how many times was it used last quarter, and who reviewed each use?
“The consultant needs SAP_ALL to do their job” is never true and always an audit finding.
Service and technical accounts
Frequently the weakest link:
❌ One shared "INTERFACE_USER" with wide permissions, password in a config file,
never rotated, used by 14 interfaces
✅ One named account per interface
Least privilege — only the objects that interface actually touches
Credentials in a secrets manager, rotated
Non-interactive (cannot log into the UI)
Usage logged and monitored
Ownership documented
When an interface account is shared, you lose attribution. “Which system posted this journal?” becomes unanswerable, and that’s an audit finding on its own.
User lifecycle
JOINER HR hire event → provision account → assign role per position
MOVER HR transfer → REMOVE OLD ROLES → assign new roles
LEAVER HR termination → disable immediately → delete after retention
The MOVER case is where access accumulates. Someone moves from Purchasing to Finance, gains Finance roles, and keeps Purchasing roles — and now has an SoD conflict nobody noticed. Role removal on transfer must be automatic, not a request someone might raise.
Standard reviews:
- Access recertification — managers confirm their team’s access quarterly or half-yearly
- Orphan account report — accounts with no matching active employee
- Dormant account report — no login in 90 days
- Privileged access review — monthly
Authentication
| Method | Notes |
|---|---|
| SSO via SAML/OIDC | The standard. Central control, MFA at the IdP |
| MFA | Mandatory for privileged access; increasingly for everyone |
| Certificate-based | Some high-security environments |
| Passwords | Fallback only; strong policy required |
Integrate the ERP with the corporate IdP (Entra ID, Okta, Ping, Keycloak). Benefits: one termination action disables all access, consistent MFA, and central session policy.
Session management: timeout for inactivity (30 minutes is typical, shorter for privileged), concurrent session limits, and forced re-authentication for sensitive actions like payment release.
Logging and monitoring
What must be logged:
- Authentication: success and failure, with source IP
- Authorisation failures (a burst indicates probing or a role gap)
- All changes to master data, with before/after values
- All changes to configuration
- All privileged/emergency access use
- All changes to user access and roles
- Access to sensitive data (salary, personal data) — reads, not just writes
- All postings, with user and timestamp
- Data exports and downloads
- Direct table modifications (if they are possible at all)
Ship these to a SIEM. Alerts worth having:
- Privileged access used outside a change window
- Vendor bank detail changed
- User granted a critical authorisation
- Mass data export
- Login from an unusual location for that user
- Repeated authorisation failures
- Posting to a closed period
- Direct table modification
Developer responsibilities
Every custom object must check authorisation:
❌ A custom report that reads every company code because
the developer forgot to check the user's org authorisation
✅ Explicit authorisation check on entry, and the org filter
applied in the query — not after fetching everything
Checklist for every custom object:
- Authorisation check performed before any data access
- Organisational scope applied in the WHERE clause, not in application code afterwards
- Sensitive fields suppressed if the user lacks field-level access
- Failure produces a clear message, not a dump or an empty screen
- The check is tested — with a restricted user, not with an admin account
- No hardcoded credentials, ever
- SQL injection impossible (parameterised queries)
- Export functions respect the same authorisation as the screen
That last one is a frequent gap. A report correctly restricts what’s displayed, and the “export to Excel” path queries the table directly with no filter.