Skip to content

feat: check read, write and delete access in destination test - #20

Merged
agentHits merged 1 commit into
AgentHits-Devfrom
feat/destination-write-check
Oct 7, 2026
Merged

agentHits merged 1 commit into
AgentHits-Devfrom
feat/destination-write-check

Conversation

@agentHits

Copy link
Copy Markdown
Collaborator

What is this PR about?

The S3 destination Test button only ran rclone ls, so a key that can read but not write passed the test, and scheduled backups later failed with PutObject ... StatusCode: 403. This PR makes the test check read, write and delete access separately and show which ones work.

Server (destination.testConnection)

  • Runs three checks in order against the configured bucket, on the same target as before (execAsync locally, execAsyncRemote in cloud mode):
    • read: rclone ls of the bucket root, same retry/timeout flags as before;
    • write: rclone rcat of a tiny object at dokploy-connection-test/<timestamp>-<random>.txt (content is piped in with printf; every argument goes through shell-quote);
    • delete: rclone deletefile of that same object. Skipped when the write failed.
  • The write check still runs when read fails, so write-only keys are reported correctly.
  • If all checks pass, the mutation returns { read: { ok: true }, write: { ok: true }, delete: { ok: true } }.
  • If any check fails, it throws BAD_REQUEST with one line per check, so API/MCP clients still get an error:
    Read: OK
    Write: FAILED - 403 AccessDenied (the key cannot upload objects)
    Delete: SKIPPED - no test object was uploaded
    
    If the upload works but the delete is denied, the delete line names the leftover object (<bucket>/dokploy-connection-test/....txt).
  • Failure reasons are reduced to the HTTP status and S3 error code plus a short hint. They go through redactSensitiveText, and the access key and secret values are replaced. The command line (which carries the credentials as env assignments) never reaches the reason.
  • The stored-secret placeholder handling and its organization check are unchanged.
  • New helpers: packages/server/src/utils/destination/connection-test.ts (commands, runner, failure summary) and connection-test-report.ts (client-safe types plus format/parse of the report lines).

UI (handle-destinations.tsx)

  • After Test, the dialog shows a small list next to the Test button: a check, cross or "skipped" icon per operation, with the reason. On success it reads "Read, write and delete access confirmed".
  • The success toast is unchanged apart from a short description. For a check failure, the toast says "Destination test failed" and points to the list in the dialog. Other errors (validation, unreachable server) keep the old toast and alert.

For a read-only key, the dialog now shows:

  • ✓ Read: OK
  • ✗ Write: Failed - 403 AccessDenied (the key cannot upload objects)
  • – Delete: Skipped - no test object was uploaded

Checks

From apps/dokploy:

  • CI=true bunx vitest run --config __test__/vitest.config.ts __test__/destination/destination-connection-test.test.ts __test__/destination/destination-connection-report.test.ts: 12 passed.
    • Against the old router, all 7 new router tests fail on assertions. The report test fails on the old code because the module does not exist there.
  • CI=true bunx vitest run --config __test__/vitest.config.ts __test__/destination: 4 files, 30 passed.
  • CI=true bunx vitest run --config __test__/vitest.config.ts __test__/backup/rclone-command-boundary.test.ts: 6 passed. It now checks that all three commands pass credentials only as env and quote hostile bucket names.
  • Full suite: 234 files passed. The only failures are the *.real.test.ts files (no Docker socket locally) and two files that time out under full-suite load (better-auth-schema-compat, cloud-queue-availability). Both pass when run on their own and are unrelated to this change.

From the repo root:

  • bun run typecheck: exit 0.
  • bun run format-and-lint: 0 errors. The 33 warnings and 16 infos were already there, and none are in the changed files.

The new tests cover:

  • all checks passing, locally and on a cloud server;
  • a read-only key, where the message lists Read OK, Write FAILED and Delete SKIPPED, with no key value in it;
  • a write-only key;
  • write OK but delete denied, where the leftover path matches the uploaded object;
  • masking of keys in unrecognized rclone output;
  • running the write and delete commands under /bin/sh with a stub rclone and a hostile bucket name. Nothing gets injected, and the target is under dokploy-connection-test/.

Not checked: a real S3 endpoint. The dialog was not opened in a browser, so only typecheck and lint cover it.

Rollout notes

  • No migration or config change.
  • The Test button now writes and deletes a small object under dokploy-connection-test/ in the bucket. If the key cannot delete, that object stays and the dialog names it so it can be removed by hand.
  • A test with a broken endpoint can take longer, because up to three rclone calls run instead of one.
  • destination.testConnection now returns an object on success instead of an empty value. Callers that ignore the result are unaffected.

🤖 Generated with Claude Code

