Skip to content

Commit fca0554

Browse files
committed
handler_blueprints: validate files, dir and mountpoint customizations
For direct feedback before creating or updating a blueprint, we'll do basic checks that will fail later on, anyway.
1 parent 8bf7934 commit fca0554

File tree

1 file changed

+199
-16
lines changed

1 file changed

+199
-16
lines changed

internal/v1/handler_blueprints.go

Lines changed: 199 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
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

2932
var (
@@ -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
257273
func 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

Comments
 (0)