diff --git a/server/plugin/plg_backend_nfs/auth_helper.go b/server/plugin/plg_backend_nfs/auth_helper.go index 7edb36af..c1e916cf 100644 --- a/server/plugin/plg_backend_nfs/auth_helper.go +++ b/server/plugin/plg_backend_nfs/auth_helper.go @@ -9,7 +9,10 @@ import ( . "github.com/mickael-kerjean/filestash/server/common" ) -var cacheForEtc AppCache +var ( + cacheForEtc AppCache + cacheForGroup AppCache +) const ( DEFAULT_UID = 1000 @@ -18,28 +21,35 @@ const ( func init() { cacheForEtc = NewAppCache(120, 60) + cacheForGroup = NewAppCache(120, 60) } -func getUid(hint string) uint32 { - if hint == "" { - return DEFAULT_UID - } else if _uid, err := strconv.Atoi(hint); err == nil { - return uint32(_uid) - } else if uid, _, err := extractFromEtcPasswd(hint); err == nil { - return uid +func extractUserInfo(uidHint string, gidHint string, gidsHint string) (uint32, uint32, []uint32) { + // case 1: everything is being sent as "uid=number, gid=number and gids=number,number,number" + if _uid, err := strconv.Atoi(uidHint); err == nil { + var ( + uid uint32 = uint32(_uid) + gid uint32 + gids []uint32 + ) + if _gid, err := strconv.Atoi(gidHint); err == nil { + gid = uint32(_gid) + } else { + gid = uid + } + for _, t := range strings.Split(gidsHint, ",") { + if gid, err := strconv.Atoi(strings.TrimSpace(t)); err == nil { + gids = append(gids, uint32(gid)) + } + } + return uid, gid, gids } - return DEFAULT_UID -} - -func getGid(hint string) uint32 { - if hint == "" { - return DEFAULT_UID - } else if _gid, err := strconv.Atoi(hint); err == nil { - return uint32(_gid) - } else if _, gid, err := extractFromEtcPasswd(hint); err == nil { - return gid + // case 2: auto detect everything, aka "uid=www-data gid=www-data gids=..." based on uid=www-data + if _uid, _gid, err := extractFromEtcPasswd(uidHint); err == nil { + return _uid, _gid, extractFromEtcGroup(uidHint, _gid) } - return DEFAULT_GID + // case 3: base case + return DEFAULT_UID, DEFAULT_GID, []uint32{} } func extractFromEtcPasswd(username string) (uint32, uint32, error) { @@ -61,18 +71,52 @@ func extractFromEtcPasswd(username string) (uint32, uint32, error) { s := strings.Split(string(line), ":") if len(s) != 7 { continue - } else if username == s[0] { - u, err := strconv.Atoi(s[2]) - if err != nil { - continue - } - g, err := strconv.Atoi(s[3]) - if err != nil { - continue - } - cacheForEtc.Set(map[string]string{"username": username}, []int{u, g}) - return uint32(u), uint32(g), nil + } else if username != s[0] { + continue } + u, err := strconv.Atoi(s[2]) + if err != nil { + continue + } + g, err := strconv.Atoi(s[3]) + if err != nil { + continue + } + cacheForEtc.Set(map[string]string{"username": username}, []int{u, g}) + return uint32(u), uint32(g), nil } return DEFAULT_UID, DEFAULT_GID, ErrNotFound } + +func extractFromEtcGroup(username string, primary uint32) []uint32 { + if v := cacheForGroup.Get(map[string]string{"username": username}); v != nil { + return v.([]uint32) + } + f, err := os.OpenFile("/etc/group", os.O_RDONLY, os.ModePerm) + if err != nil { + return []uint32{} + } + defer f.Close() + gids := []uint32{} + lines := bufio.NewReader(f) + for { + line, _, err := lines.ReadLine() + if err != nil { + break + } + s := strings.Split(string(line), ":") + if len(s) != 4 { + continue + } else if username != s[3] { + continue + } + if gid, err := strconv.Atoi(s[2]); err == nil { + ugid := uint32(gid) + if ugid != primary { + gids = append(gids, ugid) + } + } + cacheForGroup.Set(map[string]string{"username": username}, gids) + } + return gids +} diff --git a/server/plugin/plg_backend_nfs/auth_unix.go b/server/plugin/plg_backend_nfs/auth_unix.go index 91e2cba9..c0fe603b 100644 --- a/server/plugin/plg_backend_nfs/auth_unix.go +++ b/server/plugin/plg_backend_nfs/auth_unix.go @@ -9,26 +9,28 @@ import ( "github.com/vmware/go-nfs-client/nfs/xdr" ) +// ref: https://datatracker.ietf.org/doc/html/rfc5531#section-8.2 +// so far we only have implemented AUTH_SYS but one day we might want to add support +// for RPCSEC_GSS as detailed in https://datatracker.ietf.org/doc/html/rfc2203 type AuthUnix struct { Stamp uint32 Machinename string Uid uint32 Gid uint32 - GidLen uint32 Gids []uint32 } -func NewAuth(machineName string, uid, gid uint32) rpc.Auth { +func NewUnixAuth(machineName string, uid, gid uint32, gids []uint32) rpc.Auth { w := new(bytes.Buffer) xdr.Write(w, AuthUnix{ Stamp: rand.New(rand.NewSource(time.Now().UnixNano())).Uint32(), Machinename: machineName, - Uid: uid, - Gid: gid, - GidLen: 1, + Uid: 1000, + Gid: 1000, + Gids: gids, }) return rpc.Auth{ - 1, + 1, // = AUTH_SYS in RFC5531 w.Bytes(), } } diff --git a/server/plugin/plg_backend_nfs/index.go b/server/plugin/plg_backend_nfs/index.go index e26b8c6f..8e0bf798 100644 --- a/server/plugin/plg_backend_nfs/index.go +++ b/server/plugin/plg_backend_nfs/index.go @@ -33,18 +33,16 @@ func (this NfsShare) Init(params map[string]string, app *App) (IBackend, error) if params["hostname"] == "" { return nil, ErrNotFound } - if params["machine_name"] == "" { - params["machine_name"] = "filestash" + params["machine_name"] = "Filestash" } + uid, gid, gids := extractUserInfo(params["uid"], params["gid"], params["gids"]) - uid := getUid(params["uid"]) - gid := getGid(params["gid"]) mount, err := nfs.DialMount(params["hostname"]) if err != nil { return nil, err } - auth := NewAuth(params["machine_name"], uid, gid) + auth := NewUnixAuth(params["machine_name"], uid, gid, gids) v, err := mount.Mount( params["target"], auth, @@ -77,31 +75,37 @@ func (this NfsShare) LoginForm() Form { Name: "advanced", Type: "enable", Placeholder: "Advanced", - Target: []string{"nfs_uid", "nfs_gid", "nfs_machinename", "nfs_chroot"}, + Target: []string{"nfs_uid", "nfs_gid", "nfs_gids", "nfs_machinename", "nfs_chroot"}, }, FormElement{ Id: "nfs_uid", Name: "uid", Type: "text", - Placeholder: "uid", + Placeholder: "UID", }, FormElement{ Id: "nfs_gid", Name: "gid", Type: "text", - Placeholder: "gid", + Placeholder: "GID", + }, + FormElement{ + Id: "nfs_gids", + Name: "gids", + Type: "text", + Placeholder: "Auxiliary GIDs", }, FormElement{ Id: "nfs_machinename", Name: "machine_name", Type: "text", - Placeholder: "machine name", + Placeholder: "Machine Name", }, FormElement{ Id: "nfs_chroot", Name: "path", Type: "text", - Placeholder: "chroot", + Placeholder: "Chroot", }, }, }