mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 04:54:54 +00:00
remove unused file
This commit is contained in:
@@ -27,7 +27,6 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pmezard/go-difflib/difflib"
|
||||
|
||||
@@ -43,7 +42,6 @@ import (
|
||||
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/util/errors"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/util/patches"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/util/users"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -54,11 +52,6 @@ const (
|
||||
kubeSchedulerBindAddressArg = "bind-address"
|
||||
)
|
||||
|
||||
var (
|
||||
usersAndGroups *users.UsersAndGroups
|
||||
usersAndGroupsOnce sync.Once
|
||||
)
|
||||
|
||||
// ComponentPod returns a Pod object from the container, volume and annotations specifications
|
||||
func ComponentPod(container v1.Container, volumes map[string]v1.Volume, annotations map[string]string) v1.Pod {
|
||||
// priority value for system-node-critical class
|
||||
@@ -418,16 +411,6 @@ func getProbeAddress(addr string) string {
|
||||
return addr
|
||||
}
|
||||
|
||||
// GetUsersAndGroups returns the local usersAndGroups, but first creates it
|
||||
// in a thread safe way once.
|
||||
func GetUsersAndGroups() (*users.UsersAndGroups, error) {
|
||||
var err error
|
||||
usersAndGroupsOnce.Do(func() {
|
||||
usersAndGroups, err = users.AddUsersAndGroups()
|
||||
})
|
||||
return usersAndGroups, err
|
||||
}
|
||||
|
||||
// DeepHashObject writes specified object to hash using the spew library
|
||||
// which follows pointers and prints actual values of the nested objects
|
||||
// ensuring the hash does not change when a pointer changes.
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package staticpod
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
||||
certphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/certs"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/util/errors"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/util/users"
|
||||
)
|
||||
|
||||
type pathOwnerAndPermissionsUpdaterFunc func(path string, uid, gid int64, perms uint32) error
|
||||
type pathOwnerUpdaterFunc func(path string, uid, gid int64) error
|
||||
|
||||
// RunComponentAsNonRoot updates the pod manifest and the hostVolume permissions to run as non root.
|
||||
func RunComponentAsNonRoot(componentName string, pod *v1.Pod, usersAndGroups *users.UsersAndGroups, cfg *kubeadmapi.ClusterConfiguration) error {
|
||||
switch componentName {
|
||||
case kubeadmconstants.KubeAPIServer:
|
||||
return runKubeAPIServerAsNonRoot(
|
||||
pod,
|
||||
usersAndGroups.Users.ID(kubeadmconstants.KubeAPIServerUserName),
|
||||
usersAndGroups.Groups.ID(kubeadmconstants.KubeAPIServerUserName),
|
||||
usersAndGroups.Groups.ID(kubeadmconstants.ServiceAccountKeyReadersGroupName),
|
||||
users.UpdatePathOwnerAndPermissions,
|
||||
cfg,
|
||||
)
|
||||
case kubeadmconstants.KubeControllerManager:
|
||||
return runKubeControllerManagerAsNonRoot(
|
||||
pod,
|
||||
usersAndGroups.Users.ID(kubeadmconstants.KubeControllerManagerUserName),
|
||||
usersAndGroups.Groups.ID(kubeadmconstants.KubeControllerManagerUserName),
|
||||
usersAndGroups.Groups.ID(kubeadmconstants.ServiceAccountKeyReadersGroupName),
|
||||
users.UpdatePathOwnerAndPermissions,
|
||||
cfg,
|
||||
)
|
||||
case kubeadmconstants.KubeScheduler:
|
||||
return runKubeSchedulerAsNonRoot(
|
||||
pod,
|
||||
usersAndGroups.Users.ID(kubeadmconstants.KubeSchedulerUserName),
|
||||
usersAndGroups.Groups.ID(kubeadmconstants.KubeSchedulerUserName),
|
||||
users.UpdatePathOwnerAndPermissions,
|
||||
)
|
||||
case kubeadmconstants.Etcd:
|
||||
return runEtcdAsNonRoot(
|
||||
pod,
|
||||
usersAndGroups.Users.ID(kubeadmconstants.EtcdUserName),
|
||||
usersAndGroups.Groups.ID(kubeadmconstants.EtcdUserName),
|
||||
users.UpdatePathOwnerAndPermissions,
|
||||
users.UpdatePathOwner,
|
||||
cfg,
|
||||
)
|
||||
}
|
||||
return errors.New(fmt.Sprintf("component name %q is not valid", componentName))
|
||||
}
|
||||
|
||||
// runKubeAPIServerAsNonRoot updates the pod manifest and the hostVolume permissions to run kube-apiserver as non root.
|
||||
func runKubeAPIServerAsNonRoot(pod *v1.Pod, runAsUser, runAsGroup, supplementalGroup *int64, updatePathOwnerAndPermissions pathOwnerAndPermissionsUpdaterFunc, cfg *kubeadmapi.ClusterConfiguration) error {
|
||||
saPublicKeyFile := filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPublicKeyName)
|
||||
if err := updatePathOwnerAndPermissions(saPublicKeyFile, *runAsUser, *runAsGroup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
saPrivateKeyFile := filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPrivateKeyName)
|
||||
if err := updatePathOwnerAndPermissions(saPrivateKeyFile, 0, *supplementalGroup, 0640); err != nil {
|
||||
return err
|
||||
}
|
||||
apiServerKeyFile := filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKeyName)
|
||||
if err := updatePathOwnerAndPermissions(apiServerKeyFile, *runAsUser, *runAsGroup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
apiServerKubeletClientKeyFile := filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKubeletClientKeyName)
|
||||
if err := updatePathOwnerAndPermissions(apiServerKubeletClientKeyFile, *runAsUser, *runAsGroup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
frontProxyClientKeyName := filepath.Join(cfg.CertificatesDir, kubeadmconstants.FrontProxyClientKeyName)
|
||||
if err := updatePathOwnerAndPermissions(frontProxyClientKeyName, *runAsUser, *runAsGroup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Etcd.External == nil {
|
||||
apiServerEtcdClientKeyFile := filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerEtcdClientKeyName)
|
||||
if err := updatePathOwnerAndPermissions(apiServerEtcdClientKeyFile, *runAsUser, *runAsGroup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pod.Spec.Containers[0].SecurityContext = &v1.SecurityContext{
|
||||
Capabilities: &v1.Capabilities{
|
||||
// We drop all capabilities that are added by default.
|
||||
Drop: []v1.Capability{"ALL"},
|
||||
// kube-apiserver binary has the file capability cap_net_bind_service applied to it.
|
||||
// This means that we must add this capability when running as non-root even if the
|
||||
// capability is not required.
|
||||
Add: []v1.Capability{"NET_BIND_SERVICE"},
|
||||
},
|
||||
}
|
||||
pod.Spec.SecurityContext.RunAsGroup = runAsGroup
|
||||
pod.Spec.SecurityContext.RunAsUser = runAsUser
|
||||
pod.Spec.SecurityContext.SupplementalGroups = []int64{*supplementalGroup}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runKubeControllerManagerAsNonRoot updates the pod manifest and the hostVolume permissions to run kube-controller-manager as non root.
|
||||
func runKubeControllerManagerAsNonRoot(pod *v1.Pod, runAsUser, runAsGroup, supplementalGroup *int64, updatePathOwnerAndPermissions pathOwnerAndPermissionsUpdaterFunc, cfg *kubeadmapi.ClusterConfiguration) error {
|
||||
kubeconfigFile := filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ControllerManagerKubeConfigFileName)
|
||||
if err := updatePathOwnerAndPermissions(kubeconfigFile, *runAsUser, *runAsGroup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
saPrivateKeyFile := filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPrivateKeyName)
|
||||
if err := updatePathOwnerAndPermissions(saPrivateKeyFile, 0, *supplementalGroup, 0640); err != nil {
|
||||
return err
|
||||
}
|
||||
if res, _ := certphase.UsingExternalCA(cfg); !res {
|
||||
caKeyFile := filepath.Join(cfg.CertificatesDir, kubeadmconstants.CAKeyName)
|
||||
err := updatePathOwnerAndPermissions(caKeyFile, *runAsUser, *runAsGroup, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
pod.Spec.Containers[0].SecurityContext = &v1.SecurityContext{
|
||||
AllowPrivilegeEscalation: ptr.To(false),
|
||||
Capabilities: &v1.Capabilities{
|
||||
// We drop all capabilities that are added by default.
|
||||
Drop: []v1.Capability{"ALL"},
|
||||
},
|
||||
}
|
||||
pod.Spec.SecurityContext.RunAsUser = runAsUser
|
||||
pod.Spec.SecurityContext.RunAsGroup = runAsGroup
|
||||
pod.Spec.SecurityContext.SupplementalGroups = []int64{*supplementalGroup}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runKubeSchedulerAsNonRoot updates the pod manifest and the hostVolume permissions to run kube-scheduler as non root.
|
||||
func runKubeSchedulerAsNonRoot(pod *v1.Pod, runAsUser, runAsGroup *int64, updatePathOwnerAndPermissions pathOwnerAndPermissionsUpdaterFunc) error {
|
||||
kubeconfigFile := filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.SchedulerKubeConfigFileName)
|
||||
if err := updatePathOwnerAndPermissions(kubeconfigFile, *runAsUser, *runAsGroup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
pod.Spec.Containers[0].SecurityContext = &v1.SecurityContext{
|
||||
AllowPrivilegeEscalation: ptr.To(false),
|
||||
// We drop all capabilities that are added by default.
|
||||
Capabilities: &v1.Capabilities{
|
||||
Drop: []v1.Capability{"ALL"},
|
||||
},
|
||||
}
|
||||
pod.Spec.SecurityContext.RunAsUser = runAsUser
|
||||
pod.Spec.SecurityContext.RunAsGroup = runAsGroup
|
||||
return nil
|
||||
}
|
||||
|
||||
// runEtcdAsNonRoot updates the pod manifest and the hostVolume permissions to run etcd as non root.
|
||||
func runEtcdAsNonRoot(pod *v1.Pod, runAsUser, runAsGroup *int64, updatePathOwnerAndPermissions pathOwnerAndPermissionsUpdaterFunc, updatePathOwner pathOwnerUpdaterFunc, cfg *kubeadmapi.ClusterConfiguration) error {
|
||||
if err := updatePathOwner(cfg.Etcd.Local.DataDir, *runAsUser, *runAsGroup); err != nil {
|
||||
return err
|
||||
}
|
||||
etcdServerKeyFile := filepath.Join(cfg.CertificatesDir, kubeadmconstants.EtcdServerKeyName)
|
||||
if err := updatePathOwnerAndPermissions(etcdServerKeyFile, *runAsUser, *runAsGroup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
etcdPeerKeyFile := filepath.Join(cfg.CertificatesDir, kubeadmconstants.EtcdPeerKeyName)
|
||||
if err := updatePathOwnerAndPermissions(etcdPeerKeyFile, *runAsUser, *runAsGroup, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
pod.Spec.Containers[0].SecurityContext = &v1.SecurityContext{
|
||||
AllowPrivilegeEscalation: ptr.To(false),
|
||||
// We drop all capabilities that are added by default.
|
||||
Capabilities: &v1.Capabilities{
|
||||
Drop: []v1.Capability{"ALL"},
|
||||
},
|
||||
}
|
||||
pod.Spec.SecurityContext.RunAsUser = runAsUser
|
||||
pod.Spec.SecurityContext.RunAsGroup = runAsGroup
|
||||
return nil
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package staticpod
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
||||
)
|
||||
|
||||
type ownerAndPermissions struct {
|
||||
uid int64
|
||||
gid int64
|
||||
permissions uint32
|
||||
}
|
||||
|
||||
func verifyPodSecurityContext(t *testing.T, pod *v1.Pod, wantRunAsUser, wantRunAsGroup int64, wantSupGroup []int64) {
|
||||
t.Helper()
|
||||
wantPodSecurityContext := &v1.PodSecurityContext{
|
||||
RunAsUser: ptr.To(wantRunAsUser),
|
||||
RunAsGroup: ptr.To(wantRunAsGroup),
|
||||
SupplementalGroups: wantSupGroup,
|
||||
SeccompProfile: &v1.SeccompProfile{
|
||||
Type: v1.SeccompProfileTypeRuntimeDefault,
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(wantPodSecurityContext, pod.Spec.SecurityContext) {
|
||||
t.Errorf("unexpected diff in PodSecurityContext, want: %+v, got: %+v", wantPodSecurityContext, pod.Spec.SecurityContext)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyContainerSecurityContext(t *testing.T, container v1.Container, addCaps, dropCaps []v1.Capability, allowPrivielege *bool) {
|
||||
t.Helper()
|
||||
wantContainerSecurityContext := &v1.SecurityContext{
|
||||
AllowPrivilegeEscalation: allowPrivielege,
|
||||
Capabilities: &v1.Capabilities{
|
||||
Add: addCaps,
|
||||
Drop: dropCaps,
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(wantContainerSecurityContext, container.SecurityContext) {
|
||||
t.Errorf("unexpected diff in container SecurityContext, want: %+v, got: %+v", wantContainerSecurityContext, container.SecurityContext)
|
||||
}
|
||||
}
|
||||
|
||||
func verifyFilePermissions(t *testing.T, updatedFiles, wantFiles map[string]ownerAndPermissions) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(updatedFiles, wantFiles) {
|
||||
t.Errorf("unexpected diff in file owners and permissions want: %+v, got: %+v", wantFiles, updatedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunKubeAPIServerAsNonRoot(t *testing.T) {
|
||||
cfg := &kubeadm.ClusterConfiguration{}
|
||||
pod := ComponentPod(v1.Container{Name: "kube-apiserver"}, nil, nil)
|
||||
var runAsUser, runAsGroup, supGroup int64 = 1000, 1001, 1002
|
||||
updatedFiles := map[string]ownerAndPermissions{}
|
||||
if err := runKubeAPIServerAsNonRoot(&pod, &runAsUser, &runAsGroup, &supGroup, func(path string, uid, gid int64, perms uint32) error {
|
||||
updatedFiles[path] = ownerAndPermissions{uid: uid, gid: gid, permissions: perms}
|
||||
return nil
|
||||
}, cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyPodSecurityContext(t, &pod, runAsUser, runAsGroup, []int64{supGroup})
|
||||
verifyContainerSecurityContext(t, pod.Spec.Containers[0], []v1.Capability{"NET_BIND_SERVICE"}, []v1.Capability{"ALL"}, nil)
|
||||
wantUpdateFiles := map[string]ownerAndPermissions{
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPublicKeyName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPrivateKeyName): {uid: 0, gid: supGroup, permissions: 0640},
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKeyName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerKubeletClientKeyName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.FrontProxyClientKeyName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.APIServerEtcdClientKeyName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
}
|
||||
verifyFilePermissions(t, updatedFiles, wantUpdateFiles)
|
||||
}
|
||||
|
||||
func TestRunKubeControllerManagerAsNonRoot(t *testing.T) {
|
||||
cfg := &kubeadm.ClusterConfiguration{}
|
||||
pod := ComponentPod(v1.Container{Name: "kube-controller-manager"}, nil, nil)
|
||||
var runAsUser, runAsGroup, supGroup int64 = 1000, 1001, 1002
|
||||
updatedFiles := map[string]ownerAndPermissions{}
|
||||
if err := runKubeControllerManagerAsNonRoot(&pod, &runAsUser, &runAsGroup, &supGroup, func(path string, uid, gid int64, perms uint32) error {
|
||||
updatedFiles[path] = ownerAndPermissions{uid: uid, gid: gid, permissions: perms}
|
||||
return nil
|
||||
}, cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyPodSecurityContext(t, &pod, runAsUser, runAsGroup, []int64{supGroup})
|
||||
verifyContainerSecurityContext(t, pod.Spec.Containers[0], nil, []v1.Capability{"ALL"}, ptr.To(false))
|
||||
wantUpdateFiles := map[string]ownerAndPermissions{
|
||||
filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.ControllerManagerKubeConfigFileName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.ServiceAccountPrivateKeyName): {uid: 0, gid: supGroup, permissions: 0640},
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.CAKeyName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
}
|
||||
verifyFilePermissions(t, updatedFiles, wantUpdateFiles)
|
||||
}
|
||||
|
||||
func TestRunKubeSchedulerAsNonRoot(t *testing.T) {
|
||||
pod := ComponentPod(v1.Container{Name: "kube-scheduler"}, nil, nil)
|
||||
var runAsUser, runAsGroup int64 = 1000, 1001
|
||||
updatedFiles := map[string]ownerAndPermissions{}
|
||||
if err := runKubeSchedulerAsNonRoot(&pod, &runAsUser, &runAsGroup, func(path string, uid, gid int64, perms uint32) error {
|
||||
updatedFiles[path] = ownerAndPermissions{uid: uid, gid: gid, permissions: perms}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyPodSecurityContext(t, &pod, runAsUser, runAsGroup, nil)
|
||||
verifyContainerSecurityContext(t, pod.Spec.Containers[0], nil, []v1.Capability{"ALL"}, ptr.To(false))
|
||||
wantUpdateFiles := map[string]ownerAndPermissions{
|
||||
filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.SchedulerKubeConfigFileName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
}
|
||||
verifyFilePermissions(t, updatedFiles, wantUpdateFiles)
|
||||
}
|
||||
|
||||
func TestRunEtcdAsNonRoot(t *testing.T) {
|
||||
cfg := &kubeadm.ClusterConfiguration{
|
||||
Etcd: kubeadm.Etcd{
|
||||
Local: &kubeadm.LocalEtcd{
|
||||
DataDir: "/var/lib/etcd/data",
|
||||
},
|
||||
},
|
||||
}
|
||||
pod := ComponentPod(v1.Container{Name: "etcd"}, nil, nil)
|
||||
var runAsUser, runAsGroup int64 = 1000, 1001
|
||||
updatedFiles := map[string]ownerAndPermissions{}
|
||||
if err := runEtcdAsNonRoot(&pod, &runAsUser, &runAsGroup, func(path string, uid, gid int64, perms uint32) error {
|
||||
updatedFiles[path] = ownerAndPermissions{uid: uid, gid: gid, permissions: perms}
|
||||
return nil
|
||||
},
|
||||
func(path string, uid, gid int64) error {
|
||||
updatedFiles[path] = ownerAndPermissions{uid: uid, gid: gid, permissions: 0700}
|
||||
return nil
|
||||
}, cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyPodSecurityContext(t, &pod, runAsUser, runAsGroup, nil)
|
||||
verifyContainerSecurityContext(t, pod.Spec.Containers[0], nil, []v1.Capability{"ALL"}, ptr.To(false))
|
||||
wantUpdateFiles := map[string]ownerAndPermissions{
|
||||
cfg.Etcd.Local.DataDir: {uid: runAsUser, gid: runAsGroup, permissions: 0700},
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.EtcdServerKeyName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
filepath.Join(cfg.CertificatesDir, kubeadmconstants.EtcdPeerKeyName): {uid: runAsUser, gid: runAsGroup, permissions: 0600},
|
||||
}
|
||||
verifyFilePermissions(t, updatedFiles, wantUpdateFiles)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package staticpod
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
||||
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/util/users"
|
||||
)
|
||||
|
||||
// RunComponentAsNonRoot is a NO-OP on non linux.
|
||||
func RunComponentAsNonRoot(componentName string, pod *v1.Pod, usersAndGroups *users.UsersAndGroups, cfg *kubeadmapi.ClusterConfiguration) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,658 +0,0 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
|
||||
"k8s.io/kubernetes/cmd/kubeadm/app/util/errors"
|
||||
)
|
||||
|
||||
// EntryMap holds a map of user or group entries.
|
||||
type EntryMap struct {
|
||||
entries map[string]*entry
|
||||
}
|
||||
|
||||
// UsersAndGroups is a structure that holds entry maps of users and groups.
|
||||
// It is returned by AddUsersAndGroups.
|
||||
type UsersAndGroups struct {
|
||||
// Users is an entry map of users.
|
||||
Users *EntryMap
|
||||
// Groups is an entry map of groups.
|
||||
Groups *EntryMap
|
||||
}
|
||||
|
||||
// entry is a structure that holds information about a UNIX user or group.
|
||||
// It partially conforms parsing of both users from /etc/passwd and groups from /etc/group.
|
||||
type entry struct {
|
||||
name string
|
||||
id int64
|
||||
gid int64
|
||||
userNames []string
|
||||
shell string
|
||||
}
|
||||
|
||||
// limits is used to hold information about the minimum and maximum system ranges for UID and GID.
|
||||
type limits struct {
|
||||
minUID, maxUID, minGID, maxGID int64
|
||||
}
|
||||
|
||||
const (
|
||||
// These are constants used when parsing /etc/passwd or /etc/group in terms of how many
|
||||
// fields and entry has.
|
||||
totalFieldsGroup = 4
|
||||
totalFieldsUser = 7
|
||||
|
||||
// klogLevel holds the klog level to use for output.
|
||||
klogLevel = 5
|
||||
|
||||
// noshell holds a path to a binary to disable shell login.
|
||||
noshell = "/bin/false"
|
||||
|
||||
// These are constants for the default system paths on Linux.
|
||||
fileEtcLoginDefs = "/etc/login.defs"
|
||||
fileEtcPasswd = "/etc/passwd"
|
||||
fileEtcGroup = "/etc/group"
|
||||
)
|
||||
|
||||
var (
|
||||
// these entries hold the users and groups to create as defined in:
|
||||
// https://git.k8s.io/enhancements/keps/sig-cluster-lifecycle/kubeadm/2568-kubeadm-non-root-control-plane
|
||||
usersToCreateSpec = []*entry{
|
||||
{name: constants.EtcdUserName},
|
||||
{name: constants.KubeAPIServerUserName},
|
||||
{name: constants.KubeControllerManagerUserName},
|
||||
{name: constants.KubeSchedulerUserName},
|
||||
}
|
||||
groupsToCreateSpec = []*entry{
|
||||
{name: constants.EtcdUserName, userNames: []string{constants.EtcdUserName}},
|
||||
{name: constants.KubeAPIServerUserName, userNames: []string{constants.KubeAPIServerUserName}},
|
||||
{name: constants.KubeControllerManagerUserName, userNames: []string{constants.KubeControllerManagerUserName}},
|
||||
{name: constants.KubeSchedulerUserName, userNames: []string{constants.KubeSchedulerUserName}},
|
||||
{name: constants.ServiceAccountKeyReadersGroupName, userNames: []string{constants.KubeAPIServerUserName, constants.KubeControllerManagerUserName}},
|
||||
}
|
||||
|
||||
// defaultLimits holds the default limits in case values are missing in /etc/login.defs
|
||||
defaultLimits = &limits{minUID: 100, maxUID: 999, minGID: 100, maxGID: 999}
|
||||
)
|
||||
|
||||
// ID returns the ID for an entry based on the entry name.
|
||||
// In case of a user entry it returns the user UID.
|
||||
// In case of a group entry it returns the group GID.
|
||||
// It returns nil if no such entry exists.
|
||||
func (u *EntryMap) ID(name string) *int64 {
|
||||
entry, ok := u.entries[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
id := entry.id
|
||||
return &id
|
||||
}
|
||||
|
||||
// String converts an EntryMap object to a readable string.
|
||||
func (u *EntryMap) String() string {
|
||||
lines := make([]string, 0, len(u.entries))
|
||||
for k, e := range u.entries {
|
||||
lines = append(lines, fmt.Sprintf("%s{%d,%d};", k, e.id, e.gid))
|
||||
}
|
||||
sort.Strings(lines)
|
||||
return strings.Join(lines, "")
|
||||
}
|
||||
|
||||
// AddUsersAndGroups is a public wrapper around addUsersAndGroupsImpl with default system file paths.
|
||||
func AddUsersAndGroups() (*UsersAndGroups, error) {
|
||||
return addUsersAndGroupsImpl(fileEtcLoginDefs, fileEtcPasswd, fileEtcGroup)
|
||||
}
|
||||
|
||||
// addUsersAndGroupsImpl adds the managed users and groups to the files specified
|
||||
// by pathUsers and pathGroups. It uses the file specified with pathLoginDef to
|
||||
// determine limits for UID and GID. If managed users and groups exist in these files
|
||||
// validation is performed on them. The function returns a pointer to a Users object
|
||||
// that can be used to return UID and GID of managed users.
|
||||
func addUsersAndGroupsImpl(pathLoginDef, pathUsers, pathGroups string) (*UsersAndGroups, error) {
|
||||
klog.V(1).Info("Adding managed users and groups")
|
||||
klog.V(klogLevel).Infof("Parsing %q", pathLoginDef)
|
||||
|
||||
// Read and parse /etc/login.def. Some distributions might be missing this file, which makes
|
||||
// them non-standard. If an error occurs fallback to defaults by passing an empty string
|
||||
// to parseLoginDefs().
|
||||
var loginDef string
|
||||
f, close, err := openFileWithLock(pathLoginDef)
|
||||
if err != nil {
|
||||
klog.V(1).Infof("Could not open %q, using default system limits: %v", pathLoginDef, err)
|
||||
} else {
|
||||
loginDef, err = readFile(f)
|
||||
if err != nil {
|
||||
klog.V(1).Infof("Could not read %q, using default system limits: %v", pathLoginDef, err)
|
||||
}
|
||||
close()
|
||||
}
|
||||
limits, err := parseLoginDefs(loginDef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
klog.V(klogLevel).Infof("Using system UID/GID limits: %+v", limits)
|
||||
klog.V(klogLevel).Infof("Parsing %q and %q", pathUsers, pathGroups)
|
||||
|
||||
// Open /etc/passwd and /etc/group with locks.
|
||||
fUsers, close, err := openFileWithLock(pathUsers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close()
|
||||
fGroups, close, err := openFileWithLock(pathGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close()
|
||||
|
||||
// Read the files.
|
||||
fileUsers, err := readFile(fUsers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileGroups, err := readFile(fGroups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Parse the files.
|
||||
users, err := parseEntries(fileUsers, totalFieldsUser)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse %q", pathUsers)
|
||||
}
|
||||
groups, err := parseEntries(fileGroups, totalFieldsGroup)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse %q", pathGroups)
|
||||
}
|
||||
|
||||
klog.V(klogLevel).Info("Validating existing users and groups")
|
||||
|
||||
// Validate for existing tracked entries based on limits.
|
||||
usersToCreate, groupsToCreate, err := validateEntries(users, groups, limits)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error validating existing users and groups")
|
||||
}
|
||||
|
||||
// Allocate and assign IDs to users / groups.
|
||||
allocUIDs, err := allocateIDs(users, limits.minUID, limits.maxUID, len(usersToCreate))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allocGIDs, err := allocateIDs(groups, limits.minGID, limits.maxGID, len(groupsToCreate))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := assignUserAndGroupIDs(groups, usersToCreate, groupsToCreate, allocUIDs, allocGIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(usersToCreate) > 0 {
|
||||
klog.V(klogLevel).Infof("Adding users: %s", entriesToString(usersToCreate))
|
||||
}
|
||||
if len(groupsToCreate) > 0 {
|
||||
klog.V(klogLevel).Infof("Adding groups: %s", entriesToString(groupsToCreate))
|
||||
}
|
||||
|
||||
// Add users and groups.
|
||||
fileUsers = addEntries(fileUsers, usersToCreate, createUser)
|
||||
fileGroups = addEntries(fileGroups, groupsToCreate, createGroup)
|
||||
|
||||
// Write the files.
|
||||
klog.V(klogLevel).Infof("Writing %q and %q", pathUsers, pathGroups)
|
||||
if err := writeFile(fUsers, fileUsers); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := writeFile(fGroups, fileGroups); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepare the maps of users and groups.
|
||||
usersConcat := append(users, usersToCreate...)
|
||||
mapUsers, err := entriesToEntryMap(usersConcat, usersToCreateSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupsConcat := append(groups, groupsToCreate...)
|
||||
mapGroups, err := entriesToEntryMap(groupsConcat, groupsToCreateSpec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &UsersAndGroups{Users: mapUsers, Groups: mapGroups}, nil
|
||||
}
|
||||
|
||||
// RemoveUsersAndGroups is a public wrapper around removeUsersAndGroupsImpl with
|
||||
// default system file paths.
|
||||
func RemoveUsersAndGroups() error {
|
||||
return removeUsersAndGroupsImpl(fileEtcPasswd, fileEtcGroup)
|
||||
}
|
||||
|
||||
// removeUsersAndGroupsImpl removes the managed users and groups from the files specified
|
||||
// by pathUsers and pathGroups.
|
||||
func removeUsersAndGroupsImpl(pathUsers, pathGroups string) error {
|
||||
klog.V(1).Info("Removing managed users and groups")
|
||||
klog.V(klogLevel).Infof("Opening %q and %q", pathUsers, pathGroups)
|
||||
|
||||
// Open /etc/passwd and /etc/group.
|
||||
fUsers, close, err := openFileWithLock(pathUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close()
|
||||
fGroups, close, err := openFileWithLock(pathGroups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close()
|
||||
|
||||
// Read the files.
|
||||
fileUsers, err := readFile(fUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileGroups, err := readFile(fGroups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
klog.V(klogLevel).Infof("Removing users: %s", entriesToString(usersToCreateSpec))
|
||||
klog.V(klogLevel).Infof("Removing groups: %s", entriesToString(groupsToCreateSpec))
|
||||
|
||||
// Delete users / groups.
|
||||
fileUsers, _ = removeEntries(fileUsers, usersToCreateSpec)
|
||||
fileGroups, _ = removeEntries(fileGroups, groupsToCreateSpec)
|
||||
|
||||
klog.V(klogLevel).Infof("Writing %q and %q", pathUsers, pathGroups)
|
||||
|
||||
// Write the files.
|
||||
if err := writeFile(fUsers, fileUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeFile(fGroups, fileGroups); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseLoginDefs can be used to parse an /etc/login.defs file and obtain system ranges for UID and GID.
|
||||
// Passing an empty string will return the defaults. The defaults are 100-999 for both UID and GID.
|
||||
func parseLoginDefs(file string) (*limits, error) {
|
||||
l := *defaultLimits
|
||||
if len(file) == 0 {
|
||||
return &l, nil
|
||||
}
|
||||
var mapping = map[string]*int64{
|
||||
"SYS_UID_MIN": &l.minUID,
|
||||
"SYS_UID_MAX": &l.maxUID,
|
||||
"SYS_GID_MIN": &l.minGID,
|
||||
"SYS_GID_MAX": &l.maxGID,
|
||||
}
|
||||
lines := strings.Split(file, "\n")
|
||||
for i, line := range lines {
|
||||
for k, v := range mapping {
|
||||
// A line must start with one of the definitions
|
||||
if !strings.HasPrefix(line, k) {
|
||||
continue
|
||||
}
|
||||
line = strings.TrimPrefix(line, k)
|
||||
line = strings.TrimSpace(line)
|
||||
val, err := strconv.ParseInt(line, 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not parse value for %s at line %d", k, i)
|
||||
}
|
||||
*v = val
|
||||
}
|
||||
}
|
||||
return &l, nil
|
||||
}
|
||||
|
||||
// parseEntries can be used to parse an /etc/passwd or /etc/group file as their format is similar.
|
||||
// It returns a slice of entries obtained from the file.
|
||||
// https://www.cyberciti.biz/faq/understanding-etcpasswd-file-format/
|
||||
// https://www.cyberciti.biz/faq/understanding-etcgroup-file/
|
||||
func parseEntries(file string, totalFields int) ([]*entry, error) {
|
||||
if totalFields != totalFieldsUser && totalFields != totalFieldsGroup {
|
||||
return nil, errors.Errorf("unsupported total fields for entry parsing: %d", totalFields)
|
||||
}
|
||||
lines := strings.Split(file, "\n")
|
||||
entries := []*entry{}
|
||||
for i, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
fields := strings.Split(line, ":")
|
||||
if len(fields) != totalFields {
|
||||
return nil, errors.Errorf("entry must have %d fields separated by ':', "+
|
||||
"got %d at line %d: %s", totalFields, len(fields), i, line)
|
||||
}
|
||||
id, err := strconv.ParseInt(fields[2], 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing id at line %d", i)
|
||||
}
|
||||
entry := &entry{name: fields[0], id: id}
|
||||
if totalFields == totalFieldsGroup {
|
||||
entry.userNames = strings.Split(fields[3], ",")
|
||||
} else {
|
||||
gid, err := strconv.ParseInt(fields[3], 10, 64)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing GID at line %d", i)
|
||||
}
|
||||
entry.gid = gid
|
||||
entry.shell = fields[6]
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// validateEntries takes user and group entries and validates if these entries are valid based on limits,
|
||||
// mapping between users and groups and specs. Returns slices of missing user and group entries that must be created.
|
||||
// Returns an error if existing users and groups do not match requirements.
|
||||
func validateEntries(users, groups []*entry, limits *limits) ([]*entry, []*entry, error) {
|
||||
u := []*entry{}
|
||||
g := []*entry{}
|
||||
// Validate users
|
||||
for _, uc := range usersToCreateSpec {
|
||||
for _, user := range users {
|
||||
if uc.name != user.name {
|
||||
continue
|
||||
}
|
||||
// Found existing user
|
||||
if user.id < limits.minUID || user.id > limits.maxUID {
|
||||
return nil, nil, errors.Errorf("UID %d for user %q is outside the system UID range: %d - %d",
|
||||
user.id, user.name, limits.minUID, limits.maxUID)
|
||||
}
|
||||
if user.shell != noshell {
|
||||
return nil, nil, errors.Errorf("user %q has unexpected shell %q; expected %q",
|
||||
user.name, user.shell, noshell)
|
||||
}
|
||||
for _, g := range groups {
|
||||
if g.id != user.gid {
|
||||
continue
|
||||
}
|
||||
// Found matching group GID for user GID
|
||||
if g.name != uc.name {
|
||||
return nil, nil, errors.Errorf("user %q has GID %d but the group with that GID is not named %q",
|
||||
uc.name, g.id, uc.name)
|
||||
}
|
||||
goto skipUser // Valid group GID and name; skip
|
||||
}
|
||||
return nil, nil, errors.Errorf("could not find group with GID %d for user %q", user.gid, user.name)
|
||||
}
|
||||
u = append(u, uc)
|
||||
skipUser:
|
||||
}
|
||||
// validate groups
|
||||
for _, gc := range groupsToCreateSpec {
|
||||
for _, group := range groups {
|
||||
if gc.name != group.name {
|
||||
continue
|
||||
}
|
||||
if group.id < limits.minGID || group.id > limits.maxGID {
|
||||
return nil, nil, errors.Errorf("GID %d for user %q is outside the system UID range: %d - %d",
|
||||
group.id, group.name, limits.minGID, limits.maxGID)
|
||||
}
|
||||
u1 := strings.Join(gc.userNames, ",")
|
||||
u2 := strings.Join(group.userNames, ",")
|
||||
if u1 != u2 {
|
||||
return nil, nil, errors.Errorf("expected users %q for group %q; got %q",
|
||||
u1, gc.name, u2)
|
||||
}
|
||||
goto skipGroup // group has valid users; skip
|
||||
}
|
||||
g = append(g, gc)
|
||||
skipGroup:
|
||||
}
|
||||
return u, g, nil
|
||||
}
|
||||
|
||||
// allocateIDs takes a list of entries and based on minimum and maximum ID allocates a "total" of IDs.
|
||||
func allocateIDs(entries []*entry, min, max int64, total int) ([]int64, error) {
|
||||
if total == 0 {
|
||||
return []int64{}, nil
|
||||
}
|
||||
ids := make([]int64, 0, total)
|
||||
for i := min; i < max+1; i++ {
|
||||
i64 := int64(i)
|
||||
for _, e := range entries {
|
||||
if i64 == e.id {
|
||||
goto continueLoop
|
||||
}
|
||||
}
|
||||
ids = append(ids, i64)
|
||||
if len(ids) == total {
|
||||
return ids, nil
|
||||
}
|
||||
continueLoop:
|
||||
}
|
||||
return nil, errors.Errorf("could not allocate %d IDs based on existing entries in the range: %d - %d",
|
||||
total, min, max)
|
||||
}
|
||||
|
||||
// addEntries takes /etc/passwd or /etc/group file content and appends entries to it based
|
||||
// on a createEntry function. Returns the updated contents of the file.
|
||||
func addEntries(file string, entries []*entry, createEntry func(*entry) string) string {
|
||||
out := file
|
||||
newLines := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
newLines = append(newLines, createEntry(e))
|
||||
}
|
||||
newLinesStr := ""
|
||||
if len(newLines) > 0 {
|
||||
if !strings.HasSuffix(out, "\n") { // Append a new line if its missing.
|
||||
newLinesStr = "\n"
|
||||
}
|
||||
newLinesStr += strings.Join(newLines, "\n") + "\n"
|
||||
}
|
||||
return out + newLinesStr
|
||||
}
|
||||
|
||||
// removeEntries takes /etc/passwd or /etc/group file content and deletes entries from them
|
||||
// by name matching. Returns the updated contents of the file and the number of entries removed.
|
||||
func removeEntries(file string, entries []*entry) (string, int) {
|
||||
lines := strings.Split(file, "\n")
|
||||
total := len(lines) - len(entries)
|
||||
if total < 0 {
|
||||
total = 0
|
||||
}
|
||||
newLines := make([]string, 0, total)
|
||||
removed := 0
|
||||
for _, line := range lines {
|
||||
for _, entry := range entries {
|
||||
if strings.HasPrefix(line, entry.name+":") {
|
||||
removed++
|
||||
goto continueLoop
|
||||
}
|
||||
}
|
||||
newLines = append(newLines, line)
|
||||
continueLoop:
|
||||
}
|
||||
return strings.Join(newLines, "\n"), removed
|
||||
}
|
||||
|
||||
// assignUserAndGroupIDs takes the list of existing groups, the users and groups to be created,
|
||||
// and assigns UIDs and GIDs to the users and groups to be created based on a list of provided UIDs and GIDs.
|
||||
// Returns an error if not enough UIDs or GIDs are passed. It does not perform any other validation.
|
||||
func assignUserAndGroupIDs(groups, usersToCreate, groupsToCreate []*entry, uids, gids []int64) error {
|
||||
if len(gids) < len(groupsToCreate) {
|
||||
return errors.Errorf("not enough GIDs to assign to groups: have %d, want %d", len(gids), len(groupsToCreate))
|
||||
}
|
||||
if len(uids) < len(usersToCreate) {
|
||||
return errors.Errorf("not enough UIDs to assign to users: have %d, want %d", len(uids), len(usersToCreate))
|
||||
}
|
||||
for i := range groupsToCreate {
|
||||
groupsToCreate[i].id = gids[i]
|
||||
}
|
||||
// Concat the list of old and new groups to find a matching GID.
|
||||
groupsConcat := append([]*entry{}, groups...)
|
||||
groupsConcat = append(groupsConcat, groupsToCreate...)
|
||||
for i := range usersToCreate {
|
||||
usersToCreate[i].id = uids[i]
|
||||
for _, g := range groupsConcat {
|
||||
if usersToCreate[i].name == g.name {
|
||||
usersToCreate[i].gid = g.id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// createGroup is a helper function to produce a group from entry.
|
||||
func createGroup(e *entry) string {
|
||||
return fmt.Sprintf("%s:x:%d:%s", e.name, e.id, strings.Join(e.userNames, ","))
|
||||
}
|
||||
|
||||
// createUser is a helper function to produce a user from entry.
|
||||
func createUser(e *entry) string {
|
||||
return fmt.Sprintf("%s:x:%d:%d:::/bin/false", e.name, e.id, e.gid)
|
||||
}
|
||||
|
||||
// entriesToEntryMap takes a list of entries and prepares an EntryMap object.
|
||||
func entriesToEntryMap(entries, spec []*entry) (*EntryMap, error) {
|
||||
m := map[string]*entry{}
|
||||
for _, spec := range spec {
|
||||
for _, e := range entries {
|
||||
if spec.name == e.name {
|
||||
entry := *e
|
||||
m[e.name] = &entry
|
||||
goto continueLoop
|
||||
}
|
||||
}
|
||||
return nil, errors.Errorf("could not find entry %q in the list", spec.name)
|
||||
continueLoop:
|
||||
}
|
||||
return &EntryMap{entries: m}, nil
|
||||
}
|
||||
|
||||
// entriesToString is a utility to convert a list of entries to string.
|
||||
func entriesToString(entries []*entry) string {
|
||||
lines := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
lines = append(lines, e.name)
|
||||
}
|
||||
sort.Strings(lines)
|
||||
return strings.Join(lines, ",")
|
||||
}
|
||||
|
||||
// openFileWithLock opens the file at path by acquiring an exclusive write lock.
|
||||
// The returned close() function should be called to release the lock and close the file.
|
||||
// If a lock cannot be obtained the function fails after a period of time.
|
||||
func openFileWithLock(path string) (f *os.File, close func(), err error) {
|
||||
f, err = os.OpenFile(path, os.O_RDWR, os.ModePerm)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
deadline := time.Now().Add(time.Second * 5)
|
||||
for {
|
||||
// If another process is holding a write lock, this call will exit
|
||||
// with an error. F_SETLK is used instead of F_SETLKW to avoid
|
||||
// the case where a runaway process grabs the exclusive lock and
|
||||
// blocks this call indefinitely.
|
||||
// https://man7.org/linux/man-pages/man2/fcntl.2.html
|
||||
lock := syscall.Flock_t{Type: syscall.F_WRLCK}
|
||||
if err = syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &lock); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
if time.Now().After(deadline) {
|
||||
err = errors.Wrapf(err, "timeout attempting to obtain lock on file %q", path)
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
close = func() {
|
||||
// This function should be called once operations with the file are finished.
|
||||
// It unlocks the file and closes it.
|
||||
unlock := syscall.Flock_t{Type: syscall.F_UNLCK}
|
||||
syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &unlock)
|
||||
f.Close()
|
||||
}
|
||||
return f, close, nil
|
||||
}
|
||||
|
||||
// readFile reads a File into a string.
|
||||
func readFile(f *os.File) (string, error) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err := io.Copy(buf, f); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// writeFile writes a string to a File.
|
||||
func writeFile(f *os.File, str string) error {
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write([]byte(str)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Truncate(int64(len(str))); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePathOwnerAndPermissions updates the owner and permissions of the given path.
|
||||
// If the path is a directory it is not recursively updated.
|
||||
func UpdatePathOwnerAndPermissions(path string, uid, gid int64, perms uint32) error {
|
||||
if err := os.Chown(path, int(uid), int(gid)); err != nil {
|
||||
return errors.Wrapf(err, "failed to update owner of %q to uid: %d and gid: %d", path, uid, gid)
|
||||
}
|
||||
fm := os.FileMode(perms)
|
||||
if err := os.Chmod(path, fm); err != nil {
|
||||
return errors.Wrapf(err, "failed to update permissions of %q to %s", path, fm.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePathOwner recursively updates the owners of a directory.
|
||||
// It is equivalent to calling `chown -R uid:gid /path/to/dir`.
|
||||
func UpdatePathOwner(dirPath string, uid, gid int64) error {
|
||||
err := filepath.WalkDir(dirPath, func(path string, d os.DirEntry, err error) error {
|
||||
if err := os.Chown(path, int(uid), int(gid)); err != nil {
|
||||
return errors.Wrapf(err, "failed to update owner of %q to uid: %d and gid: %d", path, uid, gid)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -1,593 +0,0 @@
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package users
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseLoginDef(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expectedLimits *limits
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "non number value for tracked limit",
|
||||
input: "SYS_UID_MIN foo\n",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "empty string must return defaults",
|
||||
expectedLimits: defaultLimits,
|
||||
},
|
||||
{
|
||||
name: "no tracked limits in file must return defaults",
|
||||
input: "# some comment\n",
|
||||
expectedLimits: defaultLimits,
|
||||
},
|
||||
{
|
||||
name: "must parse all valid tracked limits",
|
||||
input: "SYS_UID_MIN 101\nSYS_UID_MAX 998\nSYS_GID_MIN 102\nSYS_GID_MAX 999\n",
|
||||
expectedLimits: &limits{minUID: 101, maxUID: 998, minGID: 102, maxGID: 999},
|
||||
},
|
||||
{
|
||||
name: "must return defaults for missing limits",
|
||||
input: "SYS_UID_MIN 101\n#SYS_UID_MAX 998\nSYS_GID_MIN 102\n#SYS_GID_MAX 999\n",
|
||||
expectedLimits: &limits{minUID: 101, maxUID: defaultLimits.maxUID, minGID: 102, maxGID: defaultLimits.maxGID},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := parseLoginDefs(tc.input)
|
||||
if err != nil != tc.expectedError {
|
||||
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
||||
}
|
||||
if err == nil && *tc.expectedLimits != *got {
|
||||
t.Fatalf("expected limits %+v, got %+v", tc.expectedLimits, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEntries(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
file string
|
||||
expectedEntries []*entry
|
||||
totalFields int
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "totalFields must be a known value",
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "unexpected number of fields",
|
||||
file: "foo:x:100::::::",
|
||||
totalFields: totalFieldsUser,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "cannot parse 'bar' as UID",
|
||||
file: "foo:x:bar:101:::\n",
|
||||
totalFields: totalFieldsUser,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "cannot parse 'bar' as GID",
|
||||
file: "foo:x:101:bar:::\n",
|
||||
totalFields: totalFieldsUser,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "valid file for users",
|
||||
file: "\nfoo:x:100:101:foo:/home/foo:/bin/bash\n\nbar:x:102:103:bar::\n",
|
||||
totalFields: totalFieldsUser,
|
||||
expectedEntries: []*entry{
|
||||
{name: "foo", id: 100, gid: 101, shell: "/bin/bash"},
|
||||
{name: "bar", id: 102, gid: 103},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid file for groups",
|
||||
file: "\nfoo:x:100:bar,baz\n\nbar:x:101:baz\n",
|
||||
totalFields: totalFieldsGroup,
|
||||
expectedEntries: []*entry{
|
||||
{name: "foo", id: 100, userNames: []string{"bar", "baz"}},
|
||||
{name: "bar", id: 101, userNames: []string{"baz"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := parseEntries(tc.file, tc.totalFields)
|
||||
if err != nil != tc.expectedError {
|
||||
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(tc.expectedEntries) != len(got) {
|
||||
t.Fatalf("expected entries %d, got %d", len(tc.expectedEntries), len(got))
|
||||
}
|
||||
for i := range got {
|
||||
if !reflect.DeepEqual(tc.expectedEntries[i], got[i]) {
|
||||
t.Fatalf("expected entry at position %d: %+v, got: %+v", i, tc.expectedEntries[i], got[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateEntries(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
users []*entry
|
||||
groups []*entry
|
||||
expectedUsers []*entry
|
||||
expectedGroups []*entry
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "UID for user is outside of system limits",
|
||||
users: []*entry{
|
||||
{name: "kubeadm-etcd", id: 2000, gid: 102, shell: noshell},
|
||||
},
|
||||
groups: []*entry{},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "user has unexpected shell",
|
||||
users: []*entry{
|
||||
{name: "kubeadm-etcd", id: 102, gid: 102, shell: "foo"},
|
||||
},
|
||||
groups: []*entry{},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "user is mapped to unknown group",
|
||||
users: []*entry{
|
||||
{name: "kubeadm-etcd", id: 102, gid: 102, shell: noshell},
|
||||
},
|
||||
groups: []*entry{},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "user and group names do not match",
|
||||
users: []*entry{
|
||||
{name: "kubeadm-etcd", id: 102, gid: 102, shell: noshell},
|
||||
},
|
||||
groups: []*entry{
|
||||
{name: "foo", id: 102},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "GID is outside system limits",
|
||||
users: []*entry{},
|
||||
groups: []*entry{
|
||||
{name: "kubeadm-etcd", id: 2000},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "group is missing users",
|
||||
users: []*entry{},
|
||||
groups: []*entry{
|
||||
{name: "kubeadm-etcd", id: 100},
|
||||
},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "empty input must return default users and groups",
|
||||
users: []*entry{},
|
||||
groups: []*entry{},
|
||||
expectedUsers: usersToCreateSpec,
|
||||
expectedGroups: groupsToCreateSpec,
|
||||
},
|
||||
{
|
||||
name: "existing valid users mapped to groups",
|
||||
users: []*entry{
|
||||
{name: "kubeadm-etcd", id: 100, gid: 102, shell: noshell},
|
||||
{name: "kubeadm-kas", id: 101, gid: 103, shell: noshell},
|
||||
},
|
||||
groups: []*entry{
|
||||
{name: "kubeadm-etcd", id: 102, userNames: []string{"kubeadm-etcd"}},
|
||||
{name: "kubeadm-kas", id: 103, userNames: []string{"kubeadm-kas"}},
|
||||
{name: "kubeadm-sa-key-readers", id: 104, userNames: []string{"kubeadm-kas", "kubeadm-kcm"}},
|
||||
},
|
||||
expectedUsers: []*entry{
|
||||
{name: "kubeadm-kcm"},
|
||||
{name: "kubeadm-ks"},
|
||||
},
|
||||
expectedGroups: []*entry{
|
||||
{name: "kubeadm-kcm", userNames: []string{"kubeadm-kcm"}},
|
||||
{name: "kubeadm-ks", userNames: []string{"kubeadm-ks"}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
users, groups, err := validateEntries(tc.users, tc.groups, defaultLimits)
|
||||
if err != nil != tc.expectedError {
|
||||
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(tc.expectedUsers) != len(users) {
|
||||
t.Fatalf("expected users %d, got %d", len(tc.expectedUsers), len(users))
|
||||
}
|
||||
for i := range users {
|
||||
if !reflect.DeepEqual(tc.expectedUsers[i], users[i]) {
|
||||
t.Fatalf("expected user at position %d: %+v, got: %+v", i, tc.expectedUsers[i], users[i])
|
||||
}
|
||||
}
|
||||
if len(tc.expectedGroups) != len(groups) {
|
||||
t.Fatalf("expected groups %d, got %d", len(tc.expectedGroups), len(groups))
|
||||
}
|
||||
for i := range groups {
|
||||
if !reflect.DeepEqual(tc.expectedGroups[i], groups[i]) {
|
||||
t.Fatalf("expected group at position %d: %+v, got: %+v", i, tc.expectedGroups[i], groups[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocateIDs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
entries []*entry
|
||||
min int64
|
||||
max int64
|
||||
total int
|
||||
expectedIDs []int64
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "zero total ids returns empty slice",
|
||||
expectedIDs: []int64{},
|
||||
},
|
||||
{
|
||||
name: "not enough free ids in range",
|
||||
entries: []*entry{
|
||||
{name: "foo", id: 101},
|
||||
{name: "bar", id: 103},
|
||||
{name: "baz", id: 105},
|
||||
},
|
||||
min: 100,
|
||||
max: 105,
|
||||
total: 4,
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "successfully allocate ids",
|
||||
entries: []*entry{
|
||||
{name: "foo", id: 101},
|
||||
{name: "bar", id: 103},
|
||||
{name: "baz", id: 105},
|
||||
},
|
||||
min: 100,
|
||||
max: 110,
|
||||
total: 4,
|
||||
expectedIDs: []int64{100, 102, 104, 106},
|
||||
expectedError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, err := allocateIDs(tc.entries, tc.min, tc.max, tc.total)
|
||||
if err != nil != tc.expectedError {
|
||||
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(tc.expectedIDs) != len(got) {
|
||||
t.Fatalf("expected id %d, got %d", len(tc.expectedIDs), len(got))
|
||||
}
|
||||
for i := range got {
|
||||
if !reflect.DeepEqual(tc.expectedIDs[i], got[i]) {
|
||||
t.Fatalf("expected id at position %d: %+v, got: %+v", i, tc.expectedIDs[i], got[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddEntries(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
file string
|
||||
entries []*entry
|
||||
createEntry func(*entry) string
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "user entries are added",
|
||||
file: "foo:x:101:101:::/bin/false\n",
|
||||
entries: []*entry{
|
||||
{name: "bar", id: 102, gid: 102},
|
||||
{name: "baz", id: 103, gid: 103},
|
||||
},
|
||||
expectedOutput: "foo:x:101:101:::/bin/false\nbar:x:102:102:::/bin/false\nbaz:x:103:103:::/bin/false\n",
|
||||
createEntry: createUser,
|
||||
},
|
||||
{
|
||||
name: "user entries are added (new line is appended)",
|
||||
file: "foo:x:101:101:::/bin/false",
|
||||
entries: []*entry{
|
||||
{name: "bar", id: 102, gid: 102},
|
||||
},
|
||||
expectedOutput: "foo:x:101:101:::/bin/false\nbar:x:102:102:::/bin/false\n",
|
||||
createEntry: createUser,
|
||||
},
|
||||
{
|
||||
name: "group entries are added",
|
||||
file: "foo:x:101:foo\n",
|
||||
entries: []*entry{
|
||||
{name: "bar", id: 102, userNames: []string{"bar"}},
|
||||
{name: "baz", id: 103, userNames: []string{"baz"}},
|
||||
},
|
||||
expectedOutput: "foo:x:101:foo\nbar:x:102:bar\nbaz:x:103:baz\n",
|
||||
createEntry: createGroup,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := addEntries(tc.file, tc.entries, tc.createEntry)
|
||||
if tc.expectedOutput != got {
|
||||
t.Fatalf("expected output:\n%s\ngot:\n%s\n", tc.expectedOutput, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveEntries(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
file string
|
||||
entries []*entry
|
||||
expectedRemoved int
|
||||
expectedOutput string
|
||||
}{
|
||||
{
|
||||
name: "entries that are missing do not cause an error",
|
||||
file: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
|
||||
entries: []*entry{},
|
||||
expectedRemoved: 0,
|
||||
expectedOutput: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
|
||||
},
|
||||
{
|
||||
name: "user entry is removed",
|
||||
file: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
|
||||
entries: []*entry{
|
||||
{name: "bar"},
|
||||
},
|
||||
expectedRemoved: 1,
|
||||
expectedOutput: "foo:x:102:102:::/bin/false\n",
|
||||
},
|
||||
{
|
||||
name: "group entry is removed",
|
||||
file: "foo:x:102:foo\nbar:x:102:bar\n",
|
||||
entries: []*entry{
|
||||
{name: "bar"},
|
||||
},
|
||||
expectedRemoved: 1,
|
||||
expectedOutput: "foo:x:102:foo\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got, removed := removeEntries(tc.file, tc.entries)
|
||||
if tc.expectedRemoved != removed {
|
||||
t.Fatalf("expected entries to be removed: %v, got: %v", tc.expectedRemoved, removed)
|
||||
}
|
||||
if tc.expectedOutput != got {
|
||||
t.Fatalf("expected output:\n%s\ngot:\n%s\n", tc.expectedOutput, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignUserAndGroupIDs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
users []*entry
|
||||
groups []*entry
|
||||
usersToCreate []*entry
|
||||
groupsToCreate []*entry
|
||||
uids []int64
|
||||
gids []int64
|
||||
expectedUsers []*entry
|
||||
expectedGroups []*entry
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "not enough UIDs",
|
||||
usersToCreate: []*entry{
|
||||
{name: "foo"},
|
||||
{name: "bar"},
|
||||
},
|
||||
uids: []int64{100},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "not enough GIDs",
|
||||
groupsToCreate: []*entry{
|
||||
{name: "foo"},
|
||||
{name: "bar"},
|
||||
},
|
||||
gids: []int64{100},
|
||||
expectedError: true,
|
||||
},
|
||||
{
|
||||
name: "valid UIDs and GIDs are assigned to input",
|
||||
groups: []*entry{
|
||||
{name: "foo", id: 110},
|
||||
{name: "bar", id: 111},
|
||||
},
|
||||
usersToCreate: []*entry{
|
||||
{name: "foo"},
|
||||
{name: "bar"},
|
||||
{name: "baz"},
|
||||
},
|
||||
groupsToCreate: []*entry{
|
||||
{name: "baz"},
|
||||
},
|
||||
uids: []int64{100, 101, 102},
|
||||
gids: []int64{112},
|
||||
expectedUsers: []*entry{
|
||||
{name: "foo", id: 100, gid: 110},
|
||||
{name: "bar", id: 101, gid: 111},
|
||||
{name: "baz", id: 102, gid: 112},
|
||||
},
|
||||
expectedGroups: []*entry{
|
||||
{name: "baz", id: 112},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := assignUserAndGroupIDs(tc.groups, tc.usersToCreate, tc.groupsToCreate, tc.uids, tc.gids)
|
||||
if err != nil != tc.expectedError {
|
||||
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(tc.expectedUsers) != len(tc.usersToCreate) {
|
||||
t.Fatalf("expected users %d, got %d", len(tc.expectedUsers), len(tc.usersToCreate))
|
||||
}
|
||||
for i := range tc.usersToCreate {
|
||||
if !reflect.DeepEqual(tc.expectedUsers[i], tc.usersToCreate[i]) {
|
||||
t.Fatalf("expected user at position %d: %+v, got: %+v", i, tc.expectedUsers[i], tc.usersToCreate[i])
|
||||
}
|
||||
}
|
||||
if len(tc.expectedGroups) != len(tc.groupsToCreate) {
|
||||
t.Fatalf("expected groups %d, got %d", len(tc.expectedGroups), len(tc.groupsToCreate))
|
||||
}
|
||||
for i := range tc.groupsToCreate {
|
||||
if !reflect.DeepEqual(tc.expectedGroups[i], tc.groupsToCreate[i]) {
|
||||
t.Fatalf("expected group at position %d: %+v, got: %+v", i, tc.expectedGroups[i], tc.groupsToCreate[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestID(t *testing.T) {
|
||||
e := &entry{name: "foo", id: 101}
|
||||
m := &EntryMap{entries: map[string]*entry{
|
||||
"foo": e,
|
||||
}}
|
||||
id := m.ID("foo")
|
||||
if *id != 101 {
|
||||
t.Fatalf("expected: id=%d; got: id=%d", 101, *id)
|
||||
}
|
||||
id = m.ID("bar")
|
||||
if id != nil {
|
||||
t.Fatalf("expected nil for unknown entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddUsersAndGroupsImpl(t *testing.T) {
|
||||
const (
|
||||
loginDef = "SYS_UID_MIN 101\nSYS_UID_MAX 998\nSYS_GID_MIN 102\nSYS_GID_MAX 999\n"
|
||||
passwd = "root:x:0:0:::/bin/bash\nkubeadm-etcd:x:101:102:::/bin/false\n"
|
||||
group = "root:x:0:root\nkubeadm-etcd:x:102:kubeadm-etcd\n"
|
||||
expectedUsers = "kubeadm-etcd{101,102};kubeadm-kas{102,103};kubeadm-kcm{103,104};kubeadm-ks{104,105};"
|
||||
expectedGroups = "kubeadm-etcd{102,0};kubeadm-kas{103,0};kubeadm-kcm{104,0};kubeadm-ks{105,0};kubeadm-sa-key-readers{106,0};"
|
||||
)
|
||||
fileLoginDef, close := writeTempFile(t, loginDef)
|
||||
defer close()
|
||||
filePasswd, close := writeTempFile(t, passwd)
|
||||
defer close()
|
||||
fileGroup, close := writeTempFile(t, group)
|
||||
defer close()
|
||||
got, err := addUsersAndGroupsImpl(fileLoginDef, filePasswd, fileGroup)
|
||||
if err != nil {
|
||||
t.Fatalf("AddUsersAndGroups failed: %v", err)
|
||||
}
|
||||
if expectedUsers != got.Users.String() {
|
||||
t.Fatalf("expected users: %q, got: %q", expectedUsers, got.Users.String())
|
||||
}
|
||||
if expectedGroups != got.Groups.String() {
|
||||
t.Fatalf("expected groups: %q, got: %q", expectedGroups, got.Groups.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveUsersAndGroups(t *testing.T) {
|
||||
const (
|
||||
passwd = "root:x:0:0:::/bin/bash\nkubeadm-etcd:x:101:102:::/bin/false\n"
|
||||
group = "root:x:0:root\nkubeadm-etcd:x:102:kubeadm-etcd\n"
|
||||
expectedPasswd = "root:x:0:0:::/bin/bash\n"
|
||||
expectedGroup = "root:x:0:root\n"
|
||||
)
|
||||
filePasswd, close := writeTempFile(t, passwd)
|
||||
defer close()
|
||||
fileGroup, close := writeTempFile(t, group)
|
||||
defer close()
|
||||
if err := removeUsersAndGroupsImpl(filePasswd, fileGroup); err != nil {
|
||||
t.Fatalf("RemoveUsersAndGroups failed: %v", err)
|
||||
}
|
||||
contentsPasswd := readTempFile(t, filePasswd)
|
||||
if expectedPasswd != contentsPasswd {
|
||||
t.Fatalf("expected passwd:\n%s\ngot:\n%s\n", expectedPasswd, contentsPasswd)
|
||||
}
|
||||
contentsGroup := readTempFile(t, fileGroup)
|
||||
if expectedGroup != contentsGroup {
|
||||
t.Fatalf("expected passwd:\n%s\ngot:\n%s\n", expectedGroup, contentsGroup)
|
||||
}
|
||||
}
|
||||
|
||||
func writeTempFile(t *testing.T, contents string) (string, func()) {
|
||||
file, err := os.CreateTemp("", "")
|
||||
if err != nil {
|
||||
t.Fatalf("could not create file: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(file.Name(), []byte(contents), os.ModePerm); err != nil {
|
||||
t.Fatalf("could not write file: %v", err)
|
||||
}
|
||||
close := func() {
|
||||
os.Remove(file.Name())
|
||||
}
|
||||
return file.Name(), close
|
||||
}
|
||||
|
||||
func readTempFile(t *testing.T, path string) string {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("could not read file: %v", err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
//go:build !linux
|
||||
// +build !linux
|
||||
|
||||
/*
|
||||
Copyright 2021 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package users
|
||||
|
||||
// EntryMap is empty on non-Linux.
|
||||
type EntryMap struct{}
|
||||
|
||||
// UsersAndGroups is empty on non-Linux.
|
||||
type UsersAndGroups struct{}
|
||||
|
||||
// ID is a NO-OP on non-Linux.
|
||||
func (*EntryMap) ID(string) *int64 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// String is NO-OP on non-Linux.
|
||||
func (*EntryMap) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// AddUsersAndGroups is a NO-OP on non-Linux.
|
||||
func AddUsersAndGroups() (*UsersAndGroups, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// RemoveUsersAndGroups is a NO-OP on non-Linux.
|
||||
func RemoveUsersAndGroups() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePathOwnerAndPermissions is a NO-OP on non-Linux.
|
||||
func UpdatePathOwnerAndPermissions(path string, uid, gid int64, perms uint32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePathOwner is a NO-OP on non-Linux.
|
||||
func UpdatePathOwner(dirPath string, uid, gid int64) error {
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user