Add validation to data directories on creation

This commit is contained in:
Jake Hyde 2024-08-13 22:53:24 -04:00
parent 40e8eae329
commit 0256fa138a
No known key found for this signature in database
GPG Key ID: 5DD41006F2B9D142
2 changed files with 371 additions and 18 deletions

View File

@ -5,8 +5,10 @@ import (
"encoding/base64"
"fmt"
"net/http"
"path/filepath"
"regexp"
"slices"
"strings"
v1 "github.com/rancher/rancher/pkg/apis/provisioning.cattle.io/v1"
rkev1 "github.com/rancher/rancher/pkg/apis/rke.cattle.io/v1"
@ -168,7 +170,8 @@ func (p *provisioningAdmitter) validateSystemAgentDataDirectory(oldCluster, newC
return admission.ResponseAllowed()
}
// validateDataDirectories will validate updates to the cluster object to ensure the data directories are not changed.
// validateDataDirectories will ensure that data directories are properly formatted on creation, not duplicated or embed
// each other, and will also validate updates to the cluster object to ensure the data directories are not changed.
// The only exception when a data directory is allowed to be changed is if cluster.Spec.AgentEnvVars has an env var with
// a name of "CATTLE_AGENT_VAR_DIR", which Rancher will perform a one-time migration to set the
// cluster.Spec.RKEConfig.DataDirectories.SystemAgent field for the cluster. validateAgentEnvVars will ensure
@ -177,6 +180,9 @@ func (p *provisioningAdmitter) validateDataDirectories(request *admission.Reques
if newCluster.Spec.RKEConfig == nil {
return admission.ResponseAllowed()
}
distro := newCluster.Spec.RKEConfig.DataDirectories.K8sDistro
provisioning := newCluster.Spec.RKEConfig.DataDirectories.Provisioning
systemAgent := newCluster.Spec.RKEConfig.DataDirectories.SystemAgent
// cannot set "CATTLE_AGENT_VAR_DIR" on create anymore, but still valid as a field until cluster is migrated.
if request.Operation == admissionv1.Create {
if slices.ContainsFunc(newCluster.Spec.AgentEnvVars, func(envVar rkev1.EnvVar) bool {
@ -185,6 +191,21 @@ func (p *provisioningAdmitter) validateDataDirectories(request *admission.Reques
return admission.ResponseBadRequest(
fmt.Sprintf(`"%s" cannot be set within "cluster.Spec.RKEConfig.AgentEnvVars": use "cluster.Spec.RKEConfig.DataDirectories.SystemAgent"`, systemAgentVarDirEnvVar))
}
dataDirectories := map[string]string{
"Distro": distro,
"Provisioning": provisioning,
"System Agent": systemAgent,
}
for name, dir := range dataDirectories {
response := validateDataDirectoryFormat(dir, name)
if !response.Allowed {
return response
}
}
response := validateDataDirectoryHierarchy(dataDirectories)
if !response.Allowed {
return response
}
return admission.ResponseAllowed()
}
if request.Operation != admissionv1.Update {
@ -194,11 +215,87 @@ func (p *provisioningAdmitter) validateDataDirectories(request *admission.Reques
if response := p.validateSystemAgentDataDirectory(oldCluster, newCluster); !response.Allowed {
return response
}
if oldCluster.Spec.RKEConfig.DataDirectories.Provisioning != newCluster.Spec.RKEConfig.DataDirectories.Provisioning {
if oldCluster.Spec.RKEConfig.DataDirectories.K8sDistro != distro {
return admission.ResponseBadRequest("Distro data directory cannot be changed after cluster creation")
}
if oldCluster.Spec.RKEConfig.DataDirectories.Provisioning != provisioning {
return admission.ResponseBadRequest("Provisioning data directory cannot be changed after cluster creation")
}
if oldCluster.Spec.RKEConfig.DataDirectories.K8sDistro != newCluster.Spec.RKEConfig.DataDirectories.K8sDistro {
return admission.ResponseBadRequest("Distro data directory cannot be changed after cluster creation")
return admission.ResponseAllowed()
}
// validateDataDirectoryFormat ensures that no data directory contains a relative path, environment variables,
// shell expressions, or references to the current or parent directory via use of "./" and "../" respectively.
// dir is the path of the data directory, and name corresponds to a print friendly name for this data directory.
func validateDataDirectoryFormat(dir, name string) *admissionv1.AdmissionResponse {
if dir == "" {
return admission.ResponseAllowed()
}
if !filepath.IsAbs(dir) {
return admission.ResponseBadRequest(
fmt.Sprintf("%s data directory must be an absolute path", name))
}
if strings.ContainsAny(dir, "\"'`*?#~=%$|&;<>{}[]()") {
return admission.ResponseBadRequest(
fmt.Sprintf("%s data directory cannot contain shell expressions", name))
}
if filepath.Clean(dir) != dir {
return admission.ResponseBadRequest(
fmt.Sprintf("%s data directory is not clean", name))
}
return admission.ResponseAllowed()
}
// validateDataDirectoryHierarchy ensures that no directories are equal, and no directories include other directories.
// dataDirs is a map with keys corresponding to print friendly names for these data directories, and values representing
// the specific data directories.
func validateDataDirectoryHierarchy(dataDirs map[string]string) *admissionv1.AdmissionResponse {
paths := make([]struct {
name string
path string
}, 0, len(dataDirs))
for name, dir := range dataDirs {
// do not attempt to validate empty directory
if dir == "" {
continue
}
paths = append(paths, struct {
name string
path string
}{
name: name,
path: dir,
})
}
for i := range paths {
for j := i + 1; j < len(paths); j++ {
path1 := paths[i]
path2 := paths[j]
if path1.path == path2.path {
return admission.ResponseBadRequest(
fmt.Sprintf("%s data directory cannot be equal to %s data directory", path1.name, path2.name))
}
// check if paths contain one another
if matched, err := filepath.Match(fmt.Sprintf("%s%c*", path1.path, filepath.Separator), path2.path); err != nil {
return admission.ResponseBadRequest(
fmt.Sprintf("error determining if %s data directory is nested inside %s data directory: %s", path2.name, path1.name, err.Error()))
} else if matched {
return admission.ResponseBadRequest(
fmt.Sprintf("%s data directory cannot be nested inside %s data directory", path2.name, path1.name))
}
if matched, err := filepath.Match(fmt.Sprintf("%s%c*", path2.path, filepath.Separator), path1.path); err != nil {
return admission.ResponseBadRequest(
fmt.Sprintf("error determining if %s data directory is nested inside %s data directory: %s", path1.name, path2.name, err.Error()))
} else if matched {
return admission.ResponseBadRequest(
fmt.Sprintf("%s data directory cannot be nested inside %s data directory", path1.name, path2.name))
}
}
}
return admission.ResponseAllowed()

View File

@ -652,7 +652,7 @@ func TestValidateDataDirectories(t *testing.T) {
AgentEnvVars: []rkev1.EnvVar{
{
Name: "CATTLE_AGENT_VAR_DIR",
Value: "a",
Value: "/a",
},
},
},
@ -660,6 +660,139 @@ func TestValidateDataDirectories(t *testing.T) {
oldCluster: nil,
shouldSucceed: false,
},
{
name: "CREATE distro data dir is relative",
request: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create}},
cluster: &v1.Cluster{
Spec: v1.ClusterSpec{
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
K8sDistro: "a",
},
},
},
},
},
shouldSucceed: false,
},
{
name: "CREATE provisioning data dir is relative",
request: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create}},
cluster: &v1.Cluster{
Spec: v1.ClusterSpec{
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
Provisioning: "a",
},
},
},
},
},
shouldSucceed: false,
},
{
name: "CREATE system agent data dir is relative",
request: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create}},
cluster: &v1.Cluster{
Spec: v1.ClusterSpec{
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
SystemAgent: "a",
},
},
},
},
},
shouldSucceed: false,
},
{
name: "CREATE distro data dir == provisioning data dir",
request: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create}},
cluster: &v1.Cluster{
Spec: v1.ClusterSpec{
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
K8sDistro: "/a",
Provisioning: "/a",
},
},
},
},
},
shouldSucceed: false,
},
{
name: "CREATE distro data dir == system agent data dir",
request: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create}},
cluster: &v1.Cluster{
Spec: v1.ClusterSpec{
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
K8sDistro: "/a",
SystemAgent: "/a",
},
},
},
},
},
shouldSucceed: false,
},
{
name: "CREATE provisioning data dir == system agent data dir",
request: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create}},
cluster: &v1.Cluster{
Spec: v1.ClusterSpec{
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
Provisioning: "/a",
SystemAgent: "/a",
},
},
},
},
},
shouldSucceed: false,
},
{
name: "CREATE distro data dir contains provisioning data dir",
request: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create}},
cluster: &v1.Cluster{
Spec: v1.ClusterSpec{
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
K8sDistro: "/a",
Provisioning: "/a/b",
},
},
},
},
},
shouldSucceed: false,
},
{
name: "CREATE provisioning data dir contains distro data dir",
request: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Create}},
cluster: &v1.Cluster{
Spec: v1.ClusterSpec{
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
K8sDistro: "/a/b",
Provisioning: "/a",
},
},
},
},
},
shouldSucceed: false,
},
{
name: "Delete",
request: &admission.Request{AdmissionRequest: admissionv1.AdmissionRequest{Operation: admissionv1.Delete}},
@ -669,7 +802,7 @@ func TestValidateDataDirectories(t *testing.T) {
AgentEnvVars: []rkev1.EnvVar{
{
Name: "CATTLE_AGENT_VAR_DIR",
Value: "a",
Value: "/a",
},
},
},
@ -710,7 +843,7 @@ func TestValidateDataDirectories(t *testing.T) {
AgentEnvVars: []rkev1.EnvVar{
{
Name: "CATTLE_AGENT_VAR_DIR",
Value: "a",
Value: "/a",
},
},
},
@ -721,7 +854,7 @@ func TestValidateDataDirectories(t *testing.T) {
AgentEnvVars: []rkev1.EnvVar{
{
Name: "CATTLE_AGENT_VAR_DIR",
Value: "a",
Value: "/a",
},
},
},
@ -737,7 +870,7 @@ func TestValidateDataDirectories(t *testing.T) {
AgentEnvVars: []rkev1.EnvVar{
{
Name: "CATTLE_AGENT_VAR_DIR",
Value: "a",
Value: "/a",
},
},
},
@ -748,7 +881,7 @@ func TestValidateDataDirectories(t *testing.T) {
AgentEnvVars: []rkev1.EnvVar{
{
Name: "CATTLE_AGENT_VAR_DIR",
Value: "b",
Value: "/b",
},
},
},
@ -763,7 +896,7 @@ func TestValidateDataDirectories(t *testing.T) {
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
SystemAgent: "a",
SystemAgent: "/a",
},
},
},
@ -775,7 +908,7 @@ func TestValidateDataDirectories(t *testing.T) {
AgentEnvVars: []rkev1.EnvVar{
{
Name: "CATTLE_AGENT_VAR_DIR",
Value: "a",
Value: "/a",
},
},
},
@ -790,7 +923,7 @@ func TestValidateDataDirectories(t *testing.T) {
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
SystemAgent: "a",
SystemAgent: "/a",
},
},
},
@ -801,7 +934,7 @@ func TestValidateDataDirectories(t *testing.T) {
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
SystemAgent: "b",
SystemAgent: "/b",
},
},
},
@ -817,7 +950,7 @@ func TestValidateDataDirectories(t *testing.T) {
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
Provisioning: "a",
Provisioning: "/a",
},
},
},
@ -828,7 +961,7 @@ func TestValidateDataDirectories(t *testing.T) {
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
Provisioning: "b",
Provisioning: "/b",
},
},
},
@ -844,7 +977,7 @@ func TestValidateDataDirectories(t *testing.T) {
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
K8sDistro: "a",
K8sDistro: "/a",
},
},
},
@ -855,7 +988,7 @@ func TestValidateDataDirectories(t *testing.T) {
RKEConfig: &v1.RKEConfig{
RKEClusterSpecCommon: rkev1.RKEClusterSpecCommon{
DataDirectories: rkev1.DataDirectories{
K8sDistro: "b",
K8sDistro: "/b",
},
},
},
@ -875,3 +1008,126 @@ func TestValidateDataDirectories(t *testing.T) {
})
}
}
func TestValidateDataDirectoryFormat(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dir string
expected bool
}{
{
name: "relative",
dir: "home",
expected: false,
},
{
name: "trailing slash",
dir: "/home/",
expected: false,
},
{
name: "env var",
dir: "/$HOME",
expected: false,
},
{
name: "env var",
dir: "/${HOME}",
expected: false,
},
{
name: "expr",
dir: "/`pwd`",
expected: false,
},
{
name: "expr",
dir: "/$(pwd)",
expected: false,
},
{
name: "current directory",
dir: "/./tmp",
expected: false,
},
{
name: "current directory",
dir: "/tmp/.",
expected: false,
},
{
name: "parent directory",
dir: "/tmp/../tmp",
expected: false,
},
{
name: "current directory",
dir: "/tmp/..",
expected: false,
},
{
name: "valid",
dir: "/tmp",
expected: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
response := validateDataDirectoryFormat(tt.dir, "Test")
assert.Equal(t, tt.expected, response.Allowed)
})
}
}
func TestValidateDataDirectoryHierarchy(t *testing.T) {
t.Parallel()
tests := []struct {
name string
dataDirs map[string]string
expected bool
}{
{
name: "equal paths",
dataDirs: map[string]string{
"a": "/a",
"b": "/a",
},
expected: false,
},
{
name: "nested paths",
dataDirs: map[string]string{
"a": "/a",
"b": "/a/b",
},
expected: false,
},
{
name: "nested paths",
dataDirs: map[string]string{
"a": "/a/b",
"b": "/a",
},
expected: false,
},
{
name: "distinct paths",
dataDirs: map[string]string{
"a": "/a",
"b": "/b",
},
expected: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
response := validateDataDirectoryHierarchy(tt.dataDirs)
assert.Equal(t, tt.expected, response.Allowed)
})
}
}