osbuild2: support PermitRootLogin in sshd.config stage

Add support for the `PermitRootLogin` option in the `sshd.config` stage.
Valid values can be of type `bool` or `string`. Due to this reason, a
custom interface type is defined and a custom `UnmarshalJSON()` method is
defined for the `SshdConfigConfig` structure.

Modify unit tests to test the newly added option and test
(un)marhsalling of valid values of both types.

Related to https://github.com/osbuild/osbuild/pull/917

Signed-off-by: Tomas Hozza <thozza@redhat.com>
This commit is contained in:
Tomas Hozza 2021-12-13 18:03:39 +01:00 committed by Tomáš Hozza
parent 33c7da9dc3
commit 37a39743bc
3 changed files with 190 additions and 4 deletions

View file

@ -1,9 +1,78 @@
package osbuild2
import (
"encoding/json"
"fmt"
)
type SshdConfigConfig struct {
PasswordAuthentication *bool `json:"PasswordAuthentication,omitempty"`
ChallengeResponseAuthentication *bool `json:"ChallengeResponseAuthentication,omitempty"`
ClientAliveInterval *int `json:"ClientAliveInterval,omitempty"`
PasswordAuthentication *bool `json:"PasswordAuthentication,omitempty"`
ChallengeResponseAuthentication *bool `json:"ChallengeResponseAuthentication,omitempty"`
ClientAliveInterval *int `json:"ClientAliveInterval,omitempty"`
PermitRootLogin PermitRootLoginValue `json:"PermitRootLogin,omitempty"`
}
// PermitRootLoginValue is defined to represent all valid types of the
// 'PermitRootLogin' item in the SshdConfigConfig structure.
type PermitRootLoginValue interface {
isPermitRootLoginValue()
}
// PermitRootLoginValueStr represents a string type of the 'PermitRootLogin'
// item in the SshdConfigConfig structure.
type PermitRootLoginValueStr string
func (v PermitRootLoginValueStr) isPermitRootLoginValue() {}
// PermitRootLoginValueBool represents a bool type of the 'PermitRootLogin'
// item in the SshdConfigConfig structure.
type PermitRootLoginValueBool bool
func (v PermitRootLoginValueBool) isPermitRootLoginValue() {}
// Valid values which can be used for the 'PermitRootLogin' item in
// the SshdConfigConfig structure.
const (
PermitRootLoginValueYes PermitRootLoginValueBool = true
PermitRootLoginValueNo PermitRootLoginValueBool = false
PermitRootLoginValueProhibitPassword PermitRootLoginValueStr = "prohibit-password"
PermitRootLoginValueForcedCommandsOnly PermitRootLoginValueStr = "forced-commands-only"
)
// Unexported struct used for Unmarshalling of SshdConfigConfig due to
// 'PermitRootLogin' being a boolean or a string.
type rawSshdConfigConfig struct {
PasswordAuthentication *bool `json:"PasswordAuthentication,omitempty"`
ChallengeResponseAuthentication *bool `json:"ChallengeResponseAuthentication,omitempty"`
ClientAliveInterval *int `json:"ClientAliveInterval,omitempty"`
PermitRootLogin interface{} `json:"PermitRootLogin,omitempty"`
}
func (c *SshdConfigConfig) UnmarshalJSON(data []byte) error {
var rawConfig rawSshdConfigConfig
if err := json.Unmarshal(data, &rawConfig); err != nil {
return err
}
var permitRootLogin PermitRootLoginValue
if rawConfig.PermitRootLogin != nil {
switch valueType := rawConfig.PermitRootLogin.(type) {
case bool:
permitRootLogin = PermitRootLoginValueBool(rawConfig.PermitRootLogin.(bool))
case string:
permitRootLogin = PermitRootLoginValueStr(rawConfig.PermitRootLogin.(string))
default:
return fmt.Errorf("the 'PermitRootLogin' item has unsupported type %q", valueType)
}
}
c.PasswordAuthentication = rawConfig.PasswordAuthentication
c.ChallengeResponseAuthentication = rawConfig.ChallengeResponseAuthentication
c.ClientAliveInterval = rawConfig.ClientAliveInterval
c.PermitRootLogin = permitRootLogin
return nil
}
type SshdConfigStageOptions struct {
@ -12,7 +81,35 @@ type SshdConfigStageOptions struct {
func (SshdConfigStageOptions) isStageOptions() {}
func (o SshdConfigStageOptions) validate() error {
if o.Config.PermitRootLogin != nil {
value, ok := o.Config.PermitRootLogin.(PermitRootLoginValueStr)
if ok {
allowedPermitRootLoginStrValues := []PermitRootLoginValueStr{
PermitRootLoginValueForcedCommandsOnly,
PermitRootLoginValueProhibitPassword,
}
valid := false
for _, validValue := range allowedPermitRootLoginStrValues {
if value == validValue {
valid = true
break
}
}
if !valid {
return fmt.Errorf("%q is not a valid value for 'PermitRootLogin' option", value)
}
}
}
return nil
}
func NewSshdConfigStage(options *SshdConfigStageOptions) *Stage {
if err := options.validate(); err != nil {
panic(err)
}
return &Stage{
Type: "org.osbuild.sshd.config",
Options: options,

View file

@ -25,13 +25,15 @@ func TestJsonSshdConfigStage(t *testing.T) {
PasswordAuthentication: common.BoolToPtr(false),
ChallengeResponseAuthentication: common.BoolToPtr(false),
ClientAliveInterval: common.IntToPtr(180),
PermitRootLogin: PermitRootLoginValueProhibitPassword,
},
}
inputString := `{
"config": {
"PasswordAuthentication": false,
"ChallengeResponseAuthentication": false,
"ClientAliveInterval": 180
"ClientAliveInterval": 180,
"PermitRootLogin": "prohibit-password"
}
}`
var inputOptions SshdConfigStageOptions
@ -51,3 +53,56 @@ func TestJsonSshdConfigStage(t *testing.T) {
assert.NoError(t, err, "failed to marshal sshd config into JSON")
assert.Equal(t, expectedString, string(inputBytes))
}
func TestSshdConfigStageOptionsValidate(t *testing.T) {
tests := []struct {
name string
options SshdConfigStageOptions
err bool
}{
{
name: "empty-options",
options: SshdConfigStageOptions{},
err: false,
},
{
name: "invalid-permit-root-login-str-value",
options: SshdConfigStageOptions{
Config: SshdConfigConfig{
PermitRootLogin: PermitRootLoginValueStr("invalid"),
},
},
err: true,
},
{
name: "valid-permit-root-login-str-value-1",
options: SshdConfigStageOptions{
Config: SshdConfigConfig{
PermitRootLogin: PermitRootLoginValueForcedCommandsOnly,
},
},
err: false,
},
{
name: "valid-permit-root-login-str-value-1",
options: SshdConfigStageOptions{
Config: SshdConfigConfig{
PermitRootLogin: PermitRootLoginValueProhibitPassword,
},
},
err: false,
},
}
for idx, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.err {
assert.Errorf(t, tt.options.validate(), "%q didn't return an error [idx: %d]", tt.name, idx)
assert.Panics(t, func() { NewSshdConfigStage(&tt.options) })
} else {
assert.NoErrorf(t, tt.options.validate(), "%q returned an error [idx: %d]", tt.name, idx)
assert.NotPanics(t, func() { NewSshdConfigStage(&tt.options) })
}
})
}
}

