kubeadm: add utilities to manage users and groups
In the Alpha stage of the feature in kubeadm to support a rootless control plane, the allocation and assignment of UID/GIDs to containers in the static pods will be automated. This automation will require management of users and groups in /etc/passwd and /etc/group. The tools on Linux for user/group management are inconsistent and non-standardized. It also requires us to include a number of more dependencies in the DEB/RPMs, while complicating the UX for non-package manager users. The format of /etc/passwd and /etc/group is standardized. Add code for managing (adding and deleting) a set of managed users and groups in these files.
This commit is contained in:
		
							
								
								
									
										631
									
								
								cmd/kubeadm/app/util/users/users_linux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										631
									
								
								cmd/kubeadm/app/util/users/users_linux.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,631 @@ | |||||||
|  | // +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" | ||||||
|  | 	"sort" | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  | 	"syscall" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  |  | ||||||
|  | 	"k8s.io/klog/v2" | ||||||
|  | 	"k8s.io/kubernetes/cmd/kubeadm/app/constants" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // 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 partialially 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, "") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // 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).Info("Could not open %q, using default system limits: %v", pathLoginDef, err) | ||||||
|  | 	} else { | ||||||
|  | 		loginDef, err = readFile(f) | ||||||
|  | 		if err != nil { | ||||||
|  | 			klog.V(1).Info("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 exclive 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 | ||||||
|  | } | ||||||
							
								
								
									
										593
									
								
								cmd/kubeadm/app/util/users/users_linux_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										593
									
								
								cmd/kubeadm/app/util/users/users_linux_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,593 @@ | |||||||
|  | // +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 ( | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"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 := ioutil.TempFile("", "") | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("could not create file: %v", err) | ||||||
|  | 	} | ||||||
|  | 	if err := ioutil.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 := ioutil.ReadFile(path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		t.Fatalf("could not read file: %v", err) | ||||||
|  | 	} | ||||||
|  | 	return string(b) | ||||||
|  | } | ||||||
							
								
								
									
										45
									
								
								cmd/kubeadm/app/util/users/users_other.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								cmd/kubeadm/app/util/users/users_other.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | // +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 | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Lubomir I. Ivanov
					Lubomir I. Ivanov