mirror of https://github.com/portainer/portainer
fix(kubernetes): clear user token from kube token cache on logout + update cluster rolebindings for user on change of team/user authorization [EE-6298] (#10598)
* clear user token from kube token cache on logoug + updates cluster role bindings for service accounts on change user/teams authorizationsrelease/2.19.2
parent
e761a00098
commit
e73b7fe0fd
|
@ -5,6 +5,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BucketName represents the name of the bucket where this service stores data.
|
// 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).
|
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||||
func (service *Service) GetNextIdentifier() int {
|
func (service *Service) GetNextIdentifier() int {
|
||||||
var identifier int
|
var identifier int
|
||||||
|
|
|
@ -122,6 +122,23 @@ func (service ServiceTx) Create(endpoint *portainer.Endpoint) error {
|
||||||
return nil
|
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).
|
// GetNextIdentifier returns the next identifier for an environment(endpoint).
|
||||||
func (service ServiceTx) GetNextIdentifier() int {
|
func (service ServiceTx) GetNextIdentifier() int {
|
||||||
return service.tx.GetNextIdentifier(BucketName)
|
return service.tx.GetNextIdentifier(BucketName)
|
||||||
|
|
|
@ -95,6 +95,7 @@ type (
|
||||||
EndpointService interface {
|
EndpointService interface {
|
||||||
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
Endpoint(ID portainer.EndpointID) (*portainer.Endpoint, error)
|
||||||
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
|
EndpointIDByEdgeID(edgeID string) (portainer.EndpointID, bool)
|
||||||
|
EndpointsByTeamID(teamID portainer.TeamID) ([]portainer.Endpoint, error)
|
||||||
Heartbeat(endpointID portainer.EndpointID) (int64, bool)
|
Heartbeat(endpointID portainer.EndpointID) (int64, bool)
|
||||||
UpdateHeartbeat(endpointID portainer.EndpointID)
|
UpdateHeartbeat(endpointID portainer.EndpointID)
|
||||||
Endpoints() ([]portainer.Endpoint, error)
|
Endpoints() ([]portainer.Endpoint, error)
|
||||||
|
|
|
@ -24,6 +24,7 @@ type Handler struct {
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||||
passwordStrengthChecker security.PasswordStrengthChecker
|
passwordStrengthChecker security.PasswordStrengthChecker
|
||||||
|
bouncer security.BouncerService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage authentication operations.
|
// NewHandler creates a handler to manage authentication operations.
|
||||||
|
@ -31,6 +32,7 @@ func NewHandler(bouncer security.BouncerService, rateLimiter *security.RateLimit
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
passwordStrengthChecker: passwordStrengthChecker,
|
passwordStrengthChecker: passwordStrengthChecker,
|
||||||
|
bouncer: bouncer,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.Handle("/auth/oauth/validate",
|
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)
|
rateLimiter.LimitAccess(bouncer.PublicAccess(httperror.LoggerHandler(h.authenticate)))).Methods(http.MethodPost)
|
||||||
h.Handle("/auth/logout",
|
h.Handle("/auth/logout",
|
||||||
bouncer.PublicAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
|
bouncer.PublicAccess(httperror.LoggerHandler(h.logout))).Methods(http.MethodPost)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,9 @@ package auth
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
"github.com/portainer/portainer/api/internal/logoutcontext"
|
"github.com/portainer/portainer/api/internal/logoutcontext"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @id Logout
|
// @id Logout
|
||||||
|
@ -21,10 +19,7 @@ import (
|
||||||
// @router /auth/logout [post]
|
// @router /auth/logout [post]
|
||||||
|
|
||||||
func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) logout(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
tokenData, err := security.RetrieveTokenData(r)
|
tokenData := handler.bouncer.JWTAuthLookup(r)
|
||||||
if err != nil {
|
|
||||||
log.Warn().Err(err).Msg("unable to retrieve user details from authentication token")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData != nil {
|
if tokenData != nil {
|
||||||
handler.KubernetesTokenCacheManager.RemoveUserFromCache(tokenData.ID)
|
handler.KubernetesTokenCacheManager.RemoveUserFromCache(tokenData.ID)
|
||||||
|
|
|
@ -3,9 +3,13 @@ package teammemberships
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
|
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
@ -13,7 +17,8 @@ import (
|
||||||
// Handler is the HTTP handler used to handle team membership operations.
|
// Handler is the HTTP handler used to handle team membership operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
DataStore dataservices.DataStore
|
DataStore dataservices.DataStore
|
||||||
|
K8sClientFactory *cli.ClientFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage team membership operations.
|
// NewHandler creates a handler to manage team membership operations.
|
||||||
|
@ -31,3 +36,27 @@ func NewHandler(bouncer security.BouncerService) *Handler {
|
||||||
|
|
||||||
return h
|
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)
|
return httperror.InternalServerError("Unable to persist team memberships inside the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer handler.updateUserServiceAccounts(membership)
|
||||||
|
|
||||||
return response.JSON(w, 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)
|
return httperror.InternalServerError("Unable to remove the team membership from the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer handler.updateUserServiceAccounts(membership)
|
||||||
|
|
||||||
return response.Empty(w)
|
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)
|
return httperror.InternalServerError("Unable to persist membership changes inside the database", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer handler.updateUserServiceAccounts(membership)
|
||||||
|
|
||||||
return response.JSON(w, membership)
|
return response.JSON(w, membership)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package kubernetes
|
package kubernetes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
const defaultServiceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||||
|
@ -43,28 +45,62 @@ func (manager *tokenManager) GetAdminServiceAccountToken() string {
|
||||||
return manager.adminToken
|
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
|
// 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) {
|
func (manager *tokenManager) GetUserServiceAccountToken(userID int, endpointID portainer.EndpointID) (string, error) {
|
||||||
tokenFunc := func() (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)
|
endpoint, err := manager.dataStore.Endpoint().Endpoint(endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed fetching environment %d", endpointID)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
restrictDefaultNamespace := endpoint.Kubernetes.Configuration.RestrictDefaultNamespace
|
if err := manager.setupUserServiceAccounts(portainer.UserID(userID), endpoint); err != nil {
|
||||||
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds, restrictDefaultNamespace)
|
return "", fmt.Errorf("failed setting-up service account for user %d: %w", userID, err)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return manager.kubecli.GetServiceAccountBearerToken(userID)
|
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])?`)
|
apiVersionRe := regexp.MustCompile(`^(/kubernetes)?/(api|apis/apps)/v[0-9](\.[0-9])?`)
|
||||||
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
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 {
|
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"):
|
case strings.EqualFold(requestPath, "/namespaces"):
|
||||||
return transport.executeKubernetesRequest(request)
|
return transport.executeKubernetesRequest(request)
|
||||||
case strings.HasPrefix(requestPath, "/namespaces"):
|
case strings.HasPrefix(requestPath, "/namespaces"):
|
||||||
|
|
|
@ -264,6 +264,7 @@ func (server *Server) Start() error {
|
||||||
|
|
||||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||||
teamMembershipHandler.DataStore = server.DataStore
|
teamMembershipHandler.DataStore = server.DataStore
|
||||||
|
teamMembershipHandler.K8sClientFactory = server.KubernetesClientFactory
|
||||||
|
|
||||||
var systemHandler = system.NewHandler(requestBouncer,
|
var systemHandler = system.NewHandler(requestBouncer,
|
||||||
server.Status,
|
server.Status,
|
||||||
|
|
|
@ -306,6 +306,19 @@ func (s *stubEndpointService) GetNextIdentifier() int {
|
||||||
return len(s.endpoints)
|
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)
|
// WithEndpoints option will instruct testDatastore to return provided environments(endpoints)
|
||||||
func WithEndpoints(endpoints []portainer.Endpoint) datastoreOption {
|
func WithEndpoints(endpoints []portainer.Endpoint) datastoreOption {
|
||||||
return func(d *testDatastore) {
|
return func(d *testDatastore) {
|
||||||
|
|
Loading…
Reference in New Issue