Design SAP access as an architect: business roles scoped by org units, duties kept apart, emergency access controlled, and AI agents inside the same design.
Every person who works in SAP needs some access, and nobody should have all of it. Authorization design decides who can do what, and where.
An architect designs access in three layers:
What a job needs. A buyer creates purchase orders. An accounts payable clerk posts invoices. These bundles of access are roles.
Where the job applies. A buyer in Rockford buys for the US company, not the German one. The org structure (company code, plant, purchasing organization) sets the boundary.
What one person must never hold together. Whoever creates a supplier should not also pay it. This is segregation of duties (SoD).
Then two exceptions need rules: emergency access, when someone needs more rights for an hour to fix production, and AI agents, which act inside SAP under an identity like anyone else.
This topic shows how to design all of that for Calder Pumpworks, the fictional pump maker this unit follows, using its procure-to-pay team.
Fraud and error. The classic losses come from one person holding two halves of a control. Someone who can change a supplier's bank account and also run payments can send money to their own account. SAP's own training uses exactly this pairing as its example access risk: a user who can maintain vendor master data and initiate payment runs (SAP Learning).
Audit cost. External auditors test who can do what. A clean role design makes that test short. A messy one turns every audit into a hunt through hundreds of users, and findings can delay the close.
Program cost. Roles are designed late on most programs, after the processes are configured. Then testing finds that nobody can post anything, and the security team builds roles by trial and error the week before go-live. Starting from the process design avoids that scramble.
License cost. In SAP S/4HANA Cloud Public Edition, each business catalog carries a price category, and SAP's training notes that standard roles often grant more than a person needs while licensing is per user per month (SAP Learning). Role design changes what you pay.
AI. An AI assistant that reads or changes SAP data does so through an identity with roles. If the roles are too broad, the assistant is too. Every AI project inherits the authorization design, good or bad.
As of October 2026, SAP offers three things an architect works with:
Need
What SAP offers
Where
Build roles
Business roles from business catalogs, with restrictions (Public Edition); roles maintained in transaction PFCG with derived roles for org scope (Private Edition, on-premise, ECC)
SAP Access Control (on-premise): risk analysis, role management, access requests, reviews, emergency "firefighter" access. SAP Cloud Identity Access Governance (on SAP BTP): the same jobs as cloud services, for cloud and on-premise systems
SAP's training describes the cloud service as a counterpart to SAP Access Control, not a replacement for it. Companies with hybrid landscapes can run both in a bridge setup (same source). The governance tools are licensed separately from S/4HANA.
A role matrix is the architect's main deliverable: roles down the side, tasks across the top, and the risky pairs marked. Here is Calder's procure-to-pay team in short form. The full matrix, with org scope and users, comes from the script in the deep layer.
Role
Supplier master
Bank details
Confirm bank change
PO
Approve PO
Goods receipt
Invoice
Release blocked invoice
Payment run
Buyer
X
Purchasing manager
X
X
Receiving clerk
X
AP accountant
X
AP supervisor
X
X
Supplier master
X
X
Master data steward
X
How the matrix turns into decisions:
Split a role when it contains a conflict on its own (the purchasing manager approves; buyers create).
Separate people when two clean roles conflict in one person (a different steward confirms bank changes).
Mitigate when a small site can't separate duties, with a named control that someone performs and documents (Augsburg's buyer also posts invoices; a finance manager reviews them monthly).
Scope by org unit so a conflict only counts where both halves reach the same company.
"SAP's standard roles are ready to use." SAP's templates are a starting point. SAP's training says to copy them, and to use templates for testing rather than for productive roles.
"Security is the Basis team's job." Technicians build roles; process owners decide what each job needs and which conflicts are acceptable.
"One user with everything is fine during go-live." Broad go-live access tends to stay. Use emergency access with logging instead.
"SoD is about people only." Integrations and AI agents hold roles too, and an agent that can release a blocked invoice is a control gap like any other.
"Cloud means SAP handles access." In S/4HANA Cloud Private Edition, SAP's training states that customers own their business roles completely. Public Edition customers build and assign their own business roles too.
Business role: the access for one job, such as buyer. In Public Edition, a business role is built from business catalogs; in PFCG systems, often a composite of single roles.
Business catalog: a group of apps and the authorizations behind them, used to build business roles.
Single, composite and derived roles: in PFCG, a single role holds authorizations; a composite role bundles single roles; a derived role copies a master role and sets its own org values.
Organizational level: a field such as company code or plant that sets where a role applies.
Segregation of duties (SoD): splitting tasks so no one person controls a whole risky transaction.
Access risk: a rule saying which tasks, alone or together, are risky; SAP Access Control distinguishes SoD, critical action and critical permission risks.
Mitigating control: a documented check that reduces a risk you can't remove.
Emergency (firefighter) access: temporary, logged elevated access for urgent fixes.
Pick one answer for each question. The explanation appears after you choose.
1A steering committee asks why role design should start during process design rather than just before go-live. What is the best answer?
Answer: A. Roles describe what each job in a process does, so they come from the process design. Built late, they are made by trial and error just before go-live, which delays testing and leaves broad access behind.
2Calder's AP team asks for one person to maintain suppliers and run the weekly payment run. What risk does this create?
Answer: A. Maintaining suppliers (including bank details) and running payments together lets one person redirect and release money. SAP's own training uses this pairing as its example access risk.
3Augsburg is small, and its buyer must also post supplier invoices. What should the design do?
Answer: C. When duties can't be separated, the risk is accepted only with a documented control, here a monthly review of POs and invoices by the same user. Switching the check off or using emergency access hides the risk instead of handling it.
4What is the main purpose of emergency (firefighter) access?
Answer: C. Emergency access is temporary, needs a reason and an approval, and every session is logged and reviewed afterwards. It is not a substitute for proper roles.
5A vendor proposes an AI agent that triages blocked supplier invoices and can release them. What should a leader ask first?
Answer: C. An agent acts through an identity with roles, so it sits inside the same SoD design as people. Releasing a blocked invoice is an approval, which should stay with a person while the agent prepares the case.
6Which statement about SAP's governance tools is accurate as of October 2026?
Answer: C. SAP's training calls SAP Cloud Identity Access Governance a counterpart to SAP Access Control, not a replacement, and describes a bridge setup for hybrid landscapes. Both analyze access risks, and both are separate products.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Think of access as a grid with three axes, and design each axis separately:
What: tasks a job performs (create a purchase order, post an invoice). Roles bundle tasks.
Where: org units the job reaches (company code CP10, plant CP11, purchasing organization PUUS). Derived roles or restrictions set them.
Who: people, technical users and agents. Assignments give them roles.
Rules sit on top of the grid. An access risk says which combinations of tasks are dangerous for one identity in the same scope. A good design keeps tasks small enough that most risks can be prevented by never putting both halves in one role, and checks assignments for the rest.
The architect's job is not to set authorization field values; that belongs to the security team. It is to decide the shape: which jobs exist, which org units bound them, which conflicts the company refuses, which it accepts with a control, and how exceptions (emergencies, integrations, agents) fit in.
At runtime, SAP checks authorization objects: groups of fields with allowed values, such as an activity and an org unit. Agent permissions and SAP authorizations explains objects, activities and the check itself; this topic works one level up.
For purchasing, SAP's documentation names objects such as M_BEST_EKO (purchase orders by purchasing organization, paired with M_BANF_EKO for requisitions), M_BEST_BSA (document type) and M_BEST_WRK (plant); an SAP knowledge base article on them lists SAP S/4HANA among the products it applies to (SAP KBA 2846590). Invoice verification uses objects such as M_RECH_WRK (invoices by plant) and M_RECH_SPG (invoices by blocking reason) (SAP Help), and the payment program checks F_REGU_BUK by company code (SAP Help). Those two help pages are older ERP documentation; confirm the objects your release checks before you rely on them.
A technical role holds the authorizations for a small, coherent task: "create and change purchase orders". It is built by the security team and rarely changes.
A business role is what a person gets: "Rockford buyer". It bundles technical roles and carries the job's org scope.
SAP's tools reflect the split. In PFCG systems, a composite role contains single roles and has no authorizations of its own; users are assigned to the composite (SAP Learning). In SAP Cloud Identity Access Governance, backend roles from the target system are included in a business role, which is what gets assigned (SAP Learning).
The design rule that follows: keep each technical role free of conflicts on its own. If no technical role contains both halves of a risk, conflicts can only arise from assignments, and assignments can be checked automatically.
How an SAP enterprise is wired showed Calder's org units. In authorization design, they become organizational levels: fields the role keeps apart from its general values. SAP's training names company code, business area, account type, controlling area, division, sales organization, distribution channel and plant among them.
The technique for scoping in PFCG is the derived role:
Build a master role with the menu and authorizations for a job, for example ZM_P2P_BUYER.
Create derived roles from it in the Derive from Role field, one per scope: ZD_P2P_BUYER_US for CP10, CP11, PUUS; ZD_P2P_BUYER_DE for CP20, CP21, PUDE.
Maintain only the org levels in each derived role. Menu and general authorizations come from the master.
SAP Learning describes what is and isn't inherited: menus and general authorization values are passed down; user assignments are not; org levels are kept separate so each derived role can set its own. Changes to the master's menu take effect in all derived roles. Inheritance can be cancelled, but the role then becomes a normal role and the cancellation can't be undone (SAP Learning).
Why architects care: the number of roles grows with jobs times scopes. Calder has ten procure-to-pay master roles and two companies, so about twenty derived roles. A company with forty plants and no derived-role strategy ends up with hundreds of hand-maintained copies that drift apart.
Public Edition. A new role's Write, Read and Value Help start as "No Access" or "Unrestricted"; you set them to Restricted and maintain values. SAP's training says business catalogs are designed to be free of SoD conflicts, so they can be combined into custom roles, and that standard roles should not be used in production. Each catalog shows a price category (Self Service, Core, Advanced, Development Support, or individually priced add-ons), so adding a catalog to a role can change a user's license level. Custom role design starts from the Application Workplace List accelerator in SAP Activate.
Private Edition. SAP's training describes "hundreds of predelivered business role templates" and access flowing from user to business role to business catalogs to apps. A business catalog's content is a subset of a technical catalog. Customers own their business roles completely, including reviews after upgrades and access for implementation partners.
ECC.CP20's Augsburg roles are PFCG roles built around SAP GUI transactions. When CP20 moves into CP10's S/4HANA system, those roles don't carry over as they are: the target uses Fiori catalogs and S/4HANA's business partner for suppliers. Redesign from Calder's master roles and add _DE derived roles; use the old roles only as evidence of who did what.
#Segregation of duties: rules, functions and risks
SAP Access Control models SoD in layers (SAP Learning):
A function groups the actions (transactions, Fiori apps, OData services) and permissions (authorization objects, fields, values) needed for one business task.
An access risk combines functions. Its type is SoD (two or more conflicting functions), critical action (one action is risky on its own) or critical permission (one authorization is risky).
Risks belong to a business process such as procure-to-pay, have a level, an owner and a control objective, and are collected in a rule set.
When a risk can't be removed, a mitigating control from a catalog is assigned.
SAP delivers predefined rule set content for SAP S/4HANA, SAP ERP and other products as a template to review and adapt. SAP's example risk for procure-to-pay is a user who can maintain vendor master data and initiate payment runs.
The classic procure-to-pay conflicts an architect checks:
Pair
Why it's risky
Maintain suppliers + run payments
Create or redirect a supplier, then pay it
Change bank details + confirm the change
Defeats the four-eyes check on bank data
Create purchase orders + post invoices
Order and invoice something that wasn't delivered
Create purchase orders + approve them
Approval becomes a formality
Create purchase orders + release blocked invoices
Approve payment of an invoice against your own order
Create purchase orders + post goods receipts
Confirm delivery of your own order
Post invoices + run payments
Enter and pay a false invoice
Maintain prices + release price-blocked invoices
Raise the price, then clear the block it causes
SAP also supports a four-eyes check on sensitive supplier fields such as bank details: a change blocks the supplier for payment until a second authorized person confirms it, in classic transaction FK08 (SAP Community); an SAP knowledge base article on the confirmation status lists SAP S/4HANA among its products (SAP KBA 2851118). The check is only as good as the role design: if the same person holds both sides, the block clears itself.
Some fixes can't wait for a role change: a payment run fails at 6 a.m., or a migration load leaves wrong bank keys. Emergency access gives elevated rights for a short time, under control.
SAP Cloud Identity Access Governance's Privileged Access Management service works like this, per SAP Learning (source):
A PAM ID with elevated rights is created per target system, with a business role that defines what it can do.
An approver owns the PAM ID and approves requests from PAM users.
The user starts a session under the PAM ID and must pick a reason code.
Every activity is logged. Logs are synchronized to the service, and a reviewer checks them.
SAP's training lists session launch for SAP S/4HANA on-premise and SAP ERP (ABAP systems). SAP Access Control offers the same idea as Emergency Access Management with "firefighter" IDs (SAP Learning).
The architect's decisions: which emergency IDs exist and per which system, who approves, who reviews, how quickly a log must be reviewed, and which actions even an emergency ID can't do.
Roles are only as current as assignments. SAP's Private Edition training describes employees being replicated from an HR system of record, with SAP SuccessFactors Employee Central recommended. That makes HR the trigger: a hire creates a user, a transfer changes roles, a leaver locks the user. Hire-to-retire touchpoints found Calder's leaver 00100094 still active a week after leaving; the role check below finds the same person still holding a requisition role.
Movers are the quiet risk. A buyer who moves to accounts payable and keeps the buyer role now holds a conflict that nobody requested. Access reviews (SAP calls them periodic reviews in Access Control and access certification in the cloud service) catch what the provisioning flow missed.
An AI agent or an integration acts in SAP through an identity. SAP's architecture guidance, covered in Agent permissions and SAP authorizations, makes the effective permission the intersection of the user's and the agent's rights. That intersection is only safe if both sides were designed:
The user's side comes from the roles in this topic. An assistant on a buyer's desk sees only what the buyer's derived role reaches.
The agent's side is a technical role. Put it through the same risk analysis as people, and add one rule: an agent never holds an approving or paying task.
You will build and check Calder's procure-to-pay role matrix: twelve tasks, ten master roles, derived roles for the two companies, eleven users and agents, a small risk rule set, one mitigating control and one emergency ID. The script finds conflicts inside roles and in assignments, flags the agent and the leaver, checks the emergency log, writes the matrix as a file, and then shows the redesign.
flowchart LR
T[TASKS] --> S[role_matrix.py]
R[RULES] --> S
M[MASTERS +<br/>DERIVED] --> S
U[USERS] --> S
S --> F[Findings]
S --> MD[calder_p2p_role_matrix.md]
S -.->|--sample| P[calder_pack/<br/>org_units.json]
S -->|--after| A[Redesigned<br/>matrix]
In VS Code, create a new file, paste the script below, and save it as role_matrix.py inside the unit1b folder.
"""Design and check Calder Pumpworks' procure-to-pay role matrix.
Calder Pumpworks is a fictional company. Roles, users, personnel numbers,
the risk rules and the mitigating control are course labels, not SAP
content. SAP's own rule sets live in SAP Access Control and SAP Cloud
Identity Access Governance; this script shows the method on a small scale.
Run it from your course folder:
python unit1b/role_matrix.py check the design, write unit1b/calder_p2p_role_matrix.md
python unit1b/role_matrix.py --user 00100250 explain one user's access and conflicts
python unit1b/role_matrix.py --after check the redesigned roles instead
python unit1b/role_matrix.py --sample also check org values against calder_pack/org_units.json
The script needs no account, no key and no network.
"""
import argparse
import copy
import json
import sys
from datetime import date
from pathlib import Path
AS_OF = date(2026, 10, 28)
OUT = Path("unit1b") / "calder_p2p_role_matrix.md"
PACK = Path("unit1b") / "calder_pack" / "org_units.json"
# ------------------------------------------------------------------ tasks
# One task = one business job a role can do. "kind" drives the agent rule:
# agents may read and prepare, never approve or pay. "sap" lists example
# ABAP authorization objects or transactions behind the task, where SAP
# documentation names them; check the real list in your own system.
TASKS = {
"F01": ("Maintain supplier master data", "maintain", "BP"),
"F02": ("Change supplier bank details", "maintain", "BP"),
"F03": ("Confirm sensitive supplier changes", "approve", "FK08"),
"F04": ("Create purchase requisitions", "maintain", "M_BANF_EKO"),
"F05": ("Create and change purchase orders", "maintain", "M_BEST_EKO, M_BEST_WRK, M_BEST_BSA"),
"F06": ("Approve purchase orders", "approve", "-"),
"F07": ("Post goods receipts", "maintain", "-"),
"F08": ("Post supplier invoices", "maintain", "M_RECH_WRK"),
"F09": ("Release blocked invoices", "approve", "M_RECH_SPG"),
"F10": ("Run the payment program", "pay", "F_REGU_BUK"),
"F11": ("Maintain purchasing prices and info records", "maintain", "-"),
"F12": ("Display purchasing and payables data", "read", "-"),
}
# ------------------------------------------------------- course rule set
# Pairs of tasks one person must not hold in the same company code.
RULES = [
("P001", "F01", "F10", "High", "Maintain suppliers and run payments"),
("P002", "F05", "F08", "High", "Create purchase orders and post supplier invoices"),
("P003", "F05", "F07", "Medium", "Create purchase orders and post goods receipts"),
("P004", "F02", "F03", "Critical", "Change supplier bank details and confirm the change"),
("P005", "F05", "F06", "High", "Create and approve purchase orders"),
("P006", "F08", "F10", "High", "Post supplier invoices and run payments"),
("P007", "F11", "F09", "Medium", "Maintain prices and release price-blocked invoices"),
("P008", "F01", "F08", "High", "Maintain suppliers and post supplier invoices"),
("P009", "F05", "F09", "High", "Create purchase orders and release blocked invoices"),
]
# ------------------------------------------- master roles and derived roles
# A master role holds the tasks. A derived role copies them and adds the
# organizational scope: company codes, plants and purchasing organizations.
MASTERS = {
"ZM_P2P_REQUESTER": ["F04", "F12"],
"ZM_P2P_BUYER": ["F04", "F05", "F11", "F12"],
"ZM_P2P_PURCH_MANAGER": ["F05", "F06", "F12"],
"ZM_P2P_RECEIVING": ["F07", "F12"],
"ZM_P2P_AP_ACCOUNTANT": ["F08", "F12"],
"ZM_P2P_AP_SUPERVISOR": ["F09", "F10", "F12"],
"ZM_P2P_SUPPLIER_MASTER": ["F01", "F02", "F12"],
"ZM_P2P_MD_STEWARD": ["F03", "F12"],
"ZM_P2P_AUDITOR": ["F12"],
"ZT_AGENT_INVOICE_TRIAGE": ["F12", "F09"],
}
US = {"company_codes": ["CP10"], "plants": ["CP11"], "purch_orgs": ["PUUS"]}
DE = {"company_codes": ["CP20"], "plants": ["CP21"], "purch_orgs": ["PUDE"]}
ALL = {"company_codes": ["CP10", "CP20"], "plants": ["CP11", "CP21"], "purch_orgs": ["PUUS", "PUDE"]}
DERIVED = {}
for master in MASTERS:
stem = master.replace("ZM_", "ZD_").replace("ZT_", "ZD_")
if master in ("ZM_P2P_SUPPLIER_MASTER", "ZM_P2P_MD_STEWARD", "ZM_P2P_AUDITOR", "ZT_AGENT_INVOICE_TRIAGE"):
DERIVED[stem + "_ALL"] = (master, ALL)
else:
DERIVED[stem + "_US"] = (master, US)
DERIVED[stem + "_DE"] = (master, DE)
# --------------------------------------------------------- users and IDs
# Personnel numbers only, no names. "agent" marks a technical identity.
USERS = {
"00100211": ("Buyer, Rockford", "person", ["ZD_P2P_BUYER_US"]),
"00100153": ("Maintenance planner, Rockford", "person", ["ZD_P2P_REQUESTER_US"]),
"00100094": ("Maintenance technician, Rockford", "person", ["ZD_P2P_REQUESTER_US"]),
"00100253": ("Purchasing manager, Rockford", "person", ["ZD_P2P_PURCH_MANAGER_US"]),
"00100247": ("Receiving clerk, Rockford DC", "person", ["ZD_P2P_RECEIVING_US"]),
"00100241": ("AP accountant, shared service", "person", ["ZD_P2P_AP_ACCOUNTANT_US", "ZD_P2P_AP_ACCOUNTANT_DE"]),
"00100244": ("AP supervisor, shared service", "person", ["ZD_P2P_AP_SUPERVISOR_US", "ZD_P2P_AP_SUPERVISOR_DE"]),
"00100250": ("Master data specialist", "person", ["ZD_P2P_SUPPLIER_MASTER_ALL", "ZD_P2P_MD_STEWARD_ALL"]),
"00100259": ("Buyer, Rockford, covers Augsburg AP", "person", ["ZD_P2P_BUYER_US", "ZD_P2P_AP_ACCOUNTANT_DE"]),
"00200031": ("Buyer, Augsburg", "person", ["ZD_P2P_BUYER_DE", "ZD_P2P_AP_ACCOUNTANT_DE"]),
"AGT_INV_TRIAGE": ("Invoice triage agent", "agent", ["ZD_AGENT_INVOICE_TRIAGE_ALL"]),
}
LEFT = {"00100094": date(2026, 10, 16)} # from the HR feed (hire-to-retire topic)
MITIGATIONS = {("00200031", "P002"): "MC-01: CP20 finance manager reviews each month's POs and invoices by the same user"}
# ------------------------------------------------- emergency (firefighter) ID
# One privileged ID for CP10, its approver, the reviewer of its session logs,
# and the course rule that every log is reviewed within 5 days.
FIREFIGHTER = {"id": "FF_P2P_CP10", "approver": "00100270", "reviewer": "00100272", "review_days": 5}
FF_SESSIONS = [
("FF-0007", "00100244", date(2026, 10, 13), "Payment run failed: correct house bank data", "reviewed"),
("FF-0008", "00100250", date(2026, 10, 21), "Correct supplier bank key after a migration load", "open"),
]
def redesign(model: dict) -> dict:
"""The fixes a design review agreed, applied to a copy of the model."""
m = copy.deepcopy(model)
m["masters"]["ZM_P2P_PURCH_MANAGER"] = ["F06", "F12"] # approver no longer creates POs
m["masters"]["ZT_AGENT_INVOICE_TRIAGE"] = ["F12"] # the agent reads; a person releases
m["users"]["00100250"] = ("Master data specialist", "person", ["ZD_P2P_SUPPLIER_MASTER_ALL"])
m["users"]["00100262"] = ("Master data steward, finance", "person", ["ZD_P2P_MD_STEWARD_ALL"])
m["users"]["00100094"] = ("Maintenance technician, Rockford (locked)", "person", [])
return m
def build_model() -> dict:
return {"masters": MASTERS, "derived": DERIVED, "users": USERS}
def role_tasks(model: dict, role: str) -> list:
master, _ = model["derived"][role]
return model["masters"][master]
def access(model: dict, uid: str) -> dict:
"""Effective tasks per company code: {company_code: {task: [roles]}}."""
result = {}
for role in model["users"][uid][2]:
_, scope = model["derived"][role]
for cc in scope["company_codes"]:
for task in role_tasks(model, role):
result.setdefault(cc, {}).setdefault(task, []).append(role)
return result
def role_conflicts(model: dict) -> dict:
"""Rules broken inside one master role, before anyone is assigned."""
found = {}
for master, tasks in model["masters"].items():
for rid, a, b, level, text in RULES:
if a in tasks and b in tasks:
found.setdefault(master, []).append((rid, level, text))
return found
def user_findings(model: dict, uid: str) -> list:
name, kind, roles = model["users"][uid]
out = []
acc = access(model, uid)
for cc, tasks in sorted(acc.items()):
for rid, a, b, level, text in RULES:
if a in tasks and b in tasks:
mitigation = MITIGATIONS.get((uid, rid))
status = "mitigated" if mitigation else "open"
out.append((level, rid, f"{text} in {cc}", status, mitigation or ""))
# the same pair across different company codes is not a conflict here
codes = sorted(acc)
for rid, a, b, level, text in RULES:
holders = {cc for cc in codes if a in acc[cc]}, {cc for cc in codes if b in acc[cc]}
if holders[0] and holders[1] and not holders[0] & holders[1]:
out.append(("Info", rid, f"{text}: {a} in {','.join(sorted(holders[0]))}, "
f"{b} in {','.join(sorted(holders[1]))}, scopes don't overlap", "ok", ""))
if kind == "agent":
for cc, tasks in sorted(acc.items()):
for task in tasks:
if TASKS[task][1] in ("approve", "pay"):
out.append(("High", "AGENT", f"Agent holds {task} {TASKS[task][0]} in {cc}", "open", ""))
if uid in LEFT and roles:
out.append(("High", "LEAVER", f"Left on {LEFT[uid]}, user still has {len(roles)} role(s)", "open", ""))
return out
def ff_findings() -> list:
out = []
for sid, uid, day, reason, state in FF_SESSIONS:
age = (AS_OF - day).days
if state != "reviewed" and age > FIREFIGHTER["review_days"]:
out.append(("High", "FIREFIGHTER", f"{sid} by {uid} on {day}: log not reviewed after {age} days "
f"(reviewer {FIREFIGHTER['reviewer']})", "open", reason))
return out
ORDER = {"Critical": 0, "High": 1, "Medium": 2, "Info": 3}
def check(model: dict, label: str) -> list:
print(f"Calder procure-to-pay role check ({label}), as of {AS_OF}\n")
print("Role design (inside master roles):")
rc = role_conflicts(model)
if not rc:
print(" no rule broken inside any master role")
for master, items in rc.items():
for rid, level, text in items:
print(f" {level:<8} {rid} {master}: {text}")
print("\nUser assignments:")
rows = []
for uid in model["users"]:
for f in user_findings(model, uid):
rows.append((uid,) + f)
for f in ff_findings():
rows.append((FIREFIGHTER["id"],) + f)
rows.sort(key=lambda r: (ORDER[r[1]], r[2], r[0]))
for uid, level, rid, text, status, _ in rows:
print(f" {level:<8} {rid:<11} {uid:<14} {status:<9} {text}")
open_ = [r for r in rows if r[4] == "open"]
mitigated = [r for r in rows if r[4] == "mitigated"]
print(f"\n{len(rc)} master role(s) with a built-in conflict, {len(open_)} open finding(s), "
f"{len(mitigated)} mitigated.")
return rows
def write_matrix(model: dict, rows: list, path: Path) -> None:
ids = list(TASKS)
rc = role_conflicts(model)
lines = ["# Calder procure-to-pay role matrix (fictional)", "", f"As of {AS_OF}. Course rule set, not SAP's.", "",
"## Master roles and tasks", "", "| Master role | " + " | ".join(ids) + " | Built-in conflict |",
"| --- | " + " | ".join("---" for _ in ids) + " | --- |"]
for master, tasks in model["masters"].items():
marks = " | ".join("X" if t in tasks else "" for t in ids)
lines.append(f"| {master} | {marks} | {', '.join(r for r, _, _ in rc.get(master, [])) or '-'} |")
lines += ["", "Tasks: " + "; ".join(f"{t} {TASKS[t][0]}" for t in ids), "",
"## Derived roles and scope", "", "| Derived role | Master | Company codes | Plants | Purchasing orgs |",
"| --- | --- | --- | --- | --- |"]
for role, (master, scope) in model["derived"].items():
lines.append(f"| {role} | {master} | {', '.join(scope['company_codes'])} | {', '.join(scope['plants'])} | "
f"{', '.join(scope['purch_orgs'])} |")
lines += ["", "## Users and findings", "", "| User | Job | Roles | Findings |", "| --- | --- | --- | --- |"]
for uid, (job, _, roles) in model["users"].items():
mine = list(dict.fromkeys(f"{r[2]} {r[1]} ({r[4]})" for r in rows if r[0] == uid and r[1] != "Info"))
lines.append(f"| {uid} | {job} | {', '.join(roles) or '-'} | {'; '.join(mine) or '-'} |")
lines += ["", "## Risk rules (course rule set)", "", "| Risk | Task A | Task B | Level | Risk |",
"| --- | --- | --- | --- | --- |"]
lines += [f"| {r} | {a} | {b} | {lvl} | {t} |" for r, a, b, lvl, t in RULES]
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"Wrote {path}.")
def explain_user(model: dict, uid: str) -> None:
if uid not in model["users"]:
sys.exit(f"No user {uid!r}. Use one of {', '.join(model['users'])}.")
job, kind, roles = model["users"][uid]
print(f"{uid}: {job} ({kind})")
print(f" roles: {', '.join(roles) or 'none'}")
for cc, tasks in sorted(access(model, uid).items()):
print(f" {cc}:")
for task in sorted(tasks):
print(f" {task} {TASKS[task][0]:<44} via {', '.join(tasks[task])}")
findings = user_findings(model, uid) + ([f for f in ff_findings() if uid in f[2]])
if not findings:
print(" no findings")
for level, rid, text, status, note in findings:
print(f" {level:<8} {rid:<11} {status:<9} {text}")
if note:
print(f" {note}")
def check_pack(model: dict) -> None:
if not PACK.exists():
sys.exit(f"Can't find {PACK}. Run make_calder_pack.py from the Unit 1B setup topic first.")
org = json.loads(PACK.read_text(encoding="utf-8"))
known = {
"company_codes": {c["CompanyCode"] for c in org["CompanyCodes"]},
"plants": {p["Plant"] for p in org["Plants"]},
"purch_orgs": {p["PurchasingOrganization"] for p in org["PurchasingOrganizations"]},
}
plant_cc = {p["Plant"]: p["CompanyCode"] for p in org["Plants"]}
porg_cc = {p["PurchasingOrganization"]: p["CompanyCode"] for p in org["PurchasingOrganizations"]}
bad = 0
for role, (_, scope) in model["derived"].items():
for field, values in scope.items():
for v in values:
if v not in known[field]:
print(f"MISSING {role}: {field} {v} is not in the sample pack")
bad += 1
for v in scope["plants"]:
if plant_cc.get(v) not in scope["company_codes"]:
print(f"MISSING {role}: plant {v} belongs to a company code outside the role")
bad += 1
for v in scope["purch_orgs"]:
if porg_cc.get(v) not in scope["company_codes"]:
print(f"MISSING {role}: purchasing org {v} belongs to a company code outside the role")
bad += 1
status = "OK" if bad == 0 else "PROBLEM"
print(f"{status:<8} {len(model['derived'])} derived roles checked against {PACK}, {bad} problem(s)\n")
def main() -> None:
parser = argparse.ArgumentParser(description="Calder procure-to-pay role matrix.")
parser.add_argument("--user", metavar="ID", help="explain one user's access and conflicts")
parser.add_argument("--after", action="store_true", help="use the redesigned roles")
parser.add_argument("--sample", action="store_true", help="check org values against the Calder sample pack")
args = parser.parse_args()
if not Path("unit1b").is_dir():
sys.exit("Can't find the unit1b folder. Run this from your orchestrate-course folder.")
model = build_model()
if args.after:
model = redesign(model)
if args.sample:
check_pack(model)
if args.user:
explain_user(model, args.user)
return
rows = check(model, "after redesign" if args.after else "as designed")
path = OUT.with_name(OUT.stem + "_after.md") if args.after else OUT
write_matrix(model, rows, path)
if __name__ == "__main__":
main()
The Calder design sits in the blocks at the top: TASKS, RULES, MASTERS with DERIVED, USERS, LEFT, MITIGATIONS and the emergency ID. Everything in them is a course label for a fictional company. The functions below them only read those blocks, so you can change the design and re-run.
Calder procure-to-pay role check (as designed), as of 2026-10-28
Role design (inside master roles):
High P005 ZM_P2P_PURCH_MANAGER: Create and approve purchase orders
User assignments:
Critical P004 00100250 open Change supplier bank details and confirm the change in CP10
Critical P004 00100250 open Change supplier bank details and confirm the change in CP20
High AGENT AGT_INV_TRIAGE open Agent holds F09 Release blocked invoices in CP10
High AGENT AGT_INV_TRIAGE open Agent holds F09 Release blocked invoices in CP20
High FIREFIGHTER FF_P2P_CP10 open FF-0008 by 00100250 on 2026-10-21: log not reviewed after 7 days (reviewer 00100272)
High LEAVER 00100094 open Left on 2026-10-16, user still has 1 role(s)
High P002 00200031 mitigated Create purchase orders and post supplier invoices in CP20
High P005 00100253 open Create and approve purchase orders in CP10
Info P002 00100259 ok Create purchase orders and post supplier invoices: F05 in CP10, F08 in CP20, scopes don't overlap
1 master role(s) with a built-in conflict, 7 open finding(s), 1 mitigated.
Wrote unit1b/calder_p2p_role_matrix.md.
Open unit1b/calder_p2p_role_matrix.md and preview it with Ctrl+Shift+V (Cmd+Shift+V on a Mac). You should see the master roles against the twelve tasks, the derived roles with their scope, the users with their findings, and the rule set.
Read the findings by kind:
Role design:ZM_P2P_PURCH_MANAGER breaks P005 on its own. Every user who gets it inherits the conflict, so fix the role, not the users.
Assignment:00100250 holds two clean roles that together break the critical rule P004, once per company code.
Agent:AGT_INV_TRIAGE holds F09, an approving task.
Leaver:00100094 left on 2026-10-16 and still has a role.
Emergency: session FF-0008 hasn't been reviewed after seven days.
Mitigated: Augsburg's buyer breaks P002, accepted with control MC-01.
Info:00100259 creates POs in CP10 and posts invoices in CP20. The rule needs both in the same company code, so this is reported but not counted.
00100250: Master data specialist (person)
roles: ZD_P2P_SUPPLIER_MASTER_ALL, ZD_P2P_MD_STEWARD_ALL
CP10:
F01 Maintain supplier master data via ZD_P2P_SUPPLIER_MASTER_ALL
F02 Change supplier bank details via ZD_P2P_SUPPLIER_MASTER_ALL
F03 Confirm sensitive supplier changes via ZD_P2P_MD_STEWARD_ALL
F12 Display purchasing and payables data via ZD_P2P_SUPPLIER_MASTER_ALL, ZD_P2P_MD_STEWARD_ALL
CP20:
F01 Maintain supplier master data via ZD_P2P_SUPPLIER_MASTER_ALL
F02 Change supplier bank details via ZD_P2P_SUPPLIER_MASTER_ALL
F03 Confirm sensitive supplier changes via ZD_P2P_MD_STEWARD_ALL
F12 Display purchasing and payables data via ZD_P2P_SUPPLIER_MASTER_ALL, ZD_P2P_MD_STEWARD_ALL
Critical P004 open Change supplier bank details and confirm the change in CP10
Critical P004 open Change supplier bank details and confirm the change in CP20
High FIREFIGHTER open FF-0008 by 00100250 on 2026-10-21: log not reviewed after 7 days (reviewer 00100272)
Correct supplier bank key after a migration load
The same person changed a bank key under the emergency ID, and that log is the one still unreviewed. An auditor would read these lines together: this user can change bank data, confirm it, and used elevated access to change bank data last week.
00100259: Buyer, Rockford, covers Augsburg AP (person)
roles: ZD_P2P_BUYER_US, ZD_P2P_AP_ACCOUNTANT_DE
CP10:
F04 Create purchase requisitions via ZD_P2P_BUYER_US
F05 Create and change purchase orders via ZD_P2P_BUYER_US
F11 Maintain purchasing prices and info records via ZD_P2P_BUYER_US
F12 Display purchasing and payables data via ZD_P2P_BUYER_US
CP20:
F08 Post supplier invoices via ZD_P2P_AP_ACCOUNTANT_DE
F12 Display purchasing and payables data via ZD_P2P_AP_ACCOUNTANT_DE
Info P002 ok Create purchase orders and post supplier invoices: F05 in CP10, F08 in CP20, scopes don't overlap
AGT_INV_TRIAGE: Invoice triage agent (agent)
roles: ZD_AGENT_INVOICE_TRIAGE_ALL
CP10:
F09 Release blocked invoices via ZD_AGENT_INVOICE_TRIAGE_ALL
F12 Display purchasing and payables data via ZD_AGENT_INVOICE_TRIAGE_ALL
CP20:
F09 Release blocked invoices via ZD_AGENT_INVOICE_TRIAGE_ALL
F12 Display purchasing and payables data via ZD_AGENT_INVOICE_TRIAGE_ALL
High AGENT open Agent holds F09 Release blocked invoices in CP10
High AGENT open Agent holds F09 Release blocked invoices in CP20
The first shows org scope at work: the two tasks never meet in one company code. The second shows what to remove from the agent's role: it should prepare the release case, and a person in the AP supervisor role should release.
#Step 5: Apply the redesign and check the sample pack
Run the redesigned model and check its org values against the Calder sample pack:
Windows (PowerShell):
python unit1b\role_matrix.py --after --sample
macOS / Linux:
python unit1b/role_matrix.py --after --sample
You should see:
OK 16 derived roles checked against unit1b/calder_pack/org_units.json, 0 problem(s)
Calder procure-to-pay role check (after redesign), as of 2026-10-28
Role design (inside master roles):
no rule broken inside any master role
User assignments:
High FIREFIGHTER FF_P2P_CP10 open FF-0008 by 00100250 on 2026-10-21: log not reviewed after 7 days (reviewer 00100272)
High P002 00200031 mitigated Create purchase orders and post supplier invoices in CP20
Info P002 00100259 ok Create purchase orders and post supplier invoices: F05 in CP10, F08 in CP20, scopes don't overlap
0 master role(s) with a built-in conflict, 1 open finding(s), 1 mitigated.
Wrote unit1b/calder_p2p_role_matrix_after.md.
The redesign removes F05 from the purchasing manager role, removes F09 from the agent's role, gives bank confirmations to a separate steward (00100262) and locks the leaver. What remains is operational, not design: someone has to review FF-0008. A design can't do that for you.
The first line confirms that every company code, plant and purchasing organization in the derived roles exists in the sample pack, and that each plant and purchasing organization belongs to a company code inside the same role. If you run --sample before creating the pack, the script tells you to run make_calder_pack.py first.
Licensing notes. SAP Access Control and SAP Cloud Identity Access Governance are separate products from S/4HANA (SAP Access Control). A bridge setup means implementing, running and licensing both, per SAP's training. In Public Edition, the business catalogs in a role set its price category. Check your own contract.
Which governance tool? SAP's training gives a simple guide: the cloud service alone for out-of-the-box workflows across cloud and on-premise applications with limited configuration; SAP Access Control alone for mainly on-premise landscapes that need customizable workflows; the bridge when Access Control stays leading and the cloud service extends it to cloud applications (SAP Learning). Access Control doesn't connect to cloud applications directly.
Where it sits. Role design spans every system that holds Calder's procure-to-pay data:
flowchart LR
HR[SuccessFactors<br/>Employee Central] -->|joiner, mover, leaver| GOV[Access governance<br/>Access Control or IAG]
GOV -->|provision roles| S4[CP10 S/4HANA<br/>CP10 + CP20 after rollout]
GOV -->|until cutover| ECC[CP20 ECC]
GOV -->|cloud apps| CL[Cloud apps such as<br/>the supplier portal]
AG[Invoice triage agent] -->|own identity,<br/>read-only role| S4
FF[Emergency ID] -->|logged sessions| S4
Non-functional requirements that decide the design
Requirement
Typical target
If missed
SoD
No open critical risks; every accepted risk has a control and an owner
Audit findings, fraud exposure
Leaver latency
Access locked within a day of the HR leaving date
Former staff keep access
Emergency log review
Every session reviewed within a few working days
Elevated access becomes routine and unchecked
Role count
Derived roles per scope, not copies
Roles drift apart; each change becomes many
Upgrade review
Template changes reviewed each release
New apps missing, or new catalogs granted unseen
Integration and identity. People come from HR and are provisioned with business roles by the governance tool. Integrations use technical users with a role for exactly their interface. Agents use their own identity, and where they act for a person, the person's roles still bound the result (Agent permissions and SAP authorizations). During the rollout, Augsburg users hold ECC roles until cutover and _DE derived roles after; both sets go through the same rule set.
Context: After CP20 joins CP10's system, Augsburg's purchasing team has three people. Its buyer also posts supplier invoices, which breaks the rule "create purchase orders + post supplier invoices".
Options: (1) move Augsburg's invoice posting to the shared service team in Rockford; (2) accept the conflict with a mitigating control; (3) give the buyer emergency access for invoice posting.
Decision: (2) for the first year, with control MC-01: the CP20 finance manager reviews each month's purchase orders and invoices by the same user. Revisit (1) once the shared service covers German invoices.
Consequences: The risk stays visible in every analysis as mitigated, not hidden. The control has an owner and evidence each month. Option (3) is rejected: emergency access is for incidents, not for a daily task.
What a design review will ask
How many master roles carry a conflict on their own, and what's the plan to split them?
Which risks are accepted with controls, and who performs and evidences each one?
How long does a leaver keep access, measured, not assumed?
Who reviews emergency logs, and how many are overdue today?
Which agents and integrations hold roles, and have they been through the same risk analysis?
Security and SAP authorizations. Restrict who can change roles and assignments in production; transport roles from development like any change. Keep the governance tool's own administrator roles out of business users' hands.
Evaluation. Track open risks by level, mitigated risks with overdue control evidence, leaver latency and overdue emergency reviews each month. Rising numbers mean the design is eroding.
Cost. In Public Edition, review the price category of the catalogs in each role before go-live. Governance tools add license and running cost; a bridge setup adds both twice.
Operations. Every role has an owner who approves assignments and answers review campaigns. Review SAP's template changes after each release.
Clean core. Custom authorization objects and checks are custom code. Prefer SAP's objects and restrictions; add custom checks only for custom apps, and test them like any extension.
Extend Calder's role design to accounts payable for intercompany invoices and write the role memo for the steering committee.
Run python unit1b/role_matrix.py --after and confirm the summary says 0 master role(s) with a built-in conflict, 1 open finding(s), 1 mitigated.
Open unit1b/role_matrix.py. Add a task F13 "Post intercompany invoices" with kind maintain, and a master role ZM_P2P_IC_ACCOUNTANT holding F13 and F12.
Add a rule P010: F05 and F13, level High, "Create purchase orders and post intercompany invoices".
Run python unit1b/role_matrix.py --user 00100241 once to check nothing broke. The DERIVED loop creates ZD_P2P_IC_ACCOUNTANT_US and ZD_P2P_IC_ACCOUNTANT_DE for the new master role automatically.
Assign ZD_P2P_IC_ACCOUNTANT_DE to 00200031 and run --after again. Note the new finding, and decide: mitigate (add an entry to MITIGATIONS) or move the task to another person. Re-run until the result matches your decision.
Run --after --sample and confirm 0 problem(s).
Open unit1b/calder_fit_gap.md from Fit-to-standard and extension choices (create a new file if you skipped it). Under a new heading Access design, write five lines: the role R03's distributor app should use and why it must be scoped to the distributor's own customer number; the one SoD rule you'd add for R05's purchase order approval; who approves emergency access at Rockford; how leavers reach SAP; and the one task the invoice triage agent must never hold.
Commit all changed files with Git.
Done when:--after shows your F13 decision either as mitigated with a named control or as resolved, --sample reports 0 problem(s), and the access design section has all five lines. Keep both files: the unit capstone, One exception, traced across the enterprise, reuses this role design in its solution package.
Pick one answer for each question. The explanation appears after you choose.
1Calder needs the same buyer access for Rockford and Augsburg. Which PFCG design fits best?
Answer: D. A derived role inherits the menu and authorizations from its master and keeps its own org levels, so tasks are maintained once and scope per company. Hand-maintained copies drift apart, and one role with every org value removes the scope altogether.
2The script reports P005 inside ZM_P2P_PURCH_MANAGER. Why fix the role rather than the users?
Answer: A. A conflict built into a role reaches everyone assigned to it, including future users. Splitting the role (the manager approves, buyers create) removes the risk once; fixing users one by one leaves the role to recreate it.
3In SAP S/4HANA Cloud Public Edition, what does an architect use to limit a business role to company code CP10?
Answer: B. In Public Edition, business roles are built from catalogs in Maintain Business Roles, and Write, Read and Value Help restrictions carry the org values. PFCG and derived roles are the Private Edition, on-premise and ECC technique.
4User 00100259 creates POs in CP10 and posts supplier invoices in CP20. Why does the script report this as information only?
Answer: B. The course rule counts a conflict only where both tasks reach the same company code, and this user's scopes don't overlap. Real rule sets can define cross-company risks, which is why the line is still reported.
5The invoice triage agent's role includes F09, release blocked invoices. What should you do?
Answer: C. Releasing a blocked invoice is an approval, and the design rule is that agents never hold approving or paying tasks. The intersection with the user's rights doesn't help when the agent runs on its own identity, and emergency IDs are for incidents.
6A Public Edition role adds three business catalogs from SAP's template. Besides access, what else should the architect check?
Answer: C. SAP's training notes that catalogs carry price categories and that licensing is per user per month, so a role built from a broad template can raise cost as well as access. Templates are a starting point, not productive roles.
7An emergency session changed a supplier bank key a week ago, and its log is unreviewed. What does this show?
Answer: B. Emergency access depends on logging and a timely review by a named reviewer; an unreviewed log means the control isn't working. A shared user would remove the reason code, approval and review that make emergency access acceptable.
8Calder's CP20 roles in ECC are transaction-based. What is the sound approach when CP20 moves into CP10's S/4HANA system?
Answer: A. ECC roles are built around SAP GUI transactions and vendor master records, while the target uses Fiori catalogs and the business partner. Deriving CP20 roles from the same master roles keeps one design for both companies.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
Creating and Customizing Business Roles (SAP Learning, SAP S/4HANA Cloud Public Edition)— Maintain Business Roles and Maintain Business Users apps; business catalogs; Write, Read and Value Help restrictions; catalogs designed to be free of SoD conflicts; standard roles not for production; price category per catalog; Application Workplace List accelerator
Developing Business Roles Using Templates (SAP Learning)— Create From Template; templates only for testing, not productive roles; instance-based restrictions; Business Role Templates app filter 'Different from Business Roles' after a release
Building a Rule Set (SAP Learning, SAP Access Control)— functions group actions and permissions; access risk types SoD, critical action, critical permission; example risk: maintain vendor master data and initiate payment runs; mitigating controls; predefined rule set content for SAP S/4HANA and SAP ERP is a template
SAP Access Control (sap.com product page)— access risk analysis for SoD and critical access; role-based access control; periodic reviews; emergency access with 'firefighter' login IDs