/
entities.go
188 lines (166 loc) · 6.38 KB
/
entities.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
/*******************************************************************************
*
* Copyright 2015 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 main
import (
"bytes"
"regexp"
"strings"
"github.com/BurntSushi/toml"
"github.com/holocm/libpackagebuild/filesystem"
)
//This file contains the parts of parser.go relating to the support for entity
//definitions (users and groups). As part of the initial parsing and validation
//process, these definitions are converted into an file entry in the package
//containing the entity definition file, so that other parts of holo-build do
//not need to know about entity definitions at all.
//UserSection only needs a nice exported name for the TOML parser to
//produce more meaningful error messages on malformed input data.
type UserSection struct {
Name string `toml:"name"`
Comment string `toml:"comment"`
UID uint32 `toml:"uid"`
System bool `toml:"system"`
Home string `toml:"home"`
Group string `toml:"group"`
Groups []string `toml:"groups"`
Shell string `toml:"shell"`
}
//GroupSection only needs a nice exported name for the TOML parser to
//produce more meaningful error messages on malformed input data.
type GroupSection struct {
Name string `toml:"name"`
Gid uint32 `toml:"gid"`
System bool `toml:"system"`
}
//this regexp copied from useradd(8) manpage
var userOrGroupRx = regexp.MustCompile(`^[a-z_][a-z0-9_-]*\$?$`)
//parseUserOrGroupRef is used for references to users/groups in FS entries.
//Those references can either be an integer ID or a string name.
func parseUserOrGroupRef(value interface{}, ec *ErrorCollector, entryDesc string) *filesystem.IntOrString {
//default value
if value == nil {
return nil
}
switch val := value.(type) {
case int64:
if val < 0 {
ec.Addf("%s is invalid: user or group ID \"%d\" may not be negative", entryDesc, val)
}
if val >= 1<<32 {
ec.Addf("%s is invalid: user or group ID \"%d\" does not fit in uint32", entryDesc, val)
}
return &filesystem.IntOrString{Int: uint32(val)}
case string:
if !userOrGroupRx.MatchString(val) {
ec.Addf("%s is invalid: \"%s\" is not an acceptable user or group name", entryDesc, val)
}
return &filesystem.IntOrString{Str: val}
default:
ec.Addf("%s is invalid: \"owner\"/\"group\" attributes must be strings or integers, found type %T", entryDesc, value)
return nil
}
}
var definitionFileRx = regexp.MustCompile(`^/usr/share/holo/users-groups/[^/]+.toml$`)
func compileEntityDefinitions(pkg PackageSection, groups []GroupSection, users []UserSection, ec *ErrorCollector) (node filesystem.Node, path string) {
//only add an entity definition file if it is required
if len(groups) == 0 && len(users) == 0 {
return nil, ""
}
//needs a valid definition file name
path = pkg.DefinitionFile
switch {
case path == "":
path = "/usr/share/holo/users-groups/" + pkg.Name + ".toml"
case !definitionFileRx.MatchString(path):
ec.Addf("\"%s\" is not an acceptable definition file (should look like \"/usr/share/holo/users-groups/01-foo.toml\")", path)
path = "" //indicate broken path to caller
default:
WarnDeprecatedKey("package.definitionFile")
}
//validate users/groups
for idx, group := range groups {
validateGroup(group, ec, idx)
}
for idx, user := range users {
validateUser(user, ec, idx)
}
//encode into a definition file
s := struct {
Group []GroupSection `toml:"group"`
User []UserSection `toml:"user"`
}{groups, users}
var buf bytes.Buffer
err := toml.NewEncoder(&buf).Encode(&s)
if err != nil {
ec.Addf("encoding of \"%s\" failed: %s", path, err.Error())
return nil, ""
}
//toml.Encode does not support the omitempty flag yet, so remove unset fields manually
pruneRx := regexp.MustCompile(`(?m:^\s*[a-z]+ = (?:0|""|false)$)\n`)
content := pruneRx.ReplaceAllString(string(buf.Bytes()), "")
return &filesystem.RegularFile{
Content: content,
Metadata: filesystem.NodeMetadata{Mode: 0644},
}, path
}
func validateGroup(group GroupSection, ec *ErrorCollector, entryIdx int) {
//check group name
switch {
case group.Name == "":
ec.Addf("group %d is invalid: missing \"name\" attribute", entryIdx)
case !userOrGroupRx.MatchString(group.Name):
ec.Addf("group \"%s\" is invalid: name is not an acceptable group name", group.Name)
}
//if GID is given, "system" attribute is useless since it's only used to choose a GID
if group.System && group.Gid != 0 {
ec.Addf("group \"%s\" is invalid: if \"gid\" is given, then \"system\" is useless", group.Name)
}
}
func validateUser(user UserSection, ec *ErrorCollector, entryIdx int) {
//check user name
switch {
case user.Name == "":
ec.Addf("user %d is invalid: missing \"name\" attribute", entryIdx)
case !userOrGroupRx.MatchString(user.Name):
ec.Addf("user \"%s\" is invalid: name is not an acceptable user name", user.Name)
}
//if UID is given, "system" attribute is useless since it's only used to choose a UID
if user.System && user.UID != 0 {
ec.Addf("user \"%s\" is invalid: if \"uid\" is given, then \"system\" is useless", user.Name)
}
//check groups
if user.Group != "" && !userOrGroupRx.MatchString(user.Group) {
ec.Addf("user \"%s\" is invalid: \"%s\" is not an acceptable group name", user.Name, user.Group)
}
for _, group := range user.Groups {
if !userOrGroupRx.MatchString(group) {
ec.Addf("user \"%s\" is invalid: \"%s\" is not an acceptable group name", user.Name, group)
}
}
//check home directory
if user.Home != "" {
if !strings.HasPrefix(user.Home, "/") {
ec.Addf("user \"%s\" is invalid: home directory \"%s\" must be an absolute path", user.Name, user.Home)
}
if strings.HasSuffix(user.Home, "/") {
ec.Addf("user \"%s\" is invalid: home directory \"%s\" has trailing slash(es)", user.Name, user.Home)
}
}
}