Browse code

OS integration for PSC

Paul Weil authored on 2015/10/27 04:20:49
Showing 7 changed files
... ...
@@ -32,8 +32,26 @@ At that time, the openshift docker registry image must be upgraded in order to c
32 32
 
33 33
 1. The `volume.metadata` field is deprecated as of Origin 1.0.6 in favor of `volume.downwardAPI`.
34 34
 
35
-1. New fields (`allowHostPID` and `allowHostIPC`) have been added to the default SCCs in Origin 1.0.7.  
36
-You may set these fields manually or [reset your default SCCs](https://docs.openshift.org/latest/admin_guide/manage_scc.html#updating-the-default-security-context-constraints).
35
+1. New fields (`fsGroup`, `supplementalGroups`, `allowHostPID` and `allowHostIPC`) have been added 
36
+to the default SCCs in Origin 1.0.7.  These allow you to control groups for persistent volumes,
37
+supplemental groups for the container, and usage of the host PID/IPC namespaces.  The fields will 
38
+default as follows for existing SCCs:
39
+
40
+  1.  allowHostPID - defaults to false.  You may wish to change this to true on any privileged SCCs or 
41
+  [reset your default SCCs](https://docs.openshift.org/latest/admin_guide/manage_scc.html#updating-the-default-security-context-constraints) 
42
+  which will set this field to true for the privileged SCC and false for the restricted SCC.
43
+  1.  allowHostIPC - defaults to false.  You may wish to change this to true on any privileged SCCs or 
44
+  [reset your default SCCs](https://docs.openshift.org/latest/admin_guide/manage_scc.html#updating-the-default-security-context-constraints) 
45
+  which will set this field to true for the privileged SCC and false for the restricted SCC.
46
+  1.  fsGroup - if the strategy type is unset this field will default based on the runAsUser strategy.
47
+  If runAsUser is set to RunAsAny this field will also be set to RunAsAny.  If the strategy type is
48
+  any other value this field will default to MustRunAs and look to the namespace for [annotation 
49
+  configuration](https://docs.openshift.org/latest/architecture/additional_concepts/authorization.html#understanding-pre-allocated-values-and-security-context-constraints).
50
+  1.  supplementalGroups - if the strategy type is unset this field will default based on the runAsUser strategy.
51
+  If runAsUser is set to RunAsAny this field will also be set to RunAsAny.  If the strategy type is
52
+  any other value this field will default to MustRunAs and look to the namespace for [annotation 
53
+  configuration](https://docs.openshift.org/latest/architecture/additional_concepts/authorization.html#understanding-pre-allocated-values-and-security-context-constraints).    
54
+   
37 55
 
38 56
 1. The `v1beta3` API version is being removed in Origin 1.1 (OSE 3.1).
39 57
 Existing `v1beta3` resources stored in etcd will still be readable and
... ...
@@ -31,6 +31,12 @@ func GetBootstrapSecurityContextConstraints(buildControllerUsername string) []ka
31 31
 			RunAsUser: kapi.RunAsUserStrategyOptions{
32 32
 				Type: kapi.RunAsUserStrategyRunAsAny,
33 33
 			},
34
+			FSGroup: kapi.FSGroupStrategyOptions{
35
+				Type: kapi.FSGroupStrategyRunAsAny,
36
+			},
37
+			SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
38
+				Type: kapi.SupplementalGroupsStrategyRunAsAny,
39
+			},
34 40
 			Users:  []string{buildControllerUsername},
35 41
 			Groups: []string{ClusterAdminGroup, NodesGroup},
36 42
 		},
... ...
@@ -50,6 +56,22 @@ func GetBootstrapSecurityContextConstraints(buildControllerUsername string) []ka
50 50
 				// will fail.
51 51
 				Type: kapi.RunAsUserStrategyMustRunAsRange,
52 52
 			},
53
+			FSGroup: kapi.FSGroupStrategyOptions{
54
+				// This strategy requires that annotations on the namespace which will be populated
55
+				// by the admission controller.  Admission will first look for the SupplementalGroupsAnnotation
56
+				// on the namespace and if it is unable to find that annotation it will attempt
57
+				// to use the UIDRangeAnnotation.  If neither annotation exists then creation
58
+				// of the SCC will fail.
59
+				Type: kapi.FSGroupStrategyMustRunAs,
60
+			},
61
+			SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
62
+				// This strategy requires that annotations on the namespace which will be populated
63
+				// by the admission controller.  Admission will first look for the SupplementalGroupsAnnotation
64
+				// on the namespace and if it is unable to find that annotation it will attempt
65
+				// to use the UIDRangeAnnotation.  If neither annotation exists then creation
66
+				// of the SCC will fail.
67
+				Type: kapi.SupplementalGroupsStrategyMustRunAs,
68
+			},
53 69
 			Groups: []string{AuthenticatedGroup},
54 70
 		},
55 71
 	}
