Browse code

add scope restrictions to oauth clients

deads2k authored on 2016/05/12 03:55:16
Showing 30 changed files
... ...
@@ -22037,6 +22037,63 @@
22037 22037
        "type": "string"
22038 22038
       },
22039 22039
       "description": "RedirectURIs is the valid redirection URIs associated with a client"
22040
+     },
22041
+     "scopeRestrictions": {
22042
+      "type": "array",
22043
+      "items": {
22044
+       "$ref": "v1.ScopeRestriction"
22045
+      },
22046
+      "description": "ScopeRestrictions describes which scopes this client can request.  Each requested scope is checked against each restriction.  If any restriction matches, then the scope is allowed. If no restriction matches, then the scope is denied."
22047
+     },
22048
+     "allowAnyScope": {
22049
+      "type": "boolean",
22050
+      "description": "AllowAnyScope indicates that the client is allowed to request a token unconstrained by scopes. If this is true, then ScopeRestrictions is ignored."
22051
+     }
22052
+    }
22053
+   },
22054
+   "v1.ScopeRestriction": {
22055
+    "id": "v1.ScopeRestriction",
22056
+    "description": "ScopeRestriction describe one restriction on scopes.  Exactly one option must be non-nil.",
22057
+    "properties": {
22058
+     "literals": {
22059
+      "type": "array",
22060
+      "items": {
22061
+       "type": "string"
22062
+      },
22063
+      "description": "ExactValues means the scope has to match a particular set of strings exactly"
22064
+     },
22065
+     "clusterRole": {
22066
+      "$ref": "v1.ClusterRoleScopeRestriction",
22067
+      "description": "ClusterRole describes a set of restrictions for cluster role scoping."
22068
+     }
22069
+    }
22070
+   },
22071
+   "v1.ClusterRoleScopeRestriction": {
22072
+    "id": "v1.ClusterRoleScopeRestriction",
22073
+    "description": "ClusterRoleScopeRestriction describes restrictions on cluster role scopes",
22074
+    "required": [
22075
+     "roleNames",
22076
+     "namespaces",
22077
+     "allowEscalation"
22078
+    ],
22079
+    "properties": {
22080
+     "roleNames": {
22081
+      "type": "array",
22082
+      "items": {
22083
+       "type": "string"
22084
+      },
22085
+      "description": "RoleNames is the list of cluster roles that can referenced.  * means anything"
22086
+     },
22087
+     "namespaces": {
22088
+      "type": "array",
22089
+      "items": {
22090
+       "type": "string"
22091
+      },
22092
+      "description": "Namespaces is the list of namespaces that can be referenced.  * means any of them (including *)"
22093
+     },
22094
+     "allowEscalation": {
22095
+      "type": "boolean",
22096
+      "description": "AllowEscalation indicates whether you can request roles and their escalating resources"
22040 22097
      }
22041 22098
     }
22042 22099
    },
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	"github.com/golang/glog"
15 15
 	"github.com/openshift/origin/pkg/auth/authenticator"
16 16
 	"github.com/openshift/origin/pkg/auth/server/csrf"
17
+	scopeauthorizer "github.com/openshift/origin/pkg/authorization/authorizer/scope"
17 18
 	oapi "github.com/openshift/origin/pkg/oauth/api"
18 19
 	"github.com/openshift/origin/pkg/oauth/registry/oauthclient"
19 20
 	"github.com/openshift/origin/pkg/oauth/registry/oauthclientauthorization"
... ...
@@ -118,6 +119,12 @@ func (l *Grant) handleForm(user user.Info, w http.ResponseWriter, req *http.Requ
118 118
 		return
119 119
 	}
120 120
 
121
+	if err := scopeauthorizer.ValidateScopeRestrictions(client, scope.Split(scopes)...); err != nil {
122
+		failure := fmt.Sprintf("%v requested illegal scopes (%v): %v", client.Name, scopes, err)
123
+		l.failed(failure, w, req)
124
+		return
125
+	}
126
+
121 127
 	uri, err := getBaseURL(req)
122 128
 	if err != nil {
123 129
 		glog.Errorf("Unable to generate base URL: %v", err)
... ...
@@ -185,6 +192,11 @@ func (l *Grant) handleGrant(user user.Info, w http.ResponseWriter, req *http.Req
185 185
 		l.failed("Could not find client for client_id", w, req)
186 186
 		return
187 187
 	}
188
+	if err := scopeauthorizer.ValidateScopeRestrictions(client, scope.Split(scopes)...); err != nil {
189
+		failure := fmt.Sprintf("%v requested illegal scopes (%v): %v", client.Name, scopes, err)
190
+		l.failed(failure, w, req)
191
+		return
192
+	}
188 193
 
189 194
 	clientAuthID := l.authregistry.ClientAuthorizationName(user.GetName(), client.Name)
190 195
 
... ...
@@ -36,9 +36,13 @@ func badAuth(err error) *testAuth {
36 36
 	return &testAuth{Success: false, User: nil, Err: err}
37 37
 }
38 38
 
39
-func goodClientRegistry(clientID string, redirectURIs []string) *test.ClientRegistry {
40
-	client := &oapi.OAuthClient{ObjectMeta: kapi.ObjectMeta{Name: clientID}, Secret: "mysecret", RedirectURIs: redirectURIs}
39
+func goodClientRegistry(clientID string, redirectURIs []string, literalScopes []string, unrestrictedScopes bool) *test.ClientRegistry {
40
+	client := &oapi.OAuthClient{ObjectMeta: kapi.ObjectMeta{Name: clientID}, Secret: "mysecret", RedirectURIs: redirectURIs, AllowAnyScope: unrestrictedScopes}
41 41
 	client.Name = clientID
42
+	if len(literalScopes) > 0 {
43
+		client.ScopeRestrictions = []oapi.ScopeRestriction{{ExactValues: literalScopes}}
44
+	}
45
+
42 46
 	return &test.ClientRegistry{Client: client}
43 47
 }
44 48
 func badClientRegistry(err error) *test.ClientRegistry {
... ...
@@ -79,7 +83,7 @@ func TestGrant(t *testing.T) {
79 79
 		"display form": {
80 80
 			CSRF:           &csrf.FakeCSRF{Token: "test"},
81 81
 			Auth:           goodAuth("username"),
82
-			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}),
82
+			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}, false),
83 83
 			AuthRegistry:   emptyAuthRegistry(),
84 84
 			Path:           "/grant?client_id=myclient&scopes=myscope1%20myscope2&redirect_uri=/myredirect&then=/authorize",
85 85
 
... ...
@@ -133,7 +137,7 @@ func TestGrant(t *testing.T) {
133 133
 		"error when POST fails CSRF": {
134 134
 			CSRF:           &csrf.FakeCSRF{Token: "test"},
135 135
 			Auth:           goodAuth("username"),
136
-			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}),
136
+			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}, false),
137 137
 			AuthRegistry:   emptyAuthRegistry(),
138 138
 			Path:           "/grant",
139 139
 			PostValues: url.Values{
... ...
@@ -181,7 +185,7 @@ func TestGrant(t *testing.T) {
181 181
 		"successful create grant with redirect": {
182 182
 			CSRF:           &csrf.FakeCSRF{Token: "test"},
183 183
 			Auth:           goodAuth("username"),
184
-			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}),
184
+			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}, false),
185 185
 			AuthRegistry:   emptyAuthRegistry(),
186 186
 			Path:           "/grant",
187 187
 			PostValues: url.Values{
... ...
@@ -201,7 +205,7 @@ func TestGrant(t *testing.T) {
201 201
 		"successful create grant without redirect": {
202 202
 			CSRF:           &csrf.FakeCSRF{Token: "test"},
203 203
 			Auth:           goodAuth("username"),
204
-			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}),
204
+			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}, false),
205 205
 			AuthRegistry:   emptyAuthRegistry(),
206 206
 			Path:           "/grant",
207 207
 			PostValues: url.Values{
... ...
@@ -223,7 +227,7 @@ func TestGrant(t *testing.T) {
223 223
 		"successful update grant with identical scopes": {
224 224
 			CSRF:           &csrf.FakeCSRF{Token: "test"},
225 225
 			Auth:           goodAuth("username"),
226
-			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}),
226
+			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}, false),
227 227
 			AuthRegistry:   existingAuthRegistry([]string{"myscope2", "myscope1"}),
228 228
 			Path:           "/grant",
229 229
 			PostValues: url.Values{
... ...
@@ -243,7 +247,7 @@ func TestGrant(t *testing.T) {
243 243
 		"successful update grant with additional scopes": {
244 244
 			CSRF:           &csrf.FakeCSRF{Token: "test"},
245 245
 			Auth:           goodAuth("username"),
246
-			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}),
246
+			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"newscope1", "existingscope1", "existingscope2"}, false),
247 247
 			AuthRegistry:   existingAuthRegistry([]string{"existingscope2", "existingscope1"}),
248 248
 			Path:           "/grant",
