Skip to content

Commit 71f0c5e

Browse files
authored
Merge pull request #182 from dmcgowan/merge-idtools
Merge functionality from `github.com/moby/moby/pkg/idtools` into `user`
2 parents ca0444f + db55716 commit 71f0c5e

File tree

4 files changed

+695
-0
lines changed

4 files changed

+695
-0
lines changed

Diff for: user/idtools.go

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package user
2+
3+
import (
4+
"fmt"
5+
"os"
6+
)
7+
8+
// MkdirOpt is a type for options to pass to Mkdir calls
9+
type MkdirOpt func(*mkdirOptions)
10+
11+
type mkdirOptions struct {
12+
onlyNew bool
13+
}
14+
15+
// WithOnlyNew is an option for MkdirAllAndChown that will only change ownership and permissions
16+
// on newly created directories. If the directory already exists, it will not be modified
17+
func WithOnlyNew(o *mkdirOptions) {
18+
o.onlyNew = true
19+
}
20+
21+
// MkdirAllAndChown creates a directory (include any along the path) and then modifies
22+
// ownership to the requested uid/gid. By default, if the directory already exists, this
23+
// function will still change ownership and permissions. If WithOnlyNew is passed as an
24+
// option, then only the newly created directories will have ownership and permissions changed.
25+
func MkdirAllAndChown(path string, mode os.FileMode, uid, gid int, opts ...MkdirOpt) error {
26+
var options mkdirOptions
27+
for _, opt := range opts {
28+
opt(&options)
29+
}
30+
31+
return mkdirAs(path, mode, uid, gid, true, options.onlyNew)
32+
}
33+
34+
// MkdirAndChown creates a directory and then modifies ownership to the requested uid/gid.
35+
// By default, if the directory already exists, this function still changes ownership and permissions.
36+
// If WithOnlyNew is passed as an option, then only the newly created directory will have ownership
37+
// and permissions changed.
38+
// Note that unlike os.Mkdir(), this function does not return IsExist error
39+
// in case path already exists.
40+
func MkdirAndChown(path string, mode os.FileMode, uid, gid int, opts ...MkdirOpt) error {
41+
var options mkdirOptions
42+
for _, opt := range opts {
43+
opt(&options)
44+
}
45+
return mkdirAs(path, mode, uid, gid, false, options.onlyNew)
46+
}
47+
48+
// getRootUIDGID retrieves the remapped root uid/gid pair from the set of maps.
49+
// If the maps are empty, then the root uid/gid will default to "real" 0/0
50+
func getRootUIDGID(uidMap, gidMap []IDMap) (int, int, error) {
51+
uid, err := toHost(0, uidMap)
52+
if err != nil {
53+
return -1, -1, err
54+
}
55+
gid, err := toHost(0, gidMap)
56+
if err != nil {
57+
return -1, -1, err
58+
}
59+
return uid, gid, nil
60+
}
61+
62+
// toContainer takes an id mapping, and uses it to translate a
63+
// host ID to the remapped ID. If no map is provided, then the translation
64+
// assumes a 1-to-1 mapping and returns the passed in id
65+
func toContainer(hostID int, idMap []IDMap) (int, error) {
66+
if idMap == nil {
67+
return hostID, nil
68+
}
69+
for _, m := range idMap {
70+
if (int64(hostID) >= m.ParentID) && (int64(hostID) <= (m.ParentID + m.Count - 1)) {
71+
contID := int(m.ID + (int64(hostID) - m.ParentID))
72+
return contID, nil
73+
}
74+
}
75+
return -1, fmt.Errorf("host ID %d cannot be mapped to a container ID", hostID)
76+
}
77+
78+
// toHost takes an id mapping and a remapped ID, and translates the
79+
// ID to the mapped host ID. If no map is provided, then the translation
80+
// assumes a 1-to-1 mapping and returns the passed in id #
81+
func toHost(contID int, idMap []IDMap) (int, error) {
82+
if idMap == nil {
83+
return contID, nil
84+
}
85+
for _, m := range idMap {
86+
if (int64(contID) >= m.ID) && (int64(contID) <= (m.ID + m.Count - 1)) {
87+
hostID := int(m.ParentID + (int64(contID) - m.ID))
88+
return hostID, nil
89+
}
90+
}
91+
return -1, fmt.Errorf("container ID %d cannot be mapped to a host ID", contID)
92+
}
93+
94+
// IdentityMapping contains a mappings of UIDs and GIDs.
95+
// The zero value represents an empty mapping.
96+
type IdentityMapping struct {
97+
UIDMaps []IDMap `json:"UIDMaps"`
98+
GIDMaps []IDMap `json:"GIDMaps"`
99+
}
100+
101+
// RootPair returns a uid and gid pair for the root user. The error is ignored
102+
// because a root user always exists, and the defaults are correct when the uid
103+
// and gid maps are empty.
104+
func (i IdentityMapping) RootPair() (int, int) {
105+
uid, gid, _ := getRootUIDGID(i.UIDMaps, i.GIDMaps)
106+
return uid, gid
107+
}
108+
109+
// ToHost returns the host UID and GID for the container uid, gid.
110+
// Remapping is only performed if the ids aren't already the remapped root ids
111+
func (i IdentityMapping) ToHost(uid, gid int) (int, int, error) {
112+
var err error
113+
ruid, rgid := i.RootPair()
114+
115+
if uid != ruid {
116+
ruid, err = toHost(uid, i.UIDMaps)
117+
if err != nil {
118+
return ruid, rgid, err
119+
}
120+
}
121+
122+
if gid != rgid {
123+
rgid, err = toHost(gid, i.GIDMaps)
124+
}
125+
return ruid, rgid, err
126+
}
127+
128+
// ToContainer returns the container UID and GID for the host uid and gid
129+
func (i IdentityMapping) ToContainer(uid, gid int) (int, int, error) {
130+
ruid, err := toContainer(uid, i.UIDMaps)
131+
if err != nil {
132+
return -1, -1, err
133+
}
134+
rgid, err := toContainer(gid, i.GIDMaps)
135+
return ruid, rgid, err
136+
}
137+
138+
// Empty returns true if there are no id mappings
139+
func (i IdentityMapping) Empty() bool {
140+
return len(i.UIDMaps) == 0 && len(i.GIDMaps) == 0
141+
}

