Orchestrate

Report an issue

Something wrong, out of date or unclear? Tell us. Corrections are prioritized over new topics. You don't need to sign in.

0 / 2000

Don't include personal or confidential company details. Reports are reviewed with AI assistance.

Roles and authorization design across modules

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.

Updated Nov 6, 2026Foundational 9 minDeep 38 min
Foundational layer · 9 min read

The 60-second version

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:

  1. What a job needs. A buyer creates purchase orders. An accounts payable clerk posts invoices. These bundles of access are roles.
  2. 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.
  3. 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.

Why it matters to the business

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.

How SAP does it

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) Inside the SAP system (Public Edition, PFCG)
Start from SAP's content Predelivered business role templates to copy, never to use as they are Inside the system (Private Edition)
Govern access 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 Separate products (SAP Learning)

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 on one page

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.

Questions to ask

  1. Who owns the role design: the security team, the process owners, or both? Who signs off a new role?
  2. Which access risk rule set do we use, and who decided which risks are critical for us?
  3. Which conflicts do we accept with mitigating controls, and who performs each control?
  4. How is emergency access requested, approved, logged and reviewed, and how quickly is each log reviewed?
  5. How do joiners, movers and leavers reach SAP from HR, and how long does a leaver keep access?
  6. Which identity does each AI agent or integration use, and has it been through the same risk analysis as people?
  7. In Public Edition: which business catalogs does each role need, and what price category do they carry?

Common misconceptions

  • "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.

Key terms

All course terms, A to Z, in the glossary

  • 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.

Check yourself

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.
Deep layer · 38 min read

Mental model

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.

How it works

From job to authorization check

flowchart LR
  J[Job: buyer] --> BR[Business role]
  BR --> C1[Catalog or<br/>single role: POs]
  BR --> C2[Catalog or<br/>single role: requisitions]
  C1 --> A[Authorizations<br/>object + field values]
  O[Org scope<br/>CP10 / CP11 / PUUS] --> A
  U[User] -->|assigned| BR
  A -->|AUTHORITY-CHECK| X[SAP checks<br/>at runtime]

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.

Business roles and technical roles

Most programs separate two kinds of role:

  • 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.

Org structure as the scope

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:

  1. Build a master role with the menu and authorizations for a job, for example ZM_P2P_BUYER.
  2. 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.
  3. 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.

How each edition builds roles

SAP S/4HANA Cloud Public Edition SAP S/4HANA Cloud Private Edition and on-premise SAP ECC
Where roles are built App Maintain Business Roles Transaction PFCG, with SAP Fiori launchpad catalogs Transaction PFCG
Building blocks Business catalogs from SAP Single roles, composite roles, derived roles; business and technical catalogs Single, composite and derived roles, mostly transaction-based
Starting point Business role templates (Create From Template) Predelivered templates; SAP's security roles start with SAP_BR_; copy, don't edit Customer roles, often grown over years
Org scope Restrictions per restriction type: Write, Read, Value Help Org levels in derived roles Org levels in derived roles
After an upgrade Business Role Templates app shows templates Different from Business Roles Customer reviews roles after upgrades Customer reviews roles after upgrades

Sources: Public Edition roles, templates, Private Edition, PFCG.

Points worth knowing in each:

  • 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.

Emergency access

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):

  1. A PAM ID with elevated rights is created per target system, with a business role that defines what it can do.
  2. An approver owns the PAM ID and approves requests from PAM users.
  3. The user starts a session under the PAM ID and must pick a reason code.
  4. 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.

Joiners, movers and leavers

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.

Agents and integrations inherit the design

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.
  • Grounding follows the same scope: an index built with a broad technical user leaks across company codes (Grounding on SAP data without breaking authorizations).

Build it yourself

Before you start: finish Set up your computer for this course and Set up for Unit 1B.

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]

What you need

  • The course folder orchestrate-course with its .venv, and the Calder sample pack from Set up for Unit 1B (only for --sample).
  • No account, no key and no network. The script uses only Python's built-in modules, so requirements.txt doesn't change.
  • Cost: free. Time: about 30 minutes.

Step 1: Open your course folder and turn on the virtual environment

  1. Open VS Code, then File > Open Folder and choose orchestrate-course.
  2. Open a terminal with Terminal > New Terminal.
  3. Turn on the virtual environment:
    • Windows (PowerShell):

      .venv\Scripts\Activate.ps1
    • macOS / Linux:

      source .venv/bin/activate

You should see (.venv) at the start of the prompt. The unit1b folder should already exist from the setup topic.

Step 2: Create the script

  1. 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.

Step 3: Check the design

  1. Run the script with no options:

    • Windows (PowerShell):

      python unit1b\role_matrix.py
    • macOS / Linux:

      python unit1b/role_matrix.py
  2. You should see:

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.
  1. 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.

Step 4: Explain one user

  1. Ask why 00100250 is critical:

    • Windows (PowerShell):

      python unit1b\role_matrix.py --user 00100250
    • macOS / Linux:

      python unit1b/role_matrix.py --user 00100250
  2. You should see:

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.

  1. Now look at the cross-company case and the agent:
python unit1b/role_matrix.py --user 00100259
python unit1b/role_matrix.py --user AGT_INV_TRIAGE
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

  1. 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
  2. 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.

Step 6: Save your work in Git

git add unit1b/role_matrix.py unit1b/calder_p2p_role_matrix.md unit1b/calder_p2p_role_matrix_after.md
git commit -m "Add Calder procure-to-pay role matrix"

What each part of the code does

