Initial commit

This commit is contained in:
GRMrGecko 2023-08-13 21:27:25 -05:00
commit 2d6bc4d3b7
9 changed files with 398 additions and 0 deletions

19
License.txt Normal file
View 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
View 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"
"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)
}
```

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/GRMrGecko/go-unixaccounts
go 1.20

6
test/group Normal file
View 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
View 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
View 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
View 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
View 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
View 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.")
}
}