diff --git a/assets/app/scripts/controllers/util/oauth.js b/assets/app/scripts/controllers/util/oauth.js index 5753a176ccba..cc391b4d2ce0 100644 --- a/assets/app/scripts/controllers/util/oauth.js +++ b/assets/app/scripts/controllers/util/oauth.js @@ -8,13 +8,22 @@ * Controller of the openshiftConsole */ angular.module('openshiftConsole') - .controller('OAuthController', function ($location, $q, RedirectLoginService, DataService, AuthService, Logger) { + .controller('OAuthController', function ($scope, $location, $q, RedirectLoginService, DataService, AuthService, Logger) { var authLogger = Logger.get("auth"); + // Initialize to a no-op function. + // Needed to let the view confirm a login when the state is unverified. + $scope.completeLogin = function(){}; + $scope.cancelLogin = function() { + $location.replace(); + $location.url("./"); + }; + RedirectLoginService.finish() .then(function(data) { var token = data.token; var then = data.then; + var verified = data.verified; var ttl = data.ttl; // Try to fetch the user @@ -25,21 +34,41 @@ angular.module('openshiftConsole') .then(function(user) { // Set the new user and token in the auth service authLogger.log("OAuthController, got user", user); - AuthService.setUser(user, token, ttl); - // Redirect to original destination (or default to '/') - var destination = then || './'; - if (URI(destination).is('absolute')) { - authLogger.log("OAuthController, invalid absolute redirect", destination); - destination = './'; + $scope.completeLogin = function() { + // Persist the user + AuthService.setUser(user, token, ttl); + + // Redirect to original destination (or default to './') + var destination = then || './'; + if (URI(destination).is('absolute')) { + authLogger.log("OAuthController, invalid absolute redirect", destination); + destination = './'; + } + authLogger.log("OAuthController, redirecting", destination); + $location.replace(); + $location.url(destination); + }; + + if (verified) { + // Automatically complete + $scope.completeLogin(); + } else { + // Require the UI to prompt + $scope.confirmUser = user; + + // Additionally, give the UI info about the user being overridden + var currentUser = AuthService.UserStore().getUser(); + if (currentUser && currentUser.metadata.name !== user.metadata.name) { + $scope.overriddenUser = currentUser; + } } - authLogger.log("OAuthController, redirecting", destination); - $location.url(destination); }) .catch(function(rejection) { // Handle an API error response fetching the user var redirect = URI('error').query({error: 'user_fetch_failed'}).toString(); authLogger.error("OAuthController, error fetching user", rejection, "redirecting", redirect); + $location.replace(); $location.url(redirect); }); @@ -51,6 +80,7 @@ angular.module('openshiftConsole') error_uri: rejection.error_uri || "" }).toString(); authLogger.error("OAuthController, error", rejection, "redirecting", redirect); + $location.replace(); $location.url(redirect); }); diff --git a/assets/app/scripts/services/login.js b/assets/app/scripts/services/login.js index a2e4d1100a4d..5bcbc4ca0749 100644 --- a/assets/app/scripts/services/login.js +++ b/assets/app/scripts/services/login.js @@ -29,6 +29,70 @@ angular.module('openshiftConsole') this.$get = function($location, $q, Logger) { var authLogger = Logger.get("auth"); + var getRandomInts = function(length) { + var randomValues; + + if (window.crypto && window.Uint32Array) { + try { + var r = new Uint32Array(length); + window.crypto.getRandomValues(r); + randomValues = []; + for (var j=0; j < length; j++) { + randomValues.push(r[j]); + } + } catch(e) { + authLogger.debug("RedirectLoginService.getRandomInts: ", e); + randomValues = null; + } + } + + if (!randomValues) { + randomValues = []; + for (var i=0; i < length; i++) { + randomValues.push(Math.floor(Math.random() * 4294967296)); + } + } + + return randomValues; + }; + + var nonceKey = "RedirectLoginService.nonce"; + var makeState = function(then) { + var nonce = String(new Date().getTime()) + "-" + getRandomInts(8).join(""); + try { + window.localStorage[nonceKey] = nonce; + } catch(e) { + authLogger.log("RedirectLoginService.makeState, localStorage error: ", e); + } + return JSON.stringify({then: then, nonce:nonce}); + }; + var parseState = function(state) { + var retval = { + then: null, + verified: false + }; + + var nonce = ""; + try { + nonce = window.localStorage[nonceKey]; + window.localStorage.removeItem(nonceKey); + } catch(e) { + authLogger.log("RedirectLoginService.parseState, localStorage error: ", e); + } + + try { + var data = state ? JSON.parse(state) : {}; + if (data && data.nonce && nonce && data.nonce === nonce) { + retval.verified = true; + retval.then = data.then; + } + } catch(e) { + authLogger.error("RedirectLoginService.parseState, state error: ", e); + } + authLogger.error("RedirectLoginService.parseState", retval); + return retval; + }; + return { // Returns a promise that resolves with {user:{...}, token:'...', ttl:X}, or rejects with {error:'...'[,error_description:'...',error_uri:'...']} login: function() { @@ -49,7 +113,7 @@ angular.module('openshiftConsole') uri.query({ client_id: _oauth_client_id, response_type: 'token', - state: returnUri.toString(), + state: makeState(returnUri.toString()), redirect_uri: _oauth_redirect_uri }); authLogger.log("RedirectLoginService.login(), redirecting", uri.toString()); @@ -59,7 +123,7 @@ angular.module('openshiftConsole') }, // Parses oauth callback parameters from window.location - // Returns a promise that resolves with {token:'...',then:'...'}, or rejects with {error:'...'[,error_description:'...',error_uri:'...']} + // Returns a promise that resolves with {token:'...',then:'...',verified:true|false}, or rejects with {error:'...'[,error_description:'...',error_uri:'...']} // If no token and no error is present, resolves with {} // Example error codes: https://tools.ietf.org/html/rfc6749#section-5.2 finish: function() { @@ -71,12 +135,12 @@ angular.module('openshiftConsole') var fragmentParams = new URI("?" + u.fragment()).query(true); authLogger.log("RedirectLoginService.finish()", queryParams, fragmentParams); - // Error codes can come in query params or fragment params - // Handle an error response from the OAuth server + // Error codes can come in query params or fragment params + // Handle an error response from the OAuth server var error = queryParams.error || fragmentParams.error; - if (error) { - var error_description = queryParams.error_description || fragmentParams.error_description; - var error_uri = queryParams.error_uri || fragmentParams.error_uri; + if (error) { + var error_description = queryParams.error_description || fragmentParams.error_description; + var error_uri = queryParams.error_uri || fragmentParams.error_uri; authLogger.log("RedirectLoginService.finish(), error", error, error_description, error_uri); return $q.reject({ error: error, @@ -85,13 +149,16 @@ angular.module('openshiftConsole') }); } + var stateData = parseState(fragmentParams.state); + // Handle an access_token response if (fragmentParams.access_token && (fragmentParams.token_type || "").toLowerCase() === "bearer") { var deferred = $q.defer(); deferred.resolve({ token: fragmentParams.access_token, ttl: fragmentParams.expires_in, - then: fragmentParams.state + then: stateData.state, + verified: stateData.verified }); return deferred.promise; } diff --git a/assets/app/views/util/oauth.html b/assets/app/views/util/oauth.html index 2856de7ff0f9..10758fe236cf 100644 --- a/assets/app/views/util/oauth.html +++ b/assets/app/views/util/oauth.html @@ -2,12 +2,26 @@
-
+

Logging in…

-
- Please wait while you are logged in... -
+

Please wait while you are logged in…

+ +
+

Confirm Login

+

You are being logged in as {{confirmUser.metadata.name}}.

+ + +
+ +
+

Confirm User Change

+

You are about to change users from {{overriddenUser.metadata.name}} to {{confirmUser.metadata.name}}.

+

If this is unexpected, click Cancel. This could be an attempt to trick you into acting as another user.

+ + +
+
diff --git a/pkg/assets/bindata.go b/pkg/assets/bindata.go index 982fc612d59c..3c0e9171717d 100644 --- a/pkg/assets/bindata.go +++ b/pkg/assets/bindata.go @@ -2582,7 +2582,49 @@ return a && (b = a), b; }, this.OAuthRedirectURI = function(a) { return a && (c = a), c; }, this.$get = [ "$location", "$q", "Logger", function(d, e, f) { -var g = f.get("auth"); +var g = f.get("auth"), h = function(a) { +var b; +if (window.crypto && window.Uint32Array) try { +var c = new Uint32Array(a); +window.crypto.getRandomValues(c), b = []; +for (var d = 0; a > d; d++) b.push(c[d]); +} catch (e) { +g.debug("RedirectLoginService.getRandomInts: ", e), b = null; +} +if (!b) { +b = []; +for (var f = 0; a > f; f++) b.push(Math.floor(4294967296 * Math.random())); +} +return b; +}, i = "RedirectLoginService.nonce", j = function(a) { +var b = String(new Date().getTime()) + "-" + h(8).join(""); +try { +window.localStorage[i] = b; +} catch (c) { +g.log("RedirectLoginService.makeState, localStorage error: ", c); +} +return JSON.stringify({ +then:a, +nonce:b +}); +}, k = function(a) { +var b = { +then:null, +verified:!1 +}, c = ""; +try { +c = window.localStorage[i], window.localStorage.removeItem(i); +} catch (d) { +g.log("RedirectLoginService.parseState, localStorage error: ", d); +} +try { +var e = a ? JSON.parse(a) :{}; +e && e.nonce && c && e.nonce === c && (b.verified = !0, b.then = e.then); +} catch (d) { +g.error("RedirectLoginService.parseState, state error: ", d); +} +return g.error("RedirectLoginService.parseState", b), b; +}; return { login:function() { if ("" === a) return e.reject({ @@ -2601,7 +2643,7 @@ var f = e.defer(), h = new URI(b), i = new URI(d.url()).fragment(""); return h.query({ client_id:a, response_type:"token", -state:i.toString(), +state:j(i.toString()), redirect_uri:c }), g.log("RedirectLoginService.login(), redirecting", h.toString()), window.location.href = h.toString(), f.promise; }, @@ -2617,13 +2659,15 @@ error_description:h, error_uri:i }); } +var j = k(c.state); if (c.access_token && "bearer" === (c.token_type || "").toLowerCase()) { -var j = e.defer(); -return j.resolve({ +var l = e.defer(); +return l.resolve({ token:c.access_token, ttl:c.expires_in, -then:c.state -}), j.promise; +then:j.state, +verified:j.verified +}), l.promise; } return e.reject({ error:"invalid_request", @@ -4924,35 +4968,43 @@ hideFilterWidget:!0 }, c.get(a.project).then(_.spread(function(a, c) { b.project = a, b.projectContext = c; })); -} ]), angular.module("openshiftConsole").controller("OAuthController", [ "$location", "$q", "RedirectLoginService", "DataService", "AuthService", "Logger", function(a, b, c, d, e, f) { -var g = f.get("auth"); -c.finish().then(function(b) { -var c = b.token, f = b.then, h = b.ttl, i = { +} ]), angular.module("openshiftConsole").controller("OAuthController", [ "$scope", "$location", "$q", "RedirectLoginService", "DataService", "AuthService", "Logger", function(a, b, c, d, e, f, g) { +var h = g.get("auth"); +a.completeLogin = function() {}, a.cancelLogin = function() { +b.replace(), b.url("./"); +}, d.finish().then(function(c) { +var d = c.token, g = c.then, i = c.verified, j = c.ttl, k = { errorNotification:!1, http:{ auth:{ -token:c, +token:d, triggerLogin:!1 } } }; -g.log("OAuthController, got token, fetching user", i), d.get("users", "~", {}, i).then(function(b) { -g.log("OAuthController, got user", b), e.setUser(b, c, h); -var d = f || "./"; -URI(d).is("absolute") && (g.log("OAuthController, invalid absolute redirect", d), d = "./"), g.log("OAuthController, redirecting", d), a.url(d); -})["catch"](function(b) { +h.log("OAuthController, got token, fetching user", k), e.get("users", "~", {}, k).then(function(c) { +if (h.log("OAuthController, got user", c), a.completeLogin = function() { +f.setUser(c, d, j); +var a = g || "./"; +URI(a).is("absolute") && (h.log("OAuthController, invalid absolute redirect", a), a = "./"), h.log("OAuthController, redirecting", a), b.replace(), b.url(a); +}, i) a.completeLogin(); else { +a.confirmUser = c; +var e = f.UserStore().getUser(); +e && e.metadata.name !== c.metadata.name && (a.overriddenUser = e); +} +})["catch"](function(a) { var c = URI("error").query({ error:"user_fetch_failed" }).toString(); -g.error("OAuthController, error fetching user", b, "redirecting", c), a.url(c); +h.error("OAuthController, error fetching user", a, "redirecting", c), b.replace(), b.url(c); }); -})["catch"](function(b) { +})["catch"](function(a) { var c = URI("error").query({ -error:b.error || "", -error_description:b.error_description || "", -error_uri:b.error_uri || "" +error:a.error || "", +error_description:a.error_description || "", +error_uri:a.error_uri || "" }).toString(); -g.error("OAuthController, error", b, "redirecting", c), a.url(c); +h.error("OAuthController, error", a, "redirecting", c), b.replace(), b.url(c); }); } ]), angular.module("openshiftConsole").controller("ErrorController", [ "$scope", function(a) { var b = URI(window.location.href).query(!0), c = b.error; @@ -14184,11 +14236,22 @@ var _scriptsTemplatesJs = []byte(`angular.module('openshiftConsoleTemplates', [] "
\n" + "
\n" + "
\n" + - "
\n" + + "
\n" + "

Logging in…

\n" + - "
\n" + - "Please wait while you are logged in...\n" + - "
\n" + + "

Please wait while you are logged in…

\n" + + "
\n" + + "
\n" + + "

Confirm Login

\n" + + "

You are being logged in as {{confirmUser.metadata.name}}.

\n" + + "\n" + + "\n" + + "
\n" + + "
\n" + + "

Confirm User Change

\n" + + "

You are about to change users from {{overriddenUser.metadata.name}} to {{confirmUser.metadata.name}}.

\n" + + "

If this is unexpected, click Cancel. This could be an attempt to trick you into acting as another user.

\n" + + "\n" + + "\n" + "
\n" + "
\n" + "
\n" + diff --git a/pkg/auth/authenticator/request/paramtoken/paramtoken.go b/pkg/auth/authenticator/request/paramtoken/paramtoken.go index 0c07be749d1a..53cf1595685d 100644 --- a/pkg/auth/authenticator/request/paramtoken/paramtoken.go +++ b/pkg/auth/authenticator/request/paramtoken/paramtoken.go @@ -2,6 +2,7 @@ package paramtoken import ( "net/http" + "regexp" "strings" "github.com/openshift/origin/pkg/auth/authenticator" @@ -26,6 +27,11 @@ func New(param string, auth authenticator.Token, removeParam bool) *Authenticato } func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + // Only accept query param auth for websocket connections + if !isWebSocketRequest(req) { + return nil, false, nil + } + q := req.URL.Query() token := strings.TrimSpace(q.Get(a.param)) if token == "" { @@ -38,3 +44,13 @@ func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, } return user, ok, err } + +var ( + // connectionUpgradeRegex matches any Connection header value that includes upgrade + connectionUpgradeRegex = regexp.MustCompile("(^|.*,\\s*)upgrade($|\\s*,)") +) + +// isWebSocketRequest returns true if the incoming request contains connection upgrade headers for WebSockets. +func isWebSocketRequest(req *http.Request) bool { + return connectionUpgradeRegex.MatchString(strings.ToLower(req.Header.Get("Connection"))) && strings.ToLower(req.Header.Get("Upgrade")) == "websocket" +} diff --git a/pkg/authorization/authorizer/attributes_builder.go b/pkg/authorization/authorizer/attributes_builder.go index 25d70817fa6e..35e4181139ca 100644 --- a/pkg/authorization/authorizer/attributes_builder.go +++ b/pkg/authorization/authorizer/attributes_builder.go @@ -5,15 +5,14 @@ import ( "strings" kapi "k8s.io/kubernetes/pkg/api" - kapiserver "k8s.io/kubernetes/pkg/apiserver" ) type openshiftAuthorizationAttributeBuilder struct { contextMapper kapi.RequestContextMapper - infoResolver *kapiserver.RequestInfoResolver + infoResolver RequestInfoResolver } -func NewAuthorizationAttributeBuilder(contextMapper kapi.RequestContextMapper, infoResolver *kapiserver.RequestInfoResolver) AuthorizationAttributeBuilder { +func NewAuthorizationAttributeBuilder(contextMapper kapi.RequestContextMapper, infoResolver RequestInfoResolver) AuthorizationAttributeBuilder { return &openshiftAuthorizationAttributeBuilder{contextMapper, infoResolver} } diff --git a/pkg/authorization/authorizer/browser_safe_request_info_resolver.go b/pkg/authorization/authorizer/browser_safe_request_info_resolver.go new file mode 100644 index 000000000000..07487f68969b --- /dev/null +++ b/pkg/authorization/authorizer/browser_safe_request_info_resolver.go @@ -0,0 +1,75 @@ +package authorizer + +import ( + "net/http" + + kapi "k8s.io/kubernetes/pkg/api" + kapiserver "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/util/sets" +) + +type browserSafeRequestInfoResolver struct { + // infoResolver is used to determine info for the request + infoResolver RequestInfoResolver + + // contextMapper is used to look up the context corresponding to a request + // to obtain the user associated with the request + contextMapper kapi.RequestContextMapper + + // list of groups, any of which indicate the request is authenticated + authenticatedGroups sets.String +} + +func NewBrowserSafeRequestInfoResolver(contextMapper kapi.RequestContextMapper, authenticatedGroups sets.String, infoResolver RequestInfoResolver) RequestInfoResolver { + return &browserSafeRequestInfoResolver{ + contextMapper: contextMapper, + authenticatedGroups: authenticatedGroups, + infoResolver: infoResolver, + } +} + +func (a *browserSafeRequestInfoResolver) GetRequestInfo(req *http.Request) (kapiserver.RequestInfo, error) { + requestInfo, err := a.infoResolver.GetRequestInfo(req) + if err != nil { + return requestInfo, err + } + + if !requestInfo.IsResourceRequest { + return requestInfo, nil + } + + isProxyVerb := requestInfo.Verb == "proxy" + isProxySubresource := requestInfo.Subresource == "proxy" + + if !isProxyVerb && !isProxySubresource { + // Requests to non-proxy resources don't expose HTML or HTTP-handling user content to browsers + return requestInfo, nil + } + + if len(req.Header.Get("X-CSRF-Token")) > 0 { + // Browsers cannot set custom headers on direct requests + return requestInfo, nil + } + + if ctx, hasContext := a.contextMapper.Get(req); hasContext { + user, hasUser := kapi.UserFrom(ctx) + if hasUser && a.authenticatedGroups.HasAny(user.GetGroups()...) { + // An authenticated request indicates this isn't a browser page load. + // Browsers cannot make direct authenticated requests. + // This depends on the API not enabling basic or cookie-based auth. + return requestInfo, nil + } + + } + + // TODO: compare request.Host to a list of hosts allowed for the requestInfo.Namespace (e.g. .proxy.example.com) + + if isProxyVerb { + requestInfo.Verb = "unsafeproxy" + } + if isProxySubresource { + requestInfo.Subresource = "unsafeproxy" + } + + return requestInfo, nil +} diff --git a/pkg/authorization/authorizer/browser_safe_request_info_resolver_test.go b/pkg/authorization/authorizer/browser_safe_request_info_resolver_test.go new file mode 100644 index 000000000000..0cbce736fb0d --- /dev/null +++ b/pkg/authorization/authorizer/browser_safe_request_info_resolver_test.go @@ -0,0 +1,147 @@ +package authorizer + +import ( + "net/http" + "testing" + + kapi "k8s.io/kubernetes/pkg/api" + kapiserver "k8s.io/kubernetes/pkg/apiserver" + "k8s.io/kubernetes/pkg/auth/user" + "k8s.io/kubernetes/pkg/util/sets" +) + +func TestUpstreamInfoResolver(t *testing.T) { + subresourceRequest, _ := http.NewRequest("GET", "/api/v1/namespaces/myns/pods/mypod/proxy", nil) + proxyRequest, _ := http.NewRequest("GET", "/api/v1/proxy/nodes/mynode", nil) + + testcases := map[string]struct { + Request *http.Request + ExpectedVerb string + ExpectedSubresource string + }{ + "unsafe proxy subresource": { + Request: subresourceRequest, + ExpectedVerb: "get", + ExpectedSubresource: "proxy", // should be "unsafeproxy" or similar once check moves upstream + }, + "unsafe proxy verb": { + Request: proxyRequest, + ExpectedVerb: "proxy", // should be "unsafeproxy" or similar once check moves upstream + }, + } + + for k, tc := range testcases { + resolver := &kapiserver.RequestInfoResolver{ + APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), + GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi"), + } + + info, err := resolver.GetRequestInfo(tc.Request) + if err != nil { + t.Errorf("%s: unexpected error: %v", k, err) + continue + } + + if info.Verb != tc.ExpectedVerb { + 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) + } + if info.Subresource != tc.ExpectedSubresource { + 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) + } + } +} + +func TestBrowserSafeRequestInfoResolver(t *testing.T) { + testcases := map[string]struct { + RequestInfo kapiserver.RequestInfo + Context kapi.Context + Host string + Headers http.Header + + ExpectedVerb string + ExpectedSubresource string + }{ + "non-resource": { + RequestInfo: kapiserver.RequestInfo{IsResourceRequest: false, Verb: "GET"}, + ExpectedVerb: "GET", + }, + + "non-proxy": { + RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "logs"}, + ExpectedVerb: "get", + ExpectedSubresource: "logs", + }, + + "unsafe proxy subresource": { + RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "proxy"}, + ExpectedVerb: "get", + ExpectedSubresource: "unsafeproxy", + }, + "unsafe proxy verb": { + RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "proxy", Resource: "nodes"}, + ExpectedVerb: "unsafeproxy", + }, + "unsafe proxy verb anonymous": { + Context: kapi.WithUser(kapi.NewContext(), &user.DefaultInfo{Name: "system:anonymous", Groups: []string{"system:unauthenticated"}}), + RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "proxy", Resource: "nodes"}, + ExpectedVerb: "unsafeproxy", + }, + + "proxy subresource authenticated": { + Context: kapi.WithUser(kapi.NewContext(), &user.DefaultInfo{Name: "bob", Groups: []string{"system:authenticated"}}), + RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "proxy"}, + ExpectedVerb: "get", + ExpectedSubresource: "proxy", + }, + "proxy subresource custom header": { + RequestInfo: kapiserver.RequestInfo{IsResourceRequest: true, Verb: "get", Resource: "pods", Subresource: "proxy"}, + Headers: http.Header{"X-Csrf-Token": []string{"1"}}, + ExpectedVerb: "get", + ExpectedSubresource: "proxy", + }, + } + + for k, tc := range testcases { + resolver := NewBrowserSafeRequestInfoResolver( + &testContextMapper{tc.Context}, + sets.NewString("system:authenticated"), + &testInfoResolver{tc.RequestInfo}, + ) + + req, _ := http.NewRequest("GET", "/", nil) + req.Host = tc.Host + req.Header = tc.Headers + + info, err := resolver.GetRequestInfo(req) + if err != nil { + t.Errorf("%s: unexpected error: %v", k, err) + continue + } + + if info.Verb != tc.ExpectedVerb { + t.Errorf("%s: expected verb %s, got %s", k, tc.ExpectedVerb, info.Verb) + } + if info.Subresource != tc.ExpectedSubresource { + t.Errorf("%s: expected verb %s, got %s", k, tc.ExpectedSubresource, info.Subresource) + } + } +} + +type testContextMapper struct { + context kapi.Context +} + +func (t *testContextMapper) Get(req *http.Request) (kapi.Context, bool) { + return t.context, t.context != nil +} +func (t *testContextMapper) Update(req *http.Request, ctx kapi.Context) error { + return nil +} + +type testInfoResolver struct { + info kapiserver.RequestInfo +} + +func (t *testInfoResolver) GetRequestInfo(req *http.Request) (kapiserver.RequestInfo, error) { + return t.info, nil +} diff --git a/pkg/authorization/authorizer/interfaces.go b/pkg/authorization/authorizer/interfaces.go index 0c430749eb5d..81b64eb6197f 100644 --- a/pkg/authorization/authorizer/interfaces.go +++ b/pkg/authorization/authorizer/interfaces.go @@ -4,6 +4,7 @@ import ( "net/http" kapi "k8s.io/kubernetes/pkg/api" + kapiserver "k8s.io/kubernetes/pkg/apiserver" "k8s.io/kubernetes/pkg/auth/user" "k8s.io/kubernetes/pkg/util/sets" ) @@ -17,6 +18,10 @@ type AuthorizationAttributeBuilder interface { GetAttributes(request *http.Request) (AuthorizationAttributes, error) } +type RequestInfoResolver interface { + GetRequestInfo(req *http.Request) (kapiserver.RequestInfo, error) +} + type AuthorizationAttributes interface { GetVerb() string GetAPIVersion() string diff --git a/pkg/cmd/server/origin/master_config.go b/pkg/cmd/server/origin/master_config.go index 1e0b4fb3e0ca..61dbb1048f89 100644 --- a/pkg/cmd/server/origin/master_config.go +++ b/pkg/cmd/server/origin/master_config.go @@ -375,7 +375,16 @@ func newAuthorizer(policyClient policyclient.ReadOnlyPolicyClient, projectReques } func newAuthorizationAttributeBuilder(requestContextMapper kapi.RequestContextMapper) authorizer.AuthorizationAttributeBuilder { - authorizationAttributeBuilder := authorizer.NewAuthorizationAttributeBuilder(requestContextMapper, &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi")}) + // Default API request resolver + requestInfoResolver := &apiserver.RequestInfoResolver{APIPrefixes: sets.NewString("api", "osapi", "oapi", "apis"), GrouplessAPIPrefixes: sets.NewString("api", "osapi", "oapi")} + // Wrap with a resolver that detects unsafe requests and modifies verbs/resources appropriately so policy can address them separately + browserSafeRequestInfoResolver := authorizer.NewBrowserSafeRequestInfoResolver( + requestContextMapper, + sets.NewString(bootstrappolicy.AuthenticatedGroup), + requestInfoResolver, + ) + + authorizationAttributeBuilder := authorizer.NewAuthorizationAttributeBuilder(requestContextMapper, browserSafeRequestInfoResolver) return authorizationAttributeBuilder }