Initial commit
This commit is contained in:
commit
2d6bc4d3b7
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"
|
||||
"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