Two days ago I released PAMlab — a sandbox with mock APIs for building PAM integration scripts. Three APIs, some PowerShell templates, a basic web UI. Cool, but kind of bare bones.
Today it’s a completely different beast. And the story of how it got there is worth telling, because it touches on something I think most tooling projects get wrong: the gap between “technically works” and “someone would actually use this.”
The Problem With V1
PAMlab v1 had working mock APIs and a code editor. You could write PowerShell, hit Run, and see results. Mission accomplished, right?
Not really. Here’s what happened when I sat down to actually use it:
- I loaded the Onboarding template. It created a user called “Sarah Connor” in the AD mock.
- I tweaked some params. Ran it again. 409 Conflict — user already exists.
- I opened the Emergency Revoke template. It tried to block a Fudo user by ID. The ID was
FROM_STEP_5. 422 — “Valid user_id required.” - I clicked the Debug button. Nothing happened. Export? Nothing. Save? You guessed it.
The buttons were decorative. The templates had placeholder values that couldn’t resolve. And running the same workflow twice was a guaranteed crash because the first run’s test data polluted the second run.
This is the kind of thing that separates a proof of concept from a tool. And I had a proof of concept.
The Fix: Everything
Instead of patching individual bugs, I decided to rebuild the entire user experience in one session. Here’s what that looked like.
Cross-Step References (The Non-Obvious Problem)
This was the most interesting engineering challenge. In a real provisioning workflow, steps depend on each other:
Step 1: Create Matrix42 Ticket → returns ticket_id
Step 5: Create Fudo PAM User → returns user_id
Step 6: Add User to Fudo Group → needs Step 5's user_id
Step 8: Close Ticket → needs Step 1's ticket_id
In production PowerShell, you’d write $step5Result.id. But in the PAMlab runner, the script is parsed into API calls and executed sequentially. There’s no PowerShell runtime — just a JavaScript loop calling fetch().
The solution was a step resolver that sits between the parser and the executor:
// After each step, extract IDs from the response
stepResults[i + 1] = extractIds(res.data);
// Before executing the next step, resolve references
const resolvedCall = resolveStepReferences(calls[i], stepResults);
The resolver understands different response formats: Fudo returns { id: "..." }, Matrix42 returns { ID: "..." }, ServiceNow wraps everything in { result: { sys_id: "..." } }, and Jira uses { key: "ITSM-12" }. One function, five different ID extraction patterns.
It’s the kind of plumbing that nobody notices when it works, but breaks everything when it doesn’t.
The Test Sandbox
The second big problem: running templates more than once. The Onboarding template creates “Sarah Connor” — run it twice and the AD mock returns 409 because Sarah already exists.
The fix was a test runner that generates random identities:
Template says: s.connor / Sarah Connor
Test run uses: test-a3f8b / Test User A3F8B
It replaces every known demo username in the generated script with a random identifier, runs it, and then offers a “Cleanup” button that deletes everything the test created. AD users, Fudo users, group memberships, tickets — all tracked during execution and reversible after.
Combined with a “Reset Mock Data” button that reloads all seed data across all 7 APIs, you can now iterate on workflows indefinitely without accumulating garbage state.
Stable UUIDs (The Subtle Fix)
Here’s a fun one. The Fudo mock used uuidv4() to generate IDs for all seed data — users, groups, safes, servers. Every time the API restarted, every ID changed.
This meant the workflow templates couldn’t reference Fudo groups or safes by ID, because the IDs were different every time. I replaced all seed IDs with deterministic UUIDs:
// Before: random every restart
{ id: uuidv4(), name: 'RDP-Server-Admins' }
// After: stable forever
{ id: '70000000-0000-0000-0000-000000000001', name: 'RDP-Server-Admins' }
Simple change, huge impact. Now templates can hard-reference 70000000-0000-0000-0000-000000000001 and it’ll always be the RDP-Server-Admins group.
Five Real Templates
With stable IDs and cross-step resolution, I could finally build templates that actually work:
| Template | Steps | Systems | What Happens |
|---|---|---|---|
| Employee Onboarding | 8 | Matrix42 → AD → Fudo | Ticket → User → Groups → PAM → Policy → Close |
| Temp Server Access | 4 | Matrix42 → AD → Fudo | Ticket → Group → Time-Limited Policy → Close |
| Offboarding | 5 | AD → Fudo → ServiceNow | Remove → Block → Disable → Incident |
| Emergency Revoke | 5 | Fudo → AD → ServiceNow | 🚨 Block → Disable → Remove → Security Incident |
| Project Access | 4 | AD → Fudo → Jira | Group → Web Policy → DB Policy → Jira Issue |
All five templates verified end-to-end: 26 API calls, 26 successful responses. You can load any of them, hit Run, and watch the entire flow execute with live status updates in the inline results panel.
The Access Policy Model
While building templates, I realized the Fudo mock was missing a critical concept: Access Policies. In real Fudo, the access chain is:
AD Group (GRP-RDP-Admins)
→ linked via ad_group_dn to
Fudo Group (RDP-Server-Admins)
→ Access Policy binds to
Fudo Safe (IT-Administration)
→ contains
Servers (DC01, DB-PROD, FileServer01) + Accounts
→ accessed via
Listener (RDP / SSH)
A user in the group can access all servers in the safe through the listener. I implemented the full model:
POST /api/v2/access-policies— create policies with group, safe, listener, time restrictions, and approval requirementsGET /api/v2/access-policies/check/:user_id/:safe_id— check if a user has access to a specific safe- Seed data with three pre-configured policies
This is the kind of domain modeling that makes mock APIs actually useful for testing — not just “does the API respond” but “does the business logic flow make sense.”
Fudo Mock Expansion
While at it, I expanded the infrastructure:
- 6 servers (was 3): DC01, DB-PROD, APP-ERP, FileServer01, Web-PROD, Citrix01
- 4 safes (was 2): IT-Administration, Application-Access, File-Server-Access, Web-Server-Deployment
- 6 accounts: One per server with realistic names
- All servers in the Production pool
Flow Visualization
The workflow builder now shows your steps as a visual flow diagram:
[Matrix42 🎫]──→──[AD 🏢]──→──[AD 🏢]──→──[Fudo 🔐]──→──[Fudo 🔐]──→──[Matrix42 🎫]
Create Ticket Create User Add Group Create User Create Policy Close Ticket
✅ ✅ ✅ ⏳ ⏸ ⏸
Each node is colored by system (blue=AD, green=Fudo, purple=Matrix42, orange=ServiceNow), shows the step label, and has a live status indicator. When you run the workflow, nodes light up one by one.
Production Export
Mock scripts are useless if you can’t deploy them. PAMlab Studio now has a production config system where you configure real system URLs and auth methods:
| System | Auth Method |
|---|---|
| Fudo PAM | API Token |
| Matrix42 | API Key |
| Active Directory | LDAP Bind |
| ServiceNow | OAuth2 (Client ID/Secret) |
| Jira SM | API Token |
| BMC Remedy | Basic Auth |
Hit “🏭 Export for Production” and the script is regenerated with proper auth blocks. Same workflow logic, different endpoints and credentials. The config can be exported/imported as JSON (with passwords masked).
The Welcome Screen
New users no longer land on a dashboard with green dots and no context. There’s now a welcome screen with:
- Feature overview (Build → Test → Ship)
- “Start with a Demo” button that loads the Onboarding template
- “Build from Scratch” option
It’s skippable and only shows once (localStorage flag). Small thing, but it’s the difference between “what am I looking at” and “oh, I should click this.”
Everything Else
- Run History: All executions saved in localStorage, viewable as a table with expandable details
- Live Dashboard Stats: Users, servers, groups, sessions, pending requests — fetched from Fudo API in real time
- Quick Actions: One-click cards for common demos (Onboarding, Emergency Revoke)
- Keyboard Shortcuts: Ctrl+Enter (Run), Ctrl+S (Save), Ctrl+E (Export)
- Settings Redesign: Three tabs — Mock APIs, Production Config, Preferences
- Mock Data Reset: POST /reset on all APIs to reload seed data
Interactive Demo
Want to see it in action without cloning the repo? Here’s an interactive walkthrough:
Use arrow keys or click the navigation dots. Open fullscreen ↗
The Numbers
Over the course of one (long) day:
- 1,400+ lines of new TypeScript/React code
- 19 files changed across frontend and all 7 mock APIs
- 6 new components: Welcome, FlowDiagram, RunHistory, testRunner, stepResolver, productionConfig
- 5 workflow templates verified end-to-end (26 API calls each)
- 0 production systems were harmed in the making of this update
What I Learned
Building developer tools is different from building user-facing products. The audience is smaller but way more demanding. They’ll find every edge case in the first five minutes because that’s literally what they do for a living.
The biggest lesson: demo data matters more than features. The test sandbox and mock data reset took maybe 20% of the development time but solved 80% of the usability problems. Nobody cares about your flow visualization if they can’t run the same template twice.
The second lesson: cross-system reference resolution is the hard part of workflow automation. Not the individual API calls — those are straightforward. It’s the $step5Result.id problem. Every orchestration tool eventually has to solve this, and most do it poorly.
Try It
git clone https://github.com/BenediktSchackenberg/PAMlab.git
cd PAMlab
docker-compose up
# Open http://localhost:3000
Load a template. Hit Run. Watch 8 API calls cascade across three different systems. Then hit “🧪 Test Run” and do it again with random data. Then export the script, change the URLs, and run it against your real environment.
That’s the whole point: build once, test safely, deploy anywhere.
GitHub — Apache 2.0, contributions welcome.