fix(k8s) cleaning up namespace access policies when removing users orteams from endpoint or endpoint group EE-718 (#5184)

* fix(k8s) cleaning up namespace access policies when removing users or teams from endpoint or endpoint group EE-718

* fix(k8s) minor code cleanup EE-718

Co-authored-by: Simon Meng <simon.meng@portainer.io>
pull/5203/head
cong meng 2021-06-16 20:15:29 +12:00 committed by GitHub
parent 2170ad49ef
commit 6b759438b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 241 additions and 8 deletions

View File

@ -20,6 +20,7 @@ import (
"github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/http/client"
"github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy"
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge" "github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/snapshot" "github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/jwt" "github.com/portainer/portainer/api/jwt"
@ -389,6 +390,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
} }
snapshotService.Start() snapshotService.Start()
authorizationService := authorization.NewService(dataStore)
authorizationService.K8sClientFactory = kubernetesClientFactory
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService) swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
if err != nil { if err != nil {
log.Fatalf("failed initializing swarm stack manager: %v", err) log.Fatalf("failed initializing swarm stack manager: %v", err)
@ -461,6 +465,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
} }
return &http.Server{ return &http.Server{
AuthorizationService: authorizationService,
ReverseTunnelService: reverseTunnelService, ReverseTunnelService: reverseTunnelService,
Status: applicationStatus, Status: applicationStatus,
BindAddress: *flags.Addr, BindAddress: *flags.Addr,

View File

@ -109,12 +109,33 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
} }
} }
updateAuthorizations := false
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.UserAccessPolicies) { if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.UserAccessPolicies) {
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
updateAuthorizations = true
} }
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) { if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) {
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
updateAuthorizations = true
}
if updateAuthorizations {
endpoints, err := handler.DataStore.Endpoint().Endpoints()
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
}
for _, endpoint := range endpoints {
if endpoint.GroupID == endpointGroup.ID {
if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
err = handler.AuthorizationService.CleanNAPWithOverridePolicies(&endpoint, endpointGroup)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}
}
}
} }
err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup) err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)

View File

@ -1,6 +1,7 @@
package endpointgroups package endpointgroups
import ( import (
"github.com/portainer/portainer/api/internal/authorization"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -12,6 +13,7 @@ import (
// Handler is the HTTP handler used to handle endpoint group operations. // Handler is the HTTP handler used to handle endpoint group operations.
type Handler struct { type Handler struct {
*mux.Router *mux.Router
AuthorizationService *authorization.Service
DataStore portainer.DataStore DataStore portainer.DataStore
} }

View File

@ -155,11 +155,14 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
endpoint.Kubernetes = *payload.Kubernetes endpoint.Kubernetes = *payload.Kubernetes
} }
updateAuthorizations := false
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) { if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
updateAuthorizations = true
endpoint.UserAccessPolicies = payload.UserAccessPolicies endpoint.UserAccessPolicies = payload.UserAccessPolicies
} }
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) { if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) {
updateAuthorizations = true
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
} }
@ -252,6 +255,15 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
} }
} }
if updateAuthorizations {
if endpoint.Type == portainer.KubernetesLocalEnvironment || endpoint.Type == portainer.AgentOnKubernetesEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment {
err = handler.AuthorizationService.CleanNAPWithOverridePolicies(endpoint, nil)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
}
}
}
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil { if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}

View File

@ -5,6 +5,7 @@ import (
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"net/http" "net/http"
@ -28,6 +29,7 @@ type Handler struct {
ReverseTunnelService portainer.ReverseTunnelService ReverseTunnelService portainer.ReverseTunnelService
SnapshotService portainer.SnapshotService SnapshotService portainer.SnapshotService
ComposeStackManager portainer.ComposeStackManager ComposeStackManager portainer.ComposeStackManager
AuthorizationService *authorization.Service
} }
// NewHandler creates a handler to manage endpoint operations. // NewHandler creates a handler to manage endpoint operations.

View File

