feat(world-model): cross-org list_rules read mode#399
Merged
Conversation
Follow-up to #386 (Pi review): rtHandleListRules went through requireRelationshipType, which 403s on any RT outside the caller's org. That blocks the public-catalog UX in lobu-ai/owletto#26 from populating the rules under cross-org rel types. Add a 'read' mode to requireRelationshipType that widens the slug lookup to (caller_org OR visibility=public) with tenant-first ORDER BY. Mutating callers (add_rule, remove_rule, update, delete) keep the strict 'write' mode and continue to deny cross-org access. Only list_rules opts into read mode.
1 task
This was referenced Apr 27, 2026
Contributor
|
Triage decision: Reasons:
Next: Queued for auto-merge with squash. Will merge automatically once all checks complete and branch is up to date. |
2 tasks
buremba
added a commit
that referenced
this pull request
Apr 27, 2026
…ng + tests Audit follow-up to #386, #399 found three additional consumers of the widened cross-org reads that need attention: 1. utils/schema-validation.ts getEntityTypeSchema() loaded only the caller's org schema. After #374 a tenant entity can carry a public catalog type (resolved via the schema search path) — validation then ran against an empty schema and silently let bad metadata through. Widened with the same (caller_org OR visibility=public) + tenant- first ORDER BY pattern as the resolver in entity-management.ts. Now creating an entity with a public catalog type validates against the catalog's metadata_schema. 2. tools/search.ts entitySelectColumns() — connection_count and active_connection_count subqueries were CASE-WHEN-gated on caller- org-equality, but the inner FROM feeds f / JOIN connections cn didn't restate f.organization_id = e.organization_id like the children/watcher_count subqueries on the same function do. Added the predicate for consistency: defensive belt-and-suspenders against any future cross-org feed.entity_ids reference. 3. tools/get_watchers.ts entity-context query — the LEFT JOIN feeds / current_event_records on entity_id was unscoped. requireReadAccess blocks cross-org callers from reaching this site today, but the join itself should match the entity's org regardless. Added the filters so the count is always entity-org-local. Also adds packages/owletto-backend/src/__tests__/integration/entity-types/ cross-org.test.ts covering the post-#386/#399 contract: - list returns local + cross-org rows tenant-first with organization_slug - get resolves public catalog types and surfaces organization_slug - $member is per-tenant: get auto-provisions in caller, never returns the catalog's $member - tenant-first ordering wins on slug collisions - list_rules works on cross-org rel types (read mode) - add_rule still 403s on cross-org (write mode strict) - create with cross-org type validates against the catalog's schema Tests run against the project's standard integration test backend (real Postgres). PGlite mode has a pre-existing auth issue affecting all integration tests in this repo.
buremba
added a commit
that referenced
this pull request
Apr 27, 2026
…ng + tests (#407) * fix(world-model): cross-org schema validation + defensive count scoping + tests Audit follow-up to #386, #399 found three additional consumers of the widened cross-org reads that need attention: 1. utils/schema-validation.ts getEntityTypeSchema() loaded only the caller's org schema. After #374 a tenant entity can carry a public catalog type (resolved via the schema search path) — validation then ran against an empty schema and silently let bad metadata through. Widened with the same (caller_org OR visibility=public) + tenant- first ORDER BY pattern as the resolver in entity-management.ts. Now creating an entity with a public catalog type validates against the catalog's metadata_schema. 2. tools/search.ts entitySelectColumns() — connection_count and active_connection_count subqueries were CASE-WHEN-gated on caller- org-equality, but the inner FROM feeds f / JOIN connections cn didn't restate f.organization_id = e.organization_id like the children/watcher_count subqueries on the same function do. Added the predicate for consistency: defensive belt-and-suspenders against any future cross-org feed.entity_ids reference. 3. tools/get_watchers.ts entity-context query — the LEFT JOIN feeds / current_event_records on entity_id was unscoped. requireReadAccess blocks cross-org callers from reaching this site today, but the join itself should match the entity's org regardless. Added the filters so the count is always entity-org-local. Also adds packages/owletto-backend/src/__tests__/integration/entity-types/ cross-org.test.ts covering the post-#386/#399 contract: - list returns local + cross-org rows tenant-first with organization_slug - get resolves public catalog types and surfaces organization_slug - $member is per-tenant: get auto-provisions in caller, never returns the catalog's $member - tenant-first ordering wins on slug collisions - list_rules works on cross-org rel types (read mode) - add_rule still 403s on cross-org (write mode strict) - create with cross-org type validates against the catalog's schema Tests run against the project's standard integration test backend (real Postgres). PGlite mode has a pre-existing auth issue affecting all integration tests in this repo. * fix(search): scope content_count subquery by entity org Pi caught one more in entitySelectColumns(): content_count subquery joined current_event_records by entityLinkMatchSql only, with no ev.organization_id filter. Cross-org events that share an entity_id or an identity-namespace match could inflate counts even though the outer CASE WHEN guards against returning anything for cross-org rows. Same pattern as the connection_count / active_connection_count fixes in the parent commit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #386 surfaced by Pi's review of lobu-ai/owletto-web#26.
rtHandleListRulescallsrequireRelationshipType, which throws "Access denied: relationship type belongs to another organization" for any RT outside the caller's org. After #386, the frontend now lists public-catalog relationship types — but their rules can't be read, so the read-only sections come up empty.This PR adds a
mode: 'read' | 'write'parameter (default'write'to preserve existing callers) torequireRelationshipType. In'read'mode it widens the slug lookup to(caller_org OR visibility = 'public')with tenant-firstORDER BY— same shape as the resolver inentity-management.ts:249–260.Only
rtHandleListRulesopts into'read'. Mutating handlers (add_rule,remove_rule,update,delete) keep the strict'write'mode and continue to deny cross-org writes — that's the correct behavior because cross-org RTs belong to the catalog and aren't editable from the tenant side.Test plan
bun run typecheck(owletto-backend) clean