package e2e_test import ( "testing" ) func TestPermissions(t *testing.T) { alice := newTestEnv(t, "alice") bob := alice.withUser("bob") // Alice creates a node (gets has_ownership automatically). aliceNode := alice.parseNode(alice.mustAx("add", "Alice's secret", "--json")) aliceNodeID := aliceNode.ID // Resolve alice's user node ID. var aliceUserID string for _, u := range alice.parseNodes(alice.mustAx("list", "--type", "user", "--json")) { if u.Title == "alice" { aliceUserID = u.ID } } if aliceUserID == "" { t.Fatal("could not resolve alice's user node ID") } t.Run("NoAccess_CannotShow", func(t *testing.T) { _, err := bob.ax("show", aliceNodeID) if err == nil { t.Error("bob should not be able to show alice's node without access") } }) t.Run("NoAccess_NotInList", func(t *testing.T) { bob.mustAx("add", "Bob's scratch", "--json") nodes := bob.parseNodes(bob.mustAx("list", "--json")) if _, ok := bob.findInList(nodes, aliceNodeID); ok { t.Error("alice's node should not appear in bob's list without access") } }) t.Run("NoAccess_CannotUpdate", func(t *testing.T) { _, err := bob.ax("update", aliceNodeID, "--title", "hacked") if err == nil { t.Error("bob should not be able to update alice's node without access") } }) // Resolve bob's user node ID. User nodes are globally readable. var bobUserID string for _, u := range alice.parseNodes(alice.mustAx("list", "--type", "user", "--json")) { if u.Title == "bob" { bobUserID = u.ID } } if bobUserID == "" { t.Fatal("could not resolve bob's user node ID") } t.Run("SelfEscalation_Denied", func(t *testing.T) { _, err := bob.ax("update", bobUserID, "--rel", "can_write:"+aliceNodeID) if err == nil { t.Error("bob should not be able to grant himself write access to alice's node") } }) t.Run("ReadAccess_Grant", func(t *testing.T) { alice.mustAx("update", bobUserID, "--rel", "can_read:"+aliceNodeID) if _, err := bob.ax("show", aliceNodeID); err != nil { t.Error("bob should be able to show alice's node after read access granted") } nodes := bob.parseNodes(bob.mustAx("list", "--json")) if _, ok := bob.findInList(nodes, aliceNodeID); !ok { t.Error("alice's node should appear in bob's list after read access granted") } }) t.Run("ReadAccess_CannotUpdate", func(t *testing.T) { _, err := bob.ax("update", aliceNodeID, "--title", "hacked with read access") if err == nil { t.Error("bob should not be able to update alice's node with only read access") } }) t.Run("ReadAccess_CannotAddRelationToNode", func(t *testing.T) { bobLinkedID := bob.parseNode(bob.mustAx("add", "Bob's linked node", "--json")).ID _, err := bob.ax("update", bobLinkedID, "--rel", "related:"+aliceNodeID) if err == nil { t.Error("bob should not be able to create a relation to alice's node with only read access") } }) t.Run("WriteAccess_Grant", func(t *testing.T) { alice.mustAx("update", bobUserID, "--rel", "can_write:"+aliceNodeID) out := bob.mustAx("update", aliceNodeID, "--title", "Bob modified this", "--json") n := bob.parseNode(out) if n.Title != "Bob modified this" { t.Errorf("expected 'Bob modified this', got %q", n.Title) } }) t.Run("WriteAccess_CanAddRelationToNode", func(t *testing.T) { bobNode2 := bob.parseNode(bob.mustAx("add", "Bob's related node", "--json")) bob.mustAx("update", bobNode2.ID, "--rel", "related:"+aliceNodeID) out := bob.mustAx("show", bobNode2.ID, "--json") n := bob.parseNode(out) if !n.HasRelation("related", aliceNodeID) { t.Error("expected related relation after write access granted") } }) t.Run("Ownership_DefaultOnCreate", func(t *testing.T) { userOut := alice.mustAx("show", aliceUserID, "--json") userNode := alice.parseNode(userOut) 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) } }) t.Run("Ownership_CascadeDelete", func(t *testing.T) { throwaway := alice.withUser("throwaway") child := throwaway.parseNode(throwaway.mustAx("add", "Child node", "--json")) var throwawayUserID string for _, u := range throwaway.parseNodes(throwaway.mustAx("list", "--type", "user", "--json")) { if u.Title == "throwaway" { throwawayUserID = u.ID } } if throwawayUserID == "" { t.Fatal("could not find throwaway user node") } throwaway.mustAx("del", throwawayUserID, "--force") _, err := throwaway.ax("show", child.ID) if err == nil { t.Error("child node should have been cascade-deleted when its owner was deleted") } }) } func TestNamespaceExplicitCreate(t *testing.T) { env := newTestEnv(t, "testuser") nsNode := env.parseNode(env.mustAx("add", "myworkspace", "--type", "namespace", "--json")) if !nsNode.HasRelation("in_namespace", nsNode.ID) { t.Errorf("expected namespace to have in_namespace pointing to itself, got relations: %v", nsNode.Relations) } users := env.parseNodes(env.mustAx("list", "--type", "user", "--json")) var userNode *NodeResponse for i := range users { if users[i].Title == "testuser" { userNode = &users[i] break } } if userNode == nil { t.Fatal("could not find testuser node") } if !userNode.HasRelation("has_ownership", nsNode.ID) { t.Errorf("expected creator to have has_ownership on new namespace, got relations: %v", userNode.Relations) } env.mustAx("add", "task in workspace", "--namespace", "myworkspace", "--json") listed := env.parseNodes(env.mustAx("list", "--namespace", "myworkspace", "--json")) if len(listed) == 0 { t.Error("expected to list nodes in newly created namespace") } }