From 63044a697d168cbc63776dfcfd5c14dad7bd5960 Mon Sep 17 00:00:00 2001 From: Elias Kohout Date: Thu, 2 Apr 2026 05:41:13 +0200 Subject: [PATCH] feat: change node ownership to namespace instead of creator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a node is created in a namespace, the namespace now owns it rather than the creator. This allows namespaces to manage content ownership. Namespace nodes themselves remain owned by their creator. Users retain transitive ownership through their default namespace: user→has_ownership→namespace→has_ownership→node. Co-Authored-By: Claude Haiku 4.5 --- src/e2e/e2e_permissions_test.go | 22 ++++++++++++++++++++-- src/service/node_service_impl.go | 18 +++++++++++++++--- 2 files changed, 35 insertions(+), 5 deletions(-) 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 }