ZDDC — Zero Day Document Control. A file-naming convention plus five single-file HTML tools (archive, transmittal, classifier, mdedit, landing) and an optional Go HTTP server (zddc-server) with ACL and a virtual archive index. Self-contained, offline-capable, dependency-free. See README.md for an overview, AGENTS.md and ARCHITECTURE.md for the build/release/architecture detail, bootstrap/README.md for the two-level deployment install pattern, and zddc/README.md for the HTTP server.
98 lines
2.7 KiB
Go
98 lines
2.7 KiB
Go
package zddc
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// PolicyChain represents a chain of .zddc files from root to leaf.
|
|
type PolicyChain struct {
|
|
Levels []ZddcFile // ordered root (index 0) → leaf (last index)
|
|
HasAnyFile bool // true if at least one .zddc file exists in the chain
|
|
}
|
|
|
|
// policyCache caches effective policies keyed by dirPath.
|
|
// Values are PolicyChain.
|
|
var policyCache sync.Map
|
|
|
|
// EffectivePolicy returns the ACL policy chain for dirPath by reading
|
|
// all .zddc files from fsRoot down to dirPath.
|
|
//
|
|
// Child directory rules take precedence over parent rules.
|
|
// A deny at any level cannot be overridden by a parent allow.
|
|
func EffectivePolicy(fsRoot, dirPath string) (PolicyChain, error) {
|
|
// Normalize: ensure fsRoot and dirPath use the same separator
|
|
fsRoot = filepath.Clean(fsRoot)
|
|
dirPath = filepath.Clean(dirPath)
|
|
|
|
// Build list of directories from root to dirPath (inclusive)
|
|
var dirs []string
|
|
if !strings.HasPrefix(dirPath, fsRoot) {
|
|
// dirPath must be under fsRoot; if not, treat it as an empty chain
|
|
return PolicyChain{}, nil
|
|
}
|
|
|
|
rel, err := filepath.Rel(fsRoot, dirPath)
|
|
if err != nil {
|
|
return PolicyChain{}, err
|
|
}
|
|
|
|
// Walk from root down: root, root/a, root/a/b, ...
|
|
dirs = append(dirs, fsRoot)
|
|
if rel != "." {
|
|
parts := strings.Split(rel, string(filepath.Separator))
|
|
current := fsRoot
|
|
for _, part := range parts {
|
|
current = filepath.Join(current, part)
|
|
dirs = append(dirs, current)
|
|
}
|
|
}
|
|
|
|
// Check cache for the most specific (deepest) cached entry we can reuse
|
|
cacheKey := dirPath
|
|
if cached, ok := policyCache.Load(cacheKey); ok {
|
|
return cached.(PolicyChain), nil
|
|
}
|
|
|
|
// Build policy chain: read each .zddc file
|
|
var chain PolicyChain
|
|
for _, dir := range dirs {
|
|
zddcPath := filepath.Join(dir, ".zddc")
|
|
// Check if .zddc file exists
|
|
_, err := os.Stat(zddcPath)
|
|
if err == nil {
|
|
// File exists
|
|
chain.HasAnyFile = true
|
|
parsed, err := ParseFile(zddcPath)
|
|
if err != nil {
|
|
// Parse error — append empty file but continue
|
|
chain.Levels = append(chain.Levels, ZddcFile{})
|
|
} else {
|
|
chain.Levels = append(chain.Levels, parsed)
|
|
}
|
|
} else if os.IsNotExist(err) {
|
|
// File doesn't exist
|
|
chain.Levels = append(chain.Levels, ZddcFile{})
|
|
} else {
|
|
// Other error (permission, etc.)
|
|
chain.Levels = append(chain.Levels, ZddcFile{})
|
|
}
|
|
}
|
|
|
|
policyCache.Store(cacheKey, chain)
|
|
return chain, nil
|
|
}
|
|
|
|
// InvalidateCache removes the cached policy for dirPath and all descendants.
|
|
func InvalidateCache(dirPath string) {
|
|
dirPath = filepath.Clean(dirPath)
|
|
policyCache.Range(func(key, _ any) bool {
|
|
k := key.(string)
|
|
if k == dirPath || strings.HasPrefix(k, dirPath+string(filepath.Separator)) {
|
|
policyCache.Delete(key)
|
|
}
|
|
return true
|
|
})
|
|
}
|