Summary
DHService FastAPI endpoints (code/DHService/v1.py) require only AuthenticatedClient — i.e. any caller with a valid OAuth2 client-credentials token gets full read access to any member's identity, status, forms, access logs, etc. Permission gating happens entirely at the Flask portal layer via decorators like @requires_view_permission(\"member.forms\").
Two new endpoints added on new-member-onboard-flow follow the same pattern:
GET /v1/member/display_name/?member_id=N → returns first/last/username for any member ID
GET /v1/onboarder_search/?query=…&limit=N → enumerates members by name/username
This is consistent with existing endpoints (it's not a regression introduced by the onboarding work), but it means:
- Anyone with a DH service client token can enumerate the entire member directory
- The admin-portal permission check (
@requires_view_permission(\"member.forms\")) is a portal-side speedbump that says nothing about who's actually authorized at the data layer
- Non-admin DH service clients (e.g. workers, member portal) can in principle query any member's data, even though their natural use case only needs the calling member's own data
Proposed approach
Push permission/scope checks down to the DHService layer. Options worth weighing:
- Annotate endpoints with required permissions, looked up against the calling client's role/scope claims (requires DH OAuth2 to issue scoped tokens — currently it doesn't differentiate)
- Add a per-client-id allowlist (e.g.
dev-admin-portal can read everyone, dev-member-portal can only read its own session member, workers get tag-specific scopes)
- Or simply harden the new endpoints (
/member/display_name/, /onboarder_search/) to require an admin-style client and leave the existing pattern alone
This is a defense-in-depth concern, not a known-exploitable bug — the gateway routing and client-credential storage limit who can talk to DHService at all.
Out of scope
- Changes to existing endpoints (track separately if we're going to do a full audit)
- OAuth2 scope work (probably needed if we want fine-grained per-endpoint perms; bigger lift)
Where
code/DHService/v1.py — every endpoint declares only current_user: AuthenticatedClient
- New endpoints landing in PR for
new-member-onboard-flow: lines around get_member_display_name and onboarder_search
Summary
DHService FastAPI endpoints (
code/DHService/v1.py) require onlyAuthenticatedClient— i.e. any caller with a valid OAuth2 client-credentials token gets full read access to any member's identity, status, forms, access logs, etc. Permission gating happens entirely at the Flask portal layer via decorators like@requires_view_permission(\"member.forms\").Two new endpoints added on
new-member-onboard-flowfollow the same pattern:GET /v1/member/display_name/?member_id=N→ returns first/last/username for any member IDGET /v1/onboarder_search/?query=…&limit=N→ enumerates members by name/usernameThis is consistent with existing endpoints (it's not a regression introduced by the onboarding work), but it means:
@requires_view_permission(\"member.forms\")) is a portal-side speedbump that says nothing about who's actually authorized at the data layerProposed approach
Push permission/scope checks down to the DHService layer. Options worth weighing:
dev-admin-portalcan read everyone,dev-member-portalcan only read its own session member, workers get tag-specific scopes)/member/display_name/,/onboarder_search/) to require an admin-style client and leave the existing pattern aloneThis is a defense-in-depth concern, not a known-exploitable bug — the gateway routing and client-credential storage limit who can talk to DHService at all.
Out of scope
Where
code/DHService/v1.py— every endpoint declares onlycurrent_user: AuthenticatedClientnew-member-onboard-flow: lines aroundget_member_display_nameandonboarder_search