Initial commit
This commit is contained in:
commit
86c46cf9fa
19
License.txt
Normal file
19
License.txt
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2023 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
27
README.md
Normal file
27
README.md
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# go-unixaccounts
|
||||||
|
A simple UNIX account information parser of /etc/passwd and /etc/group for GoLang.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
go get github.com/grmrgecko/go-unixaccounts
|
||||||
|
|
||||||
|
## Example
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
UNIXAccounts "github.com/grmrgecko/go-unixaccounts"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
accounts := UNIXAccounts.NewUNIXAccounts()
|
||||||
|
|
||||||
|
user := accounts.UserWithName("root")
|
||||||
|
groups := accounts.UserMemberOf(user)
|
||||||
|
|
||||||
|
var groupNames []string
|
||||||
|
for _, group := range groups {
|
||||||
|
groupNames = append(groupNames, group.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Found groups root is a member of:", groupNames)
|
||||||
|
}
|
||||||
|
```
|
6
test/group
Normal file
6
test/group
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
root:x:0:
|
||||||
|
bin:x:1:
|
||||||
|
daemon:x:2:
|
||||||
|
cdrom:x:11:test,root
|
||||||
|
# Test group comment
|
||||||
|
test:x:1001:
|
5
test/invalid-group
Normal file
5
test/invalid-group
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
root:x:0:
|
||||||
|
bin:x:1::test
|
||||||
|
daemon:x:2:
|
||||||
|
cdrom:x:11:test,root
|
||||||
|
test:x:1001:
|
4
test/invalid-passwd
Normal file
4
test/invalid-passwd
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
root:x:0:0:root@ipa-node4-p01.lan3.us-midwest-2.lwinternal.com:/root:/bin/bash
|
||||||
|
bin:x:1:1:bin:/bin:/sbin/nologin
|
||||||
|
daemon:x:2:2:daemon:/sbin:/sbin/nologin:hi
|
||||||
|
test:x:1000:1001:Test User:/home/test:/bin/bash
|
5
test/passwd
Normal file
5
test/passwd
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
root:x:0:0:root@ipa-node4-p01.lan3.us-midwest-2.lwinternal.com:/root:/bin/bash
|
||||||
|
bin:x:1:1:bin:/bin:/sbin/nologin
|
||||||
|
daemon:x:2:2:daemon:/sbin:/sbin/nologin
|
||||||
|
# Test user comment
|
||||||
|
test:x:1000:1001:Test User:/home/test:/bin/bash
|
257
unixAccounts.go
Normal file
257
unixAccounts.go
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
package UNIXAccounts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Base accounts structure and configuration.
|
||||||
|
type UNIXAccounts struct {
|
||||||
|
Groups []*UNIXGroup
|
||||||
|
Users []*UNIXUser
|
||||||
|
PasswdPath string
|
||||||
|
GroupPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the /etc/group and /etc/passwd files to parse information.
|
||||||
|
func NewUNIXAccounts() (*UNIXAccounts, error) {
|
||||||
|
u := &UNIXAccounts{
|
||||||
|
PasswdPath: "/etc/passwd",
|
||||||
|
GroupPath: "/etc/group",
|
||||||
|
}
|
||||||
|
err := u.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group data structure.
|
||||||
|
type UNIXGroup struct {
|
||||||
|
Name string
|
||||||
|
ID int
|
||||||
|
Users []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// User data structure.
|
||||||
|
type UNIXUser struct {
|
||||||
|
Name string
|
||||||
|
ID int
|
||||||
|
GID int
|
||||||
|
FullName string
|
||||||
|
HomeDir string
|
||||||
|
Shell string
|
||||||
|
Disabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse: Parse unix accounts and groups.
|
||||||
|
func (u *UNIXAccounts) Parse() error {
|
||||||
|
// Remove any previously parsed users to re-parse.
|
||||||
|
u.Groups = nil
|
||||||
|
u.Users = nil
|
||||||
|
|
||||||
|
// Open the group file.
|
||||||
|
groupFile, err := os.Open(u.GroupPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer groupFile.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(groupFile)
|
||||||
|
scanner.Split(bufio.ScanLines)
|
||||||
|
lineCount := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
// Read a line.
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// Ignore comments.
|
||||||
|
if line[0] == '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields are separated with a :.
|
||||||
|
fields := strings.Split(line, ":")
|
||||||
|
|
||||||
|
// Groups should have 4 fields. Nothing more, nothing less.
|
||||||
|
if len(fields) != 4 {
|
||||||
|
return fmt.Errorf("unexpected field count in group file on line %d", lineCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse information.
|
||||||
|
group := new(UNIXGroup)
|
||||||
|
group.Name = fields[0]
|
||||||
|
group.ID, _ = strconv.Atoi(fields[2])
|
||||||
|
group.Users = strings.Split(fields[3], ",")
|
||||||
|
|
||||||
|
// Add group to array.
|
||||||
|
u.Groups = append(u.Groups, group)
|
||||||
|
|
||||||
|
// Increment line count.
|
||||||
|
lineCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the user file.
|
||||||
|
passwdFile, err := os.Open(u.PasswdPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer passwdFile.Close()
|
||||||
|
|
||||||
|
scanner = bufio.NewScanner(passwdFile)
|
||||||
|
scanner.Split(bufio.ScanLines)
|
||||||
|
lineCount = 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
// Read a line.
|
||||||
|
line := scanner.Text()
|
||||||
|
|
||||||
|
// Ignore comments.
|
||||||
|
if line[0] == '#' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields are separated with a :.
|
||||||
|
fields := strings.Split(line, ":")
|
||||||
|
|
||||||
|
// Users have 7 fields. No more or less.
|
||||||
|
if len(fields) != 7 {
|
||||||
|
return fmt.Errorf("unexpected field count in passwd file on line %d", lineCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prase information.
|
||||||
|
user := new(UNIXUser)
|
||||||
|
user.Name = fields[0]
|
||||||
|
user.ID, _ = strconv.Atoi(fields[2])
|
||||||
|
user.GID, _ = strconv.Atoi(fields[3])
|
||||||
|
user.FullName = fields[4]
|
||||||
|
user.HomeDir = filepath.Clean(fields[5])
|
||||||
|
user.Shell = fields[6]
|
||||||
|
|
||||||
|
// A user is disabled if their shell is set to nologin or false. Users with no shell should also be disabled.
|
||||||
|
user.Disabled = false
|
||||||
|
if strings.Contains(user.Shell, "nologin") {
|
||||||
|
user.Disabled = true
|
||||||
|
}
|
||||||
|
if strings.Contains(user.Shell, "false") {
|
||||||
|
user.Disabled = true
|
||||||
|
}
|
||||||
|
if user.Shell == "" {
|
||||||
|
user.Disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user to array.
|
||||||
|
u.Users = append(u.Users, user)
|
||||||
|
|
||||||
|
// Increment line count.
|
||||||
|
lineCount++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user info for ID.
|
||||||
|
func (u *UNIXAccounts) UserWithID(id int) *UNIXUser {
|
||||||
|
for _, user := range u.Users {
|
||||||
|
if user.ID == id {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find user info for name.
|
||||||
|
func (u *UNIXAccounts) UserWithName(name string) *UNIXUser {
|
||||||
|
for _, user := range u.Users {
|
||||||
|
if user.Name == name {
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find group info for ID.
|
||||||
|
func (u *UNIXAccounts) GroupWithID(id int) *UNIXGroup {
|
||||||
|
for _, group := range u.Groups {
|
||||||
|
if group.ID == id {
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find group info for name.
|
||||||
|
func (u *UNIXAccounts) GroupWithName(name string) *UNIXGroup {
|
||||||
|
for _, group := range u.Groups {
|
||||||
|
if group.Name == name {
|
||||||
|
return group
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all user accounts which are members of a group.
|
||||||
|
func (u *UNIXAccounts) UsersInGroup(group *UNIXGroup) []*UNIXUser {
|
||||||
|
var users []*UNIXUser
|
||||||
|
// Users with the Group ID set to the group's ID are a member.
|
||||||
|
for _, user := range u.Users {
|
||||||
|
if user.GID == group.ID {
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Find user info for each member.
|
||||||
|
for _, name := range group.Users {
|
||||||
|
user := u.UserWithName(name)
|
||||||
|
if user == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// If the member was added previously, we do not want duplicates.
|
||||||
|
alreadyExists := false
|
||||||
|
for _, usr := range users {
|
||||||
|
if usr == user {
|
||||||
|
alreadyExists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyExists {
|
||||||
|
// The member is not a duplicate, so we add it to the array.
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return users
|
||||||
|
}
|
||||||
|
|
||||||
|
// List of groups a user is a member of.
|
||||||
|
func (u *UNIXAccounts) UserMemberOf(user *UNIXUser) []*UNIXGroup {
|
||||||
|
var groups []*UNIXGroup
|
||||||
|
|
||||||
|
// Look at each group and check if this user is a member.
|
||||||
|
for _, group := range u.Groups {
|
||||||
|
// If the GID of the user is this group, add it.
|
||||||
|
if group.ID == user.GID {
|
||||||
|
groups = append(groups, group)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each user assigned to this group and add the group if the user matches.
|
||||||
|
for _, thisUser := range group.Users {
|
||||||
|
if thisUser == user.Name {
|
||||||
|
// If the group was added previously, we do not want duplicates.
|
||||||
|
alreadyExists := false
|
||||||
|
for _, grp := range groups {
|
||||||
|
if grp == group {
|
||||||
|
alreadyExists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !alreadyExists {
|
||||||
|
// The group is not a duplicate, so we add it to the array.
|
||||||
|
groups = append(groups, group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
72
unixAccounts_test.go
Normal file
72
unixAccounts_test.go
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
package UNIXAccounts
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestAccounts(t *testing.T) {
|
||||||
|
u := &UNIXAccounts{
|
||||||
|
PasswdPath: "test/passwd",
|
||||||
|
GroupPath: "test/group",
|
||||||
|
}
|
||||||
|
err := u.Parse()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error parsing: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := u.UserWithID(2)
|
||||||
|
if user == nil || user.Name != "daemon" {
|
||||||
|
t.Error("unexpected user found by id")
|
||||||
|
}
|
||||||
|
|
||||||
|
user = u.UserWithName("test")
|
||||||
|
if user == nil || user.ID != 1000 {
|
||||||
|
t.Error("unexpected user found by name")
|
||||||
|
}
|
||||||
|
|
||||||
|
group := u.GroupWithID(1)
|
||||||
|
if group == nil || group.Name != "bin" {
|
||||||
|
t.Error("unexpected group found by id")
|
||||||
|
}
|
||||||
|
|
||||||
|
group = u.GroupWithName("cdrom")
|
||||||
|
if group == nil || group.ID != 11 {
|
||||||
|
t.Error("unexpected group found by name")
|
||||||
|
}
|
||||||
|
|
||||||
|
users := u.UsersInGroup(group)
|
||||||
|
if len(users) != 2 {
|
||||||
|
t.Error("unexpected user count found")
|
||||||
|
}
|
||||||
|
for _, usr := range users {
|
||||||
|
if usr.Name != "root" && usr.Name != "test" {
|
||||||
|
t.Errorf("found unexpected user in group: %s", usr.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groups := u.UserMemberOf(user)
|
||||||
|
if len(groups) != 2 {
|
||||||
|
t.Error("unexpected group count found")
|
||||||
|
}
|
||||||
|
for _, grp := range groups {
|
||||||
|
if grp.Name != "test" && grp.Name != "cdrom" {
|
||||||
|
t.Errorf("found unexpected group found by user: %s", grp.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
u = &UNIXAccounts{
|
||||||
|
PasswdPath: "test/invalid-passwd",
|
||||||
|
GroupPath: "test/group",
|
||||||
|
}
|
||||||
|
err = u.Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected parse to fail, but it succeeded.")
|
||||||
|
}
|
||||||
|
|
||||||
|
u = &UNIXAccounts{
|
||||||
|
PasswdPath: "test/passwd",
|
||||||
|
GroupPath: "test/invalid-group",
|
||||||
|
}
|
||||||
|
err = u.Parse()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected parse to fail, but it succeeded.")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user