package server import ( "context" "errors" "net" "net/http" "net/http/httptest" "os" "os/user" "path/filepath" "strings" "testing" "jarl/internal/api" "jarl/internal/client" ) // serveTestSocket serves e's API on a unix socket, as jarld does. func serveTestSocket(t *testing.T, e *apiEnv) string { t.Helper() path := filepath.Join(t.TempDir(), "jarl.sock") ln, err := listenSocket(path) if err != nil { t.Fatal(err) } srv := &http.Server{Handler: e.srv.Handler(), ConnContext: socketConnContext} go srv.Serve(ln) t.Cleanup(func() { srv.Close() }) return path } func TestSocketTrustedUser(t *testing.T) { e := newAPIEnv(t) path := serveTestSocket(t, e) if fi, err := os.Stat(path); err != nil || fi.Mode().Perm() != 0o600 { t.Fatalf("socket mode: %v %v", fi.Mode(), err) } // The test runs as the same user as the server: an administrator // without a token. c, err := client.New(client.Options{Server: "unix://" + path}) if err != nil { t.Fatal(err) } var who api.Whoami if err := c.Get(context.Background(), "/auth/whoami", nil, &who); err != nil { t.Fatal(err) } me, _ := user.Current() if !who.Admin || who.Method != "socket" || who.User.Username != me.Username || who.Token != nil { t.Errorf("whoami over the socket = %+v", who) } // It can change things, and the history and audit log name the // system user. if err := c.Do(context.Background(), "POST", "/zones", nil, map[string]any{"name": "sock.example", "records": []map[string]any{{"name": "ns1", "type": "A", "data": "192.0.2.1"}}}, nil); err != nil { t.Fatal(err) } var events []map[string]any c.Get(context.Background(), "/audit", nil, &events) found := false for _, ev := range events { if ev["action"] == "changeset.apply" && ev["username"] == me.Username && ev["auth"] == "socket" && ev["ip"] == "local" { found = true } } if !found { t.Errorf("no audit event for the socket user: %v", events) } // A token sent over the socket is used as such. c.Token = e.token if err := c.Get(context.Background(), "/auth/whoami", nil, &who); err != nil || who.Method != "token" { t.Errorf("token over the socket: %+v %v", who, err) } // The socket's user isn't a Jarl user, so it has no tokens of its own. c.Token = "" err = c.Do(context.Background(), "POST", "/me/tokens", nil, map[string]any{"name": "x"}, nil) var ae *client.Error if !errors.As(err, &ae) || ae.Status != 400 { t.Errorf("own token over the socket: %v", err) } } func TestSocketOtherUserNeedsToken(t *testing.T) { e := newAPIEnv(t) req := func(uid int, token string) *httptest.ResponseRecorder { r := httptest.NewRequest("GET", api.Prefix+"/auth/whoami", nil) r = r.WithContext(context.WithValue(r.Context(), peerKey{}, &peer{UID: uid})) if token != "" { r.Header.Set("Authorization", "Bearer "+token) } w := httptest.NewRecorder() e.srv.Handler().ServeHTTP(w, r) return w } other := os.Getuid() + 4242 if w := req(other, ""); w.Code != 401 || !strings.Contains(w.Body.String(), "only to root and jarld's own user") { t.Errorf("untrusted user without a token: %d %s", w.Code, w.Body) } if w := req(-1, ""); w.Code != 401 { t.Errorf("unknown peer without a token: %d", w.Code) } if w := req(other, e.token); w.Code != 200 { t.Errorf("untrusted user with a token: %d %s", w.Code, w.Body) } if w := req(0, ""); w.Code != 200 || !strings.Contains(w.Body.String(), `"method":"socket"`) { t.Errorf("root: %d %s", w.Code, w.Body) } // Network requests never get this treatment. r := httptest.NewRequest("GET", api.Prefix+"/auth/whoami", nil) w := httptest.NewRecorder() e.srv.Handler().ServeHTTP(w, r) if w.Code != 401 { t.Errorf("network request without a token: %d", w.Code) } } func TestListenSocketStale(t *testing.T) { path := filepath.Join(t.TempDir(), "jarl.sock") // A socket left by a process that is gone is replaced. old, err := net.Listen("unix", path) if err != nil { t.Fatal(err) } old.(*net.UnixListener).SetUnlinkOnClose(false) old.Close() ln, err := listenSocket(path) if err != nil { t.Fatalf("stale socket not replaced: %v", err) } // One that is in use isn't. if _, err := listenSocket(path); err == nil || !strings.Contains(err.Error(), "in use") { t.Errorf("second jarld on the same socket: %v", err) } ln.Close() // A file that isn't a socket is never removed. os.WriteFile(path, []byte("x"), 0o600) if _, err := listenSocket(path); err == nil { t.Error("replaced a regular file") } }