/
passwd.go
215 lines (195 loc) · 5.69 KB
/
passwd.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/*******************************************************************************
*
* Copyright 2015-2016 Stefan Majewsky <majewsky@gmx.net>
*
* This file is part of Holo.
*
* Holo is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* Holo is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* Holo. If not, see <http://www.gnu.org/licenses/>.
*
*******************************************************************************/
package entrypoint
import (
"errors"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
var (
etcPasswdPath string
etcGroupPath string
appliedStates map[string]EntityDefinition //= nil unless during tests
)
func init() {
rootDir := os.Getenv("HOLO_ROOT_DIR")
if rootDir == "" {
rootDir = "/"
}
etcPasswdPath = filepath.Join(rootDir, "etc/passwd")
etcGroupPath = filepath.Join(rootDir, "etc/group")
if rootDir != "/" {
appliedStates = make(map[string]EntityDefinition)
}
}
//StoreAppliedState is a no-op during normal operation. During unit tests, it
//records Apply()ed definitions, so that the next GetProvisionedState() of the
//same entity will present a consistent result.
//
//The `previous` argument contains the actual state before the apply operation.
func StoreAppliedState(def EntityDefinition, previous EntityDefinition) {
if appliedStates != nil {
//mark applied states with a fake numeric ID
switch def := def.(type) {
case *GroupDefinition:
if def.GID == 0 {
def.GID = 999
}
case *UserDefinition:
if def.UID == 0 {
def.UID = 999
}
}
//merge attributes from previous actual state that were not specified
//in the newly applied state
def, _ = def.Merge(previous, MergeEmptyOnly)
appliedStates[def.EntityID()] = def
}
}
//Getent reads entries from a UNIX user/group database (e.g. /etc/passwd
//or /etc/group) and returns the first entry matching the given predicate.
//For example, to locate the user with name "foo":
//
// fields, err := Getent("/etc/passwd", func(fields []string) bool {
// return fields[0] == "foo"
// })
func Getent(databaseFile string, predicate func([]string) bool) ([]string, error) {
//read database file
contents, err := ioutil.ReadFile(databaseFile)
if err != nil {
return nil, err
}
//each entry is one line
lines := strings.Split(strings.TrimSpace(string(contents)), "\n")
for _, line := range lines {
//fields inside the entries are separated by colons
fields := strings.Split(strings.TrimSpace(line), ":")
if predicate(fields) {
return fields, nil
}
}
//no entry matches
return nil, nil
}
//GetProvisionedState implements the EntityDefinition interface.
func (g *GroupDefinition) GetProvisionedState() (EntityDefinition, error) {
//special case for test runs
if appliedStates != nil {
if def, ok := appliedStates[g.EntityID()]; ok {
return def, nil
}
}
//fetch entry from /etc/group
fields, err := Getent(etcGroupPath, func(fields []string) bool { return fields[0] == g.Name })
if err != nil {
return nil, err
}
//is there such a group?
if fields == nil {
return &GroupDefinition{Name: g.Name}, nil
}
//is the group entry intact?
if len(fields) < 4 {
return nil, errors.New("invalid entry in /etc/group (not enough fields)")
}
//read fields in entry
gid, err := strconv.Atoi(fields[2])
return &GroupDefinition{
Name: fields[0],
GID: gid,
}, err
}
//GetProvisionedState implements the EntityDefinition interface.
func (u *UserDefinition) GetProvisionedState() (EntityDefinition, error) {
//special case for test runs
if appliedStates != nil {
if def, ok := appliedStates[u.EntityID()]; ok {
return def, nil
}
}
//fetch entry from /etc/passwd
fields, err := Getent(etcPasswdPath, func(fields []string) bool { return fields[0] == u.Name })
if err != nil {
return nil, err
}
//is there such a user?
if fields == nil {
return &UserDefinition{Name: u.Name}, nil
}
//is the passwd entry intact?
if len(fields) < 4 {
return nil, errors.New("invalid entry in /etc/passwd (not enough fields)")
}
//read fields in passwd entry
actualUID, err := strconv.Atoi(fields[2])
if err != nil {
return nil, err
}
//fetch entry for login group from /etc/group (to resolve actualGID into a
//group name)
actualGIDString := fields[3]
groupFields, err := Getent(etcGroupPath, func(fields []string) bool {
if len(fields) <= 2 {
return false
}
return fields[2] == actualGIDString
})
if err != nil {
return nil, err
}
if groupFields == nil {
return nil, errors.New("invalid entry in /etc/passwd (login group does not exist)")
}
groupName := groupFields[0]
//check /etc/group for the supplementary group memberships of this user
var groupNames []string
_, err = Getent(etcGroupPath, func(fields []string) bool {
if len(fields) <= 3 {
return false
}
//collect groups that contain this user
users := strings.Split(fields[3], ",")
for _, user := range users {
if user == u.Name {
groupNames = append(groupNames, fields[0])
}
}
//keep going
return false
})
if err != nil {
return nil, err
}
//make sure that the groups list is always sorted (esp. for reproducible test output)
sort.Strings(groupNames)
return &UserDefinition{
Name: fields[0],
Comment: fields[4],
UID: actualUID,
Home: fields[5],
Group: groupName,
Groups: groupNames,
Shell: fields[6],
}, nil
}