mirror of https://github.com/dexidp/dex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
441 lines
11 KiB
441 lines
11 KiB
package user |
|
|
|
import ( |
|
"bufio" |
|
"fmt" |
|
"io" |
|
"os" |
|
"strconv" |
|
"strings" |
|
) |
|
|
|
const ( |
|
minId = 0 |
|
maxId = 1<<31 - 1 //for 32-bit systems compatibility |
|
) |
|
|
|
var ( |
|
ErrRange = fmt.Errorf("uids and gids must be in range %d-%d", minId, maxId) |
|
) |
|
|
|
type User struct { |
|
Name string |
|
Pass string |
|
Uid int |
|
Gid int |
|
Gecos string |
|
Home string |
|
Shell string |
|
} |
|
|
|
type Group struct { |
|
Name string |
|
Pass string |
|
Gid int |
|
List []string |
|
} |
|
|
|
func parseLine(line string, v ...interface{}) { |
|
if line == "" { |
|
return |
|
} |
|
|
|
parts := strings.Split(line, ":") |
|
for i, p := range parts { |
|
// Ignore cases where we don't have enough fields to populate the arguments. |
|
// Some configuration files like to misbehave. |
|
if len(v) <= i { |
|
break |
|
} |
|
|
|
// Use the type of the argument to figure out how to parse it, scanf() style. |
|
// This is legit. |
|
switch e := v[i].(type) { |
|
case *string: |
|
*e = p |
|
case *int: |
|
// "numbers", with conversion errors ignored because of some misbehaving configuration files. |
|
*e, _ = strconv.Atoi(p) |
|
case *[]string: |
|
// Comma-separated lists. |
|
if p != "" { |
|
*e = strings.Split(p, ",") |
|
} else { |
|
*e = []string{} |
|
} |
|
default: |
|
// Someone goof'd when writing code using this function. Scream so they can hear us. |
|
panic(fmt.Sprintf("parseLine only accepts {*string, *int, *[]string} as arguments! %#v is not a pointer!", e)) |
|
} |
|
} |
|
} |
|
|
|
func ParsePasswdFile(path string) ([]User, error) { |
|
passwd, err := os.Open(path) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer passwd.Close() |
|
return ParsePasswd(passwd) |
|
} |
|
|
|
func ParsePasswd(passwd io.Reader) ([]User, error) { |
|
return ParsePasswdFilter(passwd, nil) |
|
} |
|
|
|
func ParsePasswdFileFilter(path string, filter func(User) bool) ([]User, error) { |
|
passwd, err := os.Open(path) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer passwd.Close() |
|
return ParsePasswdFilter(passwd, filter) |
|
} |
|
|
|
func ParsePasswdFilter(r io.Reader, filter func(User) bool) ([]User, error) { |
|
if r == nil { |
|
return nil, fmt.Errorf("nil source for passwd-formatted data") |
|
} |
|
|
|
var ( |
|
s = bufio.NewScanner(r) |
|
out = []User{} |
|
) |
|
|
|
for s.Scan() { |
|
if err := s.Err(); err != nil { |
|
return nil, err |
|
} |
|
|
|
line := strings.TrimSpace(s.Text()) |
|
if line == "" { |
|
continue |
|
} |
|
|
|
// see: man 5 passwd |
|
// name:password:UID:GID:GECOS:directory:shell |
|
// Name:Pass:Uid:Gid:Gecos:Home:Shell |
|
// root:x:0:0:root:/root:/bin/bash |
|
// adm:x:3:4:adm:/var/adm:/bin/false |
|
p := User{} |
|
parseLine(line, &p.Name, &p.Pass, &p.Uid, &p.Gid, &p.Gecos, &p.Home, &p.Shell) |
|
|
|
if filter == nil || filter(p) { |
|
out = append(out, p) |
|
} |
|
} |
|
|
|
return out, nil |
|
} |
|
|
|
func ParseGroupFile(path string) ([]Group, error) { |
|
group, err := os.Open(path) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
defer group.Close() |
|
return ParseGroup(group) |
|
} |
|
|
|
func ParseGroup(group io.Reader) ([]Group, error) { |
|
return ParseGroupFilter(group, nil) |
|
} |
|
|
|
func ParseGroupFileFilter(path string, filter func(Group) bool) ([]Group, error) { |
|
group, err := os.Open(path) |
|
if err != nil { |
|
return nil, err |
|
} |
|
defer group.Close() |
|
return ParseGroupFilter(group, filter) |
|
} |
|
|
|
func ParseGroupFilter(r io.Reader, filter func(Group) bool) ([]Group, error) { |
|
if r == nil { |
|
return nil, fmt.Errorf("nil source for group-formatted data") |
|
} |
|
|
|
var ( |
|
s = bufio.NewScanner(r) |
|
out = []Group{} |
|
) |
|
|
|
for s.Scan() { |
|
if err := s.Err(); err != nil { |
|
return nil, err |
|
} |
|
|
|
text := s.Text() |
|
if text == "" { |
|
continue |
|
} |
|
|
|
// see: man 5 group |
|
// group_name:password:GID:user_list |
|
// Name:Pass:Gid:List |
|
// root:x:0:root |
|
// adm:x:4:root,adm,daemon |
|
p := Group{} |
|
parseLine(text, &p.Name, &p.Pass, &p.Gid, &p.List) |
|
|
|
if filter == nil || filter(p) { |
|
out = append(out, p) |
|
} |
|
} |
|
|
|
return out, nil |
|
} |
|
|
|
type ExecUser struct { |
|
Uid int |
|
Gid int |
|
Sgids []int |
|
Home string |
|
} |
|
|
|
// GetExecUserPath is a wrapper for GetExecUser. It reads data from each of the |
|
// given file paths and uses that data as the arguments to GetExecUser. If the |
|
// files cannot be opened for any reason, the error is ignored and a nil |
|
// io.Reader is passed instead. |
|
func GetExecUserPath(userSpec string, defaults *ExecUser, passwdPath, groupPath string) (*ExecUser, error) { |
|
passwd, err := os.Open(passwdPath) |
|
if err != nil { |
|
passwd = nil |
|
} else { |
|
defer passwd.Close() |
|
} |
|
|
|
group, err := os.Open(groupPath) |
|
if err != nil { |
|
group = nil |
|
} else { |
|
defer group.Close() |
|
} |
|
|
|
return GetExecUser(userSpec, defaults, passwd, group) |
|
} |
|
|
|
// GetExecUser parses a user specification string (using the passwd and group |
|
// readers as sources for /etc/passwd and /etc/group data, respectively). In |
|
// the case of blank fields or missing data from the sources, the values in |
|
// defaults is used. |
|
// |
|
// GetExecUser will return an error if a user or group literal could not be |
|
// found in any entry in passwd and group respectively. |
|
// |
|
// Examples of valid user specifications are: |
|
// * "" |
|
// * "user" |
|
// * "uid" |
|
// * "user:group" |
|
// * "uid:gid |
|
// * "user:gid" |
|
// * "uid:group" |
|
// |
|
// It should be noted that if you specify a numeric user or group id, they will |
|
// not be evaluated as usernames (only the metadata will be filled). So attempting |
|
// to parse a user with user.Name = "1337" will produce the user with a UID of |
|
// 1337. |
|
func GetExecUser(userSpec string, defaults *ExecUser, passwd, group io.Reader) (*ExecUser, error) { |
|
if defaults == nil { |
|
defaults = new(ExecUser) |
|
} |
|
|
|
// Copy over defaults. |
|
user := &ExecUser{ |
|
Uid: defaults.Uid, |
|
Gid: defaults.Gid, |
|
Sgids: defaults.Sgids, |
|
Home: defaults.Home, |
|
} |
|
|
|
// Sgids slice *cannot* be nil. |
|
if user.Sgids == nil { |
|
user.Sgids = []int{} |
|
} |
|
|
|
// Allow for userArg to have either "user" syntax, or optionally "user:group" syntax |
|
var userArg, groupArg string |
|
parseLine(userSpec, &userArg, &groupArg) |
|
|
|
// Convert userArg and groupArg to be numeric, so we don't have to execute |
|
// Atoi *twice* for each iteration over lines. |
|
uidArg, uidErr := strconv.Atoi(userArg) |
|
gidArg, gidErr := strconv.Atoi(groupArg) |
|
|
|
// Find the matching user. |
|
users, err := ParsePasswdFilter(passwd, func(u User) bool { |
|
if userArg == "" { |
|
// Default to current state of the user. |
|
return u.Uid == user.Uid |
|
} |
|
|
|
if uidErr == nil { |
|
// If the userArg is numeric, always treat it as a UID. |
|
return uidArg == u.Uid |
|
} |
|
|
|
return u.Name == userArg |
|
}) |
|
|
|
// If we can't find the user, we have to bail. |
|
if err != nil && passwd != nil { |
|
if userArg == "" { |
|
userArg = strconv.Itoa(user.Uid) |
|
} |
|
return nil, fmt.Errorf("unable to find user %s: %v", userArg, err) |
|
} |
|
|
|
var matchedUserName string |
|
if len(users) > 0 { |
|
// First match wins, even if there's more than one matching entry. |
|
matchedUserName = users[0].Name |
|
user.Uid = users[0].Uid |
|
user.Gid = users[0].Gid |
|
user.Home = users[0].Home |
|
} else if userArg != "" { |
|
// If we can't find a user with the given username, the only other valid |
|
// option is if it's a numeric username with no associated entry in passwd. |
|
|
|
if uidErr != nil { |
|
// Not numeric. |
|
return nil, fmt.Errorf("unable to find user %s: %v", userArg, ErrNoPasswdEntries) |
|
} |
|
user.Uid = uidArg |
|
|
|
// Must be inside valid uid range. |
|
if user.Uid < minId || user.Uid > maxId { |
|
return nil, ErrRange |
|
} |
|
|
|
// Okay, so it's numeric. We can just roll with this. |
|
} |
|
|
|
// On to the groups. If we matched a username, we need to do this because of |
|
// the supplementary group IDs. |
|
if groupArg != "" || matchedUserName != "" { |
|
groups, err := ParseGroupFilter(group, func(g Group) bool { |
|
// If the group argument isn't explicit, we'll just search for it. |
|
if groupArg == "" { |
|
// Check if user is a member of this group. |
|
for _, u := range g.List { |
|
if u == matchedUserName { |
|
return true |
|
} |
|
} |
|
return false |
|
} |
|
|
|
if gidErr == nil { |
|
// If the groupArg is numeric, always treat it as a GID. |
|
return gidArg == g.Gid |
|
} |
|
|
|
return g.Name == groupArg |
|
}) |
|
if err != nil && group != nil { |
|
return nil, fmt.Errorf("unable to find groups for spec %v: %v", matchedUserName, err) |
|
} |
|
|
|
// Only start modifying user.Gid if it is in explicit form. |
|
if groupArg != "" { |
|
if len(groups) > 0 { |
|
// First match wins, even if there's more than one matching entry. |
|
user.Gid = groups[0].Gid |
|
} else if groupArg != "" { |
|
// If we can't find a group with the given name, the only other valid |
|
// option is if it's a numeric group name with no associated entry in group. |
|
|
|
if gidErr != nil { |
|
// Not numeric. |
|
return nil, fmt.Errorf("unable to find group %s: %v", groupArg, ErrNoGroupEntries) |
|
} |
|
user.Gid = gidArg |
|
|
|
// Must be inside valid gid range. |
|
if user.Gid < minId || user.Gid > maxId { |
|
return nil, ErrRange |
|
} |
|
|
|
// Okay, so it's numeric. We can just roll with this. |
|
} |
|
} else if len(groups) > 0 { |
|
// Supplementary group ids only make sense if in the implicit form. |
|
user.Sgids = make([]int, len(groups)) |
|
for i, group := range groups { |
|
user.Sgids[i] = group.Gid |
|
} |
|
} |
|
} |
|
|
|
return user, nil |
|
} |
|
|
|
// GetAdditionalGroups looks up a list of groups by name or group id |
|
// against the given /etc/group formatted data. If a group name cannot |
|
// be found, an error will be returned. If a group id cannot be found, |
|
// or the given group data is nil, the id will be returned as-is |
|
// provided it is in the legal range. |
|
func GetAdditionalGroups(additionalGroups []string, group io.Reader) ([]int, error) { |
|
var groups = []Group{} |
|
if group != nil { |
|
var err error |
|
groups, err = ParseGroupFilter(group, func(g Group) bool { |
|
for _, ag := range additionalGroups { |
|
if g.Name == ag || strconv.Itoa(g.Gid) == ag { |
|
return true |
|
} |
|
} |
|
return false |
|
}) |
|
if err != nil { |
|
return nil, fmt.Errorf("Unable to find additional groups %v: %v", additionalGroups, err) |
|
} |
|
} |
|
|
|
gidMap := make(map[int]struct{}) |
|
for _, ag := range additionalGroups { |
|
var found bool |
|
for _, g := range groups { |
|
// if we found a matched group either by name or gid, take the |
|
// first matched as correct |
|
if g.Name == ag || strconv.Itoa(g.Gid) == ag { |
|
if _, ok := gidMap[g.Gid]; !ok { |
|
gidMap[g.Gid] = struct{}{} |
|
found = true |
|
break |
|
} |
|
} |
|
} |
|
// we asked for a group but didn't find it. let's check to see |
|
// if we wanted a numeric group |
|
if !found { |
|
gid, err := strconv.Atoi(ag) |
|
if err != nil { |
|
return nil, fmt.Errorf("Unable to find group %s", ag) |
|
} |
|
// Ensure gid is inside gid range. |
|
if gid < minId || gid > maxId { |
|
return nil, ErrRange |
|
} |
|
gidMap[gid] = struct{}{} |
|
} |
|
} |
|
gids := []int{} |
|
for gid := range gidMap { |
|
gids = append(gids, gid) |
|
} |
|
return gids, nil |
|
} |
|
|
|
// GetAdditionalGroupsPath is a wrapper around GetAdditionalGroups |
|
// that opens the groupPath given and gives it as an argument to |
|
// GetAdditionalGroups. |
|
func GetAdditionalGroupsPath(additionalGroups []string, groupPath string) ([]int, error) { |
|
group, err := os.Open(groupPath) |
|
if err == nil { |
|
defer group.Close() |
|
} |
|
return GetAdditionalGroups(additionalGroups, group) |
|
}
|
|
|