Browse code

Merge pull request #8958 from liggitt/session-storage-1.2

CSRF updates - 1.2.0

Clayton Coleman authored on 2016/05/25 11:01:54
Showing 10 changed files
... ...
@@ -8,13 +8,22 @@
8 8
  * Controller of the openshiftConsole
9 9
  */
10 10
 angular.module('openshiftConsole')
11
-  .controller('OAuthController', function ($location, $q, RedirectLoginService, DataService, AuthService, Logger) {
11
+  .controller('OAuthController', function ($scope, $location, $q, RedirectLoginService, DataService, AuthService, Logger) {
12 12
     var authLogger = Logger.get("auth");
13 13
 
14
+    // Initialize to a no-op function.
15
+    // Needed to let the view confirm a login when the state is unverified. 
16
+    $scope.completeLogin = function(){};
17
+    $scope.cancelLogin = function() {
18
+      $location.replace();
19
+      $location.url("./");
20
+    };
21
+
14 22
     RedirectLoginService.finish()
15 23
     .then(function(data) {
16 24
       var token = data.token;
17 25
       var then = data.then;
26
+      var verified = data.verified;
18 27
       var ttl = data.ttl;
19 28
 
20 29
       // Try to fetch the user
... ...
@@ -25,21 +34,41 @@ angular.module('openshiftConsole')
25 25
       .then(function(user) {
26 26
         // Set the new user and token in the auth service
27 27
         authLogger.log("OAuthController, got user", user);
28
-        AuthService.setUser(user, token, ttl);
29 28
 
30
-        // Redirect to original destination (or default to '/')
31
-        var destination = then || './';
32
-        if (URI(destination).is('absolute')) {
33
-          authLogger.log("OAuthController, invalid absolute redirect", destination);
34
-          destination = './';
29
+        $scope.completeLogin = function() {
30
+          // Persist the user
31
+          AuthService.setUser(user, token, ttl);
32
+          
33
+          // Redirect to original destination (or default to './')
34
+          var destination = then || './';
35
+          if (URI(destination).is('absolute')) {
36
+            authLogger.log("OAuthController, invalid absolute redirect", destination);
37
+            destination = './';
38
+          }
39
+          authLogger.log("OAuthController, redirecting", destination);
40
+          $location.replace();
41
+          $location.url(destination);
42
+        };
43
+        
44
+        if (verified) {
45
+          // Automatically complete
46
+          $scope.completeLogin();
47
+        } else {
48
+          // Require the UI to prompt
49
+          $scope.confirmUser = user;
50
+          
51
+          // Additionally, give the UI info about the user being overridden
52
+          var currentUser = AuthService.UserStore().getUser();
53
+          if (currentUser && currentUser.metadata.name !== user.metadata.name) {
54
+            $scope.overriddenUser = currentUser;
55
+          }
35 56
         }
36
-        authLogger.log("OAuthController, redirecting", destination);
37
-        $location.url(destination);
38 57
       })
39 58
       .catch(function(rejection) {
40 59
         // Handle an API error response fetching the user
41 60
         var redirect = URI('error').query({error: 'user_fetch_failed'}).toString();
42 61
         authLogger.error("OAuthController, error fetching user", rejection, "redirecting", redirect);
62
+        $location.replace();
43 63
         $location.url(redirect);
44 64
       });
45 65
 
... ...
@@ -51,6 +80,7 @@ angular.module('openshiftConsole')
51 51
         error_uri: rejection.error_uri || ""
52 52
       }).toString();
53 53
       authLogger.error("OAuthController, error", rejection, "redirecting", redirect);
54
+      $location.replace();
54 55
       $location.url(redirect);
55 56
     });
56 57
 
... ...
@@ -29,6 +29,70 @@ angular.module('openshiftConsole')
29 29
   this.$get = function($location, $q, Logger) {
30 30
     var authLogger = Logger.get("auth");
31 31
 
32
+    var getRandomInts = function(length) {
33
+      var randomValues;
34
+
35
+      if (window.crypto && window.Uint32Array) {
36
+        try {
37
+          var r = new Uint32Array(length);
38
+          window.crypto.getRandomValues(r);
39
+          randomValues = [];
40
+          for (var j=0; j < length; j++) {
41
+            randomValues.push(r[j]);
42
+          }
43
+        } catch(e) {
44
+          authLogger.debug("RedirectLoginService.getRandomInts: ", e);
45
+          randomValues = null;
46
+        }
47
+      }
48
+      
49
+      if (!randomValues) {
50
+        randomValues = [];
51
+        for (var i=0; i < length; i++) {
52
+          randomValues.push(Math.floor(Math.random() * 4294967296));
53
+        }
54
+      }
55
+      
56
+      return randomValues;
57
+    };
58
+    
59
+    var nonceKey = "RedirectLoginService.nonce";
60
+    var makeState = function(then) {
61
+      var nonce = String(new Date().getTime()) + "-" + getRandomInts(8).join("");
62
+      try {
63
+        window.localStorage[nonceKey] = nonce;
64
+      } catch(e) {
65
+        authLogger.log("RedirectLoginService.makeState, localStorage error: ", e);
66
+      }
67
+      return JSON.stringify({then: then, nonce:nonce});
68
+    };
69
+    var parseState = function(state) {
70
+      var retval = {
71
+        then: null,
72
+        verified: false
73
+      };
74
+
75
+      var nonce = "";
76
+      try {
77
+        nonce = window.localStorage[nonceKey];
78
+        window.localStorage.removeItem(nonceKey);
79
+      } catch(e) {
80
+        authLogger.log("RedirectLoginService.parseState, localStorage error: ", e);
81
+      }
82
+      
83
+      try {
84
+        var data = state ? JSON.parse(state) : {};
85
+        if (data && data.nonce && nonce && data.nonce === nonce) {
86
+          retval.verified = true;
87
+          retval.then = data.then;
88
+        }
89
+      } catch(e) {
90
+        authLogger.error("RedirectLoginService.parseState, state error: ", e);
91
+      }
92
+      authLogger.error("RedirectLoginService.parseState", retval);
93
+      return retval;
94
+    };
95
+
32 96
     return {
33 97
       // Returns a promise that resolves with {user:{...}, token:'...', ttl:X}, or rejects with {error:'...'[,error_description:'...',error_uri:'...']}
34 98
       login: function() {
... ...
@@ -49,7 +113,7 @@ angular.module('openshiftConsole')
49 49
         uri.query({
50 50
           client_id: _oauth_client_id,
51 51
           response_type: 'token',
52
-          state: returnUri.toString(),
52
+          state: makeState(returnUri.toString()),
53 53
           redirect_uri: _oauth_redirect_uri
54 54
         });
55 55
         authLogger.log("RedirectLoginService.login(), redirecting", uri.toString());
... ...
@@ -59,7 +123,7 @@ angular.module('openshiftConsole')
59 59
       },
60 60
 
61 61
       // Parses oauth callback parameters from window.location
62
-      // Returns a promise that resolves with {token:'...',then:'...'}, or rejects with {error:'...'[,error_description:'...',error_uri:'...']}
62
+      // Returns a promise that resolves with {token:'...',then:'...',verified:true|false}, or rejects with {error:'...'[,error_description:'...',error_uri:'...']}
63 63
       // If no token and no error is present, resolves with {}
64 64
       // Example error codes: https://tools.ietf.org/html/rfc6749#section-5.2
65 65
       finish: function() {
... ...
@@ -71,12 +135,12 @@ angular.module('openshiftConsole')
71 71
         var fragmentParams = new URI("?" + u.fragment()).query(true);
72 72
         authLogger.log("RedirectLoginService.finish()", queryParams, fragmentParams);
73 73
 
74
-       // Error codes can come in query params or fragment params
75
-       // Handle an error response from the OAuth server
74
+        // Error codes can come in query params or fragment params
75
+        // Handle an error response from the OAuth server
76 76
         var error = queryParams.error || fragmentParams.error;
77
-       if (error) {
78
-         var error_description = queryParams.error_description || fragmentParams.error_description;
79
-         var error_uri = queryParams.error_uri || fragmentParams.error_uri;
77
+        if (error) {
78
+          var error_description = queryParams.error_description || fragmentParams.error_description;
79
+          var error_uri = queryParams.error_uri || fragmentParams.error_uri;
80 80
           authLogger.log("RedirectLoginService.finish(), error", error, error_description, error_uri);
81 81
           return $q.reject({
82 82
             error: error,
... ...
@@ -85,13 +149,16 @@ angular.module('openshiftConsole')
85 85
           });
86 86
         }
87 87
 
88
+        var stateData = parseState(fragmentParams.state);
89
+        
88 90
         // Handle an access_token response
89 91
         if (fragmentParams.access_token && (fragmentParams.token_type || "").toLowerCase() === "bearer") {
90 92
           var deferred = $q.defer();
91 93
           deferred.resolve({
92 94
             token: fragmentParams.access_token,
93 95
             ttl: fragmentParams.expires_in,
94
-            then: fragmentParams.state
96
+            then: stateData.state,
97
+            verified: stateData.verified
95 98
           });
96 99
           return deferred.promise;
97 100
         }
... ...
@@ -2,12 +2,26 @@
2 2
 <div class="wrap no-sidebar">
3 3
   <div class="middle surface-shaded">
4 4
     <div class="container surface-shaded">
5
-      <div>
5
+      <div ng-if="!confirmUser">
6 6
         <h1 style="margin-top: 10px;">Logging in&hellip;</h1>
7
-        <div>
8
-          Please wait while you are logged in...
9
-        </div>
7
+        <p>Please wait while you are logged in&hellip;</p>
10 8
       </div>
9
+      
10
+      <div ng-if="confirmUser && !overriddenUser">
11
+        <h1 style="margin-top: 10px;">Confirm Login</h1>
12
+        <p>You are being logged in as <code>{{confirmUser.metadata.name}}</code>.</p>
13
+        <button class="btn btn-lg btn-primary" type="button" ng-click="completeLogin();">Continue</button>
14
+        <button class="btn btn-lg btn-default" type="button" ng-click="cancelLogin();">Cancel</button>
15
+      </div>
16
+      
17
+      <div ng-if="confirmUser && overriddenUser">
18
+        <h1 style="margin-top: 10px;">Confirm User Change</h1>
19
+        <p>You are about to change users from <code>{{overriddenUser.metadata.name}}</code> to <code>{{confirmUser.metadata.name}}</code>.</p>
20
+        <p>If this is unexpected, click Cancel. This could be an attempt to trick you into acting as another user.</p>
21
+        <button class="btn btn-lg btn-danger" type="button" ng-click="completeLogin();">Switch Users</button>
22
+        <button class="btn btn-lg btn-primary" type="button" ng-click="cancelLogin();">Cancel</button>
23
+      </div>
24
+      
11 25
     </div>
12 26
   </div>
13 27
 </div>
... ...
@@ -2582,7 +2582,49 @@ return a && (b = a), b;
2582 2582
 }, this.OAuthRedirectURI = function(a) {
2583 2583
 return a && (c = a), c;
2584 2584
 }, this.$get = [ "$location", "$q", "Logger", function(d, e, f) {
2585
-var g = f.get("auth");
2585
+var g = f.get("auth"), h = function(a) {
2586
+var b;
2587
+if (window.crypto && window.Uint32Array) try {
2588
+var c = new Uint32Array(a);
2589
+window.crypto.getRandomValues(c), b = [];
2590
+for (var d = 0; a > d; d++) b.push(c[d]);
2591
+} catch (e) {
2592
+g.debug("RedirectLoginService.getRandomInts: ", e), b = null;
2593
+}
2594
+if (!b) {
2595
+b = [];
2596
+for (var f = 0; a > f; f++) b.push(Math.floor(4294967296 * Math.random()));
2597
+}
2598
+return b;
2599
+}, i = "RedirectLoginService.nonce", j = function(a) {
2600
+var b = String(new Date().getTime()) + "-" + h(8).join("");
2601
+try {
2602
+window.localStorage[i] = b;
2603
+} catch (c) {
2604
+g.log("RedirectLoginService.makeState, localStorage error: ", c);
2605
+}
2606
+return JSON.stringify({
2607
+then:a,
2608
+nonce:b
2609
+});
2610
+}, k = function(a) {
2611
+var b = {
2612
+then:null,
2613
+verified:!1
2614
+}, c = "";
2615
+try {
2616
+c = window.localStorage[i], window.localStorage.removeItem(i);
2617
+} catch (d) {
2618
+g.log("RedirectLoginService.parseState, localStorage error: ", d);
2619
+}
2620
+try {
2621
+var e = a ? JSON.parse(a) :{};
2622
+e && e.nonce && c && e.nonce === c && (b.verified = !0, b.then = e.then);
2623
+} catch (d) {
2624
+g.error("RedirectLoginService.parseState, state error: ", d);
2625
+}
2626
+return g.error("RedirectLoginService.parseState", b), b;
2627
+};
2586 2628
 return {
2587 2629
 login:function() {
2588 2630
 if ("" === a) return e.reject({
... ...
@@ -2601,7 +2643,7 @@ var f = e.defer(), h = new URI(b), i = new URI(d.url()).fragment("");
2601 2601
 return h.query({
2602 2602
 client_id:a,
2603 2603
 response_type:"token",
2604
-state:i.toString(),
2604
+state:j(i.toString()),
2605 2605
 redirect_uri:c
2606 2606
 }), g.log("RedirectLoginService.login(), redirecting", h.toString()), window.location.href = h.toString(), f.promise;
2607 2607
 },
... ...
@@ -2617,13 +2659,15 @@ error_description:h,
2617 2617
 error_uri:i
2618 2618
 });
2619 2619
 }
2620
+var j = k(c.state);
2620 2621
 if (c.access_token && "bearer" === (c.token_type || "").toLowerCase()) {
2621
-var j = e.defer();
2622
-return j.resolve({
2622
+var l = e.defer();
2623
+return l.resolve({
2623 2624
 token:c.access_token,
2624 2625
 ttl:c.expires_in,
2625
-then:c.state
2626
-}), j.promise;
2626
+then:j.state,
2627
+verified:j.verified
2628
+}), l.promise;
2627 2629
 }
2628 2630
 return e.reject({
2629 2631
 error:"invalid_request",
... ...
@@ -4924,35 +4968,43 @@ hideFilterWidget:!0
4924 4924
 }, c.get(a.project).then(_.spread(function(a, c) {
4925 4925
 b.project = a, b.projectContext = c;
4926 4926
 }));
4927
-} ]), angular.module("openshiftConsole").controller("OAuthController", [ "$location", "$q", "RedirectLoginService", "DataService", "AuthService", "Logger", function(a, b, c, d, e, f) {
4928
-var g = f.get("auth");
4929
-c.finish().then(function(b) {
4930
-var c = b.token, f = b.then, h = b.ttl, i = {
4927
+} ]), angular.module("openshiftConsole").controller("OAuthController", [ "$scope", "$location", "$q", "RedirectLoginService", "DataService", "AuthService", "Logger", function(a, b, c, d, e, f, g) {
4928
+var h = g.get("auth");
4929
+a.completeLogin = function() {}, a.cancelLogin = function() {
4930
+b.replace(), b.url("./");
4931
+}, d.finish().then(function(c) {
4932
+var d = c.token, g = c.then, i = c.verified, j = c.ttl, k = {
4931 4933
 errorNotification:!1,
4932 4934
 http:{
4933 4935
 auth:{
4934
-token:c,
4936
+token:d,
4935 4937
 triggerLogin:!1
4936 4938
 }
4937 4939
 }
4938 4940
 };
4939
-g.log("OAuthController, got token, fetching user", i), d.get("users", "~", {}, i).then(function(b) {
4940
-g.log("OAuthController, got user", b), e.setUser(b, c, h);
4941
-var d = f || "./";
4942
-URI(d).is("absolute") && (g.log("OAuthController, invalid absolute redirect", d), d = "./"), g.log("OAuthController, redirecting", d), a.url(d);
4943
-})["catch"](function(b) {
4941
+h.log("OAuthController, got token, fetching user", k), e.get("users", "~", {}, k).then(function(c) {
4942
+if (h.log("OAuthController, got user", c), a.completeLogin = function() {
4943
+f.setUser(c, d, j);
4944
+var a = g || "./";
4945
+URI(a).is("absolute") && (h.log("OAuthController, invalid absolute redirect", a), a = "./"), h.log("OAuthController, redirecting", a), b.replace(), b.url(a);
4946
+}, i) a.completeLogin(); else {
4947
+a.confirmUser = c;
4948
+var e = f.UserStore().getUser();
4949
+e && e.metadata.name !== c.metadata.name && (a.overriddenUser = e);
4950
+}
4951
+})["catch"](function(a) {
4944 4952
 var c = URI("error").query({
4945 4953
 error:"user_fetch_failed"
4946 4954
 }).toString();
4947
-g.error("OAuthController, error fetching user", b, "redirecting", c), a.url(c);
4955
+h.error("OAuthController, error fetching user", a, "redirecting", c), b.replace(), b.url(c);
4948 4956
 });
4949
-})["catch"](function(b) {
4957
+})["catch"](function(a) {
4950 4958
 var c = URI("error").query({
4951
-error:b.error || "",
4952
-error_description:b.error_description || "",
4953
-error_uri:b.error_uri || ""
4959
+error:a.error || "",
4960
+error_description:a.error_description || "",
4961
+error_uri:a.error_uri || ""
4954 4962
 }).toString();
4955
-g.error("OAuthController, error", b, "redirecting", c), a.url(c);
4963
+h.error("OAuthController, error", a, "redirecting", c), b.replace(), b.url(c);
4956 4964
 });
4957 4965
 } ]), angular.module("openshiftConsole").controller("ErrorController", [ "$scope", function(a) {
4958 4966
 var b = URI(window.location.href).query(!0), c = b.error;
... ...
@@ -14184,11 +14236,22 @@ var _scriptsTemplatesJs = []byte(`angular.module('openshiftConsoleTemplates', []
14184 14184
     "<div class=\"wrap no-sidebar\">\n" +
14185 14185
     "<div class=\"middle surface-shaded\">\n" +
14186 14186
     "<div class=\"container surface-shaded\">\n" +
14187
-    "<div>\n" +
14187
+    "<div ng-if=\"!confirmUser\">\n" +
14188 14188
     "<h1 style=\"margin-top: 10px\">Logging in&hellip;</h1>\n" +
14189
-    "<div>\n" +
14190
-    "Please wait while you are logged in...\n" +
14191
-    "</div>\n" +
14189
+    "<p>Please wait while you are logged in&hellip;</p>\n" +
14190
+    "</div>\n" +
14191
+    "<div ng-if=\"confirmUser && !overriddenUser\">\n" +
14192
+    "<h1 style=\"margin-top: 10px\">Confirm Login</h1>\n" +
14193
+    "<p>You are being logged in as <code>{{confirmUser.metadata.name}}</code>.</p>\n" +
14194
+    "<button class=\"btn btn-lg btn-primary\" type=\"button\" ng-click=\"completeLogin();\">Continue</button>\n" +
14195
+    "<button class=\"btn btn-lg btn-default\" type=\"button\" ng-click=\"cancelLogin();\">Cancel</button>\n" +
14196
+    "</div>\n" +
14197
+    "<div ng-if=\"confirmUser && overriddenUser\">\n" +
14198
+    "<h1 style=\"margin-top: 10px\">Confirm User Change</h1>\n" +
14199
+    "<p>You are about to change users from <code>{{overriddenUser.metadata.name}}</code> to <code>{{confirmUser.metadata.name}}</code>.</p>\n" +
14200
+    "<p>If this is unexpected, click Cancel. This could be an attempt to trick you into acting as another user.</p>\n" +
14201
+    "<button class=\"btn btn-lg btn-danger\" type=\"button\" ng-click=\"completeLogin();\">Switch Users</button>\n" +
14202
+    "<button class=\"btn btn-lg btn-primary\" type=\"button\" ng-click=\"cancelLogin();\">Cancel</button>\n" +
14192 14203
     "</div>\n" +
14193 14204
     "</div>\n" +
14194 14205
     "</div>\n" +
... ...
@@ -2,6 +2,7 @@ package paramtoken
2 2
 
3 3
 import (
4 4
 	"net/http"
5
+	"regexp"
5 6
 	"strings"
6 7
 
7 8
 	"github.com/openshift/origin/pkg/auth/authenticator"
... ...
@@ -26,6 +27,11 @@ func New(param string, auth authenticator.Token, removeParam bool) *Authenticato
26 26
 }
27 27
 
28 28
 func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, error) {
29
+	// Only accept query param auth for websocket connections
30
+	if !isWebSocketRequest(req) {
31
+		return nil, false, nil
32
+	}
33
+
29 34
 	q := req.URL.Query()
30 35
 	token := strings.TrimSpace(q.Get(a.param))
31 36
 	if token == "" {
... ...
@@ -38,3 +44,13 @@ func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool,
38 38
 	}
39 39
 	return user, ok, err
40 40
 }
41
+
42
+var (
43
+	// connectionUpgradeRegex matches any Connection header value that includes upgrade
44
+	connectionUpgradeRegex = regexp.MustCompile("(^|.*,\\s*)upgrade($|\\s*,)")
45
+)
46
+
47
+// isWebSocketRequest returns true if the incoming request contains connection upgrade headers for WebSockets.
48
+func isWebSocketRequest(req *http.Request) bool {
49
+	return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection"))) && strings.ToLower(req.Header.Get("Upgrade")) == "websocket"
50
+}
... ...
@@ -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