mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-26 21:17:23 +00:00
594 lines
16 KiB
Go
594 lines
16 KiB
Go
//go:build linux
|
|
// +build linux
|
|
|
|
/*
|
|
Copyright 2021 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package users
|
|
|
|
import (
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseLoginDef(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
input string
|
|
expectedLimits *limits
|
|
expectedError bool
|
|
}{
|
|
{
|
|
name: "non number value for tracked limit",
|
|
input: "SYS_UID_MIN foo\n",
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "empty string must return defaults",
|
|
expectedLimits: defaultLimits,
|
|
},
|
|
{
|
|
name: "no tracked limits in file must return defaults",
|
|
input: "# some comment\n",
|
|
expectedLimits: defaultLimits,
|
|
},
|
|
{
|
|
name: "must parse all valid tracked limits",
|
|
input: "SYS_UID_MIN 101\nSYS_UID_MAX 998\nSYS_GID_MIN 102\nSYS_GID_MAX 999\n",
|
|
expectedLimits: &limits{minUID: 101, maxUID: 998, minGID: 102, maxGID: 999},
|
|
},
|
|
{
|
|
name: "must return defaults for missing limits",
|
|
input: "SYS_UID_MIN 101\n#SYS_UID_MAX 998\nSYS_GID_MIN 102\n#SYS_GID_MAX 999\n",
|
|
expectedLimits: &limits{minUID: 101, maxUID: defaultLimits.maxUID, minGID: 102, maxGID: defaultLimits.maxGID},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := parseLoginDefs(tc.input)
|
|
if err != nil != tc.expectedError {
|
|
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
|
}
|
|
if err == nil && *tc.expectedLimits != *got {
|
|
t.Fatalf("expected limits %+v, got %+v", tc.expectedLimits, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseEntries(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
file string
|
|
expectedEntries []*entry
|
|
totalFields int
|
|
expectedError bool
|
|
}{
|
|
{
|
|
name: "totalFields must be a known value",
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "unexpected number of fields",
|
|
file: "foo:x:100::::::",
|
|
totalFields: totalFieldsUser,
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "cannot parse 'bar' as UID",
|
|
file: "foo:x:bar:101:::\n",
|
|
totalFields: totalFieldsUser,
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "cannot parse 'bar' as GID",
|
|
file: "foo:x:101:bar:::\n",
|
|
totalFields: totalFieldsUser,
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "valid file for users",
|
|
file: "\nfoo:x:100:101:foo:/home/foo:/bin/bash\n\nbar:x:102:103:bar::\n",
|
|
totalFields: totalFieldsUser,
|
|
expectedEntries: []*entry{
|
|
{name: "foo", id: 100, gid: 101, shell: "/bin/bash"},
|
|
{name: "bar", id: 102, gid: 103},
|
|
},
|
|
},
|
|
{
|
|
name: "valid file for groups",
|
|
file: "\nfoo:x:100:bar,baz\n\nbar:x:101:baz\n",
|
|
totalFields: totalFieldsGroup,
|
|
expectedEntries: []*entry{
|
|
{name: "foo", id: 100, userNames: []string{"bar", "baz"}},
|
|
{name: "bar", id: 101, userNames: []string{"baz"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := parseEntries(tc.file, tc.totalFields)
|
|
if err != nil != tc.expectedError {
|
|
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(tc.expectedEntries) != len(got) {
|
|
t.Fatalf("expected entries %d, got %d", len(tc.expectedEntries), len(got))
|
|
}
|
|
for i := range got {
|
|
if !reflect.DeepEqual(tc.expectedEntries[i], got[i]) {
|
|
t.Fatalf("expected entry at position %d: %+v, got: %+v", i, tc.expectedEntries[i], got[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateEntries(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
users []*entry
|
|
groups []*entry
|
|
expectedUsers []*entry
|
|
expectedGroups []*entry
|
|
expectedError bool
|
|
}{
|
|
{
|
|
name: "UID for user is outside of system limits",
|
|
users: []*entry{
|
|
{name: "kubeadm-etcd", id: 2000, gid: 102, shell: noshell},
|
|
},
|
|
groups: []*entry{},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "user has unexpected shell",
|
|
users: []*entry{
|
|
{name: "kubeadm-etcd", id: 102, gid: 102, shell: "foo"},
|
|
},
|
|
groups: []*entry{},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "user is mapped to unknown group",
|
|
users: []*entry{
|
|
{name: "kubeadm-etcd", id: 102, gid: 102, shell: noshell},
|
|
},
|
|
groups: []*entry{},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "user and group names do not match",
|
|
users: []*entry{
|
|
{name: "kubeadm-etcd", id: 102, gid: 102, shell: noshell},
|
|
},
|
|
groups: []*entry{
|
|
{name: "foo", id: 102},
|
|
},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "GID is outside system limits",
|
|
users: []*entry{},
|
|
groups: []*entry{
|
|
{name: "kubeadm-etcd", id: 2000},
|
|
},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "group is missing users",
|
|
users: []*entry{},
|
|
groups: []*entry{
|
|
{name: "kubeadm-etcd", id: 100},
|
|
},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "empty input must return default users and groups",
|
|
users: []*entry{},
|
|
groups: []*entry{},
|
|
expectedUsers: usersToCreateSpec,
|
|
expectedGroups: groupsToCreateSpec,
|
|
},
|
|
{
|
|
name: "existing valid users mapped to groups",
|
|
users: []*entry{
|
|
{name: "kubeadm-etcd", id: 100, gid: 102, shell: noshell},
|
|
{name: "kubeadm-kas", id: 101, gid: 103, shell: noshell},
|
|
},
|
|
groups: []*entry{
|
|
{name: "kubeadm-etcd", id: 102, userNames: []string{"kubeadm-etcd"}},
|
|
{name: "kubeadm-kas", id: 103, userNames: []string{"kubeadm-kas"}},
|
|
{name: "kubeadm-sa-key-readers", id: 104, userNames: []string{"kubeadm-kas", "kubeadm-kcm"}},
|
|
},
|
|
expectedUsers: []*entry{
|
|
{name: "kubeadm-kcm"},
|
|
{name: "kubeadm-ks"},
|
|
},
|
|
expectedGroups: []*entry{
|
|
{name: "kubeadm-kcm", userNames: []string{"kubeadm-kcm"}},
|
|
{name: "kubeadm-ks", userNames: []string{"kubeadm-ks"}},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
users, groups, err := validateEntries(tc.users, tc.groups, defaultLimits)
|
|
if err != nil != tc.expectedError {
|
|
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(tc.expectedUsers) != len(users) {
|
|
t.Fatalf("expected users %d, got %d", len(tc.expectedUsers), len(users))
|
|
}
|
|
for i := range users {
|
|
if !reflect.DeepEqual(tc.expectedUsers[i], users[i]) {
|
|
t.Fatalf("expected user at position %d: %+v, got: %+v", i, tc.expectedUsers[i], users[i])
|
|
}
|
|
}
|
|
if len(tc.expectedGroups) != len(groups) {
|
|
t.Fatalf("expected groups %d, got %d", len(tc.expectedGroups), len(groups))
|
|
}
|
|
for i := range groups {
|
|
if !reflect.DeepEqual(tc.expectedGroups[i], groups[i]) {
|
|
t.Fatalf("expected group at position %d: %+v, got: %+v", i, tc.expectedGroups[i], groups[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAllocateIDs(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
entries []*entry
|
|
min int64
|
|
max int64
|
|
total int
|
|
expectedIDs []int64
|
|
expectedError bool
|
|
}{
|
|
{
|
|
name: "zero total ids returns empty slice",
|
|
expectedIDs: []int64{},
|
|
},
|
|
{
|
|
name: "not enough free ids in range",
|
|
entries: []*entry{
|
|
{name: "foo", id: 101},
|
|
{name: "bar", id: 103},
|
|
{name: "baz", id: 105},
|
|
},
|
|
min: 100,
|
|
max: 105,
|
|
total: 4,
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "successfully allocate ids",
|
|
entries: []*entry{
|
|
{name: "foo", id: 101},
|
|
{name: "bar", id: 103},
|
|
{name: "baz", id: 105},
|
|
},
|
|
min: 100,
|
|
max: 110,
|
|
total: 4,
|
|
expectedIDs: []int64{100, 102, 104, 106},
|
|
expectedError: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, err := allocateIDs(tc.entries, tc.min, tc.max, tc.total)
|
|
if err != nil != tc.expectedError {
|
|
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(tc.expectedIDs) != len(got) {
|
|
t.Fatalf("expected id %d, got %d", len(tc.expectedIDs), len(got))
|
|
}
|
|
for i := range got {
|
|
if !reflect.DeepEqual(tc.expectedIDs[i], got[i]) {
|
|
t.Fatalf("expected id at position %d: %+v, got: %+v", i, tc.expectedIDs[i], got[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAddEntries(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
file string
|
|
entries []*entry
|
|
createEntry func(*entry) string
|
|
expectedOutput string
|
|
}{
|
|
{
|
|
name: "user entries are added",
|
|
file: "foo:x:101:101:::/bin/false\n",
|
|
entries: []*entry{
|
|
{name: "bar", id: 102, gid: 102},
|
|
{name: "baz", id: 103, gid: 103},
|
|
},
|
|
expectedOutput: "foo:x:101:101:::/bin/false\nbar:x:102:102:::/bin/false\nbaz:x:103:103:::/bin/false\n",
|
|
createEntry: createUser,
|
|
},
|
|
{
|
|
name: "user entries are added (new line is appended)",
|
|
file: "foo:x:101:101:::/bin/false",
|
|
entries: []*entry{
|
|
{name: "bar", id: 102, gid: 102},
|
|
},
|
|
expectedOutput: "foo:x:101:101:::/bin/false\nbar:x:102:102:::/bin/false\n",
|
|
createEntry: createUser,
|
|
},
|
|
{
|
|
name: "group entries are added",
|
|
file: "foo:x:101:foo\n",
|
|
entries: []*entry{
|
|
{name: "bar", id: 102, userNames: []string{"bar"}},
|
|
{name: "baz", id: 103, userNames: []string{"baz"}},
|
|
},
|
|
expectedOutput: "foo:x:101:foo\nbar:x:102:bar\nbaz:x:103:baz\n",
|
|
createEntry: createGroup,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := addEntries(tc.file, tc.entries, tc.createEntry)
|
|
if tc.expectedOutput != got {
|
|
t.Fatalf("expected output:\n%s\ngot:\n%s\n", tc.expectedOutput, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemoveEntries(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
file string
|
|
entries []*entry
|
|
expectedRemoved int
|
|
expectedOutput string
|
|
}{
|
|
{
|
|
name: "entries that are missing do not cause an error",
|
|
file: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
|
|
entries: []*entry{},
|
|
expectedRemoved: 0,
|
|
expectedOutput: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
|
|
},
|
|
{
|
|
name: "user entry is removed",
|
|
file: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
|
|
entries: []*entry{
|
|
{name: "bar"},
|
|
},
|
|
expectedRemoved: 1,
|
|
expectedOutput: "foo:x:102:102:::/bin/false\n",
|
|
},
|
|
{
|
|
name: "group entry is removed",
|
|
file: "foo:x:102:foo\nbar:x:102:bar\n",
|
|
entries: []*entry{
|
|
{name: "bar"},
|
|
},
|
|
expectedRemoved: 1,
|
|
expectedOutput: "foo:x:102:foo\n",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got, removed := removeEntries(tc.file, tc.entries)
|
|
if tc.expectedRemoved != removed {
|
|
t.Fatalf("expected entries to be removed: %v, got: %v", tc.expectedRemoved, removed)
|
|
}
|
|
if tc.expectedOutput != got {
|
|
t.Fatalf("expected output:\n%s\ngot:\n%s\n", tc.expectedOutput, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAssignUserAndGroupIDs(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
users []*entry
|
|
groups []*entry
|
|
usersToCreate []*entry
|
|
groupsToCreate []*entry
|
|
uids []int64
|
|
gids []int64
|
|
expectedUsers []*entry
|
|
expectedGroups []*entry
|
|
expectedError bool
|
|
}{
|
|
{
|
|
name: "not enough UIDs",
|
|
usersToCreate: []*entry{
|
|
{name: "foo"},
|
|
{name: "bar"},
|
|
},
|
|
uids: []int64{100},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "not enough GIDs",
|
|
groupsToCreate: []*entry{
|
|
{name: "foo"},
|
|
{name: "bar"},
|
|
},
|
|
gids: []int64{100},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "valid UIDs and GIDs are assigned to input",
|
|
groups: []*entry{
|
|
{name: "foo", id: 110},
|
|
{name: "bar", id: 111},
|
|
},
|
|
usersToCreate: []*entry{
|
|
{name: "foo"},
|
|
{name: "bar"},
|
|
{name: "baz"},
|
|
},
|
|
groupsToCreate: []*entry{
|
|
{name: "baz"},
|
|
},
|
|
uids: []int64{100, 101, 102},
|
|
gids: []int64{112},
|
|
expectedUsers: []*entry{
|
|
{name: "foo", id: 100, gid: 110},
|
|
{name: "bar", id: 101, gid: 111},
|
|
{name: "baz", id: 102, gid: 112},
|
|
},
|
|
expectedGroups: []*entry{
|
|
{name: "baz", id: 112},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
err := assignUserAndGroupIDs(tc.groups, tc.usersToCreate, tc.groupsToCreate, tc.uids, tc.gids)
|
|
if err != nil != tc.expectedError {
|
|
t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(tc.expectedUsers) != len(tc.usersToCreate) {
|
|
t.Fatalf("expected users %d, got %d", len(tc.expectedUsers), len(tc.usersToCreate))
|
|
}
|
|
for i := range tc.usersToCreate {
|
|
if !reflect.DeepEqual(tc.expectedUsers[i], tc.usersToCreate[i]) {
|
|
t.Fatalf("expected user at position %d: %+v, got: %+v", i, tc.expectedUsers[i], tc.usersToCreate[i])
|
|
}
|
|
}
|
|
if len(tc.expectedGroups) != len(tc.groupsToCreate) {
|
|
t.Fatalf("expected groups %d, got %d", len(tc.expectedGroups), len(tc.groupsToCreate))
|
|
}
|
|
for i := range tc.groupsToCreate {
|
|
if !reflect.DeepEqual(tc.expectedGroups[i], tc.groupsToCreate[i]) {
|
|
t.Fatalf("expected group at position %d: %+v, got: %+v", i, tc.expectedGroups[i], tc.groupsToCreate[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestID(t *testing.T) {
|
|
e := &entry{name: "foo", id: 101}
|
|
m := &EntryMap{entries: map[string]*entry{
|
|
"foo": e,
|
|
}}
|
|
id := m.ID("foo")
|
|
if *id != 101 {
|
|
t.Fatalf("expected: id=%d; got: id=%d", 101, *id)
|
|
}
|
|
id = m.ID("bar")
|
|
if id != nil {
|
|
t.Fatalf("expected nil for unknown entry")
|
|
}
|
|
}
|
|
|
|
func TestAddUsersAndGroupsImpl(t *testing.T) {
|
|
const (
|
|
loginDef = "SYS_UID_MIN 101\nSYS_UID_MAX 998\nSYS_GID_MIN 102\nSYS_GID_MAX 999\n"
|
|
passwd = "root:x:0:0:::/bin/bash\nkubeadm-etcd:x:101:102:::/bin/false\n"
|
|
group = "root:x:0:root\nkubeadm-etcd:x:102:kubeadm-etcd\n"
|
|
expectedUsers = "kubeadm-etcd{101,102};kubeadm-kas{102,103};kubeadm-kcm{103,104};kubeadm-ks{104,105};"
|
|
expectedGroups = "kubeadm-etcd{102,0};kubeadm-kas{103,0};kubeadm-kcm{104,0};kubeadm-ks{105,0};kubeadm-sa-key-readers{106,0};"
|
|
)
|
|
fileLoginDef, close := writeTempFile(t, loginDef)
|
|
defer close()
|
|
filePasswd, close := writeTempFile(t, passwd)
|
|
defer close()
|
|
fileGroup, close := writeTempFile(t, group)
|
|
defer close()
|
|
got, err := addUsersAndGroupsImpl(fileLoginDef, filePasswd, fileGroup)
|
|
if err != nil {
|
|
t.Fatalf("AddUsersAndGroups failed: %v", err)
|
|
}
|
|
if expectedUsers != got.Users.String() {
|
|
t.Fatalf("expected users: %q, got: %q", expectedUsers, got.Users.String())
|
|
}
|
|
if expectedGroups != got.Groups.String() {
|
|
t.Fatalf("expected groups: %q, got: %q", expectedGroups, got.Groups.String())
|
|
}
|
|
}
|
|
|
|
func TestRemoveUsersAndGroups(t *testing.T) {
|
|
const (
|
|
passwd = "root:x:0:0:::/bin/bash\nkubeadm-etcd:x:101:102:::/bin/false\n"
|
|
group = "root:x:0:root\nkubeadm-etcd:x:102:kubeadm-etcd\n"
|
|
expectedPasswd = "root:x:0:0:::/bin/bash\n"
|
|
expectedGroup = "root:x:0:root\n"
|
|
)
|
|
filePasswd, close := writeTempFile(t, passwd)
|
|
defer close()
|
|
fileGroup, close := writeTempFile(t, group)
|
|
defer close()
|
|
if err := removeUsersAndGroupsImpl(filePasswd, fileGroup); err != nil {
|
|
t.Fatalf("RemoveUsersAndGroups failed: %v", err)
|
|
}
|
|
contentsPasswd := readTempFile(t, filePasswd)
|
|
if expectedPasswd != contentsPasswd {
|
|
t.Fatalf("expected passwd:\n%s\ngot:\n%s\n", expectedPasswd, contentsPasswd)
|
|
}
|
|
contentsGroup := readTempFile(t, fileGroup)
|
|
if expectedGroup != contentsGroup {
|
|
t.Fatalf("expected passwd:\n%s\ngot:\n%s\n", expectedGroup, contentsGroup)
|
|
}
|
|
}
|
|
|
|
func writeTempFile(t *testing.T, contents string) (string, func()) {
|
|
file, err := os.CreateTemp("", "")
|
|
if err != nil {
|
|
t.Fatalf("could not create file: %v", err)
|
|
}
|
|
if err := os.WriteFile(file.Name(), []byte(contents), os.ModePerm); err != nil {
|
|
t.Fatalf("could not write file: %v", err)
|
|
}
|
|
close := func() {
|
|
os.Remove(file.Name())
|
|
}
|
|
return file.Name(), close
|
|
}
|
|
|
|
func readTempFile(t *testing.T, path string) string {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("could not read file: %v", err)
|
|
}
|
|
return string(b)
|
|
}
|