Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion taco/internal/api/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,14 @@ func RegisterRoutes(e *echo.Echo, deps Dependencies) {

// TFE api - inject auth handler, full repository, blob store for tokens, and RBAC dependencies
// TFE handler scopes to TFEOperations internally but needs blob store for API token storage
tfeHandler := tfe.NewTFETokenHandler(authHandler, deps.Repository, deps.BlobStore, deps.RBACManager)
// Create identifier resolver for org resolution
var tfeIdentifierResolver domain.IdentifierResolver
if deps.QueryStore != nil {
if db := repositories.GetDBFromQueryStore(deps.QueryStore); db != nil {
tfeIdentifierResolver = repositories.NewIdentifierResolver(db)
}
}
tfeHandler := tfe.NewTFETokenHandler(authHandler, deps.Repository, deps.BlobStore, deps.RBACManager, tfeIdentifierResolver)

// Create protected TFE group - opaque tokens only
tfeGroup := e.Group("/tfe/api/v2")
Expand Down
26 changes: 24 additions & 2 deletions taco/internal/middleware/org_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,22 @@ func JWTOrgResolverMiddleware(resolver domain.IdentifierResolver) echo.Middlewar
func ResolveOrgContextMiddleware(resolver domain.IdentifierResolver) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
log.Printf("[WebhookOrgResolver] MIDDLEWARE INVOKED for path: %s", c.Path())
path := c.Request().URL.Path
method := c.Request().Method

log.Printf("[WebhookOrgResolver] MIDDLEWARE INVOKED for path: %s, method: %s", path, method)

// Skip org resolution for endpoints that create/list orgs
// These endpoints don't require an existing org context
skipOrgResolution := (method == "POST" && path == "/internal/api/orgs") ||
(method == "POST" && path == "/internal/api/orgs/sync") ||
(method == "GET" && path == "/internal/api/orgs") ||
(method == "GET" && path == "/internal/api/orgs/user")

if skipOrgResolution {
log.Printf("[WebhookOrgResolver] Skipping org resolution for endpoint: %s %s", method, path)
return next(c)
}

// Get org name from echo context (set by WebhookAuth)
orgName, ok := c.Get("organization_id").(string)
Expand All @@ -64,7 +79,14 @@ func ResolveOrgContextMiddleware(resolver domain.IdentifierResolver) echo.Middle
orgUUID, err := resolver.ResolveOrganization(c.Request().Context(), orgName)
if err != nil {
log.Printf("[WebhookOrgResolver] ERROR: Failed to resolve organization '%s': %v", orgName, err)
return echo.NewHTTPError(500, "Failed to resolve organization")
log.Printf("[WebhookOrgResolver] ERROR: This likely means the organization doesn't exist in the database yet or the external_org_id doesn't match")
log.Printf("[WebhookOrgResolver] ERROR: Check if the organization was created successfully with external_org_id='%s'", orgName)
return echo.NewHTTPError(500, map[string]interface{}{
"error": "Failed to resolve organization",
"detail": err.Error(),
"org_identifier": orgName,
"hint": "The organization may not exist in the database or the external_org_id doesn't match",
})
}

log.Printf("[WebhookOrgResolver] SUCCESS: Resolved '%s' to UUID: %s", orgName, orgUUID)
Expand Down
113 changes: 62 additions & 51 deletions taco/internal/query/common/sql_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/diggerhq/digger/opentaco/internal/query/types"
"github.com/diggerhq/digger/opentaco/internal/rbac"
"github.com/google/uuid"
"gorm.io/gorm"
)

Expand Down Expand Up @@ -143,59 +144,72 @@ func (s *SQLStore) GetUnit(ctx context.Context, id string) (*types.Unit, error)
return &unit, nil
}

