package virtualfs import ( "context" "database/sql" "errors" "github.com/google/uuid" "github.com/uptrace/bun" ) // nodeWithinRootQuery checks if a node is within a root by traversing ancestors. const nodeWithinRootQuery = `WITH RECURSIVE path AS ( SELECT id, parent_id FROM vfs_nodes WHERE id = ? AND deleted_at IS NULL UNION ALL SELECT n.id, n.parent_id FROM vfs_nodes n JOIN path p ON n.id = p.parent_id WHERE n.deleted_at IS NULL ) SELECT 1 FROM path WHERE id = ? LIMIT 1;` // nodeWithinRootOrAllowedQuery checks if a node is: // 1. Within the root node (has root in its ancestry) // 2. Either directly in the allowed list, OR has an ancestor in the allowed list // This combines two checks into a single recursive query. const nodeWithinRootOrAllowedQuery = `WITH RECURSIVE path AS ( SELECT id, parent_id FROM vfs_nodes WHERE id = ? AND deleted_at IS NULL UNION ALL SELECT n.id, n.parent_id FROM vfs_nodes n JOIN path p ON n.id = p.parent_id WHERE n.deleted_at IS NULL ) SELECT EXISTS(SELECT 1 FROM path WHERE id = ?) AS within_root, EXISTS(SELECT 1 FROM path WHERE id IN (?)) AS within_allowed;` // filterNodesWithAllowlistQuery filters multiple nodes in a single query. // It returns node IDs that are within the root AND have an ancestor (or self) in the allowed list. const filterNodesWithAllowlistQuery = `WITH RECURSIVE node_paths AS ( -- Start from all input nodes SELECT id AS original_id, id, parent_id FROM vfs_nodes WHERE id IN (?) AND deleted_at IS NULL UNION ALL -- Traverse up to ancestors SELECT np.original_id, n.id, n.parent_id FROM vfs_nodes n JOIN node_paths np ON n.id = np.parent_id WHERE n.deleted_at IS NULL ) SELECT DISTINCT original_id FROM node_paths WHERE original_id IN ( -- Nodes that have root in their path SELECT original_id FROM node_paths WHERE id = ? ) AND original_id IN ( -- Nodes that have an allowed node in their path SELECT original_id FROM node_paths WHERE id IN (?) );` // filterNodesWithinRootQuery filters multiple nodes that are within the root (no allowlist). const filterNodesWithinRootQuery = `WITH RECURSIVE node_paths AS ( SELECT id AS original_id, id, parent_id FROM vfs_nodes WHERE id IN (?) AND deleted_at IS NULL UNION ALL SELECT np.original_id, n.id, n.parent_id FROM vfs_nodes n JOIN node_paths np ON n.id = np.parent_id WHERE n.deleted_at IS NULL ) SELECT DISTINCT original_id FROM node_paths WHERE id = ?;` func isScopeSet(scope *Scope) bool { return scope != nil && scope.DriveID != uuid.Nil && scope.RootNodeID != uuid.Nil } // canAccessNode checks if the scope permits the operation and allows access to the node. func (vfs *VirtualFS) canAccessNode(ctx context.Context, db bun.IDB, scope *Scope, op Operation, nodeID uuid.UUID) (bool, error) { if !scope.Allows(op) { return false, nil } return vfs.isNodeAllowedByScope(ctx, db, scope, nodeID) } func (vfs *VirtualFS) isNodeWithinRoot(ctx context.Context, db bun.IDB, nodeID, rootID uuid.UUID) (bool, error) { var exists int err := db.NewRaw(nodeWithinRootQuery, nodeID, rootID).Scan(ctx, &exists) if err != nil { if errors.Is(err, sql.ErrNoRows) { return false, nil } return false, err } return true, nil } // isNodeAllowedByScope checks if a node is accessible under the given scope. // It verifies the node is within the root and (if an allowlist exists) within an allowed subtree. // Optimized to use a single query when an allowlist is present. func (vfs *VirtualFS) isNodeAllowedByScope(ctx context.Context, db bun.IDB, scope *Scope, nodeID uuid.UUID) (bool, error) { if !isScopeSet(scope) { return false, nil } // Fast path: no allowlist means full subtree access under root if len(scope.AllowedNodes) == 0 { return vfs.isNodeWithinRoot(ctx, db, nodeID, scope.RootNodeID) } // Quick check: if nodeID is directly in the allowlist, just check root containment if _, ok := scope.AllowedNodes[nodeID]; ok { return vfs.isNodeWithinRoot(ctx, db, nodeID, scope.RootNodeID) } // Single query: build ancestry path and check both root and allowlist membership allowedIDs := scopeAllowedNodesList(scope) var result struct { WithinRoot bool `bun:"within_root"` WithinAllowed bool `bun:"within_allowed"` } err := db.NewRaw(nodeWithinRootOrAllowedQuery, nodeID, scope.RootNodeID, bun.In(allowedIDs)).Scan(ctx, &result) if err != nil { if errors.Is(err, sql.ErrNoRows) { return false, nil } return false, err } return result.WithinRoot && result.WithinAllowed, nil } // filterNodesByScope filters a list of nodes to only those accessible under the scope. // Optimized to use a single batch query instead of per-node queries. func (vfs *VirtualFS) filterNodesByScope(ctx context.Context, db bun.IDB, scope *Scope, nodes []*Node) ([]*Node, error) { if len(nodes) == 0 { return nodes, nil } if !isScopeSet(scope) { return nil, nil } nodeIDs := make([]uuid.UUID, len(nodes)) nodeMap := make(map[uuid.UUID]*Node, len(nodes)) for i, node := range nodes { nodeIDs[i] = node.ID nodeMap[node.ID] = node } var allowedIDs []uuid.UUID var err error if len(scope.AllowedNodes) == 0 { // No allowlist: just check nodes are within root err = db.NewRaw(filterNodesWithinRootQuery, bun.In(nodeIDs), scope.RootNodeID).Scan(ctx, &allowedIDs) } else { // With allowlist: check nodes are within root AND within an allowed subtree allowedNodesList := scopeAllowedNodesList(scope) err = db.NewRaw(filterNodesWithAllowlistQuery, bun.In(nodeIDs), scope.RootNodeID, bun.In(allowedNodesList)).Scan(ctx, &allowedIDs) } if err != nil { if errors.Is(err, sql.ErrNoRows) { return make([]*Node, 0), nil } return nil, err } allowed := make([]*Node, 0, len(allowedIDs)) for _, id := range allowedIDs { if node, ok := nodeMap[id]; ok { allowed = append(allowed, node) } } return allowed, nil } // scopeAllowedNodesList converts the AllowedNodes map to a slice for use in queries. func scopeAllowedNodesList(scope *Scope) []uuid.UUID { if scope == nil || len(scope.AllowedNodes) == 0 { return nil } ids := make([]uuid.UUID, 0, len(scope.AllowedNodes)) for id := range scope.AllowedNodes { ids = append(ids, id) } return ids }