Skip to content

Commit 4635962

Browse files
committed
add GetSeUserByName
Signed-off-by: Andrew LeFevre <[email protected]>
1 parent f2424d8 commit 4635962

File tree

4 files changed

+259
-0
lines changed

4 files changed

+259
-0
lines changed

go-selinux/selinux.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,13 @@ func DisableSecOpt() []string {
305305
return []string{"disable"}
306306
}
307307

308+
// GetSeUserByName retrieves the SELinux username and security level for a given
309+
// Linux username. The username and security level is based on the
310+
// /etc/selinux/{SELINUXTYPE}/seusers file.
311+
func GetSeUserByName(username string) (seUser string, level string, err error) {
312+
return getSeUserByName(username)
313+
}
314+
308315
// GetDefaultContextWithLevel gets a single context for the specified SELinux user
309316
// identity that is reachable from the specified scon context. The context is based
310317
// on the per-user /etc/selinux/{SELINUXTYPE}/contexts/users/<username> if it exists,

go-selinux/selinux_linux.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,117 @@ func dupSecOpt(src string) ([]string, error) {
11811181
return dup, nil
11821182
}
11831183

1184+
// checkGroup returns true if group's GID is in the list of GIDs gids.
1185+
func checkGroup(group string, gids []string, lookupGroup func(string) (*user.Group, error)) bool {
1186+
grp, err := lookupGroup(group)
1187+
if err != nil {
1188+
return false
1189+
}
1190+
1191+
for _, gid := range gids {
1192+
if grp.Gid == gid {
1193+
return true
1194+
}
1195+
}
1196+
return false
1197+
}
1198+
1199+
// getSeUserFromReader reads the seusers file: https://www.man7.org/linux/man-pages/man5/seusers.5.html
1200+
func getSeUserFromReader(username string, gids []string, r io.Reader, lookupGroup func(string) (*user.Group, error)) (seUser string, level string, err error) {
1201+
var defaultSeUser, defaultLevel string
1202+
var groupSeUser, groupLevel string
1203+
1204+
lineNum := -1
1205+
scanner := bufio.NewScanner(r)
1206+
for scanner.Scan() {
1207+
line := scanner.Text()
1208+
lineNum++
1209+
1210+
// remove any trailing comments, then extra whitespace
1211+
parts := strings.SplitN(line, "#", 2)
1212+
line = strings.TrimSpace(parts[0])
1213+
if line == "" {
1214+
continue
1215+
}
1216+
1217+
parts = strings.SplitN(line, ":", 3)
1218+
if len(parts) < 2 {
1219+
return "", "", fmt.Errorf("line %d: malformed line", lineNum)
1220+
}
1221+
userField := parts[0]
1222+
if userField == "" {
1223+
return "", "", fmt.Errorf("line %d: user_id or group_id is empty", lineNum)
1224+
}
1225+
seUserField := parts[1]
1226+
if seUserField == "" {
1227+
return "", "", fmt.Errorf("line %d: seuser_id is empty", lineNum)
1228+
}
1229+
var levelField string
1230+
// level is optional
1231+
if len(parts) > 2 {
1232+
levelField = parts[2]
1233+
}
1234+
1235+
// we found a match, return it
1236+
if userField == username {
1237+
return seUserField, levelField, nil
1238+
}
1239+
1240+
// if the first field starts with '%' it's a group, check if
1241+
// the user is a member of that group and set the group
1242+
// SELinux user and level if so
1243+
if userField[0] == '%' && groupSeUser == "" {
1244+
if checkGroup(userField[1:], gids, lookupGroup) {
1245+
groupSeUser = seUserField
1246+
groupLevel = levelField
1247+
}
1248+
} else if userField == "__default__" && defaultSeUser == "" {
1249+
defaultSeUser = seUserField
1250+
defaultLevel = levelField
1251+
}
1252+
}
1253+
if err := scanner.Err(); err != nil {
1254+
return "", "", fmt.Errorf("failed to read seusers file: %w", err)
1255+
}
1256+
1257+
if groupSeUser != "" {
1258+
return groupSeUser, groupLevel, nil
1259+
}
1260+
if defaultSeUser != "" {
1261+
return defaultSeUser, defaultLevel, nil
1262+
}
1263+
1264+
return "", "", fmt.Errorf("could not find SELinux user for %q login", username)
1265+
}
1266+
1267+
// getSeUserByName returns an SELinux user and MLS level that is
1268+
// mapped to a given Linux user.
1269+
func getSeUserByName(username string) (seUser string, level string, err error) {
1270+
seUsersConf := filepath.Join(policyRoot(), "seusers")
1271+
confFile, err := os.Open(seUsersConf)
1272+
if err != nil {
1273+
return "", "", fmt.Errorf("failed to open seusers file: %w", err)
1274+
}
1275+
defer confFile.Close()
1276+
1277+
usr, err := user.Lookup(username)
1278+
if err != nil {
1279+
return "", "", fmt.Errorf("failed to lookup user %q", username)
1280+
}
1281+
gids, err := usr.GroupIds()
1282+
if err != nil {
1283+
return "", "", fmt.Errorf("failed to find user %q's groups", username)
1284+
}
1285+
gids = append([]string{usr.Gid}, gids...)
1286+
1287+
seUser, level, err = getSeUserFromReader(username, gids, confFile, user.LookupGroup)
1288+
if err != nil {
1289+
return "", "", fmt.Errorf("failed to parse seusers file: %w", err)
1290+
}
1291+
1292+
return seUser, level, nil
1293+
}
1294+
11841295
// findUserInContext scans the reader for a valid SELinux context
11851296
// match that is verified with the verifier. Invalid contexts are
11861297
// skipped. It returns a matched context or an empty string if no

