- docs/: shiroa book with three example categories (thermo, security, homelab); pages declare access via '#let access-groups = (...)' - cmd/parser: scans docs/ and generates acl.json (public by default, skips book.typ/template.typ/partials) - internal/acl: loads acl.json, normalizes URLs to page keys so a page's renderer data (*.multi.sir.in) inherits its protection; fail closed on malformed input - internal/oidc: authorization-code flow client, verified ID tokens, groups claim from Authentik - internal/middleware: in-memory sessions, login/logout/callback handlers, CSRF states, access gate + security headers - cmd/server + embed.go: single binary embedding dist/ and acl.json; /searchindex.json blocked; graceful shutdown - Justfile (build/run/parse/check/clean/preview-docs), Gitea Actions workflow (shiroa v0.4.0-rc2), rootless Dockerfile, README - legacy docs moved to legacy_docs/; .idea untracked
68 lines
2.1 KiB
Go
68 lines
2.1 KiB
Go
package acl
|
|
|
|
import "testing"
|
|
|
|
func TestPageKey(t *testing.T) {
|
|
cases := []struct{ in, want string }{
|
|
{"/", "/index"},
|
|
{"/index.html", "/index"},
|
|
{"/thermo/fluids.html", "/thermo/fluids"},
|
|
// The renderer fetches content from theme variants; they must map
|
|
// to the same page or the ACL would be bypassable.
|
|
{"/thermo/fluids.light.multi.sir.in", "/thermo/fluids"},
|
|
{"/thermo/fluids.ayu.multi.sir.in", "/thermo/fluids"},
|
|
// Directory requests resolve to the directory index page.
|
|
{"/thermo/", "/thermo/index"},
|
|
{"/thermo", "/thermo"},
|
|
// Path traversal is neutralized before matching.
|
|
{"/thermo/../security/ssh.html", "/security/ssh"},
|
|
}
|
|
for _, c := range cases {
|
|
if got, err := PageKey(c.in); err != nil {
|
|
t.Errorf("PageKey(%q) error: %v", c.in, err)
|
|
} else if got != c.want {
|
|
t.Errorf("PageKey(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
if _, err := PageKey("relative.html"); err == nil {
|
|
t.Error("PageKey must reject relative paths")
|
|
}
|
|
}
|
|
|
|
func TestLoadAndCheck(t *testing.T) {
|
|
// Keys in acl.json are final page URLs; Load normalizes them.
|
|
data := []byte(`{
|
|
"/thermo/fluids.html": ["family", "admin"],
|
|
"/index.html": ["admin"],
|
|
"/public.html": ["family"]
|
|
}`)
|
|
a, err := Load(data)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if got, _ := a.GroupsFor("/thermo/fluids"); len(got) != 2 {
|
|
t.Errorf("expected 2 groups for /thermo/fluids, got %v", got)
|
|
}
|
|
if _, protected := a.GroupsFor("/thermo/fluids"); !protected {
|
|
t.Error("/thermo/fluids should be protected")
|
|
}
|
|
if _, protected := a.GroupsFor("/thermo/heat-pumps"); protected {
|
|
t.Error("/thermo/heat-pumps should be public")
|
|
}
|
|
if !Allows([]string{"family"}, []string{"family", "admin"}) {
|
|
t.Error("family should be allowed")
|
|
}
|
|
if Allows([]string{"guest"}, []string{"family", "admin"}) {
|
|
t.Error("guest should be denied")
|
|
}
|
|
if Allows(nil, []string{"admin"}) {
|
|
t.Error("no groups should never be allowed on a protected page")
|
|
}
|
|
}
|
|
|
|
func TestLoadMalformed(t *testing.T) {
|
|
if _, err := Load([]byte("not json")); err == nil {
|
|
t.Fatal("malformed acl.json must be an error (fail closed)")
|
|
}
|
|
}
|