// parseBlobPath parses a blob path into org and unit name
// Supports: "org/name" or "name" (defaults to "default" org)
func (s *SQLStore) parseBlobPath(ctx context.Context, blobPath string) (orgUUID, name string, err error) {
// parseBlobPath parses a UUID-based blob path into org UUID and unit UUID
// Expected format: "org-uuid/unit-uuid"
// This is the only format used - all blob paths are UUID-based for immutability
func (s *SQLStore) parseBlobPath(ctx context.Context, blobPath string) (orgUUID, unitUUID string, err error) {
parts := strings.SplitN(strings.Trim(blobPath, "/"), "/", 2)

var orgName string
if len(parts) == 2 {
// Format: "org/name"
orgName = parts[0]
name = parts[1]
} else {
// Format: "name" - use default org
orgName = "default"
name = parts[0]
if len(parts) != 2 {
return "", "", fmt.Errorf("invalid blob path format: expected 'org-uuid/unit-uuid', got '%s'", blobPath)
}

orgUUID = parts[0]
unitUUID = parts[1]

// Validate both are UUIDs
if !isUUID(orgUUID) {
return "", "", fmt.Errorf("invalid org UUID in blob path: %s", orgUUID)
}
if !isUUID(unitUUID) {
return "", "", fmt.Errorf("invalid unit UUID in blob path: %s", unitUUID)
}

// Ensure org exists
// Verify org exists
var org types.Organization
err = s.db.WithContext(ctx).Where("name = ?", orgName).First(&org).Error
err = s.db.WithContext(ctx).Where("id = ?", orgUUID).First(&org).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Create org if it doesn't exist (for migration)
org = types.Organization{
Name: orgName,
DisplayName: fmt.Sprintf("Auto-created: %s", orgName),
CreatedBy: "system-sync",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if err := s.db.WithContext(ctx).Create(&org).Error; err != nil {
return "", "", fmt.Errorf("failed to create org: %w", err)
}
} else {
return "", "", fmt.Errorf("failed to lookup org: %w", err)
return "", "", fmt.Errorf("organization not found: %s", orgUUID)
}
return "", "", fmt.Errorf("failed to lookup organization: %w", err)
}

// Verify unit exists
var unit types.Unit
err = s.db.WithContext(ctx).Where("id = ? AND org_id = ?", unitUUID, orgUUID).First(&unit).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", "", fmt.Errorf("unit not found: %s in org %s", unitUUID, orgUUID)
}
return "", "", fmt.Errorf("failed to lookup unit: %w", err)
}

return org.ID, name, nil
return orgUUID, unitUUID, nil
}

// isUUID checks if a string is a valid UUID
// Uses proper UUID parsing to validate format and structure
// This is critical for distinguishing UUID-based paths from name-based paths:
// - UUID: "123e4567-89ab-12d3-a456-426614174000" → lookup by ID
// - Name: "my-app-prod" → lookup by name
func isUUID(s string) bool {
_, err := uuid.Parse(s)
return err == nil
}