Diff for: user/idtools_unix.go

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
//go:build !windows
2+
3+
package user
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strconv"
10+
"syscall"
11+
)
12+
13+
func mkdirAs(path string, mode os.FileMode, uid, gid int, mkAll, onlyNew bool) error {
14+
path, err := filepath.Abs(path)
15+
if err != nil {
16+
return err
17+
}
18+
19+
stat, err := os.Stat(path)
20+
if err == nil {
21+
if !stat.IsDir() {
22+
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
23+
}
24+
if onlyNew {
25+
return nil
26+
}
27+
28+
// short-circuit -- we were called with an existing directory and chown was requested
29+
return setPermissions(path, mode, uid, gid, stat)
30+
}
31+
32+
// make an array containing the original path asked for, plus (for mkAll == true)
33+
// all path components leading up to the complete path that don't exist before we MkdirAll
34+
// so that we can chown all of them properly at the end. If onlyNew is true, we won't
35+
// chown the full directory path if it exists
36+
var paths []string
37+
if os.IsNotExist(err) {
38+
paths = append(paths, path)
39+
}
40+
41+
if mkAll {
42+
// walk back to "/" looking for directories which do not exist
43+
// and add them to the paths array for chown after creation
44+
dirPath := path
45+
for {
46+
dirPath = filepath.Dir(dirPath)
47+
if dirPath == "/" {
48+
break
49+
}
50+
if _, err = os.Stat(dirPath); os.IsNotExist(err) {
51+
paths = append(paths, dirPath)
52+
}
53+
}
54+
if err = os.MkdirAll(path, mode); err != nil {
55+
return err
56+
}
57+
} else if err = os.Mkdir(path, mode); err != nil {
58+
return err
59+
}
60+
// even if it existed, we will chown the requested path + any subpaths that
61+
// didn't exist when we called MkdirAll
62+
for _, pathComponent := range paths {
63+
if err = setPermissions(pathComponent, mode, uid, gid, nil); err != nil {
64+
return err
65+
}
66+
}
67+
return nil
68+
}
69+
70+
// setPermissions performs a chown/chmod only if the uid/gid don't match what's requested
71+
// 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
72+
// dir is on an NFS share, so don't call chown unless we absolutely must.
73+
// Likewise for setting permissions.
74+
func setPermissions(p string, mode os.FileMode, uid, gid int, stat os.FileInfo) error {
75+
if stat == nil {
76+
var err error
77+
stat, err = os.Stat(p)
78+
if err != nil {
79+
return err
80+
}
81+
}
82+
if stat.Mode().Perm() != mode.Perm() {
83+
if err := os.Chmod(p, mode.Perm()); err != nil {
84+
return err
85+
}
86+
}
87+
ssi := stat.Sys().(*syscall.Stat_t)
88+
if ssi.Uid == uint32(uid) && ssi.Gid == uint32(gid) {
89+
return nil
90+
}
91+
return os.Chown(p, uid, gid)
92+
}
93+
94+
// LoadIdentityMapping takes a requested username and
95+
// using the data from /etc/sub{uid,gid} ranges, creates the
96+
// proper uid and gid remapping ranges for that user/group pair
97+
func LoadIdentityMapping(name string) (IdentityMapping, error) {
98+
// TODO: Consider adding support for calling out to "getent"
99+
usr, err := LookupUser(name)
100+
if err != nil {
101+
return IdentityMapping{}, fmt.Errorf("could not get user for username %s: %w", name, err)
102+
}
103+
104+
subuidRanges, err := lookupSubRangesFile("/etc/subuid", usr)
105+
if err != nil {
106+
return IdentityMapping{}, err
107+
}
108+
subgidRanges, err := lookupSubRangesFile("/etc/subgid", usr)
109+
if err != nil {
110+
return IdentityMapping{}, err
111+
}
112+
113+
return IdentityMapping{
114+
UIDMaps: subuidRanges,
115+
GIDMaps: subgidRanges,
116+
}, nil
117+
}
118+
119+
func lookupSubRangesFile(path string, usr User) ([]IDMap, error) {
120+
uidstr := strconv.Itoa(usr.Uid)
121+
rangeList, err := ParseSubIDFileFilter(path, func(sid SubID) bool {
122+
return sid.Name == usr.Name || sid.Name == uidstr
123+
})
124+
if err != nil {
125+
return nil, err
126+
}
127+
if len(rangeList) == 0 {
128+
return nil, fmt.Errorf("no subuid ranges found for user %q", usr.Name)
129+
}
130+
131+
idMap := []IDMap{}
132+
133+
var containerID int64
134+
for _, idrange := range rangeList {
135+
idMap = append(idMap, IDMap{
136+
ID: containerID,
137+
ParentID: idrange.SubID,
138+
Count: idrange.Count,
139+
})
140+
containerID = containerID + idrange.Count
141+
}
142+
return idMap, nil
143+
}

0 commit comments

Comments
 (0)