Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions go-selinux/selinux.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,13 @@ func DisableSecOpt() []string {
return []string{"disable"}
}

// GetSeUserByName retrieves the SELinux username and security level for a given
// Linux username. The username and security level is based on the
// /etc/selinux/{SELINUXTYPE}/seusers file.
func GetSeUserByName(username string) (seUser string, level string, err error) {
return getSeUserByName(username)
}

// GetDefaultContextWithLevel gets a single context for the specified SELinux user
// identity that is reachable from the specified scon context. The context is based
// on the per-user /etc/selinux/{SELINUXTYPE}/contexts/users/<username> if it exists,
Expand Down
111 changes: 111 additions & 0 deletions go-selinux/selinux_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,117 @@ func dupSecOpt(src string) ([]string, error) {
return dup, nil
}

// checkGroup returns true if group's GID is in the list of GIDs gids.
func checkGroup(group string, gids []string, lookupGroup func(string) (*user.Group, error)) bool {
grp, err := lookupGroup(group)
if err != nil {
return false
}

for _, gid := range gids {
if grp.Gid == gid {
return true
}
}
return false
}

// getSeUserFromReader reads the seusers file: https://www.man7.org/linux/man-pages/man5/seusers.5.html
func getSeUserFromReader(username string, gids []string, r io.Reader, lookupGroup func(string) (*user.Group, error)) (seUser string, level string, err error) {
var defaultSeUser, defaultLevel string
var groupSeUser, groupLevel string

lineNum := -1
scanner := bufio.NewScanner(r)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use bufio.NewReader and ReadBytes('\n') here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can change it, but curious as to why? Is bufio.Reader more efficient in this scenario?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the change in a separate commit so we can easily revert it, I personally think using bufio.NewScanner is a bit easier to follow and use, but if there's a good reason to use bufio.Reader we can keep it as is

for scanner.Scan() {
line := scanner.Text()
lineNum++

// remove any trailing comments, then extra whitespace
parts := strings.SplitN(line, "#", 2)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are trailing comments allowed? In C code they ignore empty lines, or lines consisting entirely of whitespace, or lines that start with (optional) whitespace and then #. I don't see any trailing comments handling.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I tested before they seemed to be allowed, didn't get any errors from anything when I added them and everything kept working as it should

line = strings.TrimSpace(parts[0])
if line == "" {
continue
}

parts = strings.SplitN(line, ":", 3)
if len(parts) < 2 {
return "", "", fmt.Errorf("line %d: malformed line", lineNum)
}
userField := parts[0]
if userField == "" {
return "", "", fmt.Errorf("line %d: user_id or group_id is empty", lineNum)
}
seUserField := parts[1]
if seUserField == "" {
return "", "", fmt.Errorf("line %d: seuser_id is empty", lineNum)
}
var levelField string
// level is optional
if len(parts) > 2 {
levelField = parts[2]
}

// we found a match, return it
if userField == username {
return seUserField, levelField, nil
}

// if the first field starts with '%' it's a group, check if
// the user is a member of that group and set the group
// SELinux user and level if so
if userField[0] == '%' && groupSeUser == "" {
if checkGroup(userField[1:], gids, lookupGroup) {
groupSeUser = seUserField
groupLevel = levelField
}
} else if userField == "__default__" && defaultSeUser == "" {
defaultSeUser = seUserField
defaultLevel = levelField
}
}
if err := scanner.Err(); err != nil {
return "", "", fmt.Errorf("failed to read seusers file: %w", err)
}

if groupSeUser != "" {
return groupSeUser, groupLevel, nil
}
if defaultSeUser != "" {
return defaultSeUser, defaultLevel, nil
}

return "", "", fmt.Errorf("could not find SELinux user for %q login", username)
}

