Skip to content

Commit fcb2cb8

Browse files
authored
adjust org resolution (#2372)
* exempt org from some middleware, readable cloudblock, unique unit name * message in org unique * correct comment - we dont resolve names * blob paths uuid/uuid throughout, prevent default * fix build errors * adjust models file to match index
1 parent 1c598ba commit fcb2cb8

File tree

13 files changed

+437
-151
lines changed

13 files changed

+437
-151
lines changed

taco/internal/api/routes.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,14 @@ func RegisterRoutes(e *echo.Echo, deps Dependencies) {
246246

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

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

taco/internal/middleware/org_context.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,22 @@ func JWTOrgResolverMiddleware(resolver domain.IdentifierResolver) echo.Middlewar
4444
func ResolveOrgContextMiddleware(resolver domain.IdentifierResolver) echo.MiddlewareFunc {
4545
return func(next echo.HandlerFunc) echo.HandlerFunc {
4646
return func(c echo.Context) error {
47-
log.Printf("[WebhookOrgResolver] MIDDLEWARE INVOKED for path: %s", c.Path())
47+
path := c.Request().URL.Path
48+
method := c.Request().Method
49+
50+
log.Printf("[WebhookOrgResolver] MIDDLEWARE INVOKED for path: %s, method: %s", path, method)
51+
52+
// Skip org resolution for endpoints that create/list orgs
53+
// These endpoints don't require an existing org context
54+
skipOrgResolution := (method == "POST" && path == "/internal/api/orgs") ||
55+
(method == "POST" && path == "/internal/api/orgs/sync") ||
56+
(method == "GET" && path == "/internal/api/orgs") ||
57+
(method == "GET" && path == "/internal/api/orgs/user")
58+
59+
if skipOrgResolution {
60+
log.Printf("[WebhookOrgResolver] Skipping org resolution for endpoint: %s %s", method, path)
61+
return next(c)
62+
}
4863

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

7092
log.Printf("[WebhookOrgResolver] SUCCESS: Resolved '%s' to UUID: %s", orgName, orgUUID)

taco/internal/query/common/sql_store.go

Lines changed: 62 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/diggerhq/digger/opentaco/internal/query/types"
1212
"github.com/diggerhq/digger/opentaco/internal/rbac"
13+
"github.com/google/uuid"
1314
"gorm.io/gorm"
1415
)
1516

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

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

151-
var orgName string
152-
if len(parts) == 2 {
153-
// Format: "org/name"
154-
orgName = parts[0]
155-
name = parts[1]
156-
} else {
157-
// Format: "name" - use default org
158-
orgName = "default"
159-
name = parts[0]
153+
if len(parts) != 2 {
154+
return "", "", fmt.Errorf("invalid blob path format: expected 'org-uuid/unit-uuid', got '%s'", blobPath)
155+
}
156+
157+
orgUUID = parts[0]
158+
unitUUID = parts[1]
159+
160+
// Validate both are UUIDs
161+
if !isUUID(orgUUID) {
162+
return "", "", fmt.Errorf("invalid org UUID in blob path: %s", orgUUID)
163+
}
164+
if !isUUID(unitUUID) {
165+
return "", "", fmt.Errorf("invalid unit UUID in blob path: %s", unitUUID)
160166
}
161167

162-
// Ensure org exists
168+
// Verify org exists
163169
var org types.Organization
164-
err = s.db.WithContext(ctx).Where("name = ?", orgName).First(&org).Error
170+
err = s.db.WithContext(ctx).Where("id = ?", orgUUID).First(&org).Error
165171
if err != nil {
166172
if errors.Is(err, gorm.ErrRecordNotFound) {
167-
// Create org if it doesn't exist (for migration)
168-
org = types.Organization{
169-
Name: orgName,
170-
DisplayName: fmt.Sprintf("Auto-created: %s", orgName),
171-
CreatedBy: "system-sync",
172-
CreatedAt: time.Now(),
173-
UpdatedAt: time.Now(),
174-
}
175-
if err := s.db.WithContext(ctx).Create(&org).Error; err != nil {
176-
return "", "", fmt.Errorf("failed to create org: %w", err)
177-
}
178-
} else {
179-
return "", "", fmt.Errorf("failed to lookup org: %w", err)
173+
return "", "", fmt.Errorf("organization not found: %s", orgUUID)
174+
}
175+
return "", "", fmt.Errorf("failed to lookup organization: %w", err)
176+
}
177+
178+
// Verify unit exists
179+
var unit types.Unit
180+
err = s.db.WithContext(ctx).Where("id = ? AND org_id = ?", unitUUID, orgUUID).First(&unit).Error
181+
if err != nil {
182+
if errors.Is(err, gorm.ErrRecordNotFound) {
183+
return "", "", fmt.Errorf("unit not found: %s in org %s", unitUUID, orgUUID)
180184
}
185+
return "", "", fmt.Errorf("failed to lookup unit: %w", err)
181186
}
182187

183-
return org.ID, name, nil
188+
return orgUUID, unitUUID, nil
189+
}
190+
191+
// isUUID checks if a string is a valid UUID
192+
// Uses proper UUID parsing to validate format and structure
193+
// This is critical for distinguishing UUID-based paths from name-based paths:
194+
// - UUID: "123e4567-89ab-12d3-a456-426614174000" → lookup by ID
195+
// - Name: "my-app-prod" → lookup by name
196+
func isUUID(s string) bool {
197+
_, err := uuid.Parse(s)
198+
return err == nil
184199
}
185200

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

195209
// Check if unit exists
196210
var existing types.Unit
197211
err = s.db.WithContext(ctx).
198-
Where(queryOrgAndName, orgUUID, name).
212+
Where("id = ? AND org_id = ?", unitUUID, orgUUID).
199213
First(&existing).Error
200214

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

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

218-
func (s *SQLStore) SyncUnitMetadata(ctx context.Context, unitName string, size int64, updated time.Time) error {
219-
orgUUID, name, err := s.parseBlobPath(ctx, unitName)
229+
func (s *SQLStore) SyncUnitMetadata(ctx context.Context, blobPath string, size int64, updated time.Time) error {
230+
orgUUID, unitUUID, err := s.parseBlobPath(ctx, blobPath)
220231
if err != nil {
221232
return err
222233
}
223234

224235
return s.db.WithContext(ctx).Model(&types.Unit{}).
225-
Where(queryOrgAndName, orgUUID, name).
236+
Where("id = ? AND org_id = ?", unitUUID, orgUUID).
226237
Updates(map[string]interface{}{
227238
"size": size,
228239
"updated_at": updated,
229240
}).Error
230241
}
231242

232-
func (s *SQLStore) SyncDeleteUnit(ctx context.Context, unitName string) error {
233-
orgUUID, name, err := s.parseBlobPath(ctx, unitName)
243+
func (s *SQLStore) SyncDeleteUnit(ctx context.Context, blobPath string) error {
244+
orgUUID, unitUUID, err := s.parseBlobPath(ctx, blobPath)
234245
if err != nil {
235246
return err
236247
}
237248

238249
return s.db.WithContext(ctx).
239-
Where(queryOrgAndName, orgUUID, name).
250+
Where("id = ? AND org_id = ?", unitUUID, orgUUID).
240251
Delete(&types.Unit{}).Error
241252
}
242253

243-
func (s *SQLStore) SyncUnitLock(ctx context.Context, unitName string, lockID, lockWho string, lockCreated time.Time) error {
244-
orgUUID, name, err := s.parseBlobPath(ctx, unitName)
254+
func (s *SQLStore) SyncUnitLock(ctx context.Context, blobPath string, lockID, lockWho string, lockCreated time.Time) error {
255+
orgUUID, unitUUID, err := s.parseBlobPath(ctx, blobPath)
245256
if err != nil {
246257
return err
247258
}
248259

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

259-
func (s *SQLStore) SyncUnitUnlock(ctx context.Context, unitName string) error {
260-
orgUUID, name, err := s.parseBlobPath(ctx, unitName)
270+
func (s *SQLStore) SyncUnitUnlock(ctx context.Context, blobPath string) error {
271+
orgUUID, unitUUID, err := s.parseBlobPath(ctx, blobPath)
261272
if err != nil {
262273
return err
263274
}
264275

265276
return s.db.WithContext(ctx).Model(&types.Unit{}).
266-
Where(queryOrgAndName, orgUUID, name).
277+
Where("id = ? AND org_id = ?", unitUUID, orgUUID).
267278
Updates(map[string]interface{}{
268279
"locked": false,
269280
"lock_id": "",

taco/internal/query/types/models.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,8 @@ func (u *User) BeforeCreate(tx *gorm.DB) error {
141141

142142
type Unit struct {
143143
ID string `gorm:"type:varchar(36);primaryKey"`
144-
OrgID string `gorm:"type:varchar(36);index"` // Foreign key to organizations.id (UUID)
145-
Name string `gorm:"type:varchar(255);not null;index"`
144+
OrgID string `gorm:"type:varchar(36);index;uniqueIndex:idx_units_org_name"` // Foreign key to organizations.id (UUID)
145+
Name string `gorm:"type:varchar(255);not null;index;uniqueIndex:idx_units_org_name"`
146146
Size int64 `gorm:"default:0"`
147147
UpdatedAt time.Time `gorm:"autoUpdateTime"`
148148
Locked bool `gorm:"default:false"`

taco/internal/repositories/org_repository.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ func (r *orgRepository) Create(ctx context.Context, orgID, name, displayName, ex
7979
externalOrgIDPtr = &externalOrgID
8080
}
8181

82+
slog.Info("Creating organization entity",
83+
"orgID", orgID,
84+
"name", name,
85+
"displayName", displayName,
86+
"externalOrgID", externalOrgID,
87+
"externalOrgIDPtr", externalOrgIDPtr,
88+
"createdBy", createdBy,
89+
)
90+
8291
entity := &types.Organization{
8392
Name: orgID,
8493
DisplayName: displayName,
@@ -92,11 +101,13 @@ func (r *orgRepository) Create(ctx context.Context, orgID, name, displayName, ex
92101
return nil, fmt.Errorf("failed to create organization: %w", err)
93102
}
94103

95-
slog.Info("Organization created successfully",
104+
slog.Info("Organization created successfully in database",
105+
"dbID", entity.ID,
96106
"orgID", orgID,
97107
"name", name,
98108
"displayName", displayName,
99109
"externalOrgID", externalOrgID,
110+
"storedExternalOrgID", getStringValue(entity.ExternalOrgID),
100111
"createdBy", createdBy,
101112
)
102113

0 commit comments

Comments
 (0)