Skip to content
quarry Artifact [ec6d6487b8]
Login

Artifact [ec6d6487b8]

Artifact ec6d6487b88cd93d35f91b75726473f4559aaabefeb141473f87e65a4117afe2:


package api

import (
	"context"
	"encoding/json"
	"reflect"
	"strings"
	"testing"

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

var inviteToken = strings.Repeat("cd", 32)

type inviteAccounts struct{ *resetAccounts }

func (f inviteAccounts) InviteAccount(string, string, bool) (string, error) { return inviteToken, nil }

// #319: an invite that went to the invitee's own address is theirs.
// The admin learns that it went, and does not get a copy of the link.
func TestAnEmailedInviteIsNotHandedToTheAdmin(t *testing.T) {
	accts := inviteAccounts{&resetAccounts{fakeAccounts: newFakeAccounts()}}
	_ = accts.AddAccount("root", "root-password", true)
	mail := &textMail{}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Mail: mail, Origin: "https://forge.example"}); err != nil {
		t.Fatal(err)
	}
	ctx := ops.WithUser(context.Background(), "root")

	out, err := r.Invoke(ctx, "account.invite", accountInviteIn{Name: "bob", Email: "[email protected]"})
	if err != nil {
		t.Fatal(err)
	}
	if len(mail.sent) != 1 || !strings.Contains(mail.sent[0].Text, inviteToken) {
		t.Fatalf("FAIL: the invitee was not mailed the link: %+v", mail.sent)
	}
	wire, _ := json.Marshal(out)
	var got map[string]any
	_ = json.Unmarshal(wire, &got)
	if got["emailed"] != true {
		t.Errorf("FAIL: emailed = %v", got["emailed"])
	}
	if strings.Contains(string(wire), inviteToken) {
		t.Errorf("FAIL: the emailed invite's token came back to the admin: %s", wire)
	}

	// With no address there is nobody else to send it to: the admin
	// hands it over, and it only works while the account is unclaimed.
	out, err = r.Invoke(ctx, "account.invite", accountInviteIn{Name: "carol"})
	if err != nil {
		t.Fatal(err)
	}
	wire, _ = json.Marshal(out)
	got = nil
	_ = json.Unmarshal(wire, &got)
	if got["link"] != "https://forge.example/invite/"+inviteToken {
		t.Errorf("FAIL: an invite with no address did not return its link: %s", wire)
	}
	t.Log("ok: emailed invite withholds the link; unaddressed invite returns it")
}

// #319: the invite link is built on the forge's own configured origin,
// never on one the caller names. An admin who could choose the domain
// would send a genuine invite email whose link collects the invitee's
// new password on the admin's own site.
func TestAnInviteLinkIsNeverOnTheCallersDomain(t *testing.T) {
	const evil = "https://evil.example"
	for _, origin := range []string{"https://forge.example", ""} {
		accts := inviteAccounts{&resetAccounts{fakeAccounts: newFakeAccounts()}}
		_ = accts.AddAccount("root", "root-password", true)
		mail := &textMail{}
		r := ops.NewRegistry()
		if err := RegisterAll(r, Deps{Version: "v", Accounts: accts, Mail: mail, Origin: origin}); err != nil {
			t.Fatal(err)
		}
		ctx := ops.WithUser(context.Background(), "root")
		// Decoded the way the HTTP codec does, so a "base" the caller
		// sends reaches the op exactly as it would over the wire.
		desc, _ := r.Lookup("account.invite")
		in := desc.NewIn()
		if err := json.Unmarshal([]byte(`{"name":"bob","email":"[email protected]","base":"`+evil+`"}`), in); err != nil {
			t.Fatal(err)
		}
		out, err := r.Invoke(ctx, "account.invite", reflect.ValueOf(in).Elem().Interface())
		if err != nil {
			t.Fatalf("origin=%q: %v", origin, err)
		}
		wire, _ := json.Marshal(out)
		if strings.Contains(string(wire), evil) {
			t.Errorf("FAIL (origin=%q): the returned link is on the caller's domain: %s", origin, wire)
		}
		for _, m := range mail.sent {
			if strings.Contains(m.Text, evil) {
				t.Errorf("FAIL (origin=%q): the invite email links to the caller's domain", origin)
			}
		}
		if origin != "" && (len(mail.sent) != 1 || !strings.Contains(mail.sent[0].Text, origin+"/invite/"+inviteToken)) {
			t.Errorf("FAIL (origin=%q): the invite did not go out on the forge's origin: %+v", origin, mail.sent)
		}
		if origin == "" && len(mail.sent) != 0 {
			t.Errorf("FAIL: an invite was emailed with no origin to build its link on")
		}
	}
	t.Log("ok: invite links are on the forge's origin or not emailed")
}