go-selinux/selinux_linux_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,143 @@ func TestGlbLub(t *testing.T) {
580580
}
581581
}
582582

583+
func TestGetSeUser(t *testing.T) {
584+
lookupGroup := func(string) (*user.Group, error) {
585+
return &user.Group{
586+
Gid: "42",
587+
Name: "group",
588+
}, nil
589+
}
590+
591+
tests := []struct {
592+
name string
593+
username string
594+
gids []string
595+
seUserBuf string
596+
seUser string
597+
level string
598+
expectedErr string
599+
}{
600+
{
601+
name: "one entry match",
602+
username: "bob",
603+
seUserBuf: "bob:staff_u:s0",
604+
seUser: "staff_u",
605+
level: "s0",
606+
},
607+
{
608+
name: "match with no level",
609+
username: "bob",
610+
seUserBuf: "bob:staff_u",
611+
seUser: "staff_u",
612+
},
613+
{
614+
name: "match",
615+
username: "bob",
616+
seUserBuf: `
617+
system_u:system_u:s0-s15:c0.c255
618+
root:root:s0-s15:c0.c255
619+
bob:staff_u:s0-s15:c0.c255`,
620+
seUser: "staff_u",
621+
level: "s0-s15:c0.c255",
622+
},
623+
{
624+
name: "match with comment",
625+
username: "bob",
626+
seUserBuf: `
627+
system_u:system_u:s0-s15:c0.c255
628+
# foobar
629+
root:root:s0-s15:c0.c255
630+
bob:staff_u:s0-s15:c0.c255 #baz`,
631+
seUser: "staff_u",
632+
level: "s0-s15:c0.c255",
633+
},
634+
{
635+
name: "no match",
636+
username: "bob",
637+
seUserBuf: `
638+
system_u:system_u:s0-s15:c0.c255
639+
root:root:s0-s15:c0.c255`,
640+
expectedErr: `could not find SELinux user for "bob" login`,
641+
},
642+
{
643+
name: "group match",
644+
username: "bob",
645+
gids: []string{"42"},
646+
seUserBuf: `
647+
system_u:system_u:s0-s15:c0.c255
648+
root:root:s0-s15:c0.c255
649+
%group:staff_u:s0`,
650+
seUser: "staff_u",
651+
level: "s0",
652+
},
653+
{
654+
name: "no group match",
655+
username: "bob",
656+
gids: []string{"99"},
657+
seUserBuf: `
658+
system_u:system_u:s0-s15:c0.c255
659+
root:root:s0-s15:c0.c255
660+
%group:staff_u:s0`,
661+
expectedErr: `could not find SELinux user for "bob" login`,
662+
},
663+
{
664+
name: "malformed line",
665+
username: "bob",
666+
seUserBuf: `
667+
system_u:system_u:s0-s15:c0.c255
668+
root:root:s0-s15:c0.c255
669+
foobar
670+
bob:staff_u:s0-s15:c0.c255`,
671+
expectedErr: "line 3: malformed line",
672+
},
673+
{
674+
name: "empty user",
675+
username: "bob",
676+
seUserBuf: `
677+
system_u:system_u:s0-s15:c0.c255
678+
root:root:s0-s15:c0.c255
679+
:seuser_u
680+
bob:staff_u:s0-s15:c0.c255`,
681+
expectedErr: "line 3: user_id or group_id is empty",
682+
},
683+
{
684+
name: "empty seuser",
685+
username: "bob",
686+
seUserBuf: `
687+
system_u:system_u:s0-s15:c0.c255
688+
root:root:s0-s15:c0.c255
689+
user::s0
690+
bob:staff_u:s0-s15:c0.c255`,
691+
expectedErr: "line 3: seuser_id is empty",
692+
},
693+
}
694+
695+
for _, tt := range tests {
696+
t.Run(tt.name, func(t *testing.T) {
697+
698+
r := bytes.NewBufferString(tt.seUserBuf)
699+
seUser, level, err := getSeUserFromReader(tt.username, tt.gids, r, lookupGroup)
700+
if tt.expectedErr != "" {
701+
if err == nil {
702+
t.Fatal("expected an error but got nil")
703+
} else if err.Error() != tt.expectedErr {
704+
t.Fatalf("got error: %q but expected %q", err.Error(), tt.expectedErr)
705+
}
706+
} else if tt.expectedErr == "" && err != nil {
707+
t.Fatalf("err should not exist but is: %v", err)
708+
}
709+
710+
if seUser != tt.seUser {
711+
t.Fatalf("got seUser: %q but expected %q", seUser, tt.seUser)
712+
}
713+
if level != tt.level {
714+
t.Fatalf("got level: %q but expected %q", level, tt.level)
715+
}
716+
})
717+
}
718+
}
719+
583720
func TestContextWithLevel(t *testing.T) {
584721
want := "bob:sysadm_r:sysadm_t:SystemLow-SystemHigh"
585722

go-selinux/selinux_stub.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,10 @@ func dupSecOpt(string) ([]string, error) {
146146
return nil, nil
147147
}
148148

149+
func getSeUserByName(string) (string, string, error) {
150+
return "", "", nil
151+
}
152+
149153
func getDefaultContextWithLevel(string, string, string) (string, error) {
150154
return "", nil
151155
}

0 commit comments

Comments
 (0)