fix(sharing): gate falls back to direct ownership before share check
All checks were successful
Lint and Build / format (pull_request) Successful in 36s
Lint and Build / clippy (pull_request) Successful in 1m43s
Lint and Build / build (pull_request) Successful in 3m50s
Lint and Build / test (pull_request) Successful in 3m41s

The share-gate I added in Phase B was too strict: it only admitted the
owner via the profiles collection, so any data item whose profile_id
didn't map to a real Profile document (legacy/test data, or a profile_id
the caller used at create time without a backing Profile) got 404'd on
read — even for the item's actual owner. CI caught this:
zk_integration_tests::medication_stored_as_ciphertext_not_plaintext
creates a medication with profile_id="default" (no Profile doc) and
the new get_medication gate returned 404.

Fix: in each list/get data handler, check direct ownership
(user_id == claims.sub, or has own data for the profile) FIRST, and
only fall through to the share-gate if the caller isn't the direct
owner. This preserves the original ownership model AND adds share-
recipient access, without breaking items whose profile_id isn't a real
Profile document.

The share-only path (recipient reading shared data they don't own)
still goes through the gate as before.
This commit is contained in:
goose 2026-07-19 11:40:44 -03:00
parent e28135bc08
commit d0efecf38f
3 changed files with 74 additions and 22 deletions

View file

@ -90,14 +90,26 @@ pub async fn list_health_stats(
.into_response()
}
};
// Share-gate: if a profile_id is specified, resolve the owner (admits the
// owner or an active share recipient) and query as them.
// Resolve the effective owner. If a profile_id is specified, prefer direct
// ownership (the caller's own data for that profile); otherwise fall through
// to the share-gate (admits an active share recipient). profile_id is a
// free-form string at create time and may not map to a Profile document.
let owner = match &query.profile_id {
Some(pid) => {
match crate::handlers::profile_share::authorize_profile_read(&state, &claims, pid).await
{
Ok(o) => o,
Err(resp) => return resp.into_response(),
let has_own = repo
.find_by_user_filtered(&claims.sub, Some(pid))
.await
.map(|v| !v.is_empty())
.unwrap_or(false);
if has_own {
claims.sub.clone()
} else {
match crate::handlers::profile_share::authorize_profile_read(&state, &claims, pid)
.await
{
Ok(o) => o,
Err(resp) => return resp.into_response(),
}
}
}
None => claims.sub.clone(),
@ -146,7 +158,11 @@ pub async fn get_health_stat(
match repo.find_by_id(&object_id).await {
Ok(Some(stat)) => {
// Share-gate on the stat's profile (admits owner or recipient).
// Authorization: direct owner (user_id == caller) OR share-gate on
// the stat's profile (admits an active share recipient).
if stat.user_id == claims.sub {
return (StatusCode::OK, Json(HealthStatResponse::from(stat))).into_response();
}
match crate::handlers::profile_share::authorize_profile_read(
&state,
&claims,