Skip to content

Commit 88936dc

Browse files
authored
fix(api): forbid update with duplicate hooks (#5091)
By adding a duplicate ref check in workflow.IsValid Signed-off-by: francois samin <[email protected]>
1 parent 61e1079 commit 88936dc

File tree

2 files changed

+249
-4
lines changed

2 files changed

+249
-4
lines changed

engine/api/workflow/dao.go

+11
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,7 @@ func checkHooks(db gorp.SqlExecutor, w *sdk.Workflow, n *sdk.Node) error {
11861186

11871187
// Add missing default value for hook
11881188
model := w.HookModels[h.HookModelID]
1189+
h.HookModelName = model.Name
11891190
for k := range model.DefaultConfig {
11901191
if _, ok := h.Config[k]; !ok {
11911192
h.Config[k] = model.DefaultConfig[k]
@@ -1209,6 +1210,16 @@ func checkHooks(db gorp.SqlExecutor, w *sdk.Workflow, n *sdk.Node) error {
12091210
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid given value for hook config '%s', given value not in choices list", k)
12101211
}
12111212
}
1213+
v := h.Config[k]
1214+
v.Configurable = d.Configurable
1215+
h.Config[k] = v
1216+
}
1217+
// Check hooks duplication
1218+
for j := range n.Hooks {
1219+
h2 := n.Hooks[j]
1220+
if i != j && h.Ref() == h2.Ref() {
1221+
return sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid workflow: duplicate hook %s", model.Name)
1222+
}
12121223
}
12131224
}
12141225

engine/api/workflow_test.go

+238-4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,8 @@ import (
1111
"testing"
1212
"time"
1313

14-
"github.com/ovh/cds/engine/api/repositoriesmanager"
15-
"github.com/ovh/cds/engine/api/services"
16-
"github.com/ovh/cds/engine/service"
17-
14+
"github.com/go-gorp/gorp"
15+
"github.com/golang/mock/gomock"
1816
"github.com/stretchr/testify/assert"
1917
"github.com/stretchr/testify/require"
2018
"gopkg.in/yaml.v2"
@@ -26,9 +24,13 @@ import (
2624
"github.com/ovh/cds/engine/api/integration"
2725
"github.com/ovh/cds/engine/api/pipeline"
2826
"github.com/ovh/cds/engine/api/project"
27+
"github.com/ovh/cds/engine/api/repositoriesmanager"
28+
"github.com/ovh/cds/engine/api/services"
29+
"github.com/ovh/cds/engine/api/services/mock_services"
2930
"github.com/ovh/cds/engine/api/test"
3031
"github.com/ovh/cds/engine/api/test/assets"
3132
"github.com/ovh/cds/engine/api/workflow"
33+
"github.com/ovh/cds/engine/service"
3234
"github.com/ovh/cds/sdk"
3335
"github.com/ovh/cds/sdk/cdsclient"
3436
"github.com/ovh/cds/sdk/exportentities"
@@ -1593,3 +1595,235 @@ func TestBenchmarkGetWorkflowsWithAPI(t *testing.T) {
15931595
t.Logf("ns/op : %d", res.NsPerOp())
15941596
assert.False(t, res.NsPerOp() >= 500000000, "Workflows load is too long: GOT %d and EXPECTED lower than 500000000 (500ms)", res.NsPerOp())
15951597
}
1598+
1599+
func Test_putWorkflowShouldNotCallHOOKSIfHookDoesNotChange(t *testing.T) {
1600+
api, db, router, end := newTestAPI(t)
1601+
defer end()
1602+
1603+
_, _ = assets.InsertService(t, db, t.Name()+"_HOOKS", services.TypeHooks)
1604+
1605+
u, pass := assets.InsertAdminUser(t, api.mustDB())
1606+
key := sdk.RandomString(10)
1607+
proj := assets.InsertTestProject(t, db, api.Cache, key, key)
1608+
pip := sdk.Pipeline{
1609+
Name: "pipeline1",
1610+
ProjectID: proj.ID,
1611+
}
1612+
assert.NoError(t, pipeline.InsertPipeline(db, &pip))
1613+
1614+
wf := sdk.Workflow{
1615+
ProjectID: proj.ID,
1616+
ProjectKey: proj.Key,
1617+
Name: sdk.RandomString(10),
1618+
Groups: proj.ProjectGroups,
1619+
WorkflowData: sdk.WorkflowData{
1620+
Node: sdk.Node{
1621+
Name: "root",
1622+
Context: &sdk.NodeContext{
1623+
PipelineID: pip.ID,
1624+
},
1625+
Hooks: []sdk.NodeHook{
1626+
{
1627+
HookModelID: sdk.WebHookModel.ID,
1628+
Config: sdk.WorkflowNodeHookConfig{
1629+
"method": sdk.WorkflowNodeHookConfigValue{
1630+
Value: "POST",
1631+
},
1632+
},
1633+
},
1634+
},
1635+
},
1636+
},
1637+
}
1638+
1639+
// Setup a mock for all services called by the API
1640+
ctrl := gomock.NewController(t)
1641+
defer ctrl.Finish()
1642+
// The mock has been geenrated by mockgen: go get github.com/golang/mock/mockgen
1643+
// If you have to regenerate thi mock you just have to run, from directory $GOPATH/src/github.com/ovh/cds/engine/api/services:
1644+
// mockgen -source=http.go -destination=mock_services/services_mock.go Client
1645+
servicesClients := mock_services.NewMockClient(ctrl)
1646+
services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client {
1647+
return servicesClients
1648+
}
1649+
defer func() {
1650+
services.NewClient = services.NewDefaultClient
1651+
}()
1652+
1653+
// Mock the Hooks service
1654+
servicesClients.EXPECT().
1655+
DoJSONRequest(gomock.Any(), "POST", "/task/bulk", gomock.Any(), gomock.Any()).
1656+
DoAndReturn(
1657+
func(ctx context.Context, method, path string, in interface{}, out interface{}) (http.Header, int, error) {
1658+
actualHooks, ok := in.(map[string]sdk.NodeHook)
1659+
require.True(t, ok)
1660+
require.Len(t, actualHooks, 1)
1661+
for k, h := range actualHooks {
1662+
h.Config["method"] = sdk.WorkflowNodeHookConfigValue{
1663+
Value: "POST",
1664+
Configurable: true,
1665+
}
1666+
actualHooks[k] = h
1667+
}
1668+
out = actualHooks
1669+
return nil, 200, nil
1670+
},
1671+
)
1672+
1673+
// Insert the workflow
1674+
vars := map[string]string{
1675+
"permProjectKey": proj.Key,
1676+
}
1677+
uri := router.GetRoute("POST", api.postWorkflowHandler, vars)
1678+
test.NotEmpty(t, uri)
1679+
req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, &wf)
1680+
w := httptest.NewRecorder()
1681+
router.Mux.ServeHTTP(w, req)
1682+
assert.Equal(t, 201, w.Code)
1683+
1684+
// Load the workflow
1685+
vars = map[string]string{
1686+
"key": proj.Key,
1687+
"permWorkflowName": wf.Name,
1688+
}
1689+
uri = router.GetRoute("GET", api.getWorkflowHandler, vars)
1690+
test.NotEmpty(t, uri)
1691+
req = assets.NewAuthentifiedRequest(t, u, pass, "GET", uri, nil)
1692+
w = httptest.NewRecorder()
1693+
router.Mux.ServeHTTP(w, req)
1694+
1695+
// Unmarshal the workflow
1696+
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &wf))
1697+
1698+
// Then call the PUT handler, it should not trigger /task/bulk on hooks service
1699+
// Update the workflow
1700+
uri = router.GetRoute("PUT", api.putWorkflowHandler, vars)
1701+
test.NotEmpty(t, uri)
1702+
req = assets.NewAuthentifiedRequest(t, u, pass, "PUT", uri, &wf)
1703+
w = httptest.NewRecorder()
1704+
router.Mux.ServeHTTP(w, req)
1705+
assert.Equal(t, 200, w.Code)
1706+
1707+
}
1708+
1709+
func Test_putWorkflowWithDuplicateHooksShouldRaiseAnError(t *testing.T) {
1710+
api, db, router, end := newTestAPI(t)
1711+
defer end()
1712+
1713+
_, _ = assets.InsertService(t, db, t.Name()+"_HOOKS", services.TypeHooks)
1714+
1715+
u, pass := assets.InsertAdminUser(t, api.mustDB())
1716+
key := sdk.RandomString(10)
1717+
proj := assets.InsertTestProject(t, db, api.Cache, key, key)
1718+
pip := sdk.Pipeline{
1719+
Name: "pipeline1",
1720+
ProjectID: proj.ID,
1721+
}
1722+
assert.NoError(t, pipeline.InsertPipeline(db, &pip))
1723+
1724+
wf := sdk.Workflow{
1725+
ProjectID: proj.ID,
1726+
ProjectKey: proj.Key,
1727+
Name: sdk.RandomString(10),
1728+
Groups: proj.ProjectGroups,
1729+
WorkflowData: sdk.WorkflowData{
1730+
Node: sdk.Node{
1731+
Name: "root",
1732+
Context: &sdk.NodeContext{
1733+
PipelineID: pip.ID,
1734+
},
1735+
Hooks: []sdk.NodeHook{
1736+
{
1737+
HookModelID: sdk.WebHookModel.ID,
1738+
Config: sdk.WorkflowNodeHookConfig{
1739+
"method": sdk.WorkflowNodeHookConfigValue{
1740+
Value: "POST",
1741+
},
1742+
},
1743+
},
1744+
},
1745+
},
1746+
},
1747+
}
1748+
1749+
// Setup a mock for all services called by the API
1750+
ctrl := gomock.NewController(t)
1751+
defer ctrl.Finish()
1752+
// The mock has been geenrated by mockgen: go get github.com/golang/mock/mockgen
1753+
// If you have to regenerate thi mock you just have to run, from directory $GOPATH/src/github.com/ovh/cds/engine/api/services:
1754+
// mockgen -source=http.go -destination=mock_services/services_mock.go Client
1755+
servicesClients := mock_services.NewMockClient(ctrl)
1756+
services.NewClient = func(_ gorp.SqlExecutor, _ []sdk.Service) services.Client {
1757+
return servicesClients
1758+
}
1759+
defer func() {
1760+
services.NewClient = services.NewDefaultClient
1761+
}()
1762+
1763+
// Mock the Hooks service
1764+
servicesClients.EXPECT().
1765+
DoJSONRequest(gomock.Any(), "POST", "/task/bulk", gomock.Any(), gomock.Any()).
1766+
DoAndReturn(
1767+
func(ctx context.Context, method, path string, in interface{}, out interface{}) (http.Header, int, error) {
1768+
actualHooks, ok := in.(map[string]sdk.NodeHook)
1769+
require.True(t, ok)
1770+
require.Len(t, actualHooks, 1)
1771+
for k, h := range actualHooks {
1772+
h.Config["method"] = sdk.WorkflowNodeHookConfigValue{
1773+
Value: "POST",
1774+
Configurable: true,
1775+
}
1776+
actualHooks[k] = h
1777+
}
1778+
out = actualHooks
1779+
return nil, 200, nil
1780+
},
1781+
)
1782+
1783+
// Insert the workflow
1784+
vars := map[string]string{
1785+
"permProjectKey": proj.Key,
1786+
}
1787+
uri := router.GetRoute("POST", api.postWorkflowHandler, vars)
1788+
test.NotEmpty(t, uri)
1789+
req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, &wf)
1790+
w := httptest.NewRecorder()
1791+
router.Mux.ServeHTTP(w, req)
1792+
assert.Equal(t, 201, w.Code)
1793+
1794+
// Load the workflow
1795+
vars = map[string]string{
1796+
"key": proj.Key,
1797+
"permWorkflowName": wf.Name,
1798+
}
1799+
uri = router.GetRoute("GET", api.getWorkflowHandler, vars)
1800+
test.NotEmpty(t, uri)
1801+
req = assets.NewAuthentifiedRequest(t, u, pass, "GET", uri, nil)
1802+
w = httptest.NewRecorder()
1803+
router.Mux.ServeHTTP(w, req)
1804+
1805+
// Unmarshal the workflow
1806+
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &wf))
1807+
1808+
// Then add another hooks with similar properties. It should raise a 400 HTTP Error
1809+
1810+
wf.WorkflowData.Node.Hooks = append(wf.WorkflowData.Node.Hooks,
1811+
sdk.NodeHook{
1812+
HookModelID: sdk.WebHookModel.ID,
1813+
Config: sdk.WorkflowNodeHookConfig{
1814+
"method": sdk.WorkflowNodeHookConfigValue{
1815+
Value: "POST",
1816+
},
1817+
},
1818+
},
1819+
)
1820+
1821+
// Update the workflow
1822+
uri = router.GetRoute("PUT", api.putWorkflowHandler, vars)
1823+
test.NotEmpty(t, uri)
1824+
req = assets.NewAuthentifiedRequest(t, u, pass, "PUT", uri, &wf)
1825+
w = httptest.NewRecorder()
1826+
router.Mux.ServeHTTP(w, req)
1827+
assert.Equal(t, 400, w.Code)
1828+
1829+
}

0 commit comments

Comments
 (0)