From 0bd0ce9fc12e7b069e86494deb7e77112d017d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Hozza?= Date: Wed, 8 Feb 2023 18:19:06 +0100 Subject: [PATCH] osbuild: add support for `org.osbuild.chown` stage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomáš Hozza --- internal/osbuild/chown_stage.go | 98 +++++++++++ internal/osbuild/chown_stage_test.go | 244 +++++++++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 internal/osbuild/chown_stage.go create mode 100644 internal/osbuild/chown_stage_test.go diff --git a/internal/osbuild/chown_stage.go b/internal/osbuild/chown_stage.go new file mode 100644 index 000000000..2bd198411 --- /dev/null +++ b/internal/osbuild/chown_stage.go @@ -0,0 +1,98 @@ +package osbuild + +import ( + "fmt" + "regexp" +) + +const ( + // should be "^\\/(?!\\.\\.)((?!\\/\\.\\.\\/).)+$" but Go doesn't support lookaheads + // therefore we have to instead check for the invalid cases, which is much simpler + chownStageInvalidPathRegex = `((^|\/)[.]{2}(\/|$))|^([^/].*)*$` + chownStageUsernameRegex = `^[A-Za-z0-9_.][A-Za-z0-9_.-]{0,31}$` + chownStageGroupnameRegex = `^[A-Za-z0-9_][A-Za-z0-9_-]{0,31}$` +) + +type ChownStageOptions struct { + Items map[string]ChownStagePathOptions `json:"items"` +} + +func (ChownStageOptions) isStageOptions() {} + +func (o *ChownStageOptions) validate() error { + for path, options := range o.Items { + invalidPathRegex := regexp.MustCompile(chownStageInvalidPathRegex) + if invalidPathRegex.FindAllString(path, -1) != nil { + return fmt.Errorf("chown path %q matches invalid path pattern (%s)", path, invalidPathRegex.String()) + } + + if err := options.validate(); err != nil { + return err + } + } + + return nil +} + +type ChownStagePathOptions struct { + // User can be either a string (user name), an int64 (UID) or nil + User interface{} `json:"user,omitempty"` + // Group can be either a string (grou pname), an int64 (GID) or nil + Group interface{} `json:"group,omitempty"` + + Recursive bool `json:"recursive,omitempty"` +} + +// validate checks that the options values conform to the schema +func (o *ChownStagePathOptions) validate() error { + switch user := o.User.(type) { + case string: + usernameRegex := regexp.MustCompile(chownStageUsernameRegex) + if !usernameRegex.MatchString(user) { + return fmt.Errorf("chown user name %q doesn't conform to schema (%s)", user, usernameRegex.String()) + } + case int64: + if user < 0 { + return fmt.Errorf("chown user id %d is negative", user) + } + case nil: + // user is not set + default: + return fmt.Errorf("chown user must be either a string nor an int64, got %T", user) + } + + switch group := o.Group.(type) { + case string: + groupnameRegex := regexp.MustCompile(chownStageGroupnameRegex) + if !groupnameRegex.MatchString(group) { + return fmt.Errorf("chown group name %q doesn't conform to schema (%s)", group, groupnameRegex.String()) + } + case int64: + if group < 0 { + return fmt.Errorf("chown group id %d is negative", group) + } + case nil: + // group is not set + default: + return fmt.Errorf("chown group must be either a string nor an int64, got %T", group) + } + + // check that at least one of user or group is set + if o.User == nil && o.Group == nil { + return fmt.Errorf("chown user and group are both not set") + } + + return nil +} + +// NewChownStage creates a new org.osbuild.chown stage +func NewChownStage(options *ChownStageOptions) *Stage { + if err := options.validate(); err != nil { + panic(err) + } + + return &Stage{ + Type: "org.osbuild.chown", + Options: options, + } +} diff --git a/internal/osbuild/chown_stage_test.go b/internal/osbuild/chown_stage_test.go new file mode 100644 index 000000000..704df970b --- /dev/null +++ b/internal/osbuild/chown_stage_test.go @@ -0,0 +1,244 @@ +package osbuild + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewChownStage(t *testing.T) { + stageOptions := &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "/etc/foobar": { + User: "root", + Group: int64(12345), + Recursive: true, + }, + }, + } + expectedStage := &Stage{ + Type: "org.osbuild.chown", + Options: stageOptions, + } + actualStage := NewChownStage(stageOptions) + assert.Equal(t, expectedStage, actualStage) +} + +func TestChownStageOptionsValidate(t *testing.T) { + validPathOptions := ChownStagePathOptions{ + User: "root", + } + + testCases := []struct { + name string + options *ChownStageOptions + err bool + }{ + { + name: "empty-options", + options: &ChownStageOptions{}, + }, + { + name: "no-items", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{}, + }, + }, + { + name: "invalid-item-path-1", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "": validPathOptions, + }, + }, + err: true, + }, + { + name: "invalid-item-path-2", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "foobar": validPathOptions, + }, + }, + err: true, + }, + { + name: "invalid-item-path-3", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "/../foobar": validPathOptions, + }, + }, + err: true, + }, + { + name: "invalid-item-path-4", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "/etc/../foobar": validPathOptions, + }, + }, + err: true, + }, + { + name: "invalid-item-path-5", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "/etc/..": validPathOptions, + }, + }, + err: true, + }, + { + name: "invalid-item-path-6", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "../etc/foo/../bar": validPathOptions, + }, + }, + err: true, + }, + { + name: "valid-item-path-1", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "/etc/foobar": validPathOptions, + }, + }, + }, + { + name: "valid-item-path-2", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "/etc/foo/bar/baz": validPathOptions, + }, + }, + }, + { + name: "valid-item-path-3", + options: &ChownStageOptions{ + Items: map[string]ChownStagePathOptions{ + "/etc": validPathOptions, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.options.validate() + if tc.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestChownStagePathOptionsValidate(t *testing.T) { + testCases := []struct { + name string + options ChownStagePathOptions + err bool + }{ + { + name: "empty-options", + options: ChownStagePathOptions{}, + err: true, + }, + { + name: "invalid-user-string-1", + options: ChownStagePathOptions{ + User: "", + }, + err: true, + }, + { + name: "invalid-user-string-2", + options: ChownStagePathOptions{ + User: "r@@t", + }, + err: true, + }, + { + name: "invalid-user-id", + options: ChownStagePathOptions{ + User: int64(-1), + }, + err: true, + }, + { + name: "valid-user-string", + options: ChownStagePathOptions{ + User: "root", + }, + }, + { + name: "valid-user-id", + options: ChownStagePathOptions{ + User: int64(0), + }, + }, + { + name: "invalid-group-string-1", + options: ChownStagePathOptions{ + Group: "", + }, + err: true, + }, + { + name: "invalid-group-string-2", + options: ChownStagePathOptions{ + Group: "r@@t", + }, + err: true, + }, + { + name: "invalid-group-id", + options: ChownStagePathOptions{ + Group: int64(-1), + }, + err: true, + }, + { + name: "valid-group-string", + options: ChownStagePathOptions{ + Group: "root", + }, + }, + { + name: "valid-group-id", + options: ChownStagePathOptions{ + Group: int64(0), + }, + }, + { + name: "valid-both-1", + options: ChownStagePathOptions{ + User: "root", + Group: int64(12345), + Recursive: true, + }, + }, + { + name: "valid-both-2", + options: ChownStagePathOptions{ + User: int64(12345), + Group: "root", + Recursive: true, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.options.validate() + if tc.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +}