Files
nebula/localapi/access_unix_test.go
T
Matt RichardsonandClaude Fable 5.1 f2a8883563 Drop the info API tests that tested the control socket's code twice
The endpoint is one type under both servers, so the info API's copies of
the rebind, failed-rebind and cancellation tests ran the control socket's
code a second time, and in-place access changes were pinned twice more.

- info API: the three endpoint tests, the mode test and the wire test's mode step go
- the control socket's group hand-over test stands for both servers
- registry: one Resolve test with both the typed and the worded assertions, in registry_test

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-21 12:26:57 -04:00

211 lines
7.0 KiB
Go

//go:build !windows
package localapi
import (
"context"
"os"
"os/user"
"strconv"
"syscall"
"testing"
"github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sys/unix"
)
// ownGroup returns a group this process may hand a socket to: its own effective group,
// by name and by gid, which is the one chown accepts from an unprivileged user.
func ownGroup(t *testing.T) (name, gid string) {
t.Helper()
gid = strconv.Itoa(os.Getegid())
g, err := user.LookupGroupId(gid)
require.NoError(t, err)
return g.Name, gid
}
func socketGroup(t *testing.T, path string) uint32 {
t.Helper()
fi, err := os.Lstat(path)
require.NoError(t, err)
st, ok := fi.Sys().(*syscall.Stat_t)
require.True(t, ok)
return uint32(st.Gid)
}
// TestLookupGroupID: a group is named as the host names it or by its gid; a gid the
// host does not list is accepted as chown would accept it, and a name it does not list
// is a mistake to report.
func TestLookupGroupID(t *testing.T) {
name, gid := ownGroup(t)
got, err := lookupGroupID(name)
require.NoError(t, err)
assert.Equal(t, gid, got)
got, err = lookupGroupID(gid)
require.NoError(t, err)
assert.Equal(t, gid, got)
got, err = lookupGroupID("4000000000")
require.NoError(t, err)
assert.Equal(t, "4000000000", got)
_, err = lookupGroupID("no-such-group-nebula-test")
require.ErrorContains(t, err, "does not exist")
}
// TestControlAccessFromConfig: no group is the owner-only socket; a group is 0660 with
// that group; a group the host does not know is a config error naming the key.
func TestControlAccessFromConfig(t *testing.T) {
name, gid := ownGroup(t)
load := func(body string) (Access, error) {
c := config.NewC(test.NewLogger())
require.NoError(t, c.LoadString(body))
return controlAccessFrom(c)
}
acc, err := load("local_api: {}\n")
require.NoError(t, err)
assert.Equal(t, ownerOnly(), acc)
acc, err = load("local_api:\n control:\n group: " + name + "\n")
require.NoError(t, err)
assert.Equal(t, os.FileMode(0660), acc.Mode)
assert.Equal(t, name, acc.Group.Name)
assert.Equal(t, gid, acc.Group.ID)
_, err = load("local_api:\n control:\n group: no-such-group-nebula-test\n")
require.ErrorContains(t, err, CtlGroupKey)
assert.ErrorContains(t, err, "does not exist")
}
// TestControlSocketIsHandedToTheGroup: the socket is created with the group's gid and
// 0660, whatever the umask, and a reload that takes the group away tightens the socket
// in place, without moving it. The endpoint is shared, so this covers the info API too.
func TestControlSocketIsHandedToTheGroup(t *testing.T) {
old := unix.Umask(0077)
defer unix.Umask(old)
name, gid := ownGroup(t)
want, err := strconv.ParseUint(gid, 10, 32)
require.NoError(t, err)
s := NewServer(test.NewLogger(), basicRegistry())
path := testSocket(t)
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: Group{Name: name, ID: gid}}))
defer s.Stop()
fi, err := os.Lstat(path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0660), fi.Mode().Perm())
assert.Equal(t, uint32(want), socketGroup(t, path))
require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
fi, err = os.Lstat(path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0600), fi.Mode().Perm())
assert.Equal(t, path, s.Path())
_, err = (&Client{Path: path}).Describe()
require.NoError(t, err, "the same listener is still serving")
}
// TestControlSocketWithAGroupTrustsTheKernel: with a group sharing the socket, the
// kernel's check on connect is the access control and the daemon's peer check stands
// aside, so a connection the peer check alone would refuse is served. Without a group
// the same connection is refused; see TestPeerCheckRefusesAnotherUser.
func TestControlSocketWithAGroupTrustsTheKernel(t *testing.T) {
if !peerCheckSupported {
t.Skip("no peer credentials on this platform")
}
name, gid := ownGroup(t)
me := currentUID()
asUID(t, me+1)
s := NewServer(test.NewLogger(), basicRegistry())
asUID(t, me)
path := testSocket(t)
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: Group{Name: name, ID: gid}}))
defer s.Stop()
_, err := (&Client{Path: path}).Describe()
require.NoError(t, err, "the socket's group admitted us; the peer check must not refuse")
// Taking the group away on a reload puts the peer check back.
require.NoError(t, s.Rebind(context.Background(), path, ownerOnly()))
_, err = (&Client{Path: path}).Describe()
require.ErrorContains(t, err, "not permitted")
}
// TestInfoAccessFromConfig: a group with no mode is 0660; a mode that gives the group
// nothing is refused, with the mode that would; an explicit mode with group bits is
// kept as given.
func TestInfoAccessFromConfig(t *testing.T) {
name, gid := ownGroup(t)
load := func(body string) (infoConfig, error) {
c := config.NewC(test.NewLogger())
require.NoError(t, c.LoadString(body))
return infoConfigFrom(c)
}
section := func(extra string) string {
return "local_api:\n info:\n enabled: true\n socket: /run/nebula/info.sock\n" + extra
}
cfg, err := load(section(" group: " + name + "\n"))
require.NoError(t, err)
assert.Equal(t, os.FileMode(0660), cfg.Access.Mode)
require.NotNil(t, cfg.Access.Group)
assert.Equal(t, gid, cfg.Access.Group.ID)
cfg, err = load(section(" group: " + name + "\n socket_mode: \"0664\"\n"))
require.NoError(t, err)
assert.Equal(t, os.FileMode(0664), cfg.Access.Mode)
_, err = load(section(" group: " + name + "\n socket_mode: \"0600\"\n"))
require.ErrorContains(t, err, "gives group "+name+" no access")
require.ErrorContains(t, err, `"0660"`)
cfg, err = load(section(""))
require.NoError(t, err)
assert.Equal(t, os.FileMode(0600), cfg.Access.Mode)
assert.Nil(t, cfg.Access.Group)
}
// TestRemovingTheGroupHandsTheSocketBack: a reload that drops the group key leaves the
// mode the operator set but returns the socket to the daemon's own group, so the old
// group's members are no longer admitted. The socket is first handed to a group this
// process belongs to other than its own, which not every host offers.
func TestRemovingTheGroupHandsTheSocketBack(t *testing.T) {
own := os.Getegid()
groups, err := os.Getgroups()
require.NoError(t, err)
other := -1
for _, g := range groups {
if g != own {
other = g
break
}
}
if other < 0 {
t.Skip("this process belongs to no group but its own")
}
gid := strconv.Itoa(other)
s := NewServer(test.NewLogger(), basicRegistry())
path := testSocket(t)
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: Group{Name: gid, ID: gid}}))
defer s.Stop()
assert.Equal(t, uint32(other), socketGroup(t, path))
require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660}))
assert.Equal(t, uint32(own), socketGroup(t, path), "the daemon's own group again")
fi, err := os.Lstat(path)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0660), fi.Mode().Perm(), "the mode is the operator's")
}