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