...

Source file src/k8s.io/kubernetes/cmd/kubeadm/app/util/users/users_linux_test.go

Documentation: k8s.io/kubernetes/cmd/kubeadm/app/util/users

     1  //go:build linux
     2  // +build linux
     3  
     4  /*
     5  Copyright 2021 The Kubernetes Authors.
     6  
     7  Licensed under the Apache License, Version 2.0 (the "License");
     8  you may not use this file except in compliance with the License.
     9  You may obtain a copy of the License at
    10  
    11      http://www.apache.org/licenses/LICENSE-2.0
    12  
    13  Unless required by applicable law or agreed to in writing, software
    14  distributed under the License is distributed on an "AS IS" BASIS,
    15  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16  See the License for the specific language governing permissions and
    17  limitations under the License.
    18  */
    19  
    20  package users
    21  
    22  import (
    23  	"os"
    24  	"reflect"
    25  	"testing"
    26  )
    27  
    28  func TestParseLoginDef(t *testing.T) {
    29  	testCases := []struct {
    30  		name           string
    31  		input          string
    32  		expectedLimits *limits
    33  		expectedError  bool
    34  	}{
    35  		{
    36  			name:          "non number value for tracked limit",
    37  			input:         "SYS_UID_MIN foo\n",
    38  			expectedError: true,
    39  		},
    40  		{
    41  			name:           "empty string must return defaults",
    42  			expectedLimits: defaultLimits,
    43  		},
    44  		{
    45  			name:           "no tracked limits in file must return defaults",
    46  			input:          "# some comment\n",
    47  			expectedLimits: defaultLimits,
    48  		},
    49  		{
    50  			name:           "must parse all valid tracked limits",
    51  			input:          "SYS_UID_MIN 101\nSYS_UID_MAX 998\nSYS_GID_MIN 102\nSYS_GID_MAX 999\n",
    52  			expectedLimits: &limits{minUID: 101, maxUID: 998, minGID: 102, maxGID: 999},
    53  		},
    54  		{
    55  			name:           "must return defaults for missing limits",
    56  			input:          "SYS_UID_MIN 101\n#SYS_UID_MAX 998\nSYS_GID_MIN 102\n#SYS_GID_MAX 999\n",
    57  			expectedLimits: &limits{minUID: 101, maxUID: defaultLimits.maxUID, minGID: 102, maxGID: defaultLimits.maxGID},
    58  		},
    59  	}
    60  
    61  	for _, tc := range testCases {
    62  		t.Run(tc.name, func(t *testing.T) {
    63  			got, err := parseLoginDefs(tc.input)
    64  			if err != nil != tc.expectedError {
    65  				t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
    66  			}
    67  			if err == nil && *tc.expectedLimits != *got {
    68  				t.Fatalf("expected limits %+v, got %+v", tc.expectedLimits, got)
    69  			}
    70  		})
    71  	}
    72  }
    73  
    74  func TestParseEntries(t *testing.T) {
    75  	testCases := []struct {
    76  		name            string
    77  		file            string
    78  		expectedEntries []*entry
    79  		totalFields     int
    80  		expectedError   bool
    81  	}{
    82  		{
    83  			name:          "totalFields must be a known value",
    84  			expectedError: true,
    85  		},
    86  		{
    87  			name:          "unexpected number of fields",
    88  			file:          "foo:x:100::::::",
    89  			totalFields:   totalFieldsUser,
    90  			expectedError: true,
    91  		},
    92  		{
    93  			name:          "cannot parse 'bar' as UID",
    94  			file:          "foo:x:bar:101:::\n",
    95  			totalFields:   totalFieldsUser,
    96  			expectedError: true,
    97  		},
    98  		{
    99  			name:          "cannot parse 'bar' as GID",
   100  			file:          "foo:x:101:bar:::\n",
   101  			totalFields:   totalFieldsUser,
   102  			expectedError: true,
   103  		},
   104  		{
   105  			name:        "valid file for users",
   106  			file:        "\nfoo:x:100:101:foo:/home/foo:/bin/bash\n\nbar:x:102:103:bar::\n",
   107  			totalFields: totalFieldsUser,
   108  			expectedEntries: []*entry{
   109  				{name: "foo", id: 100, gid: 101, shell: "/bin/bash"},
   110  				{name: "bar", id: 102, gid: 103},
   111  			},
   112  		},
   113  		{
   114  			name:        "valid file for groups",
   115  			file:        "\nfoo:x:100:bar,baz\n\nbar:x:101:baz\n",
   116  			totalFields: totalFieldsGroup,
   117  			expectedEntries: []*entry{
   118  				{name: "foo", id: 100, userNames: []string{"bar", "baz"}},
   119  				{name: "bar", id: 101, userNames: []string{"baz"}},
   120  			},
   121  		},
   122  	}
   123  
   124  	for _, tc := range testCases {
   125  		t.Run(tc.name, func(t *testing.T) {
   126  			got, err := parseEntries(tc.file, tc.totalFields)
   127  			if err != nil != tc.expectedError {
   128  				t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
   129  			}
   130  			if err != nil {
   131  				return
   132  			}
   133  			if len(tc.expectedEntries) != len(got) {
   134  				t.Fatalf("expected entries %d, got %d", len(tc.expectedEntries), len(got))
   135  			}
   136  			for i := range got {
   137  				if !reflect.DeepEqual(tc.expectedEntries[i], got[i]) {
   138  					t.Fatalf("expected entry at position %d: %+v, got: %+v", i, tc.expectedEntries[i], got[i])
   139  				}
   140  			}
   141  		})
   142  	}
   143  }
   144  
   145  func TestValidateEntries(t *testing.T) {
   146  	testCases := []struct {
   147  		name           string
   148  		users          []*entry
   149  		groups         []*entry
   150  		expectedUsers  []*entry
   151  		expectedGroups []*entry
   152  		expectedError  bool
   153  	}{
   154  		{
   155  			name: "UID for user is outside of system limits",
   156  			users: []*entry{
   157  				{name: "kubeadm-etcd", id: 2000, gid: 102, shell: noshell},
   158  			},
   159  			groups:        []*entry{},
   160  			expectedError: true,
   161  		},
   162  		{
   163  			name: "user has unexpected shell",
   164  			users: []*entry{
   165  				{name: "kubeadm-etcd", id: 102, gid: 102, shell: "foo"},
   166  			},
   167  			groups:        []*entry{},
   168  			expectedError: true,
   169  		},
   170  		{
   171  			name: "user is mapped to unknown group",
   172  			users: []*entry{
   173  				{name: "kubeadm-etcd", id: 102, gid: 102, shell: noshell},
   174  			},
   175  			groups:        []*entry{},
   176  			expectedError: true,
   177  		},
   178  		{
   179  			name: "user and group names do not match",
   180  			users: []*entry{
   181  				{name: "kubeadm-etcd", id: 102, gid: 102, shell: noshell},
   182  			},
   183  			groups: []*entry{
   184  				{name: "foo", id: 102},
   185  			},
   186  			expectedError: true,
   187  		},
   188  		{
   189  			name:  "GID is outside system limits",
   190  			users: []*entry{},
   191  			groups: []*entry{
   192  				{name: "kubeadm-etcd", id: 2000},
   193  			},
   194  			expectedError: true,
   195  		},
   196  		{
   197  			name:  "group is missing users",
   198  			users: []*entry{},
   199  			groups: []*entry{
   200  				{name: "kubeadm-etcd", id: 100},
   201  			},
   202  			expectedError: true,
   203  		},
   204  		{
   205  			name:           "empty input must return default users and groups",
   206  			users:          []*entry{},
   207  			groups:         []*entry{},
   208  			expectedUsers:  usersToCreateSpec,
   209  			expectedGroups: groupsToCreateSpec,
   210  		},
   211  		{
   212  			name: "existing valid users mapped to groups",
   213  			users: []*entry{
   214  				{name: "kubeadm-etcd", id: 100, gid: 102, shell: noshell},
   215  				{name: "kubeadm-kas", id: 101, gid: 103, shell: noshell},
   216  			},
   217  			groups: []*entry{
   218  				{name: "kubeadm-etcd", id: 102, userNames: []string{"kubeadm-etcd"}},
   219  				{name: "kubeadm-kas", id: 103, userNames: []string{"kubeadm-kas"}},
   220  				{name: "kubeadm-sa-key-readers", id: 104, userNames: []string{"kubeadm-kas", "kubeadm-kcm"}},
   221  			},
   222  			expectedUsers: []*entry{
   223  				{name: "kubeadm-kcm"},
   224  				{name: "kubeadm-ks"},
   225  			},
   226  			expectedGroups: []*entry{
   227  				{name: "kubeadm-kcm", userNames: []string{"kubeadm-kcm"}},
   228  				{name: "kubeadm-ks", userNames: []string{"kubeadm-ks"}},
   229  			},
   230  		},
   231  	}
   232  
   233  	for _, tc := range testCases {
   234  		t.Run(tc.name, func(t *testing.T) {
   235  			users, groups, err := validateEntries(tc.users, tc.groups, defaultLimits)
   236  			if err != nil != tc.expectedError {
   237  				t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
   238  			}
   239  			if err != nil {
   240  				return
   241  			}
   242  			if len(tc.expectedUsers) != len(users) {
   243  				t.Fatalf("expected users %d, got %d", len(tc.expectedUsers), len(users))
   244  			}
   245  			for i := range users {
   246  				if !reflect.DeepEqual(tc.expectedUsers[i], users[i]) {
   247  					t.Fatalf("expected user at position %d: %+v, got: %+v", i, tc.expectedUsers[i], users[i])
   248  				}
   249  			}
   250  			if len(tc.expectedGroups) != len(groups) {
   251  				t.Fatalf("expected groups %d, got %d", len(tc.expectedGroups), len(groups))
   252  			}
   253  			for i := range groups {
   254  				if !reflect.DeepEqual(tc.expectedGroups[i], groups[i]) {
   255  					t.Fatalf("expected group at position %d: %+v, got: %+v", i, tc.expectedGroups[i], groups[i])
   256  				}
   257  			}
   258  		})
   259  	}
   260  }
   261  
   262  func TestAllocateIDs(t *testing.T) {
   263  	testCases := []struct {
   264  		name          string
   265  		entries       []*entry
   266  		min           int64
   267  		max           int64
   268  		total         int
   269  		expectedIDs   []int64
   270  		expectedError bool
   271  	}{
   272  		{
   273  			name:        "zero total ids returns empty slice",
   274  			expectedIDs: []int64{},
   275  		},
   276  		{
   277  			name: "not enough free ids in range",
   278  			entries: []*entry{
   279  				{name: "foo", id: 101},
   280  				{name: "bar", id: 103},
   281  				{name: "baz", id: 105},
   282  			},
   283  			min:           100,
   284  			max:           105,
   285  			total:         4,
   286  			expectedError: true,
   287  		},
   288  		{
   289  			name: "successfully allocate ids",
   290  			entries: []*entry{
   291  				{name: "foo", id: 101},
   292  				{name: "bar", id: 103},
   293  				{name: "baz", id: 105},
   294  			},
   295  			min:           100,
   296  			max:           110,
   297  			total:         4,
   298  			expectedIDs:   []int64{100, 102, 104, 106},
   299  			expectedError: false,
   300  		},
   301  	}
   302  
   303  	for _, tc := range testCases {
   304  		t.Run(tc.name, func(t *testing.T) {
   305  			got, err := allocateIDs(tc.entries, tc.min, tc.max, tc.total)
   306  			if err != nil != tc.expectedError {
   307  				t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
   308  			}
   309  			if err != nil {
   310  				return
   311  			}
   312  			if len(tc.expectedIDs) != len(got) {
   313  				t.Fatalf("expected id %d, got %d", len(tc.expectedIDs), len(got))
   314  			}
   315  			for i := range got {
   316  				if !reflect.DeepEqual(tc.expectedIDs[i], got[i]) {
   317  					t.Fatalf("expected id at position %d: %+v, got: %+v", i, tc.expectedIDs[i], got[i])
   318  				}
   319  			}
   320  		})
   321  	}
   322  }
   323  
   324  func TestAddEntries(t *testing.T) {
   325  	testCases := []struct {
   326  		name           string
   327  		file           string
   328  		entries        []*entry
   329  		createEntry    func(*entry) string
   330  		expectedOutput string
   331  	}{
   332  		{
   333  			name: "user entries are added",
   334  			file: "foo:x:101:101:::/bin/false\n",
   335  			entries: []*entry{
   336  				{name: "bar", id: 102, gid: 102},
   337  				{name: "baz", id: 103, gid: 103},
   338  			},
   339  			expectedOutput: "foo:x:101:101:::/bin/false\nbar:x:102:102:::/bin/false\nbaz:x:103:103:::/bin/false\n",
   340  			createEntry:    createUser,
   341  		},
   342  		{
   343  			name: "user entries are added (new line is appended)",
   344  			file: "foo:x:101:101:::/bin/false",
   345  			entries: []*entry{
   346  				{name: "bar", id: 102, gid: 102},
   347  			},
   348  			expectedOutput: "foo:x:101:101:::/bin/false\nbar:x:102:102:::/bin/false\n",
   349  			createEntry:    createUser,
   350  		},
   351  		{
   352  			name: "group entries are added",
   353  			file: "foo:x:101:foo\n",
   354  			entries: []*entry{
   355  				{name: "bar", id: 102, userNames: []string{"bar"}},
   356  				{name: "baz", id: 103, userNames: []string{"baz"}},
   357  			},
   358  			expectedOutput: "foo:x:101:foo\nbar:x:102:bar\nbaz:x:103:baz\n",
   359  			createEntry:    createGroup,
   360  		},
   361  	}
   362  
   363  	for _, tc := range testCases {
   364  		t.Run(tc.name, func(t *testing.T) {
   365  			got := addEntries(tc.file, tc.entries, tc.createEntry)
   366  			if tc.expectedOutput != got {
   367  				t.Fatalf("expected output:\n%s\ngot:\n%s\n", tc.expectedOutput, got)
   368  			}
   369  		})
   370  	}
   371  }
   372  
   373  func TestRemoveEntries(t *testing.T) {
   374  	testCases := []struct {
   375  		name            string
   376  		file            string
   377  		entries         []*entry
   378  		expectedRemoved int
   379  		expectedOutput  string
   380  	}{
   381  		{
   382  			name:            "entries that are missing do not cause an error",
   383  			file:            "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
   384  			entries:         []*entry{},
   385  			expectedRemoved: 0,
   386  			expectedOutput:  "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
   387  		},
   388  		{
   389  			name: "user entry is removed",
   390  			file: "foo:x:102:102:::/bin/false\nbar:x:103:103:::/bin/false\n",
   391  			entries: []*entry{
   392  				{name: "bar"},
   393  			},
   394  			expectedRemoved: 1,
   395  			expectedOutput:  "foo:x:102:102:::/bin/false\n",
   396  		},
   397  		{
   398  			name: "group entry is removed",
   399  			file: "foo:x:102:foo\nbar:x:102:bar\n",
   400  			entries: []*entry{
   401  				{name: "bar"},
   402  			},
   403  			expectedRemoved: 1,
   404  			expectedOutput:  "foo:x:102:foo\n",
   405  		},
   406  	}
   407  
   408  	for _, tc := range testCases {
   409  		t.Run(tc.name, func(t *testing.T) {
   410  			got, removed := removeEntries(tc.file, tc.entries)
   411  			if tc.expectedRemoved != removed {
   412  				t.Fatalf("expected entries to be removed: %v, got: %v", tc.expectedRemoved, removed)
   413  			}
   414  			if tc.expectedOutput != got {
   415  				t.Fatalf("expected output:\n%s\ngot:\n%s\n", tc.expectedOutput, got)
   416  			}
   417  		})
   418  	}
   419  }
   420  
   421  func TestAssignUserAndGroupIDs(t *testing.T) {
   422  	testCases := []struct {
   423  		name           string
   424  		users          []*entry
   425  		groups         []*entry
   426  		usersToCreate  []*entry
   427  		groupsToCreate []*entry
   428  		uids           []int64
   429  		gids           []int64
   430  		expectedUsers  []*entry
   431  		expectedGroups []*entry
   432  		expectedError  bool
   433  	}{
   434  		{
   435  			name: "not enough UIDs",
   436  			usersToCreate: []*entry{
   437  				{name: "foo"},
   438  				{name: "bar"},
   439  			},
   440  			uids:          []int64{100},
   441  			expectedError: true,
   442  		},
   443  		{
   444  			name: "not enough GIDs",
   445  			groupsToCreate: []*entry{
   446  				{name: "foo"},
   447  				{name: "bar"},
   448  			},
   449  			gids:          []int64{100},
   450  			expectedError: true,
   451  		},
   452  		{
   453  			name: "valid UIDs and GIDs are assigned to input",
   454  			groups: []*entry{
   455  				{name: "foo", id: 110},
   456  				{name: "bar", id: 111},
   457  			},
   458  			usersToCreate: []*entry{
   459  				{name: "foo"},
   460  				{name: "bar"},
   461  				{name: "baz"},
   462  			},
   463  			groupsToCreate: []*entry{
   464  				{name: "baz"},
   465  			},
   466  			uids: []int64{100, 101, 102},
   467  			gids: []int64{112},
   468  			expectedUsers: []*entry{
   469  				{name: "foo", id: 100, gid: 110},
   470  				{name: "bar", id: 101, gid: 111},
   471  				{name: "baz", id: 102, gid: 112},
   472  			},
   473  			expectedGroups: []*entry{
   474  				{name: "baz", id: 112},
   475  			},
   476  		},
   477  	}
   478  
   479  	for _, tc := range testCases {
   480  		t.Run(tc.name, func(t *testing.T) {
   481  			err := assignUserAndGroupIDs(tc.groups, tc.usersToCreate, tc.groupsToCreate, tc.uids, tc.gids)
   482  			if err != nil != tc.expectedError {
   483  				t.Fatalf("expected error: %v, got: %v, error: %v", tc.expectedError, err != nil, err)
   484  			}
   485  			if err != nil {
   486  				return
   487  			}
   488  			if len(tc.expectedUsers) != len(tc.usersToCreate) {
   489  				t.Fatalf("expected users %d, got %d", len(tc.expectedUsers), len(tc.usersToCreate))
   490  			}
   491  			for i := range tc.usersToCreate {
   492  				if !reflect.DeepEqual(tc.expectedUsers[i], tc.usersToCreate[i]) {
   493  					t.Fatalf("expected user at position %d: %+v, got: %+v", i, tc.expectedUsers[i], tc.usersToCreate[i])
   494  				}
   495  			}
   496  			if len(tc.expectedGroups) != len(tc.groupsToCreate) {
   497  				t.Fatalf("expected groups %d, got %d", len(tc.expectedGroups), len(tc.groupsToCreate))
   498  			}
   499  			for i := range tc.groupsToCreate {
   500  				if !reflect.DeepEqual(tc.expectedGroups[i], tc.groupsToCreate[i]) {
   501  					t.Fatalf("expected group at position %d: %+v, got: %+v", i, tc.expectedGroups[i], tc.groupsToCreate[i])
   502  				}
   503  			}
   504  		})
   505  	}
   506  }
   507  
   508  func TestID(t *testing.T) {
   509  	e := &entry{name: "foo", id: 101}
   510  	m := &EntryMap{entries: map[string]*entry{
   511  		"foo": e,
   512  	}}
   513  	id := m.ID("foo")
   514  	if *id != 101 {
   515  		t.Fatalf("expected: id=%d; got: id=%d", 101, *id)
   516  	}
   517  	id = m.ID("bar")
   518  	if id != nil {
   519  		t.Fatalf("expected nil for unknown entry")
   520  	}
   521  }
   522  
   523  func TestAddUsersAndGroupsImpl(t *testing.T) {
   524  	const (
   525  		loginDef       = "SYS_UID_MIN 101\nSYS_UID_MAX 998\nSYS_GID_MIN 102\nSYS_GID_MAX 999\n"
   526  		passwd         = "root:x:0:0:::/bin/bash\nkubeadm-etcd:x:101:102:::/bin/false\n"
   527  		group          = "root:x:0:root\nkubeadm-etcd:x:102:kubeadm-etcd\n"
   528  		expectedUsers  = "kubeadm-etcd{101,102};kubeadm-kas{102,103};kubeadm-kcm{103,104};kubeadm-ks{104,105};"
   529  		expectedGroups = "kubeadm-etcd{102,0};kubeadm-kas{103,0};kubeadm-kcm{104,0};kubeadm-ks{105,0};kubeadm-sa-key-readers{106,0};"
   530  	)
   531  	fileLoginDef, close := writeTempFile(t, loginDef)
   532  	defer close()
   533  	filePasswd, close := writeTempFile(t, passwd)
   534  	defer close()
   535  	fileGroup, close := writeTempFile(t, group)
   536  	defer close()
   537  	got, err := addUsersAndGroupsImpl(fileLoginDef, filePasswd, fileGroup)
   538  	if err != nil {
   539  		t.Fatalf("AddUsersAndGroups failed: %v", err)
   540  	}
   541  	if expectedUsers != got.Users.String() {
   542  		t.Fatalf("expected users: %q, got: %q", expectedUsers, got.Users.String())
   543  	}
   544  	if expectedGroups != got.Groups.String() {
   545  		t.Fatalf("expected groups: %q, got: %q", expectedGroups, got.Groups.String())
   546  	}
   547  }
   548  
   549  func TestRemoveUsersAndGroups(t *testing.T) {
   550  	const (
   551  		passwd         = "root:x:0:0:::/bin/bash\nkubeadm-etcd:x:101:102:::/bin/false\n"
   552  		group          = "root:x:0:root\nkubeadm-etcd:x:102:kubeadm-etcd\n"
   553  		expectedPasswd = "root:x:0:0:::/bin/bash\n"
   554  		expectedGroup  = "root:x:0:root\n"
   555  	)
   556  	filePasswd, close := writeTempFile(t, passwd)
   557  	defer close()
   558  	fileGroup, close := writeTempFile(t, group)
   559  	defer close()
   560  	if err := removeUsersAndGroupsImpl(filePasswd, fileGroup); err != nil {
   561  		t.Fatalf("RemoveUsersAndGroups failed: %v", err)
   562  	}
   563  	contentsPasswd := readTempFile(t, filePasswd)
   564  	if expectedPasswd != contentsPasswd {
   565  		t.Fatalf("expected passwd:\n%s\ngot:\n%s\n", expectedPasswd, contentsPasswd)
   566  	}
   567  	contentsGroup := readTempFile(t, fileGroup)
   568  	if expectedGroup != contentsGroup {
   569  		t.Fatalf("expected passwd:\n%s\ngot:\n%s\n", expectedGroup, contentsGroup)
   570  	}
   571  }
   572  
   573  func writeTempFile(t *testing.T, contents string) (string, func()) {
   574  	file, err := os.CreateTemp("", "")
   575  	if err != nil {
   576  		t.Fatalf("could not create file: %v", err)
   577  	}
   578  	if err := os.WriteFile(file.Name(), []byte(contents), os.ModePerm); err != nil {
   579  		t.Fatalf("could not write file: %v", err)
   580  	}
   581  	close := func() {
   582  		os.Remove(file.Name())
   583  	}
   584  	return file.Name(), close
   585  }
   586  
   587  func readTempFile(t *testing.T, path string) string {
   588  	b, err := os.ReadFile(path)
   589  	if err != nil {
   590  		t.Fatalf("could not read file: %v", err)
   591  	}
   592  	return string(b)
   593  }
   594  

View as plain text