// SyncEnsureUnit creates or updates a unit from blob storage
// Supports blob paths: "org/name" or "name" (defaults to default org)
// UUIDs are auto-generated via BeforeCreate hook
func (s *SQLStore) SyncEnsureUnit(ctx context.Context, unitName string) error {
orgUUID, name, err := s.parseBlobPath(ctx, unitName)
// Expects UUID-based blob path: "org-uuid/unit-uuid"
func (s *SQLStore) SyncEnsureUnit(ctx context.Context, blobPath string) error {
orgUUID, unitUUID, err := s.parseBlobPath(ctx, blobPath)
if err != nil {
return err
}

// Check if unit exists
var existing types.Unit
err = s.db.WithContext(ctx).
Where(queryOrgAndName, orgUUID, name).
Where("id = ? AND org_id = ?", unitUUID, orgUUID).
First(&existing).Error

if err == nil {
Expand All @@ -207,47 +221,44 @@ func (s *SQLStore) SyncEnsureUnit(ctx context.Context, unitName string) error {
return err
}

// Create new unit (UUID auto-generated by BeforeCreate)
unit := types.Unit{
OrgID: orgUUID,
Name: name,
}
return s.db.WithContext(ctx).Create(&unit).Error
// Unit doesn't exist - this shouldn't happen with UUID-based paths
// as units should be created via UnitRepository first
return fmt.Errorf("unit %s not found in database (UUID-based paths require unit to exist)", unitUUID)
}

func (s *SQLStore) SyncUnitMetadata(ctx context.Context, unitName string, size int64, updated time.Time) error {
orgUUID, name, err := s.parseBlobPath(ctx, unitName)
func (s *SQLStore) SyncUnitMetadata(ctx context.Context, blobPath string, size int64, updated time.Time) error {
orgUUID, unitUUID, err := s.parseBlobPath(ctx, blobPath)
if err != nil {
return err
}

return s.db.WithContext(ctx).Model(&types.Unit{}).
Where(queryOrgAndName, orgUUID, name).
Where("id = ? AND org_id = ?", unitUUID, orgUUID).
Updates(map[string]interface{}{
"size": size,
"updated_at": updated,
}).Error
}

func (s *SQLStore) SyncDeleteUnit(ctx context.Context, unitName string) error {
orgUUID, name, err := s.parseBlobPath(ctx, unitName)
func (s *SQLStore) SyncDeleteUnit(ctx context.Context, blobPath string) error {
orgUUID, unitUUID, err := s.parseBlobPath(ctx, blobPath)
if err != nil {
return err
}

return s.db.WithContext(ctx).
Where(queryOrgAndName, orgUUID, name).
Where("id = ? AND org_id = ?", unitUUID, orgUUID).
Delete(&types.Unit{}).Error
}

func (s *SQLStore) SyncUnitLock(ctx context.Context, unitName string, lockID, lockWho string, lockCreated time.Time) error {
orgUUID, name, err := s.parseBlobPath(ctx, unitName)
func (s *SQLStore) SyncUnitLock(ctx context.Context, blobPath string, lockID, lockWho string, lockCreated time.Time) error {
orgUUID, unitUUID, err := s.parseBlobPath(ctx, blobPath)
if err != nil {
return err
}

return s.db.WithContext(ctx).Model(&types.Unit{}).
Where(queryOrgAndName, orgUUID, name).
Where("id = ? AND org_id = ?", unitUUID, orgUUID).
Updates(map[string]interface{}{
"locked": true,
"lock_id": lockID,
Expand All @@ -256,14 +267,14 @@ func (s *SQLStore) SyncUnitLock(ctx context.Context, unitName string, lockID, lo
}).Error
}

func (s *SQLStore) SyncUnitUnlock(ctx context.Context, unitName string) error {
orgUUID, name, err := s.parseBlobPath(ctx, unitName)
func (s *SQLStore) SyncUnitUnlock(ctx context.Context, blobPath string) error {
orgUUID, unitUUID, err := s.parseBlobPath(ctx, blobPath)
if err != nil {
return err
}

return s.db.WithContext(ctx).Model(&types.Unit{}).
Where(queryOrgAndName, orgUUID, name).
Where("id = ? AND org_id = ?", unitUUID, orgUUID).
Updates(map[string]interface{}{
"locked": false,
"lock_id": "",
Expand Down
4 changes: 2 additions & 2 deletions taco/internal/query/types/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,8 @@ func (u *User) BeforeCreate(tx *gorm.DB) error {

type Unit struct {
ID string `gorm:"type:varchar(36);primaryKey"`
OrgID string `gorm:"type:varchar(36);index"` // Foreign key to organizations.id (UUID)
Name string `gorm:"type:varchar(255);not null;index"`
OrgID string `gorm:"type:varchar(36);index;uniqueIndex:idx_units_org_name"` // Foreign key to organizations.id (UUID)
Name string `gorm:"type:varchar(255);not null;index;uniqueIndex:idx_units_org_name"`
Size int64 `gorm:"default:0"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Locked bool `gorm:"default:false"`
Expand Down
13 changes: 12 additions & 1 deletion taco/internal/repositories/org_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ func (r *orgRepository) Create(ctx context.Context, orgID, name, displayName, ex
externalOrgIDPtr = &externalOrgID
}

slog.Info("Creating organization entity",
"orgID", orgID,
"name", name,
"displayName", displayName,
"externalOrgID", externalOrgID,
"externalOrgIDPtr", externalOrgIDPtr,
"createdBy", createdBy,
)

entity := &types.Organization{
Name: orgID,
DisplayName: displayName,
Expand All @@ -92,11 +101,13 @@ func (r *orgRepository) Create(ctx context.Context, orgID, name, displayName, ex
return nil, fmt.Errorf("failed to create organization: %w", err)
}

slog.Info("Organization created successfully",
slog.Info("Organization created successfully in database",
"dbID", entity.ID,
"orgID", orgID,
"name", name,
"displayName", displayName,
"externalOrgID", externalOrgID,
"storedExternalOrgID", getStringValue(entity.ExternalOrgID),
"createdBy", createdBy,
)

Expand Down
Loading
Loading