Browse code

add scope validation to tokens

deads2k authored on 2016/05/10 03:37:23
Showing 4 changed files
... ...
@@ -23,10 +23,10 @@ func ScopesToRules(scopes []string, namespace string, clusterPolicyGetter ruleva
23 23
 	for _, scope := range scopes {
24 24
 		found := false
25 25
 
26
-		for prefix, evaluator := range scopeEvaluators {
27
-			if strings.HasPrefix(scope, prefix) {
26
+		for _, evaluator := range ScopeEvaluators {
27
+			if evaluator.Handles(scope) {
28 28
 				found = true
29
-				currRules, err := evaluator(scope, namespace, clusterPolicyGetter)
29
+				currRules, err := evaluator.ResolveRules(scope, namespace, clusterPolicyGetter)
30 30
 				if err != nil {
31 31
 					errors = append(errors, err)
32 32
 					continue
... ...
@@ -51,13 +51,17 @@ const (
51 51
 	NamespaceWideIndicator = "namespace:"
52 52
 )
53 53
 
54
-// scopeEvaluator takes a scope and returns the rules that express it
55
-type scopeEvaluator func(scope, namespace string, clusterPolicyGetter rulevalidation.ClusterPolicyGetter) ([]authorizationapi.PolicyRule, error)
54
+// ScopeEvaluator takes a scope and returns the rules that express it
55
+type ScopeEvaluator interface {
56
+	Handles(scope string) bool
57
+	Validate(scope string) error
58
+	ResolveRules(scope, namespace string, clusterPolicyGetter rulevalidation.ClusterPolicyGetter) ([]authorizationapi.PolicyRule, error)
59
+}
56 60
 
57
-// scopeEvaluators map prefixes to a function that handles that prefix
58
-var scopeEvaluators = map[string]scopeEvaluator{
59
-	UserIndicator:        userEvaluator,
60
-	ClusterRoleIndicator: clusterRoleEvaluator,
61
+// ScopeEvaluators map prefixes to a function that handles that prefix
62
+var ScopeEvaluators = []ScopeEvaluator{
63
+	userEvaluator{},
64
+	clusterRoleEvaluator{},
61 65
 }
62 66
 
63 67
 // scopes are in the format
... ...
@@ -75,7 +79,23 @@ const (
75 75
 )
76 76
 
77 77
 // user:<scope name>
78
-func userEvaluator(scope, namespace string, clusterPolicyGetter rulevalidation.ClusterPolicyGetter) ([]authorizationapi.PolicyRule, error) {
78
+type userEvaluator struct{}
79
+
80
+func (userEvaluator) Handles(scope string) bool {
81
+	return strings.HasPrefix(scope, UserIndicator)
82
+}
83
+
84
+func (userEvaluator) Validate(scope string) error {
85
+	switch scope {
86
+	case UserIndicator + UserInfo,
87
+		UserIndicator + UserAccessCheck:
88
+		return nil
89
+	}
90
+
91
+	return fmt.Errorf("unrecognized scope: %v", scope)
92
+}
93
+
94
+func (userEvaluator) ResolveRules(scope, namespace string, clusterPolicyGetter rulevalidation.ClusterPolicyGetter) ([]authorizationapi.PolicyRule, error) {
79 95
 	switch scope {
80 96
 	case UserIndicator + UserInfo:
81 97
 		return []authorizationapi.PolicyRule{
... ...
@@ -91,7 +111,31 @@ func userEvaluator(scope, namespace string, clusterPolicyGetter rulevalidation.C
91 91
 }
92 92
 
93 93
 // role:<clusterrole name>:<namespace to allow the cluster role, * means all>
94
-func clusterRoleEvaluator(scope, namespace string, clusterPolicyGetter rulevalidation.ClusterPolicyGetter) ([]authorizationapi.PolicyRule, error) {
94
+type clusterRoleEvaluator struct{}
95
+
96
+func (clusterRoleEvaluator) Handles(scope string) bool {
97
+	return strings.HasPrefix(scope, ClusterRoleIndicator)
98
+}
99
+
100
+func (e clusterRoleEvaluator) Validate(scope string) error {
101
+	if !e.Handles(scope) {
102
+		return fmt.Errorf("bad format for scope %v", scope)
103
+	}
104
+	tokens := strings.SplitN(scope, ":", 2)
105
+	if len(tokens) != 2 {
106
+		return fmt.Errorf("bad format for scope %v", scope)
107
+	}
108
+
109
+	// namespaces can't have colons, but roles can.  pick last.
110
+	lastColonIndex := strings.LastIndex(tokens[1], ":")
111
+	if lastColonIndex <= 0 || lastColonIndex == (len(tokens[1])-1) {
112
+		return fmt.Errorf("bad format for scope %v", scope)
113
+	}
114
+
115
+	return nil
116
+}
117
+
118
+func (clusterRoleEvaluator) ResolveRules(scope, namespace string, clusterPolicyGetter rulevalidation.ClusterPolicyGetter) ([]authorizationapi.PolicyRule, error) {
95 119
 	tokens := strings.SplitN(scope, ":", 2)
96 120
 	if len(tokens) != 2 {
97 121
 		return nil, fmt.Errorf("bad format for scope %v", scope)
... ...
@@ -9,6 +9,7 @@ import (
9 9
 	"k8s.io/kubernetes/pkg/util/validation/field"
10 10
 
11 11
 	oapi "github.com/openshift/origin/pkg/api"
12
+	authorizerscopes "github.com/openshift/origin/pkg/authorization/authorizer/scope"
12 13
 	"github.com/openshift/origin/pkg/oauth/api"
13 14
 	uservalidation "github.com/openshift/origin/pkg/user/api/validation"
14 15
 )
... ...
@@ -53,6 +54,7 @@ func ValidateAccessToken(accessToken *api.OAuthAccessToken) field.ErrorList {
53 53
 	allErrs := validation.ValidateObjectMeta(&accessToken.ObjectMeta, false, ValidateTokenName, field.NewPath("metadata"))
54 54
 	allErrs = append(allErrs, ValidateClientNameField(accessToken.ClientName, field.NewPath("clientName"))...)
55 55
 	allErrs = append(allErrs, ValidateUserNameField(accessToken.UserName, field.NewPath("userName"))...)
56
+	allErrs = append(allErrs, ValidateScopes(accessToken.Scopes, field.NewPath("scopes"))...)
56 57
 
57 58
 	if len(accessToken.UserUID) == 0 {
58 59
 		allErrs = append(allErrs, field.Required(field.NewPath("userUID"), ""))
... ...
@@ -68,6 +70,7 @@ func ValidateAuthorizeToken(authorizeToken *api.OAuthAuthorizeToken) field.Error
68 68
 	allErrs := validation.ValidateObjectMeta(&authorizeToken.ObjectMeta, false, ValidateTokenName, field.NewPath("metadata"))
69 69
 	allErrs = append(allErrs, ValidateClientNameField(authorizeToken.ClientName, field.NewPath("clientName"))...)
70 70
 	allErrs = append(allErrs, ValidateUserNameField(authorizeToken.UserName, field.NewPath("userName"))...)
71
+	allErrs = append(allErrs, ValidateScopes(authorizeToken.Scopes, field.NewPath("scopes"))...)
71 72
 
72 73
 	if len(authorizeToken.UserUID) == 0 {
73 74
 		allErrs = append(allErrs, field.Required(field.NewPath("userUID"), ""))
... ...
@@ -132,6 +135,7 @@ func ValidateClientAuthorization(clientAuthorization *api.OAuthClientAuthorizati
132 132
 
133 133
 	allErrs = append(allErrs, ValidateClientNameField(clientAuthorization.ClientName, field.NewPath("clientName"))...)
134 134
 	allErrs = append(allErrs, ValidateUserNameField(clientAuthorization.UserName, field.NewPath("userName"))...)
135
+	allErrs = append(allErrs, ValidateScopes(clientAuthorization.Scopes, field.NewPath("scopes"))...)
135 136
 
136 137
 	if len(clientAuthorization.UserUID) == 0 {
137 138
 		allErrs = append(allErrs, field.Required(field.NewPath("useruid"), ""))
... ...
@@ -175,3 +179,45 @@ func ValidateUserNameField(value string, fldPath *field.Path) field.ErrorList {
175 175
 	}
176 176
 	return field.ErrorList{}
177 177
 }
178
+
179
+func ValidateScopes(scopes []string, fldPath *field.Path) field.ErrorList {
180
+	allErrs := field.ErrorList{}
181
+
182
+	for i, scope := range scopes {
183
+		illegalCharacter := false
184
+		// https://tools.ietf.org/html/rfc6749#section-3.3 (full list of allowed chars is %x21 / %x23-5B / %x5D-7E)
185
+		// for those without an ascii table, that's `!`, `#-[`, `]-~` inclusive.
186
+		for _, ch := range scope {
187
+			switch {
188
+			case ch == rune("!"[0]):
189
+			case ch >= rune("#"[0]) && ch <= rune("]"[0]):
190
+			case ch >= rune("]"[0]) && ch <= rune("~"[0]):
191
+			default:
192
+				allErrs = append(allErrs, field.Invalid(fldPath.Index(i), scope, fmt.Sprintf("%v not allowed", ch)))
193
+				illegalCharacter = true
194
+			}
195
+		}
196
+		if illegalCharacter {
197
+			continue
198
+		}
199
+
200
+		found := false
201
+		for _, evaluator := range authorizerscopes.ScopeEvaluators {
202
+			if !evaluator.Handles(scope) {
203
+				continue
204
+			}
205
+
206
+			found = true
207
+			if err := evaluator.Validate(scope); err != nil {
208
+				allErrs = append(allErrs, field.Invalid(fldPath.Index(i), scope, err.Error()))
209
+				break
210
+			}
211
+		}
212
+
213
+		if !found {
214
+			allErrs = append(allErrs, field.Invalid(fldPath.Index(i), scope, "no scope handler found"))
215
+		}
216
+	}
217
+
218
+	return allErrs
219
+}
... ...
@@ -131,6 +131,28 @@ func TestValidateClientAuthorization(t *testing.T) {
131 131
 			T: field.ErrorTypeForbidden,
132 132
 			F: "metadata.namespace",
133 133
 		},
134
+		"no scope handler": {
135
+			A: oapi.OAuthClientAuthorization{
136
+				ObjectMeta: api.ObjectMeta{Name: "myusername:myclientname"},
137
+				ClientName: "myclientname",
138
+				UserName:   "myusername",
139
+				UserUID:    "myuseruid",
140
+				Scopes:     []string{"invalid"},
141
+			},
142
+			T: field.ErrorTypeInvalid,
143
+			F: "scopes[0]",
144
+		},
145
+		"bad scope": {
146
+			A: oapi.OAuthClientAuthorization{
147
+				ObjectMeta: api.ObjectMeta{Name: "myusername:myclientname"},
148
+				ClientName: "myclientname",
149
+				UserName:   "myusername",
150
+				UserUID:    "myuseruid",
151
+				Scopes:     []string{"user:dne"},
152
+			},
153
+			T: field.ErrorTypeInvalid,
154
+			F: "scopes[0]",
155
+		},
134 156
 	}
135 157
 	for k, v := range errorCases {
136 158
 		errs := ValidateClientAuthorization(&v.A)
... ...
@@ -225,6 +247,28 @@ func TestValidateAccessTokens(t *testing.T) {
225 225
 			T: field.ErrorTypeForbidden,
226 226
 			F: "metadata.namespace",
227 227
 		},
228
+		"no scope handler": {
229
+			Token: oapi.OAuthAccessToken{
230
+				ObjectMeta: api.ObjectMeta{Name: "accessTokenNameWithMinimumLength"},
231
+				ClientName: "myclient",
232
+				UserName:   "myusername",
233
+				UserUID:    "myuseruid",
234
+				Scopes:     []string{"invalid"},
235
+			},
236
+			T: field.ErrorTypeInvalid,
237
+			F: "scopes[0]",
238
+		},
239
+		"bad scope": {
240
+			Token: oapi.OAuthAccessToken{
241
+				ObjectMeta: api.ObjectMeta{Name: "accessTokenNameWithMinimumLength"},
242
+				ClientName: "myclient",
243
+				UserName:   "myusername",
244
+				UserUID:    "myuseruid",
245
+				Scopes:     []string{"user:dne"},
246
+			},
247
+			T: field.ErrorTypeInvalid,
248
+			F: "scopes[0]",
249
+		},
228 250
 	}
229 251
 	for k, v := range errorCases {
230 252
 		errs := ValidateAccessToken(&v.Token)
... ...
@@ -249,6 +293,7 @@ func TestValidateAuthorizeTokens(t *testing.T) {
249 249
 		ClientName: "myclient",
250 250
 		UserName:   "myusername",
251 251
 		UserUID:    "myuseruid",
252
+		Scopes:     []string{`user:info`},
252 253
 	})
253 254
 	if len(errs) != 0 {
254 255
 		t.Errorf("expected success: %v", errs)
... ...
@@ -305,6 +350,39 @@ func TestValidateAuthorizeTokens(t *testing.T) {
305 305
 			T: field.ErrorTypeForbidden,
306 306
 			F: "metadata.namespace",
307 307
 		},
308
+		"no scope handler": {
309
+			Token: oapi.OAuthAuthorizeToken{
310
+				ObjectMeta: api.ObjectMeta{Name: "authorizeTokenNameWithMinimumLength"},
311
+				ClientName: "myclient",
312
+				UserName:   "myusername",
313
+				UserUID:    "myuseruid",
314
+				Scopes:     []string{"invalid"},
315
+			},
316
+			T: field.ErrorTypeInvalid,
317
+			F: "scopes[0]",
318
+		},
319
+		"bad scope": {
320
+			Token: oapi.OAuthAuthorizeToken{
321
+				ObjectMeta: api.ObjectMeta{Name: "authorizeTokenNameWithMinimumLength"},
322
+				ClientName: "myclient",
323
+				UserName:   "myusername",
324
+				UserUID:    "myuseruid",
325
+				Scopes:     []string{"user:dne"},
326
+			},
327
+			T: field.ErrorTypeInvalid,
328
+			F: "scopes[0]",
329
+		},
330
+		"illegal character": {
331
+			Token: oapi.OAuthAuthorizeToken{
332
+				ObjectMeta: api.ObjectMeta{Name: "authorizeTokenNameWithMinimumLength"},
333
+				ClientName: "myclient",
334
+				UserName:   "myusername",
335
+				UserUID:    "myuseruid",
336
+				Scopes:     []string{`role:asdf":foo`},
337
+			},
338
+			T: field.ErrorTypeInvalid,
339
+			F: "scopes[0]",
340
+		},
308 341
 	}
309 342
 	for k, v := range errorCases {
310 343
 		errs := ValidateAuthorizeToken(&v.Token)
... ...
@@ -141,7 +141,7 @@ func TestOAuthStorage(t *testing.T) {
141 141
 	config := &oauth2.Config{
142 142
 		ClientID:     "test",
143 143
 		ClientSecret: "",
144
-		Scopes:       []string{"a_scope"},
144
+		Scopes:       []string{"user:info"},
145 145
 		RedirectURL:  assertServer.URL + "/assert",
146 146
 		Endpoint: oauth2.Endpoint{
147 147
 			AuthURL:  server.URL + "/authorize",