249 249
 			PostValues: url.Values{
... ...
@@ -263,7 +267,7 @@ func TestGrant(t *testing.T) {
263 263
 		"successful reject grant": {
264 264
 			CSRF:           &csrf.FakeCSRF{Token: "test"},
265 265
 			Auth:           goodAuth("username"),
266
-			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}),
266
+			ClientRegistry: goodClientRegistry("myclient", []string{"myredirect"}, []string{"myscope1", "myscope2"}, false),
267 267
 			AuthRegistry:   existingAuthRegistry([]string{"existingscope2", "existingscope1"}),
268 268
 			Path:           "/grant",
269 269
 			PostValues: url.Values{
... ...
@@ -12,6 +12,7 @@ import (
12 12
 
13 13
 	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
14 14
 	"github.com/openshift/origin/pkg/authorization/rulevalidation"
15
+	oauthapi "github.com/openshift/origin/pkg/oauth/api"
15 16
 	userapi "github.com/openshift/origin/pkg/user/api"
16 17
 )
17 18
 
... ...
@@ -135,6 +136,8 @@ var escalatingScopeResources = []unversioned.GroupResource{
135 135
 // role:<clusterrole name>:<namespace to allow the cluster role, * means all>
136 136
 type clusterRoleEvaluator struct{}
137 137
 
138
+var clusterRoleEvaluatorInstance = clusterRoleEvaluator{}
139
+
138 140
 func (clusterRoleEvaluator) Handles(scope string) bool {
139 141
 	return strings.HasPrefix(scope, ClusterRoleIndicator)
140 142
 }
... ...
@@ -267,3 +270,102 @@ func getAPIGroupSet(rule authorizationapi.PolicyRule) sets.String {
267 267
 
268 268
 	return apiGroups
269 269
 }
270
+
271
+func ValidateScopeRestrictions(client *oauthapi.OAuthClient, scopes ...string) error {
272
+	if client.AllowAnyScope {
273
+		return nil
274
+	}
275
+	if len(scopes) == 0 {
276
+		return fmt.Errorf("%v may not request unscoped tokens", client.Name)
277
+	}
278
+
279
+	errs := []error{}
280
+	for _, scope := range scopes {
281
+		if err := validateScopeRestrictions(client, scope); err != nil {
282
+			errs = append(errs, err)
283
+		}
284
+	}
285
+
286
+	return kutilerrors.NewAggregate(errs)
287
+}
288
+
289
+func validateScopeRestrictions(client *oauthapi.OAuthClient, scope string) error {
290
+	errs := []error{}
291
+
292
+	if client.AllowAnyScope {
293
+		return nil
294
+	}
295
+
296
+	for _, restriction := range client.ScopeRestrictions {
297
+		if len(restriction.ExactValues) > 0 {
298
+			if err := ValidateLiteralScopeRestrictions(scope, restriction.ExactValues); err != nil {
299
+				errs = append(errs, err)
300
+				continue
301
+			}
302
+			return nil
303
+		}
304
+
305
+		if restriction.ClusterRole != nil {
306
+			if !clusterRoleEvaluatorInstance.Handles(scope) {
307
+				continue
308
+			}
309
+			if err := ValidateClusterRoleScopeRestrictions(scope, *restriction.ClusterRole); err != nil {
310
+				errs = append(errs, err)
311
+				continue
312
+			}
313
+			return nil
314
+		}
315
+	}
316
+
317
+	// if we got here, then nothing matched.   If we already have errors, do nothing, otherwise add one to make it report failed.
318
+	if len(errs) == 0 {
319
+		errs = append(errs, fmt.Errorf("%v did not match any scope restriction", scope))
320
+	}
321
+
322
+	return kutilerrors.NewAggregate(errs)
323
+}
324
+
325
+func ValidateLiteralScopeRestrictions(scope string, literals []string) error {
326
+	for _, literal := range literals {
327
+		if literal == scope {
328
+			return nil
329
+		}
330
+	}
331
+
332
+	return fmt.Errorf("%v not found in %v", scope, literals)
333
+}
334
+
335
+func ValidateClusterRoleScopeRestrictions(scope string, restriction oauthapi.ClusterRoleScopeRestriction) error {
336
+	role, namespace, escalating, err := clusterRoleEvaluatorInstance.parseScope(scope)
337
+	if err != nil {
338
+		return err
339
+	}
340
+
341
+	foundName := false
342
+	for _, restrictedRoleName := range restriction.RoleNames {
343
+		if restrictedRoleName == "*" || restrictedRoleName == role {
344
+			foundName = true
345
+			break
346
+		}
347
+	}
348
+	if !foundName {
349
+		return fmt.Errorf("%v does not use an approved name", scope)
350
+	}
351
+
352
+	foundNamespace := false
353
+	for _, restrictedNamespace := range restriction.Namespaces {
354
+		if restrictedNamespace == "*" || restrictedNamespace == namespace {
355
+			foundNamespace = true
356
+			break
357
+		}
358
+	}
359
+	if !foundNamespace {
360
+		return fmt.Errorf("%v does not use an approved namespace", scope)
361
+	}
362
+
363
+	if escalating && !restriction.AllowEscalation {
364
+		return fmt.Errorf("%v is not allowed to escalate", scope)
365
+	}
366
+
367
+	return nil
368
+}
270 369
new file mode 100644
... ...
@@ -0,0 +1,158 @@
0
+package scope
1
+
2
+import (
3
+	"strings"
4
+	"testing"
5
+
6
+	oauthapi "github.com/openshift/origin/pkg/oauth/api"
7
+)
8
+
9
+func TestValidateScopeRestrictions(t *testing.T) {
10
+	testCases := []struct {
11
+		name   string
12
+		scopes []string
13
+		client *oauthapi.OAuthClient
14
+
15
+		expectedErrors []string
16
+	}{
17
+		{
18
+			name:   "unrestricted allows any",
19
+			scopes: []string{"one"},
20
+			client: &oauthapi.OAuthClient{AllowAnyScope: true},
21
+		},
22
+		{
23
+			name:   "unrestricted allows empty",
24
+			scopes: []string{""},
25
+			client: &oauthapi.OAuthClient{AllowAnyScope: true},
26
+		},
27
+		{
28
+			name:   "unrestricted allows none",
29
+			scopes: []string{},
30
+			client: &oauthapi.OAuthClient{AllowAnyScope: true},
31
+		},
32
+		{
33
+			name:           "no restrictions denies any",
34
+			scopes:         []string{"one"},
35
+			client:         &oauthapi.OAuthClient{},
36
+			expectedErrors: []string{`one did not match any scope restriction`},
37
+		},
38
+		{
39
+			name:           "no restrictions denies empty",
40
+			scopes:         []string{""},
41
+			client:         &oauthapi.OAuthClient{},
42
+			expectedErrors: []string{`did not match any scope restriction`},
43
+		},
44
+		{
45
+			name:           "no restrictions denies none",
46
+			scopes:         []string{},
47
+			client:         &oauthapi.OAuthClient{},
48
+			expectedErrors: []string{`may not request unscoped tokens`},
49
+		},
50
+		{
51
+			name:   "simple literal",
52
+			scopes: []string{"one"},
53
+			client: &oauthapi.OAuthClient{
54
+				ScopeRestrictions: []oauthapi.ScopeRestriction{{ExactValues: []string{"two", "one"}}},
55
+			},
56
+		},
57
+		{
58
+			name:   "simple must match",
59
+			scopes: []string{"missing"},
60
+			client: &oauthapi.OAuthClient{
61
+				ScopeRestrictions: []oauthapi.ScopeRestriction{{ExactValues: []string{"two", "one"}}},
62
+			},
63
+			expectedErrors: []string{`missing not found in [two one]`},
64
+		},
65
+		{
66
+			name:   "cluster role name must match",
67
+			scopes: []string{ClusterRoleIndicator + "three:alfa"},
68
+			client: &oauthapi.OAuthClient{
69
+				ScopeRestrictions: []oauthapi.ScopeRestriction{{ClusterRole: &oauthapi.ClusterRoleScopeRestriction{
70
+					RoleNames:       []string{"one", "two"},
71
+					Namespaces:      []string{"alfa", "bravo"},
72
+					AllowEscalation: false,
73
+				}}},
74
+			},
75
+			expectedErrors: []string{`role:three:alfa does not use an approved name`},
76
+		},
77
+		{
78
+			name:   "cluster role namespace must match",
79
+			scopes: []string{ClusterRoleIndicator + "two:charlie"},
80
+			client: &oauthapi.OAuthClient{
81
+				ScopeRestrictions: []oauthapi.ScopeRestriction{{ClusterRole: &oauthapi.ClusterRoleScopeRestriction{
82
+					RoleNames:       []string{"one", "two"},
83
+					Namespaces:      []string{"alfa", "bravo"},
84
+					AllowEscalation: false,
85
+				}}},
86
+			},
87
+			expectedErrors: []string{`role:two:charlie does not use an approved namespace`},
88
+		},
89
+		{
90
+			name:   "cluster role escalation must match",
91
+			scopes: []string{ClusterRoleIndicator + "two:bravo:!"},
92
+			client: &oauthapi.OAuthClient{
93
+				ScopeRestrictions: []oauthapi.ScopeRestriction{{ClusterRole: &oauthapi.ClusterRoleScopeRestriction{
94
+					RoleNames:       []string{"one", "two"},
95
+					Namespaces:      []string{"alfa", "bravo"},
96
+					AllowEscalation: false,
97
+				}}},
98
+			},
99
+			expectedErrors: []string{`role:two:bravo:! is not allowed to escalate`},
100
+		},
101
+		{
102
+			name:   "cluster role matches",
103
+			scopes: []string{ClusterRoleIndicator + "two:bravo:!"},
104
+			client: &oauthapi.OAuthClient{
105
+				ScopeRestrictions: []oauthapi.ScopeRestriction{{ClusterRole: &oauthapi.ClusterRoleScopeRestriction{
106
+					RoleNames:       []string{"one", "two"},
107
+					Namespaces:      []string{"alfa", "bravo"},
108
+					AllowEscalation: true,
109
+				}}},
110
+			},
111
+		},
112
+		{
113
+			name:   "cluster role matches 2",
114
+			scopes: []string{ClusterRoleIndicator + "two:bravo"},
115
+			client: &oauthapi.OAuthClient{
116
+				ScopeRestrictions: []oauthapi.ScopeRestriction{{ClusterRole: &oauthapi.ClusterRoleScopeRestriction{
117
+					RoleNames:       []string{"one", "two"},
118
+					Namespaces:      []string{"alfa", "bravo"},
119
+					AllowEscalation: false,
120
+				}}},
121
+			},
122
+		},
123
+		{
124
+			name:   "cluster role star matches",
125
+			scopes: []string{ClusterRoleIndicator + "two:bravo"},
126
+			client: &oauthapi.OAuthClient{
127
+				ScopeRestrictions: []oauthapi.ScopeRestriction{{ClusterRole: &oauthapi.ClusterRoleScopeRestriction{
128
+					RoleNames:       []string{"one", "two", "*"},
129
+					Namespaces:      []string{"alfa", "bravo", "*"},
130
+					AllowEscalation: true,
131
+				}}},
132
+			},
133
+		},
134
+	}
135
+
136
+	for _, tc := range testCases {
137
+		err := ValidateScopeRestrictions(tc.client, tc.scopes...)
138
+		if err != nil && len(tc.expectedErrors) == 0 {
139
+			t.Errorf("%s: unexpected error: %v", tc.name, err)
140
+			continue
141
+		}
142
+		if err == nil && len(tc.expectedErrors) > 0 {
143
+			t.Errorf("%s: missing error: %v", tc.name, tc.expectedErrors)
144
+			continue
145
+		}
146
+		if err == nil && len(tc.expectedErrors) == 0 {
147
+			continue
148
+		}
149
+
150
+		for _, expectedErr := range tc.expectedErrors {
151
+			if !strings.Contains(err.Error(), expectedErr) {
152
+				t.Errorf("%s: error %v missing %v", tc.name, err, expectedErr)
153
+			}
154
+		}
155
+	}
156
+
157
+}
... ...
@@ -83,13 +83,14 @@ const (
83 83
 func (c *AuthConfig) InstallAPI(container *restful.Container) ([]string, error) {
84 84
 	mux := c.getMux(container)
85 85
 
86
-	accessTokenStorage := accesstokenetcd.NewREST(c.EtcdHelper, c.EtcdBackends...)
87
-	accessTokenRegistry := accesstokenregistry.NewRegistry(accessTokenStorage)
88
-	authorizeTokenStorage := authorizetokenetcd.NewREST(c.EtcdHelper, c.EtcdBackends...)
89
-	authorizeTokenRegistry := authorizetokenregistry.NewRegistry(authorizeTokenStorage)
90 86
 	clientStorage := clientetcd.NewREST(c.EtcdHelper)
91 87
 	clientRegistry := clientregistry.NewRegistry(clientStorage)
92
-	clientAuthStorage := clientauthetcd.NewREST(c.EtcdHelper)
88
+
89
+	accessTokenStorage := accesstokenetcd.NewREST(c.EtcdHelper, clientRegistry, c.EtcdBackends...)
90
+	accessTokenRegistry := accesstokenregistry.NewRegistry(accessTokenStorage)
91
+	authorizeTokenStorage := authorizetokenetcd.NewREST(c.EtcdHelper, clientRegistry, c.EtcdBackends...)
92
+	authorizeTokenRegistry := authorizetokenregistry.NewRegistry(authorizeTokenStorage)
93
+	clientAuthStorage := clientauthetcd.NewREST(c.EtcdHelper, clientRegistry)
93 94
 	clientAuthRegistry := clientauthregistry.NewRegistry(clientAuthStorage)
94 95
 
95 96
 	errorPageHandler, err := c.getErrorHandler()
... ...
@@ -242,6 +243,8 @@ func ensureOAuthClient(client oauthapi.OAuthClient, clientRegistry clientregistr
242 242
 		if len(existing.Secret) == 0 {
243 243
 			existing.Secret = client.Secret
244 244
 		}
245
+		// Ensure the correct scope setting
246
+		existing.AllowAnyScope = client.AllowAnyScope
245 247
 
246 248
 		// Preserve redirects for clients other than the CLI client
247 249
 		// The CLI client doesn't care about the redirect URL, just the token or error fragment
... ...
@@ -270,6 +273,7 @@ func CreateOrUpdateDefaultOAuthClients(masterPublicAddr string, assetPublicAddre
270 270
 			Secret:                uuid.New(),
271 271
 			RespondWithChallenges: false,
272 272
 			RedirectURIs:          assetPublicAddresses,
273
+			AllowAnyScope:         true,
273 274
 		}
274 275
 		if err := ensureOAuthClient(webConsoleClient, clientRegistry, true); err != nil {
275 276
 			return err
... ...
@@ -282,6 +286,7 @@ func CreateOrUpdateDefaultOAuthClients(masterPublicAddr string, assetPublicAddre
282 282
 			Secret:                uuid.New(),
283 283
 			RespondWithChallenges: false,
284 284
 			RedirectURIs:          []string{masterPublicAddr + path.Join(OpenShiftOAuthAPIPrefix, tokenrequest.DisplayTokenEndpoint)},
285
+			AllowAnyScope:         true,
285 286
 		}
286 287
 		if err := ensureOAuthClient(browserClient, clientRegistry, true); err != nil {
287 288
 			return err
... ...
@@ -294,6 +299,7 @@ func CreateOrUpdateDefaultOAuthClients(masterPublicAddr string, assetPublicAddre
294 294
 			Secret:                uuid.New(),
295 295
 			RespondWithChallenges: true,
296 296
 			RedirectURIs:          []string{masterPublicAddr + path.Join(OpenShiftOAuthAPIPrefix, tokenrequest.ImplicitTokenEndpoint)},
297
+			AllowAnyScope:         true,
297 298
 		}
298 299
 		if err := ensureOAuthClient(cliClient, clientRegistry, false); err != nil {
299 300
 			return err
... ...
@@ -64,6 +64,7 @@ import (
64 64
 	"github.com/openshift/origin/pkg/image/registry/imagestreamtag"
65 65
 	accesstokenetcd "github.com/openshift/origin/pkg/oauth/registry/oauthaccesstoken/etcd"
66 66
 	authorizetokenetcd "github.com/openshift/origin/pkg/oauth/registry/oauthauthorizetoken/etcd"
67
+	clientregistry "github.com/openshift/origin/pkg/oauth/registry/oauthclient"
67 68
 	clientetcd "github.com/openshift/origin/pkg/oauth/registry/oauthclient/etcd"
68 69
 	clientauthetcd "github.com/openshift/origin/pkg/oauth/registry/oauthclientauthorization/etcd"
69 70
 	projectproxy "github.com/openshift/origin/pkg/project/registry/project/proxy"
... ...
@@ -490,6 +491,9 @@ func (c *MasterConfig) GetRestStorage() map[string]rest.Storage {
490 490
 		},
491 491
 	)
492 492
 
493
+	clientStorage := clientetcd.NewREST(c.EtcdHelper)
494
+	clientRegistry := clientregistry.NewRegistry(clientStorage)
495
+
493 496
 	storage := map[string]rest.Storage{
494 497
 		"images":               imageStorage,
495 498
 		"imageStreams/secrets": imageStreamSecretsStorage,
... ...
@@ -525,10 +529,10 @@ func (c *MasterConfig) GetRestStorage() map[string]rest.Storage {
525 525
 		"identities":           identityStorage,
526 526
 		"userIdentityMappings": userIdentityMappingStorage,
527 527
 
528
-		"oAuthAuthorizeTokens":      authorizetokenetcd.NewREST(c.EtcdHelper),
529
-		"oAuthAccessTokens":         accesstokenetcd.NewREST(c.EtcdHelper),
530
-		"oAuthClients":              clientetcd.NewREST(c.EtcdHelper),
531
-		"oAuthClientAuthorizations": clientauthetcd.NewREST(c.EtcdHelper),
528
+		"oAuthAuthorizeTokens":      authorizetokenetcd.NewREST(c.EtcdHelper, clientRegistry),
529
+		"oAuthAccessTokens":         accesstokenetcd.NewREST(c.EtcdHelper, clientRegistry),
530
+		"oAuthClients":              clientStorage,
531
+		"oAuthClientAuthorizations": clientauthetcd.NewREST(c.EtcdHelper, clientRegistry),
532 532
 
533 533
 		"resourceAccessReviews":      resourceAccessReviewStorage,
534 534
 		"subjectAccessReviews":       subjectAccessReviewStorage,
... ...
@@ -384,7 +384,8 @@ func newAuthorizationAttributeBuilder(requestContextMapper kapi.RequestContextMa
384 384
 }
385 385
 
386 386
 func getEtcdTokenAuthenticator(etcdHelper storage.Interface, groupMapper identitymapper.UserToGroupMapper) authenticator.Token {
387
-	accessTokenStorage := accesstokenetcd.NewREST(etcdHelper)
387
+	// this never does a create for access tokens, so we don't need to be able to validate scopes against the client
388
+	accessTokenStorage := accesstokenetcd.NewREST(etcdHelper, nil)
388 389
 	accessTokenRegistry := accesstokenregistry.NewRegistry(accessTokenStorage)
389 390
 
390 391
 	userStorage := useretcd.NewREST(etcdHelper)
... ...
@@ -12,6 +12,7 @@ import (
12 12
 
13 13
 func init() {
14 14
 	if err := api.Scheme.AddGeneratedDeepCopyFuncs(
15
+		DeepCopy_api_ClusterRoleScopeRestriction,
15 16
 		DeepCopy_api_OAuthAccessToken,
16 17
 		DeepCopy_api_OAuthAccessTokenList,
17 18
 		DeepCopy_api_OAuthAuthorizeToken,
... ...
@@ -20,12 +21,32 @@ func init() {
20 20
 		DeepCopy_api_OAuthClientAuthorization,
21 21
 		DeepCopy_api_OAuthClientAuthorizationList,
22 22
 		DeepCopy_api_OAuthClientList,
23
+		DeepCopy_api_ScopeRestriction,
23 24
 	); err != nil {
24 25
 		// if one of the deep copy functions is malformed, detect it immediately.
25 26
 		panic(err)
26 27
 	}
27 28
 }
28 29
 
30
+func DeepCopy_api_ClusterRoleScopeRestriction(in ClusterRoleScopeRestriction, out *ClusterRoleScopeRestriction, c *conversion.Cloner) error {
31
+	if in.RoleNames != nil {
32
+		in, out := in.RoleNames, &out.RoleNames
33
+		*out = make([]string, len(in))
34
+		copy(*out, in)
35
+	} else {
36
+		out.RoleNames = nil
37
+	}
38
+	if in.Namespaces != nil {
39
+		in, out := in.Namespaces, &out.Namespaces
40
+		*out = make([]string, len(in))
41
+		copy(*out, in)
42
+	} else {
43
+		out.Namespaces = nil
44
+	}
45
+	out.AllowEscalation = in.AllowEscalation
46
+	return nil
47
+}
48
+
29 49
 func DeepCopy_api_OAuthAccessToken(in OAuthAccessToken, out *OAuthAccessToken, c *conversion.Cloner) error {
30 50
 	if err := unversioned.DeepCopy_unversioned_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil {
31 51
 		return err
... ...
@@ -131,6 +152,18 @@ func DeepCopy_api_OAuthClient(in OAuthClient, out *OAuthClient, c *conversion.Cl
131 131
 	} else {
132 132
 		out.RedirectURIs = nil
133 133
 	}
134
+	if in.ScopeRestrictions != nil {
135
+		in, out := in.ScopeRestrictions, &out.ScopeRestrictions
136
+		*out = make([]ScopeRestriction, len(in))
137
+		for i := range in {
138
+			if err := DeepCopy_api_ScopeRestriction(in[i], &(*out)[i], c); err != nil {
139
+				return err
140
+			}
141
+		}
142
+	} else {
143
+		out.ScopeRestrictions = nil
144
+	}
145
+	out.AllowAnyScope = in.AllowAnyScope
134 146
 	return nil
135 147
 }
136 148
 
... ...
@@ -195,3 +228,23 @@ func DeepCopy_api_OAuthClientList(in OAuthClientList, out *OAuthClientList, c *c
195 195
 	}
196 196
 	return nil
197 197
 }
198
+
199
+func DeepCopy_api_ScopeRestriction(in ScopeRestriction, out *ScopeRestriction, c *conversion.Cloner) error {
200
+	if in.ExactValues != nil {
201
+		in, out := in.ExactValues, &out.ExactValues
202
+		*out = make([]string, len(in))
203
+		copy(*out, in)
204
+	} else {
205
+		out.ExactValues = nil
206
+	}
207
+	if in.ClusterRole != nil {
208
+		in, out := in.ClusterRole, &out.ClusterRole
209
+		*out = new(ClusterRoleScopeRestriction)
210
+		if err := DeepCopy_api_ClusterRoleScopeRestriction(*in, *out, c); err != nil {
211
+			return err
212
+		}
213
+	} else {
214
+		out.ClusterRole = nil
215
+	}
216
+	return nil
217
+}
... ...
@@ -73,6 +73,34 @@ type OAuthClient struct {
73 73
 
74 74
 	// RedirectURIs is the valid redirection URIs associated with a client
75 75
 	RedirectURIs []string
76
+
77
+	// ScopeRestrictions describes which scopes this client can request.  Each requested scope
78
+	// is checked against each restriction.  If any restriction matches, then the scope is allowed.
79
+	// If no restriction matches, then the scope is denied.
80
+	ScopeRestrictions []ScopeRestriction
81
+
82
+	// AllowAnyScope indicates that the client is allowed to request a token unconstrained by scopes.
83
+	// If this is true, then ScopeRestrictions is ignored.
84
+	AllowAnyScope bool
85
+}
86
+
87
+// ScopeRestriction describe one restriction on scopes.  Exactly one option must be non-nil.
88
+type ScopeRestriction struct {
89
+	// ExactValues means the scope has to match a particular set of strings exactly
90
+	ExactValues []string
91
+
92
+	// ClusterRole describes a set of restrictions for cluster role scoping.
93
+	ClusterRole *ClusterRoleScopeRestriction
94
+}
95
+
96
+// ClusterRoleScopeRestriction describes restrictions on cluster role scopes
97
+type ClusterRoleScopeRestriction struct {
98
+	// RoleNames is the list of cluster roles that can referenced.  * means anything
99
+	RoleNames []string
100
+	// Namespaces is the list of namespaces that can be referenced.  * means any of them (including *)
101
+	Namespaces []string
102
+	// AllowEscalation indicates whether you can request roles and their escalating resources
103
+	AllowEscalation bool
76 104
 }
77 105
 
78 106
 type OAuthClientAuthorization struct {
... ...
@@ -13,6 +13,8 @@ import (
13 13
 
14 14
 func init() {
15 15
 	if err := api.Scheme.AddGeneratedConversionFuncs(
16
+		Convert_v1_ClusterRoleScopeRestriction_To_api_ClusterRoleScopeRestriction,
17
+		Convert_api_ClusterRoleScopeRestriction_To_v1_ClusterRoleScopeRestriction,
16 18
 		Convert_v1_OAuthAccessToken_To_api_OAuthAccessToken,
17 19
 		Convert_api_OAuthAccessToken_To_v1_OAuthAccessToken,
18 20
 		Convert_v1_OAuthAccessTokenList_To_api_OAuthAccessTokenList,
... ...
@@ -29,12 +31,66 @@ func init() {
29 29
 		Convert_api_OAuthClientAuthorizationList_To_v1_OAuthClientAuthorizationList,
30 30
 		Convert_v1_OAuthClientList_To_api_OAuthClientList,
31 31
 		Convert_api_OAuthClientList_To_v1_OAuthClientList,
32
+		Convert_v1_ScopeRestriction_To_api_ScopeRestriction,
33
+		Convert_api_ScopeRestriction_To_v1_ScopeRestriction,
32 34
 	); err != nil {
33 35
 		// if one of the conversion functions is malformed, detect it immediately.
34 36
 		panic(err)
35 37
 	}
36 38
 }
37 39
 
40
+func autoConvert_v1_ClusterRoleScopeRestriction_To_api_ClusterRoleScopeRestriction(in *ClusterRoleScopeRestriction, out *oauth_api.ClusterRoleScopeRestriction, s conversion.Scope) error {
41
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
42
+		defaulting.(func(*ClusterRoleScopeRestriction))(in)
43
+	}
44
+	if in.RoleNames != nil {
45
+		in, out := &in.RoleNames, &out.RoleNames
46
+		*out = make([]string, len(*in))
47
+		copy(*out, *in)
48
+	} else {
49
+		out.RoleNames = nil
50
+	}
51
+	if in.Namespaces != nil {
52
+		in, out := &in.Namespaces, &out.Namespaces
53
+		*out = make([]string, len(*in))
54
+		copy(*out, *in)
55
+	} else {
56
+		out.Namespaces = nil
57
+	}
58
+	out.AllowEscalation = in.AllowEscalation
59
+	return nil
60
+}
61
+
62
+func Convert_v1_ClusterRoleScopeRestriction_To_api_ClusterRoleScopeRestriction(in *ClusterRoleScopeRestriction, out *oauth_api.ClusterRoleScopeRestriction, s conversion.Scope) error {
63
+	return autoConvert_v1_ClusterRoleScopeRestriction_To_api_ClusterRoleScopeRestriction(in, out, s)
64
+}
65
+
66
+func autoConvert_api_ClusterRoleScopeRestriction_To_v1_ClusterRoleScopeRestriction(in *oauth_api.ClusterRoleScopeRestriction, out *ClusterRoleScopeRestriction, s conversion.Scope) error {
67
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
68
+		defaulting.(func(*oauth_api.ClusterRoleScopeRestriction))(in)
69
+	}
70
+	if in.RoleNames != nil {
71
+		in, out := &in.RoleNames, &out.RoleNames
72
+		*out = make([]string, len(*in))
73
+		copy(*out, *in)
74
+	} else {
75
+		out.RoleNames = nil
76
+	}
77
+	if in.Namespaces != nil {
78
+		in, out := &in.Namespaces, &out.Namespaces
79
+		*out = make([]string, len(*in))
80
+		copy(*out, *in)
81
+	} else {
82
+		out.Namespaces = nil
83
+	}
84
+	out.AllowEscalation = in.AllowEscalation
85
+	return nil
86
+}
87
+
88
+func Convert_api_ClusterRoleScopeRestriction_To_v1_ClusterRoleScopeRestriction(in *oauth_api.ClusterRoleScopeRestriction, out *ClusterRoleScopeRestriction, s conversion.Scope) error {
89
+	return autoConvert_api_ClusterRoleScopeRestriction_To_v1_ClusterRoleScopeRestriction(in, out, s)
90
+}
91
+
38 92
 func autoConvert_v1_OAuthAccessToken_To_api_OAuthAccessToken(in *OAuthAccessToken, out *oauth_api.OAuthAccessToken, s conversion.Scope) error {
39 93
 	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
40 94
 		defaulting.(func(*OAuthAccessToken))(in)
... ...
@@ -293,6 +349,18 @@ func autoConvert_v1_OAuthClient_To_api_OAuthClient(in *OAuthClient, out *oauth_a
293 293
 	} else {
294 294
 		out.RedirectURIs = nil
295 295
 	}
296
+	if in.ScopeRestrictions != nil {
297
+		in, out := &in.ScopeRestrictions, &out.ScopeRestrictions
298
+		*out = make([]oauth_api.ScopeRestriction, len(*in))
299
+		for i := range *in {
300
+			if err := Convert_v1_ScopeRestriction_To_api_ScopeRestriction(&(*in)[i], &(*out)[i], s); err != nil {
301
+				return err
302
+			}
303
+		}
304
+	} else {
305
+		out.ScopeRestrictions = nil
306
+	}
307
+	out.AllowAnyScope = in.AllowAnyScope
296 308
 	return nil
297 309
 }
298 310
 
... ...
@@ -320,6 +388,18 @@ func autoConvert_api_OAuthClient_To_v1_OAuthClient(in *oauth_api.OAuthClient, ou
320 320
 	} else {
321 321
 		out.RedirectURIs = nil
322 322
 	}
323
+	if in.ScopeRestrictions != nil {
324
+		in, out := &in.ScopeRestrictions, &out.ScopeRestrictions
325
+		*out = make([]ScopeRestriction, len(*in))
326
+		for i := range *in {
327
+			if err := Convert_api_ScopeRestriction_To_v1_ScopeRestriction(&(*in)[i], &(*out)[i], s); err != nil {
328
+				return err
329
+			}
330
+		}
331
+	} else {
332
+		out.ScopeRestrictions = nil
333
+	}
334
+	out.AllowAnyScope = in.AllowAnyScope
323 335
 	return nil
324 336
 }
325 337
 
... ...
@@ -494,3 +574,57 @@ func autoConvert_api_OAuthClientList_To_v1_OAuthClientList(in *oauth_api.OAuthCl
494 494
 func Convert_api_OAuthClientList_To_v1_OAuthClientList(in *oauth_api.OAuthClientList, out *OAuthClientList, s conversion.Scope) error {
495 495
 	return autoConvert_api_OAuthClientList_To_v1_OAuthClientList(in, out, s)
496 496
 }
497
+
498
+func autoConvert_v1_ScopeRestriction_To_api_ScopeRestriction(in *ScopeRestriction, out *oauth_api.ScopeRestriction, s conversion.Scope) error {
499
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
500
+		defaulting.(func(*ScopeRestriction))(in)
501
+	}
502
+	if in.ExactValues != nil {
503
+		in, out := &in.ExactValues, &out.ExactValues
504
+		*out = make([]string, len(*in))
505
+		copy(*out, *in)
506
+	} else {
507
+		out.ExactValues = nil
508
+	}
509
+	if in.ClusterRole != nil {
510
+		in, out := &in.ClusterRole, &out.ClusterRole
511
+		*out = new(oauth_api.ClusterRoleScopeRestriction)
512
+		if err := Convert_v1_ClusterRoleScopeRestriction_To_api_ClusterRoleScopeRestriction(*in, *out, s); err != nil {
513
+			return err
514
+		}
515
+	} else {
516
+		out.ClusterRole = nil
517
+	}
518
+	return nil
519
+}
520
+
521
+func Convert_v1_ScopeRestriction_To_api_ScopeRestriction(in *ScopeRestriction, out *oauth_api.ScopeRestriction, s conversion.Scope) error {
522
+	return autoConvert_v1_ScopeRestriction_To_api_ScopeRestriction(in, out, s)
523
+}
524
+
525
+func autoConvert_api_ScopeRestriction_To_v1_ScopeRestriction(in *oauth_api.ScopeRestriction, out *ScopeRestriction, s conversion.Scope) error {
526
+	if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
527
+		defaulting.(func(*oauth_api.ScopeRestriction))(in)
528
+	}
529
+	if in.ExactValues != nil {
530
+		in, out := &in.ExactValues, &out.ExactValues
531
+		*out = make([]string, len(*in))
532
+		copy(*out, *in)
533
+	} else {
534
+		out.ExactValues = nil
535
+	}
536
+	if in.ClusterRole != nil {
537
+		in, out := &in.ClusterRole, &out.ClusterRole
538
+		*out = new(ClusterRoleScopeRestriction)
539
+		if err := Convert_api_ClusterRoleScopeRestriction_To_v1_ClusterRoleScopeRestriction(*in, *out, s); err != nil {
540
+			return err
541
+		}
542
+	} else {
543
+		out.ClusterRole = nil
544
+	}
545
+	return nil
546
+}
547
+
548
+func Convert_api_ScopeRestriction_To_v1_ScopeRestriction(in *oauth_api.ScopeRestriction, out *ScopeRestriction, s conversion.Scope) error {
549
+	return autoConvert_api_ScopeRestriction_To_v1_ScopeRestriction(in, out, s)
550
+}
... ...
@@ -13,6 +13,7 @@ import (
13 13
 
14 14
 func init() {
15 15
 	if err := api.Scheme.AddGeneratedDeepCopyFuncs(
16
+		DeepCopy_v1_ClusterRoleScopeRestriction,
16 17
 		DeepCopy_v1_OAuthAccessToken,
17 18
 		DeepCopy_v1_OAuthAccessTokenList,
18 19
 		DeepCopy_v1_OAuthAuthorizeToken,
... ...
@@ -21,12 +22,32 @@ func init() {
21 21
 		DeepCopy_v1_OAuthClientAuthorization,
22 22
 		DeepCopy_v1_OAuthClientAuthorizationList,
23 23
 		DeepCopy_v1_OAuthClientList,
24
+		DeepCopy_v1_ScopeRestriction,
24 25
 	); err != nil {
25 26
 		// if one of the deep copy functions is malformed, detect it immediately.
26 27
 		panic(err)
27 28
 	}
28 29
 }
29 30
 
31
+func DeepCopy_v1_ClusterRoleScopeRestriction(in ClusterRoleScopeRestriction, out *ClusterRoleScopeRestriction, c *conversion.Cloner) error {
32
+	if in.RoleNames != nil {
33
+		in, out := in.RoleNames, &out.RoleNames
34
+		*out = make([]string, len(in))
35
+		copy(*out, in)
36
+	} else {
37
+		out.RoleNames = nil
38
+	}
39
+	if in.Namespaces != nil {
40
+		in, out := in.Namespaces, &out.Namespaces
41
+		*out = make([]string, len(in))
42
+		copy(*out, in)
43
+	} else {
44
+		out.Namespaces = nil
45
+	}
46
+	out.AllowEscalation = in.AllowEscalation
47
+	return nil
48
+}
49
+
30 50
 func DeepCopy_v1_OAuthAccessToken(in OAuthAccessToken, out *OAuthAccessToken, c *conversion.Cloner) error {
31 51
 	if err := unversioned.DeepCopy_unversioned_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil {
32 52
 		return err
... ...
@@ -132,6 +153,18 @@ func DeepCopy_v1_OAuthClient(in OAuthClient, out *OAuthClient, c *conversion.Clo
132 132
 	} else {
133 133
 		out.RedirectURIs = nil
134 134
 	}
135
+	if in.ScopeRestrictions != nil {
136
+		in, out := in.ScopeRestrictions, &out.ScopeRestrictions
137
+		*out = make([]ScopeRestriction, len(in))
138
+		for i := range in {
139
+			if err := DeepCopy_v1_ScopeRestriction(in[i], &(*out)[i], c); err != nil {
140
+				return err
141
+			}
142
+		}
143
+	} else {
144
+		out.ScopeRestrictions = nil
145
+	}
146
+	out.AllowAnyScope = in.AllowAnyScope
135 147
 	return nil
136 148
 }
137 149
 
... ...
@@ -196,3 +229,23 @@ func DeepCopy_v1_OAuthClientList(in OAuthClientList, out *OAuthClientList, c *co
196 196
 	}
197 197
 	return nil
198 198
 }
199
+
200
+func DeepCopy_v1_ScopeRestriction(in ScopeRestriction, out *ScopeRestriction, c *conversion.Cloner) error {
201
+	if in.ExactValues != nil {
202
+		in, out := in.ExactValues, &out.ExactValues
203
+		*out = make([]string, len(in))
204
+		copy(*out, in)
205
+	} else {
206
+		out.ExactValues = nil
207
+	}
208
+	if in.ClusterRole != nil {
209
+		in, out := in.ClusterRole, &out.ClusterRole
210
+		*out = new(ClusterRoleScopeRestriction)
211
+		if err := DeepCopy_v1_ClusterRoleScopeRestriction(*in, *out, c); err != nil {
212
+			return err
213
+		}
214
+	} else {
215
+		out.ClusterRole = nil
216
+	}
217
+	return nil
218
+}
... ...
@@ -5,6 +5,17 @@ package v1
5 5
 // by hack/update-generated-swagger-descriptions.sh and should be run after a full build of OpenShift.
6 6
 // ==== DO NOT EDIT THIS FILE MANUALLY ====
7 7
 
8
+var map_ClusterRoleScopeRestriction = map[string]string{
9
+	"":                "ClusterRoleScopeRestriction describes restrictions on cluster role scopes",
10
+	"roleNames":       "RoleNames is the list of cluster roles that can referenced.  * means anything",
11
+	"namespaces":      "Namespaces is the list of namespaces that can be referenced.  * means any of them (including *)",
12
+	"allowEscalation": "AllowEscalation indicates whether you can request roles and their escalating resources",
13
+}
14
+
15
+func (ClusterRoleScopeRestriction) SwaggerDoc() map[string]string {
16
+	return map_ClusterRoleScopeRestriction
17
+}
18
+
8 19
 var map_OAuthAccessToken = map[string]string{
9 20
 	"":               "OAuthAccessToken describes an OAuth access token",
10 21
 	"metadata":       "Standard object's metadata.",
... ...
@@ -64,6 +75,8 @@ var map_OAuthClient = map[string]string{
64 64
 	"secret":                "Secret is the unique secret associated with a client",
65 65
 	"respondWithChallenges": "RespondWithChallenges indicates whether the client wants authentication needed responses made in the form of challenges instead of redirects",
66 66
 	"redirectURIs":          "RedirectURIs is the valid redirection URIs associated with a client",
67
+	"scopeRestrictions":     "ScopeRestrictions describes which scopes this client can request.  Each requested scope is checked against each restriction.  If any restriction matches, then the scope is allowed. If no restriction matches, then the scope is denied.",
68
+	"allowAnyScope":         "AllowAnyScope indicates that the client is allowed to request a token unconstrained by scopes. If this is true, then ScopeRestrictions is ignored.",
67 69
 }
68 70
 
69 71
 func (OAuthClient) SwaggerDoc() map[string]string {
... ...
@@ -102,3 +115,13 @@ var map_OAuthClientList = map[string]string{
102 102
 func (OAuthClientList) SwaggerDoc() map[string]string {
103 103
 	return map_OAuthClientList
104 104
 }
105
+
106
+var map_ScopeRestriction = map[string]string{
107
+	"":            "ScopeRestriction describe one restriction on scopes.  Exactly one option must be non-nil.",
108
+	"literals":    "ExactValues means the scope has to match a particular set of strings exactly",
109
+	"clusterRole": "ClusterRole describes a set of restrictions for cluster role scoping.",
110
+}
111
+
112
+func (ScopeRestriction) SwaggerDoc() map[string]string {
113
+	return map_ScopeRestriction
114
+}
... ...
@@ -79,6 +79,34 @@ type OAuthClient struct {
79 79
 
80 80
 	// RedirectURIs is the valid redirection URIs associated with a client
81 81
 	RedirectURIs []string `json:"redirectURIs,omitempty"`
82
+
83
+	// ScopeRestrictions describes which scopes this client can request.  Each requested scope
84
+	// is checked against each restriction.  If any restriction matches, then the scope is allowed.
85
+	// If no restriction matches, then the scope is denied.
86
+	ScopeRestrictions []ScopeRestriction `json:"scopeRestrictions,omitempty"`
87
+
88
+	// AllowAnyScope indicates that the client is allowed to request a token unconstrained by scopes.
89
+	// If this is true, then ScopeRestrictions is ignored.
90
+	AllowAnyScope bool `json:"allowAnyScope,omitempty"`
91
+}
92
+
93
+// ScopeRestriction describe one restriction on scopes.  Exactly one option must be non-nil.
94
+type ScopeRestriction struct {
95
+	// ExactValues means the scope has to match a particular set of strings exactly
96
+	ExactValues []string `json:"literals,omitempty"`
97
+
98
+	// ClusterRole describes a set of restrictions for cluster role scoping.
99
+	ClusterRole *ClusterRoleScopeRestriction `json:"clusterRole,omitempty"`
100
+}
101
+
102
+// ClusterRoleScopeRestriction describes restrictions on cluster role scopes
103
+type ClusterRoleScopeRestriction struct {
104
+	// RoleNames is the list of cluster roles that can referenced.  * means anything
105
+	RoleNames []string `json:"roleNames"`
106
+	// Namespaces is the list of namespaces that can be referenced.  * means any of them (including *)
107
+	Namespaces []string `json:"namespaces"`
108
+	// AllowEscalation indicates whether you can request roles and their escalating resources
109
+	AllowEscalation bool `json:"allowEscalation"`
82 110
 }
83 111
 
84 112
 // OAuthClientAuthorization describes an authorization created by an OAuth client
... ...
@@ -73,6 +73,34 @@ type OAuthClient struct {
73 73
 
74 74
 	// RedirectURIs is the valid redirection URIs associated with a client
75 75
 	RedirectURIs []string `json:"redirectURIs,omitempty"`
76
+
77
+	// ScopeRestrictions describes which scopes this client can request.  Each requested scope
78
+	// is checked against each restriction.  If any restriction matches, then the scope is allowed.
79
+	// If no restriction matches, then the scope is denied.
80
+	ScopeRestrictions []ScopeRestriction `json:"scopeRestrictions,omitempty"`
81
+
82
+	// AllowAnyScope indicates that the client is allowed to request a token unconstrained by scopes.
83
+	// If this is true, then ScopeRestrictions is ignored.
84
+	AllowAnyScope bool `json:"allowAnyScope,omitempty"`
85
+}
86
+
87
+// ScopeRestriction describe one restriction on scopes.  Exactly one option must be non-nil.
88
+type ScopeRestriction struct {
89
+	// ExactValues means the scope has to match a particular set of strings exactly
90
+	ExactValues []string `json:"literals,omitempty"`
91
+
92
+	// ClusterRole describes a set of restrictions for cluster role scoping.
93
+	ClusterRole *ClusterRoleScopeRestriction `json:"clusterRole,omitempty"`
94
+}
95
+
96
+// ClusterRoleScopeRestriction describes restrictions on cluster role scopes
97
+type ClusterRoleScopeRestriction struct {
98
+	// RoleNames is the list of cluster roles that can referenced.  * means anything
99
+	RoleNames []string `json:"roleNames"`
100
+	// Namespaces is the list of namespaces that can be referenced.  * means any of them (including *)
101
+	Namespaces []string `json:"namespaces"`
102
+	// AllowEscalation indicates whether you can request roles and their escalating resources
103
+	AllowEscalation bool `json:"allowEscalation"`
76 104
 }
77 105
 
78 106
 type OAuthClientAuthorization struct {
... ...
@@ -90,6 +90,52 @@ func ValidateClient(client *api.OAuthClient) field.ErrorList {
90 90
 		}
91 91
 	}
92 92
 
93
+	if client.AllowAnyScope && len(client.ScopeRestrictions) > 0 {
94
+		allErrs = append(allErrs, field.Invalid(field.NewPath("scopeRestrictions"), client.ScopeRestrictions, "invalid when allowAnyScope is allowed"))
95
+	}
96
+	if !client.AllowAnyScope && len(client.ScopeRestrictions) == 0 {
97
+		allErrs = append(allErrs, field.Required(field.NewPath("scopeRestrictions"), "required when allowAnyScope is false"))
98
+	}
99
+
100
+	for i, restriction := range client.ScopeRestrictions {
101
+		allErrs = append(allErrs, ValidateScopeRestriction(restriction, field.NewPath("scopeRestrictions").Index(i))...)
102
+	}
103
+
104
+	return allErrs
105
+}
106
+
107
+func ValidateScopeRestriction(restriction api.ScopeRestriction, fldPath *field.Path) field.ErrorList {
108
+	allErrs := field.ErrorList{}
109
+
110
+	specifiers := 0
111
+	if len(restriction.ExactValues) > 0 {
112
+		specifiers = specifiers + 1
113
+	}
114
+	if restriction.ClusterRole != nil {
115
+		specifiers = specifiers + 1
116
+	}
117
+	if specifiers != 1 {
118
+		allErrs = append(allErrs, field.Invalid(fldPath, restriction, "exactly one of literals, clusterRole is required"))
119
+		return allErrs
120
+	}
121
+
122
+	switch {
123
+	case len(restriction.ExactValues) > 0:
124
+		for i, literal := range restriction.ExactValues {
125
+			if len(literal) == 0 {
126
+				allErrs = append(allErrs, field.Invalid(fldPath.Child("literals").Index(i), literal, "may not be empty"))
127
+			}
128
+		}
129
+
130
+	case restriction.ClusterRole != nil:
131
+		if len(restriction.ClusterRole.RoleNames) == 0 {
132
+			allErrs = append(allErrs, field.Required(fldPath.Child("clusterRole", "roleNames"), "won't match anything"))
133
+		}
134
+		if len(restriction.ClusterRole.Namespaces) == 0 {
135
+			allErrs = append(allErrs, field.Required(fldPath.Child("clusterRole", "namespaces"), "won't match anything"))
136
+		}
137
+	}
138
+
93 139
 	return allErrs
94 140
 }
95 141
 
... ...
@@ -173,7 +173,8 @@ func TestValidateClientAuthorization(t *testing.T) {
173 173
 
174 174
 func TestValidateClient(t *testing.T) {
175 175
 	errs := ValidateClient(&oapi.OAuthClient{
176
-		ObjectMeta: api.ObjectMeta{Name: "client-name"},
176
+		ObjectMeta:    api.ObjectMeta{Name: "client-name"},
177
+		AllowAnyScope: true,
177 178
 	})
178 179
 	if len(errs) != 0 {
179 180
 		t.Errorf("expected success: %v", errs)
... ...
@@ -185,15 +186,84 @@ func TestValidateClient(t *testing.T) {
185 185
 		F      string
186 186
 	}{
187 187
 		"zero-length name": {
188
-			Client: oapi.OAuthClient{},
188
+			Client: oapi.OAuthClient{AllowAnyScope: true},
189 189
 			T:      field.ErrorTypeRequired,
190 190
 			F:      "metadata.name",
191 191
 		},
192 192
 		"disallowed namespace": {
193
-			Client: oapi.OAuthClient{ObjectMeta: api.ObjectMeta{Name: "name", Namespace: "foo"}},
193
+			Client: oapi.OAuthClient{ObjectMeta: api.ObjectMeta{Name: "name", Namespace: "foo"}, AllowAnyScope: true},
194 194
 			T:      field.ErrorTypeForbidden,
195 195
 			F:      "metadata.namespace",
196 196
 		},
197
+		"some scope note": {
198
+			Client: oapi.OAuthClient{
199
+				ObjectMeta: api.ObjectMeta{Name: "client-name"},
200
+			},
201
+			T: field.ErrorTypeRequired,
202
+			F: "scopeRestrictions",
203
+		},
204
+		"not both scope notes": {
205
+			Client: oapi.OAuthClient{
206
+				ObjectMeta:        api.ObjectMeta{Name: "client-name"},
207
+				ScopeRestrictions: []oapi.ScopeRestriction{{ExactValues: []string{"a"}}},
208
+				AllowAnyScope:     true,
209
+			},
210
+			T: field.ErrorTypeInvalid,
211
+			F: "scopeRestrictions",
212
+		},
213
+		"literal must have value": {
214
+			Client: oapi.OAuthClient{
215
+				ObjectMeta:        api.ObjectMeta{Name: "client-name"},
216
+				ScopeRestrictions: []oapi.ScopeRestriction{{ExactValues: []string{""}}},
217
+			},
218
+			T: field.ErrorTypeInvalid,
219
+			F: "scopeRestrictions[0].literals[0]",
220
+		},
221
+		"must have some restriction": {
222
+			Client: oapi.OAuthClient{
223
+				ObjectMeta:        api.ObjectMeta{Name: "client-name"},
224
+				ScopeRestrictions: []oapi.ScopeRestriction{{}},
225
+			},
226
+			T: field.ErrorTypeInvalid,
227
+			F: "scopeRestrictions[0]",
228
+		},
229
+		"can't have both restrictions": {
230
+			Client: oapi.OAuthClient{
231
+				ObjectMeta: api.ObjectMeta{Name: "client-name"},
232
+				ScopeRestrictions: []oapi.ScopeRestriction{
233
+					{
234
+						ExactValues: []string{""},
235
+						ClusterRole: &oapi.ClusterRoleScopeRestriction{RoleNames: []string{"a"}, Namespaces: []string{"b"}},
236
+					},
237
+				},
238
+			},
239
+			T: field.ErrorTypeInvalid,
240
+			F: "scopeRestrictions[0]",
241
+		},
242
+		"must have role names": {
243
+			Client: oapi.OAuthClient{
244
+				ObjectMeta: api.ObjectMeta{Name: "client-name"},
245
+				ScopeRestrictions: []oapi.ScopeRestriction{
246
+					{
247
+						ClusterRole: &oapi.ClusterRoleScopeRestriction{Namespaces: []string{"b"}},
248
+					},
249
+				},
250
+			},
251
+			T: field.ErrorTypeRequired,
252
+			F: "scopeRestrictions[0].clusterRole.roleNames",
253
+		},
254
+		"must have namespaces": {
255
+			Client: oapi.OAuthClient{
256
+				ObjectMeta: api.ObjectMeta{Name: "client-name"},
257
+				ScopeRestrictions: []oapi.ScopeRestriction{
258
+					{
259
+						ClusterRole: &oapi.ClusterRoleScopeRestriction{RoleNames: []string{"a"}},
260
+					},
261
+				},
262
+			},
263
+			T: field.ErrorTypeRequired,
264
+			F: "scopeRestrictions[0].clusterRole.namespaces",
265
+		},
197 266
 	}
198 267
 	for k, v := range errorCases {
199 268
 		errs := ValidateClient(&v.Client)
... ...
@@ -14,6 +14,7 @@ import (
14 14
 
15 15
 	"github.com/openshift/origin/pkg/oauth/api"
16 16
 	"github.com/openshift/origin/pkg/oauth/registry/oauthaccesstoken"
17
+	"github.com/openshift/origin/pkg/oauth/registry/oauthclient"
17 18
 	"github.com/openshift/origin/pkg/util"
18 19
 	"github.com/openshift/origin/pkg/util/observe"
19 20
 )
... ...
@@ -27,7 +28,7 @@ type REST struct {
27 27
 const EtcdPrefix = "/oauth/accesstokens"
28 28
 
29 29
 // NewREST returns a RESTStorage object that will work against access tokens
30
-func NewREST(s storage.Interface, backends ...storage.Interface) *REST {
30
+func NewREST(s storage.Interface, clientGetter oauthclient.Getter, backends ...storage.Interface) *REST {
31 31
 	store := &etcdgeneric.Etcd{
32 32
 		NewFunc:     func() runtime.Object { return &api.OAuthAccessToken{} },
33 33
 		NewListFunc: func() runtime.Object { return &api.OAuthAccessTokenList{} },
... ...
@@ -53,7 +54,7 @@ func NewREST(s storage.Interface, backends ...storage.Interface) *REST {
53 53
 		Storage: s,
54 54
 	}
55 55
 
56
-	store.CreateStrategy = oauthaccesstoken.Strategy
56
+	store.CreateStrategy = oauthaccesstoken.NewStrategy(clientGetter)
57 57
 
58 58
 	if len(backends) > 0 {
59 59
 		// Build identical stores that talk to a single etcd, so we can verify the token is distributed after creation
... ...
@@ -11,16 +11,21 @@ import (
11 11
 	"k8s.io/kubernetes/pkg/registry/generic"
12 12
 	"k8s.io/kubernetes/pkg/runtime"
13 13
 	"k8s.io/kubernetes/pkg/util/validation/field"
14
+
15
+	scopeauthorizer "github.com/openshift/origin/pkg/authorization/authorizer/scope"
16
+	"github.com/openshift/origin/pkg/oauth/registry/oauthclient"
14 17
 )
15 18
 
16 19
 // strategy implements behavior for OAuthAccessTokens
17 20
 type strategy struct {
18 21
 	runtime.ObjectTyper
22
+
23
+	clientGetter oauthclient.Getter
19 24
 }
20 25
 
21
-// Strategy is the default logic that applies when creating OAuthAccessToken
22
-// objects via the REST API.
23
-var Strategy = strategy{kapi.Scheme}
26
+func NewStrategy(clientGetter oauthclient.Getter) strategy {
27
+	return strategy{ObjectTyper: kapi.Scheme, clientGetter: clientGetter}
28
+}
24 29
 
25 30
 func (strategy) PrepareForUpdate(obj, old runtime.Object) {}
26 31
 
... ...
@@ -37,9 +42,19 @@ func (strategy) PrepareForCreate(obj runtime.Object) {
37 37
 }
38 38
 
39 39
 // Validate validates a new token
40
-func (strategy) Validate(ctx kapi.Context, obj runtime.Object) field.ErrorList {
40
+func (s strategy) Validate(ctx kapi.Context, obj runtime.Object) field.ErrorList {
41 41
 	token := obj.(*api.OAuthAccessToken)
42
-	return validation.ValidateAccessToken(token)
42
+	validationErrors := validation.ValidateAccessToken(token)
43
+
44
+	client, err := s.clientGetter.GetClient(ctx, token.ClientName)
45
+	if err != nil {
46
+		return append(validationErrors, field.InternalError(field.NewPath("clientName"), err))
47
+	}
48
+	if err := scopeauthorizer.ValidateScopeRestrictions(client, token.Scopes...); err != nil {
49
+		return append(validationErrors, field.InternalError(field.NewPath("clientName"), err))
50
+	}
51
+
52
+	return validationErrors
43 53
 }
44 54
 
45 55
 // AllowCreateOnUpdate is false for OAuth objects
... ...
@@ -14,6 +14,7 @@ import (
14 14
 
15 15
 	"github.com/openshift/origin/pkg/oauth/api"
16 16
 	"github.com/openshift/origin/pkg/oauth/registry/oauthauthorizetoken"
17
+	"github.com/openshift/origin/pkg/oauth/registry/oauthclient"
17 18
 	"github.com/openshift/origin/pkg/util"
18 19
 	"github.com/openshift/origin/pkg/util/observe"
19 20
 )
... ...
@@ -27,7 +28,7 @@ type REST struct {
27 27
 const EtcdPrefix = "/oauth/authorizetokens"
28 28
 
29 29
 // NewREST returns a RESTStorage object that will work against authorize tokens
30
-func NewREST(s storage.Interface, backends ...storage.Interface) *REST {
30
+func NewREST(s storage.Interface, clientGetter oauthclient.Getter, backends ...storage.Interface) *REST {
31 31
 	store := &etcdgeneric.Etcd{
32 32
 		NewFunc:     func() runtime.Object { return &api.OAuthAuthorizeToken{} },
33 33
 		NewListFunc: func() runtime.Object { return &api.OAuthAuthorizeTokenList{} },
... ...
@@ -53,7 +54,7 @@ func NewREST(s storage.Interface, backends ...storage.Interface) *REST {
53 53
 		Storage: s,
54 54
 	}
55 55
 
56
-	store.CreateStrategy = oauthauthorizetoken.Strategy
56
+	store.CreateStrategy = oauthauthorizetoken.NewStrategy(clientGetter)
57 57
 
58 58
 	if len(backends) > 0 {
59 59
 		// Build identical stores that talk to a single etcd, so we can verify the token is distributed after creation
... ...
@@ -11,16 +11,21 @@ import (
11 11
 	"k8s.io/kubernetes/pkg/registry/generic"
12 12
 	"k8s.io/kubernetes/pkg/runtime"
13 13
 	"k8s.io/kubernetes/pkg/util/validation/field"
14
+
15
+	scopeauthorizer "github.com/openshift/origin/pkg/authorization/authorizer/scope"
16
+	"github.com/openshift/origin/pkg/oauth/registry/oauthclient"
14 17
 )
15 18
 
16 19
 // strategy implements behavior for OAuthAuthorizeTokens
17 20
 type strategy struct {
18 21
 	runtime.ObjectTyper
22
+
23
+	clientGetter oauthclient.Getter
19 24
 }
20 25
 
21
-// Strategy is the default logic that applies when creating OAuthAuthorizeToken
22
-// objects via the REST API.
23
-var Strategy = strategy{kapi.Scheme}
26
+func NewStrategy(clientGetter oauthclient.Getter) strategy {
27
+	return strategy{ObjectTyper: kapi.Scheme, clientGetter: clientGetter}
28
+}
24 29
 
25 30
 func (strategy) PrepareForUpdate(obj, old runtime.Object) {}
26 31
 
... ...
@@ -41,9 +46,19 @@ func (strategy) Canonicalize(obj runtime.Object) {
41 41
 }
42 42
 
43 43
 // Validate validates a new token
44
-func (strategy) Validate(ctx kapi.Context, obj runtime.Object) field.ErrorList {
44
+func (s strategy) Validate(ctx kapi.Context, obj runtime.Object) field.ErrorList {
45 45
 	token := obj.(*api.OAuthAuthorizeToken)
46
-	return validation.ValidateAuthorizeToken(token)
46
+	validationErrors := validation.ValidateAuthorizeToken(token)
47
+
48
+	client, err := s.clientGetter.GetClient(ctx, token.ClientName)
49
+	if err != nil {
50
+		return append(validationErrors, field.InternalError(field.NewPath("clientName"), err))
51
+	}
52
+	if err := scopeauthorizer.ValidateScopeRestrictions(client, token.Scopes...); err != nil {
53
+		return append(validationErrors, field.InternalError(field.NewPath("clientName"), err))
54
+	}
55
+
56
+	return validationErrors
47 57
 }
48 58
 
49 59
 // AllowCreateOnUpdate is false for OAuth objects
... ...
@@ -21,6 +21,12 @@ type Registry interface {
21 21
 	DeleteClient(ctx kapi.Context, name string) error
22 22
 }
23 23
 
24
+// Getter exposes a way to get a specific client.  This is useful for other registries to get scope limitations
25
+// on particular clients.   This interface will make its easier to write a future cache on it
26
+type Getter interface {
27
+	GetClient(ctx kapi.Context, name string) (*api.OAuthClient, error)
28
+}
29
+
24 30
 // storage puts strong typing around storage calls
25 31
 type storage struct {
26 32
 	rest.StandardStorage
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	"k8s.io/kubernetes/pkg/storage"
11 11
 
12 12
 	"github.com/openshift/origin/pkg/oauth/api"
13
+	"github.com/openshift/origin/pkg/oauth/registry/oauthclient"
13 14
 	"github.com/openshift/origin/pkg/oauth/registry/oauthclientauthorization"
14 15
 	"github.com/openshift/origin/pkg/util"
15 16
 )
... ...
@@ -22,7 +23,7 @@ type REST struct {
22 22
 const EtcdPrefix = "/oauth/clientauthorizations"
23 23
 
24 24
 // NewREST returns a RESTStorage object that will work against oauth clients
25
-func NewREST(s storage.Interface) *REST {
25
+func NewREST(s storage.Interface, clientGetter oauthclient.Getter) *REST {
26 26
 	store := &etcdgeneric.Etcd{
27 27
 		NewFunc:     func() runtime.Object { return &api.OAuthClientAuthorization{} },
28 28
 		NewListFunc: func() runtime.Object { return &api.OAuthClientAuthorizationList{} },
... ...
@@ -43,8 +44,8 @@ func NewREST(s storage.Interface) *REST {
43 43
 		Storage: s,
44 44
 	}
45 45
 
46
-	store.CreateStrategy = oauthclientauthorization.Strategy
47
-	store.UpdateStrategy = oauthclientauthorization.Strategy
46
+	store.CreateStrategy = oauthclientauthorization.NewStrategy(clientGetter)
47
+	store.UpdateStrategy = oauthclientauthorization.NewStrategy(clientGetter)
48 48
 
49 49
 	return &REST{*store}
50 50
 }
... ...
@@ -11,16 +11,21 @@ import (
11 11
 	"k8s.io/kubernetes/pkg/registry/generic"
12 12
 	"k8s.io/kubernetes/pkg/runtime"
13 13
 	"k8s.io/kubernetes/pkg/util/validation/field"
14
+
15
+	scopeauthorizer "github.com/openshift/origin/pkg/authorization/authorizer/scope"
16
+	"github.com/openshift/origin/pkg/oauth/registry/oauthclient"
14 17
 )
15 18
 
16 19
 // strategy implements behavior for OAuthClientAuthorization objects
17 20
 type strategy struct {
18 21
 	runtime.ObjectTyper
22
+
23
+	clientGetter oauthclient.Getter
19 24
 }
20 25
 
21
-// Strategy is the default logic that applies when creating or updating OAuthClientAuthorization objects
22
-// objects via the REST API.
23
-var Strategy = strategy{kapi.Scheme}
26
+func NewStrategy(clientGetter oauthclient.Getter) strategy {
27
+	return strategy{ObjectTyper: kapi.Scheme, clientGetter: clientGetter}
28
+}
24 29
 
25 30
 func (strategy) PrepareForUpdate(obj, old runtime.Object) {
26 31
 	auth := obj.(*api.OAuthClientAuthorization)
... ...
@@ -46,16 +51,36 @@ func (strategy) Canonicalize(obj runtime.Object) {
46 46
 }
47 47
 
48 48
 // Validate validates a new client
49
-func (strategy) Validate(ctx kapi.Context, obj runtime.Object) field.ErrorList {
49
+func (s strategy) Validate(ctx kapi.Context, obj runtime.Object) field.ErrorList {
50 50
 	auth := obj.(*api.OAuthClientAuthorization)
51
-	return validation.ValidateClientAuthorization(auth)
51
+	validationErrors := validation.ValidateClientAuthorization(auth)
52
+
53
+	client, err := s.clientGetter.GetClient(ctx, auth.ClientName)
54
+	if err != nil {
55
+		return append(validationErrors, field.InternalError(field.NewPath("clientName"), err))
56
+	}
57
+	if err := scopeauthorizer.ValidateScopeRestrictions(client, auth.Scopes...); err != nil {
58
+		return append(validationErrors, field.InternalError(field.NewPath("clientName"), err))
59
+	}
60
+
61
+	return validationErrors
52 62
 }
53 63
 
54 64
 // ValidateUpdate validates a client auth update
55
-func (strategy) ValidateUpdate(ctx kapi.Context, obj runtime.Object, old runtime.Object) field.ErrorList {
65
+func (s strategy) ValidateUpdate(ctx kapi.Context, obj runtime.Object, old runtime.Object) field.ErrorList {
56 66
 	clientAuth := obj.(*api.OAuthClientAuthorization)
57 67
 	oldClientAuth := old.(*api.OAuthClientAuthorization)
58
-	return validation.ValidateClientAuthorizationUpdate(clientAuth, oldClientAuth)
68
+	validationErrors := validation.ValidateClientAuthorizationUpdate(clientAuth, oldClientAuth)
69
+
70
+	client, err := s.clientGetter.GetClient(ctx, clientAuth.ClientName)
71
+	if err != nil {
72
+		return append(validationErrors, field.InternalError(field.NewPath("clientName"), err))
73
+	}
74
+	if err := scopeauthorizer.ValidateScopeRestrictions(client, clientAuth.Scopes...); err != nil {
75
+		return append(validationErrors, field.InternalError(field.NewPath("clientName"), err))
76
+	}
77
+
78
+	return validationErrors
59 79
 }
60 80
 
61 81
 func (strategy) AllowCreateOnUpdate() bool {
... ...
@@ -34,6 +34,11 @@ func Join(scopes []string) string {
34 34
 }
35 35
 
36 36
 func Covers(has, requested []string) bool {
37
+	// no scopes allows all access, so requesting an empty list is NOT covered by a list with anything in it
38
+	if len(requested) == 0 && len(has) > 0 {
39
+		return false
40
+	}
41
+
37 42
 	has, requested = sortAndCopy(has), sortAndCopy(requested)
38 43
 NextRequested:
39 44
 	for i := range requested {
... ...
@@ -35,8 +35,13 @@ func checkAdd(t *testing.T, s1, s2, expected []string) {
35 35
 func TestCovers(t *testing.T) {
36 36
 	// Empty request
37 37
 	checkCovers(t, []string{}, []string{}, true)
38
-	checkCovers(t, []string{"A"}, []string{}, true)
39
-	checkCovers(t, []string{"B", "A"}, []string{}, true)
38
+	checkCovers(t, []string{"A"}, []string{}, false)
39
+	checkCovers(t, []string{"B", "A"}, []string{}, false)
40
+
41
+	// empty list is effectively everything and we have validation to keep people from doing an empty list
42
+	// but I'm not that we'll keep it like this (might add a "full" scope), so leave it failing in this
43
+	// direction too for now.
44
+	checkCovers(t, []string{}, []string{"B", "A"}, false)
40 45
 
41 46
 	// Equal request
42 47
 	checkCovers(t, []string{"A"}, []string{"A"}, true)
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	kerrors "k8s.io/kubernetes/pkg/api/errors"
11 11
 	"k8s.io/kubernetes/pkg/api/unversioned"
12 12
 
13
+	scopeauthorizer "github.com/openshift/origin/pkg/authorization/authorizer/scope"
13 14
 	"github.com/openshift/origin/pkg/oauth/api"
14 15
 	"github.com/openshift/origin/pkg/oauth/registry/oauthaccesstoken"
15 16
 	"github.com/openshift/origin/pkg/oauth/registry/oauthauthorizetoken"
... ...
@@ -186,6 +187,9 @@ func (s *storage) convertFromAuthorizeToken(authorize *api.OAuthAuthorizeToken)
186 186
 	if err != nil {
187 187
 		return nil, err
188 188
 	}
189
+	if err := scopeauthorizer.ValidateScopeRestrictions(client, authorize.Scopes...); err != nil {
190
+		return nil, err
191
+	}
189 192
 
190 193
 	return &osin.AuthorizeData{
191 194
 		Code:        authorize.Name,
... ...
@@ -229,6 +233,9 @@ func (s *storage) convertFromAccessToken(access *api.OAuthAccessToken) (*osin.Ac
229 229
 	if err != nil {
230 230
 		return nil, err
231 231
 	}
232
+	if err := scopeauthorizer.ValidateScopeRestrictions(client, access.Scopes...); err != nil {
233
+		return nil, err
234
+	}
232 235
 
233 236
 	return &osin.AccessData{
234 237
 		AccessToken:  access.Name,
... ...
@@ -53,7 +53,7 @@ func TestAuthProxyOnAuthorize(t *testing.T) {
53 53
 	t.Logf("proxy server is on %v\n", proxyServer.URL)
54 54
 
55 55
 	// need to prime clients so that we can get back a code.  the client must be valid
56
-	result := clusterAdminClient.RESTClient.Post().Resource("oAuthClients").Body(&oauthapi.OAuthClient{ObjectMeta: kapi.ObjectMeta{Name: "test"}, Secret: "secret", RedirectURIs: []string{clusterAdminClientConfig.Host}}).Do()
56
+	result := clusterAdminClient.RESTClient.Post().Resource("oAuthClients").Body(&oauthapi.OAuthClient{ObjectMeta: kapi.ObjectMeta{Name: "test"}, Secret: "secret", RedirectURIs: []string{clusterAdminClientConfig.Host}, AllowAnyScope: true}).Do()
57 57
 	checkErr(t, result.Error())
58 58
 
59 59
 	// our simple URL to get back a code.  We want to go through the front proxy
... ...
@@ -64,13 +64,14 @@ func TestOAuthStorage(t *testing.T) {
64 64
 	}
65 65
 	etcdHelper := etcdstorage.NewEtcdStorage(etcdClient, kapi.Codecs.LegacyCodec(groupMeta.GroupVersions...), etcdtest.PathPrefix(), false)
66 66
 
67
-	accessTokenStorage := accesstokenetcd.NewREST(etcdHelper)
68
-	accessTokenRegistry := accesstokenregistry.NewRegistry(accessTokenStorage)
69
-	authorizeTokenStorage := authorizetokenetcd.NewREST(etcdHelper)
70
-	authorizeTokenRegistry := authorizetokenregistry.NewRegistry(authorizeTokenStorage)
71 67
 	clientStorage := clientetcd.NewREST(etcdHelper)
72 68
 	clientRegistry := clientregistry.NewRegistry(clientStorage)
73 69
 
70
+	accessTokenStorage := accesstokenetcd.NewREST(etcdHelper, clientRegistry)
71
+	accessTokenRegistry := accesstokenregistry.NewRegistry(accessTokenStorage)
72
+	authorizeTokenStorage := authorizetokenetcd.NewREST(etcdHelper, clientRegistry)
73
+	authorizeTokenRegistry := authorizetokenregistry.NewRegistry(authorizeTokenStorage)
74
+
74 75
 	user := &testUser{UserName: "test", UserUID: "1"}
75 76
 	storage := registrystorage.New(accessTokenRegistry, authorizeTokenRegistry, clientRegistry, user)
76 77
 
... ...
@@ -112,9 +113,10 @@ func TestOAuthStorage(t *testing.T) {
112 112
 	}))
113 113
 
114 114
 	clientRegistry.CreateClient(kapi.NewContext(), &api.OAuthClient{
115
-		ObjectMeta:   kapi.ObjectMeta{Name: "test"},
116
-		Secret:       "secret",
117
-		RedirectURIs: []string{assertServer.URL + "/assert"},
115
+		ObjectMeta:    kapi.ObjectMeta{Name: "test"},
116
+		Secret:        "secret",
117
+		RedirectURIs:  []string{assertServer.URL + "/assert"},
118
+		AllowAnyScope: true,
118 119
 	})
119 120
 	storedClient, err := storage.GetClient("test")
120 121
 	if err != nil {
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	"github.com/openshift/origin/pkg/authorization/authorizer/scope"
15 15
 	buildapi "github.com/openshift/origin/pkg/build/api"
16 16
 	"github.com/openshift/origin/pkg/client"
17
+	"github.com/openshift/origin/pkg/cmd/server/origin"
17 18
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
18 19
 	oauthapi "github.com/openshift/origin/pkg/oauth/api"
19 20
 	userapi "github.com/openshift/origin/pkg/user/api"
... ...
@@ -56,7 +57,7 @@ func TestScopedTokens(t *testing.T) {
56 56
 
57 57
 	whoamiOnlyToken := &oauthapi.OAuthAccessToken{
58 58
 		ObjectMeta: kapi.ObjectMeta{Name: "whoami-token-plus-some-padding-here-to-make-the-limit"},
59
-		ClientName: "any-client",
59
+		ClientName: origin.OpenShiftCLIClientID,
60 60
 		ExpiresIn:  200,
61 61
 		Scopes:     []string{scope.UserIndicator + scope.UserInfo},
62 62
 		UserName:   userName,
... ...
@@ -176,7 +177,7 @@ func TestScopeEscalations(t *testing.T) {
176 176
 
177 177
 	nonEscalatingEditToken := &oauthapi.OAuthAccessToken{
178 178
 		ObjectMeta: kapi.ObjectMeta{Name: "non-escalating-edit-plus-some-padding-here-to-make-the-limit"},
179
-		ClientName: "any-client",
179
+		ClientName: origin.OpenShiftCLIClientID,
180 180
 		ExpiresIn:  200,
181 181
 		Scopes:     []string{scope.ClusterRoleIndicator + "edit:*"},
182 182
 		UserName:   userName,
... ...
@@ -199,7 +200,7 @@ func TestScopeEscalations(t *testing.T) {
199 199
 
200 200
 	escalatingEditToken := &oauthapi.OAuthAccessToken{
201 201
 		ObjectMeta: kapi.ObjectMeta{Name: "escalating-edit-plus-some-padding-here-to-make-the-limit"},
202
-		ClientName: "any-client",
202
+		ClientName: origin.OpenShiftCLIClientID,
203 203
 		ExpiresIn:  200,
204 204
 		Scopes:     []string{scope.ClusterRoleIndicator + "edit:*:!"},
205 205
 		UserName:   userName,