fix(registry): deny CanCommunicate for cross-tenant org roots (#1955) #2110

Closed
core-be wants to merge 1 commits from fix/registry-cancommunicate-cross-tenant-roots-1955 into main
2 changed files with 9 additions and 10 deletions
+4 -6
View File
@@ -62,7 +62,7 @@ func isAncestorOf(ancestorID, childID string) bool {
// the org hierarchy. The rules:
//
// - self → self
// - siblings (same parent, including both root-level)
// - siblings (same parent; root-level workspaces in different orgs are NOT siblings)
// - any ancestor → any descendant (e.g. PM → Backend Engineer)
// - any descendant → any ancestor (e.g. Security Auditor → PM)
//
@@ -94,15 +94,13 @@ func CanCommunicate(callerID, targetID string) bool {
return false
}
// Siblings — same parent (including root-level where both have no parent)
// Siblings — same parent. Root-level workspaces in different orgs
// are NOT siblings (they have no shared parent_id because each org
// root is its own tree; parent_id IS NULL does not mean "same org").
if caller.ParentID != nil && target.ParentID != nil &&
*caller.ParentID == *target.ParentID {
return true
}
// Root-level siblings — both have no parent
if caller.ParentID == nil && target.ParentID == nil {
return true
}
// Direct parent → child (fast path; avoids the ancestor walk)
if target.ParentID != nil && caller.ID == *target.ParentID {
@@ -63,14 +63,15 @@ func TestCanCommunicate_Siblings(t *testing.T) {
}
}
func TestCanCommunicate_RootSiblings(t *testing.T) {
func TestCanCommunicate_Denied_CrossTenantRoots(t *testing.T) {
mock := setupMockDB(t)
// Both at root level (no parent)
// Two different org roots (both parent_id IS NULL) must NOT
// communicate — parent_id IS NULL does not mean "same org".
expectLookup(mock, "ws-a", nil)
expectLookup(mock, "ws-b", nil)
if !CanCommunicate("ws-a", "ws-b") {
t.Error("root-level siblings should communicate")
if CanCommunicate("ws-a", "ws-b") {
t.Error("cross-tenant org roots should NOT communicate")
}
}