Skip to content

Commit d9fb45b

Browse files
authored
bypass rbac for signed url upload (#2395)
* signed url change * add handler for signed urls * restore function call
1 parent f56595a commit d9fb45b

File tree

6 files changed

+65
-63
lines changed

6 files changed

+65
-63
lines changed

taco/cmd/statesman/main.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,13 @@ func main() {
201201

202202
// Register routes with interface-based dependencies
203203
api.RegisterRoutes(e, api.Dependencies{
204-
Repository: fullRepo, // RBAC-wrapped repository (used by all routes)
205-
BlobStore: blobStore, // Direct blob access (for legacy components)
206-
QueryStore: queryStore, // Direct query access
207-
RBACManager: rbacManager, // RBAC management
208-
Signer: signer, // JWT signing
209-
AuthEnabled: !*authDisable, // Auth flag
204+
Repository: fullRepo, // RBAC-wrapped repository (used by authenticated routes)
205+
UnwrappedRepository: repo, // Unwrapped repository (for pre-authorized operations like signed URLs)
206+
BlobStore: blobStore, // Direct blob access (for legacy components)
207+
QueryStore: queryStore, // Direct query access
208+
RBACManager: rbacManager, // RBAC management
209+
Signer: signer, // JWT signing
210+
AuthEnabled: !*authDisable, // Auth flag
210211
})
211212

212213
// Start server

taco/internal/api/internal.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ func RegisterInternalRoutes(e *echo.Echo, deps Dependencies) {
149149
}
150150

151151
// Create TFE handler with webhook auth context
152-
tfeHandler := tfe.NewTFETokenHandler(authHandler, deps.Repository, deps.BlobStore, deps.RBACManager, tfeIdentifierResolver)
152+
// Pass both wrapped (for authenticated calls) and unwrapped (for signed URLs) repositories
153+
tfeHandler := tfe.NewTFETokenHandler(authHandler, deps.Repository, deps.UnwrappedRepository, deps.BlobStore, deps.RBACManager, tfeIdentifierResolver)
153154

154155
// TFE group with webhook auth (for UI pass-through)
155156
tfeInternal := e.Group("/internal/tfe/api/v2")

taco/internal/api/routes.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ import (
2929
// Dependencies holds all the interface-based dependencies for routes.
3030
// This uses interface segregation - each handler gets ONLY what it needs.
3131
type Dependencies struct {
32-
Repository domain.UnitRepository // RBAC-wrapped repository (used by all routes)
33-
BlobStore storage.UnitStore // Direct blob access (for legacy components like API tokens)
34-
QueryStore query.Store // Direct query access (analytics, RBAC)
35-
RBACManager *rbac.RBACManager // RBAC management (RBAC routes only)
36-
Signer *authpkg.Signer // JWT signing (auth, middleware)
37-
AuthEnabled bool // Whether auth is enabled
32+
Repository domain.UnitRepository // RBAC-wrapped repository (used by all routes)
33+
UnwrappedRepository domain.UnitRepository // Unwrapped repository (for pre-authorized operations like signed URLs)
34+
BlobStore storage.UnitStore // Direct blob access (for legacy components like API tokens)
35+
QueryStore query.Store // Direct query access (analytics, RBAC)
36+
RBACManager *rbac.RBACManager // RBAC management (RBAC routes only)
37+
Signer *authpkg.Signer // JWT signing (auth, middleware)
38+
AuthEnabled bool // Whether auth is enabled
3839
}
3940

4041
// RegisterRoutes registers all API routes with interface-scoped dependencies.
@@ -244,16 +245,17 @@ func RegisterRoutes(e *echo.Echo, deps Dependencies) {
244245
v1.DELETE("/rbac/permissions/:id", rbacHandler.DeletePermission)
245246
v1.POST("/rbac/test", rbacHandler.TestPermissions)
246247

247-
// TFE api - inject auth handler, full repository, blob store for tokens, and RBAC dependencies
248+
// TFE api - inject auth handler, wrapped & unwrapped repositories, blob store for tokens, and RBAC dependencies
248249
// TFE handler scopes to TFEOperations internally but needs blob store for API token storage
250+
// Unwrapped repository is used for signed URL operations (pre-authorized, no RBAC checks needed)
249251
// Create identifier resolver for org resolution
250252
var tfeIdentifierResolver domain.IdentifierResolver
251253
if deps.QueryStore != nil {
252254
if db := repositories.GetDBFromQueryStore(deps.QueryStore); db != nil {
253255
tfeIdentifierResolver = repositories.NewIdentifierResolver(db)
254256
}
255257
}
256-
tfeHandler := tfe.NewTFETokenHandler(authHandler, deps.Repository, deps.BlobStore, deps.RBACManager, tfeIdentifierResolver)
258+
tfeHandler := tfe.NewTFETokenHandler(authHandler, deps.Repository, deps.UnwrappedRepository, deps.BlobStore, deps.RBACManager, tfeIdentifierResolver)
257259

258260
// Create protected TFE group - opaque tokens only
259261
tfeGroup := e.Group("/tfe/api/v2")

taco/internal/storage/s3store.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -293,55 +293,81 @@ func (s *s3Store) Download(ctx context.Context, id string) ([]byte, error) {
293293
}
294294

295295
func (s *s3Store) Upload(ctx context.Context, id string, data []byte, lockID string) error {
296+
fmt.Printf("[S3Store.Upload] START - id=%s, dataLen=%d, lockID=%s\n", id, len(data), lockID)
297+
296298
meta, err := s.Get(ctx, id)
297299
if err != nil {
300+
fmt.Printf("[S3Store.Upload] Get failed: %v\n", err)
298301
return err
299302
}
303+
fmt.Printf("[S3Store.Upload] Current meta - Size=%d, Locked=%t\n", meta.Size, meta.Locked)
304+
300305
// Lock checks
301306
if lockID != "" && meta.LockInfo != nil && meta.LockInfo.ID != lockID {
307+
fmt.Printf("[S3Store.Upload] Lock conflict: provided lockID=%s, current lockID=%s\n", lockID, meta.LockInfo.ID)
302308
return ErrLockConflict
303309
}
304310
if lockID == "" && meta.Locked {
311+
fmt.Printf("[S3Store.Upload] Lock conflict: no lockID provided but state is locked\n")
305312
return ErrLockConflict
306313
}
307314

308315
// Archive current tfstate if it exists and has content
309316
if meta.Size > 0 {
317+
fmt.Printf("[S3Store.Upload] ARCHIVING: Current state size=%d, creating version...\n", meta.Size)
318+
310319
// Download current tfstate to get its content for hashing
311320
currentData, err := s.Download(ctx, id)
312321
if err != nil {
322+
fmt.Printf("[S3Store.Upload] ARCHIVING FAILED: Could not download current state: %v\n", err)
313323
return fmt.Errorf("failed to read current state for archiving: %w", err)
314324
}
325+
fmt.Printf("[S3Store.Upload] ARCHIVING: Downloaded %d bytes\n", len(currentData))
315326

316327
// Generate version key with hash of current content
317328
timestamp := time.Now().UTC()
318329
versionKey := s.versionKeyWithHash(id, timestamp, currentData)
330+
fmt.Printf("[S3Store.Upload] ARCHIVING: Version key=%s\n", versionKey)
319331

320332
// Copy current to versioned location
333+
sourceKey := s.objKey(id)
334+
copySource := fmt.Sprintf("%s/%s", s.bucket, sourceKey)
335+
fmt.Printf("[S3Store.Upload] ARCHIVING: CopyObject from=%s to=%s\n", copySource, versionKey)
336+
321337
_, err = s.client.CopyObject(ctx, &s3.CopyObjectInput{
322338
Bucket: &s.bucket,
323339
Key: aws.String(versionKey),
324-
CopySource: aws.String(fmt.Sprintf("%s/%s", s.bucket, s.objKey(id))),
340+
CopySource: aws.String(copySource),
325341
})
326342
if err != nil {
343+
fmt.Printf("[S3Store.Upload] ARCHIVING FAILED: CopyObject error: %v\n", err)
327344
return fmt.Errorf("failed to archive current version: %w", err)
328345
}
346+
fmt.Printf("[S3Store.Upload] ARCHIVING SUCCESS: Version created at %s\n", versionKey)
329347

330348
// Clean up old versions after successful archiving
331349
if err := s.cleanupOldVersions(ctx, id); err != nil {
332-
fmt.Printf("Warning: failed to cleanup old versions for %s: %v\n", id, err)
350+
fmt.Printf("[S3Store.Upload] Warning: failed to cleanup old versions for %s: %v\n", id, err)
333351
}
352+
} else {
353+
fmt.Printf("[S3Store.Upload] SKIPPING ARCHIVE: Current state size is 0 (first upload or empty state)\n")
334354
}
335355

336356
// Upload new tfstate
357+
newKey := s.objKey(id)
358+
fmt.Printf("[S3Store.Upload] Uploading new state to key=%s, size=%d bytes\n", newKey, len(data))
359+
337360
if _, err := s.client.PutObject(ctx, &s3.PutObjectInput{
338361
Bucket: &s.bucket,
339-
Key: aws.String(s.objKey(id)),
362+
Key: aws.String(newKey),
340363
Body: bytes.NewReader(data),
341364
ContentType: aws.String("application/json"),
342365
}); err != nil {
366+
fmt.Printf("[S3Store.Upload] Upload failed: %v\n", err)
343367
return err
344368
}
369+
370+
fmt.Printf("[S3Store.Upload] SUCCESS: New state uploaded\n")
345371
return nil
346372
}
347373

taco/internal/tfe/tfe.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,24 @@ import (
1010
// TfeHandler implements Terraform Cloud/Enterprise API.
1111
// Uses TFEOperations interface (6 methods) - cannot create, list, delete, or manage versions.
1212
type TfeHandler struct {
13-
authHandler *auth.Handler
14-
stateStore domain.TFEOperations // Scoped to TFE operations
15-
rbacManager *rbac.RBACManager
16-
apiTokens *auth.APITokenManager
13+
authHandler *auth.Handler
14+
stateStore domain.TFEOperations // RBAC-wrapped for authenticated operations
15+
directStateStore domain.TFEOperations // Direct access for pre-authorized operations (signed URLs)
16+
rbacManager *rbac.RBACManager
17+
apiTokens *auth.APITokenManager
1718
identifierResolver domain.IdentifierResolver // For resolving org external IDs
1819
}
1920

2021
// NewTFETokenHandler creates a new TFE handler.
21-
// Accepts full repository for state ops and blob store for API token storage.
22-
func NewTFETokenHandler(authHandler *auth.Handler, fullRepo domain.UnitRepository, blobStore storage.UnitStore, rbacManager *rbac.RBACManager, identifierResolver domain.IdentifierResolver) *TfeHandler {
23-
// Scope to TFE operations for state management (handler only uses Get, Upload, Lock methods)
24-
stateStore := domain.TFEOperations(fullRepo)
25-
22+
// Accepts wrapped (RBAC-enforced) and unwrapped (direct) repositories.
23+
// The unwrapped repository is used for signed URL operations which are pre-authorized.
24+
func NewTFETokenHandler(authHandler *auth.Handler, wrappedRepo domain.UnitRepository, unwrappedRepo domain.UnitRepository, blobStore storage.UnitStore, rbacManager *rbac.RBACManager, identifierResolver domain.IdentifierResolver) *TfeHandler {
2625
return &TfeHandler{
27-
authHandler: authHandler,
28-
stateStore: stateStore,
29-
rbacManager: rbacManager,
30-
apiTokens: auth.NewAPITokenManagerFromStore(blobStore), // Use blob store for token storage
26+
authHandler: authHandler,
27+
stateStore: domain.TFEOperations(wrappedRepo), // Use RBAC wrapper for authenticated calls
28+
directStateStore: domain.TFEOperations(unwrappedRepo), // Bypass RBAC for signed URLs
29+
rbacManager: rbacManager,
30+
apiTokens: auth.NewAPITokenManagerFromStore(blobStore),
3131
identifierResolver: identifierResolver,
3232
}
3333
}

taco/internal/tfe/workspaces.go

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,21 +1178,8 @@ func (h *TfeHandler) DownloadStateVersion(c echo.Context) error {
11781178

11791179
// UploadStateVersion handles PUT /tfe/api/v2/state-versions/:id/upload
11801180
func (h *TfeHandler) UploadStateVersion(c echo.Context) error {
1181-
fmt.Printf("UploadStateVersion: START - Method=%s, URI=%s\n", c.Request().Method, c.Request().RequestURI)
1182-
1183-
// Debug: Check if Authorization header is present
1184-
authHeader := c.Request().Header.Get("Authorization")
1185-
fmt.Printf("UploadStateVersion: Authorization header present: %t\n", authHeader != "")
1186-
if authHeader != "" {
1187-
// Don't log the full token for security, just whether it looks like a Bearer token
1188-
fmt.Printf("UploadStateVersion: Authorization header format: %s\n",
1189-
strings.SplitN(authHeader, " ", 2)[0])
1190-
}
1191-
11921181
stateVersionID := c.Param("id")
1193-
fmt.Printf("UploadStateVersion: stateVersionID=%s\n", stateVersionID)
11941182
if stateVersionID == "" {
1195-
fmt.Printf("UploadStateVersion: ERROR - state_version_id required\n")
11961183
return c.JSON(400, map[string]string{"error": "state_version_id required"})
11971184
}
11981185

@@ -1209,47 +1196,37 @@ func (h *TfeHandler) UploadStateVersion(c echo.Context) error {
12091196
if err := h.checkWorkspacePermission(c, "unit.write", workspaceID); err != nil {
12101197
// Only enforce RBAC if we have a real auth error, not just missing headers
12111198
if !strings.Contains(err.Error(), "no authorization header") {
1212-
fmt.Printf("UploadStateVersion: RBAC permission denied: %v\n", err)
12131199
return c.JSON(http.StatusForbidden, map[string]string{
12141200
"error": "insufficient permissions to upload state",
12151201
"hint": "contact your administrator to grant unit.write permission",
12161202
})
12171203
}
1218-
// If no auth header, allow but log for security monitoring
1219-
fmt.Printf("UploadStateVersion: No auth header - allowing upload based on lock validation\n")
12201204
}
12211205

12221206
// Read the state data from request body
12231207
stateData, err := io.ReadAll(c.Request().Body)
1224-
fmt.Printf("UploadStateVersion: Read %d bytes from body, err=%v\n", len(stateData), err)
12251208
if err != nil {
1226-
fmt.Printf("UploadStateVersion: ERROR - Failed to read state data: %v\n", err)
12271209
return c.JSON(400, map[string]string{"error": "Failed to read state data"})
12281210
}
1229-
if len(stateData) > 0 {
1230-
fmt.Printf("UploadStateVersion: Body preview: %s\n", string(stateData))
1231-
}
12321211

12331212
// Extract unit UUID from state ID - repository expects just the UUID
12341213
unitUUID := extractUnitUUID(stateID)
1235-
fmt.Printf("UploadStateVersion: Extracted unitUUID=%s from stateID=%s\n", unitUUID, stateID)
12361214

1215+
// Use directStateStore for signed URL operations (pre-authorized, no RBAC checks)
12371216
// Check if state exists (no auto-creation)
1238-
_, err = h.stateStore.Get(c.Request().Context(), unitUUID)
1217+
_, err = h.directStateStore.Get(c.Request().Context(), unitUUID)
12391218
if err == storage.ErrNotFound {
1240-
fmt.Printf("UploadStateVersion: Unit not found - no auto-creation\n")
12411219
return c.JSON(404, map[string]string{
12421220
"error": "Unit not found. Please create the unit first using 'taco unit create " + unitUUID + "' or the opentaco_unit Terraform resource.",
12431221
})
12441222
} else if err != nil {
1245-
fmt.Printf("UploadStateVersion: ERROR - Failed to check state existence: %v\n", err)
12461223
return c.JSON(500, map[string]string{
12471224
"error": "Failed to check state existence",
12481225
})
12491226
}
12501227

12511228
// Get the current lock to extract lock ID for state upload
1252-
currentLock, lockErr := h.stateStore.GetLock(c.Request().Context(), unitUUID)
1229+
currentLock, lockErr := h.directStateStore.GetLock(c.Request().Context(), unitUUID)
12531230
if lockErr != nil && lockErr != storage.ErrNotFound {
12541231
return c.JSON(500, map[string]string{"error": "Failed to get lock status"})
12551232
}
@@ -1261,23 +1238,18 @@ func (h *TfeHandler) UploadStateVersion(c echo.Context) error {
12611238
}
12621239

12631240
// Upload the state with proper lock ID
1264-
fmt.Printf("UploadStateVersion: Uploading to storage with lockID=%s\n", lockID)
1265-
err = h.stateStore.Upload(c.Request().Context(), unitUUID, stateData, lockID)
1266-
fmt.Printf("UploadStateVersion: Upload result, err=%v\n", err)
1241+
err = h.directStateStore.Upload(c.Request().Context(), unitUUID, stateData, lockID)
12671242
if err != nil {
12681243
if err == storage.ErrLockConflict {
1269-
fmt.Printf("UploadStateVersion: ERROR - Workspace is locked\n")
12701244
return c.JSON(423, map[string]string{
12711245
"error": "Workspace is locked",
12721246
})
12731247
}
1274-
fmt.Printf("UploadStateVersion: ERROR - Failed to upload state: %v\n", err)
12751248
return c.JSON(500, map[string]string{
12761249
"error": "Failed to upload state",
12771250
})
12781251
}
12791252

1280-
fmt.Printf("UploadStateVersion: SUCCESS - State uploaded successfully\n")
12811253
// Return 204 No Content as expected by Terraform
12821254
return c.NoContent(204)
12831255
}

0 commit comments

Comments
 (0)