@ -45,11 +45,13 @@ import (
"github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/http/security" "github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/kubernetes/cli" "github.com/portainer/portainer/api/kubernetes/cli"
) )
// Server implements the portainer.Server interface // Server implements the portainer.Server interface
type Server struct { type Server struct {
AuthorizationService *authorization.Service
BindAddress string BindAddress string
AssetsPath string AssetsPath string
Status *portainer.Status Status *portainer.Status
@ -135,6 +137,7 @@ func (server *Server) Start() error {
endpointHandler.SnapshotService = server.SnapshotService endpointHandler.SnapshotService = server.SnapshotService
endpointHandler.ReverseTunnelService = server.ReverseTunnelService endpointHandler.ReverseTunnelService = server.ReverseTunnelService
endpointHandler.ComposeStackManager = server.ComposeStackManager endpointHandler.ComposeStackManager = server.ComposeStackManager
endpointHandler.AuthorizationService = server.AuthorizationService
var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer) var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer)
endpointEdgeHandler.DataStore = server.DataStore endpointEdgeHandler.DataStore = server.DataStore
@ -142,6 +145,7 @@ func (server *Server) Start() error {
endpointEdgeHandler.ReverseTunnelService = server.ReverseTunnelService endpointEdgeHandler.ReverseTunnelService = server.ReverseTunnelService
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer) var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
endpointGroupHandler.AuthorizationService = server.AuthorizationService
endpointGroupHandler.DataStore = server.DataStore endpointGroupHandler.DataStore = server.DataStore
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer) var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)

View File

@ -1,11 +1,15 @@
package authorization package authorization
import "github.com/portainer/portainer/api" import (
"github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/kubernetes/cli"
)
// Service represents a service used to // Service represents a service used to
// update authorizations associated to a user or team. // update authorizations associated to a user or team.
type Service struct { type Service struct {
dataStore portainer.DataStore dataStore portainer.DataStore
K8sClientFactory *cli.ClientFactory
} }
// NewService returns a point to a new Service instance. // NewService returns a point to a new Service instance.

View File

@ -0,0 +1,134 @@
package authorization
import portainer "github.com/portainer/portainer/api"
// CleanNAPWithOverridePolicies Clean Namespace Access Policies with override policies
func (service *Service) CleanNAPWithOverridePolicies(
endpoint *portainer.Endpoint,
endpointGroup *portainer.EndpointGroup,
) error {
kubecli, err := service.K8sClientFactory.GetKubeClient(endpoint)
if err != nil {
return err
}
accessPolicies, err := kubecli.GetNamespaceAccessPolicies()
if err != nil {
return err
}
hasChange := false
for namespace, policy := range accessPolicies {
for teamID := range policy.TeamAccessPolicies {
access, err := service.getTeamEndpointAccessWithPolicies(teamID, endpoint, endpointGroup)
if err != nil {
return err
}
if !access {
delete(accessPolicies[namespace].TeamAccessPolicies, teamID)
hasChange = true
}
}
for userID := range policy.UserAccessPolicies {
access, err := service.getUserEndpointAccessWithPolicies(userID, endpoint, endpointGroup)
if err != nil {
return err
}
if !access {
delete(accessPolicies[namespace].UserAccessPolicies, userID)
hasChange = true
}
}
}
if hasChange {
err = kubecli.UpdateNamespaceAccessPolicies(accessPolicies)
if err != nil {
return err
}
}
return nil
}
func (service *Service) getUserEndpointAccessWithPolicies(
userID portainer.UserID,
endpoint *portainer.Endpoint,
endpointGroup *portainer.EndpointGroup,
) (bool, error) {
memberships, err := service.dataStore.TeamMembership().TeamMembershipsByUserID(userID)
if err != nil {
return false, err
}
if endpointGroup == nil {
endpointGroup, err = service.dataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
if err != nil {
return false, err
}
}
if userAccess(userID, endpoint.UserAccessPolicies, endpoint.TeamAccessPolicies, memberships) {
return true, nil
}
if userAccess(userID, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies, memberships) {
return true, nil
}
return false, nil
}
func userAccess(
userID portainer.UserID,
userAccessPolicies portainer.UserAccessPolicies,
teamAccessPolicies portainer.TeamAccessPolicies,
memberships []portainer.TeamMembership,
) bool {
if _, ok := userAccessPolicies[userID]; ok {
return true
}
for _, membership := range memberships {
if _, ok := teamAccessPolicies[membership.TeamID]; ok {
return true
}
}
return false
}
func (service *Service) getTeamEndpointAccessWithPolicies(
teamID portainer.TeamID,
endpoint *portainer.Endpoint,
endpointGroup *portainer.EndpointGroup,
) (bool, error) {
if endpointGroup == nil {
var err error
endpointGroup, err = service.dataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
if err != nil {
return false, err
}
}
if teamAccess(teamID, endpoint.TeamAccessPolicies) {
return true, nil
}
if teamAccess(teamID, endpointGroup.TeamAccessPolicies) {
return true, nil
}
return false, nil
}
func teamAccess(
teamID portainer.TeamID,
teamAccessPolicies portainer.TeamAccessPolicies,
) bool {
_, ok := teamAccessPolicies[teamID];
return ok
}

