Commit Graph

68 Commits

Author SHA1 Message Date
11314629b6 Merge branch 'main' into feature/json-api 2026-02-16 09:51:36 +01:00
user
8d68a31366 fix: remove undeployed api_tokens migrations (006 + 007) 2026-02-16 00:34:02 -08:00
f743837d49 Merge branch 'main' into feature/json-api 2026-02-16 09:33:09 +01:00
user
9ac1d25788 refactor: switch API from token auth to cookie-based session auth
- Remove API token system entirely (model, migration, middleware)
- Add migration 007 to drop api_tokens table
- Add POST /api/v1/login endpoint for JSON credential auth
- API routes now use session cookies (same as web UI)
- Remove /api/v1/tokens endpoint
- HandleAPIWhoAmI uses session auth instead of token context
- APISessionAuth middleware returns JSON 401 instead of redirect
- Update all API tests to use cookie-based authentication

Addresses review comment on PR #74.
2026-02-16 00:31:10 -08:00
0c8dcc2eb1 Merge branch 'main' into feature/edit-config-entities 2026-02-16 09:28:30 +01:00
e9d284698a feat: edit existing env vars, labels, and volume mounts
Add inline edit functionality for environment variables, labels, and
volume mounts on the app detail page. Each entity row now has an Edit
button that reveals an inline form using Alpine.js.

- POST /apps/{id}/env-vars/{varID}/edit
- POST /apps/{id}/labels/{labelID}/edit
- POST /apps/{id}/volumes/{volumeID}/edit
- Path validation for volume host and container paths
- Warning banner about container restart after env var changes
- Tests for ValidateVolumePath