Что:
- Кнопка Test у S3-назначения теперь по очереди проверяет чтение (rclone ls корня бакета, те же retry/timeout флаги), запись (rclone rcat маленького объекта dokploy-connection-test/<время>-<случайный>.txt) и удаление этого объекта (rclone deletefile). Удаление пропускается, если запись не удалась. Команды идут туда же, что и раньше: execAsync локально или execAsyncRemote в cloud.
- При успехе testConnection возвращает { read, write, delete } с ok: true. Если хоть одна проверка не прошла, бросается BAD_REQUEST с построчным отчётом, например "Read: OK / Write: FAILED - 403 AccessDenied (the key cannot upload objects) / Delete: SKIPPED". Если объект записан, но не удалён, в отчёте указан его путь.
- Причина ошибки rclone сокращается до HTTP-статуса и кода S3, проходит через redactSensitiveText и замену значений ключей; командная строка в причину не попадает.
- Диалог назначения показывает список проверок (✓/✗/пропуск и причина) рядом с кнопкой Test; тост стал коротким и отсылает к списку.
- Новые тесты destination-connection-test.test.ts и destination-connection-report.test.ts; обновлены destination-router-assigned-server.test.ts и rclone-command-boundary.test.ts под новый результат и три команды.

Зачем:
- Ключ с правом только на чтение проходил старую проверку (только rclone ls), а ночные бэкапы падали с PutObject 403. Теперь Test показывает, какого именно доступа не хватает.

Риски:
- Проверка записи создаёт и удаляет настоящий объект в бакете под префиксом dokploy-connection-test/. Если удаление запрещено, объект остаётся, путь указан в отчёте.
- Вместо одного вызова rclone теперь до трёх, поэтому при недоступном endpoint проверка идёт дольше.
- Успешный ответ testConnection теперь объект, а не пустое значение.
- Диалог разбирает текст ошибки по формату "Read|Write|Delete: OK|FAILED|SKIPPED"; формат и разбор лежат в одном модуле connection-test-report.ts.

Проверки:
- Команды и результаты: из apps/dokploy `CI=true bunx vitest run --config __test__/vitest.config.ts __test__/destination/destination-connection-test.test.ts __test__/destination/destination-connection-report.test.ts` - 12 passed; на старом router все 7 новых router-тестов падают по assertion, report-тест падает без нового модуля; `... __test__/destination` - 4 files, 30 passed; `... __test__/backup/rclone-command-boundary.test.ts` - 6 passed; полный набор - 234 files passed, падают только *.real.test.ts (нет Docker socket) и два файла по таймауту под нагрузкой (better-auth-schema-compat, cloud-queue-availability), которые проходят отдельно. Из корня `bun run typecheck` - exit 0; `bun run format-and-lint` - 0 errors (33 warnings и 16 infos уже были, в изменённых файлах нет).
- Ограничения: не проверялось на настоящем S3 endpoint; диалог не открывался в браузере, проверен только typecheck и lint; разбор причин ошибок rclone проверен на примерах вывода, а не на всех провайдерах.

What:
- The S3 destination Test button now runs three checks in order: read (rclone ls of the bucket root with the same retry/timeout flags), write (rclone rcat of a tiny object at dokploy-connection-test/<timestamp>-<random>.txt) and delete of that object (rclone deletefile). Delete is skipped when the write fails. Commands run on the same target as before: execAsync locally or execAsyncRemote in cloud mode.
- On success testConnection returns { read, write, delete } with ok: true. If any check fails it throws BAD_REQUEST with a line per check, for example "Read: OK / Write: FAILED - 403 AccessDenied (the key cannot upload objects) / Delete: SKIPPED". When the object was written but not deleted, the report names its path.
- An rclone failure is reduced to the HTTP status and S3 error code, passed through redactSensitiveText and key-value replacement; the command line never reaches the reason.
- The destination dialog shows the per-check list (check/cross/skipped with the reason) next to the Test button; the toast is short and points to the list.
- New tests destination-connection-test.test.ts and destination-connection-report.test.ts; destination-router-assigned-server.test.ts and rclone-command-boundary.test.ts updated for the new result and the three commands.

Why:
- A read-only key passed the old check (rclone ls only) while nightly backups failed with PutObject 403. Test now shows exactly which access is missing.

Risks:
- The write check creates and deletes a real object in the bucket under dokploy-connection-test/. If delete is forbidden the object stays and its path is in the report.
- Up to three rclone calls instead of one, so the test takes longer when the endpoint is unreachable.
- A successful testConnection response is now an object instead of an empty value.
- The dialog parses the error text in the "Read|Write|Delete: OK|FAILED|SKIPPED" format; formatting and parsing live in the same connection-test-report.ts module.

Checks:
- Commands and results: from apps/dokploy `CI=true bunx vitest run --config __test__/vitest.config.ts __test__/destination/destination-connection-test.test.ts __test__/destination/destination-connection-report.test.ts` - 12 passed; against the old router all 7 new router tests fail on assertions and the report test fails without the new module; `... __test__/destination` - 4 files, 30 passed; `... __test__/backup/rclone-command-boundary.test.ts` - 6 passed; full suite - 234 files passed, only *.real.test.ts (no Docker socket) and two files timing out under load (better-auth-schema-compat, cloud-queue-availability) fail, and both pass on their own. From the root `bun run typecheck` - exit 0; `bun run format-and-lint` - 0 errors (33 warnings and 16 infos already existed, none in changed files).
- Limitations: not run against a real S3 endpoint; the dialog was not opened in a browser, only typecheck and lint cover it; rclone error parsing is checked on sample output, not on every provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@agentHits
agentHits merged commit 0cd7cca into AgentHits-Dev Oct 7, 2026
8 checks passed
@agentHits
agentHits deleted the feat/destination-write-check branch October 7, 2026 05:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant