Browse code

policy unsafe proxy requests separately

Jordan Liggitt authored on 2016/04/27 00:52:40
Showing 5 changed files
... ...
@@ -5,15 +5,14 @@ import (
5 5
 	"strings"
6 6
 
7 7
 	kapi "k8s.io/kubernetes/pkg/api"
8
-	kapiserver "k8s.io/kubernetes/pkg/apiserver"
9 8
 )
10 9
 
11 10
 type openshiftAuthorizationAttributeBuilder struct {
12 11
 	contextMapper kapi.RequestContextMapper
13
-	infoResolver  *kapiserver.RequestInfoResolver
12
+	infoResolver  RequestInfoResolver
14 13
 }
15 14
 
16
-func NewAuthorizationAttributeBuilder(contextMapper kapi.RequestContextMapper, infoResolver *kapiserver.RequestInfoResolver) AuthorizationAttributeBuilder {
15
+func NewAuthorizationAttributeBuilder(contextMapper kapi.RequestContextMapper, infoResolver RequestInfoResolver) AuthorizationAttributeBuilder {
17 16
 	return &openshiftAuthorizationAttributeBuilder{contextMapper, infoResolver}
18 17
 }
19 18
 
20 19
new file mode 100644
... ...
@@ -0,0 +1,75 @@
0
+package authorizer
1
+
2
+import (
3
+	"net/http"
4
+
5
+	kapi "k8s.io/kubernetes/pkg/api"
6
+	kapiserver "k8s.io/kubernetes/pkg/apiserver"
7
+	"k8s.io/kubernetes/pkg/util/sets"
8
+)
9
+
10
+type browserSafeRequestInfoResolver struct {
11
+	// infoResolver is used to determine info for the request
12
+	infoResolver RequestInfoResolver
13
+
14
+	// contextMapper is used to look up the context corresponding to a request
15
+	// to obtain the user associated with the request
16
+	contextMapper kapi.RequestContextMapper
17
+
18
+	// list of groups, any of which indicate the request is authenticated
19
+	authenticatedGroups sets.String
20
+}
21
+
22
+func NewBrowserSafeRequestInfoResolver(contextMapper kapi.RequestContextMapper, authenticatedGroups sets.String, infoResolver RequestInfoResolver) RequestInfoResolver {
23
+	return &browserSafeRequestInfoResolver{
24
+		contextMapper:       contextMapper,
25
+		authenticatedGroups: authenticatedGroups,
26
+		infoResolver:        infoResolver,
27
+	}
28
+}
29
+
30
+func (a *browserSafeRequestInfoResolver) GetRequestInfo(req *http.Request) (kapiserver.RequestInfo, error) {
31
+	requestInfo, err := a.infoResolver.GetRequestInfo(req)
32
+	if err != nil {
33
+		return requestInfo, err
34
+	}
35
+
36
+	if !requestInfo.IsResourceRequest {
37
+		return requestInfo, nil
38
+	}
39
+
40
+	isProxyVerb := requestInfo.Verb == "proxy"
41
+	isProxySubresource := requestInfo.Subresource == "proxy"
42
+
43
+	if !isProxyVerb && !isProxySubresource {
44
+		// Requests to non-proxy resources don't expose HTML or HTTP-handling user content to browsers
45
+		return requestInfo, nil
46
+	}
47
+
48
+	if len(req.Header.Get("X-CSRF-Token")) > 0 {
49
+		// Browsers cannot set custom headers on direct requests
50
+		return requestInfo, nil
51
+	}
52
+
53
+	if ctx, hasContext := a.contextMapper.Get(req); hasContext {
54
+		user, hasUser := kapi.UserFrom(ctx)
55
+		if hasUser && a.authenticatedGroups.HasAny(user.GetGroups()...) {
56
+			// An authenticated request indicates this isn't a browser page load.
57
+			// Browsers cannot make direct authenticated requests.
58
+			// This depends on the API not enabling basic or cookie-based auth.
59
+			return requestInfo, nil
60
+		}
61
+
62
+	}
63
+
64
+	// TODO: compare request.Host to a list of hosts allowed for the requestInfo.Namespace (e.g. <namespace>.proxy.example.com)
65
+
66
+	if isProxyVerb {
67
+		requestInfo.Verb = "unsafeproxy"
68
+	}
69
+	if isProxySubresource {
70
+		requestInfo.Subresource = "unsafeproxy"
71
+	}
72
+
73
+	return requestInfo, nil
74
+}
0 75
new file mode 100644
... ...
@@ -0,0 +1,147 @@
0
+package authorizer
1
+
2
+import (
3
+	"net/http"
4
+	"testing"
5
+
6
+	kapi "k8s.io/kubernetes/pkg/api"
7
+	kapiserver "k8s.io/kubernetes/pkg/apiserver"
8
+	"k8s.io/kubernetes/pkg/auth/user"
9
+	"k8s.io/kubernetes/pkg/util/sets"
10
+)
11
+
12
+func TestUpstreamInfoResolver(t *testing.T) {
13
+	subresourceRequest, _ := http.NewRequest("GET", "/api/v1/namespaces/myns/pods/mypod/proxy", nil)
14
+	proxyRequest, _ := http.NewRequest("GET", "/api/v1/proxy/nodes/mynode", nil)
15
+
16
+	testcases := map[string]struct {
17
+		Request             *http.Request
18
+		ExpectedVerb        string
19
+		ExpectedSubresource string
20
+	}{
21
+		"unsafe proxy subresource": {
22
+			Request:             subresourceRequest,
23
+			ExpectedVerb:        "get",
24
+			ExpectedSubresource: "proxy", // should be "unsafeproxy" or similar once check moves upstream
25
+		},
26
+		"unsafe proxy verb": {
27
+			Request:      proxyRequest,
28
+			ExpectedVerb: "proxy", // should be "unsafeproxy" or similar once check moves upstream
29
+		},
30
+	}
31
+
32
+	for k, tc := range testcases {
33
+		resolver := &kapiserver.RequestInfoResolver{
34
+			APIPrefixes:          sets.NewString("api", "osapi", "oapi", "apis"),
35
+			GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi"),
36
+		}
37
+
38
+		info, err := resolver.GetRequestInfo(tc.Request)
39
+		if err != nil {
40
+			t.Errorf("%s: unexpected error: %v", k, err)
41
+			continue
42
+		}
43
+
44
+		if info.Verb != tc.ExpectedVerb {
45
+			t.Errorf("%s: expected verb %s, got %s. If kapiserver.RequestInfoResolver now adjusts attributes for proxy safety, investigate removing the NewBrowserSafeRequestInfoResolver wrapper.", k, tc.ExpectedVerb, info.Verb)
46
+		}
47
+		if info.Subresource != tc.ExpectedSubresource {
48
+			t.Errorf("%s: expected verb %s, got %s. If kapiserver.RequestInfoResolver now adjusts attributes for proxy safety, investigate removing the NewBrowserSafeRequestInfoResolver wrapper.", k, tc.ExpectedSubresource, info.Subresource)
49
+		}
50
+	}
51
+}
52
+
53
+func TestBrowserSafeRequestInfoResolver(t *testing.T) {
54
+	testcases := map[string]struct {
55
+		RequestInfo kapiserver.RequestInfo
56
+		Context     kapi.Context
57
+		Host        string
58
+		Headers     http.Header
59
+
60
+		ExpectedVerb        string
61
+		ExpectedSubresource string
62
+	}{
63
+		"non-resource": {
64
+			RequestInfo:  kapiserver.RequestInfo{IsResourceRequest: false, Verb: "GET"},
65
+			ExpectedVerb: "GET",
66
+		},
67
+
68
+		"non-proxy": {
69
+			RequestInfo:         kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "logs"},
70
+			ExpectedVerb:        "get",
71
+			ExpectedSubresource: "logs",
72
+		},
73
+
74
+		"unsafe proxy subresource": {
75
+			RequestInfo:         kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "proxy"},
76
+			ExpectedVerb:        "get",
77
+			ExpectedSubresource: "unsafeproxy",
78
+		},
79
+		"unsafe proxy verb": {
80
+			RequestInfo:  kapiserver.RequestInfo{IsResourceRequest: true, Verb: "proxy", Resource: "nodes"},
81
+			ExpectedVerb: "unsafeproxy",
82
+		},
83
+		"unsafe proxy verb anonymous": {
84
+			Context:      kapi.WithUser(kapi.NewContext(), &user.DefaultInfo{Name: "system:anonymous", Groups: []string{"system:unauthenticated"}}),
85
+			RequestInfo:  kapiserver.RequestInfo{IsResourceRequest: true, Verb: "proxy", Resource: "nodes"},
86
+			ExpectedVerb: "unsafeproxy",
87
+		},
88
+
89
+		"proxy subresource authenticated": {
90
+			Context:             kapi.WithUser(kapi.NewContext(), &user.DefaultInfo{Name: "bob", Groups: []string{"system:authenticated"}}),
91
+			RequestInfo:         kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "proxy"},
92
+			ExpectedVerb:        "get",
93
+			ExpectedSubresource: "proxy",
94
+		},
95
+		"proxy subresource custom header": {
96
+			RequestInfo:         kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "proxy"},
97
+			Headers:             http.Header{"X-Csrf-Token": []string{"1"}},
98
+			ExpectedVerb:        "get",
99
+			ExpectedSubresource: "proxy",
100
+		},
101
+	}
102
+
103
+	for k, tc := range testcases {
104
+		resolver := NewBrowserSafeRequestInfoResolver(
105
+			&testContextMapper{tc.Context},
106
+			sets.NewString("system:authenticated"),
107
+			&testInfoResolver{tc.RequestInfo},
108
+		)
109
+
110
+		req, _ := http.NewRequest("GET", "/", nil)
111
+		req.Host = tc.Host
112
+		req.Header = tc.Headers
113
+
114
+		info, err := resolver.GetRequestInfo(req)
115
+		if err != nil {
116
+			t.Errorf("%s: unexpected error: %v", k, err)
117
+			continue
118
+		}
119
+
120
+		if info.Verb != tc.ExpectedVerb {
121
+			t.Errorf("%s: expected verb %s, got %s", k, tc.ExpectedVerb, info.Verb)
122
+		}
123
+		if info.Subresource != tc.ExpectedSubresource {
124
+			t.Errorf("%s: expected verb %s, got %s", k, tc.ExpectedSubresource, info.Subresource)
125
+		}
126
+	}
127
+}
128
+
129
+type testContextMapper struct {
130
+	context kapi.Context
131
+}
132
+
133
+func (t *testContextMapper) Get(req *http.Request) (kapi.Context, bool) {
134
+	return t.context, t.context != nil
135
+}
136
+func (t *testContextMapper) Update(req *http.Request, ctx kapi.Context) error {
137
+	return nil
138
+}
139
+
140
+type testInfoResolver struct {
141
+	info kapiserver.RequestInfo
142
+}
143
+
144
+func (t *testInfoResolver) GetRequestInfo(req *http.Request) (kapiserver.RequestInfo, error) {
145
+	return t.info, nil
146
+}
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"net/http"
5 5
 