View file

@ -664,6 +664,40 @@ func TestStage_UnmarshalJSON(t *testing.T) {
data: []byte(`{"type":"org.osbuild.sshd.config","options":{"config":{}}}`),
},
},
{
name: "sshd.config-data1",
fields: fields{
Type: "org.osbuild.sshd.config",
Options: &SshdConfigStageOptions{
Config: SshdConfigConfig{
PasswordAuthentication: common.BoolToPtr(false),
ChallengeResponseAuthentication: common.BoolToPtr(false),
ClientAliveInterval: common.IntToPtr(42),
PermitRootLogin: PermitRootLoginValueNo,
},
},
},
args: args{
data: []byte(`{"type":"org.osbuild.sshd.config","options":{"config":{"PasswordAuthentication":false,"ChallengeResponseAuthentication":false,"ClientAliveInterval":42,"PermitRootLogin":false}}}`),
},
},
{
name: "sshd.config-data2",
fields: fields{
Type: "org.osbuild.sshd.config",
Options: &SshdConfigStageOptions{
Config: SshdConfigConfig{
PasswordAuthentication: common.BoolToPtr(false),
ChallengeResponseAuthentication: common.BoolToPtr(false),
ClientAliveInterval: common.IntToPtr(42),
PermitRootLogin: PermitRootLoginValueForcedCommandsOnly,
},
},
},
args: args{
data: []byte(`{"type":"org.osbuild.sshd.config","options":{"config":{"PasswordAuthentication":false,"ChallengeResponseAuthentication":false,"ClientAliveInterval":42,"PermitRootLogin":"forced-commands-only"}}}`),
},
},
{
name: "authconfig",
fields: fields{