Browse code

add rule to allow self-subject access reviews

deads2k authored on 2015/03/20 05:16:18
Showing 10 changed files
... ...
@@ -18,6 +18,8 @@ func init() {
18 18
 		&PolicyBindingList{},
19 19
 		&RoleBindingList{},
20 20
 		&RoleList{},
21
+
22
+		&IsPersonalSubjectAccessReview{},
21 23
 	)
22 24
 }
23 25
 
... ...
@@ -33,3 +35,5 @@ func (*PolicyList) IsAnAPIObject()                   {}
33 33
 func (*PolicyBindingList) IsAnAPIObject()            {}
34 34
 func (*RoleBindingList) IsAnAPIObject()              {}
35 35
 func (*RoleList) IsAnAPIObject()                     {}
36
+
37
+func (*IsPersonalSubjectAccessReview) IsAnAPIObject() {}
... ...
@@ -83,6 +83,11 @@ type PolicyRule struct {
83 83
 	NonResourceURLs kutil.StringSet
84 84
 }
85 85
 
86
+// IsPersonalSubjectAccessReview is a marker for PolicyRule.AttributeRestrictions that denotes that subjectaccessreviews on self should be allowed
87
+type IsPersonalSubjectAccessReview struct {
88
+	kapi.TypeMeta
89
+}
90
+
86 91
 // Role is a logical grouping of PolicyRules that can be referenced as a unit by RoleBindings.
87 92
 type Role struct {
88 93
 	kapi.TypeMeta
... ...
@@ -18,6 +18,8 @@ func init() {
18 18
 		&PolicyBindingList{},
19 19
 		&RoleBindingList{},
20 20
 		&RoleList{},
21
+
22
+		&IsPersonalSubjectAccessReview{},
21 23
 	)
22 24
 }
23 25
 
... ...
@@ -33,3 +35,5 @@ func (*PolicyList) IsAnAPIObject()                   {}
33 33
 func (*PolicyBindingList) IsAnAPIObject()            {}
34 34
 func (*RoleBindingList) IsAnAPIObject()              {}
35 35
 func (*RoleList) IsAnAPIObject()                     {}
