osbuild: add support for org.osbuild.chown stage

Signed-off-by: Tomáš Hozza <thozza@redhat.com>
This commit is contained in:
Tomáš Hozza 2023-02-08 18:19:06 +01:00 committed by Sanne Raymaekers
parent 2e54557cd4
commit 0bd0ce9fc1
2 changed files with 342 additions and 0 deletions

View file

@ -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,
}
}

View file

@ -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)
}
})
}
}