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 }) }