osbuild2: add support for org.osbuild.pam.limits.conf stage

Add support for a new osbuild stage `org.osbuild.pam.limits.conf`, for
creating pam_limits module configuration files.

Add unit tests for the new stage.

Related to:
- https://github.com/osbuild/osbuild/pull/802
- https://github.com/osbuild/osbuild/pull/807

Signed-off-by: Tomas Hozza <thozza@redhat.com>
This commit is contained in:
Tomas Hozza 2021-09-10 15:28:55 +02:00 committed by Ondřej Budai
parent 3f52af2adb
commit a5097b2141
4 changed files with 249 additions and 0 deletions

View file

@ -0,0 +1,155 @@
package osbuild2
import (
"encoding/json"
"fmt"
"reflect"
)
// PamLimitsConfStageOptions represents a single pam_limits module configuration file.
type PamLimitsConfStageOptions struct {
// Filename of the configuration file to be created. Must end with '.conf'.
Filename string `json:"filename"`
// List of configuration directives. The list must contain at least one item.
Config []PamLimitsConfigLine `json:"config"`
}
func (PamLimitsConfStageOptions) isStageOptions() {}
// NewPamLimitsConfStageOptions creates a new PamLimitsConf Stage options object.
func NewPamLimitsConfStageOptions(filename string, config []PamLimitsConfigLine) *PamLimitsConfStageOptions {
return &PamLimitsConfStageOptions{
Filename: filename,
Config: config,
}
}
// Unexported alias for use in PamLimitsConfStageOptions's MarshalJSON() to prevent recursion
type pamLimitsConfStageOptions PamLimitsConfStageOptions
func (o PamLimitsConfStageOptions) MarshalJSON() ([]byte, error) {
if len(o.Config) == 0 {
return nil, fmt.Errorf("the 'Config' list must contain at least one item")
}
options := pamLimitsConfStageOptions(o)
return json.Marshal(options)
}
// NewPamLimitsConfStage creates a new PamLimitsConf Stage object.
func NewPamLimitsConfStage(options *PamLimitsConfStageOptions) *Stage {
return &Stage{
Type: "org.osbuild.pam.limits.conf",
Options: options,
}
}
type PamLimitsType string
// Valid 'Type' values for the use with the PamLimitsConfigLine structure.
const (
PamLimitsTypeHard PamLimitsType = "hard"
PamLimitsTypeSoft PamLimitsType = "soft"
PamLimitsTypeBoth PamLimitsType = "-"
)
type PamLimitsItem string
// Valid 'Item' values for the use with the PamLimitsConfigLine structure.
const (
PamLimitsItemCore PamLimitsItem = "core"
PamLimitsItemData PamLimitsItem = "data"
PamLimitsItemFsize PamLimitsItem = "fsize"
PamLimitsItemMemlock PamLimitsItem = "memlock"
PamLimitsItemNofile PamLimitsItem = "nofile"
PamLimitsItemRss PamLimitsItem = "rss"
PamLimitsItemStack PamLimitsItem = "stack"
PamLimitsItemCpu PamLimitsItem = "cpu"
PamLimitsItemNproc PamLimitsItem = "nproc"
PamLimitsItemAs PamLimitsItem = "as"
PamLimitsItemMaxlogins PamLimitsItem = "maxlogins"
PamLimitsItemMaxsyslogins PamLimitsItem = "maxsyslogins"
PamLimitsItemNonewprivs PamLimitsItem = "nonewprivs"
PamLimitsItemPriority PamLimitsItem = "priority"
PamLimitsItemLocks PamLimitsItem = "locks"
PamLimitsItemSigpending PamLimitsItem = "sigpending"
PamLimitsItemMsgqueue PamLimitsItem = "msgqueue"
PamLimitsItemNice PamLimitsItem = "nice"
PamLimitsItemRtprio PamLimitsItem = "rtprio"
)
// PamLimitsValue is defined to represent all valid types of the 'Value'
// item in the PamLimitsConfigLine structure.
type PamLimitsValue interface {
isPamLimitsValue()
}
// PamLimitsValueStr represents a string type of the 'Value' item in
// the PamLimitsConfigLine structure.
type PamLimitsValueStr string
func (v PamLimitsValueStr) isPamLimitsValue() {}
// Valid string values which can be used for the 'Value' item in
// the PamLimitsConfigLine structure.
const (
PamLimitsValueUnlimited PamLimitsValueStr = "unlimited"
PamLimitsValueInfinity PamLimitsValueStr = "infinity"
)
// PamLimitsValueInt represents an integer type of the 'Value' item in
// the PamLimitsConfigLine structure.
type PamLimitsValueInt int
func (v PamLimitsValueInt) isPamLimitsValue() {}
// PamLimitsConfigLine represents a single line in a pam_limits module configuration.
type PamLimitsConfigLine struct {
// Domain to which the limit applies. E.g. username, groupname, etc.
Domain string `json:"domain"`
// Type of the limit.
Type PamLimitsType `json:"type"`
// The resource type, which is being limited.
Item PamLimitsItem `json:"item"`
// The limit value.
Value PamLimitsValue `json:"value"`
}
// Unexported struct used for Unmarshalling of PamLimitsConfigLine due to
// 'value' being an integer or a string.
type rawPamLimitsConfigLine struct {
// Domain to which the limit applies. E.g. username, groupname, etc.
Domain string `json:"domain"`
// Type of the limit.
Type PamLimitsType `json:"type"`
// The resource type, which is being limited.
Item PamLimitsItem `json:"item"`
// The limit value.
Value interface{} `json:"value"`
}
func (l *PamLimitsConfigLine) UnmarshalJSON(data []byte) error {
var rawLine rawPamLimitsConfigLine
if err := json.Unmarshal(data, &rawLine); err != nil {
return err
}
var value PamLimitsValue
switch valueType := reflect.TypeOf(rawLine.Value); valueType.Kind() {
// json.Unmarshal() uses float64 for JSON numbers
// https://pkg.go.dev/encoding/json#Unmarshal
// However the expected value is only integer.
case reflect.Float64:
value = PamLimitsValueInt(rawLine.Value.(float64))
case reflect.String:
value = PamLimitsValueStr(rawLine.Value.(string))
default:
return fmt.Errorf("the 'value' item has unsupported type %q", valueType)
}
l.Domain = rawLine.Domain
l.Type = rawLine.Type
l.Item = rawLine.Item
l.Value = value
return nil
}

