-
Notifications
You must be signed in to change notification settings - Fork 13
/
htgroup.go
135 lines (114 loc) · 3.44 KB
/
htgroup.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Package htpasswd groups provides an autorisation mechanism using Apache-style group files.
//
// An Apache group file looks like this:
// users: user1 user2 user3
// admins: user1
//
// Basic usage of this package:
//
// userGroups, groupLoadErr := htgroup.NewGroups("./my-group-file", nil)
// ok := userGroups.IsUserInGroup(username, "admins")
package htpasswd
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"sync/atomic"
)
// Data structure for users and theirs groups (map).
// The map key is the user, the value is an array of groups.
type userGroupMap map[string][]string
// A HTGroup encompasses an Apache-style group file.
type HTGroup struct {
filePath string
userGroups atomic.Pointer[userGroupMap]
}
// NewGroups creates a HTGroup from an Apache-style group file.
//
// The filename must exist and be accessible to the process, as well as being a valid group file.
//
// bad is a function, which if not nil will be called for each malformed or rejected entry in the group file.
func NewGroups(filename string, bad BadLineHandler) (*HTGroup, error) {
htGroup := HTGroup{
filePath: filename,
}
return &htGroup, htGroup.ReloadGroups(bad)
}
// NewGroupsFromReader is like NewGroups but reads from r instead of a named file.
func NewGroupsFromReader(r io.Reader, bad BadLineHandler) (*HTGroup, error) {
htGroup := HTGroup{}
readFileErr := htGroup.ReloadGroupsFromReader(r, bad)
if readFileErr != nil {
return nil, readFileErr
}
return &htGroup, nil
}
// ReloadGroups rereads the group file.
func (htGroup *HTGroup) ReloadGroups(bad BadLineHandler) error {
file, err := os.Open(htGroup.filePath)
if err != nil {
return err
}
defer file.Close()
return htGroup.ReloadGroupsFromReader(file, bad)
}
// ReloadGroupsFromReader rereads the group file from a Reader.
func (htGroup *HTGroup) ReloadGroupsFromReader(r io.Reader, bad BadLineHandler) error {
userGroups := make(userGroupMap)
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
if lineErr := processLine(&userGroups, line); lineErr != nil && bad != nil {
bad(lineErr)
}
}
if scannerErr := scanner.Err(); scannerErr != nil {
return fmt.Errorf("Error scanning group file: %s", scannerErr.Error())
}
htGroup.userGroups.Store(&userGroups)
return nil
}
func processLine(userGroups *userGroupMap, rawLine string) error {
line := strings.TrimSpace(rawLine)
if line == "" {
return nil
}
groupAndUsers := strings.SplitN(line, ":", 2)
if len(groupAndUsers) != 2 {
return fmt.Errorf("malformed line, no colon: %s", line)
}
var group = strings.TrimSpace(groupAndUsers[0])
var users = strings.Fields(groupAndUsers[1])
for _, user := range users {
if (*userGroups)[user] == nil {
(*userGroups)[user] = []string{}
}
(*userGroups)[user] = append((*userGroups)[user], group)
}
return nil
}
// IsUserInGroup checks whether the user is in a group.
// Returns true of user is in that group, otherwise false.
func (htGroup *HTGroup) IsUserInGroup(user string, group string) bool {
groups := htGroup.GetUserGroups(user)
return containsGroup(groups, group)
}
// GetUserGroups reads all groups of a user.
// Returns all groups as a string array or an empty array.
func (htGroup *HTGroup) GetUserGroups(user string) []string {
groups := (*htGroup.userGroups.Load())[user]
if groups == nil {
return []string{}
}
return groups
}
func containsGroup(groups []string, group string) bool {
for _, g := range groups {
if g == group {
return true
}
}
return false
}