6 6
 	kapi "k8s.io/kubernetes/pkg/api"
7
+	kapiserver "k8s.io/kubernetes/pkg/apiserver"
7 8
 	"k8s.io/kubernetes/pkg/auth/user"
8 9
 	"k8s.io/kubernetes/pkg/util/sets"
9 10
 )
... ...
@@ -17,6 +18,10 @@ type AuthorizationAttributeBuilder interface {
17 17
 	GetAttributes(request *http.Request) (AuthorizationAttributes, error)
18 18
 }
19 19
 
20
+type RequestInfoResolver interface {
21
+	GetRequestInfo(req *http.Request) (kapiserver.RequestInfo, error)
22
+}
23
+
20 24
 type AuthorizationAttributes interface {
21 25
 	GetVerb() string
22 26
 	GetAPIVersion() string
... ...
@@ -375,7 +375,16 @@ func newAuthorizer(policyClient policyclient.ReadOnlyPolicyClient, projectReques
375 375
 }
376 376
 
377 377
 func newAuthorizationAttributeBuilder(requestContextMapper kapi.RequestContextMapper) authorizer.AuthorizationAttributeBuilder {
378
-	authorizationAttributeBuilder := authorizer.NewAuthorizationAttributeBuilder(requestContextMapper, &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi")})
378
+	// Default API request resolver
379
+	requestInfoResolver := &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi")}
380
+	// Wrap with a resolver that detects unsafe requests and modifies verbs/resources appropriately so policy can address them separately
381
+	browserSafeRequestInfoResolver := authorizer.NewBrowserSafeRequestInfoResolver(
382
+		requestContextMapper,
383
+		sets.NewString(bootstrappolicy.AuthenticatedGroup),
384
+		requestInfoResolver,
385
+	)
386
+
387
+	authorizationAttributeBuilder := authorizer.NewAuthorizationAttributeBuilder(requestContextMapper, browserSafeRequestInfoResolver)
379 388
 	return authorizationAttributeBuilder
380 389
 }
381 390