diff --git a/api/http/handler/kubernetes/handler.go b/api/http/handler/kubernetes/handler.go index ac53b8297..f35c9cc96 100644 --- a/api/http/handler/kubernetes/handler.go +++ b/api/http/handler/kubernetes/handler.go @@ -168,9 +168,15 @@ func (handler *Handler) kubeClientMiddleware(next http.Handler) http.Handler { isKubeAdmin := true nonAdminNamespaces := []string{} if user.Role != portainer.AdministratorRole { - nonAdminNamespaces, err = cli.GetNonAdminNamespaces(int(user.ID), endpoint, handler.KubernetesClientFactory) + pcli, err := handler.KubernetesClientFactory.GetPrivilegedKubeClient(endpoint) if err != nil { - httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the IsAdmin operation, unable to retrieve non-admin namespaces. Error: ", err) + httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the kubeClientMiddleware operation, unable to get privileged kube client to grab all namespaces. Error: ", err) + return + } + + nonAdminNamespaces, err = pcli.GetNonAdminNamespaces(int(user.ID), endpoint.Kubernetes.Configuration.RestrictDefaultNamespace) + if err != nil { + httperror.WriteError(w, http.StatusInternalServerError, "an error occurred during the kubeClientMiddleware operation, unable to retrieve non-admin namespaces. Error: ", err) return } isKubeAdmin = false diff --git a/api/kubernetes/cli/access.go b/api/kubernetes/cli/access.go index 34584f54b..2e37346cc 100644 --- a/api/kubernetes/cli/access.go +++ b/api/kubernetes/cli/access.go @@ -118,19 +118,14 @@ func (kcl *KubeClient) UpdateNamespaceAccessPolicies(accessPolicies map[string]p } // GetNonAdminNamespaces retrieves namespaces for a non-admin user, excluding the default namespace if restricted. -func GetNonAdminNamespaces(userID int, endpoint *portainer.Endpoint, clientFactory *ClientFactory) ([]string, error) { - kcl, err := clientFactory.GetPrivilegedKubeClient(endpoint) - if err != nil { - return nil, fmt.Errorf("an error occurred during the getNonAdminNamespaces operation, unable to get privileged kube client: %w", err) - } - +func (kcl *KubeClient) GetNonAdminNamespaces(userID int, isRestrictDefaultNamespace bool) ([]string, error) { accessPolicies, err := kcl.GetNamespaceAccessPolicies() if err != nil { return nil, fmt.Errorf("an error occurred during the getNonAdminNamespaces operation, unable to get namespace access policies via portainer-config. check if portainer-config configMap exists in the Kubernetes cluster: %w", err) } nonAdminNamespaces := []string{} - if !endpoint.Kubernetes.Configuration.RestrictDefaultNamespace { + if !isRestrictDefaultNamespace { nonAdminNamespaces = append(nonAdminNamespaces, defaultNamespace) } diff --git a/api/kubernetes/cli/namespace.go b/api/kubernetes/cli/namespace.go index f956c3721..ac1f45039 100644 --- a/api/kubernetes/cli/namespace.go +++ b/api/kubernetes/cli/namespace.go @@ -9,7 +9,6 @@ import ( "github.com/pkg/errors" portainer "github.com/portainer/portainer/api" - portaineree "github.com/portainer/portainer/api" models "github.com/portainer/portainer/api/http/models/kubernetes" "github.com/portainer/portainer/api/stacks/stackutils" httperror "github.com/portainer/portainer/pkg/libhttp/error" @@ -37,19 +36,15 @@ func defaultSystemNamespaces() map[string]struct{} { } // GetNamespaces gets the namespaces in the current k8s environment(endpoint). +// if the user is an admin, all namespaces in the current k8s environment(endpoint) are fetched using the fetchNamespaces function. +// otherwise, namespaces the non-admin user has access to will be used to filter the namespaces based on the allowed namespaces. func (kcl *KubeClient) GetNamespaces() (map[string]portainer.K8sNamespaceInfo, error) { if kcl.IsKubeAdmin { - return kcl.fetchNamespacesForAdmin() + return kcl.fetchNamespaces() } return kcl.fetchNamespacesForNonAdmin() } -// fetchNamespacesForAdmin gets the namespaces in the current k8s environment(endpoint) for the admin user. -// The kube client must have cluster scope read access to do this. -func (kcl *KubeClient) fetchNamespacesForAdmin() (map[string]portainer.K8sNamespaceInfo, error) { - return kcl.fetchNamespaces() -} - // fetchNamespacesForNonAdmin gets the namespaces in the current k8s environment(endpoint) for the non-admin user. func (kcl *KubeClient) fetchNamespacesForNonAdmin() (map[string]portainer.K8sNamespaceInfo, error) { log.Debug().Msgf("Fetching namespaces for non-admin user: %v", kcl.NonAdminNamespaces) @@ -63,7 +58,7 @@ func (kcl *KubeClient) fetchNamespacesForNonAdmin() (map[string]portainer.K8sNam return nil, fmt.Errorf("an error occurred during the fetchNamespacesForNonAdmin operation, unable to list namespaces for the non-admin user: %w", err) } - nonAdminNamespaceSet := kcl.BuildNonAdminNamespacesMap() + nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap() results := make(map[string]portainer.K8sNamespaceInfo) for _, namespace := range namespaces { if _, exists := nonAdminNamespaceSet[namespace.Name]; exists { @@ -259,7 +254,7 @@ func (kcl *KubeClient) DeleteNamespace(namespace string) error { } // CombineNamespacesWithResourceQuotas combines namespaces with resource quotas where matching is based on "portainer-rq-"+namespace.Name -func (kcl *KubeClient) CombineNamespacesWithResourceQuotas(namespaces map[string]portaineree.K8sNamespaceInfo, w http.ResponseWriter) (map[string]portainer.K8sNamespaceInfo, *httperror.HandlerError) { +func (kcl *KubeClient) CombineNamespacesWithResourceQuotas(namespaces map[string]portainer.K8sNamespaceInfo, w http.ResponseWriter) (map[string]portainer.K8sNamespaceInfo, *httperror.HandlerError) { resourceQuotas, err := kcl.GetResourceQuotas("") if err != nil { return nil, httperror.InternalServerError("an error occurred during the CombineNamespacesWithResourceQuotas operation, unable to retrieve resource quotas from the Kubernetes for an admin user. Error: ", err) @@ -273,7 +268,7 @@ func (kcl *KubeClient) CombineNamespacesWithResourceQuotas(namespaces map[string } // CombineNamespaceWithResourceQuota combines a namespace with a resource quota prefixed with "portainer-rq-"+namespace.Name -func (kcl *KubeClient) CombineNamespaceWithResourceQuota(namespace portaineree.K8sNamespaceInfo, w http.ResponseWriter) *httperror.HandlerError { +func (kcl *KubeClient) CombineNamespaceWithResourceQuota(namespace portainer.K8sNamespaceInfo, w http.ResponseWriter) *httperror.HandlerError { resourceQuota, err := kcl.GetPortainerResourceQuota(namespace.Name) if err != nil && !k8serrors.IsNotFound(err) { return httperror.InternalServerError(fmt.Sprintf("an error occurred during the CombineNamespaceWithResourceQuota operation, unable to retrieve the resource quota associated with the namespace: %s for a non-admin user. Error: ", namespace.Name), err) @@ -283,7 +278,9 @@ func (kcl *KubeClient) CombineNamespaceWithResourceQuota(namespace portaineree.K return response.JSON(w, namespace) } -func (kcl *KubeClient) BuildNonAdminNamespacesMap() map[string]struct{} { +// buildNonAdminNamespacesMap builds a map of non-admin namespaces. +// the map is used to filter the namespaces based on the allowed namespaces. +func (kcl *KubeClient) buildNonAdminNamespacesMap() map[string]struct{} { nonAdminNamespaceSet := make(map[string]struct{}, len(kcl.NonAdminNamespaces)) for _, namespace := range kcl.NonAdminNamespaces { nonAdminNamespaceSet[namespace] = struct{}{} @@ -291,3 +288,13 @@ func (kcl *KubeClient) BuildNonAdminNamespacesMap() map[string]struct{} { return nonAdminNamespaceSet } + +// ConvertNamespaceMapToSlice converts the namespace map to a slice of namespaces. +// this is used to for the API response. +func (kcl *KubeClient) ConvertNamespaceMapToSlice(namespaces map[string]portainer.K8sNamespaceInfo) []portainer.K8sNamespaceInfo { + namespaceSlice := make([]portainer.K8sNamespaceInfo, 0, len(namespaces)) + for _, namespace := range namespaces { + namespaceSlice = append(namespaceSlice, namespace) + } + return namespaceSlice +} diff --git a/api/kubernetes/cli/resource_quota.go b/api/kubernetes/cli/resource_quota.go index 019f89b52..e8659c3c5 100644 --- a/api/kubernetes/cli/resource_quota.go +++ b/api/kubernetes/cli/resource_quota.go @@ -11,20 +11,15 @@ import ( ) // GetResourceQuotas gets all resource quotas in the current k8s environment(endpoint). -// The kube client must have cluster scope read access to do this. +// if the user is an admin, all resource quotas in all namespaces are fetched. +// otherwise, namespaces the non-admin user has access to will be used to filter the resource quotas. func (kcl *KubeClient) GetResourceQuotas(namespace string) (*[]corev1.ResourceQuota, error) { if kcl.IsKubeAdmin { - return kcl.fetchResourceQuotasForAdmin(namespace) + return kcl.fetchResourceQuotas(namespace) } return kcl.fetchResourceQuotasForNonAdmin(namespace) } -// fetchResourceQuotasForAdmin gets the resource quotas in the current k8s environment(endpoint) for an admin user. -// The kube client must have cluster scope read access to do this. -func (kcl *KubeClient) fetchResourceQuotasForAdmin(namespace string) (*[]corev1.ResourceQuota, error) { - return kcl.fetchResourceQuotas(namespace) -} - // fetchResourceQuotasForNonAdmin gets the resource quotas in the current k8s environment(endpoint) for a non-admin user. // the role of the user must have read access to the resource quotas in the defined namespaces. func (kcl *KubeClient) fetchResourceQuotasForNonAdmin(namespace string) (*[]corev1.ResourceQuota, error) { @@ -39,7 +34,7 @@ func (kcl *KubeClient) fetchResourceQuotasForNonAdmin(namespace string) (*[]core return nil, err } - nonAdminNamespaceSet := kcl.BuildNonAdminNamespacesMap() + nonAdminNamespaceSet := kcl.buildNonAdminNamespacesMap() results := []corev1.ResourceQuota{} for _, resourceQuota := range *resourceQuotas { if _, exists := nonAdminNamespaceSet[resourceQuota.Namespace]; exists { @@ -59,8 +54,8 @@ func (kcl *KubeClient) fetchResourceQuotas(namespace string) (*[]corev1.Resource return &resourceQuotas.Items, nil } -// GetPortainerResourceQuota gets the resource quota for the portainer namespace. -// The resource quota is prefixed with "portainer-rq-". +// GetPortainerResourceQuota gets the resource quota prefixed with "portainer-rq-" in a specific namespace. +// this is to fetch a specific resource quota created by Portainer. func (kcl *KubeClient) GetPortainerResourceQuota(namespace string) (*corev1.ResourceQuota, error) { return kcl.cli.CoreV1().ResourceQuotas(namespace).Get(context.TODO(), "portainer-rq-"+namespace, metav1.GetOptions{}) } @@ -83,8 +78,7 @@ func (kcl *KubeClient) UpdateNamespacesWithResourceQuotas(namespaces map[string] return namespacesWithQuota } -// GetResourceQuotaFromNamespace updates the namespace.ResourceQuota field with the resource quota information. -// The resource quota is matched with the namespace and prefixed with "portainer-rq-". +// GetResourceQuotaFromNamespace gets the resource quota in a specific namespace where the resource quota's name is prefixed with "portainer-rq-". func (kcl *KubeClient) GetResourceQuotaFromNamespace(namespace portaineree.K8sNamespaceInfo, resourceQuotas []corev1.ResourceQuota) *corev1.ResourceQuota { for _, resourceQuota := range resourceQuotas { if resourceQuota.ObjectMeta.Namespace == namespace.Name && resourceQuota.ObjectMeta.Name == "portainer-rq-"+namespace.Name {