diff --git a/aio/develop/run-npm-on-container.sh b/aio/develop/run-npm-on-container.sh index ccb157c9a99..f9d88e8fd2f 100755 --- a/aio/develop/run-npm-on-container.sh +++ b/aio/develop/run-npm-on-container.sh @@ -26,6 +26,9 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" LOCAL_UID=$(id -u) LOCAL_GID=$(id -g) +# Set max http header size for NodeJS +NODE_OPTIONS=${NODE_OPTIONS:-"--max-http-header-size=102400"} + # K8S_DASHBOARD_NPM_CMD will be passed into container and will be used # by run-npm-command.sh on container. Then the shell sciprt will run `npm` # command with K8S_DASHBOAD_NPM_CMD. @@ -88,6 +91,7 @@ docker run \ -e K8S_DASHBOARD_DEBUG=${K8S_DASHBOARD_DEBUG} \ -e LOCAL_UID="${LOCAL_UID}" \ -e LOCAL_GID="${LOCAL_GID}" \ + -e NODE_OPTIONS="${NODE_OPTIONS}" \ -p ${K8S_DASHBOARD_PORT}:${K8S_DASHBOARD_PORT} \ -p ${K8S_DASHBOARD_DEBUG_PORT}:${K8S_DASHBOARD_DEBUG_PORT} \ ${DOCKER_RUN_OPTS} \ diff --git a/i18n/de/messages.de.xlf b/i18n/de/messages.de.xlf index eb9c1b175f3..1499d3d797d 100644 --- a/i18n/de/messages.de.xlf +++ b/i18n/de/messages.de.xlf @@ -3020,7 +3020,7 @@ Anmelden ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -3029,7 +3029,7 @@ Abmelden ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/fr/messages.fr.xlf b/i18n/fr/messages.fr.xlf index 906de78b663..e5913aea77b 100644 --- a/i18n/fr/messages.fr.xlf +++ b/i18n/fr/messages.fr.xlf @@ -3024,7 +3024,7 @@ Connexion ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -3033,7 +3033,7 @@ Déconnexion ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/ja/messages.ja.xlf b/i18n/ja/messages.ja.xlf index 18b2d969652..168076caf62 100644 --- a/i18n/ja/messages.ja.xlf +++ b/i18n/ja/messages.ja.xlf @@ -2750,7 +2750,7 @@ サインイン ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2759,7 +2759,7 @@ サインアウト ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/ko/messages.ko.xlf b/i18n/ko/messages.ko.xlf index 3604d7b9eaa..e5907512fce 100644 --- a/i18n/ko/messages.ko.xlf +++ b/i18n/ko/messages.ko.xlf @@ -2805,7 +2805,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2815,7 +2815,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/messages.xlf b/i18n/messages.xlf index 1c756f2669c..6bd31a7a77d 100644 --- a/i18n/messages.xlf +++ b/i18n/messages.xlf @@ -2597,7 +2597,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2605,7 +2605,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/zh-Hans/messages.zh-Hans.xlf b/i18n/zh-Hans/messages.zh-Hans.xlf index 55f1470f30a..cfb2ef35293 100644 --- a/i18n/zh-Hans/messages.zh-Hans.xlf +++ b/i18n/zh-Hans/messages.zh-Hans.xlf @@ -2805,7 +2805,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2815,7 +2815,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf b/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf index 1b7d45b3445..486122f0d39 100644 --- a/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf +++ b/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf @@ -2809,7 +2809,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2819,7 +2819,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/zh-Hant/messages.zh-Hant.xlf b/i18n/zh-Hant/messages.zh-Hant.xlf index 5a73c94b167..a821e669739 100644 --- a/i18n/zh-Hant/messages.zh-Hant.xlf +++ b/i18n/zh-Hant/messages.zh-Hant.xlf @@ -2809,7 +2809,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2819,7 +2819,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/package-lock.json b/package-lock.json index 99e15ade8ec..97aae4417ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4664,6 +4664,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -6710,7 +6711,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, "requires": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", @@ -11331,6 +11331,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -15456,8 +15457,7 @@ "map-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", - "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", - "dev": true + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==" }, "map-visit": { "version": "1.0.0", @@ -16207,6 +16207,11 @@ "xmldom": "^0.1.27" } }, + "ngx-webstorage": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ngx-webstorage/-/ngx-webstorage-5.0.0.tgz", + "integrity": "sha512-m96dBjUgLCpaknLRKfsJMEik393xrSX0EwO3paNSkS5d+xj2/cAendE3NwJeKY/W1D9EkKAhCvSUDX9/bAwCUg==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -18801,8 +18806,7 @@ "quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==" }, "ramda": { "version": "0.26.1", @@ -20564,6 +20568,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -24313,6 +24318,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, diff --git a/package.json b/package.json index 4bd4b6cc62a..d77d68bab12 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "angular-page-visibility": "9.0.0", "ansi-to-html": "0.6.14", "c3": "0.7.18", + "camelcase-keys": "6.2.2", "core-js": "3.6.5", "d3": "5.16.0", "file-saver": "2.0.2", @@ -126,6 +127,7 @@ "ng-in-viewport": "6.1.1", "ngx-cookie-service": "3.0.4", "ngx-filter-pipe": "2.1.2", + "ngx-webstorage": "5.0.0", "normalize.css": "8.0.1", "roboto-fontface": "0.10.0", "rxjs": "6.6.0", diff --git a/src/app/backend/auth/api/types.go b/src/app/backend/auth/api/types.go index c9167a45662..7c1be04ed97 100644 --- a/src/app/backend/auth/api/types.go +++ b/src/app/backend/auth/api/types.go @@ -29,6 +29,9 @@ const ( // Expiration time (in seconds) of tokens generated by dashboard. Default: 15 min. DefaultTokenTTL = 900 + + // Default user name for AuthInfo. This should be empty string in order not to match with user specified name. + DefaultUserName = "" ) // AuthenticationModes represents auth modes supported by dashboard. @@ -111,8 +114,8 @@ type TokenManager interface { // - Kubeconfig based - Authenticates user based on kubeconfig file. Only token/basic modes are supported within // the kubeconfig file. type Authenticator interface { - // GetAuthInfo returns filled AuthInfo structure that can be used for K8S api client creation. - GetAuthInfo() (api.AuthInfo, error) + // GetAuthInfos returns filled AuthInfo structures that can be used for K8S api client creation. + GetAuthInfos() (map[string]api.AuthInfo, error) } // LoginSpec is extracted from request coming from Dashboard frontend during login request. It contains all the @@ -127,13 +130,17 @@ type LoginSpec struct { // KubeConfig is the content of users' kubeconfig file. It will be parsed and auth data will be extracted. // Kubeconfig can not contain any paths. All data has to be provided within the file. KubeConfig string `json:"kubeconfig,omitempty"` + // Server is API server endpoint + Server string `json:"server,omitempty"` + // CertificateAuthorityData is CA data for API server endpoint + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` } // AuthResponse is returned from our backend as a response for login/refresh requests. It contains generated JWEToken // and a list of non-critical errors such as 'Failed authentication'. type AuthResponse struct { - // JWEToken is a token generated during login request that contains AuthInfo data in the payload. - JWEToken string `json:"jweToken"` + // JWETokens are tokens generated during login request that contains AuthInfo data in the payload. + JWETokens map[string]string `json:"jweTokens"` // Errors are a list of non-critical errors that happened during login request. Errors []error `json:"errors"` } @@ -141,7 +148,7 @@ type AuthResponse struct { // TokenRefreshSpec contains token that is required by token refresh operation. type TokenRefreshSpec struct { // JWEToken is a token generated during login request that contains AuthInfo data in the payload. - JWEToken string `json:"jweToken"` + JWETokens map[string]string `json:"jweTokens"` } // LoginModesResponse contains list of auth modes supported by dashboard. diff --git a/src/app/backend/auth/basic.go b/src/app/backend/auth/basic.go index 884e9abb9cd..cf6c2b94253 100644 --- a/src/app/backend/auth/basic.go +++ b/src/app/backend/auth/basic.go @@ -26,10 +26,12 @@ type basicAuthenticator struct { } // GetAuthInfo implements Authenticator interface. See Authenticator for more information. -func (self *basicAuthenticator) GetAuthInfo() (api.AuthInfo, error) { - return api.AuthInfo{ - Username: self.username, - Password: self.password, +func (self *basicAuthenticator) GetAuthInfos() (map[string]api.AuthInfo, error) { + return map[string]api.AuthInfo{ + authApi.DefaultUserName: { + Username: self.username, + Password: self.password, + }, }, nil } diff --git a/src/app/backend/auth/handler.go b/src/app/backend/auth/handler.go index ebc3cc0f680..702456c2904 100644 --- a/src/app/backend/auth/handler.go +++ b/src/app/backend/auth/handler.go @@ -16,6 +16,7 @@ package auth import ( "net/http" + "net/url" "github.com/emicklei/go-restful" @@ -64,6 +65,21 @@ func (self AuthHandler) handleLogin(request *restful.Request, response *restful. return } + cookie, err := request.Request.Cookie("server") + if err == nil && len(cookie.Value) > 0 { + server, err := url.QueryUnescape(cookie.Value) + if err == nil { + loginSpec.Server = server + } + } + cookie, err = request.Request.Cookie("certificateAuthorityData") + if err == nil && len(cookie.Value) > 0 { + ca, err := url.QueryUnescape(cookie.Value) + if err == nil { + loginSpec.CertificateAuthorityData = ca + } + } + loginResponse, err := self.manager.Login(loginSpec) if err != nil { response.AddHeader("Content-Type", "text/plain") @@ -86,16 +102,20 @@ func (self *AuthHandler) handleJWETokenRefresh(request *restful.Request, respons return } - refreshedJWEToken, err := self.manager.Refresh(tokenRefreshSpec.JWEToken) - if err != nil { - response.AddHeader("Content-Type", "text/plain") - response.WriteErrorString(errors.HandleHTTPError(err), err.Error()+"\n") - return + refreshedJWETokens := map[string]string{} + for userName, token := range tokenRefreshSpec.JWETokens { + refreshedJWEToken, err := self.manager.Refresh(token) + if err != nil { + response.AddHeader("Content-Type", "text/plain") + response.WriteErrorString(errors.HandleHTTPError(err), err.Error()+"\n") + return + } + refreshedJWETokens[userName] = refreshedJWEToken } response.WriteHeaderAndEntity(http.StatusOK, &authApi.AuthResponse{ - JWEToken: refreshedJWEToken, - Errors: make([]error, 0), + JWETokens: refreshedJWETokens, + Errors: make([]error, 0), }) } diff --git a/src/app/backend/auth/kubeconfig.go b/src/app/backend/auth/kubeconfig.go index 035d93a92d2..b90b5eabc6d 100644 --- a/src/app/backend/auth/kubeconfig.go +++ b/src/app/backend/auth/kubeconfig.go @@ -67,18 +67,18 @@ type kubeConfigAuthenticator struct { } // GetAuthInfo implements Authenticator interface. See Authenticator for more information. -func (self *kubeConfigAuthenticator) GetAuthInfo() (api.AuthInfo, error) { +func (self *kubeConfigAuthenticator) GetAuthInfos() (map[string]api.AuthInfo, error) { kubeConfig, err := self.parseKubeConfig(self.fileContent) if err != nil { - return api.AuthInfo{}, err + return map[string]api.AuthInfo{}, err } - info, err := self.getCurrentUserInfo(*kubeConfig) + infos, err := self.getUserInfos(*kubeConfig) if err != nil { - return api.AuthInfo{}, err + return map[string]api.AuthInfo{}, err } - return self.getAuthInfo(info) + return self.getAuthInfos(infos) } // Parses kubeconfig file and returns kubeConfig object. @@ -91,50 +91,63 @@ func (self *kubeConfigAuthenticator) parseKubeConfig(bytes []byte) (*kubeConfig, return kubeConfig, nil } -// Returns user info based on defined current context. In case it is not found error is returned. -func (self *kubeConfigAuthenticator) getCurrentUserInfo(config kubeConfig) (userInfo, error) { - userName := "" +// Returns user infos. User info for current context would be set at first. In case it is not found error is returned. +func (self *kubeConfigAuthenticator) getUserInfos(config kubeConfig) (map[string]userInfo, error) { + currentUserName := "" for _, context := range config.Contexts { if context.Name == config.CurrentContext { - userName = context.Context.User + currentUserName = context.Context.User } } - if len(userName) == 0 { - return userInfo{}, errors.NewInvalid("Context matching current context not found. Check if your config file is valid.") + if len(currentUserName) == 0 { + return map[string]userInfo{}, errors.NewInvalid("Context matching current context not found. Check if your config file is valid.") } + currentUser := false + userInfos := make(map[string]userInfo, len(config.Users)) for _, user := range config.Users { - if user.Name == userName { - return user.User, nil + userName := user.Name + if userName == currentUserName { + currentUser = true + userName = authApi.DefaultUserName } + userInfos[userName] = user.User } - return userInfo{}, errors.NewInvalid("User matching current context user not found. Check if your config file is valid.") + if !currentUser { + return map[string]userInfo{}, errors.NewInvalid("User matching current context user not found. Check if your config file is valid.") + } + + return userInfos, nil } // Returns auth info structure based on provided user info or error in case not enough data has been provided. -func (self *kubeConfigAuthenticator) getAuthInfo(info userInfo) (api.AuthInfo, error) { - // If "token" is empty for the current "user" entry, fallback to the value of "auth-provider.config.access-token". - if len(info.Token) == 0 { - info.Token = info.AuthProvider.Config.AccessToken - } +func (self *kubeConfigAuthenticator) getAuthInfos(infos map[string]userInfo) (map[string]api.AuthInfo, error) { + results := map[string]api.AuthInfo{} + for userName, info := range infos { + // If "token" is empty for the current "user" entry, fallback to the value of "auth-provider.config.access-token". + if len(info.Token) == 0 { + info.Token = info.AuthProvider.Config.AccessToken + } - if len(info.Token) == 0 && (len(info.Password) == 0 || len(info.Username) == 0) { - return api.AuthInfo{}, errors.NewInvalid("Not enough data to create auth info structure.") - } + if len(info.Token) == 0 && (len(info.Password) == 0 || len(info.Username) == 0) { + return map[string]api.AuthInfo{}, errors.NewInvalid("Not enough data to create auth info structure.") + } - result := api.AuthInfo{} - if self.authModes.IsEnabled(authApi.Token) { - result.Token = info.Token - } + result := api.AuthInfo{} + if self.authModes.IsEnabled(authApi.Token) { + result.Token = info.Token + } - if self.authModes.IsEnabled(authApi.Basic) { - result.Username = info.Username - result.Password = info.Password + if self.authModes.IsEnabled(authApi.Basic) { + result.Username = info.Username + result.Password = info.Password + } + results[userName] = result } - return result, nil + return results, nil } // NewBasicAuthenticator returns Authenticator based on LoginSpec. diff --git a/src/app/backend/auth/kubeconfig_test.go b/src/app/backend/auth/kubeconfig_test.go index be494361ed3..879eefe7384 100644 --- a/src/app/backend/auth/kubeconfig_test.go +++ b/src/app/backend/auth/kubeconfig_test.go @@ -75,35 +75,35 @@ func TestKubeConfigAuthenticator(t *testing.T) { info string authModes authApi.AuthenticationModes params map[string]string - expected api.AuthInfo + expected map[string]api.AuthInfo expectedErr error }{ { `If "token" is empty for the current "user" entry, the value of "auth-provider.config.access-token" is picked up.`, authModeToken, map[string]string{"accessToken": "foo", "token": ""}, - api.AuthInfo{Token: "foo"}, + map[string]api.AuthInfo{"": api.AuthInfo{Token: "foo"}}, nil, }, { `If "token" is provided for the current "user" entry, that token is picked up instead.`, authModeToken, map[string]string{"accessToken": "foo", "token": "bar"}, - api.AuthInfo{Token: "bar"}, + map[string]api.AuthInfo{"": api.AuthInfo{Token: "bar"}}, nil, }, { `If the "basic" auth mode is enabled, "username" and "password" are picked up.`, authModeBasic, map[string]string{"username": "foo", "password": "bar"}, - api.AuthInfo{Username: "foo", Password: "bar"}, + map[string]api.AuthInfo{"": api.AuthInfo{Username: "foo", Password: "bar"}}, nil, }, { `If no value for "token", "username" or "password" is provided or can be inferred, an error is returned.`, authModeBoth, map[string]string{}, - api.AuthInfo{}, + map[string]api.AuthInfo{}, errors.NewInvalid("Not enough data to create auth info structure."), }, } @@ -115,7 +115,7 @@ func TestKubeConfigAuthenticator(t *testing.T) { } kubeConfigAuthenticator := NewKubeConfigAuthenticator(&authApi.LoginSpec{KubeConfig: kb.String()}, c.authModes) - response, err := kubeConfigAuthenticator.GetAuthInfo() + response, err := kubeConfigAuthenticator.GetAuthInfos() if !areErrorsEqual(err, c.expectedErr) { t.Errorf("Test Case: %s. Expected error to be: %v, but got %v.", diff --git a/src/app/backend/auth/manager.go b/src/app/backend/auth/manager.go index 7991ca3eba7..8391a54b1e9 100644 --- a/src/app/backend/auth/manager.go +++ b/src/app/backend/auth/manager.go @@ -37,23 +37,28 @@ func (self authManager) Login(spec *authApi.LoginSpec) (*authApi.AuthResponse, e return nil, err } - authInfo, err := authenticator.GetAuthInfo() + authInfos, err := authenticator.GetAuthInfos() if err != nil { return nil, err } - err = self.healthCheck(authInfo) + // AuthInfo for current user is set in entry with "" key. + err = self.healthCheck(authInfos[authApi.DefaultUserName], spec.Server, spec.CertificateAuthorityData) nonCriticalErrors, criticalError := errors.HandleError(err) if criticalError != nil || len(nonCriticalErrors) > 0 { return &authApi.AuthResponse{Errors: nonCriticalErrors}, criticalError } - token, err := self.tokenManager.Generate(authInfo) - if err != nil { - return nil, err + tokens := map[string]string{} + for userName, authInfo := range authInfos { + token, err := self.tokenManager.Generate(authInfo) + if err != nil { + return nil, err + } + tokens[userName] = token } - return &authApi.AuthResponse{JWEToken: token, Errors: nonCriticalErrors}, nil + return &authApi.AuthResponse{JWETokens: tokens, Errors: nonCriticalErrors}, nil } // Refresh implements auth manager. See AuthManager interface for more information. @@ -89,8 +94,8 @@ func (self authManager) getAuthenticator(spec *authApi.LoginSpec) (authApi.Authe // Checks if user data extracted from provided AuthInfo structure is valid and user is correctly authenticated // by K8S apiserver. -func (self authManager) healthCheck(authInfo api.AuthInfo) error { - return self.clientManager.HasAccess(authInfo) +func (self authManager) healthCheck(authInfo api.AuthInfo, server string, caData string) error { + return self.clientManager.HasAccess(authInfo, server, caData) } // NewAuthManager creates auth manager. diff --git a/src/app/backend/auth/manager_test.go b/src/app/backend/auth/manager_test.go index 729e5bc5835..5ddbeeebdc2 100644 --- a/src/app/backend/auth/manager_test.go +++ b/src/app/backend/auth/manager_test.go @@ -82,7 +82,7 @@ func (self *fakeClientManager) CSRFKey() string { return "" } -func (self *fakeClientManager) HasAccess(authInfo api.AuthInfo) error { +func (self *fakeClientManager) HasAccess(authInfo api.AuthInfo, server string, caData string) error { return self.HasAccessError } @@ -143,7 +143,7 @@ func TestAuthManager_Login(t *testing.T) { &authApi.LoginSpec{Token: "existing-token"}, &fakeClientManager{HasAccessError: nil}, &fakeTokenManager{GeneratedToken: "generated-token"}, - &authApi.AuthResponse{JWEToken: "generated-token", Errors: make([]error, 0)}, + &authApi.AuthResponse{JWETokens: map[string]string{"": "generated-token"}, Errors: make([]error, 0)}, nil, }, { "Should propagate error on unexpected error", diff --git a/src/app/backend/auth/token.go b/src/app/backend/auth/token.go index f25bc2a632d..afc6c0b2178 100644 --- a/src/app/backend/auth/token.go +++ b/src/app/backend/auth/token.go @@ -25,9 +25,11 @@ type tokenAuthenticator struct { } // GetAuthInfo implements Authenticator interface. See Authenticator for more information. -func (self tokenAuthenticator) GetAuthInfo() (api.AuthInfo, error) { - return api.AuthInfo{ - Token: self.token, +func (self tokenAuthenticator) GetAuthInfos() (map[string]api.AuthInfo, error) { + return map[string]api.AuthInfo{ + authApi.DefaultUserName: { + Token: self.token, + }, }, nil } diff --git a/src/app/backend/client/api/types.go b/src/app/backend/client/api/types.go index 862709aa78f..31e675b1c7a 100644 --- a/src/app/backend/client/api/types.go +++ b/src/app/backend/client/api/types.go @@ -49,7 +49,7 @@ type ClientManager interface { Config(req *restful.Request) (*rest.Config, error) ClientCmdConfig(req *restful.Request) (clientcmd.ClientConfig, error) CSRFKey() string - HasAccess(authInfo api.AuthInfo) error + HasAccess(authInfo api.AuthInfo, server string, caData string) error VerberClient(req *restful.Request, config *rest.Config) (ResourceVerber, error) SetTokenManager(manager authApi.TokenManager) } diff --git a/src/app/backend/client/manager.go b/src/app/backend/client/manager.go index bbc86392327..33659f6046b 100644 --- a/src/app/backend/client/manager.go +++ b/src/app/backend/client/manager.go @@ -16,7 +16,9 @@ package client import ( "context" + "encoding/base64" "log" + "net/url" "strings" "github.com/emicklei/go-restful" @@ -208,7 +210,24 @@ func (self *clientManager) ClientCmdConfig(req *restful.Request) (clientcmd.Clie return nil, err } - cfg, err := self.buildConfigFromFlags(self.apiserverHost, self.kubeConfigPath) + server := self.apiserverHost + cookie, err := req.Request.Cookie("server") + if err == nil && len(cookie.Value) > 0 { + sv, err := url.QueryUnescape(cookie.Value) + if err == nil { + server = sv + } + } + caData := "" + cookie, err = req.Request.Cookie("certificateAuthorityData") + if err == nil && len(cookie.Value) > 0 { + ca, err := url.QueryUnescape(cookie.Value) + if err == nil { + caData = ca + } + } + + cfg, err := self.buildConfigFromFlags(server, caData, self.kubeConfigPath) if err != nil { return nil, err } @@ -223,8 +242,8 @@ func (self *clientManager) CSRFKey() string { // HasAccess configures K8S api client with provided auth info and executes a basic check against apiserver to see // if it is valid. -func (self *clientManager) HasAccess(authInfo api.AuthInfo) error { - cfg, err := self.buildConfigFromFlags(self.apiserverHost, self.kubeConfigPath) +func (self *clientManager) HasAccess(authInfo api.AuthInfo, server string, caData string) error { + cfg, err := self.buildConfigFromFlags(server, caData, self.kubeConfigPath) if err != nil { return err } @@ -295,12 +314,16 @@ func (self *clientManager) initConfig(cfg *rest.Config) { // Returns rest Config based on provided apiserverHost and kubeConfigPath flags. If both are // empty then in-cluster config will be used and if it is nil the error is returned. -func (self *clientManager) buildConfigFromFlags(apiserverHost, kubeConfigPath string) ( +func (self *clientManager) buildConfigFromFlags(apiserverHost string, caData string, kubeConfigPath string) ( *rest.Config, error) { if len(kubeConfigPath) > 0 || len(apiserverHost) > 0 { + decodedCAData := []byte{} + if len(caData) > 0 { + decodedCAData, _ = base64.StdEncoding.DecodeString(caData) + } return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeConfigPath}, - &clientcmd.ConfigOverrides{ClusterInfo: api.Cluster{Server: apiserverHost}}).ClientConfig() + &clientcmd.ConfigOverrides{ClusterInfo: api.Cluster{Server: apiserverHost, CertificateAuthorityData: decodedCAData}}).ClientConfig() } if self.isRunningInCluster() { @@ -525,7 +548,7 @@ func (self *clientManager) initInsecureClients() { } func (self *clientManager) initInsecureConfig() { - cfg, err := self.buildConfigFromFlags(self.apiserverHost, self.kubeConfigPath) + cfg, err := self.buildConfigFromFlags(self.apiserverHost, "", self.kubeConfigPath) if err != nil { panic(err) } diff --git a/src/app/backend/handler/apihandler.go b/src/app/backend/handler/apihandler.go index 343d4dc9c8e..06c505d1e81 100644 --- a/src/app/backend/handler/apihandler.go +++ b/src/app/backend/handler/apihandler.go @@ -809,7 +809,7 @@ func (apiHandler *APIHandler) handleGetStatefulSetList(request *restful.Request, dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics result, err := statefulset.GetStatefulSetList(k8sClient, namespace, dataSelect, - apiHandler.iManager.Metric().Client()) + apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -826,7 +826,7 @@ func (apiHandler *APIHandler) handleGetStatefulSetDetail(request *restful.Reques namespace := request.PathParameter("namespace") name := request.PathParameter("statefulset") - result, err := statefulset.GetStatefulSetDetail(k8sClient, apiHandler.iManager.Metric().Client(), namespace, name) + result, err := statefulset.GetStatefulSetDetail(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, name) if err != nil { errors.HandleInternalError(response, err) @@ -846,7 +846,7 @@ func (apiHandler *APIHandler) handleGetStatefulSetPods(request *restful.Request, name := request.PathParameter("statefulset") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := statefulset.GetStatefulSetPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, name, namespace) + result, err := statefulset.GetStatefulSetPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, name, namespace) if err != nil { errors.HandleInternalError(response, err) return @@ -1040,7 +1040,7 @@ func (apiHandler *APIHandler) handleGetServicePods(request *restful.Request, res name := request.PathParameter("service") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := resourceService.GetServicePods(k8sClient, apiHandler.iManager.Metric().Client(), namespace, name, dataSelect) + result, err := resourceService.GetServicePods(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, name, dataSelect) if err != nil { errors.HandleInternalError(response, err) return @@ -1057,7 +1057,7 @@ func (apiHandler *APIHandler) handleGetNodeList(request *restful.Request, respon dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := node.GetNodeList(k8sClient, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := node.GetNodeList(k8sClient, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -1075,7 +1075,7 @@ func (apiHandler *APIHandler) handleGetNodeDetail(request *restful.Request, resp name := request.PathParameter("name") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := node.GetNodeDetail(k8sClient, apiHandler.iManager.Metric().Client(), name, dataSelect) + result, err := node.GetNodeDetail(k8sClient, apiHandler.iManager.Metric().Client(request), name, dataSelect) if err != nil { errors.HandleInternalError(response, err) return @@ -1111,7 +1111,7 @@ func (apiHandler *APIHandler) handleGetNodePods(request *restful.Request, respon name := request.PathParameter("name") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := node.GetNodePods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, name) + result, err := node.GetNodePods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, name) if err != nil { errors.HandleInternalError(response, err) return @@ -1266,7 +1266,7 @@ func (apiHandler *APIHandler) handleGetReplicationControllerList(request *restfu namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := replicationcontroller.GetReplicationControllerList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := replicationcontroller.GetReplicationControllerList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -1284,7 +1284,7 @@ func (apiHandler *APIHandler) handleGetReplicaSets(request *restful.Request, res namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := replicaset.GetReplicaSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := replicaset.GetReplicaSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -1301,7 +1301,7 @@ func (apiHandler *APIHandler) handleGetReplicaSetDetail(request *restful.Request namespace := request.PathParameter("namespace") replicaSet := request.PathParameter("replicaSet") - result, err := replicaset.GetReplicaSetDetail(k8sClient, apiHandler.iManager.Metric().Client(), namespace, replicaSet) + result, err := replicaset.GetReplicaSetDetail(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, replicaSet) if err != nil { errors.HandleInternalError(response, err) @@ -1322,7 +1322,7 @@ func (apiHandler *APIHandler) handleGetReplicaSetPods(request *restful.Request, replicaSet := request.PathParameter("replicaSet") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := replicaset.GetReplicaSetPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, replicaSet, namespace) + result, err := replicaset.GetReplicaSetPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, replicaSet, namespace) if err != nil { errors.HandleInternalError(response, err) return @@ -1430,7 +1430,7 @@ func (apiHandler *APIHandler) handleGetDeployments(request *restful.Request, res namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := deployment.GetDeploymentList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := deployment.GetDeploymentList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -1522,7 +1522,7 @@ func (apiHandler *APIHandler) handleGetPods(request *restful.Request, response * namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics // download standard metrics - cpu, and memory - by default - result, err := pod.GetPodList(k8sClient, apiHandler.iManager.Metric().Client(), namespace, dataSelect) + result, err := pod.GetPodList(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, dataSelect) if err != nil { errors.HandleInternalError(response, err) return @@ -1539,7 +1539,7 @@ func (apiHandler *APIHandler) handleGetPodDetail(request *restful.Request, respo namespace := request.PathParameter("namespace") name := request.PathParameter("pod") - result, err := pod.GetPodDetail(k8sClient, apiHandler.iManager.Metric().Client(), namespace, name) + result, err := pod.GetPodDetail(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, name) if err != nil { errors.HandleInternalError(response, err) return @@ -1698,7 +1698,7 @@ func (apiHandler *APIHandler) handleGetReplicationControllerPods(request *restfu rc := request.PathParameter("replicationController") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := replicationcontroller.GetReplicationControllerPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, rc, namespace) + result, err := replicationcontroller.GetReplicationControllerPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, rc, namespace) if err != nil { errors.HandleInternalError(response, err) return @@ -1992,7 +1992,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetList(request *restful.Request, r namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := daemonset.GetDaemonSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := daemonset.GetDaemonSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -2010,7 +2010,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetDetail( namespace := request.PathParameter("namespace") name := request.PathParameter("daemonSet") - result, err := daemonset.GetDaemonSetDetail(k8sClient, apiHandler.iManager.Metric().Client(), namespace, name) + result, err := daemonset.GetDaemonSetDetail(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, name) if err != nil { errors.HandleInternalError(response, err) return @@ -2029,7 +2029,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetPods(request *restful.Request, r name := request.PathParameter("daemonSet") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := daemonset.GetDaemonSetPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, name, namespace) + result, err := daemonset.GetDaemonSetPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, name, namespace) if err != nil { errors.HandleInternalError(response, err) return @@ -2137,7 +2137,7 @@ func (apiHandler *APIHandler) handleGetJobList(request *restful.Request, respons namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := job.GetJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := job.GetJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -2173,7 +2173,7 @@ func (apiHandler *APIHandler) handleGetJobPods(request *restful.Request, respons name := request.PathParameter("name") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := job.GetJobPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, namespace, name) + result, err := job.GetJobPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, namespace, name) if err != nil { errors.HandleInternalError(response, err) return @@ -2209,7 +2209,7 @@ func (apiHandler *APIHandler) handleGetCronJobList(request *restful.Request, res namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := cronjob.GetCronJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := cronjob.GetCronJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -2249,7 +2249,7 @@ func (apiHandler *APIHandler) handleGetCronJobJobs(request *restful.Request, res } dataSelect := parser.ParseDataSelectPathParameter(request) - result, err := cronjob.GetCronJobJobs(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, namespace, name, active) + result, err := cronjob.GetCronJobJobs(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, namespace, name, active) if err != nil { errors.HandleInternalError(response, err) return diff --git a/src/app/backend/integration/metric/manager.go b/src/app/backend/integration/metric/manager.go index 2824d71ca6c..d110b800cad 100644 --- a/src/app/backend/integration/metric/manager.go +++ b/src/app/backend/integration/metric/manager.go @@ -17,8 +17,11 @@ package metric import ( "fmt" "log" + "net/url" "time" + "github.com/emicklei/go-restful" + clientapi "github.com/kubernetes/dashboard/src/app/backend/client/api" integrationapi "github.com/kubernetes/dashboard/src/app/backend/integration/api" metricapi "github.com/kubernetes/dashboard/src/app/backend/integration/metric/api" @@ -32,7 +35,7 @@ type MetricManager interface { // AddClient adds metric client to client list supported by this manager. AddClient(metricapi.MetricClient) MetricManager // Client returns active Metric client. - Client() metricapi.MetricClient + Client(*restful.Request) metricapi.MetricClient // Enable is responsible for switching active client if given integration application id // is found and related application is healthy (we can connect to it). Enable(integrationapi.IntegrationID) error @@ -64,7 +67,21 @@ func (self *metricManager) AddClient(client metricapi.MetricClient) MetricManage } // Client implements metric manager interface. See MetricManager for more information. -func (self *metricManager) Client() metricapi.MetricClient { +func (self *metricManager) Client(request *restful.Request) metricapi.MetricClient { + // Create client dynamically if `sidecarHost` is set in cookie. + cookie, err := request.Request.Cookie("sidecarHost") + if err == nil && len(cookie.Value) > 0 { + sidecarHost, err := url.QueryUnescape(cookie.Value) + if err == nil { + metricClient, err := sidecar.CreateSidecarClient(sidecarHost, nil) + if err == nil { + return metricClient + } + } + log.Printf("There was an error during sidecar client dynamic creation: %s", err.Error()) + return nil + } + return self.active } diff --git a/src/app/backend/integration/metric/manager_test.go b/src/app/backend/integration/metric/manager_test.go index 678d8523b0d..768e8feb8c4 100644 --- a/src/app/backend/integration/metric/manager_test.go +++ b/src/app/backend/integration/metric/manager_test.go @@ -15,9 +15,11 @@ package metric import ( + "net/http" "reflect" "testing" + restful "github.com/emicklei/go-restful" "github.com/kubernetes/dashboard/src/app/backend/client" "github.com/kubernetes/dashboard/src/app/backend/errors" integrationapi "github.com/kubernetes/dashboard/src/app/backend/integration/api" @@ -71,18 +73,35 @@ func TestNewMetricManager(t *testing.T) { func TestMetricManager_Client(t *testing.T) { cases := []struct { + request *restful.Request client api.MetricClient expected api.MetricClient }{ - {&FakeMetricClient{healthOk: false}, nil}, - {&FakeMetricClient{healthOk: true}, &FakeMetricClient{healthOk: true}}, + { + &restful.Request{ + Request: &http.Request{ + Header: http.Header(map[string][]string{}), + }, + }, + &FakeMetricClient{healthOk: false}, + nil, + }, + { + &restful.Request{ + Request: &http.Request{ + Header: http.Header(map[string][]string{}), + }, + }, + &FakeMetricClient{healthOk: true}, + &FakeMetricClient{healthOk: true}, + }, } for _, c := range cases { metricManager := NewMetricManager(nil) metricManager.AddClient(c.client) metricManager.Enable(fakeMetricClientID) - client := metricManager.Client() + client := metricManager.Client(c.request) if !reflect.DeepEqual(client, c.expected) { t.Errorf("Failed to get active metric client. Expected: %v, but got %v.", diff --git a/src/app/backend/plugin/config_test.go b/src/app/backend/plugin/config_test.go index fc0989a5554..8e42eb1a156 100644 --- a/src/app/backend/plugin/config_test.go +++ b/src/app/backend/plugin/config_test.go @@ -119,7 +119,7 @@ func (cm *fakeClientManager) CSRFKey() string { panic("implement me") } -func (cm *fakeClientManager) HasAccess(authInfo api.AuthInfo) error { +func (cm *fakeClientManager) HasAccess(authInfo api.AuthInfo, server string, caData string) error { panic("implement me") } diff --git a/src/app/frontend/chrome/userpanel/component.ts b/src/app/frontend/chrome/userpanel/component.ts index 7c8f093e978..79d8b511421 100644 --- a/src/app/frontend/chrome/userpanel/component.ts +++ b/src/app/frontend/chrome/userpanel/component.ts @@ -15,6 +15,7 @@ import {Component, OnInit} from '@angular/core'; import {LoginStatus} from '@api/backendapi'; import {AuthService} from '../../common/services/global/authentication'; +import {KubeconfigService} from '../../common/services/global/kubeconfig'; @Component({ selector: 'kd-user-panel', @@ -27,14 +28,18 @@ import {AuthService} from '../../common/services/global/authentication'; export class UserPanelComponent implements OnInit { loginStatus: LoginStatus; isLoginStatusInitialized = false; + currentContext: string; + contexts: string[]; - constructor(private readonly authService_: AuthService) {} + constructor(private readonly authService_: AuthService, private readonly kubeconfigService_: KubeconfigService) {} ngOnInit(): void { this.authService_.getLoginStatus().subscribe(status => { this.loginStatus = status; this.isLoginStatusInitialized = true; }); + this.contexts = this.kubeconfigService_.getContexts(); + this.currentContext = this.kubeconfigService_.getCurrentContext(); } isAuthSkipped(): boolean { @@ -49,6 +54,15 @@ export class UserPanelComponent implements OnInit { return this.loginStatus ? this.loginStatus.httpsMode : false; } + isCurrentContext(context: string): boolean { + this.currentContext = this.kubeconfigService_.getCurrentContext(); + return context === this.currentContext; + } + + switchContext(context: string): void { + this.kubeconfigService_.switchContext(context); + } + logout(): void { this.authService_.logout(); } diff --git a/src/app/frontend/chrome/userpanel/template.html b/src/app/frontend/chrome/userpanel/template.html index b26bbf14e47..39279a06167 100644 --- a/src/app/frontend/chrome/userpanel/template.html +++ b/src/app/frontend/chrome/userpanel/template.html @@ -30,6 +30,12 @@ + + -