fix: resolve all golangci-lint issues
Fixes #32 Changes: - middleware.go: use max() builtin, strconv.Itoa, fix wsl whitespace - database.go: fix nlreturn, noinlineerr, wsl whitespace - handlers.go: remove unnecessary template.HTML conversion, unused import - app.go: extract cleanupContainer to fix nestif, fix lll - client.go: break long string literals to fix lll - deploy.go: fix wsl whitespace - auth_test.go: extract helpers to fix funlen, fix wsl/nlreturn/testifylint - handlers_test.go: deduplicate IDOR tests, fix paralleltest - validation_test.go: add parallel, fix funlen/wsl, nolint testpackage - port_validation_test.go: add parallel, nolint testpackage - ratelimit_test.go: add parallel where safe, nolint testpackage/paralleltest - realip_test.go: add parallel, use NewRequestWithContext, fix wsl/funlen - user.go: (noinlineerr already fixed by database.go pattern)
This commit is contained in:
@@ -255,6 +255,33 @@ func (h *Handlers) HandleAppUpdate() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// cleanupContainer stops and removes the Docker container for the given app.
|
||||
func (h *Handlers) cleanupContainer(ctx context.Context, appID, appName string) {
|
||||
containerInfo, containerErr := h.docker.FindContainerByAppID(ctx, appID)
|
||||
if containerErr != nil || containerInfo == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if containerInfo.Running {
|
||||
stopErr := h.docker.StopContainer(ctx, containerInfo.ID)
|
||||
if stopErr != nil {
|
||||
h.log.Error("failed to stop container during app deletion",
|
||||
"error", stopErr, "app", appName,
|
||||
"container", containerInfo.ID)
|
||||
}
|
||||
}
|
||||
|
||||
removeErr := h.docker.RemoveContainer(ctx, containerInfo.ID, true)
|
||||
if removeErr != nil {
|
||||
h.log.Error("failed to remove container during app deletion",
|
||||
"error", removeErr, "app", appName,
|
||||
"container", containerInfo.ID)
|
||||
} else {
|
||||
h.log.Info("removed container during app deletion",
|
||||
"app", appName, "container", containerInfo.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleAppDelete handles app deletion.
|
||||
func (h *Handlers) HandleAppDelete() http.HandlerFunc {
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
@@ -268,27 +295,7 @@ func (h *Handlers) HandleAppDelete() http.HandlerFunc {
|
||||
}
|
||||
|
||||
// Stop and remove the Docker container before deleting the DB record
|
||||
containerInfo, containerErr := h.docker.FindContainerByAppID(request.Context(), appID)
|
||||
if containerErr == nil && containerInfo != nil {
|
||||
if containerInfo.Running {
|
||||
stopErr := h.docker.StopContainer(request.Context(), containerInfo.ID)
|
||||
if stopErr != nil {
|
||||
h.log.Error("failed to stop container during app deletion",
|
||||
"error", stopErr, "app", application.Name,
|
||||
"container", containerInfo.ID)
|
||||
}
|
||||
}
|
||||
|
||||
removeErr := h.docker.RemoveContainer(request.Context(), containerInfo.ID, true)
|
||||
if removeErr != nil {
|
||||
h.log.Error("failed to remove container during app deletion",
|
||||
"error", removeErr, "app", application.Name,
|
||||
"container", containerInfo.ID)
|
||||
} else {
|
||||
h.log.Info("removed container during app deletion",
|
||||
"app", application.Name, "container", containerInfo.ID)
|
||||
}
|
||||
}
|
||||
h.cleanupContainer(request.Context(), appID, application.Name)
|
||||
|
||||
deleteErr := application.Delete(request.Context())
|
||||
if deleteErr != nil {
|
||||
@@ -1019,7 +1026,11 @@ func parsePortValues(hostPortStr, containerPortStr string) (int, int, bool) {
|
||||
containerPort, containerErr := strconv.Atoi(containerPortStr)
|
||||
|
||||
const maxPort = 65535
|
||||
if hostErr != nil || containerErr != nil || hostPort <= 0 || containerPort <= 0 || hostPort > maxPort || containerPort > maxPort {
|
||||
|
||||
invalid := hostErr != nil || containerErr != nil ||
|
||||
hostPort <= 0 || containerPort <= 0 ||
|
||||
hostPort > maxPort || containerPort > maxPort
|
||||
if invalid {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
@@ -75,7 +74,7 @@ func (h *Handlers) addGlobals(
|
||||
data["Appname"] = h.globals.Appname
|
||||
|
||||
if request != nil {
|
||||
data["CSRFField"] = template.HTML(csrf.TemplateField(request)) //nolint:gosec // csrf.TemplateField produces safe HTML
|
||||
data["CSRFField"] = csrf.TemplateField(request)
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
@@ -450,8 +450,8 @@ func createTestApp(
|
||||
return createdApp
|
||||
}
|
||||
|
||||
// TestDeleteEnvVarOwnershipVerification tests that deleting an env var
|
||||
// via another app's URL path returns 404 (IDOR prevention).
|
||||
// TestHandleWebhookRejectsOversizedBody tests that oversized webhook payloads
|
||||
// are handled gracefully.
|
||||
func TestHandleWebhookRejectsOversizedBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -493,85 +493,113 @@ func TestHandleWebhookRejectsOversizedBody(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||
}
|
||||
|
||||
// TestDeleteEnvVarOwnershipVerification tests that deleting an env var
|
||||
// via another app's URL path returns 404 (IDOR prevention).
|
||||
func TestDeleteEnvVarOwnershipVerification(t *testing.T) {
|
||||
t.Parallel()
|
||||
// ownedResourceTestConfig configures an IDOR ownership verification test.
|
||||
type ownedResourceTestConfig struct {
|
||||
appPrefix1 string
|
||||
appPrefix2 string
|
||||
createFn func(t *testing.T, tc *testContext, app *models.App) int64
|
||||
deletePath func(appID string, resourceID int64) string
|
||||
chiParams func(appID string, resourceID int64) map[string]string
|
||||
handler func(h *handlers.Handlers) http.HandlerFunc
|
||||
verifyFn func(t *testing.T, tc *testContext, resourceID int64)
|
||||
}
|
||||
|
||||
func testOwnershipVerification(t *testing.T, cfg ownedResourceTestConfig) {
|
||||
t.Helper()
|
||||
|
||||
testCtx := setupTestHandlers(t)
|
||||
|
||||
app1 := createTestApp(t, testCtx, "envvar-owner-app")
|
||||
app2 := createTestApp(t, testCtx, "envvar-other-app")
|
||||
app1 := createTestApp(t, testCtx, cfg.appPrefix1)
|
||||
app2 := createTestApp(t, testCtx, cfg.appPrefix2)
|
||||
|
||||
// Create env var belonging to app1
|
||||
envVar := models.NewEnvVar(testCtx.database)
|
||||
envVar.AppID = app1.ID
|
||||
envVar.Key = "SECRET"
|
||||
envVar.Value = "hunter2"
|
||||
require.NoError(t, envVar.Save(context.Background()))
|
||||
resourceID := cfg.createFn(t, testCtx, app1)
|
||||
|
||||
// Try to delete app1's env var using app2's URL path
|
||||
request := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/apps/"+app2.ID+"/env/"+strconv.FormatInt(envVar.ID, 10)+"/delete",
|
||||
cfg.deletePath(app2.ID, resourceID),
|
||||
nil,
|
||||
)
|
||||
request = addChiURLParams(request, map[string]string{
|
||||
"id": app2.ID,
|
||||
"envID": strconv.FormatInt(envVar.ID, 10),
|
||||
})
|
||||
request = addChiURLParams(request, cfg.chiParams(app2.ID, resourceID))
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler := testCtx.handlers.HandleEnvVarDelete()
|
||||
handler := cfg.handler(testCtx.handlers)
|
||||
handler.ServeHTTP(recorder, request)
|
||||
|
||||
// Should return 404 because the env var doesn't belong to app2
|
||||
assert.Equal(t, http.StatusNotFound, recorder.Code)
|
||||
cfg.verifyFn(t, testCtx, resourceID)
|
||||
}
|
||||
|
||||
// Verify the env var was NOT deleted
|
||||
found, err := models.FindEnvVar(context.Background(), testCtx.database, envVar.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, found, "env var should still exist after IDOR attempt")
|
||||
// TestDeleteEnvVarOwnershipVerification tests that deleting an env var
|
||||
// via another app's URL path returns 404 (IDOR prevention).
|
||||
func TestDeleteEnvVarOwnershipVerification(t *testing.T) { //nolint:dupl // intentionally similar IDOR test pattern
|
||||
t.Parallel()
|
||||
|
||||
testOwnershipVerification(t, ownedResourceTestConfig{
|
||||
appPrefix1: "envvar-owner-app",
|
||||
appPrefix2: "envvar-other-app",
|
||||
createFn: func(t *testing.T, tc *testContext, ownerApp *models.App) int64 {
|
||||
t.Helper()
|
||||
|
||||
envVar := models.NewEnvVar(tc.database)
|
||||
envVar.AppID = ownerApp.ID
|
||||
envVar.Key = "SECRET"
|
||||
envVar.Value = "hunter2"
|
||||
require.NoError(t, envVar.Save(context.Background()))
|
||||
|
||||
return envVar.ID
|
||||
},
|
||||
deletePath: func(appID string, resourceID int64) string {
|
||||
return "/apps/" + appID + "/env/" + strconv.FormatInt(resourceID, 10) + "/delete"
|
||||
},
|
||||
chiParams: func(appID string, resourceID int64) map[string]string {
|
||||
return map[string]string{"id": appID, "envID": strconv.FormatInt(resourceID, 10)}
|
||||
},
|
||||
handler: func(h *handlers.Handlers) http.HandlerFunc { return h.HandleEnvVarDelete() },
|
||||
verifyFn: func(t *testing.T, tc *testContext, resourceID int64) {
|
||||
t.Helper()
|
||||
|
||||
found, findErr := models.FindEnvVar(context.Background(), tc.database, resourceID)
|
||||
require.NoError(t, findErr)
|
||||
assert.NotNil(t, found, "env var should still exist after IDOR attempt")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TestDeleteLabelOwnershipVerification tests that deleting a label
|
||||
// via another app's URL path returns 404 (IDOR prevention).
|
||||
func TestDeleteLabelOwnershipVerification(t *testing.T) {
|
||||
func TestDeleteLabelOwnershipVerification(t *testing.T) { //nolint:dupl // intentionally similar IDOR test pattern
|
||||
t.Parallel()
|
||||
|
||||
testCtx := setupTestHandlers(t)
|
||||
testOwnershipVerification(t, ownedResourceTestConfig{
|
||||
appPrefix1: "label-owner-app",
|
||||
appPrefix2: "label-other-app",
|
||||
createFn: func(t *testing.T, tc *testContext, ownerApp *models.App) int64 {
|
||||
t.Helper()
|
||||
|
||||
app1 := createTestApp(t, testCtx, "label-owner-app")
|
||||
app2 := createTestApp(t, testCtx, "label-other-app")
|
||||
lbl := models.NewLabel(tc.database)
|
||||
lbl.AppID = ownerApp.ID
|
||||
lbl.Key = "traefik.enable"
|
||||
lbl.Value = "true"
|
||||
require.NoError(t, lbl.Save(context.Background()))
|
||||
|
||||
// Create label belonging to app1
|
||||
label := models.NewLabel(testCtx.database)
|
||||
label.AppID = app1.ID
|
||||
label.Key = "traefik.enable"
|
||||
label.Value = "true"
|
||||
require.NoError(t, label.Save(context.Background()))
|
||||
return lbl.ID
|
||||
},
|
||||
deletePath: func(appID string, resourceID int64) string {
|
||||
return "/apps/" + appID + "/labels/" + strconv.FormatInt(resourceID, 10) + "/delete"
|
||||
},
|
||||
chiParams: func(appID string, resourceID int64) map[string]string {
|
||||
return map[string]string{"id": appID, "labelID": strconv.FormatInt(resourceID, 10)}
|
||||
},
|
||||
handler: func(h *handlers.Handlers) http.HandlerFunc { return h.HandleLabelDelete() },
|
||||
verifyFn: func(t *testing.T, tc *testContext, resourceID int64) {
|
||||
t.Helper()
|
||||
|
||||
// Try to delete app1's label using app2's URL path
|
||||
request := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/apps/"+app2.ID+"/labels/"+strconv.FormatInt(label.ID, 10)+"/delete",
|
||||
nil,
|
||||
)
|
||||
request = addChiURLParams(request, map[string]string{
|
||||
"id": app2.ID,
|
||||
"labelID": strconv.FormatInt(label.ID, 10),
|
||||
found, findErr := models.FindLabel(context.Background(), tc.database, resourceID)
|
||||
require.NoError(t, findErr)
|
||||
assert.NotNil(t, found, "label should still exist after IDOR attempt")
|
||||
},
|
||||
})
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
handler := testCtx.handlers.HandleLabelDelete()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, recorder.Code)
|
||||
|
||||
// Verify the label was NOT deleted
|
||||
found, err := models.FindLabel(context.Background(), testCtx.database, label.ID)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, found, "label should still exist after IDOR attempt")
|
||||
}
|
||||
|
||||
// TestDeleteVolumeOwnershipVerification tests that deleting a volume
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package handlers
|
||||
package handlers //nolint:testpackage // tests unexported parsePortValues function
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParsePortValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
@@ -24,6 +26,8 @@ func TestParsePortValues(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
host, cont, valid := parsePortValues(tt.host, tt.container)
|
||||
if host != tt.wantHost || cont != tt.wantCont || valid != tt.wantValid {
|
||||
t.Errorf("parsePortValues(%q, %q) = (%d, %d, %v), want (%d, %d, %v)",
|
||||
|
||||
Reference in New Issue
Block a user