From 732337615e2b1d684a8c6aebfb61d9893a538e68 Mon Sep 17 00:00:00 2001 From: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Date: Tue, 23 Sep 2025 17:20:25 -0300 Subject: [PATCH] fix(edgestacks): add a missing webhook uniqueness check BE-12219 (#1251) --- api/http/handler/stacks/stack_update_git.go | 8 ++ .../handler/stacks/stack_update_git_test.go | 78 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 api/http/handler/stacks/stack_update_git_test.go diff --git a/api/http/handler/stacks/stack_update_git.go b/api/http/handler/stacks/stack_update_git.go index 2bdf2b71f..07cd616da 100644 --- a/api/http/handler/stacks/stack_update_git.go +++ b/api/http/handler/stacks/stack_update_git.go @@ -73,6 +73,14 @@ func (handler *Handler) stackUpdateGit(w http.ResponseWriter, r *http.Request) * return httperror.InternalServerError(msg, errors.New(msg)) } + if payload.AutoUpdate != nil && payload.AutoUpdate.Webhook != "" && + (stack.AutoUpdate == nil || + (stack.AutoUpdate != nil && stack.AutoUpdate.Webhook != payload.AutoUpdate.Webhook)) { + if isUnique, err := handler.checkUniqueWebhookID(payload.AutoUpdate.Webhook); !isUnique || err != nil { + return httperror.Conflict("Webhook ID already exists", errors.New("webhook ID already exists")) + } + } + // TODO: this is a work-around for stacks created with Portainer version >= 1.17.1 // The EndpointID property is not available for these stacks, this API environment(endpoint) // can use the optional EndpointID query parameter to associate a valid environment(endpoint) identifier to the stack. diff --git a/api/http/handler/stacks/stack_update_git_test.go b/api/http/handler/stacks/stack_update_git_test.go new file mode 100644 index 000000000..261c3d13d --- /dev/null +++ b/api/http/handler/stacks/stack_update_git_test.go @@ -0,0 +1,78 @@ +package stacks + +import ( + "bytes" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/datastore" + gittypes "github.com/portainer/portainer/api/git/types" + "github.com/portainer/portainer/api/http/security" + "github.com/portainer/portainer/api/internal/testhelpers" + + "github.com/gofrs/uuid" + "github.com/segmentio/encoding/json" + "github.com/stretchr/testify/require" +) + +func TestStackUpdateGitWebhookUniqueness(t *testing.T) { + webhook, err := uuid.NewV4() + require.NoError(t, err) + + _, store := datastore.MustNewTestStore(t, false, false) + + endpoint := &portainer.Endpoint{ + ID: 123, + Name: "endpoint1", + Type: portainer.DockerEnvironment, + } + err = store.Endpoint().Create(endpoint) + require.NoError(t, err) + + stack1 := portainer.Stack{ + ID: 456, + EndpointID: endpoint.ID, + AutoUpdate: &portainer.AutoUpdateSettings{ + Webhook: webhook.String(), + }, + GitConfig: &gittypes.RepoConfig{ + URL: "https://github.com/portainer/portainer.git", + }, + } + + err = store.Stack().Create(&stack1) + require.NoError(t, err) + + stack2 := stack1 + stack2.ID++ + stack2.AutoUpdate = nil + + err = store.Stack().Create(&stack2) + require.NoError(t, err) + + handler := NewHandler(testhelpers.NewTestRequestBouncer()) + handler.DataStore = store + + payload := &stackGitUpdatePayload{ + AutoUpdate: &portainer.AutoUpdateSettings{ + Webhook: webhook.String(), + }, + } + + jsonPayload, err := json.Marshal(payload) + require.NoError(t, err) + + url := "/stacks/" + strconv.Itoa(int(stack2.ID)) + "/git?endpointId=" + strconv.Itoa(int(endpoint.ID)) + req := httptest.NewRequest(http.MethodPost, url, bytes.NewReader(jsonPayload)) + + rrc := &security.RestrictedRequestContext{} + req = req.WithContext(security.StoreRestrictedRequestContext(req, rrc)) + + rr := httptest.NewRecorder() + handler.ServeHTTP(rr, req) + + require.Equal(t, http.StatusConflict, rr.Code) +}