Compare commits
6 Commits
fix/EE-641
...
fix/EE-632
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e95cc08f54 | ||
|
|
27df5440e4 | ||
|
|
c5a51a9fb7 | ||
|
|
280a2fe093 | ||
|
|
ddd30dd17a | ||
|
|
15df3277ca |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user