diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 99dfd9571..6788867f1 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -151,11 +151,17 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * } } + updateAuthorizations := false + if payload.Kubernetes != nil { + if payload.Kubernetes.Configuration.RestrictDefaultNamespace != + endpoint.Kubernetes.Configuration.RestrictDefaultNamespace { + updateAuthorizations = true + } + endpoint.Kubernetes = *payload.Kubernetes } - updateAuthorizations := false if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) { updateAuthorizations = true endpoint.UserAccessPolicies = payload.UserAccessPolicies diff --git a/api/http/proxy/factory/kubernetes/agent_transport.go b/api/http/proxy/factory/kubernetes/agent_transport.go index ea5af40a1..2973d46c9 100644 --- a/api/http/proxy/factory/kubernetes/agent_transport.go +++ b/api/http/proxy/factory/kubernetes/agent_transport.go @@ -34,7 +34,7 @@ func NewAgentTransport(signatureService portainer.DigitalSignatureService, tlsCo // 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) + token, err := transport.getRoundTripToken(request, transport.tokenManager) if err != nil { return nil, err } diff --git a/api/http/proxy/factory/kubernetes/edge_transport.go b/api/http/proxy/factory/kubernetes/edge_transport.go index 22178fa05..042982a4d 100644 --- a/api/http/proxy/factory/kubernetes/edge_transport.go +++ b/api/http/proxy/factory/kubernetes/edge_transport.go @@ -31,7 +31,7 @@ func NewEdgeTransport(reverseTunnelService portainer.ReverseTunnelService, endpo // 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) + token, err := transport.getRoundTripToken(request, transport.tokenManager) if err != nil { return nil, err } diff --git a/api/http/proxy/factory/kubernetes/token.go b/api/http/proxy/factory/kubernetes/token.go index bfcc145d8..909df1811 100644 --- a/api/http/proxy/factory/kubernetes/token.go +++ b/api/http/proxy/factory/kubernetes/token.go @@ -45,7 +45,7 @@ func (manager *tokenManager) getAdminServiceAccountToken() string { return manager.adminToken } -func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) { +func (manager *tokenManager) getUserServiceAccountToken(userID int, endpointID portainer.EndpointID) (string, error) { manager.mutex.Lock() defer manager.mutex.Unlock() @@ -61,7 +61,13 @@ func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, err teamIds = append(teamIds, int(membership.TeamID)) } - err = manager.kubecli.SetupUserServiceAccount(userID, teamIds) + endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID) + if err != nil { + return "", err + } + + restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace + err = manager.kubecli.SetupUserServiceAccount(userID, teamIds, restrictDefaultNamespace) if err != nil { return "", err } diff --git a/api/http/proxy/factory/kubernetes/transport.go b/api/http/proxy/factory/kubernetes/transport.go index b71ea906c..d87dea133 100644 --- a/api/http/proxy/factory/kubernetes/transport.go +++ b/api/http/proxy/factory/kubernetes/transport.go @@ -87,7 +87,7 @@ func (transport *baseTransport) executeKubernetesRequest(request *http.Request) // #region ROUND TRIP func (transport *baseTransport) prepareRoundTrip(request *http.Request) (string, error) { - token, err := getRoundTripToken(request, transport.tokenManager) + token, err := transport.getRoundTripToken(request, transport.tokenManager) if err != nil { return "", err } @@ -102,7 +102,7 @@ func (transport *baseTransport) RoundTrip(request *http.Request) (*http.Response return transport.proxyKubernetesRequest(request) } -func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) { +func (transport *baseTransport) getRoundTripToken(request *http.Request, tokenManager *tokenManager) (string, error) { tokenData, err := security.RetrieveTokenData(request) if err != nil { return "", err @@ -112,7 +112,7 @@ func getRoundTripToken(request *http.Request, tokenManager *tokenManager) (strin if tokenData.Role == portainer.AdministratorRole { token = tokenManager.getAdminServiceAccountToken() } else { - token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID)) + token, err = tokenManager.getUserServiceAccountToken(int(tokenData.ID), transport.endpoint.ID) if err != nil { log.Printf("Failed retrieving service account token: %v", err) return "", err diff --git a/api/kubernetes/cli/access.go b/api/kubernetes/cli/access.go index e8725292f..1e0712e0e 100644 --- a/api/kubernetes/cli/access.go +++ b/api/kubernetes/cli/access.go @@ -9,10 +9,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type ( - namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy -) - // NamespaceAccessPoliciesDeleteNamespace removes stored policies associated with a given namespace func (kcl *KubeClient) NamespaceAccessPoliciesDeleteNamespace(ns string) error { kcl.lock.Lock() @@ -48,18 +44,8 @@ func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNam return policies, nil } -func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error { - configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{}) - if k8serrors.IsNotFound(err) { - return nil - } else if err != nil { - return err - } - - accessData := configMap.Data[portainerConfigMapAccessPoliciesKey] - - var accessPolicies namespaceAccessPolicies - err = json.Unmarshal([]byte(accessData), &accessPolicies) +func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string, restrictDefaultNamespace bool) error { + accessPolicies, err := kcl.GetNamespaceAccessPolicies() if err != nil { return err } @@ -70,20 +56,16 @@ func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, service } for _, namespace := range namespaces.Items { - if namespace.Name == defaultNamespace { - continue - } - - policies, ok := accessPolicies[namespace.Name] - if !ok { - err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name) + if namespace.Name == defaultNamespace && !restrictDefaultNamespace { + err = kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace) if err != nil { return err } continue } - if !hasUserAccessToNamespace(userID, teamIDs, policies) { + policies, ok := accessPolicies[namespace.Name] + if !ok || !hasUserAccessToNamespace(userID, teamIDs, policies) { err = kcl.removeNamespaceAccessForServiceAccount(serviceAccountName, namespace.Name) if err != nil { return err diff --git a/api/kubernetes/cli/service_account.go b/api/kubernetes/cli/service_account.go index d8abc6f0f..c09d16141 100644 --- a/api/kubernetes/cli/service_account.go +++ b/api/kubernetes/cli/service_account.go @@ -17,7 +17,7 @@ func (kcl *KubeClient) GetServiceAccountBearerToken(userID int) (string, error) // SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes // cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user. //It will also create required default RoleBinding and ClusterRoleBinding rules. -func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error { +func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error { serviceAccountName := userServiceAccountName(userID, kcl.instanceID) err := kcl.ensureRequiredResourcesExist() @@ -25,20 +25,7 @@ func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error return err } - err = kcl.ensureServiceAccountForUserExists(serviceAccountName) - if err != nil { - return err - } - - return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName) -} - -func (kcl *KubeClient) ensureRequiredResourcesExist() error { - return kcl.createPortainerUserClusterRole() -} - -func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName string) error { - err := kcl.createUserServiceAccount(portainerNamespace, serviceAccountName) + err = kcl.createUserServiceAccount(portainerNamespace, serviceAccountName) if err != nil { return err } @@ -53,7 +40,11 @@ func (kcl *KubeClient) ensureServiceAccountForUserExists(serviceAccountName stri return err } - return kcl.ensureNamespaceAccessForServiceAccount(serviceAccountName, defaultNamespace) + return kcl.setupNamespaceAccesses(userID, teamIDs, serviceAccountName, restrictDefaultNamespace) +} + +func (kcl *KubeClient) ensureRequiredResourcesExist() error { + return kcl.createPortainerUserClusterRole() } func (kcl *KubeClient) createUserServiceAccount(namespace, serviceAccountName string) error { diff --git a/api/portainer.go b/api/portainer.go index 78201f4af..ba027d5c7 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -414,10 +414,11 @@ type ( // KubernetesConfiguration represents the configuration of a Kubernetes endpoint KubernetesConfiguration struct { - UseLoadBalancer bool `json:"UseLoadBalancer"` - UseServerMetrics bool `json:"UseServerMetrics"` - StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` - IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"` + UseLoadBalancer bool `json:"UseLoadBalancer"` + UseServerMetrics bool `json:"UseServerMetrics"` + StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"` + IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"` + RestrictDefaultNamespace bool `json:"RestrictDefaultNamespace"` } // KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration @@ -1170,7 +1171,7 @@ type ( // KubeClient represents a service used to query a Kubernetes environment KubeClient interface { - SetupUserServiceAccount(userID int, teamIDs []int) error + SetupUserServiceAccount(userID int, teamIDs []int, restrictDefaultNamespace bool) error GetServiceAccountBearerToken(userID int) (string, error) StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error NamespaceAccessPoliciesDeleteNamespace(namespace string) error diff --git a/api/swagger.yaml b/api/swagger.yaml index 64f2674bc..77513bf0a 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -1003,6 +1003,8 @@ definitions: type: boolean UseServerMetrics: type: boolean + RestrictDefaultNamespace: + type: boolean type: object portainer.KubernetesData: properties: diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js index 8023b8518..520b50a8a 100644 --- a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatable.js @@ -10,5 +10,6 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsDatatab reverseOrder: '<', removeAction: '<', refreshCallback: '<', + endpoint: '<', }, }); diff --git a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js index b3181bfe9..c81af255d 100644 --- a/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js +++ b/app/kubernetes/components/datatables/resource-pools-datatable/resourcePoolsDatatableController.js @@ -18,7 +18,11 @@ angular.module('portainer.docker').controller('KubernetesResourcePoolsDatatableC }; this.canManageAccess = function (item) { - return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item); + if (!this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace) { + return item.Namespace.Name !== 'default' && !this.isSystemNamespace(item); + } else { + return !this.isSystemNamespace(item); + } }; this.disableRemove = function (item) { diff --git a/app/kubernetes/views/configure/configure.html b/app/kubernetes/views/configure/configure.html index 98f86b0d1..7927aecf5 100644 --- a/app/kubernetes/views/configure/configure.html +++ b/app/kubernetes/views/configure/configure.html @@ -145,11 +145,7 @@ - - - - This feature is available in Portainer Business Edition. - + diff --git a/app/kubernetes/views/configure/configureController.js b/app/kubernetes/views/configure/configureController.js index 10184786c..8396caa47 100644 --- a/app/kubernetes/views/configure/configureController.js +++ b/app/kubernetes/views/configure/configureController.js @@ -107,6 +107,7 @@ class KubernetesConfigureController { endpoint.Kubernetes.Configuration.UseLoadBalancer = this.formValues.UseLoadBalancer; endpoint.Kubernetes.Configuration.UseServerMetrics = this.formValues.UseServerMetrics; endpoint.Kubernetes.Configuration.IngressClasses = ingressClasses; + endpoint.Kubernetes.Configuration.RestrictDefaultNamespace = this.formValues.RestrictDefaultNamespace; } transformFormValues() { @@ -259,6 +260,7 @@ class KubernetesConfigureController { UseLoadBalancer: false, UseServerMetrics: false, IngressClasses: [], + RestrictDefaultNamespace: false, }; try { @@ -281,6 +283,7 @@ class KubernetesConfigureController { this.formValues.UseLoadBalancer = this.endpoint.Kubernetes.Configuration.UseLoadBalancer; this.formValues.UseServerMetrics = this.endpoint.Kubernetes.Configuration.UseServerMetrics; + this.formValues.RestrictDefaultNamespace = this.endpoint.Kubernetes.Configuration.RestrictDefaultNamespace; this.formValues.IngressClasses = _.map(this.endpoint.Kubernetes.Configuration.IngressClasses, (ic) => { ic.IsNew = false; ic.NeedsDeletion = false; diff --git a/app/kubernetes/views/resource-pools/resourcePools.html b/app/kubernetes/views/resource-pools/resourcePools.html index c1ec578e4..71eee9399 100644 --- a/app/kubernetes/views/resource-pools/resourcePools.html +++ b/app/kubernetes/views/resource-pools/resourcePools.html @@ -13,6 +13,7 @@ order-by="Namespace.Name" remove-action="ctrl.removeAction" refresh-callback="ctrl.getResourcePools" + endpoint="ctrl.endpoint" > diff --git a/app/kubernetes/views/resource-pools/resourcePools.js b/app/kubernetes/views/resource-pools/resourcePools.js index 030e1e6eb..bf999a576 100644 --- a/app/kubernetes/views/resource-pools/resourcePools.js +++ b/app/kubernetes/views/resource-pools/resourcePools.js @@ -2,4 +2,7 @@ angular.module('portainer.kubernetes').component('kubernetesResourcePoolsView', templateUrl: './resourcePools.html', controller: 'KubernetesResourcePoolsController', controllerAs: 'ctrl', + bindings: { + endpoint: '<', + }, }); diff --git a/package.json b/package.json index aa00f0908..56c9858c6 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js", "dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt copy:assets && grunt start:client", "start:toolkit": "grunt start:toolkit", - "build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build -a --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer", + "build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer", "clean:all": "grunt clean:all", "format": "prettier --loglevel warn --write \"**/*.{js,css,html}\"", "lint": "yarn lint:client; yarn lint:server",