tenseleyflow/shithub / c2ec557

Browse files

repo/actions_dispatch: use shared dispatch validation

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
c2ec55730999e5a8a58660ad14ff60e0587c7d29
Parents
0b2c834
Tree
c4cc469

2 changed files

StatusFile+-
M internal/web/handlers/repo/actions_dispatch.go 5 90
M internal/web/handlers/repo/actions_test.go 4 3
internal/web/handlers/repo/actions_dispatch.gomodified
@@ -16,6 +16,7 @@ import (
1616
 
1717
 	"github.com/go-chi/chi/v5"
1818
 
19
+	"github.com/tenseleyFlow/shithub/internal/actions/dispatch"
1920
 	actionsevent "github.com/tenseleyFlow/shithub/internal/actions/event"
2021
 	"github.com/tenseleyFlow/shithub/internal/actions/trigger"
2122
 	"github.com/tenseleyFlow/shithub/internal/actions/workflow"
@@ -70,10 +71,8 @@ func (h *Handlers) repoActionsDispatch(w http.ResponseWriter, r *http.Request) {
7071
 	// Workflows live under .shithub/workflows/. Authors can pass
7172
 	// either "ci.yml" (basename) or ".shithub/workflows/ci.yml" (full
7273
 	// path). Normalize so the trigger pipeline always sees the full path.
73
-	if !strings.HasPrefix(file, ".shithub/workflows/") {
74
-		file = ".shithub/workflows/" + file
75
-	}
76
-	if strings.Contains(file, "..") || !validWorkflowName(file) {
74
+	file, err := dispatch.NormalizeFilePath(file)
75
+	if err != nil {
7776
 		h.d.Render.HTTPError(w, r, http.StatusBadRequest, "invalid workflow file path")
7877
 		return
7978
 	}
@@ -132,7 +131,7 @@ func (h *Handlers) repoActionsDispatch(w http.ResponseWriter, r *http.Request) {
132131
 			"workflow does not declare on.workflow_dispatch")
133132
 		return
134133
 	}
135
-	inputs, err := normalizeDispatchInputs(req.Inputs, wf.On.WorkflowDispatch.Inputs)
134
+	inputs, err := dispatch.NormalizeInputs(req.Inputs, wf.On.WorkflowDispatch.Inputs)
136135
 	if err != nil {
137136
 		h.d.Render.HTTPError(w, r, http.StatusBadRequest, err.Error())
138137
 		return
@@ -186,7 +185,7 @@ func (h *Handlers) parseDispatchRequest(w http.ResponseWriter, r *http.Request)
186185
 		}
187186
 		return dispatchRequest{
188187
 			Ref:    strings.TrimSpace(r.PostFormValue("ref")),
189
-			Inputs: dispatchInputsFromForm(r.PostForm),
188
+			Inputs: dispatch.InputsFromForm(r.PostForm),
190189
 		}, true, true
191190
 	}
192191
 
@@ -222,90 +221,6 @@ func dispatchFormMediaType(r *http.Request) string {
222221
 	}
223222
 }
224223
 
