Browse code

make userAgentMatching take a set of required and deny regexes

deads2k authored on 2016/03/10 00:13:22
Showing 10 changed files
... ...
@@ -81,15 +81,6 @@ func fuzzInternalObject(t *testing.T, forVersion unversioned.GroupVersion, item
81 81
 				obj.PodEvictionTimeout = "5m"
82 82
 			}
83 83
 		},
84
-		func(obj *configapi.LegacyClientPolicyConfig, c fuzz.Continue) {
85
-			c.FuzzNoCustom(obj)
86
-			if len(obj.LegacyClientPolicy) == 0 {
87
-				obj.LegacyClientPolicy = configapi.AllowAll
88
-			}
89
-			if len(obj.RestrictedHTTPVerbs) == 0 {
90
-				obj.RestrictedHTTPVerbs = []string{"PUT", "POST"}
91
-			}
92
-		},
93 84
 		func(obj *configapi.NodeConfig, c fuzz.Continue) {
94 85
 			c.FuzzNoCustom(obj)
95 86
 			// Defaults/migrations for NetworkConfig
... ...
@@ -318,28 +318,38 @@ type PolicyConfig struct {
318 318
 	// OpenShiftInfrastructureNamespace is the namespace where OpenShift infrastructure resources live (like controller service accounts)
319 319
 	OpenShiftInfrastructureNamespace string
320 320
 
321
-	// LegacyClientPolicyConfig controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!
322
-	LegacyClientPolicyConfig LegacyClientPolicyConfig
321
+	// UserAgentMatchingConfig controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!
322
+	UserAgentMatchingConfig UserAgentMatchingConfig
323 323
 }
324 324
 
325
-type LegacyClientPolicyConfig struct {
326
-	// LegacyClientPolicy controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!
327
-	// The default is AllowAll
328
-	LegacyClientPolicy LegacyClientPolicy
329
-	// RestrictedHTTPVerbs specifies which HTTP verbs are restricted.  By default this is PUT and POST
330
-	RestrictedHTTPVerbs []string
325
+// UserAgentMatchingConfig controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!
326
+type UserAgentMatchingConfig struct {
327
+	// If this list is non-empty, then a User-Agent must match one of the UserAgentRegexes to be allowed
328
+	RequiredClients []UserAgentMatchRule
329
+
330
+	// If this list is non-empty, then a User-Agent must not match any of the UserAgentRegexes
331
+	DeniedClients []UserAgentDenyRule
332
+
333
+	// DefaultRejectionMessage is the message shown when rejecting a client.  If it is not a set, a generic message is given.
334
+	DefaultRejectionMessage string
331 335
 }
332 336
 
333
-type LegacyClientPolicy string
337
+// UserAgentMatchRule describes how to match a given request based on User-Agent and HTTPVerb
338
+type UserAgentMatchRule struct {
339
+	// UserAgentRegex is a regex that is checked against the User-Agent.
340
+	Regex string
334 341
 
335
-var (
336
-	// AllowAll does not prevent any kinds of client version skew
337
-	AllowAll LegacyClientPolicy = "allow-all"
338
-	// DenyOldClients prevents older clients (but not newer ones) from issuing stomping requests
339
-	DenyOldClients LegacyClientPolicy = "deny-old-clients"
340
-	// DenySkewedClients prevents any non-matching client from issuing stomping requests
341
-	DenySkewedClients LegacyClientPolicy = "deny-skewed-clients"
342
-)
342
+	// HTTPVerbs specifies which HTTP verbs should be matched.  An empty list means "match all verbs".
343
+	HTTPVerbs []string
344
+}
345
+
346
+// UserAgentDenyRule adds a rejection message that can be used to help a user figure out how to get an approved client
347
+type UserAgentDenyRule struct {
348
+	UserAgentMatchRule
349
+
350
+	// RejectionMessage is the message shown when rejecting a client.  If it is not a set, the default message is used.
351
+	RejectionMessage string
352
+}
343 353
 
344 354
 // MasterNetworkConfig to be passed to the compiled in network plugin
345 355
 type MasterNetworkConfig struct {
... ...
@@ -65,14 +65,6 @@ func addDefaultingFuncs(scheme *runtime.Scheme) {
65 65
 				obj.PodEvictionTimeout = "5m"
66 66
 			}
67 67
 		},
68
-		func(obj *LegacyClientPolicyConfig) {
69
-			if len(obj.LegacyClientPolicy) == 0 {
70
-				obj.LegacyClientPolicy = AllowAll
71
-			}
72
-			if obj.LegacyClientPolicy != AllowAll && len(obj.RestrictedHTTPVerbs) == 0 {
73
-				obj.RestrictedHTTPVerbs = []string{"PUT", "POST"}
74
-			}
75
-		},
76 68
 		func(obj *NodeConfig) {
77 69
 			// Defaults/migrations for NetworkConfig
78 70
 			if len(obj.NetworkConfig.NetworkPluginName) == 0 {
... ...
@@ -362,16 +362,6 @@ func (LDAPSyncConfig) SwaggerDoc() map[string]string {
362 362
 	return map_LDAPSyncConfig
363 363
 }
364 364
 
365
-var map_LegacyClientPolicyConfig = map[string]string{
366
-	"":                    "LegacyClientPolicyConfig holds configuration options for preventing *opt-in* clients using some HTTP verbs when talking to the API",
367
-	"legacyClientPolicy":  "LegacyClientPolicy controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS! The default is AllowAll",
368
-	"restrictedHTTPVerbs": "RestrictedHTTPVerbs specifies which HTTP verbs are restricted.  By default this is PUT and POST",
369
-}
370
-
371
-func (LegacyClientPolicyConfig) SwaggerDoc() map[string]string {
372
-	return map_LegacyClientPolicyConfig
373
-}
374
-
375 365
 var map_MasterClients = map[string]string{
376 366
 	"": "MasterClients holds references to `.kubeconfig` files that qualify master clients for OpenShift and Kubernetes",
377 367
 	"openshiftLoopbackKubeConfig":  "OpenShiftLoopbackKubeConfig is a .kubeconfig filename for system components to loopback to this master",
... ...
@@ -565,7 +555,7 @@ var map_PolicyConfig = map[string]string{
565 565
 	"bootstrapPolicyFile":               "BootstrapPolicyFile points to a template that contains roles and rolebindings that will be created if no policy object exists in the master namespace",
566 566
 	"openshiftSharedResourcesNamespace": "OpenShiftSharedResourcesNamespace is the namespace where shared OpenShift resources live (like shared templates)",
567 567
 	"openshiftInfrastructureNamespace":  "OpenShiftInfrastructureNamespace is the namespace where OpenShift infrastructure resources live (like controller service accounts)",
568
-	"legacyClientPolicyConfig":          "LegacyClientPolicyConfig controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!",
568
+	"userAgentMatchingConfig":           "UserAgentMatchingConfig controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!",
569 569
 }
570 570
 
571 571
 func (PolicyConfig) SwaggerDoc() map[string]string {
... ...
@@ -730,3 +720,33 @@ var map_TokenConfig = map[string]string{
730 730
 func (TokenConfig) SwaggerDoc() map[string]string {
731 731
 	return map_TokenConfig
732 732
 }
733
+
734
+var map_UserAgentDenyRule = map[string]string{
735
+	"":                 "UserAgentDenyRule adds a rejection message that can be used to help a user figure out how to get an approved client",
736
+	"rejectionMessage": "RejectionMessage is the message shown when rejecting a client.  If it is not a set, the default message is used.",
737
+}
738
+
739
+func (UserAgentDenyRule) SwaggerDoc() map[string]string {
740
+	return map_UserAgentDenyRule
741
+}
742
+
743
+var map_UserAgentMatchRule = map[string]string{
744
+	"":          "UserAgentMatchRule describes how to match a given request based on User-Agent and HTTPVerb",
745
+	"regex":     "UserAgentRegex is a regex that is checked against the User-Agent. Known variants of oc clients 1. oc accessing kube resources: oc/v1.2.0 (linux/amd64) kubernetes/bc4550d 2. oc accessing openshift resources: oc/v1.1.3 (linux/amd64) openshift/b348c2f 3. openshift kubectl accessing kube resources:  openshift/v1.2.0 (linux/amd64) kubernetes/bc4550d 4. openshit kubectl accessing openshift resources: openshift/v1.1.3 (linux/amd64) openshift/b348c2f 5. oadm accessing kube resources: oadm/v1.2.0 (linux/amd64) kubernetes/bc4550d 6. oadm accessing openshift resources: oadm/v1.1.3 (linux/amd64) openshift/b348c2f 7. openshift cli accessing kube resources: openshift/v1.2.0 (linux/amd64) kubernetes/bc4550d 8. openshift cli accessing openshift resources: openshift/v1.1.3 (linux/amd64) openshift/b348c2f",
746
+	"httpVerbs": "HTTPVerbs specifies which HTTP verbs should be matched.  An empty list means \"match all verbs\".",
747
+}
748
+
749
+func (UserAgentMatchRule) SwaggerDoc() map[string]string {
750
+	return map_UserAgentMatchRule
751
+}
752
+
753
+var map_UserAgentMatchingConfig = map[string]string{
754
+	"":                        "UserAgentMatchingConfig controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!",
755
+	"requiredClients":         "If this list is non-empty, then a User-Agent must match one of the UserAgentRegexes to be allowed",
756
+	"deniedClients":           "If this list is non-empty, then a User-Agent must not match any of the UserAgentRegexes",
757
+	"defaultRejectionMessage": "DefaultRejectionMessage is the message shown when rejecting a client.  If it is not a set, a generic message is given.",
758
+}
759
+
760
+func (UserAgentMatchingConfig) SwaggerDoc() map[string]string {
761
+	return map_UserAgentMatchingConfig
762
+}
... ...
@@ -270,29 +270,47 @@ type PolicyConfig struct {
270 270
 	// OpenShiftInfrastructureNamespace is the namespace where OpenShift infrastructure resources live (like controller service accounts)
271 271
 	OpenShiftInfrastructureNamespace string `json:"openshiftInfrastructureNamespace"`
272 272
 
273
-	// LegacyClientPolicyConfig controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!
274
-	LegacyClientPolicyConfig LegacyClientPolicyConfig `json:"legacyClientPolicyConfig"`
273
+	// UserAgentMatchingConfig controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!
274
+	UserAgentMatchingConfig UserAgentMatchingConfig `json:"userAgentMatchingConfig"`
275 275
 }
276 276
 
277
-// LegacyClientPolicyConfig holds configuration options for preventing *opt-in* clients using some HTTP verbs when talking to the API
278
-type LegacyClientPolicyConfig struct {
279
-	// LegacyClientPolicy controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!
280
-	// The default is AllowAll
281
-	LegacyClientPolicy LegacyClientPolicy `json:"legacyClientPolicy"`
282
-	// RestrictedHTTPVerbs specifies which HTTP verbs are restricted.  By default this is PUT and POST
283
-	RestrictedHTTPVerbs []string `json:"restrictedHTTPVerbs"`
277
+// UserAgentMatchingConfig controls how API calls from *voluntarily* identifying clients will be handled.  THIS DOES NOT DEFEND AGAINST MALICIOUS CLIENTS!
278
+type UserAgentMatchingConfig struct {
279
+	// If this list is non-empty, then a User-Agent must match one of the UserAgentRegexes to be allowed
280
+	RequiredClients []UserAgentMatchRule `json:"requiredClients"`
281
+
282
+	// If this list is non-empty, then a User-Agent must not match any of the UserAgentRegexes
283
+	DeniedClients []UserAgentDenyRule `json:"deniedClients"`
284
+
285
+	// DefaultRejectionMessage is the message shown when rejecting a client.  If it is not a set, a generic message is given.
286
+	DefaultRejectionMessage string `json:"defaultRejectionMessage"`
287
+}
288
+
289
+// UserAgentMatchRule describes how to match a given request based on User-Agent and HTTPVerb
290
+type UserAgentMatchRule struct {
291
+	// UserAgentRegex is a regex that is checked against the User-Agent.
292
+	// Known variants of oc clients
293
+	// 1. oc accessing kube resources: oc/v1.2.0 (linux/amd64) kubernetes/bc4550d
294
+	// 2. oc accessing openshift resources: oc/v1.1.3 (linux/amd64) openshift/b348c2f
295
+	// 3. openshift kubectl accessing kube resources:  openshift/v1.2.0 (linux/amd64) kubernetes/bc4550d
296
+	// 4. openshit kubectl accessing openshift resources: openshift/v1.1.3 (linux/amd64) openshift/b348c2f
297
+	// 5. oadm accessing kube resources: oadm/v1.2.0 (linux/amd64) kubernetes/bc4550d
298
+	// 6. oadm accessing openshift resources: oadm/v1.1.3 (linux/amd64) openshift/b348c2f
299
+	// 7. openshift cli accessing kube resources: openshift/v1.2.0 (linux/amd64) kubernetes/bc4550d
300
+	// 8. openshift cli accessing openshift resources: openshift/v1.1.3 (linux/amd64) openshift/b348c2f
301
+	Regex string `json:"regex"`
302
+
303
+	// HTTPVerbs specifies which HTTP verbs should be matched.  An empty list means "match all verbs".
304
+	HTTPVerbs []string `json:"httpVerbs"`
284 305
 }
285 306
 
286
-type LegacyClientPolicy string
307
+// UserAgentDenyRule adds a rejection message that can be used to help a user figure out how to get an approved client
308
+type UserAgentDenyRule struct {
309
+	UserAgentMatchRule `json:", inline"`
287 310
 
288
-var (
289
-	// AllowAll does not prevent any kinds of client version skew
290
-	AllowAll LegacyClientPolicy = "allow-all"
291
-	// DenyOldClients prevents older clients (but not newer ones) from issuing stomping requests
292
-	DenyOldClients LegacyClientPolicy = "deny-old-clients"
293
-	// DenySkewedClients prevents any non-matching client from issuing stomping requests
294
-	DenySkewedClients LegacyClientPolicy = "deny-skewed-clients"
295
-)
311
+	// RejectionMessage is the message shown when rejecting a client.  If it is not a set, the default message is used.
312
+	RejectionMessage string `json:"rejectionMessage"`
313
+}
296 314
 
297 315
 // RoutingConfig holds the necessary configuration options for routing to subdomains
298 316
 type RoutingConfig struct {
... ...
@@ -421,11 +421,12 @@ oauthConfig:
421 421
 pauseControllers: false
422 422
 policyConfig:
423 423
   bootstrapPolicyFile: ""
424
-  legacyClientPolicyConfig:
425
-    legacyClientPolicy: ""
426
-    restrictedHTTPVerbs: null
427 424
   openshiftInfrastructureNamespace: ""
428 425
   openshiftSharedResourcesNamespace: ""
426
+  userAgentMatchingConfig:
427
+    defaultRejectionMessage: ""
428
+    deniedClients: null
429
+    requiredClients: null
429 430
 projectConfig:
430 431
   defaultNodeSelector: ""
431 432
   projectRequestMessage: ""
... ...
@@ -497,6 +497,19 @@ func ValidatePolicyConfig(config api.PolicyConfig, fldPath *field.Path) field.Er
497 497
 	allErrs = append(allErrs, ValidateNamespace(config.OpenShiftSharedResourcesNamespace, fldPath.Child("openShiftSharedResourcesNamespace"))...)
498 498
 	allErrs = append(allErrs, ValidateNamespace(config.OpenShiftInfrastructureNamespace, fldPath.Child("openShiftInfrastructureNamespace"))...)
499 499
 
500
+	for i, matchingRule := range config.UserAgentMatchingConfig.DeniedClients {
501
+		_, err := regexp.Compile(matchingRule.Regex)
502
+		if err != nil {
503
+			allErrs = append(allErrs, field.Invalid(fldPath.Child("userAgentMatchingConfig", "deniedClients").Index(i), matchingRule.Regex, err.Error()))
504
+		}
505
+	}
506
+	for i, matchingRule := range config.UserAgentMatchingConfig.RequiredClients {
507
+		_, err := regexp.Compile(matchingRule.Regex)
508
+		if err != nil {
509
+			allErrs = append(allErrs, field.Invalid(fldPath.Child("userAgentMatchingConfig", "requiredClients").Index(i), matchingRule.Regex, err.Error()))
510
+		}
511
+	}
512
+
500 513
 	return allErrs
501 514
 }
502 515
 
... ...
@@ -8,10 +8,9 @@ import (
8 8
 	"net/http"
9 9
 	"regexp"
10 10
 	"sort"
11
-	"strings"
12 11
 
13
-	"github.com/coreos/go-semver/semver"
14 12
 	restful "github.com/emicklei/go-restful"
13
+	"github.com/golang/glog"
15 14
 
16 15
 	kapi "k8s.io/kubernetes/pkg/api"
17 16
 	kapierrors "k8s.io/kubernetes/pkg/api/errors"
... ...
@@ -19,12 +18,10 @@ import (
19 19
 	"k8s.io/kubernetes/pkg/apiserver"
20 20
 	"k8s.io/kubernetes/pkg/runtime"
21 21
 	"k8s.io/kubernetes/pkg/util/sets"
22
-	kversion "k8s.io/kubernetes/pkg/version"
23 22
 
24 23
 	"github.com/openshift/origin/pkg/authorization/authorizer"
25 24
 	configapi "github.com/openshift/origin/pkg/cmd/server/api"
26 25
 	"github.com/openshift/origin/pkg/util/httprequest"
27
-	"github.com/openshift/origin/pkg/version"
28 26
 )
29 27
 
30 28
 // TODO We would like to use the IndexHandler from k8s but we do not yet have a
... ...
@@ -178,85 +175,100 @@ func namespacingFilter(handler http.Handler, contextMapper kapi.RequestContextMa
178 178
 	})
179 179
 }
180 180
 
181
-// variants I know I have to worry about
182
-// 1. oc kube resources: oc/v1.2.0 (linux/amd64) kubernetes/bc4550d
183
-// 2. oc openshift resources: oc/v1.1.3 (linux/amd64) openshift/b348c2f
184
-// 3. openshift kubectl kube resources:  openshift/v1.2.0 (linux/amd64) kubernetes/bc4550d
185
-// 4. openshit kubectl openshift resources: openshift/v1.1.3 (linux/amd64) openshift/b348c2f
186
-// 5. oadm kube resources: oadm/v1.2.0 (linux/amd64) kubernetes/bc4550d
187
-// 6. oadm openshift resources: oadm/v1.1.3 (linux/amd64) openshift/b348c2f
188
-// 7. openshift cli kube resources: openshift/v1.2.0 (linux/amd64) kubernetes/bc4550d
189
-// 8. openshift cli openshift resources: openshift/v1.1.3 (linux/amd64) openshift/b348c2f
190
-var (
191
-	kubeStyleUserAgent      = regexp.MustCompile(`\w+/v([\w\.]+) \(.+/.+\) kubernetes/\w{7}`)
192
-	openshiftStyleUserAgent = regexp.MustCompile(`\w+/v([\w\.]+) \(.+/.+\) openshift/\w{7}`)
193
-)
181
+type userAgentFilter struct {
182
+	regex   *regexp.Regexp
183
+	message string
184
+	verbs   sets.String
185
+}
186
+
187
+func newUserAgentFilter(config configapi.UserAgentMatchRule) (userAgentFilter, error) {
188
+	regex, err := regexp.Compile(config.Regex)
189
+	if err != nil {
190
+		return userAgentFilter{}, err
191
+	}
192
+	userAgentFilter := userAgentFilter{regex: regex, verbs: sets.NewString(config.HTTPVerbs...)}
193
+
194
+	return userAgentFilter, nil
195
+}
196
+
197
+func (f *userAgentFilter) matches(verb, userAgent string) bool {
198
+	if len(f.verbs) > 0 && !f.verbs.Has(verb) {
199
+		return false
200
+	}
201
+
202
+	return f.regex.MatchString(userAgent)
203
+}
194 204
 
195 205
 // versionSkewFilter adds a filter that may deny requests from skewed
196 206
 // oc clients, since we know that those clients will strip unknown fields which can lead to unexpected outcomes
197
-func (c *MasterConfig) versionSkewFilter(openshiftBinaryInfo version.Info, kubeBinaryInfo kversion.Info, handler http.Handler) http.Handler {
198
-	skewedClientPolicy := c.Options.PolicyConfig.LegacyClientPolicyConfig.LegacyClientPolicy
199
-	if skewedClientPolicy == configapi.AllowAll {
207
+func (c *MasterConfig) versionSkewFilter(handler http.Handler) http.Handler {
208
+	infoResolver := &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi")}
209
+
210
+	filterConfig := c.Options.PolicyConfig.UserAgentMatchingConfig
211
+	if len(filterConfig.RequiredClients) == 0 && len(filterConfig.DeniedClients) == 0 {
200 212
 		return handler
201 213
 	}
202
-	seg := strings.SplitN(openshiftBinaryInfo.GitVersion, "-", 2)
203
-	openshiftServerVersion := seg[0][1:]
204
-	seg = strings.SplitN(kubeBinaryInfo.GitVersion, "-", 2)
205
-	kubeServerVersion := seg[0][1:]
206 214
 
207
-	restrictedVerbs := sets.NewString(c.Options.PolicyConfig.LegacyClientPolicyConfig.RestrictedHTTPVerbs...)
208
-
209
-	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
210
-		if !restrictedVerbs.Has(req.Method) {
211
-			handler.ServeHTTP(w, req)
212
-			return
213
-		}
215
+	defaultMessage := filterConfig.DefaultRejectionMessage
216
+	if len(defaultMessage) == 0 {
217
+		defaultMessage = "the cluster administrator has disabled access for this client, please upgrade or consult your administrator"
218
+	}
214 219
 
215
-		userAgent := req.Header.Get("User-Agent")
216
-		if len(userAgent) == 0 {
217
-			handler.ServeHTTP(w, req)
218
-			return
220
+	// the structure of the legacyClientPolicyConfig is pretty easy to write, but its inefficient to use at runtime
221
+	// pre-process the config elements to make a more efficicent structure.
222
+	allowedFilters := []userAgentFilter{}
223
+	deniedFilters := []userAgentFilter{}
224
+	for _, config := range filterConfig.RequiredClients {
225
+		userAgentFilter, err := newUserAgentFilter(config)
226
+		if err != nil {
227
+			glog.Errorf("Failure to compile User-Agent regex %v: %v", config.Regex, err)
228
+			continue
219 229
 		}
220 230
 
221
-		clientVersion := ""
222
-		serverVersion := ""
223
-		if submatches := kubeStyleUserAgent.FindStringSubmatch(userAgent); len(submatches) == 2 {
224
-			clientVersion = submatches[1]
225
-			serverVersion = kubeServerVersion
231
+		allowedFilters = append(allowedFilters, userAgentFilter)
232
+	}
233
+	for _, config := range filterConfig.DeniedClients {
234
+		userAgentFilter, err := newUserAgentFilter(config.UserAgentMatchRule)
235
+		if err != nil {
236
+			glog.Errorf("Failure to compile User-Agent regex %v: %v", config.Regex, err)
237
+			continue
226 238
 		}
227
-		if submatches := openshiftStyleUserAgent.FindStringSubmatch(userAgent); len(submatches) == 2 {
228
-			clientVersion = submatches[1]
229
-			serverVersion = openshiftServerVersion
239
+		userAgentFilter.message = config.RejectionMessage
240
+		if len(userAgentFilter.message) == 0 {
241
+			userAgentFilter.message = defaultMessage
230 242
 		}
231
-		if len(clientVersion) == 0 {
243
+
244
+		deniedFilters = append(deniedFilters, userAgentFilter)
245
+	}
246
+
247
+	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
248
+		if requestInfo, err := infoResolver.GetRequestInfo(req); err == nil && !requestInfo.IsResourceRequest {
232 249
 			handler.ServeHTTP(w, req)
233 250
 			return
234 251
 		}
235 252
 
236
-		switch skewedClientPolicy {
237
-		case configapi.DenyOldClients:
238
-			serverSemVer, err := semver.NewVersion(serverVersion)
239
-			if err != nil {
240
-				handler.ServeHTTP(w, req)
241
-				return
242
-			}
243
-			clientSemVer, err := semver.NewVersion(clientVersion)
244
-			if err != nil {
245
-				handler.ServeHTTP(w, req)
246
-				return
253
+		userAgent := req.Header.Get("User-Agent")
254
+
255
+		if len(allowedFilters) > 0 {
256
+			foundMatch := false
257
+			for _, filter := range allowedFilters {
258
+				if filter.matches(req.Method, userAgent) {
259
+					foundMatch = true
260
+					break
261
+				}
247 262
 			}
248 263
 
249
-			if clientSemVer.LessThan(*serverSemVer) {
250
-				forbidden(fmt.Sprintf("userVersion %v is older than the server version %v; mutation is denied", clientVersion, serverVersion), nil, w, req)
264
+			if !foundMatch {
265
+				forbidden(defaultMessage, nil, w, req)
251 266
 				return
252 267
 			}
268
+		}
253 269
 
254
-		case configapi.DenySkewedClients:
255
-			if clientVersion != serverVersion {
256
-				forbidden(fmt.Sprintf("userVersion %v is different than the server version %v; mutation is denied", clientVersion, serverVersion), nil, w, req)
270
+		for _, filter := range deniedFilters {
271
+			if filter.matches(req.Method, userAgent) {
272
+				forbidden(filter.message, nil, w, req)
257 273
 				return
258 274
 			}
259
-
260 275
 		}
261 276
 
262 277
 		handler.ServeHTTP(w, req)
... ...
@@ -7,10 +7,7 @@ import (
7 7
 	"strings"
8 8
 	"testing"
9 9
 
10
-	kversion "k8s.io/kubernetes/pkg/version"
11
-
12 10
 	configapi "github.com/openshift/origin/pkg/cmd/server/api"
13
-	"github.com/openshift/origin/pkg/version"
14 11
 )
15 12
 
16 13
 var (
... ...
@@ -25,12 +22,13 @@ var (
25 25
 
26 26
 	olderOCKubeResources                 = "oc/v1.1.10 (linux/amd64) kubernetes/bc4550d"
27 27
 	olderOCOriginResources               = "oc/v1.1.1 (linux/amd64) openshift/b348c2f"
28
+	oldestOCOriginResources              = "oc/v1.0.1 (linux/amd64) openshift/b348c2f"
28 29
 	olderOpenshiftKubectlKubeResources   = "openshift/v1.1.10 (linux/amd64) kubernetes/bc4550d"
29 30
 	olderOpenshiftKubectlOriginResources = "openshift/v1.1.1 (linux/amd64) openshift/b348c2f"
30 31
 	olderOADMKubeResources               = "oadm/v1.1.10 (linux/amd64) kubernetes/bc4550d"
31 32
 	olderOADMOriginResources             = "oadm/v1.1.1 (linux/amd64) openshift/b348c2f"
32 33
 	olderVersionUserAgents               = []string{
33
-		olderOCKubeResources, olderOCOriginResources, olderOpenshiftKubectlKubeResources, olderOpenshiftKubectlOriginResources, olderOADMKubeResources, olderOADMOriginResources}
34
+		olderOCKubeResources, olderOCOriginResources, oldestOCOriginResources, olderOpenshiftKubectlKubeResources, olderOpenshiftKubectlOriginResources, olderOADMKubeResources, olderOADMOriginResources}
34 35
 
35 36
 	newerOCKubeResources                 = "oc/v1.2.1 (linux/amd64) kubernetes/bc4550d"
36 37
 	newerOCOriginResources               = "oc/v1.1.4 (linux/amd64) openshift/b348c2f"
... ...
@@ -43,10 +41,24 @@ var (
43 43
 
44 44
 	notOCVersion = "something else"
45 45
 
46
-	openshiftServerVersion = version.Info{GitVersion: "v1.1.3"}
47
-	kubeServerVersion      = kversion.Info{GitVersion: "v1.2.0"}
46
+	openshiftServerVersion = `v1\.1\.3`
47
+	kubeServerVersion      = `v1\.2\.0`
48 48
 )
49 49
 
50
+// variants I know I have to worry about
51
+// 1. oc kube resources: oc/v1.2.0 (linux/amd64) kubernetes/bc4550d
52
+// 2. oc openshift resources: oc/v1.1.3 (linux/amd64) openshift/b348c2f
53
+// 3. openshift kubectl kube resources:  openshift/v1.2.0 (linux/amd64) kubernetes/bc4550d
54
+// 4. openshit kubectl openshift resources: openshift/v1.1.3 (linux/amd64) openshift/b348c2f
55
+// 5. oadm kube resources: oadm/v1.2.0 (linux/amd64) kubernetes/bc4550d
56
+// 6. oadm openshift resources: oadm/v1.1.3 (linux/amd64) openshift/b348c2f
57
+// 7. openshift cli kube resources: openshift/v1.2.0 (linux/amd64) kubernetes/bc4550d
58
+// 8. openshift cli openshift resources: openshift/v1.1.3 (linux/amd64) openshift/b348c2f
59
+// var (
60
+// 	kubeStyleUserAgent      = regexp.MustCompile(`\w+/v([\w\.]+) \(.+/.+\) kubernetes/\w{7}`)
61
+// 	openshiftStyleUserAgent = regexp.MustCompile(`\w+/v([\w\.]+) \(.+/.+\) openshift/\w{7}`)
62
+// )
63
+
50 64
 type versionSkewTestCase struct {
51 65
 	name           string
52 66
 	userAgents     []string
... ...
@@ -55,25 +67,6 @@ type versionSkewTestCase struct {
55 55
 }
56 56
 
57 57
 func (tc versionSkewTestCase) Run(url string, t *testing.T) {
58
-	// gets always succeed
59
-	for _, userAgent := range tc.userAgents {
60
-		req, err := http.NewRequest("GET", url, nil)
61
-		if err != nil {
62
-			t.Errorf("%s: unexpected error: %v", tc.name, err)
63
-			return
64
-		}
65
-		req.Header.Add("User-Agent", userAgent)
66
-		resp, err := http.DefaultClient.Do(req)
67
-		if err != nil {
68
-			t.Errorf("%s: unexpected error: %v", tc.name, err)
69
-			return
70
-		}
71
-		if resp.StatusCode != http.StatusOK {
72
-			t.Errorf("%s: unexpected status: %v", tc.name, resp.StatusCode)
73
-			return
74
-		}
75
-	}
76
-
77 58
 	for _, method := range tc.methods {
78 59
 		for _, userAgent := range tc.userAgents {
79 60
 			req, err := http.NewRequest(method, url, nil)
... ...
@@ -89,13 +82,13 @@ func (tc versionSkewTestCase) Run(url string, t *testing.T) {
89 89
 			}
90 90
 			if len(tc.failureMessage) == 0 {
91 91
 				if resp.StatusCode != http.StatusOK {
92
-					t.Errorf("%s: unexpected status: %v", tc.name, resp.StatusCode)
92
+					t.Errorf("%s: %s: unexpected status: %v", tc.name, userAgent, resp.StatusCode)
93 93
 					return
94 94
 				}
95 95
 
96 96
 			} else {
97 97
 				if resp.StatusCode != http.StatusForbidden {
98
-					t.Errorf("%s: unexpected status: %v", tc.name, resp.StatusCode)
98
+					t.Errorf("%s: %s: unexpected status: %v", tc.name, userAgent, resp.StatusCode)
99 99
 					return
100 100
 				}
101 101
 
... ...
@@ -115,14 +108,16 @@ func (tc versionSkewTestCase) Run(url string, t *testing.T) {
115 115
 
116 116
 }
117 117
 
118
-func TestVersionSkewFilterAllowAll(t *testing.T) {
119
-	verbs := []string{"PUT", "POST"}
118
+func TestVersionSkewFilterDenyOld(t *testing.T) {
119
+	verbs := []string{"PATCH", "POST"}
120 120
 	doNothingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
121 121
 	})
122 122
 	config := MasterConfig{}
123
-	config.Options.PolicyConfig.LegacyClientPolicyConfig.LegacyClientPolicy = configapi.AllowAll
124
-	config.Options.PolicyConfig.LegacyClientPolicyConfig.RestrictedHTTPVerbs = verbs
125
-	server := httptest.NewServer(config.versionSkewFilter(openshiftServerVersion, kubeServerVersion, doNothingHandler))
123
+	config.Options.PolicyConfig.UserAgentMatchingConfig.DeniedClients = []configapi.UserAgentDenyRule{
124
+		{UserAgentMatchRule: configapi.UserAgentMatchRule{Regex: `\w+/v1\.1\.10 \(.+/.+\) kubernetes/\w{7}`, HTTPVerbs: verbs}, RejectionMessage: "rejected for reasons!"},
125
+		{UserAgentMatchRule: configapi.UserAgentMatchRule{Regex: `\w+/v(?:(?:1\.1\.1)|(?:1\.0\.1)) \(.+/.+\) openshift/\w{7}`, HTTPVerbs: verbs}, RejectionMessage: "rejected for reasons!"},
126
+	}
127
+	server := httptest.NewServer(config.versionSkewFilter(doNothingHandler))
126 128
 	defer server.Close()
127 129
 
128 130
 	testCases := []versionSkewTestCase{
... ...
@@ -137,9 +132,10 @@ func TestVersionSkewFilterAllowAll(t *testing.T) {
137 137
 			methods:    verbs,
138 138
 		},
139 139
 		{
140
-			name:       "older",
141
-			userAgents: olderVersionUserAgents,
142
-			methods:    verbs,
140
+			name:           "older",
141
+			userAgents:     olderVersionUserAgents,
142
+			failureMessage: "rejected for reasons!",
143
+			methods:        verbs,
143 144
 		},
144 145
 		{
145 146
 			name:       "newer",
... ...
@@ -154,62 +150,71 @@ func TestVersionSkewFilterAllowAll(t *testing.T) {
154 154
 	}
155 155
 
156 156
 	for _, tc := range testCases {
157
-		tc.Run(server.URL, t)
157
+		tc.Run(server.URL+"/api/v1/namespaces", t)
158 158
 	}
159 159
 }
160 160
 
161
-func TestVersionSkewFilterDenyOld(t *testing.T) {
162
-	verbs := []string{"PATCH", "POST"}
161
+func TestVersionSkewFilterDenySkewed(t *testing.T) {
162
+	verbs := []string{"PUT", "DELETE"}
163 163
 	doNothingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
164 164
 	})
165 165
 	config := MasterConfig{}
166
-	config.Options.PolicyConfig.LegacyClientPolicyConfig.LegacyClientPolicy = configapi.DenyOldClients
167
-	config.Options.PolicyConfig.LegacyClientPolicyConfig.RestrictedHTTPVerbs = verbs
168
-	server := httptest.NewServer(config.versionSkewFilter(openshiftServerVersion, kubeServerVersion, doNothingHandler))
166
+	config.Options.PolicyConfig.UserAgentMatchingConfig.RequiredClients = []configapi.UserAgentMatchRule{
167
+		{Regex: `\w+/` + kubeServerVersion + ` \(.+/.+\) kubernetes/\w{7}`, HTTPVerbs: verbs},
168
+		{Regex: `\w+/` + openshiftServerVersion + ` \(.+/.+\) openshift/\w{7}`, HTTPVerbs: verbs},
169
+	}
170
+	config.Options.PolicyConfig.UserAgentMatchingConfig.DefaultRejectionMessage = "rejected for reasons!"
171
+	server := httptest.NewServer(config.versionSkewFilter(doNothingHandler))
169 172
 	defer server.Close()
170 173
 
171 174
 	testCases := []versionSkewTestCase{
172 175
 		{
173
-			name:       "missing",
174
-			userAgents: []string{""},
175
-			methods:    verbs,
176
+			name:           "missing",
177
+			userAgents:     []string{""},
178
+			failureMessage: "rejected for reasons!",
179
+			methods:        verbs,
176 180
 		},
177 181
 		{
178
-			name:       "not oc",
179
-			userAgents: []string{notOCVersion},
180
-			methods:    verbs,
182
+			name:           "not oc",
183
+			userAgents:     []string{notOCVersion},
184
+			failureMessage: "rejected for reasons!",
185
+			methods:        verbs,
181 186
 		},
182 187
 		{
183 188
 			name:           "older",
184 189
 			userAgents:     olderVersionUserAgents,
185
-			failureMessage: " is older than the server version",
190
+			failureMessage: "rejected for reasons!",
186 191
 			methods:        verbs,
187 192
 		},
188 193
 		{
189
-			name:       "newer",
190
-			userAgents: newerVersionUserAgents,
191
-			methods:    verbs,
194
+			name:           "newer",
195
+			userAgents:     newerVersionUserAgents,
196
+			failureMessage: "rejected for reasons!",
197
+			methods:        verbs,
192 198
 		},
193 199
 		{
194
-			name:       "exact",
200
+			name:       "current",
195 201
 			userAgents: currentVersionUserAgents,
196 202
 			methods:    verbs,
197 203
 		},
198 204
 	}
199 205
 
200 206
 	for _, tc := range testCases {
201
-		tc.Run(server.URL, t)
207
+		tc.Run(server.URL+"/api/v1/namespaces", t)
202 208
 	}
203 209
 }
204 210
 
205
-func TestVersionSkewFilterDenySkewed(t *testing.T) {
211
+func TestVersionSkewFilterSkippedOnNonAPIRequest(t *testing.T) {
206 212
 	verbs := []string{"PUT", "DELETE"}
207 213
 	doNothingHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
208 214
 	})
209 215
 	config := MasterConfig{}
210
-	config.Options.PolicyConfig.LegacyClientPolicyConfig.LegacyClientPolicy = configapi.DenySkewedClients
211
-	config.Options.PolicyConfig.LegacyClientPolicyConfig.RestrictedHTTPVerbs = verbs
212
-	server := httptest.NewServer(config.versionSkewFilter(openshiftServerVersion, kubeServerVersion, doNothingHandler))
216
+	config.Options.PolicyConfig.UserAgentMatchingConfig.RequiredClients = []configapi.UserAgentMatchRule{
217
+		{Regex: `\w+/` + kubeServerVersion + ` \(.+/.+\) kubernetes/\w{7}`, HTTPVerbs: verbs},
218
+		{Regex: `\w+/` + openshiftServerVersion + ` \(.+/.+\) openshift/\w{7}`, HTTPVerbs: verbs},
219
+	}
220
+	config.Options.PolicyConfig.UserAgentMatchingConfig.DefaultRejectionMessage = "rejected for reasons!"
221
+	server := httptest.NewServer(config.versionSkewFilter(doNothingHandler))
213 222
 	defer server.Close()
214 223
 
215 224
 	testCases := []versionSkewTestCase{
... ...
@@ -224,16 +229,14 @@ func TestVersionSkewFilterDenySkewed(t *testing.T) {
224 224
 			methods:    verbs,
225 225
 		},
226 226
 		{
227
-			name:           "older",
228
-			userAgents:     olderVersionUserAgents,
229
-			failureMessage: "is different than the server version",
230
-			methods:        verbs,
227
+			name:       "older",
228
+			userAgents: olderVersionUserAgents,
229
+			methods:    verbs,
231 230
 		},
232 231
 		{
233
-			name:           "newer",
234
-			userAgents:     newerVersionUserAgents,
235
-			failureMessage: "is different than the server version",
236
-			methods:        verbs,
232
+			name:       "newer",
233
+			userAgents: newerVersionUserAgents,
234
+			methods:    verbs,
237 235
 		},
238 236
 		{
239 237
 			name:       "current",
... ...
@@ -243,6 +246,6 @@ func TestVersionSkewFilterDenySkewed(t *testing.T) {
243 243
 	}
244 244
 
245 245
 	for _, tc := range testCases {
246
-		tc.Run(server.URL, t)
246
+		tc.Run(server.URL+"/api/v1", t)
247 247
 	}
248 248
 }
... ...
@@ -29,7 +29,6 @@ import (
29 29
 	"k8s.io/kubernetes/pkg/util"
30 30
 	"k8s.io/kubernetes/pkg/util/sets"
31 31
 	utilwait "k8s.io/kubernetes/pkg/util/wait"
32
-	kversion "k8s.io/kubernetes/pkg/version"
33 32
 
34 33
 	"github.com/openshift/origin/pkg/api/v1"
35 34
 	"github.com/openshift/origin/pkg/api/v1beta3"
... ...
@@ -105,7 +104,6 @@ import (
105 105
 	"github.com/openshift/origin/pkg/authorization/rulevalidation"
106 106
 	configapi "github.com/openshift/origin/pkg/cmd/server/api"
107 107
 	routeplugin "github.com/openshift/origin/pkg/route/allocation/simple"
108
-	"github.com/openshift/origin/pkg/version"
109 108
 )
110 109
 
111 110
 const (
... ...
@@ -161,7 +159,7 @@ func (c *MasterConfig) Run(protected []APIInstaller, unprotected []APIInstaller)
161 161
 		}
162 162
 		extra = append(extra, msgs...)
163 163
 	}
164
-	handler := c.versionSkewFilter(version.Get(), kversion.Get(), safe)
164
+	handler := c.versionSkewFilter(safe)
165 165
 	handler = c.authorizationFilter(handler)
166 166
 	handler = authenticationHandlerFilter(handler, c.Authenticator, c.getRequestContextMapper())
167 167
 	handler = namespacingFilter(handler, c.getRequestContextMapper())