Compare commits

...

6 Commits

Author SHA1 Message Date
andres-portainer
e95cc08f54 Change the condition.
Some checks are pending
Test / test-server (push) Waiting to run
Test / test-client (push) Waiting to run
2023-11-20 14:47:17 -03:00
andres-portainer
27df5440e4 fix(gitops): handle the local environment in isEnvironmentOnline() EE-6321 2023-11-14 18:16:17 -03:00
yi-portainer
c5a51a9fb7 * remove line break 2023-11-13 14:17:00 +13:00
Prabhat Khera
280a2fe093 fix(kubernetes): clear user token from kube token cache on logout + update cluster rolebindings for user on change of team/user authorization [EE-6298] (#10603)
Some checks are pending
Test / test-client (push) Waiting to run
Test / test-server (push) Waiting to run
2023-11-10 10:06:50 +13:00
Prabhat Khera
ddd30dd17a fix(app): disable deploy when there are no namespaces [EE-6295] (#10608)
* fix(app): hide services section when there are no namespaces [EE-6295] (#10588)

Co-authored-by: testa113 <testa113>

* fix(app): disable deploy when there are no namespaces [EE-6295] (#10606)

Co-authored-by: testa113 <testa113>

---------

Co-authored-by: Ali <83188384+testA113@users.noreply.github.com>
2023-11-09 20:02:02 +13:00
Chaim Lev-Ari
15df3277ca fix(edge/updates): hide sidebar item when disabled [EE-6294] (#10581) 2023-11-05 13:41:16 +02:00
16 changed files with 195 additions and 50 deletions

View File

@@ -5,6 +5,7 @@ import (
"time"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
)
// BucketName represents the name of the bucket where this service stores data.
@@ -144,6 +145,23 @@ func (service *Service) Create(endpoint *portainer.Endpoint) error {
})
}
func (service *Service) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
return endpoints, service.connection.GetAll(
BucketName,
&portainer.Endpoint{},
dataservices.FilterFn(&endpoints, func(e portainer.Endpoint) bool {
for t := range e.TeamAccessPolicies {
if t == teamID {
return true
}
}
return false
}),
)
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service *Service) GetNextIdentifier() int {
var identifier int

View File

@@ -122,6 +122,23 @@ func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
return nil
}
func (service ServiceTx) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
return endpoints, service.tx.GetAll(
BucketName,
&portainer.Endpoint{},
dataservices.FilterFn(&endpoints, func(e portainer.Endpoint) bool {
for t := range e.TeamAccessPolicies {
if t == teamID {
return true
}
}
return false
}),
)
}
// GetNextIdentifier returns the next identifier for an environment(endpoint).
func (service ServiceTx) GetNextIdentifier() int {
return service.tx.GetNextIdentifier(BucketName)

View File

@@ -89,6 +89,7 @@ type (
EndpointService interface {
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error)
Heartbeat(endpointID portainer.EndpointID) (int64, bool)
UpdateHeartbeat(endpointID portainer.EndpointID)
Endpoints() ([]portainer.Endpoint, error)

View File

@@ -24,6 +24,7 @@ type Handler struct {
ProxyManager *proxy.Manager
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
passwordStrengthChecker security.PasswordStrengthChecker
bouncer security.BouncerService
}
// NewHandler creates a handler to manage authentication operations.
@@ -31,6 +32,7 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
h := &Handler{
Router: mux.NewRouter(),
passwordStrengthChecker: passwordStrengthChecker,
bouncer: bouncer,
}
h.Handle("/auth/oauth/validate",
@@ -39,6 +41,5 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
h.Handle("/auth/logout",
bouncer.PublicAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
return h
}

View File

@@ -5,9 +5,7 @@ import (
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/response"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/logoutcontext"
"github.com/rs/zerolog/log"
)
// @id Logout
@@ -19,12 +17,8 @@ import (
// @success 204 "Success"
// @failure 500 "Server error"
// @router /auth/logout [post]
func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
tokenData, err := security.RetrieveTokenData(r)
if err != nil {
log.Warn().Err(err).Msg("unable to retrieve user details from authentication token")
}
tokenData := handler.bouncer.JWTAuthLookup(r)
if tokenData != nil {
handler.KubernetesTokenCacheManager.RemoveUserFromCache(tokenData.ID)

View File

@@ -4,8 +4,12 @@ import (
"net/http"
httperror "github.com/portainer/libhttp/error"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/kubernetes/cli"
"github.com/rs/zerolog/log"
"github.com/gorilla/mux"
)
@@ -13,7 +17,8 @@ import (
// Handler is the HTTP handler used to handle team membership operations.
type Handler struct {
*mux.Router
DataStore dataservices.DataStore
DataStore dataservices.DataStore
K8sClientFactory *cli.ClientFactory
}
// NewHandler creates a handler to manage team membership operations.
@@ -31,3 +36,27 @@ func NewHandler(bouncer security.BouncerService) *Handler {
return h
}
func (handler *Handler) updateUserServiceAccounts(membership *portainer.TeamMembership) {
endpoints, err := handler.DataStore.Endpoint().EndpointsByTeamID(membership.TeamID)
if err != nil {
log.Error().Err(err).Msgf("failed fetching environments for team %d", membership.TeamID)
return
}
for _, endpoint := range endpoints {
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
// update kubernenets service accounts if the team is associated with a kubernetes environment
if endpointutils.IsKubernetesEndpoint(&endpoint) {
kubecli, err := handler.K8sClientFactory.GetKubeClient(&endpoint)
if err != nil {
log.Error().Err(err).Msgf("failed getting kube client for environment %d", endpoint.ID)
continue
}
teamIDs := []int{int(membership.TeamID)}
err = kubecli.SetupUserServiceAccount(int(membership.UserID), teamIDs, restrictDefaultNamespace)
if err != nil {
log.Error().Err(err).Msgf("failed setting-up service account for user %d", membership.UserID)
}
}
}
}

View File

@@ -91,5 +91,7 @@ func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Requ
return httperror.InternalServerError("Unable to persist team memberships inside the database", err)
}
defer handler.updateUserServiceAccounts(membership)
return response.JSON(w, membership)
}

View File

@@ -52,5 +52,7 @@ func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Requ
return httperror.InternalServerError("Unable to remove the team membership from the database", err)
}
defer handler.updateUserServiceAccounts(membership)
return response.Empty(w)
}

View File

@@ -90,5 +90,7 @@ func (handler *Handler) teamMembershipUpdate(w http.ResponseWriter, r *http.Requ
return httperror.InternalServerError("Unable to persist membership changes inside the database", err)
}
defer handler.updateUserServiceAccounts(membership)
return response.JSON(w, membership)
}

View File

@@ -1,10 +1,12 @@
package kubernetes
import (
"fmt"
"os"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/rs/zerolog/log"
)
const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
@@ -43,28 +45,62 @@ func (manager *tokenManager) GetAdminServiceAccountToken() string {
return manager.adminToken
}
func (manager *tokenManager) setupUserServiceAccounts(userID portainer.UserID, endpoint *portainer.Endpoint) error {
memberships, err := manager.dataStore.TeamMembership().TeamMembershipsByUserID(userID)
if err != nil {
return err
}
teamIds := make([]int, 0, len(memberships))
for _, membership := range memberships {
teamIds = append(teamIds, int(membership.TeamID))
}
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
err = manager.kubecli.SetupUserServiceAccount(int(userID), teamIds, restrictDefaultNamespace)
if err != nil {
return err
}
return nil
}
func (manager *tokenManager) UpdateUserServiceAccountsForEndpoint(endpointID portainer.EndpointID) {
endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
log.Error().Err(err).Msgf("failed fetching environments %d", endpointID)
return
}
userIDs := make([]portainer.UserID, 0)
for u := range endpoint.UserAccessPolicies {
userIDs = append(userIDs, u)
}
for t := range endpoint.TeamAccessPolicies {
memberships, _ := manager.dataStore.TeamMembership().TeamMembershipsByTeamID(portainer.TeamID(t))
for _, membership := range memberships {
userIDs = append(userIDs, membership.UserID)
}
}
for _, userID := range userIDs {
if err := manager.setupUserServiceAccounts(userID, endpoint); err != nil {
log.Error().Err(err).Msgf("failed setting-up service account for user %d", userID)
}
}
}
// GetUserServiceAccountToken setup a user's service account if it does not exist, then retrieve its token
func (manager *tokenManager) GetUserServiceAccountToken(userID int, endpointID portainer.EndpointID) (string, error) {
tokenFunc := func() (string, error) {
memberships, err := manager.dataStore.TeamMembership().TeamMembershipsByUserID(portainer.UserID(userID))
if err != nil {
return "", err
}
teamIds := make([]int, 0, len(memberships))
for _, membership := range memberships {
teamIds = append(teamIds, int(membership.TeamID))
}
endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID)
if err != nil {
log.Error().Err(err).Msgf("failed fetching environment %d", endpointID)
return "", err
}
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds, restrictDefaultNamespace)
if err != nil {
return "", err
if err := manager.setupUserServiceAccounts(portainer.UserID(userID), endpoint); err != nil {
return "", fmt.Errorf("failed setting-up service account for user %d: %w", userID, err)
}
return manager.kubecli.GetServiceAccountBearerToken(userID)

View File

@@ -49,7 +49,17 @@ func (transport *baseTransport) proxyKubernetesRequest(request *http.Request) (*
apiVersionRe := regexp.MustCompile(`^(/kubernetes)?/(api|apis/apps)/v[0-9](\.[0-9])?`)
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
endpointRe := regexp.MustCompile(`([0-9]+)`)
endpointIDMatch := endpointRe.FindAllString(request.RequestURI, 1)
endpointID := 0
if len(endpointIDMatch) > 0 {
endpointID, _ = strconv.Atoi(endpointIDMatch[0])
}
switch {
case strings.EqualFold(requestPath, "/namespaces/portainer/configmaps/portainer-config") && (request.Method == "PUT" || request.Method == "POST"):
defer transport.tokenManager.UpdateUserServiceAccountsForEndpoint(portainer.EndpointID(endpointID))
return transport.executeKubernetesRequest(request)
case strings.EqualFold(requestPath, "/namespaces"):
return transport.executeKubernetesRequest(request)
case strings.HasPrefix(requestPath, "/namespaces"):

View File

@@ -259,6 +259,7 @@ func (server *Server) Start() error {
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
teamMembershipHandler.DataStore = server.DataStore
teamMembershipHandler.K8sClientFactory = server.KubernetesClientFactory
var systemHandler = system.NewHandler(requestBouncer,
server.Status,

View File

@@ -301,6 +301,19 @@ func (s *stubEndpointService) GetNextIdentifier() int {
return len(s.endpoints)
}
func (s *stubEndpointService) EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error) {
var endpoints = make([]portainer.Endpoint, 0)
for _, e := range s.endpoints {
for t := range e.TeamAccessPolicies {
if t == teamID {
endpoints = append(endpoints, e)
}
}
}
return endpoints, nil
}
// WithEndpoints option will instruct testDatastore to return provided environments(endpoints)
func WithEndpoints(endpoints []portainer.Endpoint) datastoreOption {
return func(d *testDatastore) {

View File

@@ -171,6 +171,11 @@ func getUserRegistries(datastore dataservices.DataStore, user *portainer.User, e
}
func isEnvironmentOnline(endpoint *portainer.Endpoint) bool {
if endpoint.Type != portainer.AgentOnDockerEnvironment &&
endpoint.Type != portainer.AgentOnKubernetesEnvironment {
return true
}
var err error
var tlsConfig *tls.Config
if endpoint.TLSConfig.TLS {

View File

@@ -1302,16 +1302,18 @@
</div>
<!-- kubernetes services options -->
<kube-services-form
on-change="(ctrl.onServicesChange)"
values="ctrl.formValues.Services"
load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
app-name="ctrl.formValues.Name"
selector="ctrl.formValues.Selector"
validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services, ingressPaths: ctrl.ingressPaths, originalIngressPaths: ctrl.originalIngressPaths}"
is-edit-mode="ctrl.state.isEdit"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></kube-services-form>
<div ng-if="ctrl.formValues.ResourcePool">
<kube-services-form
on-change="(ctrl.onServicesChange)"
values="ctrl.formValues.Services"
load-balancer-enabled="ctrl.publishViaLoadBalancerEnabled()"
app-name="ctrl.formValues.Name"
selector="ctrl.formValues.Selector"
validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services, ingressPaths: ctrl.ingressPaths, originalIngressPaths: ctrl.originalIngressPaths}"
is-edit-mode="ctrl.state.isEdit"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></kube-services-form>
</div>
<!-- kubernetes services options -->
<!-- summary -->
@@ -1353,15 +1355,17 @@
</div>
</div>
<!-- kubernetes services options -->
<kube-services-form
on-change="(ctrl.onServicesChange)"
values="ctrl.formValues.Services"
app-name="ctrl.formValues.Name"
selector="ctrl.formValues.Selector"
validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services, ingressPaths: ctrl.ingressPaths, originalIngressPaths: ctrl.originalIngressPaths}"
is-edit-mode="ctrl.state.isEdit"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></kube-services-form>
<div ng-if="ctrl.formValues.ResourcePool">
<kube-services-form
on-change="(ctrl.onServicesChange)"
values="ctrl.formValues.Services"
app-name="ctrl.formValues.Name"
selector="ctrl.formValues.Selector"
validation-data="{nodePortServices: ctrl.state.nodePortServices, formServices: ctrl.formValues.Services, ingressPaths: ctrl.ingressPaths, originalIngressPaths: ctrl.originalIngressPaths}"
is-edit-mode="ctrl.state.isEdit"
namespace="ctrl.formValues.ResourcePool.Namespace.Name"
></kube-services-form>
</div>
<!-- kubernetes services options -->
</div>
@@ -1376,7 +1380,7 @@
ng-if="ctrl.state.appType === ctrl.KubernetesDeploymentTypes.APPLICATION_FORM"
type="button"
class="btn btn-primary btn-sm !ml-0"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid() || ctrl.hasPortErrors()"
ng-disabled="!kubernetesApplicationCreationForm.$valid || ctrl.isDeployUpdateButtonDisabled() || !ctrl.imageValidityIsValid() || ctrl.hasPortErrors() || !ctrl.formValues.ResourcePool"
ng-click="ctrl.deployApplication()"
button-spinner="ctrl.state.actionInProgress"
data-cy="k8sAppCreate-deployButton"

View File

@@ -70,13 +70,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
label="Tags"
data-cy="portainerSidebar-environmentTags"
/>
{isBE && (
<SidebarItem
to="portainer.endpoints.updateSchedules"
label="Update & Rollback"
data-cy="portainerSidebar-updateSchedules"
/>
)}
<EdgeUpdatesSidebarItem />
</SidebarItem>
<SidebarItem
@@ -161,3 +155,19 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
</SidebarSection>
);
}
function EdgeUpdatesSidebarItem() {
const settingsQuery = usePublicSettings();
if (!isBE || !settingsQuery.data?.EnableEdgeComputeFeatures) {
return null;
}
return (
<SidebarItem
to="portainer.endpoints.updateSchedules"
label="Update & Rollback"
data-cy="portainerSidebar-updateSchedules"
/>
);
}