diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 83b1fb3c3..a852abd56 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -20,6 +20,7 @@ import ( "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/http/proxy" 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/snapshot" "github.com/portainer/portainer/api/jwt" @@ -389,6 +390,9 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { } snapshotService.Start() + authorizationService := authorization.NewService(dataStore) + authorizationService.K8sClientFactory = kubernetesClientFactory + swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService) if err != nil { log.Fatalf("failed initializing swarm stack manager: %v", err) @@ -461,6 +465,7 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { } return &http.Server{ + AuthorizationService: authorizationService, ReverseTunnelService: reverseTunnelService, Status: applicationStatus, BindAddress: *flags.Addr, diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index 280c33494..7d8c284f4 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -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) { endpointGroup.UserAccessPolicies = payload.UserAccessPolicies + updateAuthorizations = true } if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.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) diff --git a/api/http/handler/endpointgroups/handler.go b/api/http/handler/endpointgroups/handler.go index e73828814..08760bd66 100644 --- a/api/http/handler/endpointgroups/handler.go +++ b/api/http/handler/endpointgroups/handler.go @@ -1,6 +1,7 @@ package endpointgroups import ( + "github.com/portainer/portainer/api/internal/authorization" "net/http" "github.com/gorilla/mux" @@ -12,6 +13,7 @@ import ( // Handler is the HTTP handler used to handle endpoint group operations. type Handler struct { *mux.Router + AuthorizationService *authorization.Service DataStore portainer.DataStore } diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 8b0bb26f6..99dfd9571 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -155,11 +155,14 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * endpoint.Kubernetes = *payload.Kubernetes } + updateAuthorizations := false if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) { + updateAuthorizations = true endpoint.UserAccessPolicies = payload.UserAccessPolicies } if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) { + updateAuthorizations = true 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) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index e7a41b8c8..cd82092e7 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -5,6 +5,7 @@ import ( portainer "github.com/portainer/portainer/api" "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" "net/http" @@ -28,6 +29,7 @@ type Handler struct { ReverseTunnelService portainer.ReverseTunnelService SnapshotService portainer.SnapshotService ComposeStackManager portainer.ComposeStackManager + AuthorizationService *authorization.Service } // NewHandler creates a handler to manage endpoint operations. diff --git a/api/http/server.go b/api/http/server.go index 4e0e8c775..8f399e7a9 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -45,11 +45,13 @@ import ( "github.com/portainer/portainer/api/http/proxy" "github.com/portainer/portainer/api/http/proxy/factory/kubernetes" "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/authorization" "github.com/portainer/portainer/api/kubernetes/cli" ) // Server implements the portainer.Server interface type Server struct { + AuthorizationService *authorization.Service BindAddress string AssetsPath string Status *portainer.Status @@ -135,6 +137,7 @@ func (server *Server) Start() error { endpointHandler.SnapshotService = server.SnapshotService endpointHandler.ReverseTunnelService = server.ReverseTunnelService endpointHandler.ComposeStackManager = server.ComposeStackManager + endpointHandler.AuthorizationService = server.AuthorizationService var endpointEdgeHandler = endpointedge.NewHandler(requestBouncer) endpointEdgeHandler.DataStore = server.DataStore @@ -142,6 +145,7 @@ func (server *Server) Start() error { endpointEdgeHandler.ReverseTunnelService = server.ReverseTunnelService var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer) + endpointGroupHandler.AuthorizationService = server.AuthorizationService endpointGroupHandler.DataStore = server.DataStore var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer) diff --git a/api/internal/authorization/authorizations.go b/api/internal/authorization/authorizations.go index 31916cd31..805c2e38f 100644 --- a/api/internal/authorization/authorizations.go +++ b/api/internal/authorization/authorizations.go @@ -1,11 +1,15 @@ 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 // update authorizations associated to a user or team. type Service struct { dataStore portainer.DataStore + K8sClientFactory *cli.ClientFactory } // NewService returns a point to a new Service instance. diff --git a/api/internal/authorization/endpint_role_with_override.go b/api/internal/authorization/endpint_role_with_override.go new file mode 100644 index 000000000..08d2a5b15 --- /dev/null +++ b/api/internal/authorization/endpint_role_with_override.go @@ -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 +} \ No newline at end of file diff --git a/api/kubernetes/cli/access.go b/api/kubernetes/cli/access.go index bd79d3fce..e4b4729c6 100644 --- a/api/kubernetes/cli/access.go +++ b/api/kubernetes/cli/access.go @@ -9,12 +9,7 @@ import ( ) type ( - accessPolicies struct { - UserAccessPolicies portainer.UserAccessPolicies `json:"UserAccessPolicies"` - TeamAccessPolicies portainer.TeamAccessPolicies `json:"TeamAccessPolicies"` - } - - namespaceAccessPolicies map[string]accessPolicies + namespaceAccessPolicies map[string]portainer.K8sNamespaceAccessPolicy ) 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 } -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)] if userAccess { return true @@ -84,3 +79,50 @@ func hasUserAccessToNamespace(userID int, teamIDs []int, policies accessPolicies 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 +} diff --git a/api/portainer.go b/api/portainer.go index 5ead33d64..897fc6b99 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -392,6 +392,11 @@ type ( // JobType represents a job type JobType int + K8sNamespaceAccessPolicy struct { + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` + } + // KubernetesData contains all the Kubernetes related endpoint information KubernetesData struct { Snapshots []KubernetesSnapshot `json:"Snapshots"` @@ -1160,6 +1165,8 @@ type ( SetupUserServiceAccount(userID int, teamIDs []int) error GetServiceAccountBearerToken(userID int) (string, 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