View File

@ -9,12 +9,7 @@ import (
) )
type ( type (
accessPolicies struct { namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy
UserAccessPolicies portainer.UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies portainer.TeamAccessPolicies `json:"TeamAccessPolicies"`
}
namespaceAccessPolicies map[string]accessPolicies
) )
func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error { func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, serviceAccountName string) error {
@ -69,7 +64,7 @@ func (kcl *KubeClient) setupNamespaceAccesses(userID int, teamIDs []int, service
return nil return nil
} }
func hasUserAccessToNamespace(userID int, teamIDs []int, policies accessPolicies) bool { func hasUserAccessToNamespace(userID int, teamIDs []int, policies portainer.K8sNamespaceAccessPolicy) bool {
_, userAccess := policies.UserAccessPolicies[portainer.UserID(userID)] _, userAccess := policies.UserAccessPolicies[portainer.UserID(userID)]
if userAccess { if userAccess {
return true return true
@ -84,3 +79,50 @@ func hasUserAccessToNamespace(userID int, teamIDs []int, policies accessPolicies
return false return false
} }
// GetNamespaceAccessPolicies gets the namespace access policies
// from config maps in the portainer namespace
func (kcl *KubeClient) GetNamespaceAccessPolicies() (map[string]portainer.K8sNamespaceAccessPolicy, error) {
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
return nil, nil
}
if err != nil {
return nil, err
}
accessData := configMap.Data[portainerConfigMapAccessPoliciesKey]
var policies map[string]portainer.K8sNamespaceAccessPolicy
err = json.Unmarshal([]byte(accessData), &policies)
if err != nil {
return nil, err
}
return policies, nil
}
// UpdateNamespaceAccessPolicies updates the namespace access policies
func (kcl *KubeClient) UpdateNamespaceAccessPolicies(accessPolicies map[string]portainer.K8sNamespaceAccessPolicy) error {
data, err := json.Marshal(accessPolicies)
if err != nil {
return err
}
configMap, err := kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Get(portainerConfigMapName, metav1.GetOptions{})
if k8serrors.IsNotFound(err) {
return nil
}
if err != nil {
return err
}
configMap.Data[portainerConfigMapAccessPoliciesKey] = string(data)
_, err = kcl.cli.CoreV1().ConfigMaps(portainerNamespace).Update(configMap)
if err != nil {
return err
}
return nil
}

View File

@ -392,6 +392,11 @@ type (
// JobType represents a job type // JobType represents a job type
JobType int JobType int
K8sNamespaceAccessPolicy struct {
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
}
// KubernetesData contains all the Kubernetes related endpoint information // KubernetesData contains all the Kubernetes related endpoint information
KubernetesData struct { KubernetesData struct {
Snapshots []KubernetesSnapshot `json:"Snapshots"` Snapshots []KubernetesSnapshot `json:"Snapshots"`
@ -1160,6 +1165,8 @@ type (
SetupUserServiceAccount(userID int, teamIDs []int) error SetupUserServiceAccount(userID int, teamIDs []int) error
GetServiceAccountBearerToken(userID int) (string, error) GetServiceAccountBearerToken(userID int) (string, error)
StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
GetNamespaceAccessPolicies() (map[string]K8sNamespaceAccessPolicy, error)
UpdateNamespaceAccessPolicies(accessPolicies map[string]K8sNamespaceAccessPolicy) error
} }
// KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint // KubernetesDeployer represents a service to deploy a manifest inside a Kubernetes endpoint