You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
security(users): /user-management/users/search answers anonymous callers with names from every organisation #16279
GET /api/user-management/users/search (autobot-backend/api/user_management/users.py:83–:146) takes no auth dependency. It queries through TenantContext(org_id=None, user_id=None, is_platform_admin=True) (users.py:115) and returns id and name=user.full_name for each match (:126–:127). So an anonymous caller can enumerate user ids and full names across every organisation.
Live: an unauthenticated GET against the deployed backend returned 200. The control, /api/auth/me with the same empty credentials, returned 401. I didn't inspect the response body.
No shipped caller: the only frontend caller is ShareKnowledgeDialog.vue:270, apart from the generated API types. That dialog is imported only by its Storybook story, ShareKnowledgeDialog.stories.ts, so no shipped view depends on the anonymous or cross-org behaviour today.
Decision needed
Is #2072's open posture still wanted? And whatever the answer, should the search be confined to the caller's organisation? My recommendation: require authentication and scope the search to the caller's org, since sharing only makes sense within an org and the dialog is not mounted anywhere.
Acceptance criteria
The posture is decided on this issue, and user_management_route_posture_test.py:62 is updated to match.
The search is scoped to the caller's organisation, with no platform-admin context built for an anonymous or ordinary caller.
A test asserts that a user in org A gets no results from org B.
After deploy, the deployed backend's behaviour matches the ruled posture. This needs host evidence.
Found during the #16270 permission-vocabulary investigation.
Owner ruling (2026-09-11): require login, and scope results to the caller's own organisation. Nothing shipped depends on the open or cross-org behaviour: the only caller, ShareKnowledgeDialog.vue, is mounted by nothing but its Storybook story. This rides the #16278 batch PR (#16287), since it is the same file (users.py) and the same posture table as #15738.
Progress after PR #16240 (f5fe688): 3 of 4 criteria met, including live host evidence. This stays open for one test gap; the follow-up PR is next.
The ruling above planned for this to ride #16287. That PR closed unmerged, and the work landed in #16240 instead. The deployed code contains f5fe688.
Acceptance criteria (verbatim):
"The posture is decided on this issue, and user_management_route_posture_test.py:62 is updated to match." ✅ The owner ruled on this issue on 2026-09-11. The posture table now records ("GET", "/user-management/users/search"): _AUTHENTICATED, and test_no_route_is_open asserts that no user-management route is open.
"The search is scoped to the caller's organisation, with no platform-admin context built for an anonymous or ordinary caller." ✅ search_users_for_sharing depends on require_org_context, which needs a logged-in caller with an org and answers 400 otherwise. It also depends on get_user_service, whose apply_tenant_filter adds org_id == context.org_id. No hand-built context remains.
"A test asserts that a user in org A gets no results from org B." ❌ Not met as worded. The existing tests check that the service context carries the caller's org, and that the org is a bound query parameter, but no test seeds an org-B user and checks it is absent. The follow-up adds one.
"After deploy, the deployed backend's behaviour matches the ruled posture. This needs host evidence." ✅ Host evidence, 2026-09-12, on the local install, through the product's own admin API (owner-approved probe; ids redacted):
request
status
result
unauthenticated GET /api/user-management/users/search
401
refused
POST /api/user-management/organizations, throwaway orgs A and B, as platform admin
201, 201
created
POST /api/user-management/users, one user in each org
201, 201
created
POST /api/auth/login as each throwaway user
200, 200
logged in
org A user: GET .../users/search?q=<org B username>
200
available: true, 0 results
org B user: GET .../users/search?q=<org A username>
200
available: true, 0 results
control, org A user searching their own username
200
available: true, 1 result (themselves)
control, org B user searching their own username
200
available: true, 1 result (themselves)
available: true matters: on a database error the search returns an empty list with available: false. The two controls show the search itself works, so the empty cross-org results come from scoping, not from a failure.
Cleanup did not complete as planned. All four hard deletes (?hard_delete=true) returned 500. The organisation deletes fail on relation "terminal_activities" does not exist, the missing table tracked in #16464. I soft-deleted all four instead (?hard_delete=false, 200 each), and they no longer appear in listings. Hard-deleting them waits on #16464.
Stale-issue sweep (never auto-closed — Dev_new_gui wasn't GitHub's default branch at merge time). Verified against current origin/main; leaving open.
3 of 4 criteria MET: posture decided and recorded — users.py:83-127search_users_for_sharing now depends on require_org_context (:107) instead of a hand-built platform-admin TenantContext; search is scoped to the caller's org via apply_tenant_filter, no anonymous/platform-admin context remains. A prior comment on the issue (2026-09-12) also records an end-to-end probe with two throwaway orgs/users showing 401 unauthenticated and 0 cross-org results with a working-search control.
NOT MET: AC3 as literally stated — user_management_gates_test.py only has test_the_user_query_is_confined_to_the_callers_organisation (checks the compiled SQL binds _ORG as a parameter, unit-level) and test_the_search_runs_in_the_callers_own_organisation (checks a mocked context) — no test seeds two real users in two real orgs and asserts cross-org exclusion end-to-end in one place.
Delivered by merged PR #16240 (same batch as #16278) — partial; a follow-up for the test gap hasn't merged yet.
Checked against origin/main (PR #16506, abf8432). 3 of 4 ACs verified from code and ticked. AC4 needs host evidence, so under the audit rule this is reopened.
Met
AC1: the owner's ruling is on this issue; autobot-backend/api/user_management/user_management_route_posture_test.py:79("GET", "/user-management/users/search"): _AUTHENTICATED.
AC2: api/user_management/users.py:105_org: TenantContext = Depends(require_org_context). require_org_context answers 400 without org_id. Platform-admin is set only if _is_platform_admin_claim(current_user), behind get_current_user.
AC3: autobot-backend/user_management/services/user_service_org_scoping_test.py::test_a_user_in_org_a_finds_no_results_from_org_b asserts [u.username for u in users] == ["alice"] across two orgs. It tests the service, not the route.
Remaining
AC4 (host evidence): the probe comment on this issue (2026-09-12, 13:14 Riga time) records anonymous → 401, cross-org → 0 results both ways, own-org → 1, against a deploy containing the fix. test(user-management): a user in org A gets no results from org B (#16279) #16506 changed tests only. If the owner accepts that comment as the AC4 evidence, tick AC4 and re-close. This audit did not re-run the probe.
Re-closed: all 4 criteria are verified. ACs 1–3 have code evidence on origin/main (audit comment above). AC4 has the live host probe recorded on this issue on 2026-09-12: anonymous callers get 401, cross-org searches return 0 results both ways, and own-org searches return 1. That ran against a deploy containing the fix (f5fe688). #16506, which completed AC3, changed tests only, so the deployed behaviour it probed is unchanged.
Activity
mrveiss commentedon Sep 11, 2026
Owner ruling (2026-09-11): require login, and scope results to the caller's own organisation. Nothing shipped depends on the open or cross-org behaviour: the only caller, ShareKnowledgeDialog.vue, is mounted by nothing but its Storybook story. This rides the #16278 batch PR (#16287), since it is the same file (users.py) and the same posture table as #15738.
security(users): require login and the caller own org for /users/sear…
security(users): require login and the caller own org for /users/sear…
security(auth): admin-gate the open routers, settings, teams and user…
mrveiss commentedon Sep 12, 2026
Progress after PR #16240 (f5fe688): 3 of 4 criteria met, including live host evidence. This stays open for one test gap; the follow-up PR is next.
The ruling above planned for this to ride #16287. That PR closed unmerged, and the work landed in #16240 instead. The deployed code contains f5fe688.
Acceptance criteria (verbatim):
user_management_route_posture_test.py:62is updated to match." ✅ The owner ruled on this issue on 2026-09-11. The posture table now records("GET", "/user-management/users/search"): _AUTHENTICATED, andtest_no_route_is_openasserts that no user-management route is open.search_users_for_sharingdepends onrequire_org_context, which needs a logged-in caller with an org and answers 400 otherwise. It also depends onget_user_service, whoseapply_tenant_filteraddsorg_id == context.org_id. No hand-built context remains.GET /api/user-management/users/searchPOST /api/user-management/organizations, throwaway orgs A and B, as platform adminPOST /api/user-management/users, one user in each orgPOST /api/auth/loginas each throwaway userGET .../users/search?q=<org B username>available: true, 0 resultsGET .../users/search?q=<org A username>available: true, 0 resultsavailable: true, 1 result (themselves)available: true, 1 result (themselves)available: truematters: on a database error the search returns an empty list withavailable: false. The two controls show the search itself works, so the empty cross-org results come from scoping, not from a failure.Cleanup did not complete as planned. All four hard deletes (
?hard_delete=true) returned 500. The organisation deletes fail onrelation "terminal_activities" does not exist, the missing table tracked in #16464. I soft-deleted all four instead (?hard_delete=false, 200 each), and they no longer appear in listings. Hard-deleting them waits on #16464.security(settings): record the real caller on /sync and /hardware-pri…
mrveiss commentedon Sep 12, 2026
Stale-issue sweep (never auto-closed — Dev_new_gui wasn't GitHub's default branch at merge time). Verified against current
origin/main; leaving open.3 of 4 criteria MET: posture decided and recorded —
users.py:83-127search_users_for_sharingnow depends onrequire_org_context(:107) instead of a hand-built platform-adminTenantContext; search is scoped to the caller's org viaapply_tenant_filter, no anonymous/platform-admin context remains. A prior comment on the issue (2026-09-12) also records an end-to-end probe with two throwaway orgs/users showing 401 unauthenticated and 0 cross-org results with a working-search control.NOT MET: AC3 as literally stated —
user_management_gates_test.pyonly hastest_the_user_query_is_confined_to_the_callers_organisation(checks the compiled SQL binds_ORGas a parameter, unit-level) andtest_the_search_runs_in_the_callers_own_organisation(checks a mocked context) — no test seeds two real users in two real orgs and asserts cross-org exclusion end-to-end in one place.Delivered by merged PR #16240 (same batch as #16278) — partial; a follow-up for the test gap hasn't merged yet.
test(user-management): a user in org A gets no results from org B (#1…
fix(test): compile User.preferences' JSONB as JSON under SQLite (#16279)
chore(settings): merge main, keeping main's side of every hunk #16550 …
test(user-management): a user in org A gets no results from org B (#1…
mrveiss commentedon Sep 13, 2026
Closure audit (batch 2c): reopened
Checked against
origin/main(PR #16506, abf8432). 3 of 4 ACs verified from code and ticked. AC4 needs host evidence, so under the audit rule this is reopened.Met
autobot-backend/api/user_management/user_management_route_posture_test.py:79("GET", "/user-management/users/search"): _AUTHENTICATED.api/user_management/users.py:105_org: TenantContext = Depends(require_org_context).require_org_contextanswers 400 withoutorg_id. Platform-admin is set onlyif _is_platform_admin_claim(current_user), behindget_current_user.autobot-backend/user_management/services/user_service_org_scoping_test.py::test_a_user_in_org_a_finds_no_results_from_org_basserts[u.username for u in users] == ["alice"]across two orgs. It tests the service, not the route.Remaining
mrveiss commentedon Sep 13, 2026
Re-closed: all 4 criteria are verified. ACs 1–3 have code evidence on
origin/main(audit comment above). AC4 has the live host probe recorded on this issue on 2026-09-12: anonymous callers get 401, cross-org searches return 0 results both ways, and own-org searches return 1. That ran against a deploy containing the fix (f5fe688). #16506, which completed AC3, changed tests only, so the deployed behaviour it probed is unchanged.