feat(owletto-backend): gate $member list to members, emails to admins#309
Conversation
…mins The previous commit on #296 made public workspaces self-serve joinable but left PII exposed: `manage_entity action=list entity_type=$member` is a public-readable MCP action, so anonymous or non-member callers hitting POST /api/{slug}/manage_entity got back every member's email in the metadata blob. resolve_path on /{owner}/$member/{slug} leaked the same way. Policy now enforced in `handleList`, `handleGet`, and `_resolvePath`: - Non-members of the org cannot see the member list or any individual $member entity — both paths throw a "join the workspace" error rather than returning a sanitized row, so the very existence of member entries isn't catalog-scraped by anonymous callers. - Members who aren't admin/owner see the full list + entity but get the email field stripped from `metadata`. Non-PII fields (status, role, display_name, etc.) remain so the list page still has something to render. - Only admin/owner callers see emails. Threaded `memberRole` through `ToolContext` so handlers can apply the policy without extra DB lookups; the REST proxy + MCP session path both populate it from the existing `AuthContext.memberRole`. Pairs with owletto-web PR #15, which adds the Join bar and gates the Members/Agents pages client-side so the UI never even issues these calls as a non-member.
There was a problem hiding this comment.
💡 Codex Review
lobu/packages/owletto-backend/src/tools/admin/manage_entity.ts
Lines 722 to 726 in af31379
Email redaction depends on the $member schema (to find the field marked x-email), but this code only fetches metadata_schema when args.entity_type is set. For action: 'list' calls without entity_type, schema becomes null, so redaction falls back to the literal email key and leaks emails for orgs that renamed the member email field (still marked with x-email) to non-admin members.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| env: Env, | ||
| ctx: ToolContext | ||
| ): Promise<ManageEntityResult> { | ||
| if (args.entity_type === '$member' && !canSeeMemberList(ctx)) { |
There was a problem hiding this comment.
Enforce member gate on unfiltered manage_entity list
The new guard only rejects non-members when args.entity_type === '$member', so a caller can still run manage_entity { action: 'list' } without entity_type and receive $member rows from listEntities (it only filters by type when a type is provided). In a public workspace this lets anonymous/authenticated non-members continue enumerating members, which bypasses the intended “member list is only visible to members” policy.
Useful? React with 👍 / 👎.
Pulls in the client-side half of the public-workspace privacy changes: sticky Join bar for non-members, Members page gated to members, and Agents page read-only for non-members — pairs with the backend policy tightening in the previous commit on this branch.
…page-auth coverage etHandleGet now calls ensureMemberEntityType when slug=$member has no row, unstucking the /devops/%24member page. Adds page-auth-coverage integration tests covering anon / non-member / member / owner states across the APIs the workspace pages depend on, with an explicit regression for the $member auto-provision path.
…literals (#311) #309 added the required memberRole field to ToolContext but missed three non-MCP callers that build the context manually: public REST endpoints, connection provisioning, and watcher reaction executor. The Docker build runs a package-local tsc and caught these; main has been failing the build-app image since #309 merged.
Second pass on the 2-week PR review. Five more gaps closed: - gateway: unit tests for verifyOwnedAgentAccess covering owner, cross-tenant, cross-platform, agent-scoped, admin-bypass, unknown-agent, and external OAuth mismatches (#285 follow-up). Closes the test hole in the cross-tenant ownership guard. - owletto-backend: validate each CSP frame-ancestor entry against a strict host-source / scheme-source grammar before joining (#246 follow-up). Malformed env entries like `https:// lobu.ai` are now dropped instead of silently rendered into the directive. - owletto-backend: introduce normalizeHost() in utils/public-origin and use it from getSubdomainZone, extractSubdomainOrg, getCanonicalRedirectUrl, and the BetterAuth trustedOrigins wiring (#234/#224/#214 follow-up). Unifies the ad-hoc .toLowerCase()/.replace() patterns and adds IDN→punycode so `müller.lobu.ai` matches the ASCII zone configured in env. - owletto-backend: redact member emails that surface via template_data and tab template_data in resolve_path, not only on the single resolved entity (#309 follow-up). A dashboard data source that enumerates $member entities no longer leaks emails to non-admin callers. New utils/member-redaction helper plus unit coverage. - owletto-backend: treat #311 as already closed — ToolContext.memberRole is `string | null` (required, not optional), so TypeScript already catches future literal omissions at construction.
Second pass on the 2-week PR review. Five more gaps closed: - gateway: unit tests for verifyOwnedAgentAccess covering owner, cross-tenant, cross-platform, agent-scoped, admin-bypass, unknown-agent, and external OAuth mismatches (#285 follow-up). Closes the test hole in the cross-tenant ownership guard. - owletto-backend: validate each CSP frame-ancestor entry against a strict host-source / scheme-source grammar before joining (#246 follow-up). Malformed env entries like `https:// lobu.ai` are now dropped instead of silently rendered into the directive. - owletto-backend: introduce normalizeHost() in utils/public-origin and use it from getSubdomainZone, extractSubdomainOrg, getCanonicalRedirectUrl, and the BetterAuth trustedOrigins wiring (#234/#224/#214 follow-up). Unifies the ad-hoc .toLowerCase()/.replace() patterns and adds IDN→punycode so `müller.lobu.ai` matches the ASCII zone configured in env. - owletto-backend: redact member emails that surface via template_data and tab template_data in resolve_path, not only on the single resolved entity (#309 follow-up). A dashboard data source that enumerates $member entities no longer leaks emails to non-admin callers. New utils/member-redaction helper plus unit coverage. - owletto-backend: treat #311 as already closed — ToolContext.memberRole is `string | null` (required, not optional), so TypeScript already catches future literal omissions at construction.
…instr guard, MCP join rate-limit) (#325) * fix: address gaps found in post-merge review of last 2 weeks of PRs Follow-ups from an aggregated teammate review of 128 PRs merged between 2026-04-09 and 2026-04-23. Five concrete gaps patched: - worker: constrain UploadUserFile to the workspace root (#203 follow-up). path.join allowed `../` and absolute paths to escape the workspace. Now resolves and rejects anything outside workspaceDir when one is set. - core: flip Sentry sendDefaultPii to false (#172 follow-up). User content and identifiers flow through this stack; the schema has no scrubbing so PII-by-default was unsafe. - gateway: make SlackInstructionProvider extend BaseInstructionProvider (#269 follow-up). Sibling Skills/Network providers are wrapped in a try/catch that returns "" on error; Slack was bypassing it and would crash session-context assembly if listConnections threw. - owletto-backend: rate-limit the join_organization MCP tool to match the REST endpoint (#296 follow-up). Keyed on userId since MCP calls don't carry a client IP. Skipped one reviewer finding: removing the process.env fallback for API keys at worker.ts:1099/1109 (the inconsistency with #225 base-URL code). Embedded/dev workers depend on that fallback since credentialStore is only populated from gateway-supplied session context. * fix: address remaining gaps from post-merge review Second pass on the 2-week PR review. Five more gaps closed: - gateway: unit tests for verifyOwnedAgentAccess covering owner, cross-tenant, cross-platform, agent-scoped, admin-bypass, unknown-agent, and external OAuth mismatches (#285 follow-up). Closes the test hole in the cross-tenant ownership guard. - owletto-backend: validate each CSP frame-ancestor entry against a strict host-source / scheme-source grammar before joining (#246 follow-up). Malformed env entries like `https:// lobu.ai` are now dropped instead of silently rendered into the directive. - owletto-backend: introduce normalizeHost() in utils/public-origin and use it from getSubdomainZone, extractSubdomainOrg, getCanonicalRedirectUrl, and the BetterAuth trustedOrigins wiring (#234/#224/#214 follow-up). Unifies the ad-hoc .toLowerCase()/.replace() patterns and adds IDN→punycode so `müller.lobu.ai` matches the ASCII zone configured in env. - owletto-backend: redact member emails that surface via template_data and tab template_data in resolve_path, not only on the single resolved entity (#309 follow-up). A dashboard data source that enumerates $member entities no longer leaks emails to non-admin callers. New utils/member-redaction helper plus unit coverage. - owletto-backend: treat #311 as already closed — ToolContext.memberRole is `string | null` (required, not optional), so TypeScript already catches future literal omissions at construction. --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
Pairs with owletto-web#15, which ships the client-side gate + a sticky "Join" bar so the UI never issues these calls as a non-member. This parent PR will get a follow-up submodule-pointer bump once #15 merges.
Why now
PR #296 added self-serve joining for public orgs and sanitized `/public/` REST endpoints, but `manage_entity list` is marked publicly readable at the tool layer (see `auth/tool-access.ts` PUBLIC_READ_ACTIONS). That path bypasses the `/public/` allow-list and returns raw `metadata`, which for `$member` entities includes the user's email.
Test plan