1package idtools // import "github.com/docker/docker/pkg/idtools"
2
3import (
4	"bufio"
5	"fmt"
6	"os"
7	"sort"
8	"strconv"
9	"strings"
10)
11
12// IDMap contains a single entry for user namespace range remapping. An array
13// of IDMap entries represents the structure that will be provided to the Linux
14// kernel for creating a user namespace.
15type IDMap struct {
16	ContainerID int `json:"container_id"`
17	HostID      int `json:"host_id"`
18	Size        int `json:"size"`
19}
20
21type subIDRange struct {
22	Start  int
23	Length int
24}
25
26type ranges []subIDRange
27
28func (e ranges) Len() int           { return len(e) }
29func (e ranges) Swap(i, j int)      { e[i], e[j] = e[j], e[i] }
30func (e ranges) Less(i, j int) bool { return e[i].Start < e[j].Start }
31
32const (
33	subuidFileName = "/etc/subuid"
34	subgidFileName = "/etc/subgid"
35)
36
37// MkdirAllAndChown creates a directory (include any along the path) and then modifies
38// ownership to the requested uid/gid.  If the directory already exists, this
39// function will still change ownership to the requested uid/gid pair.
40func MkdirAllAndChown(path string, mode os.FileMode, owner Identity) error {
41	return mkdirAs(path, mode, owner, true, true)
42}
43
44// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid.
45// If the directory already exists, this function still changes ownership.
46// Note that unlike os.Mkdir(), this function does not return IsExist error
47// in case path already exists.
48func MkdirAndChown(path string, mode os.FileMode, owner Identity) error {
49	return mkdirAs(path, mode, owner, false, true)
50}
51
52// MkdirAllAndChownNew creates a directory (include any along the path) and then modifies
53// ownership ONLY of newly created directories to the requested uid/gid. If the
54// directories along the path exist, no change of ownership will be performed
55func MkdirAllAndChownNew(path string, mode os.FileMode, owner Identity) error {
56	return mkdirAs(path, mode, owner, true, false)
57}
58
59// GetRootUIDGID retrieves the remapped root uid/gid pair from the set of maps.
60// If the maps are empty, then the root uid/gid will default to "real" 0/0
61func GetRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) {
62	uid, err := toHost(0, uidMap)
63	if err != nil {
64		return -1, -1, err
65	}
66	gid, err := toHost(0, gidMap)
67	if err != nil {
68		return -1, -1, err
69	}
70	return uid, gid, nil
71}
72
73// toContainer takes an id mapping, and uses it to translate a
74// host ID to the remapped ID. If no map is provided, then the translation
75// assumes a 1-to-1 mapping and returns the passed in id
76func toContainer(hostID int, idMap []IDMap) (int, error) {
77	if idMap == nil {
78		return hostID, nil
79	}
80	for _, m := range idMap {
81		if (hostID >= m.HostID) && (hostID <= (m.HostID + m.Size - 1)) {
82			contID := m.ContainerID + (hostID - m.HostID)
83			return contID, nil
84		}
85	}
86	return -1, fmt.Errorf("Host ID %d cannot be mapped to a container ID", hostID)
87}
88
89// toHost takes an id mapping and a remapped ID, and translates the
90// ID to the mapped host ID. If no map is provided, then the translation
91// assumes a 1-to-1 mapping and returns the passed in id #
92func toHost(contID int, idMap []IDMap) (int, error) {
93	if idMap == nil {
94		return contID, nil
95	}
96	for _, m := range idMap {
97		if (contID >= m.ContainerID) && (contID <= (m.ContainerID + m.Size - 1)) {
98			hostID := m.HostID + (contID - m.ContainerID)
99			return hostID, nil
100		}
101	}
102	return -1, fmt.Errorf("Container ID %d cannot be mapped to a host ID", contID)
103}
104
105// Identity is either a UID and GID pair or a SID (but not both)
106type Identity struct {
107	UID int
108	GID int
109	SID string
110}
111
112// IdentityMapping contains a mappings of UIDs and GIDs
113type IdentityMapping struct {
114	uids []IDMap
115	gids []IDMap
116}
117
118// NewIdentityMapping takes a requested user and group name and
119// using the data from /etc/sub{uid,gid} ranges, creates the
120// proper uid and gid remapping ranges for that user/group pair
121func NewIdentityMapping(username, groupname string) (*IdentityMapping, error) {
122	subuidRanges, err := parseSubuid(username)
123	if err != nil {
124		return nil, err
125	}
126	subgidRanges, err := parseSubgid(groupname)
127	if err != nil {
128		return nil, err
129	}
130	if len(subuidRanges) == 0 {
131		return nil, fmt.Errorf("No subuid ranges found for user %q", username)
132	}
133	if len(subgidRanges) == 0 {
134		return nil, fmt.Errorf("No subgid ranges found for group %q", groupname)
135	}
136
137	return &IdentityMapping{
138		uids: createIDMap(subuidRanges),
139		gids: createIDMap(subgidRanges),
140	}, nil
141}
142
143// NewIDMappingsFromMaps creates a new mapping from two slices
144// Deprecated: this is a temporary shim while transitioning to IDMapping
145func NewIDMappingsFromMaps(uids []IDMap, gids []IDMap) *IdentityMapping {
146	return &IdentityMapping{uids: uids, gids: gids}
147}
148
149// RootPair returns a uid and gid pair for the root user. The error is ignored
150// because a root user always exists, and the defaults are correct when the uid
151// and gid maps are empty.
152func (i *IdentityMapping) RootPair() Identity {
153	uid, gid, _ := GetRootUIDGID(i.uids, i.gids)
154	return Identity{UID: uid, GID: gid}
155}
156
157// ToHost returns the host UID and GID for the container uid, gid.
158// Remapping is only performed if the ids aren't already the remapped root ids
159func (i *IdentityMapping) ToHost(pair Identity) (Identity, error) {
160	var err error
161	target := i.RootPair()
162
163	if pair.UID != target.UID {
164		target.UID, err = toHost(pair.UID, i.uids)
165		if err != nil {
166			return target, err
167		}
168	}
169
170	if pair.GID != target.GID {
171		target.GID, err = toHost(pair.GID, i.gids)
172	}
173	return target, err
174}
175
176// ToContainer returns the container UID and GID for the host uid and gid
177func (i *IdentityMapping) ToContainer(pair Identity) (int, int, error) {
178	uid, err := toContainer(pair.UID, i.uids)
179	if err != nil {
180		return -1, -1, err
181	}
182	gid, err := toContainer(pair.GID, i.gids)
183	return uid, gid, err
184}
185
186// Empty returns true if there are no id mappings
187func (i *IdentityMapping) Empty() bool {
188	return len(i.uids) == 0 && len(i.gids) == 0
189}
190
191// UIDs return the UID mapping
192// TODO: remove this once everything has been refactored to use pairs
193func (i *IdentityMapping) UIDs() []IDMap {
194	return i.uids
195}
196
197// GIDs return the UID mapping
198// TODO: remove this once everything has been refactored to use pairs
199func (i *IdentityMapping) GIDs() []IDMap {
200	return i.gids
201}
202
203func createIDMap(subidRanges ranges) []IDMap {
204	idMap := []IDMap{}
205
206	// sort the ranges by lowest ID first
207	sort.Sort(subidRanges)
208	containerID := 0
209	for _, idrange := range subidRanges {
210		idMap = append(idMap, IDMap{
211			ContainerID: containerID,
212			HostID:      idrange.Start,
213			Size:        idrange.Length,
214		})
215		containerID = containerID + idrange.Length
216	}
217	return idMap
218}
219
220func parseSubuid(username string) (ranges, error) {
221	return parseSubidFile(subuidFileName, username)
222}
223
224func parseSubgid(username string) (ranges, error) {
225	return parseSubidFile(subgidFileName, username)
226}
227
228// parseSubidFile will read the appropriate file (/etc/subuid or /etc/subgid)
229// and return all found ranges for a specified username. If the special value
230// "ALL" is supplied for username, then all ranges in the file will be returned
231func parseSubidFile(path, username string) (ranges, error) {
232	var rangeList ranges
233
234	subidFile, err := os.Open(path)
235	if err != nil {
236		return rangeList, err
237	}
238	defer subidFile.Close()
239
240	s := bufio.NewScanner(subidFile)
241	for s.Scan() {
242		if err := s.Err(); err != nil {
243			return rangeList, err
244		}
245
246		text := strings.TrimSpace(s.Text())
247		if text == "" || strings.HasPrefix(text, "#") {
248			continue
249		}
250		parts := strings.Split(text, ":")
251		if len(parts) != 3 {
252			return rangeList, fmt.Errorf("Cannot parse subuid/gid information: Format not correct for %s file", path)
253		}
254		if parts[0] == username || username == "ALL" {
255			startid, err := strconv.Atoi(parts[1])
256			if err != nil {
257				return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
258			}
259			length, err := strconv.Atoi(parts[2])
260			if err != nil {
261				return rangeList, fmt.Errorf("String to int conversion failed during subuid/gid parsing of %s: %v", path, err)
262			}
263			rangeList = append(rangeList, subIDRange{startid, length})
264		}
265	}
266	return rangeList, nil
267}
268