-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge idtools from github.com/moby/moby/pkg/idtools
Signed-off-by: Derek McGowan <derek@mcg.dev>
- Loading branch information
Showing
4 changed files
with
724 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package idtools | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
) | ||
|
||
// IDMap contains a single entry for user namespace range remapping. An array | ||
// of IDMap entries represents the structure that will be provided to the Linux | ||
// kernel for creating a user namespace. | ||
type IDMap struct { | ||
ContainerID int `json:"container_id"` | ||
HostID int `json:"host_id"` | ||
Size int `json:"size"` | ||
} | ||
|
||
// MkdirAllAndChown creates a directory (include any along the path) and then modifies | ||
// ownership to the requested uid/gid. If the directory already exists, this | ||
// function will still change ownership and permissions. | ||
func MkdirAllAndChown(path string, mode os.FileMode, owner Identity) error { | ||
return mkdirAs(path, mode, owner, true, true) | ||
} | ||
|
||
// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid. | ||
// If the directory already exists, this function still changes ownership and permissions. | ||
// Note that unlike os.Mkdir(), this function does not return IsExist error | ||
// in case path already exists. | ||
func MkdirAndChown(path string, mode os.FileMode, owner Identity) error { | ||
return mkdirAs(path, mode, owner, false, true) | ||
} | ||
|
||
// MkdirAllAndChownNew creates a directory (include any along the path) and then modifies | ||
// ownership ONLY of newly created directories to the requested uid/gid. If the | ||
// directories along the path exist, no change of ownership or permissions will be performed | ||
func MkdirAllAndChownNew(path string, mode os.FileMode, owner Identity) error { | ||
return mkdirAs(path, mode, owner, true, false) | ||
} | ||
|
||
// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps. | ||
// If the maps are empty, then the root uid/gid will default to "real" 0/0 | ||
func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) { | ||
uid, err := toHost(0, uidMap) | ||
if err != nil { | ||
return -1, -1, err | ||
} | ||
gid, err := toHost(0, gidMap) | ||
if err != nil { | ||
return -1, -1, err | ||
} | ||
return uid, gid, nil | ||
} | ||
|
||
// toContainer takes an id mapping, and uses it to translate a | ||
// host ID to the remapped ID. If no map is provided, then the translation | ||
// assumes a 1-to-1 mapping and returns the passed in id | ||
func toContainer(hostID int, idMap []IDMap) (int, error) { | ||
if idMap == nil { | ||
return hostID, nil | ||
} | ||
for _, m := range idMap { | ||
if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) { | ||
contID := m.ContainerID + (hostID - m.HostID) | ||
return contID, nil | ||
} | ||
} | ||
return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID) | ||
} | ||
|
||
// toHost takes an id mapping and a remapped ID, and translates the | ||
// ID to the mapped host ID. If no map is provided, then the translation | ||
// assumes a 1-to-1 mapping and returns the passed in id # | ||
func toHost(contID int, idMap []IDMap) (int, error) { | ||
if idMap == nil { | ||
return contID, nil | ||
} | ||
for _, m := range idMap { | ||
if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) { | ||
hostID := m.HostID + (contID - m.ContainerID) | ||
return hostID, nil | ||
} | ||
} | ||
return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID) | ||
} | ||
|
||
// Identity is either a UID and GID pair or a SID (but not both) | ||
type Identity struct { | ||
UID int | ||
GID int | ||
SID string | ||
} | ||
|
||
// Chown changes the numeric uid and gid of the named file to id.UID and id.GID. | ||
func (id Identity) Chown(name string) error { | ||
return os.Chown(name, id.UID, id.GID) | ||
} | ||
|
||
// IdentityMapping contains a mappings of UIDs and GIDs. | ||
// The zero value represents an empty mapping. | ||
type IdentityMapping struct { | ||
UIDMaps []IDMap `json:"UIDMaps"` | ||
GIDMaps []IDMap `json:"GIDMaps"` | ||
} | ||
|
||
// RootPair returns a uid and gid pair for the root user. The error is ignored | ||
// because a root user always exists, and the defaults are correct when the uid | ||
// and gid maps are empty. | ||
func (i IdentityMapping) RootPair() Identity { | ||
uid, gid, _ := GetRootUIDGID(i.UIDMaps, i.GIDMaps) | ||
return Identity{UID: uid, GID: gid} | ||
} | ||
|
||
// ToHost returns the host UID and GID for the container uid, gid. | ||
// Remapping is only performed if the ids aren't already the remapped root ids | ||
func (i IdentityMapping) ToHost(pair Identity) (Identity, error) { | ||
var err error | ||
target := i.RootPair() | ||
|
||
if pair.UID != target.UID { | ||
target.UID, err = toHost(pair.UID, i.UIDMaps) | ||
if err != nil { | ||
return target, err | ||
} | ||
} | ||
|
||
if pair.GID != target.GID { | ||
target.GID, err = toHost(pair.GID, i.GIDMaps) | ||
} | ||
return target, err | ||
} | ||
|
||
// ToContainer returns the container UID and GID for the host uid and gid | ||
func (i IdentityMapping) ToContainer(pair Identity) (int, int, error) { | ||
uid, err := toContainer(pair.UID, i.UIDMaps) | ||
if err != nil { | ||
return -1, -1, err | ||
} | ||
gid, err := toContainer(pair.GID, i.GIDMaps) | ||
return uid, gid, err | ||
} | ||
|
||
// Empty returns true if there are no id mappings | ||
func (i IdentityMapping) Empty() bool { | ||
return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0 | ||
} | ||
|
||
// CurrentIdentity returns the identity of the current process | ||
func CurrentIdentity() Identity { | ||
return Identity{UID: os.Getuid(), GID: os.Getegid()} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
//go:build !windows | ||
|
||
package idtools | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
"strconv" | ||
"syscall" | ||
|
||
"github.com/moby/sys/user" | ||
) | ||
|
||
func mkdirAs(path string, mode os.FileMode, owner Identity, mkAll, chownExisting bool) error { | ||
path, err := filepath.Abs(path) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
stat, err := os.Stat(path) | ||
if err == nil { | ||
if !stat.IsDir() { | ||
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR} | ||
} | ||
if !chownExisting { | ||
return nil | ||
} | ||
|
||
// short-circuit -- we were called with an existing directory and chown was requested | ||
return setPermissions(path, mode, owner, stat) | ||
} | ||
|
||
// make an array containing the original path asked for, plus (for mkAll == true) | ||
// all path components leading up to the complete path that don't exist before we MkdirAll | ||
// so that we can chown all of them properly at the end. If chownExisting is false, we won't | ||
// chown the full directory path if it exists | ||
var paths []string | ||
if os.IsNotExist(err) { | ||
paths = []string{path} | ||
} | ||
|
||
if mkAll { | ||
// walk back to "/" looking for directories which do not exist | ||
// and add them to the paths array for chown after creation | ||
dirPath := path | ||
for { | ||
dirPath = filepath.Dir(dirPath) | ||
if dirPath == "/" { | ||
break | ||
} | ||
if _, err = os.Stat(dirPath); err != nil && os.IsNotExist(err) { | ||
paths = append(paths, dirPath) | ||
} | ||
} | ||
if err = os.MkdirAll(path, mode); err != nil { | ||
return err | ||
} | ||
} else if err = os.Mkdir(path, mode); err != nil { | ||
return err | ||
} | ||
// even if it existed, we will chown the requested path + any subpaths that | ||
// didn't exist when we called MkdirAll | ||
for _, pathComponent := range paths { | ||
if err = setPermissions(pathComponent, mode, owner, nil); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// LookupUser uses traditional local system files lookup (from libcontainer/user) on a username | ||
// | ||
// Deprecated: use [user.LookupUser] instead | ||
func LookupUser(name string) (user.User, error) { | ||
return user.LookupUser(name) | ||
} | ||
|
||
// LookupUID uses traditional local system files lookup (from libcontainer/user) on a uid | ||
// | ||
// Deprecated: use [user.LookupUid] instead | ||
func LookupUID(uid int) (user.User, error) { | ||
return user.LookupUid(uid) | ||
} | ||
|
||
// LookupGroup uses traditional local system files lookup (from libcontainer/user) on a group name, | ||
// | ||
// Deprecated: use [user.LookupGroup] instead | ||
func LookupGroup(name string) (user.Group, error) { | ||
return user.LookupGroup(name) | ||
} | ||
|
||
// setPermissions performs a chown/chmod only if the uid/gid don't match what's requested | ||
// Normally a Chown is a no-op if uid/gid match, but in some cases this can still cause an error, e.g. if the | ||
// dir is on an NFS share, so don't call chown unless we absolutely must. | ||
// Likewise for setting permissions. | ||
func setPermissions(p string, mode os.FileMode, owner Identity, stat os.FileInfo) error { | ||
if stat == nil { | ||
var err error | ||
stat, err = os.Stat(p) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
if stat.Mode().Perm() != mode.Perm() { | ||
if err := os.Chmod(p, mode.Perm()); err != nil { | ||
return err | ||
} | ||
} | ||
ssi := stat.Sys().(*syscall.Stat_t) | ||
if ssi.Uid == uint32(owner.UID) && ssi.Gid == uint32(owner.GID) { | ||
return nil | ||
} | ||
return os.Chown(p, owner.UID, owner.GID) | ||
} | ||
|
||
// LoadIdentityMapping takes a requested username and | ||
// using the data from /etc/sub{uid,gid} ranges, creates the | ||
// proper uid and gid remapping ranges for that user/group pair | ||
func LoadIdentityMapping(name string) (IdentityMapping, error) { | ||
// TODO: Consider adding support for calling out to "getent" | ||
usr, err := user.LookupUser(name) | ||
if err != nil { | ||
return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %v", name, err) | ||
} | ||
|
||
subuidRanges, err := lookupSubRangesFile("/etc/subuid", usr) | ||
if err != nil { | ||
return IdentityMapping{}, err | ||
} | ||
subgidRanges, err := lookupSubRangesFile("/etc/subgid", usr) | ||
if err != nil { | ||
return IdentityMapping{}, err | ||
} | ||
|
||
return IdentityMapping{ | ||
UIDMaps: subuidRanges, | ||
GIDMaps: subgidRanges, | ||
}, nil | ||
} | ||
|
||
func lookupSubRangesFile(path string, usr user.User) ([]IDMap, error) { | ||
uidstr := strconv.Itoa(usr.Uid) | ||
rangeList, err := user.ParseSubIDFileFilter(path, func(sid user.SubID) bool { | ||
return sid.Name == usr.Name || sid.Name == uidstr | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(rangeList) == 0 { | ||
return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name) | ||
} | ||
|
||
idMap := []IDMap{} | ||
|
||
containerID := 0 | ||
for _, idrange := range rangeList { | ||
idMap = append(idMap, IDMap{ | ||
ContainerID: containerID, | ||
HostID: int(idrange.SubID), | ||
Size: int(idrange.Count), | ||
}) | ||
containerID = containerID + int(idrange.Count) | ||
} | ||
return idMap, nil | ||
} |
Oops, something went wrong.