diff --git a/src/e2e/e2e_permissions_test.go b/src/e2e/e2e_permissions_test.go index ec101e0..e4dccf5 100644 --- a/src/e2e/e2e_permissions_test.go +++ b/src/e2e/e2e_permissions_test.go @@ -117,8 +117,26 @@ func TestPermissions(t *testing.T) { if !userNode.HasRelation("has_ownership", aliceUserID) { t.Errorf("expected user node to have self-ownership, got relations: %v", userNode.Relations) } - if !userNode.HasRelation("has_ownership", aliceNodeID) { - t.Errorf("expected alice to own her node, got relations: %v", userNode.Relations) + // Nodes are now owned by the namespace they belong to, not directly by the creator. + // Alice's default namespace is owned by alice, so she retains transitive ownership. + if userNode.HasRelation("has_ownership", aliceNodeID) { + t.Errorf("alice should not directly own her node (namespace owns it), got relations: %v", userNode.Relations) + } + // Find alice's default namespace and verify it owns the node. + namespaces := alice.parseNodes(alice.mustAx("list", "--type", "namespace", "--json")) + var aliceNsID string + for _, ns := range namespaces { + if ns.Title == "alice" { + aliceNsID = ns.ID + } + } + if aliceNsID == "" { + t.Fatal("could not find alice's default namespace") + } + nsOut := alice.mustAx("show", aliceNsID, "--json") + nsNode := alice.parseNode(nsOut) + if !nsNode.HasRelation("has_ownership", aliceNodeID) { + t.Errorf("expected alice's namespace to own her node, got relations: %v", nsNode.Relations) } }) diff --git a/src/service/node_service_impl.go b/src/service/node_service_impl.go index 20ce1f4..1ea540c 100644 --- a/src/service/node_service_impl.go +++ b/src/service/node_service_impl.go @@ -364,8 +364,9 @@ func (s *nodeServiceImpl) Add(input AddInput) (*models.Node, error) { } } - // Edge rels. + // Edge rels. Track the namespace the node is placed in for ownership. hasCreated := false + var actualNsID string for _, ri := range input.Rels { if ri.Target == "" { continue // already stored as tag @@ -377,6 +378,9 @@ func (s *nodeServiceImpl) Add(input AddInput) (*models.Node, error) { if err != nil { return err } + if ri.Type == models.RelInNamespace { + actualNsID = resolved + } if ri.Type == models.RelHasOwnership { // Ownership transfer: remove existing owner of the target. existingOwners, _ := st.FindNodes([]*models.Rel{{Type: models.RelHasOwnership, Target: resolved}}) @@ -398,6 +402,7 @@ func (s *nodeServiceImpl) Add(input AddInput) (*models.Node, error) { if err := st.AddRel(id, string(models.RelInNamespace), nsID); err != nil { return err } + actualNsID = nsID } // Default created. @@ -411,12 +416,19 @@ func (s *nodeServiceImpl) Add(input AddInput) (*models.Node, error) { } } - // Grant creator ownership of the new node. + // Grant ownership of the new node. + // Namespace nodes are owned by their creator. All other nodes are owned + // by the namespace they belong to — the user retains transitive ownership + // through the namespace's own ownership chain (e.g. user→owns→default-ns→owns→node). creatorID, err := s.resolveUserRef(st, s.userID) if err != nil { return err } - if err := st.AddRel(creatorID, string(models.RelHasOwnership), id); err != nil { + ownerID := creatorID + if tmp.GetProperty("type") != "namespace" && actualNsID != "" { + ownerID = actualNsID + } + if err := st.AddRel(ownerID, string(models.RelHasOwnership), id); err != nil { return err }