internal/sshd/revoke_test.go

e2a32d5f8d59e4213571c602bd9009b6c8fa86ed
gitbay/internal/sshd/revoke_test.go history · blame · raw

112 lines · 3244 bytes

  1package sshd
  2
  3import (
  4	"bytes"
  5	"errors"
  6	"strings"
  7	"testing"
  8	"time"
  9
 10	"golang.org/x/crypto/ssh"
 11)
 12
 13// execStatus runs cmd on a new session and returns its exit status and
 14// stderr; -1 when the session could not run.
 15func execStatus(client *ssh.Client, cmd string) (int, string) {
 16	sess, err := client.NewSession()
 17	if err != nil {
 18		return -1, err.Error()
 19	}
 20	defer sess.Close()
 21	var stderr bytes.Buffer
 22	sess.Stderr = &stderr
 23	err = sess.Run(cmd)
 24	var exit *ssh.ExitError
 25	switch {
 26	case err == nil:
 27		return 0, stderr.String()
 28	case errors.As(err, &exit):
 29		return exit.ExitStatus(), stderr.String()
 30	}
 31	return -1, err.Error()
 32}
 33
 34// waitClosed fails unless the server closes the client's connection
 35// within five seconds.
 36func waitClosed(t *testing.T, client *ssh.Client) {
 37	t.Helper()
 38	done := make(chan struct{})
 39	go func() { client.Wait(); close(done) }()
 40	select {
 41	case <-done:
 42	case <-time.After(5 * time.Second):
 43		t.Fatal("the connection stayed open")
 44	}
 45}
 46
 47// Each exec reads the key again. The rows change behind the store's
 48// back here, so no revocation is announced and the connection stays up:
 49// what refuses the command is the per-exec check alone.
 50func TestExecRevalidatesKey(t *testing.T) {
 51	ts := newTestServer(t)
 52	if code, errOut := execStatus(ts.client, "whoami"); code != 0 {
 53		t.Fatalf("whoami: %d %s", code, errOut)
 54	}
 55	if _, err := ts.st.DB.Exec("UPDATE ssh_keys SET scope = 'git' WHERE id = ?", ts.keyID); err != nil {
 56		t.Fatal(err)
 57	}
 58	if code, errOut := execStatus(ts.client, "whoami"); code != 4 || !strings.Contains(errOut, "does not allow control commands") {
 59		t.Fatalf("whoami after re-scope: %d %q", code, errOut)
 60	}
 61	if _, err := ts.st.DB.Exec("DELETE FROM ssh_keys WHERE id = ?", ts.keyID); err != nil {
 62		t.Fatal(err)
 63	}
 64	if code, errOut := execStatus(ts.client, "whoami"); code != 4 || !strings.Contains(errOut, "no longer registered") {
 65		t.Fatalf("whoami after delete: %d %q", code, errOut)
 66	}
 67}
 68
 69// Removing the key cuts the connection, ending a command running on it.
 70func TestRemoveKeyCutsConnection(t *testing.T) {
 71	ts := newTestServer(t)
 72	withBuild(t, ts)
 73	var stderr bytes.Buffer
 74	sess := startFollow(t, ts.client, &stderr)
 75	if err := ts.st.RemoveSSHKey(ts.uid, ts.fp); err != nil {
 76		t.Fatal(err)
 77	}
 78	waited := make(chan error, 1)
 79	go func() { waited <- sess.Wait() }()
 80	select {
 81	case err := <-waited:
 82		if err == nil {
 83			t.Fatal("the follow exited cleanly after its key was removed")
 84		}
 85	case <-time.After(5 * time.Second):
 86		t.Fatal("the follow outlived its key")
 87	}
 88	waitClosed(t, ts.client)
 89}
 90
 91func TestDisableCutsConnection(t *testing.T) {
 92	ts := newTestServer(t)
 93	if err := ts.st.SetUserDisabled(ts.uid, true); err != nil {
 94		t.Fatal(err)
 95	}
 96	waitClosed(t, ts.client)
 97}
 98
 99// A revocation made by another process is not announced here; the
100// sweep finds it. A live key survives the sweep.
101func TestSweepCutsOutOfProcessRevocation(t *testing.T) {
102	ts := newTestServer(t)
103	ts.srv.sweepOnce()
104	if code, errOut := execStatus(ts.client, "whoami"); code != 0 {
105		t.Fatalf("the sweep cut a live key: %d %s", code, errOut)
106	}
107	if _, err := ts.st.DB.Exec("UPDATE users SET disabled = 1 WHERE id = ?", ts.uid); err != nil {
108		t.Fatal(err)
109	}
110	ts.srv.sweepOnce()
111	waitClosed(t, ts.client)
112}