Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63044a697d | |||
| ea8a9ca0c3 |
@@ -117,8 +117,26 @@ func TestPermissions(t *testing.T) {
|
|||||||
if !userNode.HasRelation("has_ownership", aliceUserID) {
|
if !userNode.HasRelation("has_ownership", aliceUserID) {
|
||||||
t.Errorf("expected user node to have self-ownership, got relations: %v", userNode.Relations)
|
t.Errorf("expected user node to have self-ownership, got relations: %v", userNode.Relations)
|
||||||
}
|
}
|
||||||
if !userNode.HasRelation("has_ownership", aliceNodeID) {
|
// Nodes are now owned by the namespace they belong to, not directly by the creator.
|
||||||
t.Errorf("expected alice to own her node, got relations: %v", userNode.Relations)
|
// 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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
hasCreated := false
|
||||||
|
var actualNsID string
|
||||||
for _, ri := range input.Rels {
|
for _, ri := range input.Rels {
|
||||||
if ri.Target == "" {
|
if ri.Target == "" {
|
||||||
continue // already stored as tag
|
continue // already stored as tag
|
||||||
@@ -377,6 +378,9 @@ func (s *nodeServiceImpl) Add(input AddInput) (*models.Node, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if ri.Type == models.RelInNamespace {
|
||||||
|
actualNsID = resolved
|
||||||
|
}
|
||||||
if ri.Type == models.RelHasOwnership {
|
if ri.Type == models.RelHasOwnership {
|
||||||
// Ownership transfer: remove existing owner of the target.
|
// Ownership transfer: remove existing owner of the target.
|
||||||
existingOwners, _ := st.FindNodes([]*models.Rel{{Type: models.RelHasOwnership, Target: resolved}})
|
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 {
|
if err := st.AddRel(id, string(models.RelInNamespace), nsID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
actualNsID = nsID
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default created.
|
// 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)
|
creatorID, err := s.resolveUserRef(st, s.userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,6 +822,37 @@ func (s *nodeServiceImpl) resolveUserRef(st store.GraphStore, ref string) (strin
|
|||||||
return s.ensureUser(st, ref)
|
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) {
|
func (s *nodeServiceImpl) ensureUser(st store.GraphStore, username string) (string, error) {
|
||||||
userID, err := s.resolveIDByNameAndType(st, username, "user")
|
userID, err := s.resolveIDByNameAndType(st, username, "user")
|
||||||
if err != nil {
|
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 {
|
if err := st.AddRel(id, string(models.RelHasOwnership), id); err != nil {
|
||||||
return "", err
|
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
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user