Unify auth middleware and remove X-User-ID trust, add project_members authorization, sandbox workspaces via resolver, and admin-only reconcile by run_id. Introduce goose migrations with startup schema gate, fail closed when DB unavailable outside explicit ALLOW_IN_MEMORY, bundle web/dist in app Dockerfile, and remove frontend pseudo-login fallback.
81 lines
2.0 KiB
Go
81 lines
2.0 KiB
Go
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
var (
|
|
ErrPathEscape = errors.New("workspace: path escapes data root")
|
|
ErrWorkDirNotFound = errors.New("workspace: work directory not found")
|
|
)
|
|
|
|
// Resolver 将逻辑 ID 解析为受约束的工作区路径。
|
|
type Resolver struct {
|
|
dataRoot string
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewResolver(dataRoot string, pool *pgxpool.Pool) *Resolver {
|
|
return &Resolver{dataRoot: dataRoot, pool: pool}
|
|
}
|
|
|
|
// EnsureUnderDataRoot 校验绝对路径位于 dataRoot 内。
|
|
func (r *Resolver) EnsureUnderDataRoot(path string) (string, error) {
|
|
if strings.TrimSpace(path) == "" {
|
|
return "", fmt.Errorf("workspace: empty path")
|
|
}
|
|
abs, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
root, err := filepath.Abs(r.dataRoot)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rel, err := filepath.Rel(root, abs)
|
|
if err != nil {
|
|
return "", ErrPathEscape
|
|
}
|
|
if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
|
return "", ErrPathEscape
|
|
}
|
|
return abs, nil
|
|
}
|
|
|
|
// ResolveDocWorkDir 从 document_records 读取 work_dir 并沙箱校验。
|
|
func (r *Resolver) ResolveDocWorkDir(ctx context.Context, docID string) (string, error) {
|
|
docID = strings.TrimSpace(docID)
|
|
if docID == "" {
|
|
return "", fmt.Errorf("workspace: doc_id required")
|
|
}
|
|
if r.pool == nil {
|
|
return "", ErrWorkDirNotFound
|
|
}
|
|
var workDir string
|
|
err := r.pool.QueryRow(ctx, `
|
|
SELECT COALESCE(work_dir, '')
|
|
FROM document_records
|
|
WHERE doc_id = $1
|
|
`, docID).Scan(&workDir)
|
|
if err != nil || strings.TrimSpace(workDir) == "" {
|
|
return "", ErrWorkDirNotFound
|
|
}
|
|
return r.EnsureUnderDataRoot(workDir)
|
|
}
|
|
|
|
// ResolveStoredWorkDir 沙箱校验已持久化的工作区路径(如 task run workspace_id)。
|
|
func (r *Resolver) ResolveStoredWorkDir(storedPath string) (string, error) {
|
|
storedPath = strings.TrimSpace(storedPath)
|
|
if storedPath == "" {
|
|
return "", ErrWorkDirNotFound
|
|
}
|
|
return r.EnsureUnderDataRoot(storedPath)
|
|
}
|