From 71ad21598bb11f8d9c793ef2434abc78de22525f Mon Sep 17 00:00:00 2001 From: Hui Date: Wed, 30 Jun 2021 16:49:48 +1200 Subject: [PATCH 01/15] remove expiry time copy logic (#5259) --- api/http/handler/auth/authenticate.go | 9 --------- api/http/handler/auth/authenticate_oauth.go | 17 ++++++++--------- api/oauth/oauth.go | 9 ++++----- api/portainer.go | 2 +- 4 files changed, 13 insertions(+), 24 deletions(-) diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index 8f2575cf4..ae2201855 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -5,7 +5,6 @@ import ( "log" "net/http" "strings" - "time" "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" @@ -134,14 +133,6 @@ func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) return handler.persistAndWriteToken(w, composeTokenData(user)) } -func (handler *Handler) writeTokenForOAuth(w http.ResponseWriter, user *portainer.User, expiryTime *time.Time) *httperror.HandlerError { - token, err := handler.JWTService.GenerateTokenForOAuth(composeTokenData(user), expiryTime) - if err != nil { - return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to generate JWT token", Err: err} - } - return response.JSON(w, &authenticateResponse{JWT: token}) -} - func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError { token, err := handler.JWTService.GenerateToken(tokenData) if err != nil { diff --git a/api/http/handler/auth/authenticate_oauth.go b/api/http/handler/auth/authenticate_oauth.go index e5b7e7885..5fd2075d0 100644 --- a/api/http/handler/auth/authenticate_oauth.go +++ b/api/http/handler/auth/authenticate_oauth.go @@ -4,7 +4,6 @@ import ( "errors" "log" "net/http" - "time" "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" @@ -26,21 +25,21 @@ func (payload *oauthPayload) Validate(r *http.Request) error { return nil } -func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, *time.Time, error) { +func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) { if code == "" { - return "", nil, errors.New("Invalid OAuth authorization code") + return "", errors.New("Invalid OAuth authorization code") } if settings == nil { - return "", nil, errors.New("Invalid OAuth configuration") + return "", errors.New("Invalid OAuth configuration") } - username, expiryTime, err := handler.OAuthService.Authenticate(code, settings) + username, err := handler.OAuthService.Authenticate(code, settings) if err != nil { - return "", nil, err + return "", err } - return username, expiryTime, nil + return username, nil } // @id ValidateOAuth @@ -70,7 +69,7 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{StatusCode: http.StatusForbidden, Message: "OAuth authentication is not enabled", Err: errors.New("OAuth authentication is not enabled")} } - username, expiryTime, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings) + username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings) if err != nil { log.Printf("[DEBUG] - OAuth authentication error: %s", err) return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to authenticate through OAuth", Err: httperrors.ErrUnauthorized} @@ -111,5 +110,5 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h } - return handler.writeTokenForOAuth(w, user, expiryTime) + return handler.writeToken(w, user) } diff --git a/api/oauth/oauth.go b/api/oauth/oauth.go index aff93bcfb..ef039d056 100644 --- a/api/oauth/oauth.go +++ b/api/oauth/oauth.go @@ -9,7 +9,6 @@ import ( "mime" "net/http" "net/url" - "time" "golang.org/x/oauth2" @@ -27,18 +26,18 @@ func NewService() *Service { // Authenticate takes an access code and exchanges it for an access token from portainer OAuthSettings token endpoint. // On success, it will then return the username and token expiry time associated to authenticated user by fetching this information // from the resource server and matching it with the user identifier setting. -func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, *time.Time, error) { +func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) { token, err := getOAuthToken(code, configuration) if err != nil { log.Printf("[DEBUG] - Failed retrieving access token: %v", err) - return "", nil, err + return "", err } username, err := getUsername(token.AccessToken, configuration) if err != nil { log.Printf("[DEBUG] - Failed retrieving oauth user name: %v", err) - return "", nil, err + return "", err } - return username, &token.Expiry, nil + return username, nil } func getOAuthToken(code string, configuration *portainer.OAuthSettings) (*oauth2.Token, error) { diff --git a/api/portainer.go b/api/portainer.go index ab61f37bf..041edd228 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1189,7 +1189,7 @@ type ( // OAuthService represents a service used to authenticate users using OAuth OAuthService interface { - Authenticate(code string, configuration *OAuthSettings) (string, *time.Time, error) + Authenticate(code string, configuration *OAuthSettings) (string, error) } // RegistryService represents a service for managing registry data From a8265a44d0bd1cdbb6d88ad016af8b160ff01667 Mon Sep 17 00:00:00 2001 From: cong meng Date: Wed, 7 Jul 2021 12:52:37 +1200 Subject: [PATCH 02/15] fix EE-1078 Too strict form validation for docker environment variables (#5278) Co-authored-by: Simon Meng --- .../environment-variables-simple-mode-item.controller.js | 8 -------- .../environment-variables-simple-mode-item.html | 2 -- app/portainer/helpers/env-vars.js | 7 +++---- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.controller.js b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.controller.js index 2138e4f57..30d61e8d3 100644 --- a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.controller.js +++ b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.controller.js @@ -1,12 +1,4 @@ -import { KEY_REGEX, VALUE_REGEX } from '@/portainer/helpers/env-vars'; - class EnvironmentVariablesSimpleModeItemController { - /* @ngInject */ - constructor() { - this.KEY_REGEX = KEY_REGEX; - this.VALUE_REGEX = VALUE_REGEX; - } - onChangeName(name) { const fieldIsInvalid = typeof name === 'undefined'; if (fieldIsInvalid) { diff --git a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.html b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.html index c53af0699..27ace7bc2 100644 --- a/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.html +++ b/app/portainer/components/environment-variables-panel/environment-variables-simple-mode/environment-variables-simple-mode-item/environment-variables-simple-mode-item.html @@ -9,7 +9,6 @@ placeholder="e.g. FOO" ng-model="$ctrl.variable.name" ng-disabled="$ctrl.variable.added" - ng-pattern="$ctrl.KEY_REGEX" ng-change="$ctrl.onChangeName($ctrl.variable.name)" required /> @@ -36,7 +35,6 @@ ng-model="$ctrl.variable.value" placeholder="e.g. bar" ng-trim="false" - ng-pattern="$ctrl.VALUE_REGEX" name="value" ng-change="$ctrl.onChangeValue($ctrl.variable.value)" /> diff --git a/app/portainer/helpers/env-vars.js b/app/portainer/helpers/env-vars.js index 972fc5ebd..c55c448ea 100644 --- a/app/portainer/helpers/env-vars.js +++ b/app/portainer/helpers/env-vars.js @@ -1,7 +1,6 @@ import _ from 'lodash-es'; -export const KEY_REGEX = /[a-zA-Z]([-_a-zA-Z0-9]*[a-zA-Z0-9])?/.source; - +export const KEY_REGEX = /(.+)/.source; export const VALUE_REGEX = /(.*)?/.source; const KEY_VALUE_REGEX = new RegExp(`^(${KEY_REGEX})\\s*=(${VALUE_REGEX})$`); @@ -16,7 +15,7 @@ export function parseDotEnvFile(src) { return parseArrayOfStrings( _.compact(src.split(NEWLINES_REGEX)) .map((v) => v.trim()) - .filter((v) => !v.startsWith('#')) + .filter((v) => !v.startsWith('#') && v !== '') ); } @@ -40,7 +39,7 @@ export function parseArrayOfStrings(array) { const parsedKeyValArr = variableString.trim().match(KEY_VALUE_REGEX); if (parsedKeyValArr != null && parsedKeyValArr.length > 4) { - return { name: parsedKeyValArr[1], value: parsedKeyValArr[3] || '' }; + return { name: parsedKeyValArr[1].trim(), value: parsedKeyValArr[3].trim() || '' }; } }) ); From 2a3c8079787daabb714ddfc9813e4df01d8e26ac Mon Sep 17 00:00:00 2001 From: cong meng Date: Wed, 7 Jul 2021 14:08:20 +1200 Subject: [PATCH 03/15] fix(ingress): EE-1049 Ingress config is lost when deleting an application deployed with ingress (#5264) Co-authored-by: Simon Meng --- app/kubernetes/ingress/converter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/kubernetes/ingress/converter.js b/app/kubernetes/ingress/converter.js index 5651e29da..284958b5b 100644 --- a/app/kubernetes/ingress/converter.js +++ b/app/kubernetes/ingress/converter.js @@ -171,7 +171,7 @@ export class KubernetesIngressConverter { res.spec.rules = []; _.forEach(data.Hosts, (host) => { if (!host.NeedsDeletion) { - res.spec.rules.push({ host: host.Host }); + res.spec.rules.push({ host: host.Host || host }); } }); } else { From e831fa4a038afb18e0a74f33d73e2c3b7200eb58 Mon Sep 17 00:00:00 2001 From: yi-portainer Date: Wed, 7 Jul 2021 17:20:18 +1200 Subject: [PATCH 04/15] * update versions to 2.6.1 --- api/http/handler/handler.go | 2 +- api/portainer.go | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 2942c3a17..1cf65d947 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -67,7 +67,7 @@ type Handler struct { } // @title PortainerCE API -// @version 2.1.1 +// @version 2.6.1 // @description.markdown api-description.md // @termsOfService diff --git a/api/portainer.go b/api/portainer.go index 041edd228..cf3e664e6 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1341,7 +1341,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "2.6.0" + APIVersion = "2.6.1" // DBVersion is the version number of the Portainer database DBVersion = 30 // ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax diff --git a/package.json b/package.json index 03731fd25..52879ab37 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "2.6.0", + "version": "2.6.1", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git" From 9cd64664cc26f15e760197bf3a98388ff500addb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Busso?= Date: Mon, 5 Jul 2021 11:10:10 +1200 Subject: [PATCH 05/15] fix download logs (#5243) --- app/docker/components/log-viewer/logViewerController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/docker/components/log-viewer/logViewerController.js b/app/docker/components/log-viewer/logViewerController.js index 9fe0b979c..5b797d630 100644 --- a/app/docker/components/log-viewer/logViewerController.js +++ b/app/docker/components/log-viewer/logViewerController.js @@ -48,7 +48,7 @@ angular.module('portainer.docker').controller('LogViewerController', [ }; this.downloadLogs = function () { - const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log, '')]); + const data = new Blob([_.reduce(this.state.filteredLogs, (acc, log) => acc + '\n' + log.line, '')]); FileSaver.saveAs(data, this.resourceName + '_logs.txt'); }; }, From 084cdcd8dc0b426a29d498d5e8d3305aa157edb1 Mon Sep 17 00:00:00 2001 From: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Date: Thu, 8 Jul 2021 12:08:10 +1200 Subject: [PATCH 06/15] fix(app):Set resource assignment default to off EE-1043 (#5286) --- .../views/resource-pools/create/createResourcePoolController.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js index 41e36bdbf..21d070bcb 100644 --- a/app/kubernetes/views/resource-pools/create/createResourcePoolController.js +++ b/app/kubernetes/views/resource-pools/create/createResourcePoolController.js @@ -172,6 +172,7 @@ class KubernetesCreateResourcePoolController { this.endpoint = endpoint; this.defaults = KubernetesResourceQuotaDefaults; this.formValues = new KubernetesResourcePoolFormValues(this.defaults); + this.formValues.HasQuota = true; this.state = { actionInProgress: false, From 65ded647b626f1311c946be69bbdf9f203d5549b Mon Sep 17 00:00:00 2001 From: cong meng Date: Thu, 8 Jul 2021 12:08:20 +1200 Subject: [PATCH 07/15] fix(ingress): fixed hostname field when having multiple ingresses EE-1072 (#5273) (#5285) Co-authored-by: fhanportainer <79428273+fhanportainer@users.noreply.github.com> --- app/kubernetes/converters/application.js | 4 ++-- app/kubernetes/helpers/application/index.js | 3 ++- app/kubernetes/models/application/formValues.js | 1 + .../views/applications/create/createApplication.html | 2 +- .../views/applications/create/createApplicationController.js | 5 ++++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/kubernetes/converters/application.js b/app/kubernetes/converters/application.js index 97f257f95..a4b3406cf 100644 --- a/app/kubernetes/converters/application.js +++ b/app/kubernetes/converters/application.js @@ -261,7 +261,7 @@ class KubernetesApplicationConverter { return res; } - static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels) { + static applicationToFormValues(app, resourcePools, configurations, persistentVolumeClaims, nodesLabels, ingresses) { const res = new KubernetesApplicationFormValues(); res.ApplicationType = app.ApplicationType; res.ResourcePool = _.find(resourcePools, ['Namespace.Name', app.ResourcePool]); @@ -278,7 +278,7 @@ class KubernetesApplicationConverter { res.PersistedFolders = KubernetesApplicationHelper.generatePersistedFoldersFormValuesFromPersistedFolders(app.PersistedFolders, persistentVolumeClaims); // generate from PVC and app.PersistedFolders res.Configurations = KubernetesApplicationHelper.generateConfigurationFormValuesFromEnvAndVolumes(app.Env, app.ConfigurationVolumes, configurations); res.AutoScaler = KubernetesApplicationHelper.generateAutoScalerFormValueFromHorizontalPodAutoScaler(app.AutoScaler, res.ReplicaCount); - res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts); + res.PublishedPorts = KubernetesApplicationHelper.generatePublishedPortsFormValuesFromPublishedPorts(app.ServiceType, app.PublishedPorts, ingresses); res.Containers = app.Containers; const isIngress = _.filter(res.PublishedPorts, (p) => p.IngressName).length; diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index dd93beef7..82aa97c4e 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -274,7 +274,7 @@ class KubernetesApplicationHelper { /* #endregion */ /* #region PUBLISHED PORTS FV <> PUBLISHED PORTS */ - static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts) { + static generatePublishedPortsFormValuesFromPublishedPorts(serviceType, publishedPorts, ingress) { const generatePort = (port, rule) => { const res = new KubernetesApplicationPublishedPortFormValue(); res.IsNew = false; @@ -282,6 +282,7 @@ class KubernetesApplicationHelper { res.IngressName = rule.IngressName; res.IngressRoute = rule.Path; res.IngressHost = rule.Host; + res.IngressHosts = ingress.find((i) => i.Name === rule.IngressName).Hosts; } res.Protocol = port.Protocol; res.ContainerPort = port.TargetPort; diff --git a/app/kubernetes/models/application/formValues.js b/app/kubernetes/models/application/formValues.js index 4d47a5ba5..6faffaeb6 100644 --- a/app/kubernetes/models/application/formValues.js +++ b/app/kubernetes/models/application/formValues.js @@ -124,6 +124,7 @@ export function KubernetesApplicationPublishedPortFormValue() { IngressName: undefined, IngressRoute: undefined, IngressHost: undefined, + IngressHosts: [], }; } diff --git a/app/kubernetes/views/applications/create/createApplication.html b/app/kubernetes/views/applications/create/createApplication.html index fbd9d3373..152bbc87a 100644 --- a/app/kubernetes/views/applications/create/createApplication.html +++ b/app/kubernetes/views/applications/create/createApplication.html @@ -1368,7 +1368,7 @@ class="form-control" name="ingress_hostname_{{ $index }}" ng-model="publishedPort.IngressHost" - ng-options="host as (host | kubernetesApplicationIngressEmptyHostname) for host in ctrl.ingressHostnames" + ng-options="host as (host | kubernetesApplicationIngressEmptyHostname) for host in publishedPort.IngressHosts" ng-change="ctrl.onChangePublishedPorts()" ng-disabled="ctrl.disableLoadBalancerEdit() || ctrl.isEditAndNotNewPublishedPort($index)" > diff --git a/app/kubernetes/views/applications/create/createApplicationController.js b/app/kubernetes/views/applications/create/createApplicationController.js index dcbc18faa..09b25384f 100644 --- a/app/kubernetes/views/applications/create/createApplicationController.js +++ b/app/kubernetes/views/applications/create/createApplicationController.js @@ -321,6 +321,7 @@ class KubernetesCreateApplicationController { const ingresses = this.filteredIngresses; p.IngressName = ingresses && ingresses.length ? ingresses[0].Name : undefined; p.IngressHost = ingresses && ingresses.length ? ingresses[0].Hosts[0] : undefined; + p.IngressHosts = ingresses && ingresses.length ? ingresses[0].Hosts : undefined; if (this.formValues.PublishedPorts.length) { p.Protocol = this.formValues.PublishedPorts[0].Protocol; } @@ -388,6 +389,7 @@ class KubernetesCreateApplicationController { onChangePortMappingIngress(index) { const publishedPort = this.formValues.PublishedPorts[index]; const ingress = _.find(this.filteredIngresses, { Name: publishedPort.IngressName }); + publishedPort.IngressHosts = ingress.Hosts; this.ingressHostnames = ingress.Hosts; publishedPort.IngressHost = this.ingressHostnames.length ? this.ingressHostnames[0] : []; this.onChangePublishedPorts(); @@ -972,7 +974,8 @@ class KubernetesCreateApplicationController { this.resourcePools, this.configurations, this.persistentVolumeClaims, - this.nodesLabels + this.nodesLabels, + this.filteredIngresses ); this.formValues.OriginalIngresses = this.filteredIngresses; this.savedFormValues = angular.copy(this.formValues); From 278667825ad1171c67e6f81105eb4685b6ddbbdc Mon Sep 17 00:00:00 2001 From: cong meng Date: Fri, 9 Jul 2021 10:39:14 +1200 Subject: [PATCH 08/15] EE-1110 Ingress routes and their mapping to a application name are not deleted when the application is deleted (#5291) Co-authored-by: Simon Meng --- app/kubernetes/helpers/application/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/kubernetes/helpers/application/index.js b/app/kubernetes/helpers/application/index.js index 82aa97c4e..657e63afd 100644 --- a/app/kubernetes/helpers/application/index.js +++ b/app/kubernetes/helpers/application/index.js @@ -282,7 +282,7 @@ class KubernetesApplicationHelper { res.IngressName = rule.IngressName; res.IngressRoute = rule.Path; res.IngressHost = rule.Host; - res.IngressHosts = ingress.find((i) => i.Name === rule.IngressName).Hosts; + res.IngressHosts = ingress && ingress.find((i) => i.Name === rule.IngressName).Hosts; } res.Protocol = port.Protocol; res.ContainerPort = port.TargetPort; From fa80a7b7e56bb3d8d3a4a3c29f11c902a8cd15ca Mon Sep 17 00:00:00 2001 From: fhanportainer <79428273+fhanportainer@users.noreply.github.com> Date: Mon, 19 Jul 2021 19:45:14 +1200 Subject: [PATCH 09/15] fix(k8s): fixed generating kube auction summary issue (#5332) --- app/kubernetes/ingress/converter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/kubernetes/ingress/converter.js b/app/kubernetes/ingress/converter.js index 284958b5b..f3860e453 100644 --- a/app/kubernetes/ingress/converter.js +++ b/app/kubernetes/ingress/converter.js @@ -57,7 +57,9 @@ export class KubernetesIngressConverter { rule.IngressName = ingress.Name; rule.ServiceName = serviceName; rule.Port = p.ContainerPort; - rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute; + if (p.IngressRoute) { + rule.Path = _.startsWith(p.IngressRoute, '/') ? p.IngressRoute : '/' + p.IngressRoute; + } rule.Host = p.IngressHost; ingress.Paths.push(rule); } From a5eac07b0c8254748ac993562840679ba3cea576 Mon Sep 17 00:00:00 2001 From: Dmitry Salakhov Date: Tue, 20 Jul 2021 14:05:40 +1200 Subject: [PATCH 10/15] fix(namespace): update portainer-config when delete a namespace (#5328) --- api/go.sum | 7 + api/http/proxy/factory/kubernetes.go | 6 +- .../factory/kubernetes/agent_transport.go | 55 ++++++ .../factory/kubernetes/edge_transport.go | 52 ++++++ .../factory/kubernetes/local_transport.go | 46 +++++ .../proxy/factory/kubernetes/namespaces.go | 15 ++ .../proxy/factory/kubernetes/transport.go | 170 +++++------------- api/kubernetes/cli/access.go | 16 ++ api/kubernetes/cli/access_test.go | 68 +++++++ api/kubernetes/cli/client.go | 5 +- api/portainer.go | 1 + 11 files changed, 315 insertions(+), 126 deletions(-) create mode 100644 api/http/proxy/factory/kubernetes/agent_transport.go create mode 100644 api/http/proxy/factory/kubernetes/edge_transport.go create mode 100644 api/http/proxy/factory/kubernetes/local_transport.go create mode 100644 api/http/proxy/factory/kubernetes/namespaces.go create mode 100644 api/kubernetes/cli/access_test.go diff --git a/api/go.sum b/api/go.sum index b7e4bb5c2..37a687dde 100644 --- a/api/go.sum +++ b/api/go.sum @@ -80,6 +80,7 @@ github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkg github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= @@ -153,6 +154,7 @@ github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= @@ -219,8 +221,10 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420 h1:Yu3681ykYHDfLoI6XVjL4JWmkE+3TX9yfIWwRCh1kFM= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -366,9 +370,11 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= @@ -395,6 +401,7 @@ k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUc k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= diff --git a/api/http/proxy/factory/kubernetes.go b/api/http/proxy/factory/kubernetes.go index a1774c0d2..ae1d65edd 100644 --- a/api/http/proxy/factory/kubernetes.go +++ b/api/http/proxy/factory/kubernetes.go @@ -39,7 +39,7 @@ func (factory *ProxyFactory) newKubernetesLocalProxy(endpoint *portainer.Endpoin return nil, err } - transport, err := kubernetes.NewLocalTransport(tokenManager) + transport, err := kubernetes.NewLocalTransport(tokenManager, endpoint, factory.dataStore) if err != nil { return nil, err } @@ -72,7 +72,7 @@ func (factory *ProxyFactory) newKubernetesEdgeHTTPProxy(endpoint *portainer.Endp endpointURL.Scheme = "http" proxy := newSingleHostReverseProxyWithHostHeader(endpointURL) - proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.reverseTunnelService, endpoint.ID, tokenManager) + proxy.Transport = kubernetes.NewEdgeTransport(factory.dataStore, factory.reverseTunnelService, endpoint, tokenManager) return proxy, nil } @@ -103,7 +103,7 @@ func (factory *ProxyFactory) newKubernetesAgentHTTPSProxy(endpoint *portainer.En } proxy := newSingleHostReverseProxyWithHostHeader(remoteURL) - proxy.Transport = kubernetes.NewAgentTransport(factory.dataStore, factory.signatureService, tlsConfig, tokenManager) + proxy.Transport = kubernetes.NewAgentTransport(factory.dataStore, factory.signatureService, tlsConfig, tokenManager, endpoint) return proxy, nil } diff --git a/api/http/proxy/factory/kubernetes/agent_transport.go b/api/http/proxy/factory/kubernetes/agent_transport.go new file mode 100644 index 000000000..fb87f1123 --- /dev/null +++ b/api/http/proxy/factory/kubernetes/agent_transport.go @@ -0,0 +1,55 @@ +package kubernetes + +import ( + "crypto/tls" + "net/http" + "strings" + + portainer "github.com/portainer/portainer/api" +) + +type agentTransport struct { + *baseTransport + signatureService portainer.DigitalSignatureService +} + +// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent +func NewAgentTransport(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager, endpoint *portainer.Endpoint) *agentTransport { + transport := &agentTransport{ + signatureService: signatureService, + baseTransport: newBaseTransport( + &http.Transport{ + TLSClientConfig: tlsConfig, + }, + tokenManager, + endpoint, + dataStore, + ), + } + + return transport +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) { + token, err := getRoundTripToken(request, transport.tokenManager, transport.endpoint.ID) + if err != nil { + return nil, err + } + + request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) + + if strings.HasPrefix(request.URL.Path, "/v2") { + decorateAgentRequest(request, transport.dataStore) + } + + signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) + if err != nil { + return nil, err + } + + request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey()) + request.Header.Set(portainer.PortainerAgentSignatureHeader, signature) + + return transport.baseTransport.RoundTrip(request) +} diff --git a/api/http/proxy/factory/kubernetes/edge_transport.go b/api/http/proxy/factory/kubernetes/edge_transport.go new file mode 100644 index 000000000..70ff737ff --- /dev/null +++ b/api/http/proxy/factory/kubernetes/edge_transport.go @@ -0,0 +1,52 @@ +package kubernetes + +import ( + "net/http" + "strings" + + portainer "github.com/portainer/portainer/api" +) + +type edgeTransport struct { + *baseTransport + reverseTunnelService portainer.ReverseTunnelService +} + +// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent +func NewEdgeTransport(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, endpoint *portainer.Endpoint, tokenManager *tokenManager) *edgeTransport { + transport := &edgeTransport{ + reverseTunnelService: reverseTunnelService, + baseTransport: newBaseTransport( + &http.Transport{}, + tokenManager, + endpoint, + dataStore, + ), + } + + return transport +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) { + token, err := getRoundTripToken(request, transport.tokenManager, transport.endpoint.ID) + if err != nil { + return nil, err + } + + request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) + + if strings.HasPrefix(request.URL.Path, "/v2") { + decorateAgentRequest(request, transport.dataStore) + } + + response, err := transport.baseTransport.RoundTrip(request) + + if err == nil { + transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpoint.ID) + } else { + transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpoint.ID) + } + + return response, err +} diff --git a/api/http/proxy/factory/kubernetes/local_transport.go b/api/http/proxy/factory/kubernetes/local_transport.go new file mode 100644 index 000000000..0378d71f2 --- /dev/null +++ b/api/http/proxy/factory/kubernetes/local_transport.go @@ -0,0 +1,46 @@ +package kubernetes + +import ( + "fmt" + "net/http" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" +) + +type localTransport struct { + *baseTransport +} + +// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API +func NewLocalTransport(tokenManager *tokenManager, endpoint *portainer.Endpoint, dataStore portainer.DataStore) (*localTransport, error) { + config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true) + if err != nil { + return nil, err + } + + transport := &localTransport{ + baseTransport: newBaseTransport( + &http.Transport{ + TLSClientConfig: config, + }, + tokenManager, + endpoint, + dataStore, + ), + } + + return transport, nil +} + +// RoundTrip is the implementation of the the http.RoundTripper interface +func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) { + token, err := getRoundTripToken(request, transport.tokenManager, transport.endpoint.ID) + if err != nil { + return nil, err + } + + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + return transport.baseTransport.RoundTrip(request) +} diff --git a/api/http/proxy/factory/kubernetes/namespaces.go b/api/http/proxy/factory/kubernetes/namespaces.go new file mode 100644 index 000000000..d5039455a --- /dev/null +++ b/api/http/proxy/factory/kubernetes/namespaces.go @@ -0,0 +1,15 @@ +package kubernetes + +import ( + "net/http" + + "github.com/pkg/errors" +) + +func (transport *baseTransport) deleteNamespaceRequest(request *http.Request, namespace string) (*http.Response, error) { + if err := transport.tokenManager.kubecli.NamespaceAccessPoliciesDeleteNamespace(namespace); err != nil { + return nil, errors.WithMessagef(err, "failed to delete a namespace [%s] from portainer config", namespace) + } + + return transport.executeKubernetesRequest(request, true) +} diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go index 377c7a3dc..5867f6bdd 100644 --- a/api/http/proxy/factory/kubernetes/transport.go +++ b/api/http/proxy/factory/kubernetes/transport.go @@ -2,147 +2,73 @@ package kubernetes import ( "bytes" - "crypto/tls" "encoding/json" - "fmt" "io/ioutil" "log" "net/http" + "regexp" "strings" "github.com/portainer/portainer/api/http/security" portainer "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/crypto" ) -type ( - localTransport struct { - httpTransport *http.Transport - tokenManager *tokenManager - endpointIdentifier portainer.EndpointID - } - - agentTransport struct { - dataStore portainer.DataStore - httpTransport *http.Transport - tokenManager *tokenManager - signatureService portainer.DigitalSignatureService - endpointIdentifier portainer.EndpointID - } - - edgeTransport struct { - dataStore portainer.DataStore - httpTransport *http.Transport - tokenManager *tokenManager - reverseTunnelService portainer.ReverseTunnelService - endpointIdentifier portainer.EndpointID - } -) - -// NewLocalTransport returns a new transport that can be used to send requests to the local Kubernetes API -func NewLocalTransport(tokenManager *tokenManager) (*localTransport, error) { - config, err := crypto.CreateTLSConfigurationFromBytes(nil, nil, nil, true, true) - if err != nil { - return nil, err - } - - transport := &localTransport{ - httpTransport: &http.Transport{ - TLSClientConfig: config, - }, - tokenManager: tokenManager, - } - - return transport, nil +type baseTransport struct { + httpTransport *http.Transport + tokenManager *tokenManager + endpoint *portainer.Endpoint + dataStore portainer.DataStore } -// RoundTrip is the implementation of the the http.RoundTripper interface -func (transport *localTransport) RoundTrip(request *http.Request) (*http.Response, error) { - token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier) - if err != nil { - return nil, err +func newBaseTransport(httpTransport *http.Transport, tokenManager *tokenManager, endpoint *portainer.Endpoint, dataStore portainer.DataStore) *baseTransport { + return &baseTransport{ + httpTransport: httpTransport, + tokenManager: tokenManager, + endpoint: endpoint, + dataStore: dataStore, + } +} + +func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response, error) { + apiVersionRe := regexp.MustCompile(`^(/kubernetes)?/api/v[0-9](\.[0-9])?`) + requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "") + + switch { + case strings.EqualFold(requestPath, "/namespaces"): + return transport.executeKubernetesRequest(request, true) + case strings.HasPrefix(requestPath, "/namespaces"): + return transport.proxyNamespacedRequest(request, requestPath) + default: + return transport.executeKubernetesRequest(request, true) + } +} + +func (transport *baseTransport) proxyNamespacedRequest(request *http.Request, fullRequestPath string) (*http.Response, error) { + requestPath := strings.TrimPrefix(fullRequestPath, "/namespaces/") + split := strings.SplitN(requestPath, "/", 2) + namespace := split[0] + + requestPath = "" + if len(split) > 1 { + requestPath = split[1] } - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + switch { + case requestPath == "" && request.Method == "DELETE": + return transport.deleteNamespaceRequest(request, namespace) + default: + return transport.executeKubernetesRequest(request, true) + } +} +func (transport *baseTransport) executeKubernetesRequest(request *http.Request, shouldLog bool) (*http.Response, error) { return transport.httpTransport.RoundTrip(request) } -// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent -func NewAgentTransport(datastore portainer.DataStore, signatureService portainer.DigitalSignatureService, tlsConfig *tls.Config, tokenManager *tokenManager) *agentTransport { - transport := &agentTransport{ - dataStore: datastore, - httpTransport: &http.Transport{ - TLSClientConfig: tlsConfig, - }, - tokenManager: tokenManager, - signatureService: signatureService, - } - - return transport -} - -// RoundTrip is the implementation of the the http.RoundTripper interface -func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Response, error) { - token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier) - if err != nil { - return nil, err - } - - request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) - - if strings.HasPrefix(request.URL.Path, "/v2") { - decorateAgentRequest(request, transport.dataStore) - } - - signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) - if err != nil { - return nil, err - } - - request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey()) - request.Header.Set(portainer.PortainerAgentSignatureHeader, signature) - - return transport.httpTransport.RoundTrip(request) -} - -// NewEdgeTransport returns a new transport that can be used to send signed requests to a Portainer Edge agent -func NewEdgeTransport(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, endpointIdentifier portainer.EndpointID, tokenManager *tokenManager) *edgeTransport { - transport := &edgeTransport{ - dataStore: datastore, - httpTransport: &http.Transport{}, - tokenManager: tokenManager, - reverseTunnelService: reverseTunnelService, - endpointIdentifier: endpointIdentifier, - } - - return transport -} - -// RoundTrip is the implementation of the the http.RoundTripper interface -func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response, error) { - token, err := getRoundTripToken(request, transport.tokenManager, transport.endpointIdentifier) - if err != nil { - return nil, err - } - - request.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) - - if strings.HasPrefix(request.URL.Path, "/v2") { - decorateAgentRequest(request, transport.dataStore) - } - - response, err := transport.httpTransport.RoundTrip(request) - - if err == nil { - transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpointIdentifier) - } else { - transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpointIdentifier) - } - - return response, err -} +var ( + namespaceRegex = regexp.MustCompile(`^/namespaces/([^/]*)$`) +) func getRoundTripToken( request *http.Request, diff --git a/api/kubernetes/cli/access.go b/api/kubernetes/cli/access.go index e4b4729c6..b6f5f47f7 100644 --- a/api/kubernetes/cli/access.go +++ b/api/kubernetes/cli/access.go @@ -3,6 +3,7 @@ package cli import ( "encoding/json" + "github.com/pkg/errors" portainer "github.com/portainer/portainer/api" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -80,6 +81,21 @@ func hasUserAccessToNamespace(userID int, teamIDs []int, policies portainer.K8sN return false } +// NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace +func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error { + kcl.lock.Lock() + defer kcl.lock.Unlock() + + policies, err := kcl.GetNamespaceAccessPolicies() + if err != nil { + return errors.WithMessage(err, "failed to fetch access policies") + } + + delete(policies, ns) + + return kcl.UpdateNamespaceAccessPolicies(policies) +} + // GetNamespaceAccessPolicies gets the namespace access policies // from config maps in the portainer namespace func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) { diff --git a/api/kubernetes/cli/access_test.go b/api/kubernetes/cli/access_test.go new file mode 100644 index 000000000..b43d06dc6 --- /dev/null +++ b/api/kubernetes/cli/access_test.go @@ -0,0 +1,68 @@ +package cli + +import ( + "sync" + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/stretchr/testify/assert" + ktypes "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kfake "k8s.io/client-go/kubernetes/fake" +) + +func Test_NamespaceAccessPoliciesDeleteNamespace_updatesPortainerConfig_whenConfigExists(t *testing.T) { + testcases := []struct { + name string + namespaceToDelete string + expectedConfig map[string]portainer.K8sNamespaceAccessPolicy + }{ + { + name: "doesn't change config, when designated namespace absent", + namespaceToDelete: "missing-namespace", + expectedConfig: map[string]portainer.K8sNamespaceAccessPolicy{ + "ns1": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}}, + "ns2": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}}, + }, + }, + { + name: "removes designated namespace from config, when namespace is present", + namespaceToDelete: "ns2", + expectedConfig: map[string]portainer.K8sNamespaceAccessPolicy{ + "ns1": {UserAccessPolicies: portainer.UserAccessPolicies{2: {RoleID: 0}}}, + }, + }, + } + + for _, test := range testcases { + t.Run(test.name, func(t *testing.T) { + k := &KubeClient{ + cli: kfake.NewSimpleClientset(), + instanceID: "instance", + lock: &sync.Mutex{}, + } + + config := &ktypes.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: portainerConfigMapName, + Namespace: portainerNamespace, + }, + Data: map[string]string{ + "NamespaceAccessPolicies": `{"ns1":{"UserAccessPolicies":{"2":{"RoleId":0}}}, "ns2":{"UserAccessPolicies":{"2":{"RoleId":0}}}}`, + }, + } + _, err := k.cli.CoreV1().ConfigMaps(portainerNamespace).Create(config) + assert.NoError(t, err, "failed to create a portainer config") + defer func() { + k.cli.CoreV1().ConfigMaps(portainerNamespace).Delete(portainerConfigMapName, nil) + }() + + err = k.NamespaceAccessPoliciesDeleteNamespace(test.namespaceToDelete) + assert.NoError(t, err, "failed to delete namespace") + + policies, err := k.GetNamespaceAccessPolicies() + assert.NoError(t, err, "failed to fetch policies") + assert.Equal(t, test.expectedConfig, policies) + }) + } +} diff --git a/api/kubernetes/cli/client.go b/api/kubernetes/cli/client.go index a268150c9..5b778837a 100644 --- a/api/kubernetes/cli/client.go +++ b/api/kubernetes/cli/client.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "strconv" + "sync" cmap "github.com/orcaman/concurrent-map" @@ -25,8 +26,9 @@ type ( // KubeClient represent a service used to execute Kubernetes operations KubeClient struct { - cli *kubernetes.Clientset + cli kubernetes.Interface instanceID string + lock *sync.Mutex } ) @@ -72,6 +74,7 @@ func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (po kubecli := &KubeClient{ cli: cli, instanceID: factory.instanceID, + lock: &sync.Mutex{}, } return kubecli, nil diff --git a/api/portainer.go b/api/portainer.go index cf3e664e6..be2b30f80 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1165,6 +1165,7 @@ type ( SetupUserServiceAccount(userID int, teamIDs []int) error GetServiceAccountBearerToken(userID int) (string, error) StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error + NamespaceAccessPoliciesDeleteNamespace(namespace string) error GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error) UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error } From 5b55b890e7d99ef3c8b485b660e11990e453ea96 Mon Sep 17 00:00:00 2001 From: Richard Wei <54336863+WaysonWei@users.noreply.github.com> Date: Wed, 21 Jul 2021 13:54:26 +1200 Subject: [PATCH 11/15] fix charts x label padding (#5339) --- app/portainer/services/chartService.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/portainer/services/chartService.js b/app/portainer/services/chartService.js index 0fadc7bdf..f142ce108 100644 --- a/app/portainer/services/chartService.js +++ b/app/portainer/services/chartService.js @@ -26,6 +26,11 @@ angular.module('portainer.app').factory('ChartService', [ }, }, }, + layout: { + padding: { + left: 15, + }, + }, hover: { animationDuration: 0 }, scales: { yAxes: [ From be30e1c453f4efe69ca9285fb50f71f549ed4146 Mon Sep 17 00:00:00 2001 From: Hui Date: Thu, 22 Jul 2021 11:39:47 +1200 Subject: [PATCH 12/15] fix(swagger): add swagger annotation for pull and redeploy stack --- api/git/types/types.go | 9 ++++++--- api/http/handler/stacks/stack_delete.go | 2 +- api/http/handler/stacks/stack_start.go | 2 +- api/http/handler/stacks/stack_stop.go | 2 +- api/http/handler/stacks/stack_update_git.go | 18 +++++++++++++++++- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/api/git/types/types.go b/api/git/types/types.go index 2a91f61b6..055222700 100644 --- a/api/git/types/types.go +++ b/api/git/types/types.go @@ -1,7 +1,10 @@ package gittypes type RepoConfig struct { - URL string - ReferenceName string - ConfigFilePath string + // The repo url + URL string `example:"https://github.com/portainer/portainer-ee.git"` + // The reference name + ReferenceName string `example:"refs/heads/branch_name"` + // Path to where the config file is in this url/refName + ConfigFilePath string `example:"docker-compose.yml"` } diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index 52f003cea..11d2c31b5 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -27,7 +27,7 @@ import ( // @success 204 "Success" // @failure 400 "Invalid request" // @failure 403 "Permission denied" -// @failure 404 " not found" +// @failure 404 "Not found" // @failure 500 "Server error" // @router /stacks/{id} [delete] func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { diff --git a/api/http/handler/stacks/stack_start.go b/api/http/handler/stacks/stack_start.go index 0a915600b..dc4163e92 100644 --- a/api/http/handler/stacks/stack_start.go +++ b/api/http/handler/stacks/stack_start.go @@ -26,7 +26,7 @@ import ( // @success 200 {object} portainer.Stack "Success" // @failure 400 "Invalid request" // @failure 403 "Permission denied" -// @failure 404 " not found" +// @failure 404 "Not found" // @failure 500 "Server error" // @router /stacks/{id}/start [post] func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { diff --git a/api/http/handler/stacks/stack_stop.go b/api/http/handler/stacks/stack_stop.go index 7dabfb265..700cec976 100644 --- a/api/http/handler/stacks/stack_stop.go +++ b/api/http/handler/stacks/stack_stop.go @@ -24,7 +24,7 @@ import ( // @success 200 {object} portainer.Stack "Success" // @failure 400 "Invalid request" // @failure 403 "Permission denied" -// @failure 404 " not found" +// @failure 404 "Not found" // @failure 500 "Server error" // @router /stacks/{id}/stop [post] func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { diff --git a/api/http/handler/stacks/stack_update_git.go b/api/http/handler/stacks/stack_update_git.go index 9d266cd89..dd46e409e 100644 --- a/api/http/handler/stacks/stack_update_git.go +++ b/api/http/handler/stacks/stack_update_git.go @@ -33,7 +33,23 @@ func (payload *updateStackGitPayload) Validate(r *http.Request) error { return nil } -// PUT request on /api/stacks/:id/git?endpointId= +// @id StackUpdateGit +// @summary Redeploy a stack +// @description Pull and redeploy a stack via Git +// @description **Access policy**: restricted +// @tags stacks +// @security jwt +// @accept json +// @produce json +// @param id path int true "Stack identifier" +// @param endpointId query int false "Stacks created before version 1.18.0 might not have an associated endpoint identifier. Use this optional parameter to set the endpoint identifier used by the stack." +// @param body body updateStackGitPayload true "Git configs for pull and redeploy a stack" +// @success 200 {object} portainer.Stack "Success" +// @failure 400 "Invalid request" +// @failure 403 "Permission denied" +// @failure 404 "Not found" +// @failure 500 "Server error" +// @router /stacks/{id}/git [put] func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { stackID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { From 31fdef1e605d73d3e4f006dc20750f9ae4352f67 Mon Sep 17 00:00:00 2001 From: cong meng Date: Tue, 27 Jul 2021 09:55:09 +1200 Subject: [PATCH 13/15] fix(advance deploy): EE-1141 A standard user can escalate to cluster administrator privileges on Kubernetes (#5324) * fix(advance deploy): EE-1141 A standard user can escalate to cluster administrator privileges on Kubernetes * fix(advance deploy): EE-1141 reuse existing token cache when do deployment * fix: EE-1141 use user's SA token to exec pod command * fix: EE-1141 stop advanced-deploy or pod-exec if user's SA token is empty Co-authored-by: Simon Meng --- api/cmd/portainer/main.go | 6 +- api/exec/kubernetes_deploy.go | 70 +++++++++++++++---- .../handler/stacks/create_kubernetes_stack.go | 8 +-- api/http/handler/websocket/handler.go | 17 +++-- api/http/handler/websocket/pod.go | 45 +++++++++++- api/http/handler/websocket/proxy.go | 2 + api/http/handler/websocket/types.go | 1 + api/http/proxy/factory/kubernetes/token.go | 14 ++-- .../proxy/factory/kubernetes/token_cache.go | 15 ++++ .../proxy/factory/kubernetes/transport.go | 4 +- api/http/server.go | 2 +- api/kubernetes/cli/exec.go | 9 ++- api/portainer.go | 5 +- 13 files changed, 155 insertions(+), 43 deletions(-) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index a852abd56..51fa6f192 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -89,8 +89,8 @@ func initSwarmStackManager(assetsPath string, dataStorePath string, signatureSer return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService) } -func initKubernetesDeployer(dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer { - return exec.NewKubernetesDeployer(dataStore, reverseTunnelService, signatureService, assetsPath) +func initKubernetesDeployer(kubernetesTokenCacheManager *kubeproxy.TokenCacheManager, kubernetesClientFactory *kubecli.ClientFactory, dataStore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, assetsPath string) portainer.KubernetesDeployer { + return exec.NewKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, signatureService, assetsPath) } func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error) { @@ -402,7 +402,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager) - kubernetesDeployer := initKubernetesDeployer(dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets) + kubernetesDeployer := initKubernetesDeployer(kubernetesTokenCacheManager, kubernetesClientFactory, dataStore, reverseTunnelService, digitalSignatureService, *flags.Assets) if dataStore.IsNew() { err = updateSettingsFromFlags(dataStore, flags) diff --git a/api/exec/kubernetes_deploy.go b/api/exec/kubernetes_deploy.go index 9d3a07d1d..ed4723c2d 100644 --- a/api/exec/kubernetes_deploy.go +++ b/api/exec/kubernetes_deploy.go @@ -5,6 +5,9 @@ import ( "encoding/json" "errors" "fmt" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/kubernetes/cli" "io/ioutil" "net/http" "net/url" @@ -20,27 +23,64 @@ import ( // KubernetesDeployer represents a service to deploy resources inside a Kubernetes environment. type KubernetesDeployer struct { - binaryPath string - dataStore portainer.DataStore - reverseTunnelService portainer.ReverseTunnelService - signatureService portainer.DigitalSignatureService + binaryPath string + dataStore portainer.DataStore + reverseTunnelService portainer.ReverseTunnelService + signatureService portainer.DigitalSignatureService + kubernetesClientFactory *cli.ClientFactory + kubernetesTokenCacheManager *kubernetes.TokenCacheManager } // NewKubernetesDeployer initializes a new KubernetesDeployer service. -func NewKubernetesDeployer(datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer { +func NewKubernetesDeployer(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, kubernetesClientFactory *cli.ClientFactory, datastore portainer.DataStore, reverseTunnelService portainer.ReverseTunnelService, signatureService portainer.DigitalSignatureService, binaryPath string) *KubernetesDeployer { return &KubernetesDeployer{ - binaryPath: binaryPath, - dataStore: datastore, - reverseTunnelService: reverseTunnelService, - signatureService: signatureService, + binaryPath: binaryPath, + dataStore: datastore, + reverseTunnelService: reverseTunnelService, + signatureService: signatureService, + kubernetesClientFactory: kubernetesClientFactory, + kubernetesTokenCacheManager: kubernetesTokenCacheManager, } } +func (deployer *KubernetesDeployer) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return "", err + } + + kubecli, err := deployer.kubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return "", err + } + + tokenCache := deployer.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID)) + + tokenManager, err := kubernetes.NewTokenManager(kubecli, deployer.dataStore, tokenCache, setLocalAdminToken) + if err != nil { + return "", err + } + + if tokenData.Role == portainer.AdministratorRole { + return tokenManager.GetAdminServiceAccountToken(), nil + } + + token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID)) + if err != nil { + return "", err + } + + if token == "" { + return "", fmt.Errorf("can not get a valid user service account token") + } + return token, nil +} + // Deploy will deploy a Kubernetes manifest inside a specific namespace in a Kubernetes endpoint. // Otherwise it will use kubectl to deploy the manifest. -func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) { +func (deployer *KubernetesDeployer) Deploy(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, namespace string) (string, error) { if endpoint.Type == portainer.KubernetesLocalEnvironment { - token, err := ioutil.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token") + token, err := deployer.getToken(request, endpoint, true); if err != nil { return "", err } @@ -53,7 +93,7 @@ func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackCo args := make([]string, 0) args = append(args, "--server", endpoint.URL) args = append(args, "--insecure-skip-tls-verify") - args = append(args, "--token", string(token)) + args = append(args, "--token", token) args = append(args, "--namespace", namespace) args = append(args, "apply", "-f", "-") @@ -139,8 +179,14 @@ func (deployer *KubernetesDeployer) Deploy(endpoint *portainer.Endpoint, stackCo return "", err } + token, err := deployer.getToken(request, endpoint, false); + if err != nil { + return "", err + } + req.Header.Set(portainer.PortainerAgentPublicKeyHeader, deployer.signatureService.EncodedPublicKey()) req.Header.Set(portainer.PortainerAgentSignatureHeader, signature) + req.Header.Set(portainer.PortainerAgentKubernetesSATokenHeader, token) resp, err := httpCli.Do(req) if err != nil { diff --git a/api/http/handler/stacks/create_kubernetes_stack.go b/api/http/handler/stacks/create_kubernetes_stack.go index 918d1047d..4de14d3a3 100644 --- a/api/http/handler/stacks/create_kubernetes_stack.go +++ b/api/http/handler/stacks/create_kubernetes_stack.go @@ -95,7 +95,7 @@ func (handler *Handler) createKubernetesStackFromFileContent(w http.ResponseWrit doCleanUp := true defer handler.cleanUp(stack, &doCleanUp) - output, err := handler.deployKubernetesStack(endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace) + output, err := handler.deployKubernetesStack(r, endpoint, payload.StackFileContent, payload.ComposeFormat, payload.Namespace) if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err} } @@ -139,7 +139,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Failed to process manifest from Git repository", Err: err} } - output, err := handler.deployKubernetesStack(endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace) + output, err := handler.deployKubernetesStack(r, endpoint, stackFileContent, payload.ComposeFormat, payload.Namespace) if err != nil { return &httperror.HandlerError{StatusCode: http.StatusInternalServerError, Message: "Unable to deploy Kubernetes stack", Err: err} } @@ -155,7 +155,7 @@ func (handler *Handler) createKubernetesStackFromGitRepository(w http.ResponseWr return response.JSON(w, resp) } -func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) { +func (handler *Handler) deployKubernetesStack(request *http.Request, endpoint *portainer.Endpoint, stackConfig string, composeFormat bool, namespace string) (string, error) { handler.stackCreationMutex.Lock() defer handler.stackCreationMutex.Unlock() @@ -167,7 +167,7 @@ func (handler *Handler) deployKubernetesStack(endpoint *portainer.Endpoint, stac stackConfig = string(convertedConfig) } - return handler.KubernetesDeployer.Deploy(endpoint, stackConfig, namespace) + return handler.KubernetesDeployer.Deploy(request, endpoint, stackConfig, namespace) } diff --git a/api/http/handler/websocket/handler.go b/api/http/handler/websocket/handler.go index 05cd88cfc..517df5756 100644 --- a/api/http/handler/websocket/handler.go +++ b/api/http/handler/websocket/handler.go @@ -5,6 +5,7 @@ import ( "github.com/gorilla/websocket" httperror "github.com/portainer/libhttp/error" portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/kubernetes/cli" ) @@ -12,20 +13,22 @@ import ( // Handler is the HTTP handler used to handle websocket operations. type Handler struct { *mux.Router - DataStore portainer.DataStore - SignatureService portainer.DigitalSignatureService - ReverseTunnelService portainer.ReverseTunnelService - KubernetesClientFactory *cli.ClientFactory - requestBouncer *security.RequestBouncer - connectionUpgrader websocket.Upgrader + DataStore portainer.DataStore + SignatureService portainer.DigitalSignatureService + ReverseTunnelService portainer.ReverseTunnelService + KubernetesClientFactory *cli.ClientFactory + requestBouncer *security.RequestBouncer + connectionUpgrader websocket.Upgrader + kubernetesTokenCacheManager *kubernetes.TokenCacheManager } // NewHandler creates a handler to manage websocket operations. -func NewHandler(bouncer *security.RequestBouncer) *Handler { +func NewHandler(kubernetesTokenCacheManager *kubernetes.TokenCacheManager, bouncer *security.RequestBouncer) *Handler { h := &Handler{ Router: mux.NewRouter(), connectionUpgrader: websocket.Upgrader{}, requestBouncer: bouncer, + kubernetesTokenCacheManager: kubernetesTokenCacheManager, } h.PathPrefix("/websocket/exec").Handler( bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec))) diff --git a/api/http/handler/websocket/pod.go b/api/http/handler/websocket/pod.go index 3ae12750a..07e14c39d 100644 --- a/api/http/handler/websocket/pod.go +++ b/api/http/handler/websocket/pod.go @@ -1,6 +1,8 @@ package websocket import ( + "fmt" + "github.com/portainer/portainer/api/http/security" "io" "log" "net/http" @@ -11,6 +13,7 @@ import ( "github.com/portainer/libhttp/request" portainer "github.com/portainer/portainer/api" bolterrors "github.com/portainer/portainer/api/bolt/errors" + "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" ) // @summary Execute a websocket on pod @@ -70,8 +73,14 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } + token, useAdminToken, err := handler.getToken(r, endpoint, false) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to get user service account token", err} + } + params := &webSocketRequestParams{ endpoint: endpoint, + token: token, } r.Header.Del("Origin") @@ -112,7 +121,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create Kubernetes client", err} } - err = cli.StartExecProcess(namespace, podName, containerName, commandArray, stdinReader, stdoutWriter) + err = cli.StartExecProcess(token, useAdminToken, namespace, podName, containerName, commandArray, stdinReader, stdoutWriter) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to start exec process inside container", err} } @@ -124,3 +133,37 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request) return nil } + +func (handler *Handler) getToken(request *http.Request, endpoint *portainer.Endpoint, setLocalAdminToken bool) (string, bool, error) { + tokenData, err := security.RetrieveTokenData(request) + if err != nil { + return "", false, err + } + + kubecli, err := handler.KubernetesClientFactory.GetKubeClient(endpoint) + if err != nil { + return "", false, err + } + + tokenCache := handler.kubernetesTokenCacheManager.GetOrCreateTokenCache(int(endpoint.ID)) + + tokenManager, err := kubernetes.NewTokenManager(kubecli, handler.DataStore, tokenCache, setLocalAdminToken) + if err != nil { + return "", false, err + } + + if tokenData.Role == portainer.AdministratorRole { + return tokenManager.GetAdminServiceAccountToken(), true, nil + } + + token, err := tokenManager.GetUserServiceAccountToken(int(tokenData.ID)) + if err != nil { + return "", false, err + } + + if token == "" { + return "", false, fmt.Errorf("can not get a valid user service account token") + } + + return token, false, nil +} diff --git a/api/http/handler/websocket/proxy.go b/api/http/handler/websocket/proxy.go index 984240256..14072d315 100644 --- a/api/http/handler/websocket/proxy.go +++ b/api/http/handler/websocket/proxy.go @@ -24,6 +24,7 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r proxy.Director = func(incoming *http.Request, out http.Header) { out.Set(portainer.PortainerAgentTargetHeader, params.nodeName) + out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token) } handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID) @@ -64,6 +65,7 @@ func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *htt out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey()) out.Set(portainer.PortainerAgentSignatureHeader, signature) out.Set(portainer.PortainerAgentTargetHeader, params.nodeName) + out.Set(portainer.PortainerAgentKubernetesSATokenHeader, params.token) } proxy.ServeHTTP(w, r) diff --git a/api/http/handler/websocket/types.go b/api/http/handler/websocket/types.go index abb86c7db..b321ea075 100644 --- a/api/http/handler/websocket/types.go +++ b/api/http/handler/websocket/types.go @@ -8,4 +8,5 @@ type webSocketRequestParams struct { ID string nodeName string endpoint *portainer.Endpoint + token string } diff --git a/api/http/proxy/factory/kubernetes/token.go b/api/http/proxy/factory/kubernetes/token.go index bfcc145d8..1e13bd9df 100644 --- a/api/http/proxy/factory/kubernetes/token.go +++ b/api/http/proxy/factory/kubernetes/token.go @@ -1,10 +1,8 @@ package kubernetes import ( - "io/ioutil" - "sync" - portainer "github.com/portainer/portainer/api" + "io/ioutil" ) const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" @@ -13,7 +11,6 @@ type tokenManager struct { tokenCache *tokenCache kubecli portainer.KubeClient dataStore portainer.DataStore - mutex sync.Mutex adminToken string } @@ -25,7 +22,6 @@ func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore tokenCache: cache, kubecli: kubecli, dataStore: dataStore, - mutex: sync.Mutex{}, adminToken: "", } @@ -41,13 +37,13 @@ func NewTokenManager(kubecli portainer.KubeClient, dataStore portainer.DataStore return tokenManager, nil } -func (manager *tokenManager) getAdminServiceAccountToken() string { +func (manager *tokenManager) GetAdminServiceAccountToken() string { return manager.adminToken } -func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) { - manager.mutex.Lock() - defer manager.mutex.Unlock() +func (manager *tokenManager) GetUserServiceAccountToken(userID int) (string, error) { + manager.tokenCache.mutex.Lock() + defer manager.tokenCache.mutex.Unlock() token, ok := manager.tokenCache.getToken(userID) if !ok { diff --git a/api/http/proxy/factory/kubernetes/token_cache.go b/api/http/proxy/factory/kubernetes/token_cache.go index 552e6b3a1..316b3a3e9 100644 --- a/api/http/proxy/factory/kubernetes/token_cache.go +++ b/api/http/proxy/factory/kubernetes/token_cache.go @@ -2,6 +2,7 @@ package kubernetes import ( "strconv" + "sync" "github.com/orcaman/concurrent-map" ) @@ -14,6 +15,7 @@ type ( tokenCache struct { userTokenCache cmap.ConcurrentMap + mutex sync.Mutex } ) @@ -35,6 +37,18 @@ func (manager *TokenCacheManager) CreateTokenCache(endpointID int) *tokenCache { return tokenCache } +// GetOrCreateTokenCache will get the tokenCache from the manager map of caches if it exists, +// otherwise it will create a new tokenCache object, associate it to the manager map of caches +// and return a pointer to that tokenCache instance. +func (manager *TokenCacheManager) GetOrCreateTokenCache(endpointID int) *tokenCache { + key := strconv.Itoa(endpointID) + if epCache, ok := manager.tokenCaches.Get(key); ok { + return epCache.(*tokenCache) + } + + return manager.CreateTokenCache(endpointID) +} + // RemoveUserFromCache will ensure that the specific userID is removed from all registered caches. func (manager *TokenCacheManager) RemoveUserFromCache(userID int) { for cache := range manager.tokenCaches.IterBuffered() { @@ -45,6 +59,7 @@ func (manager *TokenCacheManager) RemoveUserFromCache(userID int) { func newTokenCache() *tokenCache { return &tokenCache{ userTokenCache: cmap.New(), + mutex: sync.Mutex{}, } } diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go index 5867f6bdd..3fa4b9f03 100644 --- a/api/http/proxy/factory/kubernetes/transport.go +++ b/api/http/proxy/factory/kubernetes/transport.go @@ -82,9 +82,9 @@ func getRoundTripToken( var token string if tokenData.Role == portainer.AdministratorRole { - token = tokenManager.getAdminServiceAccountToken() + token = tokenManager.GetAdminServiceAccountToken() } else { - token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID)) + token, err = tokenManager.GetUserServiceAccountToken(int(tokenData.ID)) if err != nil { log.Printf("Failed retrieving service account token: %v", err) return "", err diff --git a/api/http/server.go b/api/http/server.go index 8f399e7a9..decbe10aa 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -204,7 +204,7 @@ func (server *Server) Start() error { userHandler.DataStore = server.DataStore userHandler.CryptoService = server.CryptoService - var websocketHandler = websocket.NewHandler(requestBouncer) + var websocketHandler = websocket.NewHandler(server.KubernetesTokenCacheManager, requestBouncer) websocketHandler.DataStore = server.DataStore websocketHandler.SignatureService = server.SignatureService websocketHandler.ReverseTunnelService = server.ReverseTunnelService diff --git a/api/kubernetes/cli/exec.go b/api/kubernetes/cli/exec.go index 1716b10e6..55cc38bc9 100644 --- a/api/kubernetes/cli/exec.go +++ b/api/kubernetes/cli/exec.go @@ -14,13 +14,18 @@ import ( // StartExecProcess will start an exec process inside a container located inside a pod inside a specific namespace // using the specified command. The stdin parameter will be bound to the stdin process and the stdout process will write // to the stdout parameter. -// This function only works against a local endpoint using an in-cluster config. -func (kcl *KubeClient) StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error { +// This function only works against a local endpoint using an in-cluster config with the user's SA token. +func (kcl *KubeClient) StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error { config, err := rest.InClusterConfig() if err != nil { return err } + if !useAdminToken { + config.BearerToken = token + config.BearerTokenFile = "" + } + req := kcl.cli.CoreV1().RESTClient(). Post(). Resource("pods"). diff --git a/api/portainer.go b/api/portainer.go index be2b30f80..6d10dacea 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -2,6 +2,7 @@ package portainer import ( "io" + "net/http" "time" gittypes "github.com/portainer/portainer/api/git/types" @@ -1164,7 +1165,7 @@ type ( KubeClient interface { SetupUserServiceAccount(userID int, teamIDs []int) error GetServiceAccountBearerToken(userID int) (string, error) - StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error + StartExecProcess(token string, useAdminToken bool, namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error NamespaceAccessPoliciesDeleteNamespace(namespace string) error GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error) UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error @@ -1172,7 +1173,7 @@ type ( // KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint KubernetesDeployer interface { - Deploy(endpoint *Endpoint, data string, namespace string) (string, error) + Deploy(request *http.Request, endpoint *Endpoint, data string, namespace string) (string, error) ConvertCompose(data string) ([]byte, error) } From a0b52fc3d7eceeb2df04156820abc85cd6afea65 Mon Sep 17 00:00:00 2001 From: Matt Hook Date: Tue, 27 Jul 2021 10:41:58 +1200 Subject: [PATCH 14/15] Fixes for EE-1035 and dockerhub pro accounts. (#5343) --- .../handler/endpoints/endpoint_dockerhub_status.go | 10 ++++++++++ .../por-image-registry-rate-limits.controller.js | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/api/http/handler/endpoints/endpoint_dockerhub_status.go b/api/http/handler/endpoints/endpoint_dockerhub_status.go index 793b85715..b454f9378 100644 --- a/api/http/handler/endpoints/endpoint_dockerhub_status.go +++ b/api/http/handler/endpoints/endpoint_dockerhub_status.go @@ -115,8 +115,18 @@ func getDockerHubLimits(httpClient *client.HTTPClient, token string) (*dockerhub return nil, errors.New("failed fetching dockerhub limits") } + // An error with rateLimit-Limit or RateLimit-Remaining is likely for dockerhub pro accounts where there is no rate limit. + // In that specific case the headers will not be present. Don't bubble up the error as its normal + // See: https://docs.docker.com/docker-hub/download-rate-limit/ rateLimit, err := parseRateLimitHeader(resp.Header, "RateLimit-Limit") + if err != nil { + return nil, nil + } + rateLimitRemaining, err := parseRateLimitHeader(resp.Header, "RateLimit-Remaining") + if err != nil { + return nil, nil + } return &dockerhubStatusResponse{ Limit: rateLimit, diff --git a/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js b/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js index d823c848d..f3114b526 100644 --- a/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js +++ b/app/docker/components/imageRegistry/por-image-registry-rate-limits.controller.js @@ -19,7 +19,7 @@ export default class porImageRegistryContainerController { if (this.EndpointHelper.isAgentEndpoint(this.endpoint) || this.EndpointHelper.isLocalEndpoint(this.endpoint)) { try { this.pullRateLimits = await this.DockerHubService.checkRateLimits(this.endpoint); - this.setValidity(this.pullRateLimits.remaining >= 0); + this.setValidity(!this.pullRateLimits.limit || (this.pullRateLimits.limit && this.pullRateLimits.remaining >= 0)); } catch (e) { // eslint-disable-next-line no-console console.error('Failed loading DockerHub pull rate limits', e); From 325405164762240da240c405489a3c00fb52d4d7 Mon Sep 17 00:00:00 2001 From: yi-portainer Date: Fri, 30 Jul 2021 10:28:09 +1200 Subject: [PATCH 15/15] * update version to 2.6.2 --- api/http/handler/handler.go | 2 +- api/portainer.go | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 1cf65d947..b0900af09 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -67,7 +67,7 @@ type Handler struct { } // @title PortainerCE API -// @version 2.6.1 +// @version 2.6.2 // @description.markdown api-description.md // @termsOfService diff --git a/api/portainer.go b/api/portainer.go index 6d10dacea..e72879d0e 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1343,7 +1343,7 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "2.6.1" + APIVersion = "2.6.2" // DBVersion is the version number of the Portainer database DBVersion = 30 // ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax diff --git a/package.json b/package.json index 52879ab37..738ffa430 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "author": "Portainer.io", "name": "portainer", "homepage": "http://portainer.io", - "version": "2.6.1", + "version": "2.6.2", "repository": { "type": "git", "url": "git@github.com:portainer/portainer.git"