... ...
@@ -15,6 +15,7 @@ import (
15 15
 	"k8s.io/kubernetes/pkg/fields"
16 16
 	"k8s.io/kubernetes/pkg/labels"
17 17
 	"k8s.io/kubernetes/pkg/runtime"
18
+	sc "k8s.io/kubernetes/pkg/securitycontext"
18 19
 	scc "k8s.io/kubernetes/pkg/securitycontextconstraints"
19 20
 	"k8s.io/kubernetes/pkg/util/sets"
20 21
 	"k8s.io/kubernetes/pkg/watch"
... ...
@@ -164,24 +165,46 @@ func assignSecurityContext(provider scc.SecurityContextConstraintsProvider, pod
164 164
 
165 165
 	errs := fielderrors.ValidationErrorList{}
166 166
 
167
-	for i, c := range pod.Spec.Containers {
168
-		sc, err := provider.CreateSecurityContext(pod, &c)
167
+	psc, err := provider.CreatePodSecurityContext(pod)
168
+	if err != nil {
169
+		errs = append(errs, fielderrors.NewFieldInvalid("spec.securityContext", pod.Spec.SecurityContext, err.Error()))
170
+	}
171
+
172
+	// save the original PSC and validate the generated PSC.  Leave the generated PSC
173
+	// set for container generation/validation.  We will reset to original post container
174
+	// validation.
175
+	originalPSC := pod.Spec.SecurityContext
176
+	pod.Spec.SecurityContext = psc
177
+	errs = append(errs, provider.ValidatePodSecurityContext(pod).Prefix("spec.securityContext")...)
178
+
179
+	// Note: this is not changing the original container, we will set container SCs later so long
180
+	// as all containers validated under the same SCC.
181
+	for i, containerCopy := range pod.Spec.Containers {
182
+		// We will determine the effective security context for the container and validate against that
183
+		// since that is how the sc provider will eventually apply settings in the runtime.
184
+		// This results in an SC that is based on the Pod's PSC with the set fields from the container
185
+		// overriding pod level settings.
186
+		containerCopy.SecurityContext = sc.DetermineEffectiveSecurityContext(pod, &containerCopy)
187
+
188
+		sc, err := provider.CreateContainerSecurityContext(pod, &containerCopy)
169 189
 		if err != nil {
170 190
 			errs = append(errs, fielderrors.NewFieldInvalid(fmt.Sprintf("spec.containers[%d].securityContext", i), "", err.Error()))
171 191
 			continue
172 192
 		}
173 193
 		generatedSCs[i] = sc
174 194
 
175
-		c.SecurityContext = sc
176
-		errs = append(errs, provider.ValidateSecurityContext(pod, &c).Prefix(fmt.Sprintf("spec.containers[%d].securityContext", i))...)
195
+		containerCopy.SecurityContext = sc
196
+		errs = append(errs, provider.ValidateContainerSecurityContext(pod, &containerCopy).Prefix(fmt.Sprintf("spec.containers[%d].securityContext", i))...)
177 197
 	}
178 198
 
179 199
 	if len(errs) > 0 {
200
+		// ensure psc is not mutated if there are errors
201
+		pod.Spec.SecurityContext = originalPSC
180 202
 		return errs
181 203
 	}
182 204
 
183 205
 	// if we've reached this code then we've generated and validated an SC for every container in the
184
-	// pod so let's apply what we generated
206
+	// pod so let's apply what we generated.  Note: the psc is already applied.
185 207
 	for i, sc := range generatedSCs {
186 208
 		pod.Spec.Containers[i].SecurityContext = sc
187 209
 	}
... ...
@@ -205,51 +228,63 @@ func (c *constraint) createProvidersFromConstraints(ns string, sccs []*kapi.Secu
205 205
 		var err error
206 206
 		resolveUIDRange := requiresPreAllocatedUIDRange(constraint)
207 207
 		resolveSELinuxLevel := requiresPreAllocatedSELinuxLevel(constraint)
208
+		resolveFSGroup := requiresPreallocatedFSGroup(constraint)
209
+		resolveSupplementalGroups := requiresPreallocatedSupplementalGroups(constraint)
210
+		requiresNamespaceAllocations := resolveUIDRange || resolveSELinuxLevel || resolveFSGroup || resolveSupplementalGroups
208 211
 
209
-		if resolveUIDRange || resolveSELinuxLevel {
210
-			var min, max *int64
211
-			var level string
212
-
212
+		if requiresNamespaceAllocations {
213 213
 			// Ensure we have the namespace
214
-			if namespace, err = c.getNamespace(ns, namespace); err != nil {
214
+			namespace, err = c.getNamespace(ns, namespace)
215
+			if err != nil {
215 216
 				errs = append(errs, fmt.Errorf("error fetching namespace %s required to preallocate values for %s: %v", ns, constraint.Name, err))
216 217
 				continue
217 218
 			}
219
+		}
218 220
 
219
-			// Resolve the values from the namespace
220
-			if resolveUIDRange {
221
-				if min, max, err = getPreallocatedUIDRange(namespace); err != nil {
222
-					errs = append(errs, fmt.Errorf("unable to find pre-allocated uid annotation for namespace %s while trying to configure SCC %s: %v", namespace.Name, constraint.Name, err))
223
-					continue
224
-				}
221
+		// Make a copy of the constraint so we don't mutate the store's cache
222
+		var constraintCopy kapi.SecurityContextConstraints = *constraint
223
+		constraint = &constraintCopy
224
+
225
+		// Resolve the values from the namespace
226
+		if resolveUIDRange {
227
+			constraint.RunAsUser.UIDRangeMin, constraint.RunAsUser.UIDRangeMax, err = getPreallocatedUIDRange(namespace)
228
+			if err != nil {
229
+				errs = append(errs, fmt.Errorf("unable to find pre-allocated uid annotation for namespace %s while trying to configure SCC %s: %v", namespace.Name, constraint.Name, err))
230
+				continue
225 231
 			}
226
-			if resolveSELinuxLevel {
227
-				if level, err = getPreallocatedLevel(namespace); err != nil {
228
-					errs = append(errs, fmt.Errorf("unable to find pre-allocated mcs annotation for namespace %s while trying to configure SCC %s: %v", namespace.Name, constraint.Name, err))
229
-					continue
230
-				}
232
+		}
233
+		if resolveSELinuxLevel {
234
+			var level string
235
+			if level, err = getPreallocatedLevel(namespace); err != nil {
236
+				errs = append(errs, fmt.Errorf("unable to find pre-allocated mcs annotation for namespace %s while trying to configure SCC %s: %v", namespace.Name, constraint.Name, err))
237
+				continue
231 238
 			}
232 239
 
233
-			// Make a copy of the constraint so we don't mutate the store's cache
234
-			var constraintCopy kapi.SecurityContextConstraints = *constraint
235
-			constraint = &constraintCopy
236
-			if resolveSELinuxLevel && constraint.SELinuxContext.SELinuxOptions != nil {
237
-				// Make a copy of the SELinuxOptions so we don't mutate the store's cache
240
+			// SELinuxOptions is a pointer, if we are resolving and it is already initialized
241
+			// we need to make a copy of it so we don't manipulate the store's cache.
242
+			if constraint.SELinuxContext.SELinuxOptions != nil {
238 243
 				var seLinuxOptionsCopy kapi.SELinuxOptions = *constraint.SELinuxContext.SELinuxOptions
239 244
 				constraint.SELinuxContext.SELinuxOptions = &seLinuxOptionsCopy
245
+			} else {
246
+				constraint.SELinuxContext.SELinuxOptions = &kapi.SELinuxOptions{}
240 247
 			}
241
-
242
-			// Set the resolved values
243
-			if resolveUIDRange {
244
-				constraint.RunAsUser.UIDRangeMin = min
245
-				constraint.RunAsUser.UIDRangeMax = max
248
+			constraint.SELinuxContext.SELinuxOptions.Level = level
249
+		}
250
+		if resolveFSGroup {
251
+			fsGroup, err := getPreallocatedFSGroup(namespace)
252
+			if err != nil {
253
+				errs = append(errs, fmt.Errorf("unable to find pre-allocated group annotation for namespace %s while trying to configure SCC %s: %v", namespace.Name, constraint.Name, err))
254
+				continue
246 255
 			}
247
-			if resolveSELinuxLevel {
248
-				if constraint.SELinuxContext.SELinuxOptions == nil {
249
-					constraint.SELinuxContext.SELinuxOptions = &kapi.SELinuxOptions{}
250
-				}
251
-				constraint.SELinuxContext.SELinuxOptions.Level = level
256
+			constraint.FSGroup.Ranges = fsGroup
257
+		}
258
+		if resolveSupplementalGroups {
259
+			supplementalGroups, err := getPreallocatedSupplementalGroups(namespace)
260
+			if err != nil {
261
+				errs = append(errs, fmt.Errorf("unable to find pre-allocated group annotation for namespace %s while trying to configure SCC %s: %v", namespace.Name, constraint.Name, err))
262
+				continue
252 263
 			}
264
+			constraint.SupplementalGroups.Ranges = supplementalGroups
253 265
 		}
254 266
 
255 267
 		// Create the provider
... ...
@@ -315,7 +350,7 @@ func constraintSupportsGroup(group string, constraintGroups []string) bool {
315 315
 	return false
316 316
 }
317 317
 
318
-// getPreallocatedUIDRange retrieves the annotated value from the service account, splits it to make
318
+// getPreallocatedUIDRange retrieves the annotated value from the namespace, splits it to make
319 319
 // the min/max and formats the data into the necessary types for the strategy options.
320 320
 func getPreallocatedUIDRange(ns *kapi.Namespace) (*int64, *int64, error) {
321 321
 	annotationVal, ok := ns.Annotations[allocator.UIDRangeAnnotation]
... ...
@@ -336,7 +371,7 @@ func getPreallocatedUIDRange(ns *kapi.Namespace) (*int64, *int64, error) {
336 336
 	return &min, &max, nil
337 337
 }
338 338
 
339
-// getPreallocatedLevel gets the annotated value from the service account.
339
+// getPreallocatedLevel gets the annotated value from the namespace.
340 340
 func getPreallocatedLevel(ns *kapi.Namespace) (string, error) {
341 341
 	level, ok := ns.Annotations[allocator.MCSAnnotation]
342 342
 	if !ok {
... ...
@@ -349,6 +384,87 @@ func getPreallocatedLevel(ns *kapi.Namespace) (string, error) {
349 349
 	return level, nil
350 350
 }
351 351
 
352
+// getSupplementalGroupsAnnotation provides a backwards compatible way to get supplemental groups
353
+// annotations from a namespace by looking for SupplementalGroupsAnnotation and falling back to
354
+// UIDRangeAnnotation if it is not found.
355
+func getSupplementalGroupsAnnotation(ns *kapi.Namespace) (string, error) {
356
+	groups, ok := ns.Annotations[allocator.SupplementalGroupsAnnotation]
357
+	if !ok {
358
+		glog.V(4).Infof("unable to find supplemental group annotation %s falling back to %s", allocator.SupplementalGroupsAnnotation, allocator.UIDRangeAnnotation)
359
+
360
+		groups, ok = ns.Annotations[allocator.UIDRangeAnnotation]
361
+		if !ok {
362
+			return "", fmt.Errorf("unable to find supplemental group or uid annotation for namespace %s", ns.Name)
363
+		}
364
+	}
365
+
366
+	if len(groups) == 0 {
367
+		return "", fmt.Errorf("unable to find groups using %s and %s annotations", allocator.SupplementalGroupsAnnotation, allocator.UIDRangeAnnotation)
368
+	}
369
+	return groups, nil
370
+}
371
+
372
+// getPreallocatedFSGroup gets the annotated value from the namespace.
373
+func getPreallocatedFSGroup(ns *kapi.Namespace) ([]kapi.IDRange, error) {
374
+	groups, err := getSupplementalGroupsAnnotation(ns)
375
+	if err != nil {
376
+		return nil, err
377
+	}
378
+	glog.V(4).Infof("got preallocated value for groups: %s in namespace %s", groups, ns.Name)
379
+
380
+	blocks, err := parseSupplementalGroupAnnotation(groups)
381
+	if err != nil {
382
+		return nil, err
383
+	}
384
+	return []kapi.IDRange{
385
+		{
386
+			Min: int64(blocks[0].Start),
387
+			Max: int64(blocks[0].Start),
388
+		},
389
+	}, nil
390
+}
391
+
392
+// getPreallocatedSupplementalGroups gets the annotated value from the namespace.
393
+func getPreallocatedSupplementalGroups(ns *kapi.Namespace) ([]kapi.IDRange, error) {
394
+	groups, err := getSupplementalGroupsAnnotation(ns)
395
+	if err != nil {
396
+		return nil, err
397
+	}
398
+	glog.V(4).Infof("got preallocated value for groups: %s in namespace %s", groups, ns.Name)
399
+
400
+	blocks, err := parseSupplementalGroupAnnotation(groups)
401
+	if err != nil {
402
+		return nil, err
403
+	}
404
+
405
+	idRanges := []kapi.IDRange{}
406
+	for _, block := range blocks {
407
+		rng := kapi.IDRange{
408
+			Min: int64(block.Start),
409
+			Max: int64(block.End),
410
+		}
411
+		idRanges = append(idRanges, rng)
412
+	}
413
+	return idRanges, nil
414
+}
415
+
416
+// parseSupplementalGroupAnnotation parses the group annotation into blocks.
417
+func parseSupplementalGroupAnnotation(groups string) ([]uid.Block, error) {
418
+	blocks := []uid.Block{}
419
+	segments := strings.Split(groups, ",")
420
+	for _, segment := range segments {
421
+		block, err := uid.ParseBlock(segment)
422
+		if err != nil {
423
+			return nil, err
424
+		}
425
+		blocks = append(blocks, block)
426
+	}
427
+	if len(blocks) == 0 {
428
+		return nil, fmt.Errorf("no blocks parsed from annotation %s", groups)
429
+	}
430
+	return blocks, nil
431
+}
432
+
352 433
 // requiresPreAllocatedUIDRange returns true if the strategy is must run in range and the min or max
353 434
 // is not set.
354 435
 func requiresPreAllocatedUIDRange(constraint *kapi.SecurityContextConstraints) bool {
... ...
@@ -369,6 +485,24 @@ func requiresPreAllocatedSELinuxLevel(constraint *kapi.SecurityContextConstraint
369 369
 	return constraint.SELinuxContext.SELinuxOptions.Level == ""
370 370
 }
371 371
 
372
+// requiresPreAllocatedSELinuxLevel returns true if the strategy is must run as and there is no
373
+// range specified.
374
+func requiresPreallocatedSupplementalGroups(constraint *kapi.SecurityContextConstraints) bool {
375
+	if constraint.SupplementalGroups.Type != kapi.SupplementalGroupsStrategyMustRunAs {
376
+		return false
377
+	}
378
+	return len(constraint.SupplementalGroups.Ranges) == 0
379
+}
380
+
381
+// requiresPreallocatedFSGroup returns true if the strategy is must run as and there is no
382
+// range specified.
383
+func requiresPreallocatedFSGroup(constraint *kapi.SecurityContextConstraints) bool {
384
+	if constraint.FSGroup.Type != kapi.FSGroupStrategyMustRunAs {
385
+		return false
386
+	}
387
+	return len(constraint.FSGroup.Ranges) == 0
388
+}
389
+
372 390
 // deduplicateSecurityContextConstraints ensures we have a unique slice of constraints.
373 391
 func deduplicateSecurityContextConstraints(sccs []*kapi.SecurityContextConstraints) []*kapi.SecurityContextConstraints {
374 392
 	deDuped := []*kapi.SecurityContextConstraints{}
... ...
@@ -15,6 +15,7 @@ import (
15 15
 	"k8s.io/kubernetes/pkg/util"
16 16
 
17 17
 	allocator "github.com/openshift/origin/pkg/security"
18
+	"github.com/openshift/origin/pkg/security/uid"
18 19
 )
19 20
 
20 21
 func NewTestAdmission(store cache.Store, kclient client.Interface) kadmission.Interface {
... ...
@@ -31,11 +32,16 @@ func TestAdmit(t *testing.T) {
31 31
 		ObjectMeta: kapi.ObjectMeta{
32 32
 			Name: "default",
33 33
 			Annotations: map[string]string{
34
-				allocator.UIDRangeAnnotation: "1/3",
35
-				allocator.MCSAnnotation:      "s0:c1,c0",
34
+				allocator.UIDRangeAnnotation:           "1/3",
35
+				allocator.MCSAnnotation:                "s0:c1,c0",
36
+				allocator.SupplementalGroupsAnnotation: "2/3",
36 37
 			},
37 38
 		},
38 39
 	}
40
+
41
+	// used for cases where things are preallocated
42
+	defaultGroup := int64(2)
43
+
39 44
 	serviceAccount := &kapi.ServiceAccount{
40 45
 		ObjectMeta: kapi.ObjectMeta{
41 46
 			Name: "default",
... ...
@@ -55,6 +61,12 @@ func TestAdmit(t *testing.T) {
55 55
 		SELinuxContext: kapi.SELinuxContextStrategyOptions{
56 56
 			Type: kapi.SELinuxStrategyMustRunAs,
57 57
 		},
58
+		FSGroup: kapi.FSGroupStrategyOptions{
59
+			Type: kapi.FSGroupStrategyMustRunAs,
60
+		},
61
+		SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
62
+			Type: kapi.SupplementalGroupsStrategyMustRunAs,
63
+		},
58 64
 		Groups: []string{"system:serviceaccounts"},
59 65
 	}
60 66
 	// create scc that has specific requirements that shouldn't match but is permissioned to
... ...
@@ -74,6 +86,18 @@ func TestAdmit(t *testing.T) {
74 74
 				Level: "s9:z0,z1",
75 75
 			},
76 76
 		},
77
+		FSGroup: kapi.FSGroupStrategyOptions{
78
+			Type: kapi.FSGroupStrategyMustRunAs,
79
+			Ranges: []kapi.IDRange{
80
+				{Min: 999, Max: 999},
81
+			},
82
+		},
83
+		SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
84
+			Type: kapi.SupplementalGroupsStrategyMustRunAs,
85
+			Ranges: []kapi.IDRange{
86
+				{Min: 999, Max: 999},
87
+			},
88
+		},
77 89
 		Groups: []string{"system:serviceaccounts"},
78 90
 	}
79 91
 	store := cache.NewStore(cache.MetaNamespaceKeyFunc)
... ...
@@ -125,6 +149,23 @@ func TestAdmit(t *testing.T) {
125 125
 		Level: "s0:c1,c0",
126 126
 	}
127 127
 
128
+	// specifies an FSGroup in the range of preallocated sup group annotation
129
+	specifyFSGroupInRange := goodPod()
130
+	// group in the range of a preallocated fs group which, by default is a single digit range
131
+	// based on the first value of the ns annotation.
132
+	goodFSGroup := int64(2)
133
+	specifyFSGroupInRange.Spec.SecurityContext.FSGroup = &goodFSGroup
134
+
135
+	// specifies a sup group in the range of preallocated sup group annotation
136
+	specifySupGroup := goodPod()
137
+	// group is not the default but still in the range
138
+	specifySupGroup.Spec.SecurityContext.SupplementalGroups = []int64{3}
139
+
140
+	specifyPodLevelSELinux := goodPod()
141
+	specifyPodLevelSELinux.Spec.SecurityContext.SELinuxOptions = &kapi.SELinuxOptions{
142
+		Level: "s0:c1,c0",
143
+	}
144
+
128 145
 	requestsHostNetwork := goodPod()
129 146
 	requestsHostNetwork.Spec.SecurityContext.HostNetwork = true
130 147
 
... ...
@@ -137,12 +178,29 @@ func TestAdmit(t *testing.T) {
137 137
 	requestsHostPorts := goodPod()
138 138
 	requestsHostPorts.Spec.Containers[0].Ports = []kapi.ContainerPort{{HostPort: 1}}
139 139
 
140
+	requestsSupplementalGroup := goodPod()
141
+	requestsSupplementalGroup.Spec.SecurityContext.SupplementalGroups = []int64{1}
142
+
143
+	requestsFSGroup := goodPod()
144
+	fsGroup := int64(1)
145
+	requestsFSGroup.Spec.SecurityContext.FSGroup = &fsGroup
146
+
147
+	requestsPodLevelMCS := goodPod()
148
+	requestsPodLevelMCS.Spec.SecurityContext.SELinuxOptions = &kapi.SELinuxOptions{
149
+		User:  "user",
150
+		Type:  "type",
151
+		Role:  "role",
152
+		Level: "level",
153
+	}
154
+
140 155
 	testCases := map[string]struct {
141
-		pod           *kapi.Pod
142
-		shouldAdmit   bool
143
-		expectedUID   int64
144
-		expectedLevel string
145
-		expectedPriv  bool
156
+		pod               *kapi.Pod
157
+		shouldAdmit       bool
158
+		expectedUID       int64
159
+		expectedLevel     string
160
+		expectedFSGroup   int64
161
+		expectedSupGroups []int64
162
+		expectedPriv      bool
146 163
 	}{
147 164
 		"uidNotInRange": {
148 165
 			pod:         uidNotInRange,
... ...
@@ -157,16 +215,44 @@ func TestAdmit(t *testing.T) {
157 157
 			shouldAdmit: false,
158 158
 		},
159 159
 		"specifyUIDInRange": {
160
-			pod:           specifyUIDInRange,
161
-			shouldAdmit:   true,
162
-			expectedUID:   *specifyUIDInRange.Spec.Containers[0].SecurityContext.RunAsUser,
163
-			expectedLevel: "s0:c1,c0",
160
+			pod:               specifyUIDInRange,
161
+			shouldAdmit:       true,
162
+			expectedUID:       *specifyUIDInRange.Spec.Containers[0].SecurityContext.RunAsUser,
163
+			expectedLevel:     "s0:c1,c0",
164
+			expectedFSGroup:   defaultGroup,
165
+			expectedSupGroups: []int64{defaultGroup},
164 166
 		},
165 167
 		"specifyLabels": {
166
-			pod:           specifyLabels,
167
-			shouldAdmit:   true,
168
-			expectedUID:   1,
169
-			expectedLevel: specifyLabels.Spec.Containers[0].SecurityContext.SELinuxOptions.Level,
168
+			pod:               specifyLabels,
169
+			shouldAdmit:       true,
170
+			expectedUID:       1,
171
+			expectedLevel:     specifyLabels.Spec.Containers[0].SecurityContext.SELinuxOptions.Level,
172
+			expectedFSGroup:   defaultGroup,
173
+			expectedSupGroups: []int64{defaultGroup},
174
+		},
175
+		"specifyFSGroup": {
176
+			pod:               specifyFSGroupInRange,
177
+			shouldAdmit:       true,
178
+			expectedUID:       1,
179
+			expectedLevel:     "s0:c1,c0",
180
+			expectedFSGroup:   *specifyFSGroupInRange.Spec.SecurityContext.FSGroup,
181
+			expectedSupGroups: []int64{defaultGroup},
182
+		},
183
+		"specifySupGroup": {
184
+			pod:               specifySupGroup,
185
+			shouldAdmit:       true,
186
+			expectedUID:       1,
187
+			expectedLevel:     "s0:c1,c0",
188
+			expectedFSGroup:   defaultGroup,
189
+			expectedSupGroups: []int64{specifySupGroup.Spec.SecurityContext.SupplementalGroups[0]},
190
+		},
191
+		"specifyPodLevelSELinuxLevel": {
192
+			pod:               specifyPodLevelSELinux,
193
+			shouldAdmit:       true,
194
+			expectedUID:       1,
195
+			expectedLevel:     "s0:c1,c0",
196
+			expectedFSGroup:   defaultGroup,
197
+			expectedSupGroups: []int64{defaultGroup},
170 198
 		},
171 199
 		"requestsHostNetwork": {
172 200
 			pod:         requestsHostNetwork,
... ...
@@ -184,6 +270,18 @@ func TestAdmit(t *testing.T) {
184 184
 			pod:         requestsHostIPC,
185 185
 			shouldAdmit: false,
186 186
 		},
187
+		"requestsSupplementalGroup": {
188
+			pod:         requestsSupplementalGroup,
189
+			shouldAdmit: false,
190
+		},
191
+		"requestsFSGroup": {
192
+			pod:         requestsFSGroup,
193
+			shouldAdmit: false,
194
+		},
195
+		"requestsPodLevelMCS": {
196
+			pod:         requestsPodLevelMCS,
197
+			shouldAdmit: false,
198
+		},
187 199
 	}
188 200
 
189 201
 	for k, v := range testCases {
... ...
@@ -205,12 +303,30 @@ func TestAdmit(t *testing.T) {
205 205
 			if validatedSCC != saSCC.Name {
206 206
 				t.Errorf("%s should have validated against %s but found %s", k, saSCC.Name, validatedSCC)
207 207
 			}
208
+
209
+			// ensure anything we expected to be defaulted on the container level is set
208 210
 			if *v.pod.Spec.Containers[0].SecurityContext.RunAsUser != v.expectedUID {
209 211
 				t.Errorf("%s expected UID %d but found %d", k, v.expectedUID, *v.pod.Spec.Containers[0].SecurityContext.RunAsUser)
210 212
 			}
211 213
 			if v.pod.Spec.Containers[0].SecurityContext.SELinuxOptions.Level != v.expectedLevel {
212 214
 				t.Errorf("%s expected Level %s but found %s", k, v.expectedLevel, v.pod.Spec.Containers[0].SecurityContext.SELinuxOptions.Level)
213 215
 			}
216
+
217
+			// ensure anything we expected to be defaulted on the pod level is set
218
+			if v.pod.Spec.SecurityContext.SELinuxOptions.Level != v.expectedLevel {
219
+				t.Errorf("%s expected pod level SELinux Level %s but found %s", k, v.expectedLevel, v.pod.Spec.SecurityContext.SELinuxOptions.Level)
220
+			}
221
+			if *v.pod.Spec.SecurityContext.FSGroup != v.expectedFSGroup {
222
+				t.Errorf("%s expected fsgroup %d but found %d", k, v.expectedFSGroup, *v.pod.Spec.SecurityContext.FSGroup)
223
+			}
224
+			if len(v.pod.Spec.SecurityContext.SupplementalGroups) != len(v.expectedSupGroups) {
225
+				t.Errorf("%s found unexpected supplemental groups.  Expected: %v, actual %v", k, v.expectedSupGroups, v.pod.Spec.SecurityContext.SupplementalGroups)
226
+			}
227
+			for _, g := range v.expectedSupGroups {
228
+				if !hasSupGroup(g, v.pod.Spec.SecurityContext.SupplementalGroups) {
229
+					t.Errorf("%s expected sup group %d", k, g)
230
+				}
231
+			}
214 232
 		}
215 233
 	}
216 234
 
... ...
@@ -231,6 +347,12 @@ func TestAdmit(t *testing.T) {
231 231
 		SELinuxContext: kapi.SELinuxContextStrategyOptions{
232 232
 			Type: kapi.SELinuxStrategyRunAsAny,
233 233
 		},
234
+		FSGroup: kapi.FSGroupStrategyOptions{
235
+			Type: kapi.FSGroupStrategyRunAsAny,
236
+		},
237
+		SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
238
+			Type: kapi.SupplementalGroupsStrategyRunAsAny,
239
+		},
234 240
 		Groups: []string{"system:serviceaccounts"},
235 241
 	}
236 242
 	store.Add(adminSCC)
... ...
@@ -253,10 +375,20 @@ func TestAdmit(t *testing.T) {
253 253
 	}
254 254
 }
255 255
 
256
+func hasSupGroup(group int64, groups []int64) bool {
257
+	for _, g := range groups {
258
+		if g == group {
259
+			return true
260
+		}
261
+	}
262
+	return false
263
+}
264
+
256 265
 func TestAssignSecurityContext(t *testing.T) {
257 266
 	// set up test data
258 267
 	// scc that will deny privileged container requests and has a default value for a field (uid)
259 268
 	var uid int64 = 9999
269
+	fsGroup := int64(1)
260 270
 	scc := &kapi.SecurityContextConstraints{
261 271
 		ObjectMeta: kapi.ObjectMeta{
262 272
 			Name: "test scc",
... ...
@@ -268,6 +400,17 @@ func TestAssignSecurityContext(t *testing.T) {
268 268
 			Type: kapi.RunAsUserStrategyMustRunAs,
269 269
 			UID:  &uid,
270 270
 		},
271
+
272
+		// require allocation for a field in the psc as well to test changes/no changes
273
+		FSGroup: kapi.FSGroupStrategyOptions{
274
+			Type: kapi.FSGroupStrategyMustRunAs,
275
+			Ranges: []kapi.IDRange{
276
+				{Min: fsGroup, Max: fsGroup},
277
+			},
278
+		},
279
+		SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
280
+			Type: kapi.SupplementalGroupsStrategyRunAsAny,
281
+		},
271 282
 	}
272 283
 	provider, err := kscc.NewSimpleProvider(scc)
273 284
 	if err != nil {
... ...
@@ -291,7 +434,7 @@ func TestAssignSecurityContext(t *testing.T) {
291 291
 		shouldValidate bool
292 292
 		expectedUID    *int64
293 293
 	}{
294
-		"container SC is not changed when invalid": {
294
+		"pod and container SC is not changed when invalid": {
295 295
 			pod: &kapi.Pod{
296 296
 				Spec: kapi.PodSpec{
297 297
 					SecurityContext: &kapi.PodSecurityContext{},
... ...
@@ -333,16 +476,23 @@ func TestAssignSecurityContext(t *testing.T) {
333 333
 		}
334 334
 
335 335
 		// if we shouldn't have validated ensure that uid is not set on the containers
336
+		// and ensure the psc does not have fsgroup set
336 337
 		if !v.shouldValidate {
338
+			if v.pod.Spec.SecurityContext.FSGroup != nil {
339
+				t.Errorf("%s had a non-nil FSGroup %d.  FSGroup should not be set on test cases that don't validate", k, *v.pod.Spec.SecurityContext.FSGroup)
340
+			}
337 341
 			for _, c := range v.pod.Spec.Containers {
338 342
 				if c.SecurityContext.RunAsUser != nil {
339
-					t.Errorf("%s had non-nil UID %d.  UID should not be set on test cases that dont' validate", k, *c.SecurityContext.RunAsUser)
343
+					t.Errorf("%s had non-nil UID %d.  UID should not be set on test cases that don't validate", k, *c.SecurityContext.RunAsUser)
340 344
 				}
341 345
 			}
342 346
 		}
343 347
 
344 348
 		// if we validated then the pod sc should be updated now with the defaults from the SCC
345 349
 		if v.shouldValidate {
350
+			if *v.pod.Spec.SecurityContext.FSGroup != fsGroup {
351
+				t.Errorf("%s expected fsgroup to be defaulted but found %v", k, v.pod.Spec.SecurityContext.FSGroup)
352
+			}
346 353
 			for _, c := range v.pod.Spec.Containers {
347 354
 				if *c.SecurityContext.RunAsUser != uid {
348 355
 					t.Errorf("%s expected uid to be defaulted to %d but found %v", k, uid, c.SecurityContext.RunAsUser)
... ...
@@ -357,8 +507,9 @@ func TestCreateProvidersFromConstraints(t *testing.T) {
357 357
 		ObjectMeta: kapi.ObjectMeta{
358 358
 			Name: "default",
359 359
 			Annotations: map[string]string{
360
-				allocator.UIDRangeAnnotation: "1/3",
361
-				allocator.MCSAnnotation:      "s0:c1,c0",
360
+				allocator.UIDRangeAnnotation:           "1/3",
361
+				allocator.MCSAnnotation:                "s0:c1,c0",
362
+				allocator.SupplementalGroupsAnnotation: "1/3",
362 363
 			},
363 364
 		},
364 365
 	}
... ...
@@ -366,7 +517,8 @@ func TestCreateProvidersFromConstraints(t *testing.T) {
366 366
 		ObjectMeta: kapi.ObjectMeta{
367 367
 			Name: "default",
368 368
 			Annotations: map[string]string{
369
-				allocator.MCSAnnotation: "s0:c1,c0",
369
+				allocator.MCSAnnotation:                "s0:c1,c0",
370
+				allocator.SupplementalGroupsAnnotation: "1/3",
370 371
 			},
371 372
 		},
372 373
 	}
... ...
@@ -374,7 +526,29 @@ func TestCreateProvidersFromConstraints(t *testing.T) {
374 374
 		ObjectMeta: kapi.ObjectMeta{
375 375
 			Name: "default",
376 376
 			Annotations: map[string]string{
377
+				allocator.UIDRangeAnnotation:           "1/3",
378
+				allocator.SupplementalGroupsAnnotation: "1/3",
379
+			},
380
+		},
381
+	}
382
+
383
+	namespaceNoSupplementalGroupsFallbackToUID := &kapi.Namespace{
384
+		ObjectMeta: kapi.ObjectMeta{
385
+			Name: "default",
386
+			Annotations: map[string]string{
377 387
 				allocator.UIDRangeAnnotation: "1/3",
388
+				allocator.MCSAnnotation:      "s0:c1,c0",
389
+			},
390
+		},
391
+	}
392
+
393
+	namespaceBadSupGroups := &kapi.Namespace{
394
+		ObjectMeta: kapi.ObjectMeta{
395
+			Name: "default",
396
+			Annotations: map[string]string{
397
+				allocator.UIDRangeAnnotation:           "1/3",
398
+				allocator.MCSAnnotation:                "s0:c1,c0",
399
+				allocator.SupplementalGroupsAnnotation: "",
378 400
 			},
379 401
 		},
380 402
 	}
... ...
@@ -397,6 +571,12 @@ func TestCreateProvidersFromConstraints(t *testing.T) {
397 397
 					RunAsUser: kapi.RunAsUserStrategyOptions{
398 398
 						Type: kapi.RunAsUserStrategyRunAsAny,
399 399
 					},
400
+					FSGroup: kapi.FSGroupStrategyOptions{
401
+						Type: kapi.FSGroupStrategyRunAsAny,
402
+					},
403
+					SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
404
+						Type: kapi.SupplementalGroupsStrategyRunAsAny,
405
+					},
400 406
 				}
401 407
 			},
402 408
 			namespace: namespaceValid,
... ...
@@ -414,6 +594,12 @@ func TestCreateProvidersFromConstraints(t *testing.T) {
414 414
 					RunAsUser: kapi.RunAsUserStrategyOptions{
415 415
 						Type: kapi.RunAsUserStrategyMustRunAsRange,
416 416
 					},
417
+					FSGroup: kapi.FSGroupStrategyOptions{
418
+						Type: kapi.FSGroupStrategyMustRunAs,
419
+					},
420
+					SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
421
+						Type: kapi.SupplementalGroupsStrategyMustRunAs,
422
+					},
417 423
 				}
418 424
 			},
419 425
 			namespace: namespaceValid,
... ...
@@ -430,6 +616,12 @@ func TestCreateProvidersFromConstraints(t *testing.T) {
430 430
 					RunAsUser: kapi.RunAsUserStrategyOptions{
431 431
 						Type: kapi.RunAsUserStrategyMustRunAsRange,
432 432
 					},
433
+					FSGroup: kapi.FSGroupStrategyOptions{
434
+						Type: kapi.FSGroupStrategyRunAsAny,
435
+					},
436
+					SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
437
+						Type: kapi.SupplementalGroupsStrategyRunAsAny,
438
+					},
433 439
 				}
434 440
 			},
435 441
 			namespace:   namespaceNoUID,
... ...
@@ -447,11 +639,62 @@ func TestCreateProvidersFromConstraints(t *testing.T) {
447 447
 					RunAsUser: kapi.RunAsUserStrategyOptions{
448 448
 						Type: kapi.RunAsUserStrategyMustRunAsRange,
449 449
 					},
450
+					FSGroup: kapi.FSGroupStrategyOptions{
451
+						Type: kapi.FSGroupStrategyRunAsAny,
452
+					},
453
+					SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
454
+						Type: kapi.SupplementalGroupsStrategyRunAsAny,
455
+					},
450 456
 				}
451 457
 			},
452 458
 			namespace:   namespaceNoMCS,
453 459
 			expectedErr: "unable to find pre-allocated mcs annotation",
454 460
 		},
461
+		"pre-allocated group falls back to UID annotation": {
462
+			scc: func() *kapi.SecurityContextConstraints {
463
+				return &kapi.SecurityContextConstraints{
464
+					ObjectMeta: kapi.ObjectMeta{
465
+						Name: "pre-allocated no sup group annotation",
466
+					},
467
+					SELinuxContext: kapi.SELinuxContextStrategyOptions{
468
+						Type: kapi.SELinuxStrategyRunAsAny,
469
+					},
470
+					RunAsUser: kapi.RunAsUserStrategyOptions{
471
+						Type: kapi.RunAsUserStrategyRunAsAny,
472
+					},
473
+					FSGroup: kapi.FSGroupStrategyOptions{
474
+						Type: kapi.FSGroupStrategyMustRunAs,
475
+					},
476
+					SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
477
+						Type: kapi.SupplementalGroupsStrategyMustRunAs,
478
+					},
479
+				}
480
+			},
481
+			namespace: namespaceNoSupplementalGroupsFallbackToUID,
482
+		},
483
+		"pre-allocated group bad value fails": {
484
+			scc: func() *kapi.SecurityContextConstraints {
485
+				return &kapi.SecurityContextConstraints{
486
+					ObjectMeta: kapi.ObjectMeta{
487
+						Name: "pre-allocated no sup group annotation",
488
+					},
489
+					SELinuxContext: kapi.SELinuxContextStrategyOptions{
490
+						Type: kapi.SELinuxStrategyRunAsAny,
491
+					},
492
+					RunAsUser: kapi.RunAsUserStrategyOptions{
493
+						Type: kapi.RunAsUserStrategyRunAsAny,
494
+					},
495
+					FSGroup: kapi.FSGroupStrategyOptions{
496
+						Type: kapi.FSGroupStrategyMustRunAs,
497
+					},
498
+					SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
499
+						Type: kapi.SupplementalGroupsStrategyMustRunAs,
500
+					},
501
+				}
502
+			},
503
+			namespace:   namespaceBadSupGroups,
504
+			expectedErr: "unable to find pre-allocated group annotation",
505
+		},
455 506
 		"bad scc strategy options": {
456 507
 			scc: func() *kapi.SecurityContextConstraints {
457 508
 				return &kapi.SecurityContextConstraints{
... ...
@@ -464,6 +707,12 @@ func TestCreateProvidersFromConstraints(t *testing.T) {
464 464
 					RunAsUser: kapi.RunAsUserStrategyOptions{
465 465
 						Type: kapi.RunAsUserStrategyMustRunAs,
466 466
 					},
467
+					FSGroup: kapi.FSGroupStrategyOptions{
468
+						Type: kapi.FSGroupStrategyRunAsAny,
469
+					},
470
+					SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
471
+						Type: kapi.SupplementalGroupsStrategyRunAsAny,
472
+					},
467 473
 				}
468 474
 			},
469 475
 			namespace:   namespaceValid,
... ...
@@ -722,3 +971,315 @@ func TestDeduplicateSecurityContextConstraints(t *testing.T) {
722 722
 	}
723 723
 
724 724
 }
725
+
726
+func TestRequiresPreallocatedSupplementalGroups(t *testing.T) {
727
+	testCases := map[string]struct {
728
+		scc      *kapi.SecurityContextConstraints
729
+		requires bool
730
+	}{
731
+		"must run as": {
732
+			scc: &kapi.SecurityContextConstraints{
733
+				SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
734
+					Type: kapi.SupplementalGroupsStrategyMustRunAs,
735
+				},
736
+			},
737
+			requires: true,
738
+		},
739
+		"must with range specified": {
740
+			scc: &kapi.SecurityContextConstraints{
741
+				SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
742
+					Type: kapi.SupplementalGroupsStrategyMustRunAs,
743
+					Ranges: []kapi.IDRange{
744
+						{Min: 1, Max: 1},
745
+					},
746
+				},
747
+			},
748
+		},
749
+		"run as any": {
750
+			scc: &kapi.SecurityContextConstraints{
751
+				SupplementalGroups: kapi.SupplementalGroupsStrategyOptions{
752
+					Type: kapi.SupplementalGroupsStrategyRunAsAny,
753
+				},
754
+			},
755
+		},
756
+	}
757
+	for k, v := range testCases {
758
+		result := requiresPreallocatedSupplementalGroups(v.scc)
759
+		if result != v.requires {
760
+			t.Errorf("%s expected result %t but got %t", k, v.requires, result)
761
+		}
762
+	}
763
+}
764
+
765
+func TestRequiresPreallocatedFSGroup(t *testing.T) {
766
+	testCases := map[string]struct {
767
+		scc      *kapi.SecurityContextConstraints
768
+		requires bool
769
+	}{
770
+		"must run as": {
771
+			scc: &kapi.SecurityContextConstraints{
772
+				FSGroup: kapi.FSGroupStrategyOptions{
773
+					Type: kapi.FSGroupStrategyMustRunAs,
774
+				},
775
+			},
776
+			requires: true,
777
+		},
778
+		"must with range specified": {
779
+			scc: &kapi.SecurityContextConstraints{
780
+				FSGroup: kapi.FSGroupStrategyOptions{
781
+					Type: kapi.FSGroupStrategyMustRunAs,
782
+					Ranges: []kapi.IDRange{
783
+						{Min: 1, Max: 1},
784
+					},
785
+				},
786
+			},
787
+		},
788
+		"run as any": {
789
+			scc: &kapi.SecurityContextConstraints{
790
+				FSGroup: kapi.FSGroupStrategyOptions{
791
+					Type: kapi.FSGroupStrategyRunAsAny,
792
+				},
793
+			},
794
+		},
795
+	}
796
+	for k, v := range testCases {
797
+		result := requiresPreallocatedFSGroup(v.scc)
798
+		if result != v.requires {
799
+			t.Errorf("%s expected result %t but got %t", k, v.requires, result)
800
+		}
801
+	}
802
+}
803
+
804
+func TestParseSupplementalGroupAnnotation(t *testing.T) {
805
+	tests := map[string]struct {
806
+		groups     string
807
+		expected   []uid.Block
808
+		shouldFail bool
809
+	}{
810
+		"single block slash": {
811
+			groups: "1/5",
812
+			expected: []uid.Block{
813
+				{Start: 1, End: 5},
814
+			},
815
+		},
816
+		"single block dash": {
817
+			groups: "1-5",
818
+			expected: []uid.Block{
819
+				{Start: 1, End: 5},
820
+			},
821
+		},
822
+		"multiple blocks": {
823
+			groups: "1/5,6/5,11/5",
824
+			expected: []uid.Block{
825
+				{Start: 1, End: 5},
826
+				{Start: 6, End: 10},
827
+				{Start: 11, End: 15},
828
+			},
829
+		},
830
+		"dash format": {
831
+			groups: "1-5,6-10,11-15",
832
+			expected: []uid.Block{
833
+				{Start: 1, End: 5},
834
+				{Start: 6, End: 10},
835
+				{Start: 11, End: 15},
836
+			},
837
+		},
838
+		"no blocks": {
839
+			groups:     "",
840
+			shouldFail: true,
841
+		},
842
+	}
843
+	for k, v := range tests {
844
+		blocks, err := parseSupplementalGroupAnnotation(v.groups)
845
+
846
+		if v.shouldFail && err == nil {
847
+			t.Errorf("%s was expected to fail but received no error and blocks %v", k, blocks)
848
+			continue
849
+		}
850
+
851
+		if !v.shouldFail && err != nil {
852
+			t.Errorf("%s had an unexpected error %v", k, err)
853
+			continue
854
+		}
855
+
856
+		if len(blocks) != len(v.expected) {
857
+			t.Errorf("%s received unexpected number of blocks expected: %v, actual %v", k, v.expected, blocks)
858
+		}
859
+
860
+		for _, b := range v.expected {
861
+			if !hasBlock(b, blocks) {
862
+				t.Errorf("%s was missing block %v", k, b)
863
+			}
864
+		}
865
+	}
866
+}
867
+
868
+func hasBlock(block uid.Block, blocks []uid.Block) bool {
869
+	for _, b := range blocks {
870
+		if b.Start == block.Start && b.End == block.End {
871
+			return true
872
+		}
873
+	}
874
+	return false
875
+}
876
+
877
+func TestGetPreallocatedFSGroup(t *testing.T) {
878
+	ns := func() *kapi.Namespace {
879
+		return &kapi.Namespace{
880
+			ObjectMeta: kapi.ObjectMeta{
881
+				Annotations: map[string]string{},
882
+			},
883
+		}
884
+	}
885
+
886
+	fallbackNS := ns()
887
+	fallbackNS.Annotations[allocator.UIDRangeAnnotation] = "1/5"
888
+
889
+	emptyAnnotationNS := ns()
890
+	emptyAnnotationNS.Annotations[allocator.SupplementalGroupsAnnotation] = ""
891
+
892
+	badBlockNS := ns()
893
+	badBlockNS.Annotations[allocator.SupplementalGroupsAnnotation] = "foo"
894
+
895
+	goodNS := ns()
896
+	goodNS.Annotations[allocator.SupplementalGroupsAnnotation] = "1/5"
897
+
898
+	tests := map[string]struct {
899
+		ns         *kapi.Namespace
900
+		expected   []kapi.IDRange
901
+		shouldFail bool
902
+	}{
903
+		"fall back to uid if sup group doesn't exist": {
904
+			ns: fallbackNS,
905
+			expected: []kapi.IDRange{
906
+				{Min: 1, Max: 1},
907
+			},
908
+		},
909
+		"no annotation": {
910
+			ns:         ns(),
911
+			shouldFail: true,
912
+		},
913
+		"empty annotation": {
914
+			ns:         emptyAnnotationNS,
915
+			shouldFail: true,
916
+		},
917
+		"bad block": {
918
+			ns:         badBlockNS,
919
+			shouldFail: true,
920
+		},
921
+		"good sup group annotation": {
922
+			ns: goodNS,
923
+			expected: []kapi.IDRange{
924
+				{Min: 1, Max: 1},
925
+			},
926
+		},
927
+	}
928
+
929
+	for k, v := range tests {
930
+		ranges, err := getPreallocatedFSGroup(v.ns)
931
+		if v.shouldFail && err == nil {
932
+			t.Errorf("%s was expected to fail but received no error and ranges %v", k, ranges)
933
+			continue
934
+		}
935
+
936
+		if !v.shouldFail && err != nil {
937
+			t.Errorf("%s had an unexpected error %v", k, err)
938
+			continue
939
+		}
940
+
941
+		if len(ranges) != len(v.expected) {
942
+			t.Errorf("%s received unexpected number of ranges expected: %v, actual %v", k, v.expected, ranges)
943
+		}
944
+
945
+		for _, r := range v.expected {
946
+			if !hasRange(r, ranges) {
947
+				t.Errorf("%s was missing range %v", k, r)
948
+			}
949
+		}
950
+	}
951
+}
952
+
953
+func TestGetPreallocatedSupplementalGroups(t *testing.T) {
954
+	ns := func() *kapi.Namespace {
955
+		return &kapi.Namespace{
956
+			ObjectMeta: kapi.ObjectMeta{
957
+				Annotations: map[string]string{},
958
+			},
959
+		}
960
+	}
961
+
962
+	fallbackNS := ns()
963
+	fallbackNS.Annotations[allocator.UIDRangeAnnotation] = "1/5"
964
+
965
+	emptyAnnotationNS := ns()
966
+	emptyAnnotationNS.Annotations[allocator.SupplementalGroupsAnnotation] = ""
967
+
968
+	badBlockNS := ns()
969
+	badBlockNS.Annotations[allocator.SupplementalGroupsAnnotation] = "foo"
970
+
971
+	goodNS := ns()
972
+	goodNS.Annotations[allocator.SupplementalGroupsAnnotation] = "1/5"
973
+
974
+	tests := map[string]struct {
975
+		ns         *kapi.Namespace
976
+		expected   []kapi.IDRange
977
+		shouldFail bool
978
+	}{
979
+		"fall back to uid if sup group doesn't exist": {
980
+			ns: fallbackNS,
981
+			expected: []kapi.IDRange{
982
+				{Min: 1, Max: 5},
983
+			},
984
+		},
985
+		"no annotation": {
986
+			ns:         ns(),
987
+			shouldFail: true,
988
+		},
989
+		"empty annotation": {
990
+			ns:         emptyAnnotationNS,
991
+			shouldFail: true,
992
+		},
993
+		"bad block": {
994
+			ns:         badBlockNS,
995
+			shouldFail: true,
996
+		},
997
+		"good sup group annotation": {
998
+			ns: goodNS,
999
+			expected: []kapi.IDRange{
1000
+				{Min: 1, Max: 5},
1001
+			},
1002
+		},
1003
+	}
1004
+
1005
+	for k, v := range tests {
1006
+		ranges, err := getPreallocatedSupplementalGroups(v.ns)
1007
+		if v.shouldFail && err == nil {
1008
+			t.Errorf("%s was expected to fail but received no error and ranges %v", k, ranges)
1009
+			continue
1010
+		}
1011
+
1012
+		if !v.shouldFail && err != nil {
1013
+			t.Errorf("%s had an unexpected error %v", k, err)
1014
+			continue
1015
+		}
1016
+
1017
+		if len(ranges) != len(v.expected) {
1018
+			t.Errorf("%s received unexpected number of ranges expected: %v, actual %v", k, v.expected, ranges)
1019
+		}
1020
+
1021
+		for _, r := range v.expected {
1022
+			if !hasRange(r, ranges) {
1023
+				t.Errorf("%s was missing range %v", k, r)
1024
+			}
1025
+		}
1026
+	}
1027
+}
1028
+
1029
+func hasRange(rng kapi.IDRange, ranges []kapi.IDRange) bool {
1030
+	for _, r := range ranges {
1031
+		if r.Min == rng.Min && r.Max == rng.Max {
1032
+			return true
1033
+		}
1034
+	}
1035
+	return false
1036
+}
... ...
@@ -3,8 +3,7 @@ package security
3 3
 const (
4 4
 	UIDRangeAnnotation = "openshift.io/sa.scc.uid-range"
5 5
 	// SupplementalGroupsAnnotation contains a comma delimited list of allocated supplemental groups
6
-	// for the namespace.  Groups are in the form of individual group ids or a range of group ids.
7
-	// Range format is supported in the form of a Block which supports {start}/{length} or {start}-{end}
6
+	// for the namespace.  Groups are in the form of a Block which supports {start}/{length} or {start}-{end}
8 7
 	SupplementalGroupsAnnotation = "openshift.io/sa.scc.supplemental-groups"
9 8
 	MCSAnnotation                = "openshift.io/sa.scc.mcs"
10 9
 	ValidatedSCCAnnotation       = "openshift.io/scc"
... ...
@@ -7,6 +7,7 @@ import (
7 7
 	_ "github.com/openshift/origin/test/extended/cli"
8 8
 	_ "github.com/openshift/origin/test/extended/images"
9 9
 	_ "github.com/openshift/origin/test/extended/router"
10
+	_ "github.com/openshift/origin/test/extended/security"
10 11
 
11 12
 	exutil "github.com/openshift/origin/test/extended/util"
12 13
 )
13 14
new file mode 100644
... ...
@@ -0,0 +1,182 @@
0
+package security
1
+
2
+import (
3
+	"fmt"
4
+	"strconv"
5
+	"strings"
6
+
7
+	"github.com/fsouza/go-dockerclient"
8
+	g "github.com/onsi/ginkgo"
9
+	o "github.com/onsi/gomega"
10
+
11
+	kapi "k8s.io/kubernetes/pkg/api"
12
+	"k8s.io/kubernetes/test/e2e"
13
+
14
+	testutil "github.com/openshift/origin/test/util"
15
+)
16
+
17
+var _ = g.Describe("security: supplemental groups", func() {
18
+	defer g.GinkgoRecover()
19
+
20
+	var (
21
+		f = e2e.NewFramework("security-supgroups")
22
+	)
23
+
24
+	g.Describe("Ensure supplemental groups propagate to docker", func() {
25
+		g.It("should propagate requested groups to the docker host config", func() {
26
+			// Before running any of this test we need to first check that
27
+			// the docker version being used supports the supplemental groups feature
28
+			g.By("ensuring the feature is supported")
29
+			dockerCli, err := testutil.NewDockerClient()
30
+			o.Expect(err).NotTo(o.HaveOccurred())
31
+
32
+			env, err := dockerCli.Version()
33
+			o.Expect(err).NotTo(o.HaveOccurred(), "error getting docker environment")
34
+			version := env.Get("Version")
35
+			supports, err, requiredVersion := supportsSupplementalGroups(version)
36
+			o.Expect(err).NotTo(o.HaveOccurred())
37
+
38
+			if !supports {
39
+				msg := fmt.Sprintf("skipping supplemental groups test, docker version %s does not meet required version %s", version, requiredVersion)
40
+				g.Skip(msg)
41
+			}
42
+
43
+			// on to the real test
44
+			fsGroup := int64(1111)
45
+			supGroup := int64(2222)
46
+
47
+			// create a pod that is requesting supplemental groups.  We request specific sup groups
48
+			// so that we can check for the exact values later and not rely on SCC allocation.
49
+			g.By("creating a pod that requests supplemental groups")
50
+			submittedPod := supGroupPod(fsGroup, supGroup)
51
+			_, err = f.Client.Pods(f.Namespace.Name).Create(submittedPod)
52
+			o.Expect(err).NotTo(o.HaveOccurred())
53
+			defer f.Client.Pods(f.Namespace.Name).Delete(submittedPod.Name, nil)
54
+
55
+			// we should have been admitted with the groups that we requested but if for any
56
+			// reason they are different we will fail.
57
+			g.By("retrieving the pod and ensuring groups are set")
58
+			retrievedPod, err := f.Client.Pods(f.Namespace.Name).Get(submittedPod.Name)
59
+			o.Expect(err).NotTo(o.HaveOccurred())
60
+			o.Expect(*retrievedPod.Spec.SecurityContext.FSGroup).To(o.Equal(*submittedPod.Spec.SecurityContext.FSGroup))
61
+			o.Expect(retrievedPod.Spec.SecurityContext.SupplementalGroups).To(o.Equal(submittedPod.Spec.SecurityContext.SupplementalGroups))
62
+
63
+			// wait for the pod to run so we can inspect it.
64
+			g.By("waiting for the pod to become running")
65
+			err = f.WaitForPodRunning(submittedPod.Name)
66
+			o.Expect(err).NotTo(o.HaveOccurred())
67
+
68
+			// find the docker id of our running container.
69
+			g.By("finding the docker container id on the pod")
70
+			retrievedPod, err = f.Client.Pods(f.Namespace.Name).Get(submittedPod.Name)
71
+			o.Expect(err).NotTo(o.HaveOccurred())
72
+			containerID, err := getContainerID(retrievedPod)
73
+			o.Expect(err).NotTo(o.HaveOccurred())
74
+
75
+			// now check the host config of the container which should have been updated by the
76
+			// kubelet.  If that is good then ensure we have the groups we expected.
77
+			g.By("inspecting the container")
78
+			dockerContainer, err := dockerCli.InspectContainer(containerID)
79
+			o.Expect(err).NotTo(o.HaveOccurred())
80
+
81
+			g.By("ensuring the host config has GroupAdd")
82
+			groupAdd := dockerContainer.HostConfig.GroupAdd
83
+			o.Expect(groupAdd).ToNot(o.BeEmpty(), fmt.Sprintf("groupAdd on host config was %v", groupAdd))
84
+
85
+			g.By("ensuring the groups are set")
86
+			o.Expect(configHasGroup(fsGroup, dockerContainer.HostConfig)).To(o.Equal(true), fmt.Sprintf("fsGroup should exist on host config: %v", groupAdd))
87
+			o.Expect(configHasGroup(supGroup, dockerContainer.HostConfig)).To(o.Equal(true), fmt.Sprintf("supGroup should exist on host config: %v", groupAdd))
88
+		})
89
+
90
+	})
91
+})
92
+
93
+// supportsSupplementalGroups does a check on the docker version to ensure it is at least
94
+// 1.8.2.  This could still fail if the version does not have the /etc/groups patch
95
+// but it will fail when launching the pod so this is as safe as we can get.
96
+func supportsSupplementalGroups(dockerVersion string) (bool, error, string) {
97
+	parts := strings.Split(dockerVersion, ".")
98
+
99
+	var (
100
+		requiredMajor   = 1
101
+		requiredMinor   = 8
102
+		requiredPatch   = 2
103
+		requiredVersion = fmt.Sprintf("%d.%d.%d", requiredMajor, requiredMinor, requiredPatch)
104
+
105
+		major       = 0
106
+		minor       = 0
107
+		patch       = 0
108
+		err   error = nil
109
+	)
110
+	if len(parts) > 0 {
111
+		major, err = strconv.Atoi(parts[0])
112
+		if err != nil {
113
+			return false, err, requiredVersion
114
+		}
115
+	}
116
+
117
+	if len(parts) > 1 {
118
+		minor, err = strconv.Atoi(parts[1])
119
+		if err != nil {
120
+			return false, err, requiredVersion
121
+		}
122
+	}
123
+
124
+	if len(parts) > 2 {
125
+		patch, err = strconv.Atoi(parts[2])
126
+		if err != nil {
127
+			return false, err, requiredVersion
128
+		}
129
+	}
130
+
131
+	// requires at least 1.8.2
132
+	if major > requiredMajor || (major == requiredMajor && minor > requiredMinor) ||
133
+		(major == requiredMajor && minor == requiredMinor && patch >= requiredPatch) {
134
+		return true, nil, requiredVersion
135
+	}
136
+
137
+	return false, nil, requiredVersion
138
+}
139
+
140
+// configHasGroup is a helper to ensure that a group is in the host config's addGroups field.
141
+func configHasGroup(group int64, config *docker.HostConfig) bool {
142
+	strGroup := strconv.FormatInt(group, 10)
143
+	for _, g := range config.GroupAdd {
144
+		if g == strGroup {
145
+			return true
146
+		}
147
+	}
148
+	return false
149
+}
150
+
151
+// getContainerID is a helper to parse the docker container id from a status.
152
+func getContainerID(p *kapi.Pod) (string, error) {
153
+	for _, status := range p.Status.ContainerStatuses {
154
+		if len(status.ContainerID) > 0 {
155
+			containerID := strings.Replace(status.ContainerID, "docker://", "", -1)
156
+			return containerID, nil
157
+		}
158
+	}
159
+	return "", fmt.Errorf("unable to find container id on pod")
160
+}
161
+
162
+// supGroupPod generates the pod requesting supplemental groups.
163
+func supGroupPod(fsGroup int64, supGroup int64) *kapi.Pod {
164
+	return &kapi.Pod{
165
+		ObjectMeta: kapi.ObjectMeta{
166
+			Name: "supplemental-groups",
167
+		},
168
+		Spec: kapi.PodSpec{
169
+			SecurityContext: &kapi.PodSecurityContext{
170
+				FSGroup:            &fsGroup,
171
+				SupplementalGroups: []int64{supGroup},
172
+			},
173
+			Containers: []kapi.Container{
174
+				{
175
+					Name:  "supplemental-groups",
176
+					Image: "openshift/origin-pod",
177
+				},
178
+			},
179
+		},
180
+	}
181
+}