| ... | ... |
@@ -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", |