// getSeUserByName returns an SELinux user and MLS level that is
// mapped to a given Linux user.
func getSeUserByName(username string) (seUser string, level string, err error) {
seUsersConf := filepath.Join(policyRoot(), "seusers")
confFile, err := os.Open(seUsersConf)
if err != nil {
return "", "", fmt.Errorf("failed to open seusers file: %w", err)
}
defer confFile.Close()

usr, err := user.Lookup(username)
if err != nil {
return "", "", fmt.Errorf("failed to lookup user %q", username)
}
gids, err := usr.GroupIds()
if err != nil {
return "", "", fmt.Errorf("failed to find user %q's groups", username)
}
gids = append([]string{usr.Gid}, gids...)

seUser, level, err = getSeUserFromReader(username, gids, confFile, user.LookupGroup)
if err != nil {
return "", "", fmt.Errorf("failed to parse seusers file: %w", err)
}

return seUser, level, nil
}

// findUserInContext scans the reader for a valid SELinux context
// match that is verified with the verifier. Invalid contexts are
// skipped. It returns a matched context or an empty string if no
Expand Down
137 changes: 137 additions & 0 deletions go-selinux/selinux_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,143 @@ func TestGlbLub(t *testing.T) {
}
}

func TestGetSeUser(t *testing.T) {
lookupGroup := func(string) (*user.Group, error) {
return &user.Group{
Gid: "42",
Name: "group",
}, nil
}

tests := []struct {
name string
username string
gids []string
seUserBuf string
seUser string
level string
expectedErr string
}{
{
name: "one entry match",
username: "bob",
seUserBuf: "bob:staff_u:s0",
seUser: "staff_u",
level: "s0",
},
{
name: "match with no level",
username: "bob",
seUserBuf: "bob:staff_u",
seUser: "staff_u",
},
{
name: "match",
username: "bob",
seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
bob:staff_u:s0-s15:c0.c255`,
seUser: "staff_u",
level: "s0-s15:c0.c255",
},
{
name: "match with comment",
username: "bob",
seUserBuf: `
system_u:system_u:s0-s15:c0.c255
# foobar
root:root:s0-s15:c0.c255
bob:staff_u:s0-s15:c0.c255 #baz`,
seUser: "staff_u",
level: "s0-s15:c0.c255",
},
{
name: "no match",
username: "bob",
seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255`,
expectedErr: `could not find SELinux user for "bob" login`,
},
{
name: "group match",
username: "bob",
gids: []string{"42"},
seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
%group:staff_u:s0`,
seUser: "staff_u",
level: "s0",
},
{
name: "no group match",
username: "bob",
gids: []string{"99"},
seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
%group:staff_u:s0`,
expectedErr: `could not find SELinux user for "bob" login`,
},
{
name: "malformed line",
username: "bob",
seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
foobar
bob:staff_u:s0-s15:c0.c255`,
expectedErr: "line 3: malformed line",
},
{
name: "empty user",
username: "bob",
seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
:seuser_u
bob:staff_u:s0-s15:c0.c255`,
expectedErr: "line 3: user_id or group_id is empty",
},
{
name: "empty seuser",
username: "bob",
seUserBuf: `
system_u:system_u:s0-s15:c0.c255
root:root:s0-s15:c0.c255
user::s0
bob:staff_u:s0-s15:c0.c255`,
expectedErr: "line 3: seuser_id is empty",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

r := bytes.NewBufferString(tt.seUserBuf)
seUser, level, err := getSeUserFromReader(tt.username, tt.gids, r, lookupGroup)
if tt.expectedErr != "" {
if err == nil {
t.Fatal("expected an error but got nil")
} else if err.Error() != tt.expectedErr {
t.Fatalf("got error: %q but expected %q", err.Error(), tt.expectedErr)
}
} else if tt.expectedErr == "" && err != nil {
t.Fatalf("err should not exist but is: %v", err)
}

if seUser != tt.seUser {
t.Fatalf("got seUser: %q but expected %q", seUser, tt.seUser)
}
if level != tt.level {
t.Fatalf("got level: %q but expected %q", level, tt.level)
}
})
}
}

func TestContextWithLevel(t *testing.T) {
want := "bob:sysadm_r:sysadm_t:SystemLow-SystemHigh"

Expand Down
4 changes: 4 additions & 0 deletions go-selinux/selinux_stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ func dupSecOpt(string) ([]string, error) {
return nil, nil
}

func getSeUserByName(string) (string, string, error) {
return "", "", nil
}

func getDefaultContextWithLevel(string, string, string) (string, error) {
return "", nil
}
Expand Down