Part What it does
TASKS Twelve procure-to-pay tasks, each with a kind (maintain, approve, pay, read) and example SAP objects
RULES The course rule set: pairs of tasks one person must not hold in the same company code
MASTERS, DERIVED Master roles with tasks; derived roles that copy them and add company codes, plants and purchasing organizations
USERS, LEFT, MITIGATIONS Who holds which roles, who has left according to HR, and the accepted risk with its control
FIREFIGHTER, FF_SESSIONS The emergency ID, its approver and reviewer, and the session log
access() A user's effective tasks per company code, with the role that grants each
role_conflicts() Rules broken inside one master role
user_findings() Conflicts per company code, cross-company info lines, agent and leaver checks
ff_findings() Emergency sessions not reviewed in time
redesign() The agreed fixes, applied to a copy so the original stays as evidence
write_matrix(), explain_user(), check_pack() The matrix file, one user's view, and the sample pack check

If something goes wrong

What you see What it means What to do
python is not recognized, or command not found The terminal can't find Python Repeat Step 1 of the course setup; on macOS and Linux use python3 until .venv is active
No module named ... You pasted only part of the script The script uses only built-in modules; paste it again in full
Can't find the unit1b folder You're not in the course folder Open orchestrate-course in VS Code and run from its terminal
No user 'XX' The ID isn't in USERS Use an ID from the list the message prints
Can't find unit1b/calder_pack/org_units.json The sample pack doesn't exist yet Run python unit1b/make_calder_pack.py from Set up for Unit 1B, or drop --sample
KeyError with a role name A user names a derived role that DERIVED doesn't have Check the spelling; derived names end in _US, _DE or _ALL
Your output differs from this page You changed the design blocks Fine if you meant to; copy the script again to compare

This script needs no API key and no network, so key and proxy problems don't apply here.

The SAP way

On a real program, each part of the script has an SAP counterpart. As of October 2026:

Job SAP tool Edition Note
Build roles Maintain Business Roles; Maintain Business Users Public Edition Catalogs and restrictions (SAP Learning)
Build roles Transaction PFCG: single, composite, derived roles Private Edition, on-premise, ECC Derived roles carry org levels (SAP Learning)
Start from SAP content Business role templates; SAP_BR_ security roles Public and Private Edition Copy; templates are for testing, not productive roles
Find the roles an app needs SAP Fiori Apps Reference Library All S/4HANA Lists catalogs and roles per app (SAP Learning)
Analyze access risks Access Risk Analysis (SAP Access Control); Access Analysis (SAP Cloud Identity Access Governance) Access Control on-premise; IAG for cloud and on-premise Predefined rule set content as a template
Manage roles centrally Business Role Management; Role Design Same split IAG's Role Design uses machine learning for bottom-up role design (SAP Learning)
Request access Access Request Management; Access Request Same split Provisioning with an audit log
Review access Periodic Review; Access Certification Same split Campaigns and automated reviews
Emergency access Emergency Access Management; Privileged Access Management Same split Firefighter or PAM IDs, reason codes, log review

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.

Build vs. SAP

Need Build your own Use SAP Choose
Draft a role matrix in a workshop A script or spreadsheet, as here Role Design or Business Role Management Your own for the design phase; SAP's tool once roles exist in systems
Analyze SoD on real users Export and compare Access Risk Analysis or Access Analysis SAP; it reads actual authorizations, not labels someone typed
Rule set Write from scratch SAP's predefined content, adapted SAP's content as the base, reviewed with process owners
Emergency access Shared high-privilege user Firefighter or PAM IDs with logs SAP; a shared user has no reason, no approval and no reviewer
Agent access rules A check like AGENT in the script Same risk analysis, agent identities included Both: put agents in the SAP tool, and test the "never approve" rule in your own code too

Architecture view

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.

The decision to record (in the ADR form from Writing a solution design doc):

  • 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

  1. How many master roles carry a conflict on their own, and what's the plan to split them?
  2. Which risks are accepted with controls, and who performs and evidences each one?
  3. How long does a leaver keep access, measured, not assumed?
  4. Who reviews emergency logs, and how many are overdue today?
  5. Which agents and integrations hold roles, and have they been through the same risk analysis?

Production concerns

  • 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.

Pitfalls

  • Copying ECC roles into S/4HANA. Transaction-based roles don't map to Fiori catalogs and the business partner. Redesign from jobs.
  • One role per person. Personal roles look precise and make analysis and reviews impossible.
  • Conflicts inside roles. A role that breaks a rule on its own gives the conflict to everyone assigned to it.
  • Using standard templates in production. They grant more than a job needs and, in Public Edition, can raise license cost.
  • Mitigations without evidence. A control nobody performs is a risk with a label.
  • Firefighter as a habit. If the same task needs emergency access every week, it needs a role.
  • Forgetting movers and agents. Both acquire conflicts that nobody requested.

Exercise

Extend Calder's role design to accounts payable for intercompany invoices and write the role memo for the steering committee.

  1. 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.
  2. 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.
  3. Add a rule P010: F05 and F13, level High, "Create purchase orders and post intercompany invoices".
  4. 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.
  5. 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.
  6. Run --after --sample and confirm 0 problem(s).
  7. 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.
  8. 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.

Check yourself

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.

Sources

Sign in to track your progress

We'll email you a one-time sign-in link. No password needed.

or

Tell us a little about you

Optional, every field. It helps us pitch answers to your questions at the right level and decide which topics to write next. It is never shown publicly, and you can change or clear it anytime from the account menu.

SAP areas you work in