fix(auth): allow nesting + delete from tenant canvas (same-origin)

PATCH /workspaces/:id field-level auth for parent_id/tier/runtime
required a bearer token, blocking canvas nesting (drag-to-nest).
Added IsSameOriginCanvas check so the tenant canvas can update
sensitive fields without a bearer.

Exported IsSameOriginCanvas from middleware package so workspace.go
can call it for the field-level auth path.

DELETE /workspaces/:id is behind AdminAuth which already has the
same-origin check — if delete still fails, it's a different issue.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-16 11:22:45 -07:00
parent f05a986b85
commit 1949846001
2 changed files with 11 additions and 0 deletions

View File

@ -12,6 +12,7 @@ import (
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/events"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/middleware"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/models"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/provisioner"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/wsauth"
@ -488,6 +489,9 @@ func (h *WorkspaceHandler) Update(c *gin.Context) {
}
tok := wsauth.BearerTokenFromHeader(c.GetHeader("Authorization"))
if tok == "" {
if middleware.IsSameOriginCanvas(c) {
break // tenant canvas — trusted same-origin
}
c.JSON(http.StatusUnauthorized, gin.H{"error": "admin auth required for field: " + field})
return
}

View File

@ -209,6 +209,13 @@ func canvasOriginAllowed(origin string) bool {
// on every request.
var canvasProxyActive = os.Getenv("CANVAS_PROXY_URL") != ""
// IsSameOriginCanvas is the exported version for use outside the middleware
// package (e.g. workspace.go field-level auth). Same logic as the internal
// callers in AdminAuth/WorkspaceAuth/CanvasOrBearer.
func IsSameOriginCanvas(c *gin.Context) bool {
return isSameOriginCanvas(c)
}
func isSameOriginCanvas(c *gin.Context) bool {
if !canvasProxyActive {
return false