Skip to content

Commit 653b70a

Browse files
Copilotintel352
andauthored
Fix multiWorkflowAddr undeclared variable, path traversal in bundle import, and test robustness (#154)
* Initial plan * Apply review feedback: add multiWorkflowAddr flag, path traversal validation, error logging, and test improvements Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
1 parent 7d24c44 commit 653b70a

4 files changed

Lines changed: 152 additions & 26 deletions

File tree

cmd/server/main.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,9 @@ var (
8686
multiWorkflowAddr = flag.String("multi-workflow-addr", ":8080", "Listen address for the multi-workflow API server")
8787
databaseDSN = flag.String("database-dsn", "", "PostgreSQL connection string for multi-workflow mode")
8888
jwtSecret = flag.String("jwt-secret", "", "JWT signing secret for API authentication")
89-
adminEmail = flag.String("admin-email", "", "Initial admin user email (first-run bootstrap)")
90-
adminPassword = flag.String("admin-password", "", "Initial admin user password (first-run bootstrap)")
89+
adminEmail = flag.String("admin-email", "", "Initial admin user email (first-run bootstrap)")
90+
adminPassword = flag.String("admin-password", "", "Initial admin user password (first-run bootstrap)")
91+
multiWorkflowAddr = flag.String("multi-workflow-addr", ":8081", "HTTP listen address (multi-workflow API)")
9192

9293
// License flags
9394
licenseKey = flag.String("license-key", "", "License key for the workflow engine (or set WORKFLOW_LICENSE_KEY env var)")
@@ -1197,10 +1198,29 @@ func (app *serverApp) importBundles(logger *slog.Logger) error {
11971198
continue
11981199
}
11991200

1201+
// Ensure the extracted workflow.yaml path is within the expected destination directory
1202+
absDestDir, absDestErr := filepath.Abs(destDir)
1203+
if absDestErr != nil {
1204+
logger.Error("Failed to resolve destination directory", "destDir", destDir, "error", absDestErr)
1205+
continue
1206+
}
1207+
1208+
absWorkflowPath, absWorkflowErr := filepath.Abs(workflowPath)
1209+
if absWorkflowErr != nil {
1210+
logger.Error("Failed to resolve workflow path", "path", workflowPath, "error", absWorkflowErr)
1211+
continue
1212+
}
1213+
1214+
rel, relErr := filepath.Rel(absDestDir, absWorkflowPath)
1215+
if relErr != nil || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
1216+
logger.Error("Workflow path is outside destination directory; possible path traversal", "path", absWorkflowPath, "destDir", absDestDir, "error", relErr)
1217+
continue
1218+
}
1219+
12001220
// Read the extracted workflow.yaml
1201-
yamlData, err := os.ReadFile(workflowPath) //nolint:gosec // G703: path from trusted bundle extraction
1221+
yamlData, err := os.ReadFile(absWorkflowPath) //nolint:gosec // G703: path validated to be within destDir
12021222
if err != nil {
1203-
logger.Error("Failed to read workflow.yaml", "path", workflowPath, "error", err)
1223+
logger.Error("Failed to read workflow.yaml", "path", absWorkflowPath, "error", err)
12041224
continue
12051225
}
12061226
yamlContent := string(yamlData)
@@ -1326,10 +1346,14 @@ func run(ctx context.Context, app *serverApp, listenAddr string) error {
13261346

13271347
// Clean up temp files and directories
13281348
for _, f := range app.cleanupFiles {
1329-
os.Remove(f) //nolint:gosec // G703: cleaning up server-managed temp files
1349+
if err := os.Remove(f); err != nil && !os.IsNotExist(err) { //nolint:gosec // G703: cleaning up server-managed temp files
1350+
app.logger.Error("Temp file cleanup error", "path", f, "error", err)
1351+
}
13301352
}
13311353
for _, d := range app.cleanupDirs {
1332-
os.RemoveAll(d) //nolint:gosec // G703: cleaning up server-managed temp dirs
1354+
if err := os.RemoveAll(d); err != nil && !os.IsNotExist(err) { //nolint:gosec // G703: cleaning up server-managed temp dirs
1355+
app.logger.Error("Temp directory cleanup error", "path", d, "error", err)
1356+
}
13331357
}
13341358

13351359
return nil

engine_structural_test.go

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ import (
1414

1515
// --- Negative tests: core engine rejects unknown module types ---
1616

17+
// findPluginByName returns the plugin with the given name from allPlugins,
18+
// or nil if no such plugin exists.
19+
func findPluginByName(name string) plugin.EnginePlugin {
20+
for _, p := range allPlugins() {
21+
if p.Name() == name {
22+
return p
23+
}
24+
}
25+
return nil
26+
}
27+
1728
// TestCoreRejectsUnknownModuleType verifies that the engine returns a clear
1829
// error when a config references a module type that no plugin provides.
1930
func TestCoreRejectsUnknownModuleType(t *testing.T) {
@@ -201,7 +212,10 @@ func TestSelectivePluginLoading_HTTPOnly(t *testing.T) {
201212
engine := NewStdEngine(app, &mock.Logger{})
202213

203214
// Load only the HTTP plugin.
204-
httpPlugin := allPlugins()[0] // http plugin is first
215+
httpPlugin := findPluginByName("workflow-plugin-http")
216+
if httpPlugin == nil {
217+
t.Fatalf("HTTP plugin not found in allPlugins")
218+
}
205219
if err := engine.LoadPlugin(httpPlugin); err != nil {
206220
t.Fatalf("LoadPlugin(http) failed: %v", err)
207221
}
@@ -274,13 +288,28 @@ func TestEngineFactoryMapPopulatedByPlugins(t *testing.T) {
274288
"auth.jwt",
275289
}
276290

291+
// Build a set of known module types from the public schema API.
292+
known := schema.KnownModuleTypes()
293+
knownSet := make(map[string]bool, len(known))
294+
for _, k := range known {
295+
knownSet[k] = true
296+
}
297+
277298
for _, mt := range expectedTypes {
278-
if _, ok := engine.moduleFactories[mt]; !ok {
279-
t.Errorf("module type %q not found in factory map after loading all plugins", mt)
299+
if !knownSet[mt] {
300+
t.Errorf("module type %q not found in schema.KnownModuleTypes() after loading all plugins", mt)
280301
}
281302
}
282303
}
283304

305+
func getEngineModuleTypes(engine *StdEngine) []string {
306+
types := make([]string, 0, len(engine.moduleFactories))
307+
for mt := range engine.moduleFactories {
308+
types = append(types, mt)
309+
}
310+
return types
311+
}
312+
284313
// TestSchemaKnowsPluginModuleTypes verifies that schema.RegisterModuleType is
285314
// called for each plugin's module types during LoadPlugin.
286315
func TestSchemaKnowsPluginModuleTypes(t *testing.T) {
@@ -295,7 +324,7 @@ func TestSchemaKnowsPluginModuleTypes(t *testing.T) {
295324
}
296325

297326
// Every module type in the factory map should be in the schema.
298-
for mt := range engine.moduleFactories {
327+
for _, mt := range getEngineModuleTypes(engine) {
299328
if !knownSet[mt] {
300329
t.Errorf("module type %q is in factory map but not in schema.KnownModuleTypes()", mt)
301330
}

module/api_v1_test.go

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ func setupTestStore(t *testing.T) *V1Store {
2929

3030
// --- V1Store Tests ---
3131

32+
func mustCreateCompany(t *testing.T, store *V1Store, name, desc, userID string) *V1Company {
33+
t.Helper()
34+
company, err := store.CreateCompany(name, desc, userID)
35+
if err != nil {
36+
t.Fatalf("CreateCompany(%q) failed: %v", name, err)
37+
}
38+
return company
39+
}
40+
41+
func mustCreateOrganization(t *testing.T, store *V1Store, companyID, name, desc, userID string) *V1Company {
42+
t.Helper()
43+
org, err := store.CreateOrganization(companyID, name, desc, userID)
44+
if err != nil {
45+
t.Fatalf("CreateOrganization(%q) failed: %v", name, err)
46+
}
47+
return org
48+
}
49+
50+
func mustCreateProject(t *testing.T, store *V1Store, orgID, name, desc, userID string) *V1Project {
51+
t.Helper()
52+
proj, err := store.CreateProject(orgID, name, desc, userID)
53+
if err != nil {
54+
t.Fatalf("CreateProject(%q) failed: %v", name, err)
55+
}
56+
return proj
57+
}
58+
59+
// --- V1Store Tests ---
60+
3261
func TestV1Store_CreateAndListCompanies(t *testing.T) {
3362
store := setupTestStore(t)
3463

@@ -116,8 +145,14 @@ func TestV1Store_CreateAndListOrganizations(t *testing.T) {
116145
func TestV1Store_CreateAndListProjects(t *testing.T) {
117146
store := setupTestStore(t)
118147

119-
company, _ := store.CreateCompany("Co", "", "u1")
120-
org, _ := store.CreateOrganization(company.ID, "Org", "", "u1")
148+
company, err := store.CreateCompany("Co", "", "u1")
149+
if err != nil {
150+
t.Fatalf("CreateCompany: %v", err)
151+
}
152+
org, err := store.CreateOrganization(company.ID, "Org", "", "u1")
153+
if err != nil {
154+
t.Fatalf("CreateOrganization: %v", err)
155+
}
121156

122157
p, err := store.CreateProject(org.ID, "My Project", "", "A cool project")
123158
if err != nil {
@@ -151,9 +186,9 @@ func TestV1Store_CreateAndListProjects(t *testing.T) {
151186
func TestV1Store_WorkflowCRUD(t *testing.T) {
152187
store := setupTestStore(t)
153188

154-
company, _ := store.CreateCompany("Co", "", "u1")
155-
org, _ := store.CreateOrganization(company.ID, "Org", "", "u1")
156-
proj, _ := store.CreateProject(org.ID, "Proj", "", "")
189+
company := mustCreateCompany(t, store, "Co", "", "u1")
190+
org := mustCreateOrganization(t, store, company.ID, "Org", "", "u1")
191+
proj := mustCreateProject(t, store, org.ID, "Proj", "", "")
157192

158193
// Create
159194
wf, err := store.CreateWorkflow(proj.ID, "Test Workflow", "", "desc", "modules: []", "u1")
@@ -221,7 +256,10 @@ func TestV1Store_WorkflowCRUD(t *testing.T) {
221256
t.Fatalf("DeleteWorkflow: %v", err)
222257
}
223258

224-
wfs, _ = store.ListWorkflows(proj.ID)
259+
wfs, err = store.ListWorkflows(proj.ID)
260+
if err != nil {
261+
t.Fatalf("ListWorkflows after delete: %v", err)
262+
}
225263
if len(wfs) != 0 {
226264
t.Errorf("got %d workflows after delete, want 0", len(wfs))
227265
}
@@ -230,15 +268,21 @@ func TestV1Store_WorkflowCRUD(t *testing.T) {
230268
func TestV1Store_WorkflowVersioning(t *testing.T) {
231269
store := setupTestStore(t)
232270

233-
company, _ := store.CreateCompany("Co", "", "u1")
234-
org, _ := store.CreateOrganization(company.ID, "Org", "", "u1")
235-
proj, _ := store.CreateProject(org.ID, "Proj", "", "")
271+
company := mustCreateCompany(t, store, "Co", "", "u1")
272+
org := mustCreateOrganization(t, store, company.ID, "Org", "", "u1")
273+
proj := mustCreateProject(t, store, org.ID, "Proj", "", "")
236274
wf, _ := store.CreateWorkflow(proj.ID, "WF", "", "", "v1 config", "u1")
237275

238276
// Update config 3 times to create versions 2, 3, 4
239-
store.UpdateWorkflow(wf.ID, "", "", "v2 config", "u1")
240-
store.UpdateWorkflow(wf.ID, "", "", "v3 config", "u1")
241-
store.UpdateWorkflow(wf.ID, "", "", "v4 config", "u1")
277+
if _, err := store.UpdateWorkflow(wf.ID, "", "", "v2 config", "u1"); err != nil {
278+
t.Fatalf("UpdateWorkflow v2: %v", err)
279+
}
280+
if _, err := store.UpdateWorkflow(wf.ID, "", "", "v3 config", "u1"); err != nil {
281+
t.Fatalf("UpdateWorkflow v3: %v", err)
282+
}
283+
if _, err := store.UpdateWorkflow(wf.ID, "", "", "v4 config", "u1"); err != nil {
284+
t.Fatalf("UpdateWorkflow v4: %v", err)
285+
}
242286

243287
versions, err := store.ListVersions(wf.ID)
244288
if err != nil {
@@ -311,12 +355,18 @@ func TestV1Store_EnsureSystemHierarchy(t *testing.T) {
311355
if err != nil {
312356
t.Fatalf("EnsureSystemHierarchy (second call): %v", err)
313357
}
358+
if c2 != companyID {
359+
t.Errorf("expected same company ID %q, got %q", companyID, c2)
360+
}
361+
if o2 != orgID {
362+
t.Errorf("expected same organization ID %q, got %q", orgID, o2)
363+
}
364+
if p2 != projectID {
365+
t.Errorf("expected same project ID %q, got %q", projectID, p2)
366+
}
314367
if w2 != workflowID {
315368
t.Errorf("expected same workflow ID %q, got %q", workflowID, w2)
316369
}
317-
_ = c2
318-
_ = o2
319-
_ = p2
320370
}
321371

322372
func TestV1Store_ResetSystemWorkflow(t *testing.T) {

plugins/admin/plugin_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,19 @@ import (
99

1010
func TestPluginImplementsEnginePlugin(t *testing.T) {
1111
p := New()
12+
// Compile-time assertion that *Plugin implements plugin.EnginePlugin.
1213
var _ plugin.EnginePlugin = p
14+
15+
// Runtime checks via the EnginePlugin interface.
16+
var ep plugin.EnginePlugin = p
17+
18+
manifest := ep.EngineManifest()
19+
if manifest == nil {
20+
t.Fatal("EngineManifest() returned nil")
21+
}
22+
if err := manifest.Validate(); err != nil {
23+
t.Fatalf("EngineManifest.Validate() failed: %v", err)
24+
}
1325
}
1426

1527
func TestPluginManifest(t *testing.T) {
@@ -55,9 +67,14 @@ func TestModuleFactories(t *testing.T) {
5567
t.Errorf("missing factory for %q", typ)
5668
continue
5769
}
58-
mod := factory("test-"+typ, map[string]any{})
70+
name := "test-" + typ
71+
mod := factory(name, map[string]any{})
5972
if mod == nil {
6073
t.Errorf("factory for %q returned nil", typ)
74+
continue
75+
}
76+
if mod.Name() != name {
77+
t.Errorf("factory for %q produced module with unexpected name: got %q, want %q", typ, mod.Name(), name)
6178
}
6279
}
6380
}
@@ -141,6 +158,7 @@ func TestWiringHookMergesAdminConfig(t *testing.T) {
141158
},
142159
}
143160

161+
originalLen := len(cfg.Modules)
144162
err := hooks[0].Hook(nil, cfg)
145163
if err != nil {
146164
t.Fatalf("wiring hook failed: %v", err)
@@ -157,6 +175,11 @@ func TestWiringHookMergesAdminConfig(t *testing.T) {
157175
if !found {
158176
t.Error("admin-server module not found after wiring hook merge")
159177
}
178+
179+
// Ensure that additional admin modules have been merged
180+
if len(cfg.Modules) <= originalLen {
181+
t.Errorf("expected modules to be added by wiring hook, before=%d after=%d", originalLen, len(cfg.Modules))
182+
}
160183
}
161184

162185
func TestWiringHookSkipsIfAlreadyPresent(t *testing.T) {

0 commit comments

Comments
 (0)