@@ -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" : "" ,
0 commit comments