Monorepo for Tangled
Something went wrong. Try again.
3.4 kB · 108 lines
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109package sandbox
import ( "fmt" "io/fs" "os" "path/filepath" "sort" "strings" "syscall")
// ChmodRepoTree sets directory modes to 2770 (with the setgid bit) and// file modes to 0660 under root, preserving the executable bit on files// (hook scripts need it). Symlinks are skipped since their mode is not// meaningful.//// The group bits exist so the knot service (running as the git user, which// is in the git group that owns the repos) can still read and write the// repo via group permissions even though the repo's UID owner is a virtual// UID. Sandbox subprocesses drop supplementary groups so cross-owner// isolation still holds.//// The setgid bit on directories makes new files and subdirectories created// by sandbox subprocesses inherit the directory's group (the git group)// rather than the subprocess's primary group (the virtual UID). Without// it, sandbox-created files would be unreadable to the knot service.func ChmodRepoTree(root string) error { return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } if d.Type()&fs.ModeSymlink != 0 { return nil } if d.IsDir() { return os.Chmod(path, 0770|os.ModeSetgid) } info, err := d.Info() if err != nil { return err } mode := fs.FileMode(0660) if info.Mode()&0100 != 0 { mode = 0770 } return os.Chmod(path, mode) })}
// ChownRepoTree recursively chowns every entry under root to uid:gid.// Entries are processed deepest-first so a directory is only chowned after// its contents, preserving the calling process's access throughout the walk.// Call ChmodRepoTree first if you also want to tighten permissions; this// function only changes ownership.func ChownRepoTree(root string, uid int, gid int) error { type entry struct { path string depth int } var entries []entry if err := filepath.WalkDir(root, func(path string, _ fs.DirEntry, err error) error { if err != nil { return err } depth := strings.Count(path, string(filepath.Separator)) entries = append(entries, entry{path, depth}) return nil }); err != nil { return err }
sort.Slice(entries, func(i, j int) bool { return entries[i].depth > entries[j].depth })
for _, e := range entries { if err := os.Lchown(e.path, uid, gid); err != nil { return err } } return nil}
// LookupUIDForRepoPath returns the owner UID and GID of the repo directory at// repoPath. scanPath is validated as a prefix to guard against directory escape.func LookupUIDForRepoPath(scanPath, repoPath string) (uid uint32, gid uint32, err error) { if !strings.HasPrefix(repoPath, scanPath) { return 0, 0, fmt.Errorf("repo path %q is outside scan path %q", repoPath, scanPath) } var stat syscall.Stat_t if err := syscall.Stat(repoPath, &stat); err != nil { return 0, 0, err } return stat.Uid, stat.Gid, nil}
// ServiceGid returns the GID of scanPath, which is treated as the "service// group" that owns all repositories. Callers chown repo trees to// (virtualUID, ServiceGid(scanPath)) so the knot service (a member of this// group) retains read+write access via the group bits set by ChmodRepoTree.func ServiceGid(scanPath string) (uint32, error) { var stat syscall.Stat_t if err := syscall.Stat(scanPath, &stat); err != nil { return 0, fmt.Errorf("stat %s: %w", scanPath, err) } return stat.Gid, nil}