Replaces the super-admin-only /.admin/ surface with a public-by-default /.profile/ page that layers admin tools server-side based on the caller's effective access: - Universal (everyone, anonymous included): identity card, effective access summary, theme picker, localStorage utilities (export / import / clear, landing-presets viewer). - Subtree admins additionally see: editable .zddc files list (linking to the existing form-based editor) and a "Create new project folder" form. - Super-admins additionally see: server config, log viewer, whoami headers (the old /.admin/ JSON endpoints, repointed under /.profile/). Project creation is gated on CanEditZddc(newDir) — the same strict- ancestor rule that already governs .zddc writes — so no new authority concept is introduced. ValidateProjectName mirrors the existing reserved-prefix policy (no leading '.' or '_', no path separators). /.admin/* is hard-cut: no redirect shim. Old URLs fall through to the existing dot-prefix guard and 404. Custom CSS file rename: prefer <root>/.profile.css, fall back to legacy <root>/.admin.css. Per-resource 404 leakage gates preserved on whoami / config / logs / zddc / projects so non-admin callers cannot detect the existence of admin-only sub-resources. Tree-wide gofmt -w applied as a side-effect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
package zddc
|
|
|
|
import "path/filepath"
|
|
|
|
// IsAdmin reports whether email is listed in the admins entry of the ROOT
|
|
// .zddc file (<fsRoot>/.zddc). Subdirectory .zddc files' admins keys are
|
|
// deliberately ignored by this function — it gates the server-wide debug
|
|
// admin role (/.profile/{whoami,config,logs}) which only the bootstrap
|
|
// super-admin should reach.
|
|
//
|
|
// Subtree-scoped admin authority (the "fiefdom" model) is checked via
|
|
// IsSubtreeAdmin / CanEditZddc instead.
|
|
//
|
|
// Patterns use the same glob syntax as acl.allow / acl.deny (see
|
|
// MatchesPattern). Returns false if the root file does not exist, has an
|
|
// empty Admins list, or no entry matches. An empty email never matches.
|
|
func IsAdmin(fsRoot, email string) bool {
|
|
if email == "" {
|
|
return false
|
|
}
|
|
zf, err := ParseFile(filepath.Join(fsRoot, ".zddc"))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, pattern := range zf.Admins {
|
|
if MatchesPattern(pattern, email) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// IsSubtreeAdmin reports whether email administers the subtree rooted at
|
|
// dirPath. Authority cascades: a match against any Admins entry on the chain
|
|
// from fsRoot down to dirPath (inclusive) confers admin rights for dirPath.
|
|
//
|
|
// This is the read-side check — "can email *see* admin tools for this
|
|
// subtree?". For write authority over a specific .zddc file, use
|
|
// CanEditZddc, which adds the strict-ancestor rule that prevents
|
|
// self-elevation.
|
|
func IsSubtreeAdmin(fsRoot, dirPath, email string) bool {
|
|
if email == "" {
|
|
return false
|
|
}
|
|
chain, err := EffectivePolicy(fsRoot, dirPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
for _, level := range chain.Levels {
|
|
for _, pattern := range level.Admins {
|
|
if MatchesPattern(pattern, email) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// CanEditZddc reports whether email may write or delete <dirPath>/.zddc.
|
|
//
|
|
// The strict-ancestor rule: authority must come from a .zddc file STRICTLY
|
|
// ABOVE dirPath. An admin granted in <dirPath>/.zddc cannot edit that same
|
|
// file (which is what grants their own authority) — they can only edit
|
|
// .zddc files in deeper subtrees. This prevents a subtree admin from
|
|
// adding peers at their own level, removing their delegator, or otherwise
|
|
// elevating themselves.
|
|
//
|
|
// The root file <fsRoot>/.zddc is the bootstrap exception: it has no
|
|
// strict ancestor, so it is governed by its own Admins list (the same
|
|
// allowlist IsAdmin checks). The very first super-admin is created by
|
|
// hand-editing this file at server install time.
|
|
func CanEditZddc(fsRoot, dirPath, email string) bool {
|
|
if email == "" {
|
|
return false
|
|
}
|
|
fsRoot = filepath.Clean(fsRoot)
|
|
dirPath = filepath.Clean(dirPath)
|
|
|
|
chain, err := EffectivePolicy(fsRoot, dirPath)
|
|
if err != nil || len(chain.Levels) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Bootstrap: the root file is governed by its own Admins.
|
|
if dirPath == fsRoot {
|
|
for _, pattern := range chain.Levels[0].Admins {
|
|
if MatchesPattern(pattern, email) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Strict-ancestor: scan all levels EXCEPT the deepest, which IS dirPath.
|
|
// EffectivePolicy returns levels ordered root (index 0) → leaf (last).
|
|
for i := 0; i < len(chain.Levels)-1; i++ {
|
|
for _, pattern := range chain.Levels[i].Admins {
|
|
if MatchesPattern(pattern, email) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|