2 Commits

Author SHA1 Message Date
63044a697d feat: change node ownership to namespace instead of creator
Some checks failed
Build and Publish APK Package / build-apk (amd64, x86_64) (push) Successful in 44s
Build and Publish APK Package / build-apk (arm64, aarch64) (push) Successful in 51s
Build and Publish Arch Package / build-arch (amd64, x86_64) (push) Successful in 58s
Build and Publish Arch Package / build-arch (arm64, aarch64) (push) Successful in 1m5s
Build and Push Docker Container / build-and-push (push) Failing after 10m5s
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 <noreply@anthropic.com>
2026-04-02 05:41:13 +02:00
ea8a9ca0c3 feat: add global user namespace for shared access
Create a special _global namespace that every new user automatically
gets can_create_rel access to (inclusive of can_read). This provides
a shared space where all users can see and interact with published nodes.

- ensureGlobalNamespace creates _global on first use with self-ownership
- ensureUser grants can_create_rel to each new user on the global namespace
- User visibility still relies on existing post-BFS global readability

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-04-02 05:31:04 +02:00
2 changed files with 77 additions and 5 deletions

View File

@@ -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)
}
})

View File

@@ -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
}
@@ -810,6 +822,37 @@ func (s *nodeServiceImpl) resolveUserRef(st store.GraphStore, ref string) (strin
return s.ensureUser(st, ref)
}
const globalNamespace = "_global"
func (s *nodeServiceImpl) ensureGlobalNamespace(st store.GraphStore) (string, error) {
nsID, err := s.resolveIDByNameAndType(st, globalNamespace, "namespace")
if err != nil {
return "", err
}
if nsID != "" {
return nsID, nil
}
id, err := st.GenerateID()
if err != nil {
return "", err
}
now := time.Now().UTC().Format(time.RFC3339)
if err := st.AddNode(id, globalNamespace, "", nil, now, now); err != nil {
return "", err
}
if err := st.AddRel(id, "_type::namespace", ""); err != nil {
return "", err
}
if err := st.AddRel(id, string(models.RelInNamespace), id); err != nil {
return "", err
}
// Self-owned so no single user controls it.
if err := st.AddRel(id, string(models.RelHasOwnership), id); err != nil {
return "", err
}
return id, nil
}
func (s *nodeServiceImpl) ensureUser(st store.GraphStore, username string) (string, error) {
userID, err := s.resolveIDByNameAndType(st, username, "user")
if err != nil {
@@ -833,6 +876,17 @@ func (s *nodeServiceImpl) ensureUser(st store.GraphStore, username string) (stri
if err := st.AddRel(id, string(models.RelHasOwnership), id); err != nil {
return "", err
}
// Every user gets can_create_rel access to the global namespace (inclusive
// of can_read), providing a shared space where all users can see and interact with
// nodes that are published there. User nodes themselves are already
// globally readable via the post-BFS identity override in getPermContext.
globalNsID, err := s.ensureGlobalNamespace(st)
if err != nil {
return "", err
}
if err := st.AddRel(id, string(models.RelCanCreateRel), globalNsID); err != nil {
return "", err
}
return id, nil
}