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 jan on a token cut to r on one
// repository: the shape every credential op must refuse.
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") }, Limited: true})
}
// 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)
}
ctx := narrowToken("jan")
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{}},
} {
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")
}
}