package configkit import "gitea.maximumdirect.net/eric/ConfigKit/internal/document" type secretDependency struct { target Path dependency Path } func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, final *document.Node, applications []historyApplication, projections ...*document.Node) map[string]bool { paths := make(map[string]Path) finalPaths := make(map[string]*document.Node) indexDocument(final, Path{}, finalPaths) for pointer := range finalPaths { addSecretPath(paths, pointer) } for _, application := range applications { applicationPaths := make(map[string]*document.Node) indexDocument(application.value, application.path, applicationPaths) for pointer := range applicationPaths { addSecretPath(paths, pointer) } } var dependencies []secretDependency for pointer, history := range metadata.history { addSecretPath(paths, pointer) for _, change := range history { paths[change.Path.String()] = change.Path.clone() for _, dependency := range change.Dependencies { paths[dependency.String()] = dependency.clone() dependencies = append(dependencies, secretDependency{ target: change.Path.clone(), dependency: dependency.clone(), }) } } } secret := make(map[string]bool) for _, tree := range append(projections, final) { projected := make(map[string]*document.Node) indexDocument(tree, Path{}, projected) for pointer, node := range projected { addSecretPath(paths, pointer) if node.Sensitive { secret[pointer] = true } } } for pointer, path := range paths { if root, ok := schemaSecretRoot(schema, path); ok { secret[root.String()] = true } if predicate != nil && predicate(path.clone()) { secret[pointer] = true } } for { changed := false for _, dependency := range dependencies { if secretIntersectsPath(paths, secret, dependency.dependency) && !secret[dependency.target.String()] { secret[dependency.target.String()] = true changed = true } } if !changed { return secret } } } func addSecretPath(paths map[string]Path, pointer string) { if path, err := ParsePath(pointer); err == nil { paths[pointer] = path } } func secretIntersectsPath(paths map[string]Path, secret map[string]bool, candidate Path) bool { for pointer := range secret { classified, ok := paths[pointer] if !ok { continue } if pathContains(classified, candidate) || pathContains(candidate, classified) { return true } } return false } func schemaSecretRoot(schema *schemaDescriptor, path Path) (Path, bool) { if schema == nil || schema.root == nil || !path.valid() { return Path{}, false } current := schema.root walked := Path{} for _, segment := range path.segments { walked.segments = append(walked.segments, segment) for current.kind == descriptorPointer { current = current.element } switch current.kind { case descriptorStruct: if segment.kind == pathSegmentIndex { return Path{}, false } position, ok := current.fieldByName[segment.key] var field *fieldDescriptor if ok { field = ¤t.fields[position] } else { field = current.inlineMap } if field == nil { return Path{}, false } if field.secret { return walked.clone(), true } if field.inlineMap { current = field.descriptor.element } else { current = field.descriptor } case descriptorMap: if segment.kind == pathSegmentIndex { return Path{}, false } current = current.element case descriptorSlice, descriptorArray: if _, ok := schemaIndex(segment); !ok { return Path{}, false } current = current.element case descriptorDynamic: return Path{}, false default: return Path{}, false } } return Path{}, false } // Dynamic type replacement can change classification even when projected text // is equal. Validators must preserve this inspection metadata as well as values. func projectedSensitivity(root *document.Node) map[string]Path { result := make(map[string]Path) var visit func(*document.Node, Path) visit = func(node *document.Node, path Path) { if node == nil { return } if node.Sensitive { result[path.String()] = path.clone() } for _, entry := range node.Mapping { visit(entry.Value, path.Key(entry.Key)) } for i, child := range node.Sequence { visit(child, path.Index(i)) } } visit(root, Path{}) return result }