Skip to content

Commit 4965954

Browse files
committed
fix weird behavior with auxiliary groups in user base image
By introducing a new field SkipBaseGroups.
1 parent 9a9994d commit 4965954

File tree

12 files changed

+99
-44
lines changed

12 files changed

+99
-44
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# v2.2 (TBD)
2+
3+
Changes:
4+
5+
- User definitions now have a new flag `SkipBaseGroups` which, if set, causes `holo-build` to ignore existing auxiliary
6+
groups that this user is in before the first `holo apply` of this user.
7+
- Fix a bug where user definitions previously behaved as if `SkipBaseGroups = true`, even if the intention always was to
8+
behave like `SkipBaseGroups = false` (which is the default).
9+
- Fix a bug where `user:root` could not be provisioned to because the user ID 0 was misinterpreted as "no UID".
10+
111
# v2.1 (2017-11-30)
212

313
Changes:

cmd/holo-users-groups/definition.go

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ const (
4343
MergeNumericIDOnly
4444
)
4545

46+
//SkipMethod is the third argument for EntityDefinition.Merge().
47+
type SkipMethod uint
48+
49+
const (
50+
//SkipDisabled does not skip any fields during merging.
51+
SkipDisabled SkipMethod = iota
52+
//SkipEnabled skips fields where one of the definitions instructs us to do so
53+
//(causing the other side's attribute to win).
54+
SkipEnabled
55+
)
56+
4657
//EntityDefinition contains data from a definition file that describes an entity
4758
//(a user account or group). Definitions can also be obtained by scanning the
4859
//user/group databases.
@@ -76,7 +87,7 @@ type EntityDefinition interface {
7687
//
7788
//The merge `method` tells which attributes may be merged. Possible values
7889
//are MergeWhereCompatible, MergeEmptyOnly and MergeNumericIDOnly.
79-
Merge(other EntityDefinition, method MergeMethod) (EntityDefinition, []error)
90+
Merge(other EntityDefinition, mMethod MergeMethod, sMethod SkipMethod) (EntityDefinition, []error)
8091
//Apply provisions this entity. The argument indicates the currently
8192
//provisioned state. The argument's concrete type must match the callee.
8293
Apply(provisioned EntityDefinition) error
@@ -116,14 +127,15 @@ type GroupDefinition struct {
116127

117128
//UserDefinition represents a UNIX user account (as registered in /etc/passwd).
118129
type UserDefinition struct {
119-
Name string `toml:"name"` //the user name (the first field in /etc/passwd)
120-
Comment string `toml:"comment,omitempty"` //the full name (sometimes also called "comment"; the fifth field in /etc/passwd)
121-
UID *int `toml:"uid,omitzero"` //the user ID (the third field in /etc/passwd), or nil if no specific UID is enforced
122-
System bool `toml:"system,omitempty"` //whether the group is a system group (this influences the GID selection if gid = 0)
123-
Home string `toml:"home,omitempty"` //path to the user's home directory (or empty to use the default)
124-
Group string `toml:"group,omitempty"` //the name of the user's initial login group (or empty to use the default)
125-
Groups []string `toml:"groups,omitempty"` //the names of supplementary groups which the user is also a member of
126-
Shell string `toml:"shell,omitempty"` //path to the user's login shell (or empty to use the default)
130+
Name string `toml:"name"` //the user name (the first field in /etc/passwd)
131+
Comment string `toml:"comment,omitempty"` //the full name (sometimes also called "comment"; the fifth field in /etc/passwd)
132+
UID *int `toml:"uid,omitzero"` //the user ID (the third field in /etc/passwd), or nil if no specific UID is enforced
133+
System bool `toml:"system,omitempty"` //whether the group is a system group (this influences the GID selection if gid = 0)
134+
Home string `toml:"home,omitempty"` //path to the user's home directory (or empty to use the default)
135+
Group string `toml:"group,omitempty"` //the name of the user's initial login group (or empty to use the default)
136+
Groups []string `toml:"groups,omitempty"` //the names of supplementary groups which the user is also a member of
137+
Shell string `toml:"shell,omitempty"` //path to the user's login shell (or empty to use the default)
138+
SkipBaseGroups bool `toml:"skipBaseGroups,omitempty"` //whether to consider supplementary groups in the base image during merging
127139
}
128140

129141
//TypeName implements the EntityDefinition interface.
@@ -172,7 +184,11 @@ func (u *UserDefinition) Attributes() string {
172184
attrs = append(attrs, "login group: "+u.Group)
173185
}
174186
if len(u.Groups) > 0 {
175-
attrs = append(attrs, "groups: "+strings.Join(u.Groups, ","))
187+
if u.SkipBaseGroups {
188+
attrs = append(attrs, "exact groups: "+strings.Join(u.Groups, ","))
189+
} else {
190+
attrs = append(attrs, "groups: "+strings.Join(u.Groups, ","))
191+
}
176192
}
177193
if u.Shell != "" {
178194
attrs = append(attrs, "login shell: "+u.Shell)
@@ -194,9 +210,12 @@ func (g *GroupDefinition) WithSerializableState(callback func(EntityDefinition))
194210

195211
//WithSerializableState implements the EntityDefinition interface.
196212
func (u *UserDefinition) WithSerializableState(callback func(EntityDefinition)) {
197-
//we don't want to serialize the `system` attribute in diffs etc.
213+
//we don't want to serialize the `system` and `skipBaseGroups` attributes in diffs etc.
198214
system := u.System
215+
skipBaseGroups := u.SkipBaseGroups
199216
u.System = false
217+
u.SkipBaseGroups = false
200218
callback(u)
201219
u.System = system
220+
u.SkipBaseGroups = skipBaseGroups
202221
}

cmd/holo-users-groups/entity.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*******************************************************************************
22
*
3-
* Copyright 2015-2016 Stefan Majewsky <majewsky@gmx.net>
3+
* Copyright 2015-2017 Stefan Majewsky <majewsky@gmx.net>
44
*
55
* This file is part of Holo.
66
*
@@ -110,9 +110,11 @@ func (e *Entity) Apply(withForce bool) error {
110110
//case, conflicts worthy of "--force" are detected by comparing that to the
111111
//definition)
112112
conflictCheckImage := e.Definition
113+
conflictCheckMethod := MergeWhereCompatible
113114
provisionedState, err := ProvisionedImageDir.LoadImageFor(e.Definition)
114115
if err == nil {
115-
conflictCheckImage, _ = conflictCheckImage.Merge(provisionedState, MergeEmptyOnly)
116+
conflictCheckImage, _ = conflictCheckImage.Merge(provisionedState, MergeEmptyOnly, SkipDisabled)
117+
conflictCheckMethod = MergeEmptyOnly
116118
} else {
117119
if os.IsNotExist(err) {
118120
provisionedState = nil
@@ -132,22 +134,22 @@ func (e *Entity) Apply(withForce bool) error {
132134
// actual state and provisioned state)
133135
//3. the entity has *not* been provisioned yet and the definition conflicts
134136
// with the current state (i.e. the base image)
135-
_, conflicts := actualState.Merge(conflictCheckImage, MergeEmptyOnly)
137+
_, conflicts := actualState.Merge(conflictCheckImage, conflictCheckMethod, SkipEnabled)
136138
if len(conflicts) > 0 {
137139
PrintCommandMessage("requires --force to overwrite\n")
138140
return nil
139141
}
140142
}
141143

142144
//desired state is obtained by merging the definition with the base image
143-
desiredState, _ := e.Definition.Merge(baseImage, MergeWhereCompatible)
145+
desiredState, _ := e.Definition.Merge(baseImage, MergeWhereCompatible, SkipEnabled)
144146
//but make sure that we don't see a difference just because the definition
145147
//does not define a particular attribute
146-
desiredState, _ = desiredState.Merge(actualState, MergeEmptyOnly)
148+
desiredState, _ = desiredState.Merge(actualState, MergeEmptyOnly, SkipDisabled)
147149
//but if no one knows what to do, re-use the provisioned state (this is
148150
//important when restoring an entity that was deleted by someone else)
149151
if provisionedState != nil {
150-
desiredState, _ = desiredState.Merge(provisionedState, MergeEmptyOnly)
152+
desiredState, _ = desiredState.Merge(provisionedState, MergeEmptyOnly, SkipDisabled)
151153
}
152154

153155
//check if changes are necessary
@@ -217,7 +219,7 @@ func (e *Entity) PrepareDiff() error {
217219
if baseState == nil {
218220
baseState = actualState
219221
}
220-
desiredState, _ := e.Definition.Merge(baseState, MergeWhereCompatible)
222+
desiredState, _ := e.Definition.Merge(baseState, MergeWhereCompatible, SkipDisabled)
221223
desiredPath := filepath.Join(tempDir, "desired.toml")
222224
err = SerializeDefinitionIntoFile(desiredState, desiredPath)
223225
if err != nil {

cmd/holo-users-groups/merge.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func (e MergeError) Error() string {
4242
}
4343

4444
//Merge implements the EntityDefinition interface.
45-
func (g *GroupDefinition) Merge(other EntityDefinition, method MergeMethod) (EntityDefinition, []error) {
45+
func (g *GroupDefinition) Merge(other EntityDefinition, mMethod MergeMethod, sMethod SkipMethod) (EntityDefinition, []error) {
4646
//start by cloning `other`
4747
if other.EntityID() != g.EntityID() {
4848
panic("tried to merge entities with different IDs")
@@ -59,7 +59,7 @@ func (g *GroupDefinition) Merge(other EntityDefinition, method MergeMethod) (Ent
5959
}
6060

6161
//with MergeNumericIDOnly, only the GID may be merged
62-
if method == MergeNumericIDOnly {
62+
if mMethod == MergeNumericIDOnly {
6363
//we need all the other attributes from `u`, so it's easier to just take another copy
6464
theResult := *g
6565
theResult.GID = result.GID
@@ -73,7 +73,7 @@ func (g *GroupDefinition) Merge(other EntityDefinition, method MergeMethod) (Ent
7373
}
7474

7575
//Merge implements the EntityDefinition interface.
76-
func (u *UserDefinition) Merge(other EntityDefinition, method MergeMethod) (EntityDefinition, []error) {
76+
func (u *UserDefinition) Merge(other EntityDefinition, mMethod MergeMethod, sMethod SkipMethod) (EntityDefinition, []error) {
7777
//start by cloning `other`
7878
if other.EntityID() != u.EntityID() {
7979
panic("tried to merge entities with different IDs")
@@ -91,7 +91,7 @@ func (u *UserDefinition) Merge(other EntityDefinition, method MergeMethod) (Enti
9191
}
9292

9393
//with MergeNumericIDOnly, only the UID may be merged
94-
if method == MergeNumericIDOnly {
94+
if mMethod == MergeNumericIDOnly {
9595
//we need all the other attributes from `u`, so it's easier to just take another copy
9696
theResult := *u
9797
theResult.UID = result.UID
@@ -127,7 +127,7 @@ func (u *UserDefinition) Merge(other EntityDefinition, method MergeMethod) (Enti
127127

128128
//auxiliary groups can always be added, but only under the
129129
//MergeWhereCompatible method
130-
if method == MergeEmptyOnly {
130+
if mMethod == MergeEmptyOnly {
131131
if len(result.Groups) > 0 && len(u.Groups) > 0 {
132132
sort.Strings(result.Groups)
133133
sort.Strings(u.Groups)
@@ -141,8 +141,15 @@ func (u *UserDefinition) Merge(other EntityDefinition, method MergeMethod) (Enti
141141
result.Groups = u.Groups
142142
}
143143
} else {
144-
for _, group := range u.Groups {
145-
result.Groups, _ = appendIfMissing(result.Groups, group)
144+
//auxiliary groups will not be merged if we are respecting a SkipBaseGroups flag
145+
if sMethod == SkipEnabled && u.SkipBaseGroups {
146+
result.Groups = u.Groups
147+
} else if sMethod == SkipEnabled && result.SkipBaseGroups {
148+
//do not change result.Groups
149+
} else {
150+
for _, group := range u.Groups {
151+
result.Groups, _ = appendIfMissing(result.Groups, group)
152+
}
146153
}
147154
}
148155
//make sure that the groups list is always sorted (esp. for reproducible test output)

cmd/holo-users-groups/passwd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func StoreAppliedState(def EntityDefinition, previous EntityDefinition) {
7070

7171
//merge attributes from previous actual state that were not specified
7272
//in the newly applied state
73-
def, _ = def.Merge(previous, MergeEmptyOnly)
73+
def, _ = def.Merge(previous, MergeEmptyOnly, SkipDisabled)
7474

7575
appliedStates[def.EntityID()] = def
7676
}

cmd/holo-users-groups/scan.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ func readDefinitionFile(definitionPath string, entities *map[string]*Entity) err
167167
entity, exists := (*entities)[id]
168168
if exists {
169169
//stacked definition for this entity -> merge into existing entity
170-
mergedDef, mergeErrors := def.Merge(entity.Definition, MergeWhereCompatible)
170+
mergedDef, mergeErrors := def.Merge(entity.Definition, MergeWhereCompatible, SkipDisabled)
171171
if len(mergeErrors) == 0 {
172172
entity.Definition = mergedDef
173173
} else {

doc/holo-users-groups.8.pod

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,32 @@ Whenever an entity is successfully applied, its state will be recorded into
5656
F</var/lib/holo/users-groups/provisioned/$entity_id.toml>. This provisioned
5757
image will be used for diffing.
5858

59+
=head3 Merging of attributes
60+
61+
The desired state of the entity is computed by merging each attribute of the
62+
entity definition into the base image at
63+
F</var/lib/holo/users-groups/base/$entity_id.toml>. For most attributes, merging
64+
succeeds if the attribute is set by only one side or the other (or by neither
65+
side), or if the attribute is set to the same value on both sides.
66+
67+
As an exception, the C<groups> field (the list of auxiliary groups) of a user
68+
definition is merged by compiling a list of all groups mentioned on either side.
69+
For example, if C</etc/passwd> contains a user C<http> by default, which is part
70+
of the auxiliary group C<grp1>, and you then add an entity definition like so:
71+
72+
[[user]]
73+
name = "http"
74+
groups = ["grp2"]
75+
76+
Then after C<holo apply user:http>, the user will be part of both C<grp1>
77+
and C<grp2>. If you want to overwrite the set of auxiliary groups from the
78+
base image, set the C<skipBaseGroups> flag like so:
79+
80+
[[user]]
81+
name = "http"
82+
groups = ["grp2"]
83+
skipBaseGroups = true
84+
5985
=head2 Diff operation
6086

6187
Display a diff if the current state of the entity conflicts with the entity

test/users-groups/02-users/expected-apply-force-output

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ MOCK: usermod --gid users wronggroup
1818

1919
Working on user:wronggroups
2020
found in target/usr/share/holo/users-groups/01-users.toml
21-
with groups: network
21+
with exact groups: network
2222

23-
MOCK: usermod --groups network,video wronggroups
23+
MOCK: usermod --groups network wronggroups
2424

2525
Working on user:wronghome
2626
found in target/usr/share/holo/users-groups/01-users.toml

test/users-groups/02-users/expected-apply-output

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,20 +29,9 @@ Working on user:wronggroup
2929

3030
Working on user:wronggroups
3131
found in target/usr/share/holo/users-groups/01-users.toml
32-
with groups: network
32+
with exact groups: network
3333

34-
!! Entity has been modified by user (use --force to overwrite)
35-
36-
diff --holo target/tmp/holo/users-groups/user:wronggroups/desired.toml target/tmp/holo/users-groups/user:wronggroups/actual.toml
37-
--- target/tmp/holo/users-groups/user:wronggroups/desired.toml
38-
+++ target/tmp/holo/users-groups/user:wronggroups/actual.toml
39-
@@ -3,5 +3,5 @@ name = "wronggroups"
40-
uid = 1005
41-
home = "/home/wronggroups"
42-
group = "users"
43-
-groups = ["network", "video"]
44-
+groups = ["video"]
45-
shell = "/bin/zsh"
34+
MOCK: usermod --groups network wronggroups
4635

4736
Working on user:wronghome
4837
found in target/usr/share/holo/users-groups/01-users.toml

test/users-groups/02-users/expected-scan-output

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ user:wronggroup
1616

1717
user:wronggroups
1818
found in target/usr/share/holo/users-groups/01-users.toml
19-
with groups: network
19+
with exact groups: network
2020

2121
user:wronghome
2222
found in target/usr/share/holo/users-groups/01-users.toml

0 commit comments

Comments
 (0)