| // Copyright 2018 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // Package filecheck helps tests check permissions and ownership of on-disk files. |
| package filecheck |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "path/filepath" |
| "regexp" |
| "strings" |
| "syscall" |
| |
| "chromiumos/tast/local/sysutil" |
| "chromiumos/tast/testing" |
| ) |
| |
| // Pattern matches one or more paths. |
| // It can be used to verify that matched paths have expected ownership and permissions. |
| type Pattern struct { |
| match Matcher |
| uids, gids []uint32 // allowed IDs; nil or empty to not check |
| mode *os.FileMode // mode perm bits must exactly match |
| notMode *os.FileMode // none of these perm bits may be set |
| skipChildren bool // should children (if this is a dir) be skipped? |
| errors []string // set when the pattern is invalid |
| } |
| |
| // NewPattern returns a new Pattern that verifies that paths matched by m meet the requirements specified by rs. |
| func NewPattern(m Matcher, opts ...Option) *Pattern { |
| pat := &Pattern{match: m} |
| for _, o := range opts { |
| o(pat) |
| } |
| return pat |
| } |
| |
| // modeMask contains permission-related os.FileMode bits. |
| const modeMask = os.ModePerm | os.ModeSetuid | os.ModeSetgid | os.ModeSticky |
| |
| // check inspects fi and returns a list of problems. |
| func (p *Pattern) check(fi os.FileInfo) (problems []string) { |
| contains := func(allowed []uint32, id uint32) bool { |
| for _, aid := range allowed { |
| if id == aid { |
| return true |
| } |
| } |
| return false |
| } |
| |
| if len(p.errors) > 0 { |
| problems = append(problems, p.errors...) |
| return |
| } |
| |
| st := fi.Sys().(*syscall.Stat_t) |
| if len(p.uids) > 0 { |
| if !contains(p.uids, st.Uid) { |
| problems = append(problems, fmt.Sprintf("UID %v (want %v)", st.Uid, p.uids)) |
| } |
| } |
| if len(p.gids) > 0 { |
| if !contains(p.gids, st.Gid) { |
| problems = append(problems, fmt.Sprintf("GID %v (want %v)", st.Gid, p.gids)) |
| } |
| } |
| |
| // Skip checking meaningless permissions on symbolic links. |
| if fi.Mode()&os.ModeSymlink == 0 { |
| mode := fi.Mode() & modeMask |
| if p.mode != nil && mode != *p.mode { |
| problems = append(problems, fmt.Sprintf("mode %04o (want %04o)", mode, *p.mode)) |
| } |
| if p.notMode != nil { |
| if bad := mode & *p.notMode; bad != 0 { |
| problems = append(problems, fmt.Sprintf("mode %04o (%04o disallowed)", mode, bad)) |
| } |
| } |
| } |
| |
| return problems |
| } |
| |
| func (p *Pattern) String() string { |
| var fields []string |
| if len(p.errors) > 0 { |
| fields = append(fields, fmt.Sprintf("error=%v", p.errors)) |
| } |
| if len(p.uids) > 0 { |
| fields = append(fields, fmt.Sprintf("uids=%v", p.uids)) |
| } |
| if len(p.gids) > 0 { |
| fields = append(fields, fmt.Sprintf("gids=%d", p.gids)) |
| } |
| if p.mode != nil { |
| fields = append(fields, fmt.Sprintf("mode=%04o", *p.mode)) |
| } |
| if p.notMode != nil { |
| fields = append(fields, fmt.Sprintf("notMode=%04o", *p.notMode)) |
| } |
| if p.skipChildren { |
| fields = append(fields, "skipChildren") |
| } |
| return "[" + strings.Join(fields, " ") + "]" |
| } |
| |
| // Option is used to configure a Pattern. |
| type Option func(*Pattern) |
| |
| // UID requires that the path be owned by one of the supplied user IDs. |
| func UID(uids ...uint32) Option { return func(p *Pattern) { p.uids = uids } } |
| |
| // GID requires that the path be owned by one of the supplied group IDs. |
| func GID(gids ...uint32) Option { return func(p *Pattern) { p.gids = gids } } |
| |
| // Users returns options that permit a path to be owned by any of the supplied |
| // users (all of which must exist). |
| func Users(usernames ...string) Option { |
| uids := make([]uint32, len(usernames)) |
| var err error |
| for i, u := range usernames { |
| uids[i], err = sysutil.GetUID(u) |
| if err != nil { |
| return func(p *Pattern) { p.errors = append(p.errors, fmt.Sprintf("Failed to find uid: %v", err)) } |
| } |
| } |
| return UID(uids...) |
| } |
| |
| // Groups returns options that permit a path to be owned by any of the supplied |
| // groups (all of which must exist). |
| func Groups(gs ...string) Option { |
| gids := make([]uint32, len(gs)) |
| var err error |
| for i, g := range gs { |
| gids[i], err = sysutil.GetGID(g) |
| if err != nil { |
| return func(p *Pattern) { p.errors = append(p.errors, fmt.Sprintf("Failed to find gid: %v", err)) } |
| } |
| } |
| return GID(gids...) |
| } |
| |
| // checkMode returns false if m contains any non-permission-related bits. |
| func checkMode(m os.FileMode, p *Pattern) bool { |
| if invalid := m & ^modeMask; invalid != 0 { |
| p.errors = append(p.errors, fmt.Sprintf("invalid bit(s) %04o", m)) |
| return false |
| } |
| return true |
| } |
| |
| // Mode requires that permission-related bits in the path's mode exactly match m. |
| // Only 0777, setuid, setgid, and the sticky bit may be supplied. |
| func Mode(m os.FileMode) Option { |
| return func(p *Pattern) { |
| if checkMode(m, p) { |
| p.mode = &m |
| } |
| } |
| } |
| |
| // NotMode requires that the permission-related bits in the path's mode contain none of the bits in nm. |
| // Only 0777, setuid, setgid, and the sticky bit may be supplied. |
| func NotMode(nm os.FileMode) Option { |
| return func(p *Pattern) { |
| if checkMode(nm, p) { |
| p.notMode = &nm |
| } |
| } |
| } |
| |
| // SkipChildren indicates that any child paths should not be checked. |
| // The directory itself will still be checked. This has no effect for non-directories. |
| func SkipChildren() Option { return func(p *Pattern) { p.skipChildren = true } } |
| |
| // Matcher matches a path relative to the root passed to Check. |
| type Matcher func(path string) bool |
| |
| // AllPaths returns a Matcher that matches all paths. |
| func AllPaths() Matcher { |
| return func(p string) bool { return true } |
| } |
| |
| // Path returns a Matcher that matches only the supplied path (relative to the root passed to Check). |
| func Path(path string) Matcher { |
| if path == "" || path[0] == '/' { |
| panic("Path must be relative") |
| } |
| return func(p string) bool { return p == path } |
| } |
| |
| // Root returns a Matcher that matches the root path passed to Check. |
| func Root() Matcher { |
| return func(p string) bool { return p == "" } |
| } |
| |
| // PathRegexp returns a Matcher that matches all paths matched by regular expression r. |
| // r is evaluated against paths relative to the root passed to Check. |
| func PathRegexp(r string) Matcher { |
| re := regexp.MustCompile(r) |
| return func(p string) bool { return re.MatchString(p) } |
| } |
| |
| // Tree returns a Matcher that matches both path and its children. |
| // The path is relative to the root passed to Check. |
| func Tree(path string) Matcher { |
| if path == "" { |
| panic("Use AllPaths to match all paths") |
| } |
| pre := path + "/" |
| return func(p string) bool { return p == path || strings.HasPrefix(p, pre) } |
| } |
| |
| // Check inspects all files within (and including) root. |
| // pats are executed in-order against each path. |
| // If a pattern matches a path, no later patterns are evaluated against it. |
| // If SkipChildren is included in a pattern , any matched directories' children are skipped. |
| // A map from absolute path names to strings describing problems is returned, |
| // along with the number of paths (not including ones skipped by SkipChildren) that were inspected. |
| func Check(ctx context.Context, root string, pats []*Pattern) ( |
| problems map[string][]string, numPaths int, err error) { |
| problems = make(map[string][]string) |
| err = filepath.Walk(root, func(fullPath string, fi os.FileInfo, err error) error { |
| // Check for test timeout. |
| if ctx.Err() != nil { |
| return ctx.Err() |
| } |
| |
| // If filepath.Walk encountered an error inspecting the file, skip it. |
| // This generally seems to happen due to a file getting deleted mid-run, but we also sometimes |
| // see "readdirent: input/output error" errors: https://crbug.com/908416 |
| if err != nil { |
| testing.ContextLogf(ctx, "Failed to check %v: %v", fullPath, err) |
| return nil |
| } |
| |
| relPath := "" |
| if fullPath != root { |
| relPath = fullPath[len(root+"/"):] |
| } |
| numPaths++ |
| |
| for _, pat := range pats { |
| if pat.match(relPath) { |
| if msgs := pat.check(fi); len(msgs) > 0 { |
| problems[fullPath] = append(problems[fullPath], msgs...) |
| } |
| if pat.skipChildren && fi.IsDir() { |
| return filepath.SkipDir |
| } |
| break |
| } |
| } |
| |
| return nil |
| }) |
| |
| return problems, numPaths, err |
| } |