github.com/argoproj/argo-cd/v3@v3.2.1/util/app/path/path.go (about) 1 package path 2 3 import ( 4 "errors" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" 11 "github.com/argoproj/argo-cd/v3/util/io/files" 12 "github.com/argoproj/argo-cd/v3/util/security" 13 ) 14 15 func Path(root, path string) (string, error) { 16 if filepath.IsAbs(path) { 17 return "", fmt.Errorf("%s: app path is absolute", path) 18 } 19 appPath := filepath.Join(root, path) 20 if !strings.HasPrefix(appPath, filepath.Clean(root)) { 21 return "", fmt.Errorf("%s: app path outside root", path) 22 } 23 info, err := os.Stat(appPath) 24 if os.IsNotExist(err) { 25 return "", fmt.Errorf("%s: app path does not exist", path) 26 } 27 if err != nil { 28 return "", err 29 } 30 if !info.IsDir() { 31 return "", fmt.Errorf("%s: app path is not a directory", path) 32 } 33 return appPath, nil 34 } 35 36 type OutOfBoundsSymlinkError struct { 37 File string 38 Err error 39 } 40 41 func (e *OutOfBoundsSymlinkError) Error() string { 42 return "out of bounds symlink found" 43 } 44 45 // CheckOutOfBoundsSymlinks determines if basePath contains any symlinks that 46 // are absolute or point to a path outside of the basePath. If found, an 47 // OutOfBoundsSymlinkError is returned. 48 func CheckOutOfBoundsSymlinks(basePath string) error { 49 absBasePath, err := filepath.Abs(basePath) 50 if err != nil { 51 return fmt.Errorf("failed to get absolute path: %w", err) 52 } 53 return filepath.Walk(absBasePath, func(path string, info os.FileInfo, err error) error { 54 if err != nil { 55 // Ignore "no such file or directory" errors than can happen with 56 // temporary files such as .git/*.lock 57 if errors.Is(err, os.ErrNotExist) { 58 return nil 59 } 60 return fmt.Errorf("failed to walk for symlinks in %s: %w", absBasePath, err) 61 } 62 if files.IsSymlink(info) { 63 // We don't use filepath.EvalSymlinks because it fails without returning a path 64 // if the target doesn't exist. 65 linkTarget, err := os.Readlink(path) 66 if err != nil { 67 return fmt.Errorf("failed to read link %s: %w", path, err) 68 } 69 // get the path of the symlink relative to basePath, used for error description 70 linkRelPath, err := filepath.Rel(absBasePath, path) 71 if err != nil { 72 return fmt.Errorf("failed to get relative path for symlink: %w", err) 73 } 74 // deny all absolute symlinks 75 if filepath.IsAbs(linkTarget) { 76 return &OutOfBoundsSymlinkError{File: linkRelPath} 77 } 78 // get the parent directory of the symlink 79 currentDir := filepath.Dir(path) 80 81 // walk each part of the symlink target to make sure it never leaves basePath 82 parts := strings.Split(linkTarget, string(os.PathSeparator)) 83 for _, part := range parts { 84 newDir := filepath.Join(currentDir, part) 85 rel, err := filepath.Rel(absBasePath, newDir) 86 if err != nil { 87 return fmt.Errorf("failed to get relative path for symlink target: %w", err) 88 } 89 if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) { 90 // return an error so we don't keep traversing the tree 91 return &OutOfBoundsSymlinkError{File: linkRelPath} 92 } 93 currentDir = newDir 94 } 95 } 96 return nil 97 }) 98 } 99 100 // GetAppRefreshPaths returns the list of paths that should trigger a refresh for an application 101 func GetAppRefreshPaths(app *v1alpha1.Application) []string { 102 var paths []string 103 if val, ok := app.Annotations[v1alpha1.AnnotationKeyManifestGeneratePaths]; ok && val != "" { 104 for _, item := range strings.Split(val, ";") { 105 if item == "" { 106 continue 107 } 108 if filepath.IsAbs(item) { 109 paths = append(paths, item[1:]) 110 } else { 111 for _, source := range app.Spec.GetSources() { 112 paths = append(paths, filepath.Clean(filepath.Join(source.Path, item))) 113 } 114 } 115 } 116 } 117 return paths 118 } 119 120 // AppFilesHaveChanged returns true if any of the changed files are under the given refresh paths 121 // If refreshPaths or changedFiles are empty, it will always return true 122 func AppFilesHaveChanged(refreshPaths []string, changedFiles []string) bool { 123 // an empty slice of changed files means that the payload didn't include a list 124 // of changed files and we have to assume that a refresh is required 125 if len(changedFiles) == 0 { 126 return true 127 } 128 129 if len(refreshPaths) == 0 { 130 // Apps without a given refreshed paths always be refreshed, regardless of changed files 131 // this is the "default" behavior 132 return true 133 } 134 135 // At last one changed file must be under refresh path 136 for _, f := range changedFiles { 137 f = ensureAbsPath(f) 138 for _, item := range refreshPaths { 139 item = ensureAbsPath(item) 140 if f == item { 141 return true 142 } else if _, err := security.EnforceToCurrentRoot(item, f); err == nil { 143 return true 144 } else if matched, err := filepath.Match(item, f); err == nil && matched { 145 return true 146 } 147 } 148 } 149 150 return false 151 } 152 153 func ensureAbsPath(input string) string { 154 if !filepath.IsAbs(input) { 155 return string(filepath.Separator) + input 156 } 157 return input 158 }