Skip to content

Commit 9827232

Browse files
committed
bugfix: don't crash when a user has no group. Instead display an error
1 parent 9a9ba38 commit 9827232

6 files changed

Lines changed: 135 additions & 43 deletions

File tree

cmd/sshproxyctl/sshproxyctl.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,15 @@ func (fu flatUsers) getAllUsers(allFlag bool, passthrough bool) ([][]string, map
309309
totalOut := 0
310310

311311
for i, v := range fu {
312+
groups := v.Groups
313+
if !passthrough && groups == "" {
314+
groups = "\u274C"
315+
}
312316
if allFlag {
313317
rows[i] = []string{
314318
v.User,
315319
v.Service,
316-
v.Groups,
320+
groups,
317321
fmt.Sprintf("%d", v.N),
318322
byteToHuman(v.BwIn, passthrough),
319323
byteToHuman(v.BwOut, passthrough),
@@ -323,7 +327,7 @@ func (fu flatUsers) getAllUsers(allFlag bool, passthrough bool) ([][]string, map
323327
} else {
324328
rows[i] = []string{
325329
v.User,
326-
v.Groups,
330+
groups,
327331
fmt.Sprintf("%d", v.N),
328332
byteToHuman(v.BwIn, passthrough),
329333
byteToHuman(v.BwOut, passthrough),
@@ -422,9 +426,13 @@ type flatGroups []*utils.FlatGroup
422426
func (fg flatGroups) getAllGroups(allFlag bool, passthrough bool) [][]string {
423427
rows := make([][]string, len(fg))
424428
for i, v := range fg {
429+
group := v.Group
430+
if !passthrough && group == "" {
431+
group = "\u274C"
432+
}
425433
if allFlag {
426434
rows[i] = []string{
427-
v.Group,
435+
group,
428436
v.Service,
429437
v.Users,
430438
fmt.Sprintf("%d", v.N),
@@ -433,7 +441,7 @@ func (fg flatGroups) getAllGroups(allFlag bool, passthrough bool) [][]string {
433441
}
434442
} else {
435443
rows[i] = []string{
436-
v.Group,
444+
group,
437445
v.Users,
438446
fmt.Sprintf("%d", v.N),
439447
byteToHuman(v.BwIn, passthrough),

pkg/utils/etcd.go

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -744,16 +744,7 @@ func (c *Client) GetAllUsers(allFlag bool) ([]*FlatUser, error) {
744744
}
745745
if users[key] == nil {
746746
v := &FlatUser{}
747-
groups, err := GetGroupList(connection.User)
748-
if err != nil {
749-
return nil, err
750-
}
751-
g := make([]string, 0, len(groups))
752-
for group := range groups {
753-
g = append(g, group)
754-
}
755-
sort.Strings(g)
756-
v.Groups = strings.Join(g, " ")
747+
v.Groups = GetSortedGroups(connection.User)
757748
v.N = 1
758749
v.BwIn = connection.BwIn
759750
v.BwOut = connection.BwOut
@@ -774,16 +765,7 @@ func (c *Client) GetAllUsers(allFlag bool) ([]*FlatUser, error) {
774765
key := hist.User
775766
if users[key] == nil {
776767
v := &FlatUser{}
777-
groups, err := GetGroupList(strings.Split(hist.User, "@")[0])
778-
if err != nil {
779-
return nil, err
780-
}
781-
g := make([]string, 0, len(groups))
782-
for group := range groups {
783-
g = append(g, group)
784-
}
785-
sort.Strings(g)
786-
v.Groups = strings.Join(g, " ")
768+
v.Groups = GetSortedGroups(strings.Split(hist.User, "@")[0])
787769
v.Dest = hist.Dest
788770
v.TTL = hist.TTL
789771
users[key] = v

pkg/utils/utils.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@ import (
1414
"crypto/sha1"
1515
"fmt"
1616
"net"
17+
"os"
1718
"os/user"
19+
"sort"
1820
"strconv"
21+
"strings"
1922
"time"
2023
)
2124

@@ -111,6 +114,26 @@ func GetGroupList(username string) (map[string]bool, error) {
111114
return groups, nil
112115
}
113116

117+
// GetSortedGroups returns a string of sorted space-separated groups for the
118+
// specified user.
119+
//
120+
// It displays a warning when a user has no group (happens when a user has been
121+
// deleted, but still has an open connection
122+
func GetSortedGroups(username string) string {
123+
groups, err := GetGroupList(username)
124+
if err != nil {
125+
fmt.Fprintln(os.Stderr, err.Error())
126+
return ""
127+
} else {
128+
g := make([]string, 0, len(groups))
129+
for group := range groups {
130+
g = append(g, group)
131+
}
132+
sort.Strings(g)
133+
return strings.Join(g, " ")
134+
}
135+
}
136+
114137
// Mocking net.LookupHost for testing.
115138
var netLookupHost = net.LookupHost
116139

pkg/utils/utils_test.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111
package utils
1212

1313
import (
14+
"bytes"
1415
"errors"
1516
"fmt"
17+
"io"
18+
"os"
1619
"os/user"
1720
"reflect"
1821
"sort"
@@ -174,7 +177,7 @@ var getGroupListTests = []struct {
174177
{"testuser", "testgroup", ""},
175178
{"userwithnogroupid", "", "user: list groups for userwithnogroupid: invalid gid \"\""},
176179
{"userwithinvalidgroup", "nonexistentgroup", "group: unknown group ID 1002"},
177-
{"nonexistentuser", "nonexistentgroup", "user: unknown user nonexistentuser"},
180+
{"nonexistentuser", "", "user: unknown user nonexistentuser"},
178181
}
179182

180183
func TestGetGroupList(t *testing.T) {
@@ -211,6 +214,57 @@ func BenchmarkGetGroupList(b *testing.B) {
211214
}
212215
}
213216

217+
var getSortedGroupsTests = []struct {
218+
user, groups, err string
219+
}{
220+
{"root", "root", ""},
221+
{"testuser", "testgroup", ""},
222+
{"userwithnogroupid", "", "user: list groups for userwithnogroupid: invalid gid \"\"\n"},
223+
{"userwithinvalidgroup", "", "group: unknown group ID 1002\n"},
224+
{"nonexistentuser", "", "user: unknown user nonexistentuser\n"},
225+
}
226+
227+
func TestGetSortedGroups(t *testing.T) {
228+
userLookup = mockUserLookup
229+
userLookupGroupId = mockUserLookupGroupId
230+
for _, tt := range getSortedGroupsTests {
231+
// Save the original stderr
232+
originalStderr := os.Stderr
233+
234+
// Create a new buffer and redirect stderr
235+
var buf bytes.Buffer
236+
r, w, err := os.Pipe()
237+
if err != nil {
238+
t.Fatalf("Failed to create pipe: %v", err)
239+
}
240+
os.Stderr = w
241+
242+
groups := GetSortedGroups(tt.user)
243+
244+
// Stop writing and restore stderr
245+
w.Close()
246+
os.Stderr = originalStderr
247+
io.Copy(&buf, r)
248+
249+
// Verify the output
250+
errStr := buf.String()
251+
if errStr != tt.err {
252+
t.Errorf("GetSortedGroups err = %s, want %s", errStr, tt.err)
253+
}
254+
if groups != tt.groups {
255+
t.Errorf("GetSortedGroups groups = %s, want %s", groups, tt.groups)
256+
}
257+
}
258+
}
259+
260+
func BenchmarkGetSortedGroups(b *testing.B) {
261+
b.Run("root", func(b *testing.B) {
262+
for i := 0; i < b.N; i++ {
263+
GetSortedGroups("root")
264+
}
265+
})
266+
}
267+
214268
func mockNetLookupHost(host string) ([]string, error) {
215269
if host == "err" {
216270
return nil, errors.New("LookupHost error")

test/fedora-image/Dockerfile

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ RUN set -ex \
55
&& yum -y update \
66
&& yum -y install asciidoc etcd git golang hostname iproute make openssh-server rpm-build procps
77

8-
# Create fedora, user1 and user2 users ; fedora and user1 groups
8+
# Create fedora, user1, user2 and tmpuser users ; fedora and user1 groups
99
RUN set -ex \
1010
&& useradd fedora \
1111
&& install -d -m0755 -o fedora -g fedora /home/fedora/.ssh \
1212
&& useradd user1 \
1313
&& install -d -m0755 -o user1 -g user1 /home/user1/.ssh \
1414
&& useradd -g user1 user2 \
15-
&& install -d -m0755 -o user2 -g user1 /home/user2/.ssh
15+
&& install -d -m0755 -o user2 -g user1 /home/user2/.ssh \
16+
&& useradd -g user1 tmpuser \
17+
&& install -d -m0755 -o tmpuser -g user1 /home/tmpuser/.ssh
1618

1719
# Copy fedora public key to root authorized_keys
1820
RUN set -ex && install -d -m0700 /root/.ssh
@@ -40,6 +42,10 @@ COPY --chown=user2:user1 ./ssh/id_ed25519.pub /home/user2/.ssh/authorized_keys
4042
COPY --chown=user2:user1 ./ssh/id_ed25519* ./ssh/known_hosts /home/user2/.ssh/
4143
RUN chmod 0600 /home/user2/.ssh/id_ed25519 /home/user2/.ssh/authorized_keys
4244

45+
# Copy tmpuser ssh keys
46+
COPY --chown=tmpuser:user1 ./ssh/id_ed25519.pub /home/tmpuser/.ssh/authorized_keys
47+
COPY --chown=tmpuser:user1 ./ssh/id_ed25519* ./ssh/known_hosts /home/tmpuser/.ssh/
48+
RUN chmod 0600 /home/tmpuser/.ssh/id_ed25519 /home/tmpuser/.ssh/authorized_keys
4349

4450
# Copy etcd certificates and keys
4551
COPY ./etcd/*.pem /etc/etcd/

test/fedora-image/sshproxy_test.go

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -42,31 +42,23 @@ var (
4242
)
4343

4444
func addLineSSHProxyConf(line string) {
45-
ctx := context.Background()
46-
for _, gateway := range gateways {
47-
_, _, _, err := runCommand(ctx, "ssh", []string{fmt.Sprintf("root@%s", gateway), "--", fmt.Sprintf("echo \"%s\" >> %s", line, SSHPROXYCONFIG)}, nil, nil)
48-
if err != nil {
49-
log.Fatal(err)
50-
}
51-
}
45+
runRootCommand(fmt.Sprintf("echo \"%s\" >> %s", line, SSHPROXYCONFIG))
5246
}
5347

5448
func removeLineSSHProxyConf(line string) {
55-
ctx := context.Background()
5649
line = strings.ReplaceAll(line, "/", "\\/")
57-
for _, gateway := range gateways {
58-
_, _, _, err := runCommand(ctx, "ssh", []string{fmt.Sprintf("root@%s", gateway), "--", fmt.Sprintf("sed -i 's/^%s$//' %s", line, SSHPROXYCONFIG)}, nil, nil)
59-
if err != nil {
60-
log.Fatal(err)
61-
}
62-
}
50+
runRootCommand(fmt.Sprintf("sed -i 's/^%s$//' %s", line, SSHPROXYCONFIG))
6351
}
6452

6553
func updateLineSSHProxyConf(key string, value string) {
66-
ctx := context.Background()
6754
value = strings.ReplaceAll(value, "/", "\\/")
55+
runRootCommand(fmt.Sprintf("sed -i '/%s:/s/: .*$/: %s/' %s", key, value, SSHPROXYCONFIG))
56+
}
57+
58+
func runRootCommand(cmd string) {
59+
ctx := context.Background()
6860
for _, gateway := range gateways {
69-
_, _, _, err := runCommand(ctx, "ssh", []string{fmt.Sprintf("root@%s", gateway), "--", fmt.Sprintf("sed -i '/%s:/s/: .*$/: %s/' %s", key, value, SSHPROXYCONFIG)}, nil, nil)
61+
_, _, _, err := runCommand(ctx, "ssh", []string{fmt.Sprintf("root@%s", gateway), "--", cmd}, nil, nil)
7062
if err != nil {
7163
log.Fatal(err)
7264
}
@@ -662,6 +654,33 @@ func TestForgetPersist(t *testing.T) {
662654
}
663655
}
664656

657+
func TestMissingUser(t *testing.T) {
658+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
659+
defer cancel()
660+
args, _ := prepareCommand("tmpuser@gateway1", 2022, "sleep 20")
661+
ch := make(chan *os.Process)
662+
go func() {
663+
runCommand(ctx, "ssh", args, nil, ch)
664+
}()
665+
process1 := <-ch
666+
667+
time.Sleep(time.Second)
668+
users, _ := getEtcdAllUsers()
669+
if len(users) != 1 {
670+
t.Errorf("Want 1 user, got %d", len(users))
671+
} else if users[0].Groups != "user1" {
672+
t.Errorf("Want Groups=\"user1\", got \"%s\"", users[0].Groups)
673+
}
674+
runRootCommand("userdel tmpuser")
675+
users, _ = getEtcdAllUsers()
676+
process1.Kill()
677+
if len(users) != 1 {
678+
t.Errorf("Want 1 user, got %d", len(users))
679+
} else if users[0].Groups != "" {
680+
t.Errorf("Want Groups=\"\", got \"%s\"", users[0].Groups)
681+
}
682+
}
683+
665684
func TestBalancedConnections(t *testing.T) {
666685
// remove old connections stored in etcd
667686
time.Sleep(4 * time.Second)

0 commit comments

Comments
 (0)