| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- //go:build !windows
- package localapi
- import (
- "context"
- "net/http"
- "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"
- )
- // 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)
- assert.Nil(t, acc.Group)
- acc, err = load("local_api:\n control:\n group: " + name + "\n")
- require.NoError(t, err)
- assert.Equal(t, os.FileMode(0660), acc.Mode)
- require.NotNil(t, acc.Group)
- 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, and a reload that takes the group away tightens the socket in place, without
- // moving it.
- func TestControlSocketIsHandedToTheGroup(t *testing.T) {
- 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)
- }
- // TestSocketIsHandedToTheGroup: the socket is created with the group's gid and the
- // mode, and a changed group or mode on a reload is applied to the socket in place.
- func TestSocketIsHandedToTheGroup(t *testing.T) {
- name, gid := ownGroup(t)
- group := &Group{Name: name, ID: gid}
- want, err := strconv.ParseUint(gid, 10, 32)
- require.NoError(t, err)
- s, path := serveInfo(t, infoRegistry(), 0600)
- require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0660, Group: group}))
- 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))
- status, _ := test.HTTPGet(t, path, "/v1/whoami")
- assert.Equal(t, http.StatusOK, status, "the same listener is still serving")
- // The mode is the operator's to widen further.
- require.NoError(t, s.Rebind(context.Background(), path, Access{Mode: 0664, Group: group}))
- fi, err = os.Lstat(path)
- require.NoError(t, err)
- assert.Equal(t, os.FileMode(0664), fi.Mode().Perm())
- assert.Equal(t, path, s.Path())
- }
|