Skip to content
quarry Artifact [7c9a941509]
Login

Artifact [7c9a941509]

Artifact 7c9a94150956bb1bc548dadd847e0f6dcbe4e43c124b6917038203e1d5fecbcd:


package api

import (
	"context"
	"errors"
	"path/filepath"
	"testing"

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

// An op that asks for the caller's letters itself, past the router's
// check, gets the token's letters when the request came in on a token
// (RFC 0021). landing.accept's gate for a repository other than the one
// the request named is the representative: the router only resolved the
// named repository, so this check is the whole of the gate there.
func TestAnOpsOwnLetterCheckHonoursTheTokensCeiling(t *testing.T) {
	d := Deps{CapsFor: func(string, string) CapSet { return oltp.CapSet("oi") },
		TrustedAuthor: func(string, string) bool { return true }}
	jan := ops.WithUser(context.Background(), "jan")
	if !d.mayAcceptIn(jan, "jan", "elsewhere") {
		t.Fatalf("FAIL: the account's own i does not accept")
	}
	readOnly := ops.WithToken(jan, ops.TokenAuth{ID: "T1", Caps: func(string) ops.Caps { return oltp.CapSet("o") }})
	if d.mayAcceptIn(readOnly, "jan", "elsewhere") {
		t.Errorf("FAIL: a token ceilinged to o accepted a landing")
	} else {
		t.Log("ok: the token's ceiling binds the op's own check")
	}
	// Another account's letters are still that account's.
	if !d.mayAcceptIn(readOnly, "kim", "elsewhere") {
		t.Errorf("FAIL: the caller's token narrowed somebody else's letters")
	}
	if d.trustedAuthor(readOnly, "jan", "elsewhere") {
		t.Errorf("FAIL: a token without 4 or 5 is a trusted forum author")
	}
}

// narrowToken is a context for who on a token ceilinged to r, with no
// repository list, so that the forge-scope refusal for limited tokens
// is not what answers.
func narrowToken(who string) context.Context {
	return ops.WithToken(ops.WithUser(context.Background(), who),
		ops.TokenAuth{ID: "T1", Caps: func(string) ops.Caps { return oltp.CapSet("r") }})
}

// A token cannot manage the account's credentials: minting would hand
// it the account's full letters everywhere, and revoking would let it
// end its siblings. A person on a password or session is unaffected.
func TestATokenCannotManageCredentials(t *testing.T) {
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	if err := db.AddAccount("jan", "pw-jan-12345", false); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v", Repos: testRepos(wikiIndex(t)), SyncTokens: db}); err != nil {
		t.Fatal(err)
	}
	// Ceilinged but not limited to repositories, so the forge-scope
	// refusal for limited tokens (ops.refuseLimited) is not what answers.
	ctx := ops.WithToken(ops.WithUser(context.Background(), "jan"),
		ops.TokenAuth{ID: "T1", Caps: func(string) ops.Caps { return oltp.CapSet("r") }})
	for _, op := range []struct {
		name string
		in   any
	}{
		{"me.sync.token", struct{}{}},
		{"me.sync.token.revoke", meSyncTokenRevokeIn{}},
		{"me.sync.token.list", struct{}{}},
		{"me.passwd", mePasswdIn{}},
		{"session.list", sessionListIn{}},
		{"session.end", sessionEndIn{}},
		{"me.email.set", meEmailSetIn{}},
		{"me.email.clear", meEmailClearIn{}},
		{"me.email.confirm", meEmailConfirmIn{}},
		{"me.prefs", mePrefsIn{}},
		{"me.prefs.set", mePrefsSetIn{}},
		{"me.push.key", mePushKeyIn{}},
		{"me.push.test", mePushTestIn{}},
		{"me.push.subscribe", mePushIn{}},
		{"me.push.unsubscribe", mePushDeleteIn{}},
	} {
		if _, err := r.Invoke(ctx, op.name, op.in); !errors.Is(err, ops.ErrForbidden) {
			t.Errorf("FAIL: %s by a token: err = %v, want ErrForbidden", op.name, err)
		}
	}
	if toks, _ := db.Tokens("jan"); len(toks) != 0 {
		t.Errorf("FAIL: a token minted %d credentials", len(toks))
	}
	// The person, signed in, still mints and revokes.
	person := ops.WithUser(context.Background(), "jan")
	if _, err := r.Invoke(person, "me.sync.token", struct{}{}); err != nil {
		t.Errorf("FAIL: a signed-in person could not mint: %v", err)
	}
	if _, err := r.Invoke(person, "me.sync.token.revoke", meSyncTokenRevokeIn{}); err != nil {
		t.Errorf("FAIL: a signed-in person could not revoke: %v", err)
	} else {
		t.Log("ok: credential ops refuse tokens and serve the person")
	}
}

// The site-admin flag does not flow through a token: a site admin's
// token is refused by account.agent on another account and by a deed
// act on a repository somebody else holds.
func TestASiteAdminsTokenReachesNoOperatorPath(t *testing.T) {
	r, _ := agentRig(t) // dmestas is the site admin
	if _, err := r.Invoke(narrowToken("dmestas"), "account.agent",
		accountAgentIn{Name: "jva", Operator: "dmestas"}); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a site admin's token marked another account: %v", err)
	} else {
		t.Log("ok: account.agent refuses a site admin's token")
	}
	if _, err := r.Invoke(ops.WithUser(context.Background(), "dmestas"), "account.agent",
		accountAgentIn{Name: "jva", Operator: "dmestas"}); err != nil {
		t.Errorf("FAIL: the site admin signed in could not: %v", err)
	}
	d := Deps{Owners: deedOwners{owner: map[string]string{"held": "ada"}}, Accounts: deedAccounts{admin: "root"}}
	if err := d.refuseUnlessTheirs(narrowToken("root"), "held", "destroy"); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a site admin's token passed the deed gate: %v", err)
	} else {
		t.Log("ok: the deed gate refuses a site admin's token")
	}
}