Skip to main content

Python Order Isolation

Build a small application facade that permits a buyer to read and edit only their Orders in their tenant. Start with the Python 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.

This isolation workspace remains WORKSPACE PASS — RELEASE PENDING. 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.

RevisionReader taskDatabase/example boundary
v1.0Read/edit, export, retained IDs and summaries.Keep the original examples and receipts unchanged. A buyer predicate alone does not distinguish tenants.
v1.1Authorized tenant-root + buyer facade.Use its regenerated application/library and a fresh isolation database, not the v1.0 nine-/ten-order query fixture.
v1.2Inventory 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 keyActorTenantBuyerVisible Orders / USD cents
aACTOR-ATENANT-ABUYER-A2 / 7700
bACTOR-BTENANT-ABUYER-B2 / 7700
cACTOR-CTENANT-BBUYER-A2 / 7700
dACTOR-DTENANT-BBUYER-B2 / 7700
readonlyACTOR-READTENANT-ABUYER-ASame 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=UserContext.new().install(GENERATED_RUNTIME_MODULE);context.set_id_set_store(InMemoryIdSetStore());context.insert_resource('dataService',create_sqlite_service(os.environ['ORDER_DATABASE']))

The generated runtime module, native in-memory ID store and SQLite service are installed on the server-owned context. The cache loop awaits handle repeatedly on that 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:

def validate(token,r):
if token not in TRUSTED: raise Denied('UNAUTHENTICATED')
if any(k in r for k in ['tenant','buyer','actor','permissions','policy','userContext']): raise Denied('FORGED_SCOPE')
op=r.get('op');allowed={'list','detail','items','summary','retained','edit-note','edit-item'}
if op not in allowed or set(r)-{'op','orderId','orderNumber','itemId','limit','patch'}: raise Denied('INVALID_REQUEST')
limit=r.get('limit',2)
if type(limit) is not int or not 1<=limit<=2: raise Denied('INVALID_BOUNDS')
for k in ['orderId','itemId']:
if k in r and (type(r[k]) is not int or r[k]<=0): raise Denied('INVALID_REQUEST')
if op in ['detail','items','edit-note','edit-item'] and not ('orderId' in r or isinstance(r.get('orderNumber'),str)): raise Denied('INVALID_REQUEST')
if op in ['items','edit-item'] and 'itemId' not in r: raise Denied('INVALID_REQUEST')
if op.startswith('edit-'):
if token=='readonly': raise Denied('FORBIDDEN_OPERATION')
field='orderNote' if op=='edit-note' else 'productName';patch=r.get('patch')
if not isinstance(patch,dict) or set(patch)!={field} or not isinstance(patch[field],str) or not 1<=len(patch[field])<=80: raise Denied('FORBIDDEN_FIELD')
elif 'patch' in r: raise Denied('FORBIDDEN_FIELD')
return op,limit

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:

op,limit=validate(token,r);actor,tenant,buyer=TRUSTED[token]
roots=await (Q.commerce_platforms().with_tenant_key_is(tenant).limit(2)
.comment('Resolve tenant from trusted server session').purpose('Bind the authorized Order scope').execute_for_list(context))
if len(roots)!=1: raise Denied('INVALID_TRUSTED_SCOPE')
root=roots[0]
def scoped(): return Q.customer_orders().filter_by_commerce_platform(E.commerce_platform(root).id().eval()).with_buyer_reference_is(buyer)
if op=='summary':
values=await (scoped().count_as('count').sum_total_minor_as('amount').limit(1)
.comment('Summarize only authorized Orders').purpose('Enforce tenant and buyer scope on aggregates').execute_for_rows(context))
return {'code':'OK','actor':actor,'tenant':tenant,'buyer':buyer,'count':values[0]['count'],'amount':values[0]['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:

q=scoped().order_by_id_ascending()
if op!='retained':q=q.select_order_item_list_with(Q.order_items().order_by_id_ascending().limit(3))
if 'orderId' in r:q=q.with_id_is(r['orderId'])
if 'orderNumber' in r:q=q.with_order_number_is(r['orderNumber'])
if op=='retained':q=q.optimize_pagination_with_id_set_config('order-isolation-shared',300,20)
fetched=await (q.limit(limit).comment('Load authorized Orders and bounded owned items')
.purpose('Apply trusted tenant and buyer predicates before provider access').execute_for_page(context,0,limit))
page=fetched.data
plan=context.id_set_plan() if op=='retained' else None
if op in ['list','retained']:
result={'code':'OK','actor':actor,'tenant':tenant,'buyer':buyer,'rows':[row(o,op!='retained') for o in page],'total':fetched.total_count}
if plan is not None:result['plan']=plan
return result
if len(page)!=1:raise Denied('NOT_FOUND')
order=page[0];items=order.order_item_list()
if len(items)>2:raise Denied('ITEM_LIMIT')
item=next((i for i in items if E.order_item(i).id().eval()==r.get('itemId')),None)
if op in ['items','edit-item'] and item is None:raise 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.

Use filter_by_commerce_platform(...). execute_for_page exposes .data and .total_count; summary rows are dictionaries from execute_for_rows. type(limit) is int intentionally excludes Python booleans. The note edit reloads scoped complete scalars before audit_as(NOTE).save(context) so loaded child graphs do not advance item versions.

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=='edit-note':
order=await (scoped().with_id_is(E.customer_order(order).id().eval()).limit(2)
.comment('Reload authorized complete Order scalars').purpose('Save one authorized note without touching item graphs').execute_for_one(context))
if order is None:raise Denied('NOT_FOUND')
order.update_order_note(r['patch']['orderNote']);await order.audit_as(NOTE).save(context);return {'code':'OK','changed':True}
if op=='edit-item':
item.update_product_name(r['patch']['productName']);await item.audit_as(ITEM).save(context);return {'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.

Catch Denied as a business outcome; retain other exceptions and awaited saves. Do not replace the strict limit check with isinstance(limit, int), which would accept booleans.

6. Follow the existing console flow​

Use the complete Python 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/python/python-app-console"
export ORDER_DATABASE="$(mktemp -d)/isolation.sqlite"
export ORDER_REQUEST="$(mktemp)"
cd "$ORDER_WORKSPACE"
run_order_phase() { ./.venv/bin/python -m app.main "$1"; }
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 / symptomInspectRecovery
UNAUTHENTICATEDMissing/unknown server session.Resolve authenticated membership before calling the facade; never accept a client tuple as the fix.
FORGED_SCOPE / INVALID_REQUESTIdentity/policy fields, undeclared keys or unknown operation/filter.Send only the business request schema; retain server ownership of scope.
INVALID_TRUSTED_SCOPETenant-key lookup returned zero or multiple roots.Repair tenant provisioning/membership and model/database alignment; never select an arbitrary root.
INVALID_BOUNDSNative numeric type and 1–2 range.Use an integer bound; do not coerce null/boolean/string values.
NOT_FOUNDBoth 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_LIMITDetail/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_FIELDRead-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 identitiesScope 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 versionsWrong 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.python.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 Python debugging, the API guide and coverage matrix.