2026-04-02 02:23:00 +02:00
|
|
|
package e2e_test
|
2026-04-02 01:58:48 +02:00
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
}
|