225
-func dispatchInputsFromForm(values url.Values) map[string]string {
226
-	inputs := make(map[string]string)
227
-	for key, vals := range values {
228
-		name, ok := strings.CutPrefix(key, "inputs.")
229
-		if !ok || name == "" || len(vals) == 0 {
230
-			continue
231
-		}
232
-		inputs[name] = vals[len(vals)-1]
233
-	}
234
-	if len(inputs) == 0 {
235
-		return nil
236
-	}
237
-	return inputs
238
-}
239
-
240
-func normalizeDispatchInputs(raw map[string]string, specs []workflow.DispatchInput) (map[string]string, error) {
241
-	if raw == nil {
242
-		raw = map[string]string{}
243
-	}
244
-	known := make(map[string]workflow.DispatchInput, len(specs))
245
-	for _, spec := range specs {
246
-		known[spec.Name] = spec
247
-	}
248
-	for name := range raw {
249
-		if _, ok := known[name]; !ok {
250
-			return nil, fmt.Errorf("unknown workflow_dispatch input %q", name)
251
-		}
252
-	}
253
-
254
-	out := make(map[string]string, len(specs))
255
-	for _, spec := range specs {
256
-		value, provided := raw[spec.Name]
257
-		if !provided || value == "" {
258
-			value = spec.Default
259
-		}
260
-		if spec.Type == "boolean" && !provided && value == "" {
261
-			value = "false"
262
-		}
263
-		if spec.Required && spec.Type != "boolean" && strings.TrimSpace(value) == "" {
264
-			return nil, fmt.Errorf("workflow_dispatch input %q is required", spec.Name)
265
-		}
266
-		switch spec.Type {
267
-		case "boolean":
268
-			if value != "true" && value != "false" {
269
-				return nil, fmt.Errorf("workflow_dispatch input %q must be true or false", spec.Name)
270
-			}
271
-		case "choice":
272
-			if value != "" && !dispatchChoiceAllowed(value, spec.Options) {
273
-				return nil, fmt.Errorf("workflow_dispatch input %q must be one of the declared options", spec.Name)
274
-			}
275
-		}
276
-		if value != "" || spec.Type == "boolean" {
277
-			out[spec.Name] = value
278
-		}
279
-	}
280
-	if len(out) == 0 {
281
-		return nil, nil
282
-	}
283
-	return out, nil
284
-}
285
-
286
-func dispatchChoiceAllowed(value string, options []string) bool {
287
-	for _, option := range options {
288
-		if value == option {
289
-			return true
290
-		}
291
-	}
292
-	return false
293
-}
294
-
295
-// validWorkflowName guards against URL parameter shenanigans by
296
-// requiring the resolved file path to look like
297
-// `.shithub/workflows/<basename>.{yml,yaml}` with no path traversal.
298
-func validWorkflowName(file string) bool {
299
-	if !strings.HasPrefix(file, ".shithub/workflows/") {
300
-		return false
301
-	}
302
-	rest := strings.TrimPrefix(file, ".shithub/workflows/")
303
-	if rest == "" || strings.ContainsAny(rest, "/\x00") {
304
-		return false
305
-	}
306
-	return strings.HasSuffix(rest, ".yml") || strings.HasSuffix(rest, ".yaml")
307
-}
308
-
309224
 // randHex returns 2*n hex chars from crypto/rand. 8 bytes (16 hex
310225
 // chars) is plenty of entropy for a dispatch request id.
311226
 func randHex(n int) (string, error) {
internal/web/handlers/repo/actions_test.gomodified
@@ -17,6 +17,7 @@ import (
1717
 	"github.com/go-chi/chi/v5"
1818
 	"github.com/jackc/pgx/v5/pgtype"
1919
 
20
+	"github.com/tenseleyFlow/shithub/internal/actions/dispatch"
2021
 	actionsdb "github.com/tenseleyFlow/shithub/internal/actions/sqlc"
2122
 	"github.com/tenseleyFlow/shithub/internal/actions/workflow"
2223
 	"github.com/tenseleyFlow/shithub/internal/infra/storage"
@@ -213,13 +214,13 @@ func TestRepoActionsDispatchAcceptsFormInputs(t *testing.T) {
213214
 func TestNormalizeDispatchInputsRejectsUnknownAndInvalidChoice(t *testing.T) {
214215
 	t.Parallel()
215216
 	specs := dispatchWorkflowInputSpecs()
216
-	if _, err := normalizeDispatchInputs(map[string]string{"bogus": "x"}, specs); err == nil {
217
+	if _, err := dispatch.NormalizeInputs(map[string]string{"bogus": "x"}, specs); err == nil {
217218
 		t.Fatal("unknown input accepted")
218219
 	}
219
-	if _, err := normalizeDispatchInputs(map[string]string{"env": "qa"}, specs); err == nil {
220
+	if _, err := dispatch.NormalizeInputs(map[string]string{"env": "qa"}, specs); err == nil {
220221
 		t.Fatal("invalid choice accepted")
221222
 	}
222
-	if _, err := normalizeDispatchInputs(nil, specs); err == nil {
223
+	if _, err := dispatch.NormalizeInputs(nil, specs); err == nil {
223224
 		t.Fatal("missing required input accepted")
224225
 	}
225226
 }