View file

@ -0,0 +1,52 @@
package osbuild2
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewPamLimitsConfStageOptions(t *testing.T) {
filename := "example.conf"
config := []PamLimitsConfigLine{{
Domain: "user1",
Type: PamLimitsTypeHard,
Item: PamLimitsItemCpu,
Value: PamLimitsValueInt(123),
}}
expectedOptions := &PamLimitsConfStageOptions{
Filename: filename,
Config: config,
}
actualOptions := NewPamLimitsConfStageOptions(filename, config)
assert.Equal(t, expectedOptions, actualOptions)
}
func TestNewPamLimitsConfStage(t *testing.T) {
expectedStage := &Stage{
Type: "org.osbuild.pam.limits.conf",
Options: &PamLimitsConfStageOptions{},
}
actualStage := NewPamLimitsConfStage(&PamLimitsConfStageOptions{})
assert.Equal(t, expectedStage, actualStage)
}
func TestPamLimitsConfStageOptions_MarshalJSON_Invalid(t *testing.T) {
tests := []struct {
name string
options PamLimitsConfStageOptions
}{
{
name: "empty-options",
options: PamLimitsConfStageOptions{},
},
}
for idx, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotBytes, err := json.Marshal(tt.options)
assert.NotNilf(t, err, "json.Marshal() didn't return an error, but: %s [idx: %d]", string(gotBytes), idx)
})
}
}

View file

@ -114,6 +114,8 @@ func (stage *Stage) UnmarshalJSON(data []byte) error {
options = new(OSTreeInitStageOptions)
case "org.osbuild.ostree.preptree":
options = new(OSTreePrepTreeStageOptions)
case "org.osbuild.pam.limits.conf":
options = new(PamLimitsConfStageOptions)
case "org.osbuild.truncate":
options = new(TruncateStageOptions)
case "org.osbuild.sfdisk":

View file

@ -328,6 +328,46 @@ func TestStage_UnmarshalJSON(t *testing.T) {
data: []byte(`{"type":"org.osbuild.locale","options":{"language":""}}`),
},
},
{
name: "pam-limits-conf-str",
fields: fields{
Type: "org.osbuild.pam.limits.conf",
Options: &PamLimitsConfStageOptions{
Filename: "example.conf",
Config: []PamLimitsConfigLine{
{
Domain: "user1",
Type: PamLimitsTypeHard,
Item: PamLimitsItemNofile,
Value: PamLimitsValueUnlimited,
},
},
},
},
args: args{
data: []byte(`{"type":"org.osbuild.pam.limits.conf","options":{"filename":"example.conf","config":[{"domain":"user1","type":"hard","item":"nofile","value":"unlimited"}]}}`),
},
},
{
name: "pam-limits-conf-int",
fields: fields{
Type: "org.osbuild.pam.limits.conf",
Options: &PamLimitsConfStageOptions{
Filename: "example.conf",
Config: []PamLimitsConfigLine{
{
Domain: "user1",
Type: PamLimitsTypeHard,
Item: PamLimitsItemNofile,
Value: PamLimitsValueInt(-1),
},
},
},
},
args: args{
data: []byte(`{"type":"org.osbuild.pam.limits.conf","options":{"filename":"example.conf","config":[{"domain":"user1","type":"hard","item":"nofile","value":-1}]}}`),
},
},
{
name: "rhsm-empty",
fields: fields{