Stéphane Tailland a81bf74b28 feat(mcp): authorise tool-call parameters, not just the tool name
The ACL answers 'may this identity call create-product'. Nothing answered 'may
it call it with these arguments' — write authorises writing, not this
particular record. And the read cap counts calls, so one call asking for a
hundred thousand rows cost the same as one asking for ten.

The OPA plugin sends the decoded JSON-RPC body, so the decision point sees the
tool name and its arguments. The policy lives once in k8s-platform/opa and every
domain asks the same server.

Enforced at the gateway rather than in an agent, because an agent that has been
talked into something is the case this exists for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 18:09:22 +02:00

workforce-api

Kong routing/policy layer for the workforce slice of classic-models-api (see /Users/stephanetailland/git/classic-models-api), split out per the DDD decomposition in ../kong-konnect-deployment-strategy.md. The backend stays a single monolith — only Kong does the domain split. Structure mirrors ../sales-api/../payments-api — single MCP route from the start, not ../catalog-api's original two-route pattern (see ../STATUS.md section 18).

Two resources, both with a REST surface and MCP tools:

  • Office (single key officecode) — full CRUD + an employees sub-resource.
  • Employee (single key employeenumber) — full CRUD + reports (direct reports, self-referential FK) and customers (assigned sales rep's customers) sub-resources.
Route Method Backend path
/workforce/v1/offices/ GET, POST /classic-models/api/v1/offices/
/workforce/v1/offices/{officecode}/ GET, PUT, PATCH, DELETE /classic-models/api/v1/offices/{officecode}/
/workforce/v1/offices/{officecode}/employees/ GET /classic-models/api/v1/offices/{officecode}/employees/
/workforce/v1/employees/ GET, POST /classic-models/api/v1/employees/
/workforce/v1/employees/{employeenumber}/ GET, PUT, PATCH, DELETE /classic-models/api/v1/employees/{employeenumber}/
/workforce/v1/employees/{employeenumber}/reports/ GET /classic-models/api/v1/employees/{employeenumber}/reports/
/workforce/v1/employees/{employeenumber}/customers/ GET /classic-models/api/v1/employees/{employeenumber}/customers/
/workforce-mcp GET, POST, DELETE MCP streamable-http

Auth & MCP tool ACLs — the one domain where customer gets zero access

Both viewsets use ReferenceDataPermission (authentication/permissions.py): read_only/product_manager/support get read access, writes are admin-only, and — unlike every other domain built this session — customer is not in the allow-list at all, not even for reads. Verified directly against the permission class, not assumed: this is genuinely internal staff/organizational data, and the backend enforces that explicitly. All 7 read tools (list-offices, get-office, get-office-employees, list-employees, get-employee, get-employee-reports, get-employee-customers) have ACL [admin, read_only, product_manager, support] — no customer. Both write tools (create-office, create-employee) are admin only.

openid-connect on both surfaces (workforce-api REST audience, mcp-workforce MCP audience), RFC 8693 exchange chain MCP → workforce-api → classic-models-api. IdP: local Keycloak.

Structure

Same dev/ + kong/rest/ + kong/mcp/ layout as sales-api. Deploy key and broker secret reused/mirrored from catalog-api and the live broker client, same pattern as every domain since payments-api.

S
Description
No description provided
Readme 69 KiB