Skip to content

Commit c756d07

Browse files
authored
signed url download (#2402)
* signed url download * exclude download path * direct state store
1 parent 2e4fae4 commit c756d07

File tree

3 files changed

+20
-8
lines changed

3 files changed

+20
-8
lines changed

taco/internal/api/routes.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,14 +274,14 @@ func RegisterRoutes(e *echo.Echo, deps Dependencies) {
274274
tfeGroup.POST("/workspaces/:workspace_id/actions/force-unlock", tfeHandler.ForceUnlockWorkspace)
275275
tfeGroup.GET("/workspaces/:workspace_id/current-state-version", tfeHandler.GetCurrentStateVersion)
276276
tfeGroup.POST("/workspaces/:workspace_id/state-versions", tfeHandler.CreateStateVersion)
277-
tfeGroup.GET("/state-versions/:id/download", tfeHandler.DownloadStateVersion)
278277
tfeGroup.GET("/state-versions/:id", tfeHandler.ShowStateVersion)
279278

280-
// Upload endpoints exempt from auth middleware (Terraform doesn't send auth headers)
281-
// Security: These validate lock ownership and have RBAC checks in handlers
282-
// Upload URLs can only be obtained from authenticated CreateStateVersion calls
279+
// Upload/Download endpoints use signed URLs instead of auth middleware
280+
// Reason: Terraform 1.5.x doesn't send Authorization headers for downloads
281+
// Security: URLs are signed with expiration and can only be obtained from authenticated calls
283282
tfeSignedUrlsGroup := e.Group("/tfe/api/v2")
284283
tfeSignedUrlsGroup.Use(middleware.VerifySignedURL)
284+
tfeSignedUrlsGroup.GET("/state-versions/:id/download", tfeHandler.DownloadStateVersion)
285285
tfeSignedUrlsGroup.PUT("/state-versions/:id/upload", tfeHandler.UploadStateVersion)
286286
tfeSignedUrlsGroup.PUT("/state-versions/:id/json-upload", tfeHandler.UploadJSONStateOutputs)
287287

taco/internal/tfe/workspaces.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -864,7 +864,11 @@ func (h *TfeHandler) GetCurrentStateVersion(c echo.Context) error {
864864
stateVersionID := generateStateVersionID(stateID, stateMeta.Updated.Unix())
865865

866866
baseURL := getBaseURL(c)
867-
downloadURL := fmt.Sprintf("%s/tfe/api/v2/state-versions/%s/download", baseURL, stateVersionID)
867+
// Sign the download URL for Terraform 1.5.x compatibility (doesn't send auth headers)
868+
downloadURL, err := auth.SignURL(baseURL, fmt.Sprintf("/tfe/api/v2/state-versions/%s/download", stateVersionID), time.Now().Add(10*time.Minute))
869+
if err != nil {
870+
return c.JSON(500, map[string]string{"error": "Failed to sign download URL"})
871+
}
868872

869873
// Return current state version info
870874
return c.JSON(200, map[string]interface{}{
@@ -1174,7 +1178,7 @@ func (h *TfeHandler) DownloadStateVersion(c echo.Context) error {
11741178
unitUUID := extractUnitUUID(stateID)
11751179

11761180
// Download the state data
1177-
stateData, err := h.stateStore.Download(c.Request().Context(), unitUUID)
1181+
stateData, err := h.directStateStore.Download(c.Request().Context(), unitUUID)
11781182
if err != nil {
11791183
if err == storage.ErrNotFound {
11801184
return c.JSON(404, map[string]string{"error": "State version not found"})
@@ -1359,7 +1363,11 @@ func (h *TfeHandler) ShowStateVersion(c echo.Context) error {
13591363
}
13601364

13611365
baseURL := getBaseURL(c)
1362-
downloadURL := fmt.Sprintf("%s/tfe/api/v2/state-versions/%s/download", baseURL, id)
1366+
// Sign the download URL for Terraform 1.5.x compatibility
1367+
downloadURL, err := auth.SignURL(baseURL, fmt.Sprintf("/tfe/api/v2/state-versions/%s/download", id), time.Now().Add(10*time.Minute))
1368+
if err != nil {
1369+
return c.JSON(500, map[string]string{"error": "Failed to sign download URL"})
1370+
}
13631371

13641372
resp := map[string]interface{}{
13651373
"data": map[string]interface{}{

ui/src/routes/tfe/$.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ async function handler({ request }) {
1717
/^\/tfe\/api\/v2\/state-versions\/[^\/]+\/upload$/.test(url.pathname) ||
1818
/^\/tfe\/api\/v2\/state-versions\/[^\/]+\/json-upload$/.test(url.pathname);
1919

20+
const isDownloadPath =
21+
/^\/tfe\/api\/v2\/state-versions\/[^\/]+\/download$/.test(url.pathname);
22+
23+
2024
// OAuth and upload paths: forward directly to public statesman endpoints
21-
if (isOAuthPath || isUploadPath) {
25+
if (isOAuthPath || isUploadPath || isDownloadPath) {
2226
const outgoingHeaders = new Headers(request.headers);
2327
const originalHost = outgoingHeaders.get('host') ?? '';
2428
if (originalHost) outgoingHeaders.set('x-forwarded-host', originalHost);

0 commit comments

Comments
 (0)