36
+
37
+func (*IsPersonalSubjectAccessReview) IsAnAPIObject() {}
... ...
@@ -20,7 +20,7 @@ type PolicyRule struct {
20 20
 	Verbs []string `json:"verbs"`
21 21
 	// AttributeRestrictions will vary depending on what the Authorizer/AuthorizationAttributeBuilder pair supports.
22 22
 	// If the Authorizer does not recognize how to handle the AttributeRestrictions, the Authorizer should report an error.
23
-	AttributeRestrictions kruntime.RawExtension `json:"attributeRestrictions"`
23
+	AttributeRestrictions kruntime.RawExtension `json:"attributeRestrictions,omitempty"`
24 24
 	// ResourceKinds is a list of resources this rule applies to.  ResourceAll represents all resources.
25 25
 	// DEPRECATED
26 26
 	ResourceKinds []string `json:"resourceKinds,omitempty"`
... ...
@@ -33,6 +33,11 @@ type PolicyRule struct {
33 33
 	NonResourceURLsSlice []string `json:"nonResourceURLs,omitempty"`
34 34
 }
35 35
 
36
+// IsPersonalSubjectAccessReview is a marker for PolicyRule.AttributeRestrictions that denotes that subjectaccessreviews on self should be allowed
37
+type IsPersonalSubjectAccessReview struct {
38
+	kapi.TypeMeta `json:",inline"`
39
+}
40
+
36 41
 // Role is a logical grouping of PolicyRules that can be referenced as a unit by RoleBindings.
37 42
 type Role struct {
38 43
 	kapi.TypeMeta   `json:",inline"`
... ...
@@ -1,6 +1,7 @@
1 1
 package authorizer
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"path"
5 6
 	"strings"
6 7
 
... ...
@@ -34,6 +35,16 @@ func (a DefaultAuthorizationAttributes) RuleMatches(rule authorizationapi.Policy
34 34
 
35 35
 		if a.resourceMatches(allowedResourceTypes) {
36 36
 			if a.nameMatches(rule.ResourceNames) {
37
+				// this rule matches the request, so we should check the additional restrictions to be sure that it's allowed
38
+				if rule.AttributeRestrictions.Object != nil {
39
+					switch rule.AttributeRestrictions.Object.(type) {
40
+					case (*authorizationapi.IsPersonalSubjectAccessReview):
41
+						return IsPersonalAccessReview(a)
42
+					default:
43
+						return false, fmt.Errorf("unable to interpret: %#v", rule.AttributeRestrictions.Object)
44
+					}
45
+				}
46
+
37 47
 				return true, nil
38 48
 			}
39 49
 		}
... ...
@@ -40,7 +40,7 @@ func (a *openshiftAuthorizationAttributeBuilder) GetAttributes(req *http.Request
40 40
 		Verb:              requestInfo.Verb,
41 41
 		Resource:          requestInfo.Resource,
42 42
 		ResourceName:      requestInfo.Name,
43
-		RequestAttributes: nil,
43
+		RequestAttributes: req,
44 44
 		NonResourceURL:    false,
45 45
 		URL:               req.URL.Path,
46 46
 	}, nil
... ...
@@ -5,6 +5,7 @@ import (
5 5
 
6 6
 	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
7 7
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
8
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
8 9
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
9 10
 
10 11
 	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
... ...
@@ -12,6 +13,40 @@ import (
12 12
 	"github.com/openshift/origin/pkg/cmd/server/bootstrappolicy"
13 13
 )
14 14
 
15
+func TestInvalidRole(t *testing.T) {
16
+	test := &authorizeTest{
17
+		context: kapi.WithUser(kapi.WithNamespace(kapi.NewContext(), "mallet"), &user.DefaultInfo{Name: "Brad"}),
18
+		attributes: &DefaultAuthorizationAttributes{
19
+			Verb:     "get",
20
+			Resource: "buildConfigs",
21
+		},
22
+		expectedAllowed: false,
23
+		expectedError:   "unable to interpret:",
24
+	}
25
+	test.policies = newDefaultGlobalPolicies()
26
+	test.policies = append(test.policies, newInvalidExtensionPolicies()...)
27
+	test.bindings = newDefaultGlobalBinding()
28
+	test.bindings = append(test.bindings, newInvalidExtensionBindings()...)
29
+
30
+	test.test(t)
31
+}
32
+func TestInvalidRoleButRuleNotUsed(t *testing.T) {
33
+	test := &authorizeTest{
34
+		context: kapi.WithUser(kapi.WithNamespace(kapi.NewContext(), "mallet"), &user.DefaultInfo{Name: "Brad"}),
35
+		attributes: &DefaultAuthorizationAttributes{
36
+			Verb:     "update",
37
+			Resource: "buildConfigs",
38
+		},
39
+		expectedAllowed: true,
40
+		expectedReason:  "allowed by rule in mallet",
41
+	}
42
+	test.policies = newDefaultGlobalPolicies()
43
+	test.policies = append(test.policies, newInvalidExtensionPolicies()...)
44
+	test.bindings = newDefaultGlobalBinding()
45
+	test.bindings = append(test.bindings, newInvalidExtensionBindings()...)
46
+
47
+	test.test(t)
48
+}
15 49
 func TestViewerGetAllowedKindInMallet(t *testing.T) {
16 50
 	test := &authorizeTest{
17 51
 		context: kapi.WithUser(kapi.WithNamespace(kapi.NewContext(), "mallet"), &user.DefaultInfo{Name: "Victor"}),
... ...
@@ -418,6 +453,57 @@ func newMalletBindings() []authorizationapi.PolicyBinding {
418 418
 		},
419 419
 	}
420 420
 }
421
+func newInvalidExtensionPolicies() []authorizationapi.Policy {
422
+	return []authorizationapi.Policy{
423
+		{
424
+			ObjectMeta: kapi.ObjectMeta{
425
+				Name:      authorizationapi.PolicyName,
426
+				Namespace: "mallet",
427
+			},
428
+			Roles: map[string]authorizationapi.Role{
429
+				"badExtension": {
430
+					ObjectMeta: kapi.ObjectMeta{
431
+						Name:      "failure",
432
+						Namespace: "mallet",
433
+					},
434
+					Rules: []authorizationapi.PolicyRule{
435
+						{
436
+							Verbs:                 util.NewStringSet("watch", "list", "get"),
437
+							Resources:             util.NewStringSet("buildConfigs"),
438
+							AttributeRestrictions: runtime.EmbeddedObject{&authorizationapi.Role{}},
439
+						},
440
+						{
441
+							Verbs:     util.NewStringSet("update"),
442
+							Resources: util.NewStringSet("buildConfigs"),
443
+						},
444
+					},
445
+				},
446
+			},
447
+		}}
448
+}
449
+func newInvalidExtensionBindings() []authorizationapi.PolicyBinding {
450
+	return []authorizationapi.PolicyBinding{
451
+		{
452
+			ObjectMeta: kapi.ObjectMeta{
453
+				Name:      "mallet",
454
+				Namespace: "mallet",
455
+			},
456
+			RoleBindings: map[string]authorizationapi.RoleBinding{
457
+				"borked": {
458
+					ObjectMeta: kapi.ObjectMeta{
459
+						Name:      "borked",
460
+						Namespace: "mallet",
461
+					},
462
+					RoleRef: kapi.ObjectReference{
463
+						Name:      "badExtension",
464
+						Namespace: "mallet",
465
+					},
466
+					Users: util.NewStringSet("Brad"),
467
+				},
468
+			},
469
+		},
470
+	}
471
+}
421 472
 
422 473
 func GetBootstrapPolicy(masterNamespace string) *authorizationapi.Policy {
423 474
 	policy := &authorizationapi.Policy{
424 475
new file mode 100644
... ...
@@ -0,0 +1,37 @@
0
+package authorizer
1
+
2
+import (
3
+	"bytes"
4
+	"errors"
5
+	"io/ioutil"
6
+	"net/http"
7
+
8
+	"github.com/openshift/origin/pkg/api/latest"
9
+	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
10
+)
11
+
12
+func IsPersonalAccessReview(a AuthorizationAttributes) (bool, error) {
13
+	req, ok := a.GetRequestAttributes().(*http.Request)
14
+	if !ok {
15
+		return false, errors.New("expected request, but did not get one")
16
+	}
17
+
18
+	// TODO once we're integrated with the api installer, we should have direct access to the deserialized content
19
+	// for now, this only happens on subjectaccessreviews with a personal check, pay the double retrieve and decode cost
20
+	body, err := ioutil.ReadAll(req.Body)
21
+	if err != nil {
22
+		return false, err
23
+	}
24
+	req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
25
+
26
+	subjectAccessReview := &authorizationapi.SubjectAccessReview{}
27
+	if err := latest.Codec.DecodeInto(body, subjectAccessReview); err != nil {
28
+		return false, err
29
+	}
30
+
31
+	if (len(subjectAccessReview.User) == 0) && (len(subjectAccessReview.Groups) == 0) {
32
+		return true, nil
33
+	}
34
+
35
+	return false, nil
36
+}
... ...
@@ -2,6 +2,7 @@ package bootstrappolicy
2 2
 
3 3
 import (
4 4
 	kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api"
5
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
5 6
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
6 7
 
7 8
 	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
... ...
@@ -102,6 +103,7 @@ func GetBootstrapMasterRoles(masterNamespace string) []authorizationapi.Role {
102 102
 			Rules: []authorizationapi.PolicyRule{
103 103
 				{Verbs: util.NewStringSet("get"), Resources: util.NewStringSet("users"), ResourceNames: util.NewStringSet("~")},
104 104
 				{Verbs: util.NewStringSet("list"), Resources: util.NewStringSet("projects")},
105
+				{Verbs: util.NewStringSet("create"), Resources: util.NewStringSet("subjectaccessreviews"), AttributeRestrictions: runtime.EmbeddedObject{&authorizationapi.IsPersonalSubjectAccessReview{}}},
105 106
 			},
106 107
 		},
107 108
 		{
... ...
@@ -10,6 +10,7 @@ import (
10 10
 	kapierror "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors"
11 11
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/fields"
12 12
 	"github.com/GoogleCloudPlatform/kubernetes/pkg/labels"
13
+	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
13 14
 
14 15
 	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
15 16
 	"github.com/openshift/origin/pkg/client"
... ...
@@ -106,3 +107,56 @@ func TestOverwritePolicyCommand(t *testing.T) {
106 106
 		t.Errorf("unexpected error: %v", err)
107 107
 	}
108 108
 }
109
+
110
+func TestSelfSubjectAccessReviews(t *testing.T) {
111
+	_, clusterAdminKubeConfig, err := testutil.StartTestMaster()
112
+	if err != nil {
113
+		t.Fatalf("unexpected error: %v", err)
114
+	}
115
+
116
+	clusterAdminClientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig)
117
+	if err != nil {
118
+		t.Errorf("unexpected error: %v", err)
119
+	}
120
+
121
+	valerieClientConfig := *clusterAdminClientConfig
122
+	valerieClientConfig.Username = ""
123
+	valerieClientConfig.Password = ""
124
+	valerieClientConfig.BearerToken = ""
125
+	valerieClientConfig.CertFile = ""
126
+	valerieClientConfig.KeyFile = ""
127
+	valerieClientConfig.CertData = nil
128
+	valerieClientConfig.KeyData = nil
129
+
130
+	accessToken, err := tokencmd.RequestToken(&valerieClientConfig, nil, "valerie", "security!")
131
+	if err != nil {
132
+		t.Fatalf("unexpected error: %v", err)
133
+	}
134
+
135
+	valerieClientConfig.BearerToken = accessToken
136
+	valerieOpenshiftClient, err := client.New(&valerieClientConfig)
137
+	if err != nil {
138
+		t.Fatalf("unexpected error: %v", err)
139
+	}
140
+
141
+	// can I get a subjectaccessreview on myself even if I have no rights to do it generally
142
+	askCanICreatePolicyBindings := &authorizationapi.SubjectAccessReview{Verb: "create", Resource: "policybindings"}
143
+	subjectAccessReviewTest{
144
+		clientInterface: valerieOpenshiftClient.SubjectAccessReviews("openshift"),
145
+		review:          askCanICreatePolicyBindings,
146
+		response: authorizationapi.SubjectAccessReviewResponse{
147
+			Allowed:   false,
148
+			Reason:    "denied by default",
149
+			Namespace: "openshift",
150
+		},
151
+	}.run(t)
152
+
153
+	// I shouldn't be allowed to ask whether someone else can perform an action
154
+	askCanClusterAdminsCreateProject := &authorizationapi.SubjectAccessReview{Groups: util.NewStringSet("system:cluster-admins"), Verb: "create", Resource: "projects"}
155
+	subjectAccessReviewTest{
156
+		clientInterface: valerieOpenshiftClient.SubjectAccessReviews("openshift"),
157
+		review:          askCanClusterAdminsCreateProject,
158
+		err:             "Forbidden",
159
+	}.run(t)
160
+
161
+}