Skip to content
quarry Artifact [c7b38cd769]
Login

Artifact [c7b38cd769]

Artifact c7b38cd7694b073d92cb1d5f6b528f88b568062e3e33900ffae4d1f7837f9f98:


package main

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"reflect"
	"strings"
	"sync"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// The networked CLI is a forge door like the web: it can ask for a
// reset link to go to the owner, and it cannot set anybody's password.
func TestTheNetworkedCLIAsksForAResetAndSetsNoPassword(t *testing.T) {
	var mu sync.Mutex
	var hits []string
	var bodies []map[string]any
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		var b map[string]any
		_ = json.NewDecoder(r.Body).Decode(&b)
		mu.Lock()
		hits, bodies = append(hits, r.URL.Path), append(bodies, b)
		mu.Unlock()
		w.Header().Set("Content-Type", "application/json")
		_, _ = w.Write([]byte(`{"name":"jo","emailed":true}`))
	}))
	defer srv.Close()

	if err := accountAPICmd([]string{"passwd", "--server", srv.URL, "--as", "root", "--as-password", "pw",
		"--user", "jo", "--password", "chosen-by-root"}); err == nil {
		t.Error("FAIL: `quarry accounts passwd` still answers")
	}
	for _, h := range hits {
		if strings.Contains(h, "passwd") {
			t.Fatalf("FAIL: the CLI reached %s", h)
		}
	}

	if err := accountAPICmd([]string{"reset", "--server", srv.URL, "--as", "root", "--as-password", "pw",
		"--user", "jo"}); err != nil {
		t.Fatal(err)
	}
	if len(hits) != 1 || hits[0] != "/api/v1/accounts/reset" {
		t.Fatalf("FAIL: want one call to /api/v1/accounts/reset, got %v", hits)
	}
	if bodies[0]["name"] != "jo" || len(bodies[0]) != 1 {
		t.Errorf("FAIL: reset body = %v", bodies[0])
	}
	t.Log("ok: the CLI asks for a reset link and has no passwd")
}

// Recovering an account on the box stays possible — it is the host
// operator's act, not the forge admin's — and it is journalled.
func TestTheHostOperatorsPasswdIsJournalled(t *testing.T) {
	dir := t.TempDir()
	if err := accountCmd([]string{"add", "--data", dir, "--user", "jo", "--password", "jos-password"}); err != nil {
		t.Fatal(err)
	}
	if err := accountCmd([]string{"passwd", "--data", dir, "--user", "jo", "--password", "a-recovered-password"}); err != nil {
		t.Fatal(err)
	}
	db, err := oltp.Open(filepath.Join(dir, "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	acts, err := db.AccountActs("jo", 10)
	if err != nil {
		t.Fatal(err)
	}
	if len(acts) != 1 || acts[0].Op != "account passwd" || acts[0].Owner != "jo" || !strings.HasPrefix(acts[0].Actor, "host:") {
		t.Fatalf("FAIL: journal = %+v", acts)
	}
	if strings.Contains(acts[0].Actor+acts[0].Op+acts[0].Repo+acts[0].Owner, "a-recovered-password") {
		t.Fatal("FAIL: the password is in the journal")
	}
	t.Log("ok: the host operator's recovery is journalled")
}

// An imported fossil user has no native password but signs in with the
// one their old server held. Through the real account store, an admin
// can neither re-invite it (a link that sets its password) nor point
// its confirmed address somewhere else (#319).
func TestAnAdminCannotTakeOverAnImportedFossilUser(t *testing.T) {
	db, err := oltp.Open(filepath.Join(t.TempDir(), "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer db.Close()
	if err := db.AddAccount("root", "root-password", true); err != nil {
		t.Fatal(err)
	}
	if _, _, err := db.ImportUsers("repo", "projectcode", []oltp.ImportedUser{
		{Login: "alice", Caps: "ei", PWHash: "a-fossil-credential"},
	}); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v", Accounts: accountStore{db: db},
		Repos: api.NewRepos(map[string]*api.Repo{})}); err != nil {
		t.Fatal(err)
	}
	ctx := ops.WithUser(context.Background(), "root")
	// The op inputs are unexported; build each from JSON, as the API does.
	invoke := func(op, body string) error {
		d, ok := r.Lookup(op)
		if !ok {
			t.Fatalf("no op %s", op)
		}
		in := reflect.New(d.In)
		if err := json.Unmarshal([]byte(body), in.Interface()); err != nil {
			t.Fatal(err)
		}
		_, err := r.Invoke(ctx, op, in.Elem().Interface())
		return err
	}

	if err := invoke("account.invite", `{"name":"alice","email":"[email protected]"}`); err == nil || !strings.Contains(err.Error(), "has a password") {
		t.Error("FAIL: an imported user was re-invited, handing root a link to set its password")
	}
	if err := invoke("account.email.set", `{"name":"alice","email":"[email protected]"}`); err != nil {
		t.Fatal(err)
	}
	a, _ := db.AccountByName("alice")
	if a.Email == "[email protected]" {
		t.Fatal("FAIL: root set an imported user's confirmed address")
	}
	if a.EmailPending != "[email protected]" {
		t.Errorf("FAIL: the proposed address is not pending: %+v", a)
	}
	t.Log("ok: no re-invite, and the address is only proposed")
}