Java Order Isolation
Build a small application facade that permits a buyer to read and edit only their Orders in their tenant. Start with the Java Order tutorial for Q/E and audited writes, then use this separate model revision 1.1.0. The v1.0 query recipes demonstrate business filters; this chapter explains who is allowed to choose those filters.
The isolation receipt records a published runtime PASS for Java. The later JDBC lifecycle/soak workspace fix remains WORKSPACE PASS — RELEASE PENDING for that separate scope. Generator graph-save/relationship Assist repairs remain pending deployment. Earlier School, core Order, and later lifecycle/runtime qualifications keep their original scopes; this tutorial does not reclassify them.
1. Upgrade the ownership model
Use the retained v1.1 model,
fixture,
local discovery manifest
and evaluation together.
The model adds commerce_platform.tenant_key; an Order belongs to that root through
customer_order.commerce_platform, and an item belongs through order_item.customer_order.
The model's TENANT-A sample value is not an authenticated membership or authorization rule.
Shared OrderStatus constants remain a global catalog.
| Revision | Reader task | Database/example boundary |
|---|---|---|
| v1.0 | Read/edit, export, retained IDs and summaries. | Keep the original examples and receipts unchanged. A buyer predicate alone does not distinguish tenants. |
| v1.1 | Authorized tenant-root + buyer facade. | Use its regenerated application/library and a fresh isolation database, not the v1.0 nine-/ten-order query fixture. |
| v1.2 | Inventory reservation. | Separate stock extension; it is not required for this chapter. |
The recorded local evaluation has 0 errors, one accepted nullable-note warning, two optional suggestions and no skipped rules. Generation and scoped Assist were performed locally with explicit runtime pins; their retained repairs remain qualified. Keep generated domain source unchanged. This walkthrough reuses that output and does not submit a model or regenerate it.
This is a fresh-database walkthrough, not a production migration recipe. A live upgrade must separately decide how existing Orders map to tenant roots, how tenant keys are provisioned uniquely, and how authenticated membership is maintained. Adding the sample field or bootstrapping a schema does not resolve those data/membership decisions. The facade's exact-one root guard rejects zero or multiple matches; do not replace it with “take the first”. The source contains that guard; the receipt uses the normal two-root fixture, not a duplicate-root migration campaign.
2. Establish the trusted context and session seam
The application owns the provider, native UserContext and facade. Its fixture
registry maps a server-selected session key to (actor, tenant, buyer):
| Fixture key | Actor | Tenant | Buyer | Visible Orders / USD cents |
|---|---|---|---|---|
a | ACTOR-A | TENANT-A | BUYER-A | 2 / 7700 |
b | ACTOR-B | TENANT-A | BUYER-B | 2 / 7700 |
c | ACTOR-C | TENANT-B | BUYER-A | 2 / 7700 |
d | ACTOR-D | TENANT-B | BUYER-B | 2 / 7700 |
readonly | ACTOR-READ | TENANT-A | BUYER-A | Same read scope; writes denied. |
Both tenants reuse BUYER-A/BUYER-B, and every scope reuses ORD-SHARED-001/002. Neither a buyer reference nor an order number is a globally authorized identity. There are eight Orders and sixteen items; each Order stores 3850 cents and two items.
Existing context/store setup (keep the imports and surrounding runner from the full source):
context.putAttribute(io.teaql.core.IdSetStore.class.getName(),new io.teaql.core.sql.portable.InMemoryIdSetStore());
The existing Java runner passes UserContext into OrderCheck.run; this line installs the native retained-ID store on that server-owned context.
The session tuple remains application-owned data passed through handle; these
examples do not set a per-session tenant/actor on the native context. Returned
actor metadata and audit-reason text do not prove that a runtime audit principal
was rebound to that actor. Keep actual authentication, native principal/audit
mapping, HTTP status mapping and request-lifetime wiring as explicit service
integration work. The console's ORDER_SESSION=a selects a synthetic fixture;
it is not a credential and must not become a public impersonation parameter.
3. Validate before any provider access
Keep the complete validator and its existing trusted registry/session helper:
static int validate(String token,Map<String,Object> r){
if(!TRUSTED.containsKey(token))throw new Denied("UNAUTHENTICATED");
if(r.keySet().stream().anyMatch(Set.of("tenant","buyer","actor","permissions","policy","userContext")::contains))throw new Denied("FORGED_SCOPE");
if(!Set.of("list","detail","items","summary","retained","edit-note","edit-item").contains(r.get("op"))||!Set.of("op","orderId","orderNumber","itemId","limit","patch").containsAll(r.keySet()))throw new Denied("INVALID_REQUEST");
Object limit=r.getOrDefault("limit",2);if(!integer(limit)||((Number)limit).intValue()<1||((Number)limit).intValue()>2)throw new Denied("INVALID_BOUNDS");
for(String k:List.of("orderId","itemId"))if(r.containsKey(k)&&(!integer(r.get(k))||((Number)r.get(k)).longValue()<=0))throw new Denied("INVALID_REQUEST");
String op=(String)r.get("op");
if(Set.of("detail","items","edit-note","edit-item").contains(op)&&!r.containsKey("orderId")&&!(r.get("orderNumber") instanceof String))throw new Denied("INVALID_REQUEST");
if(Set.of("items","edit-item").contains(op)&&!r.containsKey("itemId"))throw new Denied("INVALID_REQUEST");
if(op.startsWith("edit-")){
if(token.equals("readonly"))throw new Denied("FORBIDDEN_OPERATION");String field=op.equals("edit-note")?"orderNote":"productName";
if(!(r.get("patch") instanceof Map<?,?> patch)||!patch.keySet().equals(Set.of(field))||!(patch.get(field) instanceof String v)||v.isEmpty()||v.length()>80)throw new Denied("FORBIDDEN_FIELD");
}else if(r.containsKey("patch"))throw new Denied("FORBIDDEN_FIELD");
return ((Number)limit).intValue();
}
The public request contains only op, optional identity/business-key selectors,
limit, and an operation-specific patch. Tenant, buyer, actor, permissions,
policy and userContext fields are rejected. Missing/unknown session keys,
unknown filter input and invalid limits are rejected before the facade's first
provider SELECT. The default limit is 2; explicit limits must be integers 1–2,
not null, booleans, strings or fractions.
Only edit-note with one orderNote string or edit-item with one productName
string is writable here. Extra patch fields fail as a whole; no partial allowed
field is saved. Keep native length/numeric handling from the validator. The
recorded ASCII patch probes do not establish identical Unicode length semantics
or exhaustive hostile-JSON validation across languages. Add a transport parser
and input-size/schema contract when integrating this facade into a service.
4. Resolve the tenant and scope every read
The handler validates first, resolves exactly one tenant root from the trusted session, and applies both root and buyer before reading any Order:
int limit=validate(token,r);String op=(String)r.get("op");var s=TRUSTED.get(token);
var roots=Q.commercePlatforms().withTenantKeyIs(s.tenant()).limit(2).comment("Resolve tenant from trusted server session").purpose("Bind the authorized Order scope").executeForList(context);
if(roots.size()!=1)throw new Denied("INVALID_TRUSTED_SCOPE");long root=E.commercePlatform(roots.get(0)).getId().eval();
if(op.equals("summary")){
var values=Q.customerOrders().filterByCommercePlatform(root).withBuyerReferenceIs(s.buyer()).countAs("count").sumTotalMinorAs("amount").limit(1).comment("Summarize only authorized Orders").purpose("Enforce tenant and buyer scope on aggregates").aggregation(context).toList();
return obj("code","OK","actor",s.actor(),"tenant",s.tenant(),"buyer",s.buyer(),"count",values.get(0).get("count"),"amount",values.get(0).get("amount"));
}
This excerpt also shows the provider summary. The same two scope predicates must reach list, detail, business-key lookup, count/sum, retained-ID requests and a scalar reload before mutation. Do not load unscoped Orders and filter them in memory. The initial root lookup uses the trusted tenant key; it is not an unscoped Order read. Shared status constants are intentionally outside tenant-private data.
Continue within the same handler with bounded reads and parent-owned item resolution:
var q=Q.customerOrders().filterByCommercePlatform(root).withBuyerReferenceIs(s.buyer()).orderByIdAscending();
if(!op.equals("retained"))q.selectOrderItemListWith(Q.orderItems().orderByIdAscending().limit(3));
if(r.containsKey("orderId"))q.withIdIs(((Number)r.get("orderId")).longValue());if(r.containsKey("orderNumber"))q.withOrderNumberIs((String)r.get("orderNumber"));
if(op.equals("retained"))q.optimizePaginationWithIdSet("order-isolation-shared",300,20);
var page=q.limit(limit).count().comment("Load authorized Orders and bounded owned items").purpose("Apply trusted tenant and buyer predicates before provider access").executeForList(context);
if(Set.of("list","retained").contains(op)){var result=obj("code","OK","actor",s.actor(),"tenant",s.tenant(),"buyer",s.buyer(),"rows",page.stream().map(o->pageRow(o,!op.equals("retained"))).toList(),"total",page.getTotalCount());if(op.equals("retained"))result.put("plan",context.getAttribute(io.teaql.core.sql.portable.PortableSQLRepository.ID_SET_PLAN));return result;}
if(page.size()!=1)throw new Denied("NOT_FOUND");var order=page.get(0);if(order.getOrderItemList().size()>2)throw new Denied("ITEM_LIMIT");
var item=order.getOrderItemList().stream().filter(i->r.containsKey("itemId")&&E.orderItem(i).getId().eval()==((Number)r.get("itemId")).longValue()).findFirst().orElse(null);
if(Set.of("items","edit-item").contains(op)&&item==null)throw new Denied("NOT_FOUND");
The normal parent bound is 2. The child request loads 3 as a sentinel for the
supported maximum of 2: detail/item/edit paths reject ITEM_LIMIT if a third
item appears. List output remains bounded to three items per parent; it does not
promise a complete child list for an oversized Order. The receipt's seed has two
items per Order. Retained pages deliberately omit children, so their serializer
must not evaluate an unloaded relation.
For item access, resolve the authorized parent first, then locate the requested
item inside that parent's loaded list. A foreign buyer/tenant Order, missing Order,
foreign-parent item and cross-parent item all return NOT_FOUND. This common code
avoids exposing a distinct foreign-ownership result; it is not a timing side-channel
or complete HTTP non-enumeration guarantee.
The summary terminal is aggregation(context).toList() and returns analytic map rows. Ordinary reads use .count().executeForList(context) and page.getTotalCount(). The query builder is mutated in place. The loaded parent/item objects remain attached for audited writes.
Retained IDs share a namespace, not authority
The cache phase invokes sessions a, b, c, a on one server context with fixed
namespace order-isolation-shared, TTL 300 seconds, capacity 20 IDs and page limit 1.
Expected plans are ID_SET_BUILD, ID_SET_BUILD, ID_SET_BUILD, ID_SET_HIT.
Each response still has exact total 2 and identities from its own root/buyer.
Typed predicate identity separates the sets; a namespace is not authorization.
Recreating the context on each call would not demonstrate the final HIT.
The TypeScript store is application-owned; other native store behavior and the
.NET condition-key repair retain their existing qualifications.
5. Authorize narrowly before an audited write
After the parent/item checks above, the handler permits these two writes:
if(op.equals("edit-note")){order.updateOrderNote((String)((Map<?,?>)r.get("patch")).get("orderNote"));order.auditAs(NOTE).save(context);return obj("code","OK","changed",true);}
if(op.equals("edit-item")){item.updateProductName((String)((Map<?,?>)r.get("patch")).get("productName"));item.auditAs(ITEM).save(context);return obj("code","OK","changed",true);}
Keep the scoped complete-state load and the native audited save contract. In Python, TypeScript, Go and .NET, note edits explicitly reload Order scalars without the child graph, preventing graph traversal from advancing item versions. Java, Rust and Swift use the already loaded authorized parent; keep their recorded save semantics rather than translating one language's reload strategy mechanically. Item-label edits save only the selected owned item. This is not a generic patch, status-change or tenant-transfer endpoint.
A successful note write changes only that Order's note, version and update time;
an item-label write changes only that item's label, version and update time.
Other amounts, statuses, identities, rows and relationships remain unchanged in
the retained SQLite snapshots. The read-only actor is denied before provider
access. FORGED_SCOPE, FORBIDDEN_FIELD and foreign-ID requests never become
writes. Keep the audit reason and underlying provider/save failures distinct from
expected authorization responses.
Java uses Session records, Map input and Denied exceptions caught by the facade. Other provider errors propagate. The published Java isolation pass remains distinct from the later JDBC lifecycle repair.
6. Follow the existing console flow
Use the complete Java application and retained built workspace/toolchain from the isolation receipt. This is not a clean-machine bootstrap; the fresh site checkout does not include a portable generated application/dependency bundle. Keep the recorded runtime, regenerated library, manifest/lock files and installed artifact identity together.
From the site root in that prepared local workspace:
export ORDER_WORKSPACE="$PWD/artifacts/order-isolation/2026-10-27/java/java-app-console"
export ORDER_DATABASE="$(mktemp -d)/isolation.sqlite"
export ORDER_REQUEST="$(mktemp)"
cd "$ORDER_WORKSPACE"
run_order_phase() { java -jar target/order-workflow-service-0.0.1-SNAPSHOT.jar --phase="$1" --teaql.datasource.url="jdbc:sqlite:$ORDER_DATABASE"; }
run_order_phase bootstrap
run_order_phase seed
export ORDER_SESSION=a
printf '%s\n' '{"op":"list"}' > "$ORDER_REQUEST"
run_order_phase request
printf '%s\n' '{"op":"summary"}' > "$ORDER_REQUEST"
run_order_phase request
printf '%s\n' '{"op":"detail","orderNumber":"ORD-SHARED-001"}' > "$ORDER_REQUEST"
run_order_phase request
Stop on a native command failure. Read ORDER_ISOLATION_RESULT, not only
ORDER_OK: expected denials are successful console executions with a response
code. List returns the two scoped Orders, summary count 2/amount 7700, and the
business-key request returns one scoped Order. Run cache as one invocation to
observe the four-session sequence; changing ORDER_SESSION across separate
processes creates fresh context/store state.
For a denial control, send {"op":"list","tenant":"TENANT-B"} under a:
expect FORGED_SCOPE before provider access. For a note edit, take the numeric ID
from the authorized detail response and send edit-note with exactly
patch: {"orderNote":"Authorized delivery note"}. Use readonly to inspect
FORBIDDEN_OPERATION on the same valid write shape. The catalog's $ownOrder and
$foreignTenantItem are runner substitutions, not literal console IDs. Obtain
fresh IDs from the corresponding local authorized fixture read when exploring
those probes; do not copy IDs between databases.
Diagnose isolation failures
| Response / symptom | Inspect | Recovery |
|---|---|---|
UNAUTHENTICATED | Missing/unknown server session. | Resolve authenticated membership before calling the facade; never accept a client tuple as the fix. |
FORGED_SCOPE / INVALID_REQUEST | Identity/policy fields, undeclared keys or unknown operation/filter. | Send only the business request schema; retain server ownership of scope. |
INVALID_TRUSTED_SCOPE | Tenant-key lookup returned zero or multiple roots. | Repair tenant provisioning/membership and model/database alignment; never select an arbitrary root. |
INVALID_BOUNDS | Native numeric type and 1–2 range. | Use an integer bound; do not coerce null/boolean/string values. |
NOT_FOUND | Both predicates, selected database and authorized parent/item relationship. | Treat missing and foreign identities alike at this facade; do not issue an unscoped diagnostic lookup in the public request. |
ITEM_LIMIT | Detail/edit sentinel load contains more than two children. | Reject the incomplete edit path; design a new bounded complete-load contract before increasing supported data. |
FORBIDDEN_OPERATION / FORBIDDEN_FIELD | Read-only session or disallowed/extra patch key. | Use the allowed operation and exact patch schema; do not strip fields silently and perform a partial write. |
| Summary sees 8 Orders or cache leaks identities | Scope predicates absent from aggregate/cache key, wrong runtime artifact or bypassed facade. | Require root + buyer before execution, compare actual IDs and exact count 2, and retain the condition-key qualification. |
| Note edit advances item versions | Wrong model/library or graph-save path. | Keep the recorded scalar reload/save semantics and pending generator repairs; compare unrelated-row snapshots. |
Read the existing evidence
The catalog contains 45 request
probes plus the four-step cache sequence. The
source-bound receipt
records runtimes.java.verification.cases.tenant-isolation, fresh-process
readback of both allowed edits, provider SQL and 50 independent SQLite checkpoints
per runtime. Guard denials are checked after ORDER_ISOLATION_BEGIN, separating
facade SQL from initialization. SQL and full snapshots establish different facts;
a JSON code alone does not prove scope enforcement or absence of writes.
Keep the execution notes, generator patch and original published/workspace qualifications. These console SQLite results establish application-facade enforcement for the named probes. Raw Q/database access bypassing this facade, production authentication, row-level security, live authorization changes during a save and distributed caches require their own concrete integration contract.
Continue with Java debugging, the API guide and coverage matrix.