package auth import ( "os" "path/filepath" "testing" "time" "send/internal/config" ) func defaults() *config.Config { return &config.Config{ MaxSize: 2 << 30, MaxExpiry: 72 * time.Hour, DefaultExpiry: 72 * time.Hour, } } func newFile(t *testing.T) *File { t.Helper() f, err := Load(filepath.Join(t.TempDir(), "tokens.json")) if err != nil { t.Fatal(err) } return f } func TestMissingFileIsNotAnError(t *testing.T) { f := newFile(t) if len(f.List()) != 0 { t.Error("a missing token file produced tokens") } if f.Lookup("anything") != nil { t.Error("a missing token file authenticated something") } } func TestAddLookupRemove(t *testing.T) { f := newFile(t) secret := "0123456789abcdef0123456789abcdef" if err := f.Add(&Token{Name: "friend", Hash: HashSecret(secret), AllowVanity: true}); err != nil { t.Fatal(err) } tok := f.Lookup(secret) if tok == nil || tok.Name != "friend" { t.Fatalf("Lookup(secret) = %v", tok) } if f.Lookup("wrong") != nil || f.Lookup("") != nil { t.Error("an unknown secret authenticated") } // A second token with the same name is refused. if err := f.Add(&Token{Name: "friend", Hash: HashSecret("other")}); err != ErrExists { t.Errorf("duplicate name => %v, want ErrExists", err) } if err := f.Remove("friend"); err != nil { t.Fatal(err) } if f.Lookup(secret) != nil { t.Error("a removed token still authenticates") } if err := f.Remove("friend"); err != ErrNotFound { t.Errorf("removing twice => %v, want ErrNotFound", err) } } // The token file holds credential material, so it is the one thing in the data // directory that must stay owner-only. func TestFilePermissions(t *testing.T) { f := newFile(t) if err := f.Add(&Token{Name: "a", Hash: HashSecret("s")}); err != nil { t.Fatal(err) } info, err := os.Stat(f.Path()) if err != nil { t.Fatal(err) } if perm := info.Mode().Perm(); perm != 0o600 { t.Errorf("token file mode = %#o, want 0600", perm) } // A file loosened by hand must be refused rather than silently used. if err := os.Chmod(f.Path(), 0o644); err != nil { t.Fatal(err) } if _, err := Load(f.Path()); err == nil { t.Error("a world-readable token file was accepted") } } func TestLimitsInheritDefaults(t *testing.T) { c := defaults() // A token with nothing set behaves like the anonymous tier, except that it // has a name and may claim vanity names. bare := &Token{Name: "bare", Hash: HashSecret("bare"), AllowVanity: true} got := bare.Limits(c) want := Anonymous(c) want.Name, want.AllowVanity = "bare", true if got != want { t.Errorf("bare token limits = %+v, want %+v", got, want) } // Overrides win, including "unlimited". size, expiry := "8GiB", "never" rich := &Token{Name: "rich", Hash: HashSecret("rich"), MaxSize: &size, MaxExpiry: &expiry} if err := rich.resolve(); err != nil { t.Fatal(err) } l := rich.Limits(c) if l.MaxSize != 8<<30 { t.Errorf("MaxSize = %d, want 8GiB", l.MaxSize) } if l.MaxExpiry != config.Unlimited { t.Errorf("MaxExpiry = %s, want unlimited", l.MaxExpiry) } // The inherited 3d default is still fine under an unlimited maximum. if l.DefaultExpiry != c.DefaultExpiry { t.Errorf("DefaultExpiry = %s, want the inherited %s", l.DefaultExpiry, c.DefaultExpiry) } } func TestDefaultExpiryIsClampedToTheMaximum(t *testing.T) { c := defaults() short := "1h" // A token that narrows its maximum below the inherited default must not // end up handing out the longer inherited lifetime. tok := &Token{Name: "short", Hash: HashSecret("short"), MaxExpiry: &short} if err := tok.resolve(); err != nil { t.Fatal(err) } if l := tok.Limits(c); l.DefaultExpiry != time.Hour { t.Errorf("DefaultExpiry = %s, want it clamped to 1h", l.DefaultExpiry) } } func TestMalformedTokenFileIsRejected(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "tokens.json") for _, body := range []string{ `[{"name":"a","hash":"not-hex"}]`, `[{"name":"","hash":"` + HashSecret("s") + `"}]`, `[{"name":"a","hash":"` + HashSecret("s") + `","max_size":"lots"}]`, `[{"name":"a","hash":"` + HashSecret("s") + `","max_expiry":"soon"}]`, `[{"name":"a","hash":"` + HashSecret("1") + `"},{"name":"a","hash":"` + HashSecret("2") + `"}]`, `not json`, } { if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } if _, err := Load(path); err == nil { t.Errorf("accepted a malformed token file: %s", body) } } } func TestReloadPicksUpChanges(t *testing.T) { f := newFile(t) secret := "aaaa" if err := f.Add(&Token{Name: "a", Hash: HashSecret(secret)}); err != nil { t.Fatal(err) } // Simulate an edit by another process. body := `[{"name":"b","hash":"` + HashSecret("bbbb") + `","allow_vanity":true}]` if err := os.WriteFile(f.Path(), []byte(body), 0o600); err != nil { t.Fatal(err) } // Ensure the mtime actually differs on filesystems with coarse timestamps. future := time.Now().Add(time.Second) os.Chtimes(f.Path(), future, future) if err := f.MaybeReload(); err != nil { t.Fatal(err) } if f.Lookup(secret) != nil { t.Error("a removed token still authenticates after a reload") } if tok := f.Lookup("bbbb"); tok == nil || !tok.AllowVanity { t.Error("the newly written token was not picked up") } }