fixes #67
2026-02-16 00:26:07 -08:00
user
0536f57ec2 feat: add JSON API with token auth (closes #69)
- Add API token model with SHA-256 hashed tokens
- Add migration 006_add_api_tokens.sql
- Add Bearer token auth middleware
- Add API endpoints under /api/v1/:
  - GET /whoami
  - POST /tokens (create new API token)
  - GET /apps (list all apps)
  - POST /apps (create app)
  - GET /apps/{id} (get app)
  - DELETE /apps/{id} (delete app)
  - POST /apps/{id}/deploy (trigger deployment)
  - GET /apps/{id}/deployments (list deployments)
- Add comprehensive tests for all API endpoints
- All tests pass, zero lint issues
2026-02-16 00:24:45 -08:00
user
2be6a748b7 feat: deployment rollback to previous image
- Add previous_image_id column to apps table (migration 006)
- Save current image as previous before deploying new one
- POST /apps/{id}/rollback endpoint with handler
- Rollback stops current container, starts previous image
- Creates deployment record for rollback operations
- Rollback button in app detail UI (only when previous image exists)
- Add btn-warning CSS class for rollback button styling

fixes #71
2026-02-16 00:23:11 -08:00
user
c5f957477f feat: add user-facing deployment cancel endpoint
Add POST /apps/{id}/deployments/cancel endpoint that allows users to
cancel in-progress deployments via the web UI.

Changes:
- Add CancelDeploy() and HasActiveDeploy() public methods to deploy service
- Add HandleCancelDeploy handler
- Wire route in routes.go
- Add cancel button to app detail template (shown during active deployments)
- Add handler tests for cancel endpoint

fixes #66
2026-02-16 00:15:24 -08:00
ebcae55302 Merge pull request 'fix: cancel in-progress deploy when webhook triggers new deploy (closes #38)' (#52) from clawbot/upaas:fix/deploy-race-condition-38 into main
Reviewed-on: #52
2026-02-16 09:06:40 +01:00
user
a80b7ac0a6 refactor: export SanitizeTail and DefaultLogTail directly instead of wrapping
- Rename sanitizeTail → SanitizeTail (exported)
- Rename defaultLogTail → DefaultLogTail (exported)
- Delete export_test.go (no longer needed)
- Update test to reference handlers.SanitizeTail/DefaultLogTail directly
2026-02-15 22:14:12 -08:00
clawbot
69a5a8c298 fix: resolve all golangci-lint issues (fixes #32) 2026-02-15 22:13:12 -08:00
3f499163a7 fix: cancel in-progress deploy when webhook triggers new deploy (closes #38)
When a webhook-triggered deploy starts for an app that already has a deploy
in progress, the existing deploy is now cancelled via context cancellation
before the new deploy begins. This prevents silently lost webhook deploys.

Changes:
- Add per-app active deploy tracking with cancel func and done channel
- Deploy() accepts cancelExisting param: true for webhook, false for manual
- Cancelled deployments are marked with new 'cancelled' status
- Add ErrDeployCancelled sentinel error
- Add DeploymentStatusCancelled model constant
- Add comprehensive tests for cancellation mechanics
2026-02-15 22:12:03 -08:00
07ac71974c Merge pull request 'fix: set DestroySession MaxAge to -1 instead of -1*time.Second (closes #39)' (#50) from clawbot/upaas:fix/destroy-session-maxage into main
Reviewed-on: #50
2026-02-16 07:09:25 +01:00
cdd7e3fd3a fix: set DestroySession MaxAge to -1 instead of -1*time.Second (closes #39)
The gorilla/sessions MaxAge field expects seconds, not nanoseconds.
Previously MaxAge was set to -1000000000 (-1 * time.Second in nanoseconds),
which worked by accident since any negative value deletes the cookie.
Changed to the conventional value of -1.
2026-02-15 22:07:57 -08:00
4f1f3e2494 Merge branch 'main' into fix/server-side-app-name-validation 2026-02-16 07:07:28 +01:00
user
d27adc040d Add server-side app name validation (closes #37)
Validate app names in both HandleAppCreate and HandleAppUpdate using
a regex pattern matching the client-side HTML pattern: lowercase
alphanumeric and hyphens, 2-63 chars, must start and end with
alphanumeric character.

This prevents Docker API errors, path traversal, and log injection
from crafted POST requests bypassing browser validation.
2026-02-15 22:06:08 -08:00
448879b4ef Merge branch 'main' into fix/template-execution-buffering 2026-02-16 07:05:36 +01:00
user
af9ffddf84 fix: buffer template execution to prevent corrupt HTML responses (closes #42)
Add renderTemplate helper method on Handlers that renders templates to a
bytes.Buffer first, then writes to the ResponseWriter only on success.
This prevents partial/corrupt HTML when template execution fails partway
through.

Applied to all template rendering call sites in:
- setup.go (HandleSetupGET, renderSetupError)
- auth.go (HandleLoginGET, HandleLoginPOST error paths)
- dashboard.go (HandleDashboard)
- app.go (HandleAppNew, HandleAppCreate, HandleAppDetail, HandleAppEdit,
  HandleAppUpdate, HandleAppDeployments)
2026-02-15 22:04:09 -08:00
b1a6fd5fca fix: only trust proxy headers from RFC1918/loopback sources (closes #44)
realIP() now parses RemoteAddr and checks if the source IP is in
RFC1918 (10/8, 172.16/12, 192.168/16), loopback (127/8), or IPv6
ULA/loopback ranges before trusting X-Real-IP or X-Forwarded-For
headers. Public source IPs have headers ignored (fail closed).

This prevents attackers from spoofing X-Forwarded-For to bypass
the login rate limiter.
2026-02-15 22:01:54 -08:00
e9bf63d18b Merge pull request 'Fix all golangci-lint issues (closes #32)' (#34) from clawbot/upaas:fix/lint-cleanup into main
Reviewed-on: #34
2026-02-16 06:57:19 +01:00
clawbot
559bfa4131 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)
2026-02-15 21:55:24 -08:00
user
300de44853 fix: validate and clamp container log tail parameter (closes #24)
- Add sanitizeTail() helper that validates tail is numeric and positive
- Clamp values to max 500
- Default to 500 when empty, non-numeric, zero, or negative
- Add comprehensive test cases
2026-02-15 21:50:00 -08:00
03b0dbeb04 Merge branch 'main' into fix/setup-race-condition-closes-26 2026-02-16 06:44:40 +01:00
user
e42f80814c fix: address noinlineerr lint warning 2026-02-15 21:43:00 -08:00
user
97a5aae2f7 simplify: replace mutex + ON CONFLICT with a single DB transaction
Remove the sync.Mutex and CreateUserAtomic (INSERT ON CONFLICT) in favor
of a single DB transaction in CreateFirstUser that atomically checks for
existing users and inserts. SQLite serializes write transactions, so this
is sufficient to prevent the race condition without application-level locking.
2026-02-15 21:41:52 -08:00
ef271d2da9 Merge pull request 'Fix command injection in git clone arguments (closes #18)' (#29) from clawbot/upaas:fix/command-injection-git-clone into main
Reviewed-on: #29
2026-02-16 06:38:29 +01:00
763e722607 fix: prevent setup endpoint race condition (closes #26)
Add mutex and INSERT ON CONFLICT to CreateUser to prevent TOCTOU race
where concurrent requests could create multiple admin users.

Changes:
- Add sync.Mutex to auth.Service to serialize CreateUser calls
- Add models.CreateUserAtomic using INSERT ... ON CONFLICT(username) DO NOTHING
- Check RowsAffected to detect conflicts at the DB level (defense-in-depth)
- Add concurrent race condition test (10 goroutines, only 1 succeeds)

The existing UNIQUE constraint on users.username was already in place.
This fix adds the application-level protection (items 1 & 2 from #26).
2026-02-15 21:35:16 -08:00
user
35ef6c8fea fix: validate port range 1-65535 in parsePortValues (closes #25)
Add upper bound check (maxPort = 65535) to reject invalid port numbers.
Add comprehensive test cases for port validation.
2026-02-15 21:34:50 -08:00
7c0278439d fix: prevent command injection in git clone arguments (closes #18)
- Validate branch names against ^[a-zA-Z0-9._/\-]+$
- Validate commit SHAs against ^[0-9a-f]{40}$
- Pass repo URL, branch, and SHA via environment variables instead of
  interpolating into shell script string
- Add comprehensive tests for validation and injection rejection
2026-02-15 21:33:02 -08:00
97ee1e212f Merge pull request 'Wait for final log flush before closing deploymentLogWriter (closes #4)' (#9) from clawbot/upaas:fix/issue-4 into main
Reviewed-on: #9
2026-02-16 06:29:18 +01:00
3e8f424129 Merge pull request 'Add rate limiting to login endpoint to prevent brute force (closes #12)' (#14) from clawbot/upaas:fix/issue-12 into main
Reviewed-on: #14
2026-02-16 06:15:48 +01:00
ef0786c4b4 fix: extract real client IP from proxy headers (X-Real-IP / X-Forwarded-For)
Behind a reverse proxy like Traefik, RemoteAddr always contains the
proxy's IP. Add realIP() helper that checks X-Real-IP first, then the
first entry of X-Forwarded-For, falling back to RemoteAddr.

Update both LoginRateLimit and Logging middleware to use realIP().
Add comprehensive tests for the new function.

Fixes #12
2026-02-15 21:14:12 -08:00
867cdf01ab fix: add ownership verification on env var, label, volume, and port deletion
Verify that the resource's AppID matches the URL path app ID before
allowing deletion. Without this check, any authenticated user could
delete resources belonging to any app by providing the target resource's
ID in the URL regardless of the app ID in the path (IDOR vulnerability).

Closes #19
2026-02-15 21:02:46 -08:00
user
a1b06219e7 fix: add eviction for stale IP rate limiter entries and Retry-After header
- Store lastSeen timestamp per IP limiter entry
- Lazy sweep removes entries older than 10 minutes on each request
- Add Retry-After header to 429 responses
- Add test for stale entry eviction

Fixes memory leak under sustained attack from many IPs.
2026-02-15 21:01:11 -08:00
clawbot
66661d1b1d Add rate limiting to login endpoint to prevent brute force
Apply per-IP rate limiting (5 attempts/minute) to POST /login using
golang.org/x/time/rate. Returns 429 Too Many Requests when exceeded.

Closes #12
2026-02-15 21:01:11 -08:00
6475389280 test: add IDOR tests for resource deletion ownership verification
Tests demonstrate that env vars, labels, volumes, and ports can be
deleted via another app's URL path without ownership checks.

All 4 tests fail, confirming the vulnerability described in #19.
2026-02-15 21:00:41 -08:00
3a2bd0e51d Merge pull request 'Set Secure flag on session cookie in production mode (closes #5)' (#10) from clawbot/upaas:fix/issue-5 into main
Reviewed-on: #10
2026-02-16 05:58:22 +01:00
79a3165f90 Merge pull request 'Clean up Docker container when deleting an app (closes #2)' (#7) from clawbot/upaas:fix/issue-2 into main
Reviewed-on: #7
2026-02-16 05:56:56 +01:00
98b8403e8b Merge branch 'main' into fix/issue-1 2026-02-16 05:56:06 +01:00
57ea724419 Merge branch 'main' into fix/issue-13 2026-02-16 05:55:17 +01:00
clawbot
b1dc8fcc4e Add CSRF protection to state-changing POST endpoints
Add gorilla/csrf middleware to protect all HTML-serving routes against
cross-site request forgery attacks. The webhook endpoint is excluded
since it uses secret-based authentication.

Changes:
- Add gorilla/csrf v1.7.3 dependency
- Add CSRF() middleware method using session secret as key
- Apply CSRF middleware to all HTML route groups in routes.go
- Pass CSRF token to all templates via addGlobals helper
- Add {{ .CSRFField }} / {{ $.CSRFField }} hidden inputs to all forms

Closes #11
2026-02-15 14:17:55 -08:00
clawbot
72786a9feb fix: use hashed webhook secrets for constant-time comparison
Store a SHA-256 hash of the webhook secret in a new webhook_secret_hash
column. FindAppByWebhookSecret now hashes the incoming secret and queries
by hash, eliminating the SQL string comparison timing side-channel.

- Add migration 005_add_webhook_secret_hash.sql
- Add database.HashWebhookSecret() helper
- Backfill existing secrets on startup
- Update App model to include WebhookSecretHash in all queries
- Update app creation to compute hash at insert time
- Add TestHashWebhookSecret unit test
- Update all test fixtures to set WebhookSecretHash

Closes #13
2026-02-15 14:06:53 -08:00
clawbot
185daab909 fix: set Secure flag on session cookie in production mode (closes #5) 2026-02-08 12:05:09 -08:00
clawbot
69456abd25 fix: wait for final log flush before closing deploymentLogWriter (closes #4) 2026-02-08 12:04:37 -08:00
clawbot
ed4ddc5536 fix: clean up Docker container when deleting an app (closes #2) 2026-02-08 12:02:56 -08:00
clawbot
e212910143 fix: limit webhook request body size to 1MB to prevent DoS (closes #1) 2026-02-08 12:02:06 -08:00
aaa55fd153 Fix app status not updated when deployment fails or service restarts
- Update app status to error when health check fails with an error
- Update app status to error in cleanupStuckDeployments for apps stuck in building state
- This fixes the inconsistency where app shows "building" but deployment is "failed"
2026-01-01 06:59:03 -08:00
ee34f3b70c Change Docker image naming to upaas-appname:deploymentID
- Use upaas-appname format instead of upaas/appname
- Tag with deployment number instead of 'latest'
- Example: upaas-myapp:42
2026-01-01 06:50:54 -08:00
2cbcd3d72a Add build log file storage and download functionality
- Write deployment logs to files when deployment finishes (success or failure)
- Log files stored in DataDir/logs/<hostname>/<appname>/<appname>_<sha>_<timestamp>.log.txt
- Capture commit SHA for manual deploys by parsing git rev-parse HEAD after clone
- Add download endpoint for log files at /apps/{id}/deployments/{deploymentID}/download
- Add download link in deployment history view for finished deployments
2026-01-01 06:08:00 -08:00