mirror of
https://github.com/portainer/portainer.git
synced 2026-08-07 09:04:48 +00:00
fix(edge): verify endpoint assignment for edge resources BE-13248 (#3280)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user