mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-24 12:15:52 +00:00
Merge pull request #102463 from neolit123/1.22-add-usergroup-utils
kubeadm: add utilities to manage users and groups
This commit is contained in:
commit
ed1a2b411d
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user