fix(edge): verify endpoint assignment for edge resources BE-13248 (#3280)

This commit is contained in:
andres-portainer
2026-07-27 09:09:23 -03:00
committed by GitHub
parent e178cd67bb
commit 7616319900
4 changed files with 170 additions and 0 deletions
@@ -90,6 +90,17 @@ func (handler *Handler) edgeStackStatusUpdate(w http.ResponseWriter, r *http.Req
return httperror.InternalServerError("Unable to retrieve Edge stack from the database", err)
}
relation, err := tx.EndpointRelation().EndpointRelation(payload.EndpointID)
if err != nil && !dataservices.IsErrObjectNotFound(err) {
return httperror.InternalServerError("Unable to retrieve relation object from the database", fmt.Errorf("%w. Environment ID: %d", err, payload.EndpointID))
}
// Removal reports must still go through after the relation was cleared.
isRemovalReport := *payload.Status == portainer.EdgeStackStatusRemoved || *payload.Status == portainer.EdgeStackStatusRemoving
if (relation == nil || !relation.EdgeStacks[stack.ID]) && !isRemovalReport {
return httperror.Forbidden("Permission denied to update status for this Edge stack", fmt.Errorf("Edge stack %d is not assigned to environment %d", stack.ID, payload.EndpointID))
}
if err := handler.updateEdgeStackStatus(tx, stack, stack.ID, payload); err != nil {
return httperror.InternalServerError("Unable to update Edge stack status", err)
}
@@ -78,6 +78,72 @@ func TestUpdateStatusAndInspect(t *testing.T) {
}
}
func TestUpdateStatusForUnassignedEdgeStack(t *testing.T) {
t.Parallel()
handler, _ := setupHandler(t)
endpoint := createEndpoint(t, handler.DataStore)
unrelatedEndpoint := createEndpointWithId(t, handler.DataStore, 6)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
newStatus := portainer.EdgeStackStatusRunning
payload := updateStatusPayload{
Status: &newStatus,
EndpointID: unrelatedEndpoint.ID,
}
jsonPayload, err := json.Marshal(payload)
require.NoError(t, err)
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
require.NoError(t, err)
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, unrelatedEndpoint.EdgeID)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusForbidden, rec.Code)
_, err = handler.DataStore.EdgeStackStatus().Read(edgeStack.ID, unrelatedEndpoint.ID)
require.True(t, handler.DataStore.IsErrObjectNotFound(err))
}
func TestUpdateStatusRemovedAllowedAfterUnassignment(t *testing.T) {
t.Parallel()
handler, _ := setupHandler(t)
endpoint := createEndpoint(t, handler.DataStore)
edgeStack := createEdgeStack(t, handler.DataStore, endpoint.ID)
// Simulate the endpoint being unassigned from the stack (e.g. edge group changed)
// before the agent has torn down and reported its final status back.
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
require.NoError(t, err)
delete(relation.EdgeStacks, edgeStack.ID)
require.NoError(t, handler.DataStore.EndpointRelation().UpdateEndpointRelation(endpoint.ID, relation))
newStatus := portainer.EdgeStackStatusRemoved
payload := updateStatusPayload{
Status: &newStatus,
EndpointID: endpoint.ID,
}
jsonPayload, err := json.Marshal(payload)
require.NoError(t, err)
r := bytes.NewBuffer(jsonPayload)
req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("/edge_stacks/%d/status", edgeStack.ID), r)
require.NoError(t, err)
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, endpoint.EdgeID)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
}
func TestUpdateStatusWithInvalidPayload(t *testing.T) {
t.Parallel()
handler, _ := setupHandler(t)
@@ -68,6 +68,15 @@ func (handler *Handler) endpointEdgeStackInspect(w http.ResponseWriter, r *http.
// WARNING: this variable must not be mutated
edgeStack := s.(*portainer.EdgeStack)
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
if err != nil && !handler.DataStore.IsErrObjectNotFound(err) {
return httperror.InternalServerError("Unable to retrieve relation object from the database", fmt.Errorf("%w. Environment ID: %d", err, endpoint.ID))
}
if relation == nil || !relation.EdgeStacks[edgeStack.ID] {
return httperror.Forbidden("Permission denied to access this Edge stack", fmt.Errorf("Edge stack %d is not assigned to environment %d", edgeStack.ID, endpoint.ID))
}
fileName := edgeStack.EntryPoint
if endpointutils.IsDockerEndpoint(endpoint) {
if fileName == "" {
@@ -0,0 +1,84 @@
package endpointedge
import (
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/require"
)
func TestEndpointEdgeStackInspectRejectsUnassignedStack(t *testing.T) {
t.Parallel()
handler := mustSetupHandler(t)
endpointID := portainer.EndpointID(10)
require.NoError(t, createEndpoint(handler, portainer.Endpoint{
ID: endpointID,
Name: "endpoint-10",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
}, portainer.EndpointRelation{EndpointID: endpointID}))
edgeStackID := portainer.EdgeStackID(99)
edgeStack := &portainer.EdgeStack{
ID: edgeStackID,
Name: "unassigned-stack",
EntryPoint: "docker-compose.yml",
}
require.NoError(t, handler.DataStore.EdgeStack().Create(edgeStackID, edgeStack))
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/stacks/%d", endpointID, edgeStackID), nil)
require.NoError(t, err)
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusForbidden, rec.Code)
}
func TestEndpointEdgeStackInspectAllowsAssignedStack(t *testing.T) {
t.Parallel()
handler := mustSetupHandler(t)
fileName := "docker-compose.yml"
edgeStackID := portainer.EdgeStackID(100)
projectPath, err := handler.FileService.StoreEdgeStackFileFromBytes(strconv.Itoa(int(edgeStackID)), fileName, []byte("version: '3'"))
require.NoError(t, err)
edgeStack := &portainer.EdgeStack{
ID: edgeStackID,
Name: "assigned-stack",
EntryPoint: fileName,
ProjectPath: projectPath,
}
require.NoError(t, handler.DataStore.EdgeStack().Create(edgeStackID, edgeStack))
endpointID := portainer.EndpointID(11)
require.NoError(t, createEndpoint(handler, portainer.Endpoint{
ID: endpointID,
Name: "endpoint-11",
Type: portainer.EdgeAgentOnDockerEnvironment,
URL: "https://portainer.io:9443",
EdgeID: "edge-id",
}, portainer.EndpointRelation{
EndpointID: endpointID,
EdgeStacks: map[portainer.EdgeStackID]bool{edgeStackID: true},
}))
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/stacks/%d", endpointID, edgeStackID), nil)
require.NoError(t, err)
req.Header.Set(portainer.PortainerAgentEdgeIDHeader, "edge-id")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
}