198 lines
5.9 KiB
Go
198 lines
5.9 KiB
Go
|
package idtools
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// 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"`
|
||
|
}
|
||
|
|
||
|
type subIDRange struct {
|
||
|
Start int
|
||
|
Length int
|
||
|
}
|
||
|
|
||
|
type ranges []subIDRange
|
||
|
|
||
|
func (e ranges) Len() int { return len(e) }
|
||
|
func (e ranges) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
|
||
|
func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start }
|
||
|
|
||
|
const (
|
||
|
subuidFileName string = "/etc/subuid"
|
||
|
subgidFileName string = "/etc/subgid"
|
||
|
)
|
||
|
|
||
|
// MkdirAllAs 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 to the requested uid/gid pair.
|
||
|
func MkdirAllAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
|
||
|
return mkdirAs(path, mode, ownerUID, ownerGID, true, true)
|
||
|
}
|
||
|
|
||
|
// MkdirAllNewAs 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 will be performed
|
||
|
func MkdirAllNewAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
|
||
|
return mkdirAs(path, mode, ownerUID, ownerGID, true, false)
|
||
|
}
|
||
|
|
||
|
// MkdirAs creates a directory and then modifies ownership to the requested uid/gid.
|
||
|
// If the directory already exists, this function still changes ownership
|
||
|
func MkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int) error {
|
||
|
return mkdirAs(path, mode, ownerUID, ownerGID, false, true)
|
||
|
}
|
||
|
|
||
|
// 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) {
|
||
|
var uid, gid int
|
||
|
|
||
|
if uidMap != nil {
|
||
|
xUID, err := ToHost(0, uidMap)
|
||
|
if err != nil {
|
||
|
return -1, -1, err
|
||
|
}
|
||
|
uid = xUID
|
||
|
}
|
||
|
if gidMap != nil {
|
||
|
xGID, err := ToHost(0, gidMap)
|
||
|
if err != nil {
|
||
|
return -1, -1, err
|
||
|
}
|
||
|
gid = xGID
|
||
|
}
|
||
|
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)
|
||
|
}
|
||
|
|
||
|
// CreateIDMappings takes a requested user and group name and
|
||
|
// using the data from /etc/sub{uid,gid} ranges, creates the
|
||
|
// proper uid and gid remapping ranges for that user/group pair
|
||
|
func CreateIDMappings(username, groupname string) ([]IDMap, []IDMap, error) {
|
||
|
subuidRanges, err := parseSubuid(username)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
subgidRanges, err := parseSubgid(groupname)
|
||
|
if err != nil {
|
||
|
return nil, nil, err
|
||
|
}
|
||
|
if len(subuidRanges) == 0 {
|
||
|
return nil, nil, fmt.Errorf("No subuid ranges found for user %q", username)
|
||
|
}
|
||
|
if len(subgidRanges) == 0 {
|
||
|
return nil, nil, fmt.Errorf("No subgid ranges found for group %q", groupname)
|
||
|
}
|
||
|
|
||
|
return createIDMap(subuidRanges), createIDMap(subgidRanges), nil
|
||
|
}
|
||
|
|
||
|
func createIDMap(subidRanges ranges) []IDMap {
|
||
|
idMap := []IDMap{}
|
||
|
|
||
|
// sort the ranges by lowest ID first
|
||
|
sort.Sort(subidRanges)
|
||
|
containerID := 0
|
||
|
for _, idrange := range subidRanges {
|
||
|
idMap = append(idMap, IDMap{
|
||
|
ContainerID: containerID,
|
||
|
HostID: idrange.Start,
|
||
|
Size: idrange.Length,
|
||
|
})
|
||
|
containerID = containerID + idrange.Length
|
||
|
}
|
||
|
return idMap
|
||
|
}
|
||
|
|
||
|
func parseSubuid(username string) (ranges, error) {
|
||
|
return parseSubidFile(subuidFileName, username)
|
||
|
}
|
||
|
|
||
|
func parseSubgid(username string) (ranges, error) {
|
||
|
return parseSubidFile(subgidFileName, username)
|
||
|
}
|
||
|
|
||
|
// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid)
|
||
|
// and return all found ranges for a specified username. If the special value
|
||
|
// "ALL" is supplied for username, then all ranges in the file will be returned
|
||
|
func parseSubidFile(path, username string) (ranges, error) {
|
||
|
var rangeList ranges
|
||
|
|
||
|
subidFile, err := os.Open(path)
|
||
|
if err != nil {
|
||
|
return rangeList, err
|
||
|
}
|
||
|
defer subidFile.Close()
|
||
|
|
||
|
s := bufio.NewScanner(subidFile)
|
||
|
for s.Scan() {
|
||
|
if err := s.Err(); err != nil {
|
||
|
return rangeList, err
|
||
|
}
|
||
|
|
||
|
text := strings.TrimSpace(s.Text())
|
||
|
if text == "" || strings.HasPrefix(text, "#") {
|
||
|
continue
|
||
|
}
|
||
|
parts := strings.Split(text, ":")
|
||
|
if len(parts) != 3 {
|
||
|
return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path)
|
||
|
}
|
||
|
if parts[0] == username || username == "ALL" {
|
||
|
startid, err := strconv.Atoi(parts[1])
|
||
|
if err != nil {
|
||
|
return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
|
||
|
}
|
||
|
length, err := strconv.Atoi(parts[2])
|
||
|
if err != nil {
|
||
|
return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
|
||
|
}
|
||
|
rangeList = append(rangeList, subIDRange{startid, length})
|
||
|
}
|
||
|
}
|
||
|
return rangeList, nil
|
||
|
}
|