Fetch the caller's tenant role once in a new load_workspace_role prepare
step (stashed on AuthData.tenant_role) instead of querying TenantAccountJoin
twice — once for membership, once for role. check_workspace_role becomes a
pure assertion over the stashed role; non-members get 404 (no cross-tenant
ID leak), out-of-set roles 403.
Other cleanups:
- All prepare steps are now skip-if-already-loaded for safe re-entry.
- Move the app.enable_api policy check out of load_app into a dedicated
check_app_api_enabled verify step (data-load vs policy-assert separation).
- Validate workspace_id is a UUID in load_tenant_from_request (malformed
id -> 404, not 500).
- Collapse guard/guard_workspace duplication into a shared _make_decorator.
- Delete the now-unused role_gate.require_workspace_role decorator and its
tests; enforcement lives entirely in the auth pipeline.
Move workspace_id ownership validation from handler-level decorators into
the OpenAPI auth pipeline. guard_workspace() sets workspace_membership and
allowed_roles on RequestContext; pipeline steps load the tenant, verify
membership via TenantAccountJoin, check workspace_id mismatch, and gate
on role — all before the handler runs. Covers CE/EE/SaaS editions.
Migrates apps.py and workspaces.py endpoints; replaces the ad-hoc
require_workspace_role decorator with pipeline-native enforcement.