Skip to content

Commit a0d9020

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 a0d9020

File tree

1 file changed

+146
-0
lines changed

1 file changed

+146
-0
lines changed

internal/v1/handler_blueprints.go

Lines changed: 146 additions & 0 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 (
@@ -294,6 +297,149 @@ func validateBlueprintRequest(ctx echo.Context, blueprintRequest *CreateBlueprin
294297
}
295298
}
296299

300+
// Validate Files using images library fsnode validation
301+
if blueprintRequest.Customizations.Files != nil {
302+
for _, file := range *blueprintRequest.Customizations.Files {
303+
// Convert API types to fsnode types for validation
304+
var mode *os.FileMode
305+
if file.Mode != nil {
306+
// Parse octal mode string to os.FileMode
307+
if modeVal, err := strconv.ParseUint(*file.Mode, 8, 32); err == nil {
308+
m := os.FileMode(modeVal)
309+
mode = &m
310+
}
311+
}
312+
313+
var user interface{}
314+
if file.User != nil {
315+
// Handle union type - could be string or int64
316+
if userStr, err := file.User.AsFileUser0(); err == nil {
317+
user = userStr
318+
} else if userInt, err := file.User.AsFileUser1(); err == nil {
319+
user = userInt
320+
}
321+
}
322+
323+
var group interface{}
324+
if file.Group != nil {
325+
// Handle union type - could be string or int64
326+
if groupStr, err := file.Group.AsFileGroup0(); err == nil {
327+
group = groupStr
328+
} else if groupInt, err := file.Group.AsFileGroup1(); err == nil {
329+
group = groupInt
330+
}
331+
}
332+
333+
var data []byte
334+
if file.Data != nil {
335+
data = []byte(*file.Data)
336+
}
337+
338+
// Use fsnode.NewFile for validation - this handles all path, mode, user, group validation
339+
_, err := fsnode.NewFile(file.Path, mode, user, group, data)
340+
if err != nil {
341+
return ctx.JSON(http.StatusUnprocessableEntity, HTTPErrorList{
342+
Errors: []HTTPError{{
343+
Title: "Invalid file customization",
344+
Detail: fmt.Sprintf("file %q: %s", file.Path, err.Error()),
345+
}},
346+
})
347+
}
348+
}
349+
}
350+
351+
// Validate Directories using images library fsnode validation
352+
if blueprintRequest.Customizations.Directories != nil {
353+
for _, dir := range *blueprintRequest.Customizations.Directories {
354+
// Convert API types to fsnode types for validation
355+
var mode *os.FileMode
356+
if dir.Mode != nil {
357+
// Parse octal mode string to os.FileMode
358+
if modeVal, err := strconv.ParseUint(*dir.Mode, 8, 32); err == nil {
359+
m := os.FileMode(modeVal)
360+
mode = &m
361+
}
362+
}
363+
364+
var user interface{}
365+
if dir.User != nil {
366+
// Handle union type - could be string or int64
367+
if userStr, err := dir.User.AsDirectoryUser0(); err == nil {
368+
user = userStr
369+
} else if userInt, err := dir.User.AsDirectoryUser1(); err == nil {
370+
user = userInt
371+
}
372+
}
373+
374+
var group interface{}
375+
if dir.Group != nil {
376+
// Handle union type - could be string or int64
377+
if groupStr, err := dir.Group.AsDirectoryGroup0(); err == nil {
378+
group = groupStr
379+
} else if groupInt, err := dir.Group.AsDirectoryGroup1(); err == nil {
380+
group = groupInt
381+
}
382+
}
383+
384+
ensureParents := false
385+
if dir.EnsureParents != nil {
386+
ensureParents = *dir.EnsureParents
387+
}
388+
389+
// Use fsnode.NewDirectory for validation - this handles all path, mode, user, group validation
390+
_, err := fsnode.NewDirectory(dir.Path, mode, user, group, ensureParents)
391+
if err != nil {
392+
return ctx.JSON(http.StatusUnprocessableEntity, HTTPErrorList{
393+
Errors: []HTTPError{{
394+
Title: "Invalid directory customization",
395+
Detail: fmt.Sprintf("directory %q: %s", dir.Path, err.Error()),
396+
}},
397+
})
398+
}
399+
}
400+
}
401+
402+
// Validate Filesystem using basic path validation patterns from fsnode library
403+
if blueprintRequest.Customizations.Filesystem != nil {
404+
for _, fs := range *blueprintRequest.Customizations.Filesystem {
405+
// Use the same path validation logic as fsnode (following library patterns)
406+
if fs.Mountpoint == "" {
407+
return ctx.JSON(http.StatusUnprocessableEntity, HTTPErrorList{
408+
Errors: []HTTPError{{
409+
Title: "Invalid filesystem customization",
410+
Detail: "mountpoint must not be empty",
411+
}},
412+
})
413+
}
414+
if fs.Mountpoint[0] != '/' {
415+
return ctx.JSON(http.StatusUnprocessableEntity, HTTPErrorList{
416+
Errors: []HTTPError{{
417+
Title: "Invalid filesystem customization",
418+
Detail: fmt.Sprintf("mountpoint %q must be absolute", fs.Mountpoint),
419+
}},
420+
})
421+
}
422+
if fs.Mountpoint != filepath.Clean(fs.Mountpoint) {
423+
return ctx.JSON(http.StatusUnprocessableEntity, HTTPErrorList{
424+
Errors: []HTTPError{{
425+
Title: "Invalid filesystem customization",
426+
Detail: fmt.Sprintf("mountpoint %q must be canonical", fs.Mountpoint),
427+
}},
428+
})
429+
}
430+
431+
// Validate minimum size is reasonable
432+
if fs.MinSize > 0 && fs.MinSize < 1024*1024 { // 1MB minimum
433+
return ctx.JSON(http.StatusUnprocessableEntity, HTTPErrorList{
434+
Errors: []HTTPError{{
435+
Title: "Invalid filesystem customization",
436+
Detail: fmt.Sprintf("mountpoint %q minimum size must be at least 1MB", fs.Mountpoint),
437+
}},
438+
})
439+
}
440+
}
441+
}
442+
297443
return nil
298444
}
299445

0 commit comments

Comments
 (0)