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
After a user leaves an organization (via the Leave Organization flow, #544), they can still view — and likely still fetch/update/delete — properties belonging to the organization they just left. This is a broken-access-control bug, not a UI/cache staleness issue: the backend genuinely still authorizes and returns the data.
Root cause
Two compounding backend defects:
getUserRole ignores membership status. SupabaseOrganizationDatastore.getUserRole(userId, orgId) (edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseOrganizationDatastore.kt:198-212) filters only by organizationId + userId, with no status = ACTIVE filter. leaveOrganization/removeMember (.../service/MembershipService.kt:77-107, via SupabaseMembershipDatastore.removeMember at .../datastore/supabase/SupabaseMembershipDatastore.kt:89-100) soft-deactivate membership by setting user_organization_mapping.status = INACTIVE rather than deleting the row. Since getUserRole doesn't check status, it keeps returning the user's old role after they've left. RBACService.getUserRoleForOrganizationAction / getUserRoleForPropertyAction (.../service/authorization/RBACService.kt:424-460) consume this role directly, so hasRoleOrHigher still returns true post-leave — this affects every RBAC-gated endpoint that checks org/property role, not just property listing (e.g. getProperty, updateProperty, deleteProperty on PropertyController).
getAssignedProperties has no RBAC check at all, and relies on a stale mapping table. PropertyController.getAssignedProperties (edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/PropertyController.kt:87-98) returns all properties for context.payload.userId with no rbacService.hasRoleOrHigher check (unlike getProperty/updateProperty/deleteProperty, which do call it). It calls PropertyService.getProperties(userId) (.../service/PropertyService.kt:58-68) → SupabasePropertyDatastore.getProperties (.../datastore/supabase/SupabasePropertyDatastore.kt:99-112), which queries the view v_user_properties (edifikana/back-end/supabase/migrations/20260317000004_membership_view_rpc_and_security_fix.sql:20-25), defined as properties JOIN user_property_mapping. This user_property_mapping table is populated only when a property is created and is never cleaned up when a user leaves an org — leaveOrganization/removeMember only touch user_organization_mapping, never user_property_mapping. So stale rows persist indefinitely and getAssignedProperties keeps returning properties for orgs the caller is no longer a member of.
Front-end was ruled out as a contributing cause: PropertyManager/PropertyServiceImpl have no local cache — they hit the network every time, so the server is genuinely still authorizing and serving this data.
Impact
An ex-member retains read (and possibly write/delete, per defect #1) access to an organization's properties indefinitely after leaving — a real data-access boundary violation, not just a stale UI list.
Suggested fix (needs backend work in both places)
Add a status = ACTIVE filter to getUserRole in SupabaseOrganizationDatastore, so RBAC checks correctly stop authorizing deactivated memberships.
Either add an RBAC check to getAssignedProperties, and/or clean up (delete/deactivate) user_property_mapping rows for a user when they leave an org / are removed, so the underlying v_user_properties view stops returning stale rows.
Summary
After a user leaves an organization (via the Leave Organization flow, #544), they can still view — and likely still fetch/update/delete — properties belonging to the organization they just left. This is a broken-access-control bug, not a UI/cache staleness issue: the backend genuinely still authorizes and returns the data.
Root cause
Two compounding backend defects:
getUserRoleignores membership status.SupabaseOrganizationDatastore.getUserRole(userId, orgId)(edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/datastore/supabase/SupabaseOrganizationDatastore.kt:198-212) filters only byorganizationId+userId, with nostatus = ACTIVEfilter.leaveOrganization/removeMember(.../service/MembershipService.kt:77-107, viaSupabaseMembershipDatastore.removeMemberat.../datastore/supabase/SupabaseMembershipDatastore.kt:89-100) soft-deactivate membership by settinguser_organization_mapping.status = INACTIVErather than deleting the row. SincegetUserRoledoesn't check status, it keeps returning the user's old role after they've left.RBACService.getUserRoleForOrganizationAction/getUserRoleForPropertyAction(.../service/authorization/RBACService.kt:424-460) consume this role directly, sohasRoleOrHigherstill returnstruepost-leave — this affects every RBAC-gated endpoint that checks org/property role, not just property listing (e.g.getProperty,updateProperty,deletePropertyonPropertyController).getAssignedPropertieshas no RBAC check at all, and relies on a stale mapping table.PropertyController.getAssignedProperties(edifikana/back-end/src/main/kotlin/com/cramsan/edifikana/server/controller/PropertyController.kt:87-98) returns all properties forcontext.payload.userIdwith norbacService.hasRoleOrHighercheck (unlikegetProperty/updateProperty/deleteProperty, which do call it). It callsPropertyService.getProperties(userId)(.../service/PropertyService.kt:58-68) →SupabasePropertyDatastore.getProperties(.../datastore/supabase/SupabasePropertyDatastore.kt:99-112), which queries the viewv_user_properties(edifikana/back-end/supabase/migrations/20260317000004_membership_view_rpc_and_security_fix.sql:20-25), defined asproperties JOIN user_property_mapping. Thisuser_property_mappingtable is populated only when a property is created and is never cleaned up when a user leaves an org —leaveOrganization/removeMemberonly touchuser_organization_mapping, neveruser_property_mapping. So stale rows persist indefinitely andgetAssignedPropertieskeeps returning properties for orgs the caller is no longer a member of.Front-end was ruled out as a contributing cause:
PropertyManager/PropertyServiceImplhave no local cache — they hit the network every time, so the server is genuinely still authorizing and serving this data.Impact
An ex-member retains read (and possibly write/delete, per defect #1) access to an organization's properties indefinitely after leaving — a real data-access boundary violation, not just a stale UI list.
Suggested fix (needs backend work in both places)
status = ACTIVEfilter togetUserRoleinSupabaseOrganizationDatastore, so RBAC checks correctly stop authorizing deactivated memberships.getAssignedProperties, and/or clean up (delete/deactivate)user_property_mappingrows for a user when they leave an org / are removed, so the underlyingv_user_propertiesview stops returning stale rows.getProperty, etc.) exploitable via Transfer Repos #1.Related