88 "log/slog"
99 "net/http"
1010 "net/url"
11+ "os"
12+ "path/filepath"
1113 "regexp"
1214 "slices"
1315 "strconv"
@@ -24,6 +26,7 @@ import (
2426 "github.com/osbuild/image-builder-crc/internal/common"
2527 "github.com/osbuild/image-builder-crc/internal/db"
2628 "github.com/osbuild/images/pkg/crypt"
29+ "github.com/osbuild/images/pkg/customizations/fsnode"
2730)
2831
2932var (
@@ -252,17 +255,32 @@ func WithRedactedFiles(paths []string) BlueprintBodyOption {
252255 }
253256}
254257
258+ // ValidationError represents a validation error that should be returned as HTTP 422
259+ type ValidationError struct {
260+ HTTPErrorList HTTPErrorList
261+ }
262+
263+ func (e ValidationError ) Error () string {
264+ if len (e .HTTPErrorList .Errors ) > 0 {
265+ return e .HTTPErrorList .Errors [0 ].Detail
266+ }
267+ return "validation error"
268+ }
269+
255270// validateBlueprintRequest performs common validation for blueprint requests
256271// used by both CreateBlueprint and UpdateBlueprint handlers
272+ // Returns a ValidationError if validation fails, nil if validation passes
257273func validateBlueprintRequest (ctx echo.Context , blueprintRequest * CreateBlueprintRequest , existingUsers []User ) error {
258274 // Validate blueprint name
259275 if ! blueprintNameRegex .MatchString (blueprintRequest .Name ) {
260- return ctx .JSON (http .StatusUnprocessableEntity , HTTPErrorList {
261- Errors : []HTTPError {{
262- Title : "Invalid blueprint name" ,
263- Detail : blueprintInvalidNameDetail ,
264- }},
265- })
276+ return ValidationError {
277+ HTTPErrorList : HTTPErrorList {
278+ Errors : []HTTPError {{
279+ Title : "Invalid blueprint name" ,
280+ Detail : blueprintInvalidNameDetail ,
281+ }},
282+ },
283+ }
266284 }
267285
268286 // Validate users if present
@@ -273,22 +291,181 @@ func validateBlueprintRequest(ctx echo.Context, blueprintRequest *CreateBlueprin
273291 if existingUsers != nil {
274292 err := (* users )[i ].MergeForUpdate (existingUsers )
275293 if err != nil {
276- return ctx .JSON (http .StatusUnprocessableEntity , HTTPErrorList {
277- Errors : []HTTPError {{
278- Title : "Invalid user" ,
279- Detail : err .Error (),
280- }},
281- })
294+ return ValidationError {
295+ HTTPErrorList : HTTPErrorList {
296+ Errors : []HTTPError {{
297+ Title : "Invalid user" ,
298+ Detail : err .Error (),
299+ }},
300+ },
301+ }
282302 }
283303 } else {
284304 // For create operations, validate directly
285305 if err := user .Valid (); err != nil {
286- return ctx .JSON (http .StatusUnprocessableEntity , HTTPErrorList {
306+ return ValidationError {
307+ HTTPErrorList : HTTPErrorList {
308+ Errors : []HTTPError {{
309+ Title : "Invalid user" ,
310+ Detail : err .Error (),
311+ }},
312+ },
313+ }
314+ }
315+ }
316+ }
317+ }
318+
319+ // Validate Files using images library fsnode validation
320+ if blueprintRequest .Customizations .Files != nil {
321+ for _ , file := range * blueprintRequest .Customizations .Files {
322+ // Convert API types to fsnode types for validation
323+ var mode * os.FileMode
324+ if file .Mode != nil {
325+ // Parse octal mode string to os.FileMode
326+ if modeVal , err := strconv .ParseUint (* file .Mode , 8 , 32 ); err == nil {
327+ m := os .FileMode (modeVal )
328+ mode = & m
329+ }
330+ }
331+
332+ var user interface {}
333+ if file .User != nil {
334+ // Handle union type - could be string or int64
335+ if userStr , err := file .User .AsFileUser0 (); err == nil {
336+ user = userStr
337+ } else if userInt , err := file .User .AsFileUser1 (); err == nil {
338+ user = userInt
339+ }
340+ }
341+
342+ var group interface {}
343+ if file .Group != nil {
344+ // Handle union type - could be string or int64
345+ if groupStr , err := file .Group .AsFileGroup0 (); err == nil {
346+ group = groupStr
347+ } else if groupInt , err := file .Group .AsFileGroup1 (); err == nil {
348+ group = groupInt
349+ }
350+ }
351+
352+ var data []byte
353+ if file .Data != nil {
354+ data = []byte (* file .Data )
355+ }
356+
357+ // Use fsnode.NewFile for validation - this handles all path, mode, user, group validation
358+ _ , err := fsnode .NewFile (file .Path , mode , user , group , data )
359+ if err != nil {
360+ return ValidationError {
361+ HTTPErrorList : HTTPErrorList {
362+ Errors : []HTTPError {{
363+ Title : "Invalid file customization" ,
364+ Detail : fmt .Sprintf ("file %q: %s" , file .Path , err .Error ()),
365+ }},
366+ },
367+ }
368+ }
369+ }
370+ }
371+
372+ // Validate Directories using images library fsnode validation
373+ if blueprintRequest .Customizations .Directories != nil {
374+ for _ , dir := range * blueprintRequest .Customizations .Directories {
375+ // Convert API types to fsnode types for validation
376+ var mode * os.FileMode
377+ if dir .Mode != nil {
378+ // Parse octal mode string to os.FileMode
379+ if modeVal , err := strconv .ParseUint (* dir .Mode , 8 , 32 ); err == nil {
380+ m := os .FileMode (modeVal )
381+ mode = & m
382+ }
383+ }
384+
385+ var user interface {}
386+ if dir .User != nil {
387+ // Handle union type - could be string or int64
388+ if userStr , err := dir .User .AsDirectoryUser0 (); err == nil {
389+ user = userStr
390+ } else if userInt , err := dir .User .AsDirectoryUser1 (); err == nil {
391+ user = userInt
392+ }
393+ }
394+
395+ var group interface {}
396+ if dir .Group != nil {
397+ // Handle union type - could be string or int64
398+ if groupStr , err := dir .Group .AsDirectoryGroup0 (); err == nil {
399+ group = groupStr
400+ } else if groupInt , err := dir .Group .AsDirectoryGroup1 (); err == nil {
401+ group = groupInt
402+ }
403+ }
404+
405+ ensureParents := false
406+ if dir .EnsureParents != nil {
407+ ensureParents = * dir .EnsureParents
408+ }
409+
410+ // Use fsnode.NewDirectory for validation - this handles all path, mode, user, group validation
411+ _ , err := fsnode .NewDirectory (dir .Path , mode , user , group , ensureParents )
412+ if err != nil {
413+ return ValidationError {
414+ HTTPErrorList : HTTPErrorList {
415+ Errors : []HTTPError {{
416+ Title : "Invalid directory customization" ,
417+ Detail : fmt .Sprintf ("directory %q: %s" , dir .Path , err .Error ()),
418+ }},
419+ },
420+ }
421+ }
422+ }
423+ }
424+
425+ // Validate Filesystem using basic path validation patterns from fsnode library
426+ if blueprintRequest .Customizations .Filesystem != nil {
427+ for _ , fs := range * blueprintRequest .Customizations .Filesystem {
428+ // Use the same path validation logic as fsnode (following library patterns)
429+ if fs .Mountpoint == "" {
430+ return ValidationError {
431+ HTTPErrorList : HTTPErrorList {
432+ Errors : []HTTPError {{
433+ Title : "Invalid filesystem customization" ,
434+ Detail : "mountpoint must not be empty" ,
435+ }},
436+ },
437+ }
438+ }
439+ if fs .Mountpoint [0 ] != '/' {
440+ return ValidationError {
441+ HTTPErrorList : HTTPErrorList {
442+ Errors : []HTTPError {{
443+ Title : "Invalid filesystem customization" ,
444+ Detail : fmt .Sprintf ("mountpoint %q must be absolute" , fs .Mountpoint ),
445+ }},
446+ },
447+ }
448+ }
449+ if fs .Mountpoint != filepath .Clean (fs .Mountpoint ) {
450+ return ValidationError {
451+ HTTPErrorList : HTTPErrorList {
287452 Errors : []HTTPError {{
288- Title : "Invalid user " ,
289- Detail : err . Error ( ),
453+ Title : "Invalid filesystem customization " ,
454+ Detail : fmt . Sprintf ( "mountpoint %q must be canonical" , fs . Mountpoint ),
290455 }},
291- })
456+ },
457+ }
458+ }
459+
460+ // Validate minimum size is reasonable
461+ if fs .MinSize > 0 && fs .MinSize < 1024 * 1024 { // 1MB minimum
462+ return ValidationError {
463+ HTTPErrorList : HTTPErrorList {
464+ Errors : []HTTPError {{
465+ Title : "Invalid filesystem customization" ,
466+ Detail : fmt .Sprintf ("mountpoint %q minimum size must be at least 1MB" , fs .Mountpoint ),
467+ }},
468+ },
292469 }
293470 }
294471 }
@@ -319,6 +496,9 @@ func (h *Handlers) CreateBlueprint(ctx echo.Context) error {
319496
320497 // Validate blueprint request (name and users)
321498 if err := validateBlueprintRequest (ctx , & blueprintRequest , nil ); err != nil {
499+ if validationErr , ok := err .(ValidationError ); ok {
500+ return ctx .JSON (http .StatusUnprocessableEntity , validationErr .HTTPErrorList )
501+ }
322502 return err
323503 }
324504
@@ -574,6 +754,9 @@ func (h *Handlers) UpdateBlueprint(ctx echo.Context, blueprintId uuid.UUID) erro
574754
575755 // Validate blueprint request (name and users with merge logic for updates)
576756 if err := validateBlueprintRequest (ctx , & blueprintRequest , existingUsers ); err != nil {
757+ if validationErr , ok := err .(ValidationError ); ok {
758+ return ctx .JSON (http .StatusUnprocessableEntity , validationErr .HTTPErrorList )
759+ }
577760 return err
578761 }
579762
0 commit comments