diff --git a/CHANGELOG.md b/CHANGELOG.md index 00c37cd..9d07d58 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,7 +115,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- `testdata/cronjob/{add_cronjob_response_success,add_cronjob_response_warning,update_cronjob_response_success}.xml` +- `testdata/cronjob/{add_cronjob_response_success,add_cronjob_response_success_warning,update_cronjob_response_success}.xml` carry a top-of-file XML comment documenting that KAS itself echoes the notification address under `mail_address` (double d) in the `KasRequestParams` echo block, while the documented request key is @@ -575,6 +575,138 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Whole-codebase re-review follow-up (third pass, High + Med + Low + findings): + - Group commands (`mail`, `accounts`, `config`, ...) invoked with an + unknown subcommand no longer print help and exit 0 — a typo'd + subcommand read as success to scripts. `cli.Finalize` now gives + every non-runnable group an explicit unknown-subcommand rejection + (exit 1, with cobra's "Did you mean this?" suggestions); a bare + group invocation keeps printing help with exit 0. + - The lazily-registered `completion` command is registered before the + exit-code walkers run, so its args-validation failures exit 1 (user + error) instead of 2. + - The success audit record of create actions whose identifier KAS + generates server-side (`add_ftpuser`, `add_database`, + `add_sambauser`, `add_ddnsuser`, `add_cronjob`, `add_mailaccount`, + `add_mailforward`, `add_mailinglist`) now carries the assigned + identifier as `created_id`, so the create correlates with the + identifier later update/delete records carry as their target. + - A confirmation-prompt I/O failure (neither a yes nor a no was read) + now leaves an audit record with the new outcome `aborted` instead + of silently skipping the trace of a blocked destructive attempt. + - The transport 5xx fault sniff also recognises a prefix-less + default-namespace `` element, matching what the SOAP decoder + accepts; an end-to-end api-layer test pins that a fault delivered + with HTTP 500 surfaces as the same typed `*api.Error` a 200-wrapped + fault produces. + - `sambausers update` binds its own replacement-flag set instead of + sharing `add`'s (whose `--help` texts claimed "required for add") — + the same add/update split the cronjob/ftpuser slices got in the + second pass. + - The remaining `get_` fixtures whose filename encoded a + non-existent singular KAS action while embedding the plural one + were renamed to `__.xml` across + `testdata/{cronjob,ddns,domain,ftpuser,mailaccount,mailforward,mailinglist,sambauser}/` + (e.g. `get_ftpuser_response_success.xml` → + `get_ftpusers_response_success_single.xml`), and the convention + example in `CLAUDE.md` now uses a conforming name. + +- Whole-codebase re-review follow-up (second pass, Med + Low findings): + - Bad user input now consistently exits 1: cobra positional-args + failures (e.g. a missing required argument) and an unknown root + subcommand were falling through to the API-error exit 2. The + command tree wraps every `Args` validator via + `cli.MarkArgErrorsAsUserErrors` and the root command classifies + unknown subcommands itself. + - `transport.Client.Do` no longer blindly retries a 5xx response + whose body carries a SOAP fault: the body is passed through to the + decoder so the typed-fault path (auth refresh, flood fallback, + exit-code classification) applies. Fault-free 5xx bodies stay + retryable. + - `testutil.AssertFaultFixtures` fails on `want` entries that match + no fixture file on disk, so a renamed or deleted fixture cannot + leave a dead pin behind; the shared top-level + `response_failed_*.xml` set is now anchored by a dedicated test in + `internal/api` pinning each fixture to its `api.Code*` constant. + - `config.Resolve` with a nil config and `--profile` wraps + `ErrUnknownProfile` instead of returning a string-only error. + - `session.Store.Load` treats an entry without `expires_at` (only + producible by hand-editing sessions.toml) as expired instead of + never-expiring. + - `cronjobs update` / `ftpusers update` bind their own flag sets + instead of sharing the add flags, so `--help` no longer advertises + add defaults (`https`, `*`, `default`) or "required for add" texts + — matching the database/mailaccount/ddnsuser/mailinglist split. + - The `--dry-run` help text says "write command" instead of + "destructive command" — the flag covers non-gated `add` writes too. + - `testdata/domain/get_topleveldomains_request.xml` carried + `kas_action: get_subdomains` (mis-captured copy); corrected to the + action the filename and the response fixture encode. + - Fixture names aligned with the documented convention: + `dns/get_dns_settings_{request,response_success}_zone_host_and_record_id.xml` + (variant after kind) and + `cronjob/add_cronjob_response_success_warning.xml` (a success + variant, not a distinct status). + - `internal/{ftpuser,cronjob,mailaccount}/doc.go` no longer name + non-existent singular get actions; the stale ftpuser "verify + against the live API" note now records the #119 verification + result. The `internal/ddns` `in_progress` comment no longer claims + fixture backing the captured fixtures do not contain. + - Docs refreshed to the shipped state: README status/CI/what-it-does + (write slices are live, not "pending"), the destructive-writes + prompt example (single line, `permanently delete` verb, stderr + note), the audit-trace scope (the `sessions delete` / `config + use-profile` session logout is explicitly outside the pipeline), + `ROADMAP.md` (`mail lists get`, `` placeholder), and + the CLAUDE.md repository-state paragraph. + +- Whole-codebase review follow-up (Low findings): + - `api.TokenSource.Invalidate` now reports whether the next + Credentials call can produce fresh credentials; `api.Client` skips + the auth-failure retry for a `StaticTokenSource` instead of + doubling the failing request with identical credentials. + - `transport.Client` no longer classifies `context.Canceled` / + `DeadlineExceeded` as retryable — the caller's cancellation is not + a transient server condition. + - The destructive-write `[y/N]` prompt goes to **stderr** so a + redirected stdout cannot swallow the question. + - A dispatched write whose audit sink fails keeps its true exit + classification: a KAS fault stays exit 2 (audit failure reported as + a warning), and a successful write renders its result before the + audit error surfaces as exit 1. + - Refused (`outcome=refused`, non-TTY without `--yes`) and declined + (`outcome=declined`, prompt answered no) destructive attempts now + leave an audit record; previously only dispatched writes and + dry-runs were traced. Documented in + `docs/usage/destructive-writes.md`. + - `auth.DecodeResponse` validates the credential token shape (40 + alphanumeric characters) before it is cached and persisted; the + error reports only the length, never the content. + - `auth.EncodeRequest` range-checks `Lifetime` against the documented + 1..30000 session_lifetime bound (0 = server default). + - New `config.ErrUnknownProfile` / `config.ErrMissingCredentials` + sentinels; `Resolve` failures are `errors.Is`-able instead of + string-matchable only. + - `dns` / `directoryprotection` / `server` / `usage` now keep their + `Caller` unexported (`c`), matching the other ten modules; the + exported `API` field was construction surface no caller used. + - `cronjob.FieldID` replaces three hardcoded `"cronjob_id"` literals. + - `internal/{softwareinstall,directoryprotection,database}/doc.go` + no longer name non-existent KAS actions (`get_softwareinstalls`, + `get_directoryprotections`, `get_database`); fixtures encoding + those non-actions in their filenames were renamed to the real + action plus a variant suffix + (`get_databases_{request,response_success}_single.xml`, + `get_directoryprotection_{request,response_success}_all.xml`, + `get_softwareinstall_{request,response_success}_all.xml`). + - The captured `testdata/{chown,ssl,symlink}/` fault fixtures (#125 + placeholders) are now anchored by one-line + `testutil.AssertFaultFixtures` tests instead of sitting + unreferenced. + - Fixture values `info@example1.org` and `/example-new.com/` replaced + with RFC 2606-reserved names (`example.org`, `example.net`). + - Whole-codebase review follow-up (Med findings): - `transport.Client` now enforces the 16-MB `soap.MaxResponseBytes` cap at the HTTP body read. Previously the cap lived only in the diff --git a/CLAUDE.md b/CLAUDE.md index f87bbe4..6d18eec 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,9 +4,9 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository State -The read-phase modules are wired up (accounts, server, domains/subdomains/TLDs, DNS, mail, databases, FTP/Samba users, cronjobs, directory protection, software installs, DDNS users, usage statistics) and the standard CI gate (`lint & test` + `docs sync`) runs on every push and pull request. `main` is protected: signed commits are required, force-pushes to `main` are blocked for the GitHub UI / `gh pr merge --rebase` (signatures are stripped server-side), and merging happens via a locally-rebased fast-forward push by the maintainer (see `.claude/skills/kasapi-cli-git-workflow/SKILL.md`). +The read-phase modules are wired up (accounts, server, domains/subdomains/TLDs, DNS, mail, databases, FTP/Samba users, cronjobs, directory protection, software installs, DDNS users, usage statistics), and so are most v0.2.0 write slices (mail accounts/forwards/filters/lists, databases, FTP/Samba users, DDNS users, cronjobs, directory protection) including the destructive-write safety contract (confirmation gate, `--dry-run`, audit records — see `docs/usage/destructive-writes.md`). The CI gate (`lint & test`, `docs sync`, `goreleaser` config check, `govulncheck`, CodeQL) runs on every push and pull request. `main` is protected: signed commits are required, force-pushes to `main` are blocked for the GitHub UI / `gh pr merge --rebase` (signatures are stripped server-side), and merging happens via a locally-rebased fast-forward push by the maintainer (see `.claude/skills/kasapi-cli-git-workflow/SKILL.md`). -Write paths and the remaining read endpoints are part of the v0.2.0 backlog tracked on the *kasapi-cli v0.1.0* GitHub project; do not invent endpoints not documented at . +The remaining write endpoints (software installs, filesystem/SSL helpers) are part of the v0.2.0 backlog tracked on the *kasapi-cli v0.1.0* GitHub project; do not invent endpoints not documented at . There is no predecessor library and no inherited backlog. Do not assume or import patterns from any other KAS client; design from the KAS API docs and the fixtures in `testdata/`. @@ -31,7 +31,7 @@ Filename convention: - One file per `(kas_action, kind)` pair: `_[_].xml`, where `kind` is `request` or `response_`, and `status` is `success` or `failed`. Examples: - `get_accounts_response_success.xml` - `add_account_response_failed_account_kas_password_syntax_incorrect.xml` - - `get_ftpuser_response_success_empty_list.xml` (variant of the success shape) + - `get_ftpusers_response_success_empty_list.xml` (variant of the success shape; the `` prefix is always the real — plural — action the file embeds, never a singular alias) Add a new fixture whenever a new KAS call is wired up; redact secrets before committing. diff --git a/README.md b/README.md index 6c02cdd..1f11205 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,13 @@ ## Status -Early development. The transport, authentication, configuration, and the v0.1.0 read modules are wired up; write paths and the remaining read endpoints are still pending — see [ROADMAP.md](ROADMAP.md) for the current state. The repository also ships recorded KAS-API response fixtures under `testdata/` that drive offline parser tests. +Early development. The transport, authentication, configuration, the v0.1.0 read modules, and most of the v0.2.0 write paths (mail accounts/forwards/filters/lists, databases, FTP/Samba users, DDNS users, cronjobs, directory protection) are wired up. The remaining write endpoints (software installs, the filesystem/SSL helpers) are still pending — see [ROADMAP.md](ROADMAP.md) for the current state. The repository also ships recorded KAS-API response fixtures under `testdata/` that drive offline parser tests. -CI gates `gofmt`/`go vet`/`golangci-lint` (with `gosec`)/`go test`/`go test -race`/`go build`, plus a `govulncheck` job on every PR. Dependabot keeps `gomod` and `github-actions` versions current. +CI gates `gofmt`/`go vet`/`golangci-lint` (with `gosec`)/`go test`/`go test -race`/`go build`, a docs-sync job (`make docs` must produce no diff against the checked-in `docs/cli/`), a `goreleaser` config check, CodeQL, plus a `govulncheck` job on every PR. Dependabot keeps `gomod` and `github-actions` versions current. ## What it does -`kasapi-cli` is a command-line client for the All-Inkl KAS-API. It wraps the SOAP/`ns2:Map` wire format the API uses, handles the `KasAuth` credential-token flow (plain or session, optional 2FA), enforces the `KasFloodDelay` between calls, and exposes read operations for the resources documented at . Write paths are scheduled for v0.2.0 — see [ROADMAP.md](ROADMAP.md). +`kasapi-cli` is a command-line client for the All-Inkl KAS-API. It wraps the SOAP/`ns2:Map` wire format the API uses, handles the `KasAuth` credential-token flow (plain or session, optional 2FA), enforces the `KasFloodDelay` between calls, and exposes read and write operations for the resources documented at . Destructive writes are gated behind a confirmation prompt, support `--dry-run`, and leave an audit trace — see [docs/usage/destructive-writes.md](docs/usage/destructive-writes.md). The remaining write endpoints are tracked in [ROADMAP.md](ROADMAP.md). ## Install diff --git a/ROADMAP.md b/ROADMAP.md index db6c5b6..3d5c916 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -58,7 +58,7 @@ Cross-cutting prerequisites for the v0.2.0 write phase — these are not KAS-API - [x] `mail forwards add/update/delete` (`add_mailforward`, `update_mailforward`, `delete_mailforward`, #115) - [x] `mail filters list` (`get_mailstandardfilter`) - [x] `mail filters add/delete` (`add_mailstandardfilter`, `delete_mailstandardfilter`, #116) -- [x] `mail lists list` (`get_mailinglists`) +- [x] `mail lists list` / `mail lists get ` (`get_mailinglists`) - [x] `mail lists add/update/delete` (`add_mailinglist`, `update_mailinglist`, `delete_mailinglist`, #117) ## Hosting resources @@ -69,7 +69,7 @@ Cross-cutting prerequisites for the v0.2.0 write phase — these are not KAS-API - [x] `ftpusers add/update/delete` (`add_ftpuser`, `update_ftpuser`, `delete_ftpuser`, #119) - [x] `sambausers list` / `sambausers get ` (`get_sambausers`, with `samba_login` filter) - [x] `sambausers add/update/delete` (`add_sambauser`, `update_sambauser`, `delete_sambauser`, #120) -- [x] `ddnsusers list` / `ddnsusers get ` (`get_ddnsusers`, with `ddns_login` filter) +- [x] `ddnsusers list` / `ddnsusers get ` (`get_ddnsusers`, with `ddns_login` filter) - [x] `ddnsusers add/update/delete` (`add_ddnsuser`, `update_ddnsuser`, `delete_ddnsuser`, #121) - [x] `cronjobs list` / `cronjobs get ` (`get_cronjobs`, with `cronjob_id` filter) - [x] `cronjobs add/update/delete` (`add_cronjob`, `update_cronjob`, `delete_cronjob`, #118) diff --git a/cmd/kasapi-cli/main.go b/cmd/kasapi-cli/main.go index 14b22c5..66db5e4 100644 --- a/cmd/kasapi-cli/main.go +++ b/cmd/kasapi-cli/main.go @@ -34,6 +34,7 @@ func main() { cli.NewConfigCmd(opts), cli.NewGenDocsCmd(), ) + cli.Finalize(root) if err := root.Execute(); err != nil { fmt.Fprintln(os.Stderr, "kasapi-cli:", err) os.Exit(cli.CodeFor(err)) diff --git a/docs/cli/kasapi-cli.md b/docs/cli/kasapi-cli.md index 3084403..fa30b08 100644 --- a/docs/cli/kasapi-cli.md +++ b/docs/cli/kasapi-cli.md @@ -17,7 +17,7 @@ kasapi-cli [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output -h, --help help for kasapi-cli --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. diff --git a/docs/cli/kasapi-cli_accounts.md b/docs/cli/kasapi-cli_accounts.md index 90d80b5..177fbdb 100644 --- a/docs/cli/kasapi-cli_accounts.md +++ b/docs/cli/kasapi-cli_accounts.md @@ -2,6 +2,10 @@ Inspect KAS accounts owned by the authenticated login +``` +kasapi-cli accounts [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect KAS accounts owned by the authenticated login --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_accounts_get.md b/docs/cli/kasapi-cli_accounts_get.md index 15d4559..e1102da 100644 --- a/docs/cli/kasapi-cli_accounts_get.md +++ b/docs/cli/kasapi-cli_accounts_get.md @@ -19,7 +19,7 @@ kasapi-cli accounts get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_accounts_list.md b/docs/cli/kasapi-cli_accounts_list.md index 65bd227..73cb1df 100644 --- a/docs/cli/kasapi-cli_accounts_list.md +++ b/docs/cli/kasapi-cli_accounts_list.md @@ -19,7 +19,7 @@ kasapi-cli accounts list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_accounts_resources.md b/docs/cli/kasapi-cli_accounts_resources.md index 1ede531..ec6eb55 100644 --- a/docs/cli/kasapi-cli_accounts_resources.md +++ b/docs/cli/kasapi-cli_accounts_resources.md @@ -19,7 +19,7 @@ kasapi-cli accounts resources [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_accounts_settings.md b/docs/cli/kasapi-cli_accounts_settings.md index f571e80..03ec835 100644 --- a/docs/cli/kasapi-cli_accounts_settings.md +++ b/docs/cli/kasapi-cli_accounts_settings.md @@ -19,7 +19,7 @@ kasapi-cli accounts settings [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_completion.md b/docs/cli/kasapi-cli_completion.md index 8855a8a..0cd88ad 100644 --- a/docs/cli/kasapi-cli_completion.md +++ b/docs/cli/kasapi-cli_completion.md @@ -8,6 +8,10 @@ Generate the autocompletion script for kasapi-cli for the specified shell. See each sub-command's help for details on how to use the generated script. +``` +kasapi-cli completion [flags] +``` + ### Options ``` @@ -21,7 +25,7 @@ See each sub-command's help for details on how to use the generated script. --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_completion_bash.md b/docs/cli/kasapi-cli_completion_bash.md index c107f51..a2017c5 100644 --- a/docs/cli/kasapi-cli_completion_bash.md +++ b/docs/cli/kasapi-cli_completion_bash.md @@ -44,7 +44,7 @@ kasapi-cli completion bash --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_completion_fish.md b/docs/cli/kasapi-cli_completion_fish.md index ad87526..aefc6ad 100644 --- a/docs/cli/kasapi-cli_completion_fish.md +++ b/docs/cli/kasapi-cli_completion_fish.md @@ -35,7 +35,7 @@ kasapi-cli completion fish [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_completion_powershell.md b/docs/cli/kasapi-cli_completion_powershell.md index f2a33dd..0eef938 100644 --- a/docs/cli/kasapi-cli_completion_powershell.md +++ b/docs/cli/kasapi-cli_completion_powershell.md @@ -32,7 +32,7 @@ kasapi-cli completion powershell [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_completion_zsh.md b/docs/cli/kasapi-cli_completion_zsh.md index ef559f2..2872683 100644 --- a/docs/cli/kasapi-cli_completion_zsh.md +++ b/docs/cli/kasapi-cli_completion_zsh.md @@ -46,7 +46,7 @@ kasapi-cli completion zsh [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_config.md b/docs/cli/kasapi-cli_config.md index 5c10898..9769796 100644 --- a/docs/cli/kasapi-cli_config.md +++ b/docs/cli/kasapi-cli_config.md @@ -2,6 +2,10 @@ Inspect and bootstrap the kasapi-cli configuration +``` +kasapi-cli config [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and bootstrap the kasapi-cli configuration --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_config_add-profile.md b/docs/cli/kasapi-cli_config_add-profile.md index 4f8569b..bc44bb9 100644 --- a/docs/cli/kasapi-cli_config_add-profile.md +++ b/docs/cli/kasapi-cli_config_add-profile.md @@ -20,7 +20,7 @@ kasapi-cli config add-profile [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_config_init.md b/docs/cli/kasapi-cli_config_init.md index 17f61d8..2d9879b 100644 --- a/docs/cli/kasapi-cli_config_init.md +++ b/docs/cli/kasapi-cli_config_init.md @@ -21,7 +21,7 @@ kasapi-cli config init [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_config_list-profiles.md b/docs/cli/kasapi-cli_config_list-profiles.md index 5cff09a..8b0341d 100644 --- a/docs/cli/kasapi-cli_config_list-profiles.md +++ b/docs/cli/kasapi-cli_config_list-profiles.md @@ -19,7 +19,7 @@ kasapi-cli config list-profiles [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_config_path.md b/docs/cli/kasapi-cli_config_path.md index 10b23ee..1a5fb78 100644 --- a/docs/cli/kasapi-cli_config_path.md +++ b/docs/cli/kasapi-cli_config_path.md @@ -19,7 +19,7 @@ kasapi-cli config path [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_config_show.md b/docs/cli/kasapi-cli_config_show.md index 77988e8..aaa2081 100644 --- a/docs/cli/kasapi-cli_config_show.md +++ b/docs/cli/kasapi-cli_config_show.md @@ -19,7 +19,7 @@ kasapi-cli config show [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_config_use-profile.md b/docs/cli/kasapi-cli_config_use-profile.md index d1b8d48..c21e60e 100644 --- a/docs/cli/kasapi-cli_config_use-profile.md +++ b/docs/cli/kasapi-cli_config_use-profile.md @@ -19,7 +19,7 @@ kasapi-cli config use-profile [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_cronjobs.md b/docs/cli/kasapi-cli_cronjobs.md index bebaac0..be5aa95 100644 --- a/docs/cli/kasapi-cli_cronjobs.md +++ b/docs/cli/kasapi-cli_cronjobs.md @@ -2,6 +2,10 @@ Inspect and manage cronjobs (get/add/update/delete_cronjob) +``` +kasapi-cli cronjobs [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and manage cronjobs (get/add/update/delete_cronjob) --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_cronjobs_add.md b/docs/cli/kasapi-cli_cronjobs_add.md index b10ffb3..d2b28b3 100644 --- a/docs/cli/kasapi-cli_cronjobs_add.md +++ b/docs/cli/kasapi-cli_cronjobs_add.md @@ -10,20 +10,20 @@ kasapi-cli cronjobs add --url --comment --minute --hour [fl ``` --active whether the cronjob is active; pass --active=false to disable (default true) - --comment string cronjob comment / label (required for add) + --comment string cronjob comment / label (required) --day-of-month string schedule day-of-month field (default "*") --day-of-week string schedule day-of-week field (0-7, Sun=0|7) (default "*") -h, --help help for add - --hour string schedule hour field (required for add) + --hour string schedule hour field (required) --http-password string HTTP basic-auth password for the call --http-user string HTTP basic-auth user for the call --mail-address string notification mail address (mail_adress) --mail-condition string when to send the notification mail --mail-subject string notification mail subject (default|comment) (default "default") - --minute string schedule minute field (required for add) + --minute string schedule minute field (required) --month string schedule month field (default "*") --protocol string request protocol (http|https) (default "https") - --url string URL to call (http_url; required for add) + --url string URL to call (http_url; required) ``` ### Options inherited from parent commands @@ -33,7 +33,7 @@ kasapi-cli cronjobs add --url --comment --minute --hour [fl --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_cronjobs_delete.md b/docs/cli/kasapi-cli_cronjobs_delete.md index 368958a..8d26c2d 100644 --- a/docs/cli/kasapi-cli_cronjobs_delete.md +++ b/docs/cli/kasapi-cli_cronjobs_delete.md @@ -19,7 +19,7 @@ kasapi-cli cronjobs delete [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_cronjobs_get.md b/docs/cli/kasapi-cli_cronjobs_get.md index 2e55904..531ed51 100644 --- a/docs/cli/kasapi-cli_cronjobs_get.md +++ b/docs/cli/kasapi-cli_cronjobs_get.md @@ -19,7 +19,7 @@ kasapi-cli cronjobs get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_cronjobs_list.md b/docs/cli/kasapi-cli_cronjobs_list.md index 751d14f..a5f9069 100644 --- a/docs/cli/kasapi-cli_cronjobs_list.md +++ b/docs/cli/kasapi-cli_cronjobs_list.md @@ -19,7 +19,7 @@ kasapi-cli cronjobs list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_cronjobs_update.md b/docs/cli/kasapi-cli_cronjobs_update.md index 83a175a..c2ef8bf 100644 --- a/docs/cli/kasapi-cli_cronjobs_update.md +++ b/docs/cli/kasapi-cli_cronjobs_update.md @@ -9,21 +9,21 @@ kasapi-cli cronjobs update [schedule/mail flags] [flags] ### Options ``` - --active whether the cronjob is active; pass --active=false to disable (default true) - --comment string cronjob comment / label (required for add) - --day-of-month string schedule day-of-month field (default "*") - --day-of-week string schedule day-of-week field (0-7, Sun=0|7) (default "*") + --active replacement active state; pass --active=false to disable (default true) + --comment string replacement cronjob comment / label + --day-of-month string replacement schedule day-of-month field + --day-of-week string replacement schedule day-of-week field (0-7, Sun=0|7) -h, --help help for update - --hour string schedule hour field (required for add) - --http-password string HTTP basic-auth password for the call - --http-user string HTTP basic-auth user for the call - --mail-address string notification mail address (mail_adress) - --mail-condition string when to send the notification mail - --mail-subject string notification mail subject (default|comment) (default "default") - --minute string schedule minute field (required for add) - --month string schedule month field (default "*") - --protocol string request protocol (http|https) (default "https") - --url string URL to call (http_url; required for add) + --hour string replacement schedule hour field + --http-password string replacement HTTP basic-auth password for the call + --http-user string replacement HTTP basic-auth user for the call + --mail-address string replacement notification mail address (mail_adress) + --mail-condition string replacement notification-mail condition + --mail-subject string replacement notification mail subject (default|comment) + --minute string replacement schedule minute field + --month string replacement schedule month field + --protocol string replacement request protocol (http|https) + --url string replacement URL to call (http_url) ``` ### Options inherited from parent commands @@ -33,7 +33,7 @@ kasapi-cli cronjobs update [schedule/mail flags] [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_databases.md b/docs/cli/kasapi-cli_databases.md index e938226..ef2a1e4 100644 --- a/docs/cli/kasapi-cli_databases.md +++ b/docs/cli/kasapi-cli_databases.md @@ -2,6 +2,10 @@ Inspect and manage databases (get/add/update/delete_database) +``` +kasapi-cli databases [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and manage databases (get/add/update/delete_database) --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_databases_add.md b/docs/cli/kasapi-cli_databases_add.md index 0e9909f..5282e04 100644 --- a/docs/cli/kasapi-cli_databases_add.md +++ b/docs/cli/kasapi-cli_databases_add.md @@ -34,7 +34,7 @@ kasapi-cli databases add --password --comment [--allowed-hosts [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_databases_get.md b/docs/cli/kasapi-cli_databases_get.md index b4c3cbe..107a554 100644 --- a/docs/cli/kasapi-cli_databases_get.md +++ b/docs/cli/kasapi-cli_databases_get.md @@ -19,7 +19,7 @@ kasapi-cli databases get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_databases_list.md b/docs/cli/kasapi-cli_databases_list.md index 6df5ed6..bf8d094 100644 --- a/docs/cli/kasapi-cli_databases_list.md +++ b/docs/cli/kasapi-cli_databases_list.md @@ -19,7 +19,7 @@ kasapi-cli databases list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_databases_update.md b/docs/cli/kasapi-cli_databases_update.md index a16663a..ff47247 100644 --- a/docs/cli/kasapi-cli_databases_update.md +++ b/docs/cli/kasapi-cli_databases_update.md @@ -22,7 +22,7 @@ kasapi-cli databases update [password/comment/allowed-hosts fla --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ddnsusers.md b/docs/cli/kasapi-cli_ddnsusers.md index 4ef37e0..e9fdfff 100644 --- a/docs/cli/kasapi-cli_ddnsusers.md +++ b/docs/cli/kasapi-cli_ddnsusers.md @@ -2,6 +2,10 @@ Inspect and manage DDNS users (get/add/update/delete_ddnsuser) +``` +kasapi-cli ddnsusers [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and manage DDNS users (get/add/update/delete_ddnsuser) --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ddnsusers_add.md b/docs/cli/kasapi-cli_ddnsusers_add.md index b0c10cb..de06280 100644 --- a/docs/cli/kasapi-cli_ddnsusers_add.md +++ b/docs/cli/kasapi-cli_ddnsusers_add.md @@ -37,7 +37,7 @@ kasapi-cli ddnsusers add --password --zone --label --target-ip --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ddnsusers_delete.md b/docs/cli/kasapi-cli_ddnsusers_delete.md index 608bfa7..f52e499 100644 --- a/docs/cli/kasapi-cli_ddnsusers_delete.md +++ b/docs/cli/kasapi-cli_ddnsusers_delete.md @@ -19,7 +19,7 @@ kasapi-cli ddnsusers delete [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ddnsusers_get.md b/docs/cli/kasapi-cli_ddnsusers_get.md index 2d9ae86..b7c8099 100644 --- a/docs/cli/kasapi-cli_ddnsusers_get.md +++ b/docs/cli/kasapi-cli_ddnsusers_get.md @@ -19,7 +19,7 @@ kasapi-cli ddnsusers get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ddnsusers_list.md b/docs/cli/kasapi-cli_ddnsusers_list.md index 4d42e8e..2845c71 100644 --- a/docs/cli/kasapi-cli_ddnsusers_list.md +++ b/docs/cli/kasapi-cli_ddnsusers_list.md @@ -19,7 +19,7 @@ kasapi-cli ddnsusers list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ddnsusers_update.md b/docs/cli/kasapi-cli_ddnsusers_update.md index 553d8a4..5121b13 100644 --- a/docs/cli/kasapi-cli_ddnsusers_update.md +++ b/docs/cli/kasapi-cli_ddnsusers_update.md @@ -24,7 +24,7 @@ kasapi-cli ddnsusers update [password/target/dual-stack flags] [f --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_directoryprotection.md b/docs/cli/kasapi-cli_directoryprotection.md index 96d36e7..1bc3fd7 100644 --- a/docs/cli/kasapi-cli_directoryprotection.md +++ b/docs/cli/kasapi-cli_directoryprotection.md @@ -2,6 +2,10 @@ Inspect and manage directory (htaccess) protections (get/add/update/delete_directoryprotection) +``` +kasapi-cli directoryprotection [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and manage directory (htaccess) protections (get/add/update/delete_direc --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_directoryprotection_add.md b/docs/cli/kasapi-cli_directoryprotection_add.md index 92124cb..d4b00da 100644 --- a/docs/cli/kasapi-cli_directoryprotection_add.md +++ b/docs/cli/kasapi-cli_directoryprotection_add.md @@ -21,7 +21,7 @@ kasapi-cli directoryprotection add --password [--authname [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_directoryprotection_list.md b/docs/cli/kasapi-cli_directoryprotection_list.md index 8fc5a5b..fbbc679 100644 --- a/docs/cli/kasapi-cli_directoryprotection_list.md +++ b/docs/cli/kasapi-cli_directoryprotection_list.md @@ -20,7 +20,7 @@ kasapi-cli directoryprotection list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_directoryprotection_update.md b/docs/cli/kasapi-cli_directoryprotection_update.md index 4896a6e..1246443 100644 --- a/docs/cli/kasapi-cli_directoryprotection_update.md +++ b/docs/cli/kasapi-cli_directoryprotection_update.md @@ -21,7 +21,7 @@ kasapi-cli directoryprotection update [--password ] [--authnam --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_dns.md b/docs/cli/kasapi-cli_dns.md index 5d6f96b..69a37ab 100644 --- a/docs/cli/kasapi-cli_dns.md +++ b/docs/cli/kasapi-cli_dns.md @@ -2,6 +2,10 @@ Inspect DNS records for a zone +``` +kasapi-cli dns [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect DNS records for a zone --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_dns_list.md b/docs/cli/kasapi-cli_dns_list.md index c480920..a7fcfb6 100644 --- a/docs/cli/kasapi-cli_dns_list.md +++ b/docs/cli/kasapi-cli_dns_list.md @@ -21,7 +21,7 @@ kasapi-cli dns list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_domains.md b/docs/cli/kasapi-cli_domains.md index 8bb0ba0..c63926d 100644 --- a/docs/cli/kasapi-cli_domains.md +++ b/docs/cli/kasapi-cli_domains.md @@ -2,6 +2,10 @@ Inspect domains owned by the authenticated account +``` +kasapi-cli domains [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect domains owned by the authenticated account --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_domains_get.md b/docs/cli/kasapi-cli_domains_get.md index ef03887..a80562e 100644 --- a/docs/cli/kasapi-cli_domains_get.md +++ b/docs/cli/kasapi-cli_domains_get.md @@ -19,7 +19,7 @@ kasapi-cli domains get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_domains_list.md b/docs/cli/kasapi-cli_domains_list.md index b1690c3..e9ce923 100644 --- a/docs/cli/kasapi-cli_domains_list.md +++ b/docs/cli/kasapi-cli_domains_list.md @@ -19,7 +19,7 @@ kasapi-cli domains list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ftpusers.md b/docs/cli/kasapi-cli_ftpusers.md index 2572160..7db7b1d 100644 --- a/docs/cli/kasapi-cli_ftpusers.md +++ b/docs/cli/kasapi-cli_ftpusers.md @@ -2,6 +2,10 @@ Inspect and manage FTP users (get/add/update/delete_ftpuser) +``` +kasapi-cli ftpusers [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and manage FTP users (get/add/update/delete_ftpuser) --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ftpusers_add.md b/docs/cli/kasapi-cli_ftpusers_add.md index ce233f0..519b596 100644 --- a/docs/cli/kasapi-cli_ftpusers_add.md +++ b/docs/cli/kasapi-cli_ftpusers_add.md @@ -9,9 +9,9 @@ kasapi-cli ftpusers add --password --comment [flags] ### Options ``` - --comment string user comment / label (required for add) + --comment string user comment / label (required) -h, --help help for add - --password string FTP password (required for add; new password for update) + --password string FTP password (required) --path string home directory the user is jailed to (ftp_path) (default "/") --permission-list grant directory-list access; pass --permission-list=false to deny (default true) --permission-read grant read access; pass --permission-read=false to deny (default true) @@ -26,7 +26,7 @@ kasapi-cli ftpusers add --password --comment [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ftpusers_delete.md b/docs/cli/kasapi-cli_ftpusers_delete.md index 7507604..a576855 100644 --- a/docs/cli/kasapi-cli_ftpusers_delete.md +++ b/docs/cli/kasapi-cli_ftpusers_delete.md @@ -19,7 +19,7 @@ kasapi-cli ftpusers delete [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ftpusers_get.md b/docs/cli/kasapi-cli_ftpusers_get.md index adaf910..be410a1 100644 --- a/docs/cli/kasapi-cli_ftpusers_get.md +++ b/docs/cli/kasapi-cli_ftpusers_get.md @@ -19,7 +19,7 @@ kasapi-cli ftpusers get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ftpusers_list.md b/docs/cli/kasapi-cli_ftpusers_list.md index 9a6d1dc..b859c3a 100644 --- a/docs/cli/kasapi-cli_ftpusers_list.md +++ b/docs/cli/kasapi-cli_ftpusers_list.md @@ -19,7 +19,7 @@ kasapi-cli ftpusers list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_ftpusers_update.md b/docs/cli/kasapi-cli_ftpusers_update.md index 2a862db..181c221 100644 --- a/docs/cli/kasapi-cli_ftpusers_update.md +++ b/docs/cli/kasapi-cli_ftpusers_update.md @@ -9,14 +9,14 @@ kasapi-cli ftpusers update [password/permission flags] [flags] ### Options ``` - --comment string user comment / label (required for add) + --comment string replacement user comment / label -h, --help help for update - --password string FTP password (required for add; new password for update) - --path string home directory the user is jailed to (ftp_path) (default "/") - --permission-list grant directory-list access; pass --permission-list=false to deny (default true) - --permission-read grant read access; pass --permission-read=false to deny (default true) - --permission-write grant write access; pass --permission-write=false to deny (default true) - --virus-clamav enable ClamAV scanning; pass --virus-clamav=false to disable (default true) + --password string replacement FTP password (sent as ftp_new_password) + --path string replacement home directory the user is jailed to (ftp_path) + --permission-list replacement directory-list access; pass --permission-list=false to deny (default true) + --permission-read replacement read access; pass --permission-read=false to deny (default true) + --permission-write replacement write access; pass --permission-write=false to deny (default true) + --virus-clamav replacement ClamAV scanning; pass --virus-clamav=false to disable (default true) ``` ### Options inherited from parent commands @@ -26,7 +26,7 @@ kasapi-cli ftpusers update [password/permission flags] [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail.md b/docs/cli/kasapi-cli_mail.md index bc4a36a..3ef6910 100644 --- a/docs/cli/kasapi-cli_mail.md +++ b/docs/cli/kasapi-cli_mail.md @@ -2,6 +2,10 @@ Inspect mail accounts and filters; inspect and manage forwards and mailing lists +``` +kasapi-cli mail [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect mail accounts and filters; inspect and manage forwards and mailing lists --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_accounts.md b/docs/cli/kasapi-cli_mail_accounts.md index b683a5c..7d5503b 100644 --- a/docs/cli/kasapi-cli_mail_accounts.md +++ b/docs/cli/kasapi-cli_mail_accounts.md @@ -2,6 +2,10 @@ Inspect and manage mail accounts (get/add/update/delete_mailaccount) +``` +kasapi-cli mail accounts [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and manage mail accounts (get/add/update/delete_mailaccount) --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_accounts_add.md b/docs/cli/kasapi-cli_mail_accounts_add.md index 44557f2..3d29bf7 100644 --- a/docs/cli/kasapi-cli_mail_accounts_add.md +++ b/docs/cli/kasapi-cli_mail_accounts_add.md @@ -45,7 +45,7 @@ kasapi-cli mail accounts add
--password [field flags] [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_accounts_delete.md b/docs/cli/kasapi-cli_mail_accounts_delete.md index 6ab52c9..0121e0a 100644 --- a/docs/cli/kasapi-cli_mail_accounts_delete.md +++ b/docs/cli/kasapi-cli_mail_accounts_delete.md @@ -19,7 +19,7 @@ kasapi-cli mail accounts delete [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_accounts_get.md b/docs/cli/kasapi-cli_mail_accounts_get.md index 9ee0ebd..f1dc659 100644 --- a/docs/cli/kasapi-cli_mail_accounts_get.md +++ b/docs/cli/kasapi-cli_mail_accounts_get.md @@ -19,7 +19,7 @@ kasapi-cli mail accounts get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_accounts_list.md b/docs/cli/kasapi-cli_mail_accounts_list.md index 5b01d1c..3568c7a 100644 --- a/docs/cli/kasapi-cli_mail_accounts_list.md +++ b/docs/cli/kasapi-cli_mail_accounts_list.md @@ -19,7 +19,7 @@ kasapi-cli mail accounts list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_accounts_update.md b/docs/cli/kasapi-cli_mail_accounts_update.md index 2af6460..2de778f 100644 --- a/docs/cli/kasapi-cli_mail_accounts_update.md +++ b/docs/cli/kasapi-cli_mail_accounts_update.md @@ -35,7 +35,7 @@ kasapi-cli mail accounts update [field flags] [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_filters.md b/docs/cli/kasapi-cli_mail_filters.md index 400a31e..9baa94c 100644 --- a/docs/cli/kasapi-cli_mail_filters.md +++ b/docs/cli/kasapi-cli_mail_filters.md @@ -2,6 +2,10 @@ Inspect and manage mail standard filters (get/add/delete_mailstandardfilter) +``` +kasapi-cli mail filters [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and manage mail standard filters (get/add/delete_mailstandardfilter) --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_filters_add.md b/docs/cli/kasapi-cli_mail_filters_add.md index a1589eb..1e01069 100644 --- a/docs/cli/kasapi-cli_mail_filters_add.md +++ b/docs/cli/kasapi-cli_mail_filters_add.md @@ -29,7 +29,7 @@ kasapi-cli mail filters add --filter [--filter ...] [f --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_filters_delete.md b/docs/cli/kasapi-cli_mail_filters_delete.md index cbb8bd9..ed090d0 100644 --- a/docs/cli/kasapi-cli_mail_filters_delete.md +++ b/docs/cli/kasapi-cli_mail_filters_delete.md @@ -31,7 +31,7 @@ kasapi-cli mail filters delete [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_filters_list.md b/docs/cli/kasapi-cli_mail_filters_list.md index 8e7f1da..a774206 100644 --- a/docs/cli/kasapi-cli_mail_filters_list.md +++ b/docs/cli/kasapi-cli_mail_filters_list.md @@ -19,7 +19,7 @@ kasapi-cli mail filters list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_forwards.md b/docs/cli/kasapi-cli_mail_forwards.md index 344a15f..292e3c5 100644 --- a/docs/cli/kasapi-cli_mail_forwards.md +++ b/docs/cli/kasapi-cli_mail_forwards.md @@ -2,6 +2,10 @@ Inspect and manage mail forwards (get/add/update/delete_mailforward) +``` +kasapi-cli mail forwards [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and manage mail forwards (get/add/update/delete_mailforward) --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_forwards_add.md b/docs/cli/kasapi-cli_mail_forwards_add.md index d6be65d..ac3c48b 100644 --- a/docs/cli/kasapi-cli_mail_forwards_add.md +++ b/docs/cli/kasapi-cli_mail_forwards_add.md @@ -20,7 +20,7 @@ kasapi-cli mail forwards add
--target [--target ...] [fla --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_forwards_delete.md b/docs/cli/kasapi-cli_mail_forwards_delete.md index 1781fae..f8281c5 100644 --- a/docs/cli/kasapi-cli_mail_forwards_delete.md +++ b/docs/cli/kasapi-cli_mail_forwards_delete.md @@ -19,7 +19,7 @@ kasapi-cli mail forwards delete
[flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_forwards_get.md b/docs/cli/kasapi-cli_mail_forwards_get.md index 1980c31..1edb87a 100644 --- a/docs/cli/kasapi-cli_mail_forwards_get.md +++ b/docs/cli/kasapi-cli_mail_forwards_get.md @@ -19,7 +19,7 @@ kasapi-cli mail forwards get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_forwards_list.md b/docs/cli/kasapi-cli_mail_forwards_list.md index 71cb7c8..95682f4 100644 --- a/docs/cli/kasapi-cli_mail_forwards_list.md +++ b/docs/cli/kasapi-cli_mail_forwards_list.md @@ -19,7 +19,7 @@ kasapi-cli mail forwards list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_forwards_update.md b/docs/cli/kasapi-cli_mail_forwards_update.md index 3f3e3fe..2f00f45 100644 --- a/docs/cli/kasapi-cli_mail_forwards_update.md +++ b/docs/cli/kasapi-cli_mail_forwards_update.md @@ -20,7 +20,7 @@ kasapi-cli mail forwards update
--target [--target ...] [ --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_lists.md b/docs/cli/kasapi-cli_mail_lists.md index 5cebdb3..8a6ed75 100644 --- a/docs/cli/kasapi-cli_mail_lists.md +++ b/docs/cli/kasapi-cli_mail_lists.md @@ -2,6 +2,10 @@ Inspect and manage mailing lists (get/add/update/delete_mailinglist) +``` +kasapi-cli mail lists [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect and manage mailing lists (get/add/update/delete_mailinglist) --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_lists_add.md b/docs/cli/kasapi-cli_mail_lists_add.md index 10eb7df..c8375d5 100644 --- a/docs/cli/kasapi-cli_mail_lists_add.md +++ b/docs/cli/kasapi-cli_mail_lists_add.md @@ -21,7 +21,7 @@ kasapi-cli mail lists add --domain --password [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_lists_delete.md b/docs/cli/kasapi-cli_mail_lists_delete.md index 21881bf..8b199ee 100644 --- a/docs/cli/kasapi-cli_mail_lists_delete.md +++ b/docs/cli/kasapi-cli_mail_lists_delete.md @@ -19,7 +19,7 @@ kasapi-cli mail lists delete [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_lists_get.md b/docs/cli/kasapi-cli_mail_lists_get.md index 52667a1..fcd4ad4 100644 --- a/docs/cli/kasapi-cli_mail_lists_get.md +++ b/docs/cli/kasapi-cli_mail_lists_get.md @@ -19,7 +19,7 @@ kasapi-cli mail lists get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_lists_list.md b/docs/cli/kasapi-cli_mail_lists_list.md index 2296b21..a16a04b 100644 --- a/docs/cli/kasapi-cli_mail_lists_list.md +++ b/docs/cli/kasapi-cli_mail_lists_list.md @@ -19,7 +19,7 @@ kasapi-cli mail lists list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_mail_lists_update.md b/docs/cli/kasapi-cli_mail_lists_update.md index ed779e3..2e8a8ee 100644 --- a/docs/cli/kasapi-cli_mail_lists_update.md +++ b/docs/cli/kasapi-cli_mail_lists_update.md @@ -23,7 +23,7 @@ kasapi-cli mail lists update [--subscriber ...] [--restrict-post --comment --path

[flags] ### Options ``` - --comment string user comment / label (required for add) + --comment string user comment / label (required) -h, --help help for add - --password string Samba password (required for add; new password for update) - --path string share path the user is granted (samba_path; required for add) + --password string Samba password (required) + --path string share path the user is granted (samba_path; required) ``` ### Options inherited from parent commands @@ -22,7 +22,7 @@ kasapi-cli sambausers add --password --comment --path

[flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_sambausers_delete.md b/docs/cli/kasapi-cli_sambausers_delete.md index db746cb..02f6265 100644 --- a/docs/cli/kasapi-cli_sambausers_delete.md +++ b/docs/cli/kasapi-cli_sambausers_delete.md @@ -19,7 +19,7 @@ kasapi-cli sambausers delete [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_sambausers_get.md b/docs/cli/kasapi-cli_sambausers_get.md index 5e31cd4..582e782 100644 --- a/docs/cli/kasapi-cli_sambausers_get.md +++ b/docs/cli/kasapi-cli_sambausers_get.md @@ -19,7 +19,7 @@ kasapi-cli sambausers get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_sambausers_list.md b/docs/cli/kasapi-cli_sambausers_list.md index 6f67fd8..25640d1 100644 --- a/docs/cli/kasapi-cli_sambausers_list.md +++ b/docs/cli/kasapi-cli_sambausers_list.md @@ -19,7 +19,7 @@ kasapi-cli sambausers list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_sambausers_update.md b/docs/cli/kasapi-cli_sambausers_update.md index 8da8cb1..0fef04a 100644 --- a/docs/cli/kasapi-cli_sambausers_update.md +++ b/docs/cli/kasapi-cli_sambausers_update.md @@ -9,10 +9,10 @@ kasapi-cli sambausers update [password/path flags] [flags] ### Options ``` - --comment string user comment / label (required for add) + --comment string replacement user comment / label -h, --help help for update - --password string Samba password (required for add; new password for update) - --path string share path the user is granted (samba_path; required for add) + --password string replacement Samba password (sent as samba_new_password) + --path string replacement share path the user is granted (samba_path) ``` ### Options inherited from parent commands @@ -22,7 +22,7 @@ kasapi-cli sambausers update [password/path flags] [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_server.md b/docs/cli/kasapi-cli_server.md index 8e23f40..776ce53 100644 --- a/docs/cli/kasapi-cli_server.md +++ b/docs/cli/kasapi-cli_server.md @@ -2,6 +2,10 @@ Inspect the host server kasapi-cli is talking to +``` +kasapi-cli server [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect the host server kasapi-cli is talking to --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_server_info.md b/docs/cli/kasapi-cli_server_info.md index 7366a0c..b1f4610 100644 --- a/docs/cli/kasapi-cli_server_info.md +++ b/docs/cli/kasapi-cli_server_info.md @@ -19,7 +19,7 @@ kasapi-cli server info [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_sessions.md b/docs/cli/kasapi-cli_sessions.md index 0b05896..0909328 100644 --- a/docs/cli/kasapi-cli_sessions.md +++ b/docs/cli/kasapi-cli_sessions.md @@ -8,6 +8,10 @@ Manage KAS session tokens. add_session is not a separate endpoint — it is the KasAuth credential-token flow driven transparently by auth_type=session (see `config init`). Only delete_session is exposed here, as the explicit counterpart to the implicit logout in `config use-profile`. +``` +kasapi-cli sessions [flags] +``` + ### Options ``` @@ -21,7 +25,7 @@ add_session is not a separate endpoint — it is the KasAuth credential-token fl --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_sessions_delete.md b/docs/cli/kasapi-cli_sessions_delete.md index 22919e0..ec35d82 100644 --- a/docs/cli/kasapi-cli_sessions_delete.md +++ b/docs/cli/kasapi-cli_sessions_delete.md @@ -25,7 +25,7 @@ kasapi-cli sessions delete [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_softwareinstalls.md b/docs/cli/kasapi-cli_softwareinstalls.md index b3cf00c..b59fe7b 100644 --- a/docs/cli/kasapi-cli_softwareinstalls.md +++ b/docs/cli/kasapi-cli_softwareinstalls.md @@ -2,6 +2,10 @@ Inspect installable software templates (get_softwareinstall) +``` +kasapi-cli softwareinstalls [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect installable software templates (get_softwareinstall) --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_softwareinstalls_get.md b/docs/cli/kasapi-cli_softwareinstalls_get.md index 3928a6d..fcf6ec9 100644 --- a/docs/cli/kasapi-cli_softwareinstalls_get.md +++ b/docs/cli/kasapi-cli_softwareinstalls_get.md @@ -19,7 +19,7 @@ kasapi-cli softwareinstalls get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_softwareinstalls_list.md b/docs/cli/kasapi-cli_softwareinstalls_list.md index 0f13948..99d77b7 100644 --- a/docs/cli/kasapi-cli_softwareinstalls_list.md +++ b/docs/cli/kasapi-cli_softwareinstalls_list.md @@ -19,7 +19,7 @@ kasapi-cli softwareinstalls list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_subdomains.md b/docs/cli/kasapi-cli_subdomains.md index fedc7bf..7de14b7 100644 --- a/docs/cli/kasapi-cli_subdomains.md +++ b/docs/cli/kasapi-cli_subdomains.md @@ -2,6 +2,10 @@ Inspect subdomains owned by the authenticated account +``` +kasapi-cli subdomains [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect subdomains owned by the authenticated account --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_subdomains_get.md b/docs/cli/kasapi-cli_subdomains_get.md index 7559fed..164dedc 100644 --- a/docs/cli/kasapi-cli_subdomains_get.md +++ b/docs/cli/kasapi-cli_subdomains_get.md @@ -19,7 +19,7 @@ kasapi-cli subdomains get [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_subdomains_list.md b/docs/cli/kasapi-cli_subdomains_list.md index 15794c3..adbdd72 100644 --- a/docs/cli/kasapi-cli_subdomains_list.md +++ b/docs/cli/kasapi-cli_subdomains_list.md @@ -19,7 +19,7 @@ kasapi-cli subdomains list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_tlds.md b/docs/cli/kasapi-cli_tlds.md index 457bc54..6efc4d2 100644 --- a/docs/cli/kasapi-cli_tlds.md +++ b/docs/cli/kasapi-cli_tlds.md @@ -2,6 +2,10 @@ Inspect the catalog of registrable top-level domains +``` +kasapi-cli tlds [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect the catalog of registrable top-level domains --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_tlds_list.md b/docs/cli/kasapi-cli_tlds_list.md index fc3b350..61cf92d 100644 --- a/docs/cli/kasapi-cli_tlds_list.md +++ b/docs/cli/kasapi-cli_tlds_list.md @@ -19,7 +19,7 @@ kasapi-cli tlds list [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_usage.md b/docs/cli/kasapi-cli_usage.md index 8b202d6..292bc19 100644 --- a/docs/cli/kasapi-cli_usage.md +++ b/docs/cli/kasapi-cli_usage.md @@ -2,6 +2,10 @@ Inspect webspace and traffic counters +``` +kasapi-cli usage [flags] +``` + ### Options ``` @@ -15,7 +19,7 @@ Inspect webspace and traffic counters --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_usage_space-detail.md b/docs/cli/kasapi-cli_usage_space-detail.md index 749ae8b..23e170e 100644 --- a/docs/cli/kasapi-cli_usage_space-detail.md +++ b/docs/cli/kasapi-cli_usage_space-detail.md @@ -20,7 +20,7 @@ kasapi-cli usage space-detail [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_usage_space.md b/docs/cli/kasapi-cli_usage_space.md index 8608cb2..9eb75b0 100644 --- a/docs/cli/kasapi-cli_usage_space.md +++ b/docs/cli/kasapi-cli_usage_space.md @@ -19,7 +19,7 @@ kasapi-cli usage space [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/cli/kasapi-cli_usage_traffic.md b/docs/cli/kasapi-cli_usage_traffic.md index e1e757b..5465d1e 100644 --- a/docs/cli/kasapi-cli_usage_traffic.md +++ b/docs/cli/kasapi-cli_usage_traffic.md @@ -21,7 +21,7 @@ kasapi-cli usage traffic [flags] --auth-data string KAS auth data (overrides config and KAS_AUTHDATA) --auth-type string KAS auth strategy: 'plain' = send password on each KasApi call (no KasAuth, no 2FA support); 'session' = bootstrap via KasAuth and reuse the credential token. Overrides config and KAS_AUTHTYPE. --config string path to the kasapi-cli config file (overrides the default location) - --dry-run preview a destructive command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output + --dry-run preview a write command's KAS request (action + redacted parameters) and exit 0 without dispatching or prompting; honours --output --login string KAS login (overrides config and KAS_LOGIN) --otp string 2FA one-time PIN — sent to KasAuth as session_2fa during the credential-token bootstrap. Requires auth_type=session; the KAS API does not document 2FA on direct kas_auth_type=plain calls. -o, --output string output format: json|yaml|table (default table) diff --git a/docs/usage/destructive-writes.md b/docs/usage/destructive-writes.md index cfd89f2..57537fd 100644 --- a/docs/usage/destructive-writes.md +++ b/docs/usage/destructive-writes.md @@ -54,10 +54,12 @@ Before a destructive call leaves the machine, the command prints a one-line summary of the pending change and asks for confirmation: ``` -About to delete mail account "m0000001". This cannot be undone. -Proceed? [y/N]: +About to permanently delete mail account "m0000001". This cannot be undone. [y/N]: ``` +The prompt is written to **stderr**, so redirecting stdout (`cmd > +file`) cannot swallow the question the command is waiting on. + - Only an explicit `y` / `yes` (case-insensitive) proceeds. Empty input or anything else aborts. - Declining exits with code **1** (user error); nothing is sent to KAS. @@ -88,11 +90,17 @@ non-interactively. ## Audit log Independently of the confirmation prompt and of `--verbose`, every -dispatched write action leaves a structured trace +write action dispatched through a module write subcommand (the +`add`/`update`/`delete` slices listed above) leaves a structured trace ([#131](https://github.com/chmmou/kasapi-cli/issues/131)). The record is emitted **after** the SOAP call returns, regardless of success or failure. +The session logout dispatched by `sessions delete` and `config +use-profile` (`delete_session`) is not part of this pipeline: it only +invalidates the caller's own session token, is not gated, and leaves +no audit record. + A `logfmt`-style line always goes to **stderr**: ``` @@ -100,7 +108,17 @@ ts=2026-05-16T12:00:00Z login=w0000001 action=delete_dns_settings target="record ``` `outcome` is `success`, `failure:` for a typed KAS fault, or a -bare `failure` for a transport/decode error. +bare `failure` for a transport/decode error. A destructive attempt that +never dispatched is also recorded: `declined` when the `[y/N]` prompt +was answered with no, `refused` when stdin was not a TTY and `--yes` +was not given, `aborted` when the interactive prompt failed with an +I/O error before an answer was read. + +For the `add` actions whose identifier KAS generates server-side (the +slices listed in the baseline above), the success record additionally +carries the assigned identifier as `created_id=`, so the create +can be correlated with the identifier later `update`/`delete` records +carry as their `target`. Passing `--audit-log ` (or setting `KAS_AUDIT_LOG`; the flag wins) additionally appends the same record as one JSON object per line @@ -108,7 +126,10 @@ additionally appends the same record as one JSON object per line Secret request parameters (`auth_data`, `*password`, `*token`, `*secret`, …) are replaced with `` in **both** sinks and never -written. Read commands produce no audit record. +written. Multi-line or oversized values (e.g. the `mail lists update` +`--config-file` / `--subscriber` blobs, which can contain the list +password) are elided to ``. Read commands produce no +audit record. ## `--dry-run`: preview without dispatching diff --git a/internal/api/client.go b/internal/api/client.go index ce97e48..bb6896b 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -25,10 +25,14 @@ const floodFallback = 2 * time.Second // Plain auth returns the password as AuthData; session auth returns a // short-lived 40-char token. After an authentication failure, Client // calls Invalidate so the next Credentials call may obtain a fresh -// token (e.g. by re-running the KasAuth flow). +// token (e.g. by re-running the KasAuth flow). Invalidate reports +// whether that next call can actually produce fresh credentials: +// a session source discards its cached token and returns true, while a +// static credential cannot refresh and returns false — Client then +// skips the pointless retry with identical credentials. type TokenSource interface { Credentials(ctx context.Context) (login, authData string, authType soap.AuthType, err error) - Invalidate() + Invalidate() bool } // Heartbeater is an optional TokenSource extension. After every @@ -60,8 +64,9 @@ func (s *StaticTokenSource) Credentials(_ context.Context) (string, string, soap } // Invalidate is a no-op for the static source. A static credential -// cannot refresh itself; an auth failure is therefore terminal. -func (s *StaticTokenSource) Invalidate() {} +// cannot refresh itself; an auth failure is therefore terminal and +// the returned false suppresses the Client's auth-failure retry. +func (s *StaticTokenSource) Invalidate() bool { return false } // Client posts KasApi calls through the transport, refreshes session // tokens on auth failures, and feeds the server-reported KasFloodDelay @@ -118,9 +123,12 @@ func (c *Client) Call(ctx context.Context, action string, params map[string]any) c.logger().Info("api: call", "action", action) resp, err := c.callOnce(ctx, action, params) if err != nil && IsAuthFailure(err) { - c.logger().Info("api: auth failure, refreshing token and retrying", "action", action) - c.Tokens.Invalidate() - resp, err = c.callOnce(ctx, action, params) + if c.Tokens.Invalidate() { + c.logger().Info("api: auth failure, refreshing token and retrying", "action", action) + resp, err = c.callOnce(ctx, action, params) + } else { + c.logger().Info("api: auth failure with non-refreshable credentials, not retrying", "action", action) + } } if err == nil { if hb, ok := c.Tokens.(Heartbeater); ok { diff --git a/internal/api/client_test.go b/internal/api/client_test.go index 0356ade..972fa3a 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -142,6 +142,42 @@ func TestCallReturnsTypedFault(t *testing.T) { } } +// TestCallReturnsTypedFaultOnHTTP500 pins the end-to-end contract +// behind the transport 5xx fault pass-through: a KAS fault delivered +// with HTTP 500 (PHP SOAP servers do this) must reach the decoder and +// surface as the same typed *api.Error a 200-wrapped fault produces, +// instead of being burned in blind transport retries. +func TestCallReturnsTypedFaultOnHTTP500(t *testing.T) { + body := loadFixture(t, "account/add_account_response_failed_max_account_reached.xml") + var calls atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write(body) + })) + defer srv.Close() + + tr := transport.New() + tr.HTTPClient = srv.Client() + tr.MaxRetries = 2 + tr.Now = time.Now + tr.Sleep = func(_ context.Context, _ time.Duration) error { return nil } + c := api.New(tr, staticTokens()) + c.Endpoint = srv.URL + + _, err := c.Call(context.Background(), "add_account", nil) + apiErr := api.AsError(err) + if apiErr == nil { + t.Fatalf("expected *api.Error, got %T: %v", err, err) + } + if apiErr.Code != "max_account_reached" { + t.Errorf("Code = %q, want max_account_reached", apiErr.Code) + } + if calls.Load() != 1 { + t.Errorf("calls = %d, want 1 (500-wrapped fault must not be retried)", calls.Load()) + } +} + func TestCallRetriesOnAuthFailure(t *testing.T) { authBody := loadFixture(t, "response_failed_no_auth.xml") okBody := loadFixture(t, "account/get_accounts_response_success.xml") @@ -208,6 +244,27 @@ func TestCallRetriesOnSessionInvalid(t *testing.T) { } } +// A static credential cannot refresh itself, so an auth failure with a +// StaticTokenSource is terminal: retrying with identical credentials +// would only double the failing request against the flood gate. +func TestCallNoRetryOnAuthFailureWithStaticTokens(t *testing.T) { + body := loadFixture(t, "response_failed_no_auth.xml") + var calls atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + _, _ = w.Write(body) + })) + defer srv.Close() + + c := newAPIClient(srv, staticTokens()) + if _, err := c.Call(context.Background(), "get_accounts", nil); err == nil { + t.Fatal("expected auth-failure error") + } + if calls.Load() != 1 { + t.Errorf("server calls = %d, want 1 (no retry with non-refreshable credentials)", calls.Load()) + } +} + func TestCallNoRetryOnNonAuthFault(t *testing.T) { body := loadFixture(t, "account/add_account_response_failed_max_account_reached.xml") var calls atomic.Int32 @@ -285,9 +342,10 @@ func (c *countingTokens) Credentials(_ context.Context) (string, string, soap.Au return c.login, c.data, c.typ, nil } -func (c *countingTokens) Invalidate() { +func (c *countingTokens) Invalidate() bool { c.invalidations++ c.data = c.refresh + return true } // beatingTokens implements TokenSource + Heartbeater to verify the @@ -302,5 +360,5 @@ type beatingTokens struct { func (b *beatingTokens) Credentials(_ context.Context) (string, string, soap.AuthType, error) { return b.login, b.data, b.typ, nil } -func (b *beatingTokens) Invalidate() {} +func (b *beatingTokens) Invalidate() bool { return true } func (b *beatingTokens) Heartbeat(_ context.Context) { b.heartbeats++ } diff --git a/internal/api/fault_fixtures_test.go b/internal/api/fault_fixtures_test.go new file mode 100644 index 0000000..fd48ce6 --- /dev/null +++ b/internal/api/fault_fixtures_test.go @@ -0,0 +1,25 @@ +package api_test + +import ( + "testing" + + "github.com/chmmou/kasapi-cli/internal/api" + "github.com/chmmou/kasapi-cli/internal/testutil" +) + +// TestSharedFaultFixtures anchors the shared top-level testdata/ +// response_failed_*.xml set (the cross-module session/auth/action +// faults) to the KAS contract, pinning each fixture to the api.Code* +// constant the error helpers classify it by. Module-specific fault +// fixtures are anchored by the per-module fault tests instead. +func TestSharedFaultFixtures(t *testing.T) { + t.Parallel() + testutil.AssertFaultFixtures(t, "", map[string]string{ + "response_failed_no_auth.xml": api.CodeNoAuth, + "response_failed_kas_session_invalid.xml": api.CodeSessionInvalid, + "response_failed_got_no_login_data.xml": api.CodeGotNoLoginData, + "response_failed_kas_access_forbidden.xml": api.CodeAccessForbidden, + "response_failed_no_action.xml": api.CodeNoAction, + "response_failed_unkown_action.xml": api.CodeUnknownAction, + }) +} diff --git a/internal/auth/codec.go b/internal/auth/codec.go index 489fbbc..b7a5c04 100644 --- a/internal/auth/codec.go +++ b/internal/auth/codec.go @@ -23,6 +23,16 @@ type Request struct { OTP string } +// maxSessionLifetime is the documented upper bound of the KasAuth +// session_lifetime parameter (seconds); the documented range is +// 1..30000, with an unset value (0 here) leaving the server default. +const maxSessionLifetime = 30000 + +// credentialTokenLength is the length of the credential token a +// successful KasAuth call returns (see doc.go): 40 alphanumeric +// characters. +const credentialTokenLength = 40 + const requestTemplate = ` @@ -43,6 +53,9 @@ func EncodeRequest(w io.Writer, r Request) error { if r.AuthData == "" { return errors.New("auth: Request.AuthData is required") } + if r.Lifetime < 0 || r.Lifetime > maxSessionLifetime { + return fmt.Errorf("auth: Request.Lifetime %d out of range 1..%d (0 = server default)", r.Lifetime, maxSessionLifetime) + } payload := map[string]any{ "kas_login": r.Login, "kas_auth_type": string(r.AuthType), @@ -126,6 +139,22 @@ func decodeBody(d *xml.Decoder, parent xml.StartElement) (string, error) { } } +// validToken reports whether s has the shape of a KasAuth credential +// token: exactly credentialTokenLength alphanumeric characters. +func validToken(s string) bool { + if len(s) != credentialTokenLength { + return false + } + for _, r := range s { + switch { + case r >= '0' && r <= '9', r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z': + default: + return false + } + } + return true +} + func decodeKasAuthResponse(d *xml.Decoder, parent xml.StartElement) (string, error) { for { tok, err := d.Token() @@ -142,6 +171,12 @@ func decodeKasAuthResponse(d *xml.Decoder, parent xml.StartElement) (string, err if s == "" { return "", errors.New("auth: empty element") } + // Guard the token shape before it gets cached and + // persisted; only the length is reported so a partial + // secret can never leak into an error message. + if !validToken(s) { + return "", fmt.Errorf("auth: malformed credential token: got %d bytes, want %d alphanumeric characters", len(s), credentialTokenLength) + } return s, nil } if err := d.Skip(); err != nil { diff --git a/internal/auth/codec_test.go b/internal/auth/codec_test.go index 57cc0da..ee2c1f0 100644 --- a/internal/auth/codec_test.go +++ b/internal/auth/codec_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "os" "path/filepath" "strings" @@ -120,3 +121,49 @@ func TestDecodeResponseEmptyDocument(t *testing.T) { t.Fatal("expected error") } } + +// The credential token contract is 40 alphanumeric characters; anything +// else must be rejected before it gets cached and persisted. The error +// must not echo the token content — only its length. +func TestDecodeResponseRejectsMalformedToken(t *testing.T) { + const envelope = ` + + + + %s + + +` + for _, tok := range []string{ + "short", + strings.Repeat("a", 41), + strings.Repeat("a", 39) + "!", + } { + _, err := auth.DecodeResponse(strings.NewReader(fmt.Sprintf(envelope, tok))) + if err == nil { + t.Errorf("token %q: expected malformed-token error, got nil", tok) + continue + } + if strings.Contains(err.Error(), tok) { + t.Errorf("token content leaked into error: %v", err) + } + } +} + +// session_lifetime is documented as 1..30000 seconds; 0 means "leave +// the server default". Out-of-range values must fail at encode time so +// the local expiry mirror cannot silently diverge from the server. +func TestEncodeRequestRejectsOutOfRangeLifetime(t *testing.T) { + for _, lifetime := range []int{-1, 30001} { + var buf bytes.Buffer + err := auth.EncodeRequest(&buf, auth.Request{ + Login: "w0", + AuthType: soap.AuthPlain, + AuthData: "pw", + Lifetime: lifetime, + }) + if err == nil { + t.Errorf("Lifetime %d: expected range error, got nil", lifetime) + } + } +} diff --git a/internal/auth/source.go b/internal/auth/source.go index 0baf9ca..f7f09ed 100644 --- a/internal/auth/source.go +++ b/internal/auth/source.go @@ -152,7 +152,11 @@ func (s *SessionTokenSource) Credentials(ctx context.Context) (string, string, s // context.Background so a successful invalidation cannot be lost just // because the user pressed Ctrl-C between the API failure and the // cleanup. The in-memory clear happens unconditionally either way. -func (s *SessionTokenSource) Invalidate() { +// +// Invalidate always returns true: a session source can re-run the +// KasAuth flow on the next Credentials call, so an auth-failure retry +// with fresh credentials is worthwhile. +func (s *SessionTokenSource) Invalidate() bool { s.mu.Lock() defer s.mu.Unlock() s.token = "" @@ -167,6 +171,7 @@ func (s *SessionTokenSource) Invalidate() { s.logger().Warn("auth: session store delete failed; in-memory cache cleared", "err", err) } } + return true } // Heartbeat extends the cached expiry by Lifetime when UpdateLifetime diff --git a/internal/chown/fault_test.go b/internal/chown/fault_test.go new file mode 100644 index 0000000..c5e952d --- /dev/null +++ b/internal/chown/fault_test.go @@ -0,0 +1,18 @@ +package chown_test + +import ( + "testing" + + "github.com/chmmou/kasapi-cli/internal/testutil" +) + +// The chown write slice is not implemented yet (#125), but its captured +// fault fixtures already live in testdata/chown/. Anchoring them via the +// shared testutil.AssertFaultFixtures keeps them bound to the KAS +// contract instead of rotting unreferenced until the slice lands. +func TestFaultFixturesDecodeToDocumentedCodes(t *testing.T) { + t.Parallel() + testutil.AssertFaultFixtures(t, "chown", map[string]string{ + "update_chown_response_failed_missing_parameter.xml": "missing_parameter", + }) +} diff --git a/internal/cli/audit.go b/internal/cli/audit.go index 8460574..d201f0f 100644 --- a/internal/cli/audit.go +++ b/internal/cli/audit.go @@ -33,6 +33,19 @@ type AuditRecord struct { // from a real success or failure. const AuditOutcomeDryRun = "dry-run" +// AuditOutcomeDeclined, AuditOutcomeRefused and AuditOutcomeAborted are +// the Outcome values for destructive attempts that never dispatched: +// "declined" when the user answered the [y/N] prompt with no, "refused" +// when stdin was not a TTY and --yes was not given, "aborted" when the +// interactive prompt failed with an I/O error before an answer was +// read. Auditing the attempt keeps the trace complete — a blocked +// destructive action is still an action someone tried to run. +const ( + AuditOutcomeDeclined = "declined" + AuditOutcomeRefused = "refused" + AuditOutcomeAborted = "aborted" +) + // OutcomeFor maps a write call's error to the audit outcome string: // "success" on nil, "failure:" for a typed KAS fault, and a // bare "failure" for any other (transport/decode) error. diff --git a/internal/cli/confirm.go b/internal/cli/confirm.go index ab15e5f..5c08daa 100644 --- a/internal/cli/confirm.go +++ b/internal/cli/confirm.go @@ -56,6 +56,13 @@ var ErrConfirmationDeclined = errors.New("cli: destructive operation cancelled b // interactively or pass --yes to proceed non-interactively. var ErrConfirmationRequired = errors.New("cli: refusing destructive operation: stdin is not a TTY and --yes was not given") +// ErrConfirmationAborted is returned by GateDestructive when the +// interactive [y/N] prompt failed with an I/O error before an answer +// was read. Like a decline it means the destructive attempt never +// dispatched, so the write runner still records it in the audit trace +// (outcome "aborted"). +var ErrConfirmationAborted = errors.New("cli: destructive operation aborted: confirmation prompt failed") + // ConfirmAction is the one-line description of a pending destructive // change, shown to the user before the [y/N] prompt: "About to // \"\". This cannot be undone." @@ -93,7 +100,7 @@ func GateDestructive(in io.Reader, out io.Writer, isTTY, yes bool, a ConfirmActi } ok, err := Confirm(in, out, a.Summary()) if err != nil { - return UserError(err, "confirm") + return UserError(fmt.Errorf("%w: %w", ErrConfirmationAborted, err), "") } if !ok { return UserError(ErrConfirmationDeclined, "") diff --git a/internal/cli/confirm_test.go b/internal/cli/confirm_test.go index 915b02e..db42564 100644 --- a/internal/cli/confirm_test.go +++ b/internal/cli/confirm_test.go @@ -5,6 +5,7 @@ import ( "errors" "strings" "testing" + "testing/iotest" "github.com/chmmou/kasapi-cli/internal/cli" ) @@ -84,3 +85,19 @@ func TestGateDestructive(t *testing.T) { }) } } + +// A prompt I/O failure (neither a yes nor a no was read) is an aborted +// attempt: ErrConfirmationAborted, exit 1, so the write runner can +// audit it as outcome=aborted. +func TestGateDestructiveAbortsOnPromptIOError(t *testing.T) { + t.Parallel() + var out bytes.Buffer + action := cli.ConfirmAction{Verb: "delete", Resource: "mail account", ID: "m0000001"} + err := cli.GateDestructive(iotest.ErrReader(errors.New("tty gone")), &out, true, false, action) + if !errors.Is(err, cli.ErrConfirmationAborted) { + t.Errorf("err = %v, want wrapped ErrConfirmationAborted", err) + } + if got := cli.CodeFor(err); got != cli.ExitUserError { + t.Errorf("CodeFor = %d, want ExitUserError (%d)", got, cli.ExitUserError) + } +} diff --git a/internal/cli/cronjobs.go b/internal/cli/cronjobs.go index fb6c3ac..026e7d4 100644 --- a/internal/cli/cronjobs.go +++ b/internal/cli/cronjobs.go @@ -52,10 +52,9 @@ func newCronjobsGetCmd(opts *RootOptions) *cobra.Command { } } -// cronjobWriteFlags binds the shared add_cronjob / update_cronjob -// request fields to a command. The same flag set serves both: add -// reads every value (defaults included), update sends only the flags -// the user explicitly changed (see cronjobChangedFields). +// cronjobWriteFlags binds the add_cronjob request fields to a command. +// The update subcommand binds its own cronjobUpdateFlags so its --help +// does not advertise add defaults or "required for add" texts. type cronjobWriteFlags struct { protocol string url string @@ -76,10 +75,10 @@ type cronjobWriteFlags struct { func (f *cronjobWriteFlags) bind(cmd *cobra.Command) { fl := cmd.Flags() fl.StringVar(&f.protocol, "protocol", "https", "request protocol (http|https)") - fl.StringVar(&f.url, "url", "", "URL to call (http_url; required for add)") - fl.StringVar(&f.comment, "comment", "", "cronjob comment / label (required for add)") - fl.StringVar(&f.minute, "minute", "", "schedule minute field (required for add)") - fl.StringVar(&f.hour, "hour", "", "schedule hour field (required for add)") + fl.StringVar(&f.url, "url", "", "URL to call (http_url; required)") + fl.StringVar(&f.comment, "comment", "", "cronjob comment / label (required)") + fl.StringVar(&f.minute, "minute", "", "schedule minute field (required)") + fl.StringVar(&f.hour, "hour", "", "schedule hour field (required)") fl.StringVar(&f.dayOfMonth, "day-of-month", "*", "schedule day-of-month field") fl.StringVar(&f.month, "month", "*", "schedule month field") fl.StringVar(&f.dayOfWeek, "day-of-week", "*", "schedule day-of-week field (0-7, Sun=0|7)") @@ -134,16 +133,19 @@ func newCronjobsAddCmd(opts *RootOptions) *cobra.Command { return writeSpec{}, fmt.Errorf("--minute and --hour are required") } s := f.spec() + var createdID string return writeSpec{ action: "add_cronjob", destructive: false, confirm: ConfirmAction{Verb: "create", Resource: "cronjob", ID: f.comment}, params: cronjob.AddParams(s), + createdID: &createdID, dispatch: func(c *api.Client, ctx context.Context) (string, error) { id, derr := cronjob.NewClient(c).Add(ctx, s) if derr != nil { return "", derr } + createdID = id return "created cronjob " + id, nil }, }, nil @@ -153,13 +155,53 @@ func newCronjobsAddCmd(opts *RootOptions) *cobra.Command { return cmd } +// cronjobUpdateFlags binds the update_cronjob mutable surface. +// Disjoint from cronjobWriteFlags so update's --help describes the +// flags as replacements without the add defaults ("https", "*", +// "default") that update never sends unless explicitly set — the same +// split the database/mailaccount/ddnsuser/mailinglist slices use. +type cronjobUpdateFlags struct { + protocol string + url string + comment string + minute string + hour string + dayOfMonth string + month string + dayOfWeek string + httpUser string + httpPassword string + mailAddress string + mailCondition string + mailSubject string + active bool +} + +func (f *cronjobUpdateFlags) bind(cmd *cobra.Command) { + fl := cmd.Flags() + fl.StringVar(&f.protocol, "protocol", "", "replacement request protocol (http|https)") + fl.StringVar(&f.url, "url", "", "replacement URL to call (http_url)") + fl.StringVar(&f.comment, "comment", "", "replacement cronjob comment / label") + fl.StringVar(&f.minute, "minute", "", "replacement schedule minute field") + fl.StringVar(&f.hour, "hour", "", "replacement schedule hour field") + fl.StringVar(&f.dayOfMonth, "day-of-month", "", "replacement schedule day-of-month field") + fl.StringVar(&f.month, "month", "", "replacement schedule month field") + fl.StringVar(&f.dayOfWeek, "day-of-week", "", "replacement schedule day-of-week field (0-7, Sun=0|7)") + fl.StringVar(&f.httpUser, "http-user", "", "replacement HTTP basic-auth user for the call") + fl.StringVar(&f.httpPassword, "http-password", "", "replacement HTTP basic-auth password for the call") + fl.StringVar(&f.mailAddress, "mail-address", "", "replacement notification mail address (mail_adress)") + fl.StringVar(&f.mailCondition, "mail-condition", "", "replacement notification-mail condition") + fl.StringVar(&f.mailSubject, "mail-subject", "", "replacement notification mail subject (default|comment)") + fl.BoolVar(&f.active, "active", true, "replacement active state; pass --active=false to disable") +} + // cronjobChangedFields collects only the write flags the user // explicitly set into the update_cronjob field map (keyed on the // cronjob.Field* constants). Each field is a wholesale replacement and // an empty value is a meaningful set, so presence is keyed on cobra // Changed, not on the value being non-empty — the same pattern the // mailing-list update uses. -func cronjobChangedFields(cmd *cobra.Command, f *cronjobWriteFlags) map[string]string { +func cronjobChangedFields(cmd *cobra.Command, f *cronjobUpdateFlags) map[string]string { fields := map[string]string{} // flag name -> {KAS request key, current flag value}. for flag, kv := range map[string][2]string{ @@ -188,7 +230,7 @@ func cronjobChangedFields(cmd *cobra.Command, f *cronjobWriteFlags) map[string]s } func newCronjobsUpdateCmd(opts *RootOptions) *cobra.Command { - f := &cronjobWriteFlags{} + f := &cronjobUpdateFlags{} cmd := &cobra.Command{ Use: "update [schedule/mail flags]", Short: "Replace mutable fields of a cronjob (update_cronjob)", diff --git a/internal/cli/databases.go b/internal/cli/databases.go index 6f613b1..424edb0 100644 --- a/internal/cli/databases.go +++ b/internal/cli/databases.go @@ -108,16 +108,19 @@ access.`, return writeSpec{}, fmt.Errorf("--comment is required") } s := f.spec() + var createdID string return writeSpec{ action: "add_database", destructive: false, confirm: ConfirmAction{Verb: "create", Resource: "database", ID: f.comment}, params: database.AddParams(s), + createdID: &createdID, dispatch: func(c *api.Client, ctx context.Context) (string, error) { login, derr := database.NewClient(c).Add(ctx, s) if derr != nil { return "", derr } + createdID = login return "created database " + login, nil }, }, nil diff --git a/internal/cli/ddnsusers.go b/internal/cli/ddnsusers.go index 5e73c65..3adcc69 100644 --- a/internal/cli/ddnsusers.go +++ b/internal/cli/ddnsusers.go @@ -130,16 +130,19 @@ flag) so the follow-up update can set both ipv4 and ipv6 targets — see return writeSpec{}, fmt.Errorf("--comment is required") } s := f.spec() + var createdID string return writeSpec{ action: "add_ddnsuser", destructive: false, confirm: ConfirmAction{Verb: "create", Resource: "ddns user", ID: f.comment}, params: ddns.AddParams(s), + createdID: &createdID, dispatch: func(c *api.Client, ctx context.Context) (string, error) { login, derr := ddns.NewClient(c).Add(ctx, s) if derr != nil { return "", derr } + createdID = login return "created ddns user " + login, nil }, }, nil diff --git a/internal/cli/directoryprotection_test.go b/internal/cli/directoryprotection_test.go index 01cb563..de594e4 100644 --- a/internal/cli/directoryprotection_test.go +++ b/internal/cli/directoryprotection_test.go @@ -166,6 +166,10 @@ func TestDirectoryProtectionDestructiveRefuseNonTTY(t *testing.T) { if !errors.Is(err, cli.ErrConfirmationRequired) { t.Errorf("err = %v, want ErrConfirmationRequired", err) } + // The blocked attempt still leaves an audit trace. + if !strings.Contains(buf.String(), "outcome=refused") { + t.Errorf("audit line with outcome=refused missing; output: %s", buf.String()) + } }) } } diff --git a/internal/cli/dryrun.go b/internal/cli/dryrun.go index 3fc94d4..26f6554 100644 --- a/internal/cli/dryrun.go +++ b/internal/cli/dryrun.go @@ -124,7 +124,10 @@ func ResolveDestructive( if handled, herr := previewAndAudit(opts, out, stderr, auditFile, login, kasAction, confirm, params); handled { return false, herr } - if gerr := GateDestructive(in, out, isTTY, opts != nil && opts.Yes, confirm); gerr != nil { + // The prompt goes to stderr, not out: stdout carries the command's + // machine-readable result, and a redirected `cmd > file` must not + // swallow the [y/N] question the user is expected to answer. + if gerr := GateDestructive(in, stderr, isTTY, opts != nil && opts.Yes, confirm); gerr != nil { return false, gerr } return true, nil diff --git a/internal/cli/dryrun_test.go b/internal/cli/dryrun_test.go index ac3371c..07c79dd 100644 --- a/internal/cli/dryrun_test.go +++ b/internal/cli/dryrun_test.go @@ -159,8 +159,11 @@ func TestResolveDestructiveDelegatesToGate(t *testing.T) { if proceed || !errors.Is(err, cli.ErrConfirmationDeclined) { t.Fatalf("proceed=%v err=%v, want false/ErrConfirmationDeclined", proceed, err) } - if !strings.Contains(out.String(), "[y/N]") { - t.Errorf("expected a prompt; out=%q", out.String()) + if !strings.Contains(stderr.String(), "[y/N]") { + t.Errorf("expected the prompt on stderr; stderr=%q", stderr.String()) + } + if strings.Contains(out.String(), "[y/N]") { + t.Errorf("prompt must not pollute stdout; out=%q", out.String()) } }) diff --git a/internal/cli/ftpusers.go b/internal/cli/ftpusers.go index eb34b18..5d010d3 100644 --- a/internal/cli/ftpusers.go +++ b/internal/cli/ftpusers.go @@ -52,13 +52,10 @@ func newFTPUsersGetCmd(opts *RootOptions) *cobra.Command { } } -// ftpuserWriteFlags binds the shared add_ftpuser / update_ftpuser -// request fields to a command. The same flag set serves both: add -// reads every value (defaults included), update sends only the flags -// the user explicitly changed (see ftpuserChangedFields). The password -// flag maps to a different KAS key per action (add_ftpuser: -// ftp_password, update_ftpuser: ftp_new_password) — see spec() and -// ftpuserChangedFields. +// ftpuserWriteFlags binds the add_ftpuser request fields to a command. +// The password flag maps to the add-only ftp_password key — the update +// subcommand binds its own ftpuserUpdateFlags so its --help does not +// advertise add defaults or "required for add" texts. type ftpuserWriteFlags struct { password string comment string @@ -71,8 +68,8 @@ type ftpuserWriteFlags struct { func (f *ftpuserWriteFlags) bind(cmd *cobra.Command) { fl := cmd.Flags() - fl.StringVar(&f.password, "password", "", "FTP password (required for add; new password for update)") - fl.StringVar(&f.comment, "comment", "", "user comment / label (required for add)") + fl.StringVar(&f.password, "password", "", "FTP password (required)") + fl.StringVar(&f.comment, "comment", "", "user comment / label (required)") fl.StringVar(&f.path, "path", "/", "home directory the user is jailed to (ftp_path)") fl.BoolVar(&f.permRead, "permission-read", true, "grant read access; pass --permission-read=false to deny") fl.BoolVar(&f.permWrite, "permission-write", true, "grant write access; pass --permission-write=false to deny") @@ -106,16 +103,19 @@ func newFTPUsersAddCmd(opts *RootOptions) *cobra.Command { return writeSpec{}, fmt.Errorf("--comment is required") } s := f.spec() + var createdID string return writeSpec{ action: "add_ftpuser", destructive: false, confirm: ConfirmAction{Verb: "create", Resource: "ftp user", ID: f.comment}, params: ftpuser.AddParams(s), + createdID: &createdID, dispatch: func(c *api.Client, ctx context.Context) (string, error) { login, derr := ftpuser.NewClient(c).Add(ctx, s) if derr != nil { return "", derr } + createdID = login return "created ftp user " + login, nil }, }, nil @@ -125,6 +125,36 @@ func newFTPUsersAddCmd(opts *RootOptions) *cobra.Command { return cmd } +// ftpuserUpdateFlags binds the update_ftpuser mutable surface. +// Disjoint from ftpuserWriteFlags so update's --help describes the +// flags as replacements (not "required for add") and does not +// advertise add defaults that update never sends — the same split the +// database/mailaccount/ddnsuser/mailinglist slices use. +// +// The password flag maps to ftp_new_password on this subcommand (the +// update_ftpuser key) rather than the add-only ftp_password — see +// ftpuserChangedFields. +type ftpuserUpdateFlags struct { + password string + comment string + path string + permRead bool + permWrite bool + permList bool + virusClam bool +} + +func (f *ftpuserUpdateFlags) bind(cmd *cobra.Command) { + fl := cmd.Flags() + fl.StringVar(&f.password, "password", "", "replacement FTP password (sent as ftp_new_password)") + fl.StringVar(&f.comment, "comment", "", "replacement user comment / label") + fl.StringVar(&f.path, "path", "", "replacement home directory the user is jailed to (ftp_path)") + fl.BoolVar(&f.permRead, "permission-read", true, "replacement read access; pass --permission-read=false to deny") + fl.BoolVar(&f.permWrite, "permission-write", true, "replacement write access; pass --permission-write=false to deny") + fl.BoolVar(&f.permList, "permission-list", true, "replacement directory-list access; pass --permission-list=false to deny") + fl.BoolVar(&f.virusClam, "virus-clamav", true, "replacement ClamAV scanning; pass --virus-clamav=false to disable") +} + // ftpuserChangedFields collects only the write flags the user // explicitly set into the update_ftpuser field map (keyed on the // ftpuser.Field* constants). Each field is a wholesale replacement and @@ -132,7 +162,7 @@ func newFTPUsersAddCmd(opts *RootOptions) *cobra.Command { // Changed, not on the value being non-empty — the same pattern the // cronjob update uses. The password flag maps to ftp_new_password here // (update_ftpuser's key) rather than the add-only ftp_password. -func ftpuserChangedFields(cmd *cobra.Command, f *ftpuserWriteFlags) map[string]string { +func ftpuserChangedFields(cmd *cobra.Command, f *ftpuserUpdateFlags) map[string]string { fields := map[string]string{} if cmd.Flags().Changed("password") { fields[ftpuser.FieldNewPassword] = f.password @@ -158,7 +188,7 @@ func ftpuserChangedFields(cmd *cobra.Command, f *ftpuserWriteFlags) map[string]s } func newFTPUsersUpdateCmd(opts *RootOptions) *cobra.Command { - f := &ftpuserWriteFlags{} + f := &ftpuserUpdateFlags{} cmd := &cobra.Command{ Use: "update [password/permission flags]", Short: "Replace mutable fields of an FTP user (update_ftpuser)", diff --git a/internal/cli/mail.go b/internal/cli/mail.go index ba5fcb8..f1a7374 100644 --- a/internal/cli/mail.go +++ b/internal/cli/mail.go @@ -62,16 +62,19 @@ func newMailListsAddCmd(opts *RootOptions) *cobra.Command { if password == "" { return writeSpec{}, fmt.Errorf("--password is required") } + var createdID string return writeSpec{ action: "add_mailinglist", destructive: false, confirm: ConfirmAction{Verb: "create", Resource: "mailing list", ID: name}, params: mailinglist.AddParams(name, domain, password), + createdID: &createdID, dispatch: func(c *api.Client, ctx context.Context) (string, error) { id, derr := mailinglist.NewClient(c).Add(ctx, name, domain, password) if derr != nil { return "", derr } + createdID = id return "created mailing list " + id, nil }, }, nil @@ -411,16 +414,19 @@ complete create; override any field with its flag.`, return writeSpec{}, fmt.Errorf("--password is required") } s := f.spec(local, domain) + var createdID string return writeSpec{ action: "add_mailaccount", destructive: false, confirm: ConfirmAction{Verb: "create", Resource: "mail account", ID: args[0]}, params: mailaccount.AddParams(s), + createdID: &createdID, dispatch: func(c *api.Client, ctx context.Context) (string, error) { login, derr := mailaccount.NewClient(c).Add(ctx, s) if derr != nil { return "", derr } + createdID = login return "created mail account " + login, nil }, }, nil @@ -619,16 +625,19 @@ func newMailForwardsAddCmd(opts *RootOptions) *cobra.Command { if len(targets) == 0 { return writeSpec{}, fmt.Errorf("at least one --target is required") } + var createdID string return writeSpec{ action: "add_mailforward", destructive: false, confirm: ConfirmAction{Verb: "create", Resource: "mail forward", ID: args[0]}, params: mailforward.AddParams(local, domain, targets), + createdID: &createdID, dispatch: func(c *api.Client, ctx context.Context) (string, error) { addr, derr := mailforward.NewClient(c).Add(ctx, local, domain, targets) if derr != nil { return "", derr } + createdID = addr return "created mail forward " + addr, nil }, }, nil diff --git a/internal/cli/root.go b/internal/cli/root.go index 1e448f7..6713049 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -1,6 +1,7 @@ package cli import ( + "errors" "fmt" "strings" @@ -62,6 +63,11 @@ func NewRootCmd() (*cobra.Command, *RootOptions) { opts.Output = f return nil }, + // Replicates cobra's root-level legacyArgs "unknown command" + // rejection, but as a UserError so `kasapi-cli nonsense` exits 1 + // (bad user input) instead of falling through CodeFor to the + // API-error exit 2. + Args: unknownSubcommandArgs, RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, @@ -97,7 +103,7 @@ func NewRootCmd() (*cobra.Command, *RootOptions) { "append a JSON-Lines audit record for each write action to this file "+ "(also KAS_AUDIT_LOG); a logfmt line always goes to stderr regardless") pf.BoolVar(&opts.DryRun, "dry-run", false, - "preview a destructive command's KAS request (action + redacted parameters) "+ + "preview a write command's KAS request (action + redacted parameters) "+ "and exit 0 without dispatching or prompting; honours --output") // --yes is wired: it is honoured by the destructive-write @@ -114,3 +120,72 @@ func NewRootCmd() (*cobra.Command, *RootOptions) { func joinFormats() string { return strings.Join(formatNames, "|") } + +// unknownSubcommandArgs is the positional-args validator shared by the +// root command and every group command: any positional argument is a +// subcommand name that did not resolve, so it is rejected as a +// UserError (exit 1) with cobra's own "Did you mean this?" suggestions. +func unknownSubcommandArgs(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return nil + } + msg := fmt.Sprintf("unknown command %q for %q", args[0], cmd.CommandPath()) + if s := cmd.SuggestionsFor(args[0]); len(s) > 0 { + msg += fmt.Sprintf("\n\nDid you mean this?\n\t%s\n", strings.Join(s, "\n\t")) + } + return UserError(errors.New(msg), "") +} + +// Finalize prepares the fully-assembled command tree for Execute. It +// registers cobra's lazily-added completion command up front (so the +// walkers below see it), rejects unknown subcommands on group commands, +// and marks args-validation failures as user errors. Lives in cli, not +// cmd/kasapi-cli, so tests exercise exactly the wiring the binary runs. +func Finalize(root *cobra.Command) { + root.InitDefaultCompletionCmd() + RejectUnknownSubcommands(root) + MarkArgErrorsAsUserErrors(root) +} + +// RejectUnknownSubcommands walks the command tree and gives every +// non-runnable group command (mail, accounts, config, ...) an explicit +// unknown-subcommand rejection. Without it cobra treats a group invoked +// with an unresolved name as a bare help call and exits 0 — a typo'd +// subcommand would read as success to scripts (cobra's legacyArgs only +// rejects unknown names at the root). A bare group invocation keeps +// printing help and exiting 0, matching the root command's behaviour. +func RejectUnknownSubcommands(cmd *cobra.Command) { + if cmd.HasSubCommands() && !cmd.Runnable() { + cmd.Args = unknownSubcommandArgs + cmd.RunE = func(c *cobra.Command, _ []string) error { + return c.Help() + } + } + for _, sub := range cmd.Commands() { + RejectUnknownSubcommands(sub) + } +} + +// MarkArgErrorsAsUserErrors walks the command tree and wraps every +// positional-args validator (cobra.ExactArgs, cobra.NoArgs, ...) so a +// validation failure carries ExitUserError. Without it those errors +// surface raw from Execute and CodeFor maps them to the API-error exit +// 2, contradicting the documented "1 = user error" contract. Called by +// cmd/kasapi-cli after all subcommands are registered. +func MarkArgErrorsAsUserErrors(cmd *cobra.Command) { + if validate := cmd.Args; validate != nil { + cmd.Args = func(c *cobra.Command, args []string) error { + if err := validate(c, args); err != nil { + var ee *ExitError + if errors.As(err, &ee) { + return err + } + return UserError(err, "") + } + return nil + } + } + for _, sub := range cmd.Commands() { + MarkArgErrorsAsUserErrors(sub) + } +} diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 8643f9b..350e4a6 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -108,6 +108,114 @@ func TestRootRejectsUnknownFlag(t *testing.T) { } } +// TestRootUnknownCommandExitsUserError pins the exit-code contract for +// a mistyped subcommand: bad user input must map to exit 1, not fall +// through CodeFor to the API-error exit 2. +func TestRootUnknownCommandExitsUserError(t *testing.T) { + t.Parallel() + root, _ := cli.NewRootCmd() + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"frobnicate"}) + err := root.Execute() + if err == nil { + t.Fatal("Execute frobnicate: want error, got nil") + } + if cli.CodeFor(err) != cli.ExitUserError { + t.Errorf("unknown command should map to ExitUserError, got %d", cli.CodeFor(err)) + } + if !strings.Contains(err.Error(), `unknown command "frobnicate"`) { + t.Errorf("error message: %q", err.Error()) + } +} + +// TestGroupUnknownSubcommandExitsUserError pins the exit-code contract +// for a typo'd subcommand under a group command: cobra alone treats it +// as a bare help call and exits 0, so Finalize must reject it as a +// user error (exit 1). +func TestGroupUnknownSubcommandExitsUserError(t *testing.T) { + t.Parallel() + root, opts := cli.NewRootCmd() + root.AddCommand(cli.NewMailCmd(opts)) + cli.Finalize(root) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"mail", "frobnicate"}) + err := root.Execute() + if err == nil { + t.Fatal("Execute mail frobnicate: want error, got nil") + } + if cli.CodeFor(err) != cli.ExitUserError { + t.Errorf("unknown subcommand should map to ExitUserError, got %d", cli.CodeFor(err)) + } + if !strings.Contains(err.Error(), `unknown command "frobnicate"`) { + t.Errorf("error message: %q", err.Error()) + } +} + +// A bare group invocation stays a help call with exit 0 after Finalize. +func TestGroupBareInvocationPrintsHelp(t *testing.T) { + t.Parallel() + root, opts := cli.NewRootCmd() + root.AddCommand(cli.NewMailCmd(opts)) + cli.Finalize(root) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"mail"}) + if err := root.Execute(); err != nil { + t.Fatalf("Execute mail: %v", err) + } + if !strings.Contains(out.String(), "accounts") { + t.Errorf("group help output missing subcommands:\n%s", out.String()) + } +} + +// TestCompletionArgErrorsExitUserError pins that the lazily-registered +// completion command is covered too: Finalize registers it before the +// walkers run, so its args-validation failures exit 1, not 2. +func TestCompletionArgErrorsExitUserError(t *testing.T) { + t.Parallel() + root, opts := cli.NewRootCmd() + root.AddCommand(cli.NewDomainsCmd(opts)) + cli.Finalize(root) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"completion", "bash", "extra"}) + err := root.Execute() + if err == nil { + t.Fatal("Execute completion bash extra: want error, got nil") + } + if cli.CodeFor(err) != cli.ExitUserError { + t.Errorf("completion args error should map to ExitUserError, got %d", cli.CodeFor(err)) + } +} + +// TestMarkArgErrorsAsUserErrors pins the exit-code contract for a +// positional-args validation failure (e.g. a missing required argument +// on an ExactArgs(1) subcommand) after the cmd/kasapi-cli wiring has +// applied the tree-wide wrapper. +func TestMarkArgErrorsAsUserErrors(t *testing.T) { + t.Parallel() + root, opts := cli.NewRootCmd() + root.AddCommand(cli.NewDomainsCmd(opts)) + cli.MarkArgErrorsAsUserErrors(root) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(&out) + root.SetArgs([]string{"domains", "get"}) + err := root.Execute() + if err == nil { + t.Fatal("Execute domains get (missing arg): want error, got nil") + } + if cli.CodeFor(err) != cli.ExitUserError { + t.Errorf("missing positional arg should map to ExitUserError, got %d", cli.CodeFor(err)) + } +} + // TestConfigInitDoesNotShadowRootProfile guards against the local // --profile flag on `config init` reappearing and silently overriding // the persistent root --profile. The local flag is now --name; the diff --git a/internal/cli/run.go b/internal/cli/run.go index 5665136..35cc27b 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -2,6 +2,8 @@ package cli import ( "context" + "errors" + "fmt" "io" "time" @@ -86,6 +88,14 @@ type writeSpec struct { confirm ConfirmAction params map[string]any dispatch func(c *api.Client, ctx context.Context) (string, error) + + // createdID, when non-nil, points at a variable the dispatch closure + // fills with the server-generated identifier of a create action + // (add_ftpuser, add_database, ...). A non-empty value is added to + // the success audit record's Fields under "created_id" so the trace + // correlates the create with the identifier later update/delete + // records carry as their Target. Nil for every other action. + createdID *string } // runWriteE is the write-subcommand counterpart of runListE/runGetE and @@ -135,6 +145,23 @@ func runWriteE(opts *RootOptions, build func(args []string) (writeSpec, error)) creds.Login, spec.action, spec.confirm, spec.params) if !proceed { // --dry-run → (false, nil): exit 0; declined/refused → (false, err). + // A gate refusal still leaves an audit trace: the attempt to run + // a destructive action is at least as interesting to an auditor + // as a dispatched one. An audit-write failure stays secondary — + // the refusal error is what the user must see. + if outcome := refusalOutcome(err); outcome != "" { + rec := AuditRecord{ + Time: time.Now().UTC(), + Login: creds.Login, + Action: spec.action, + Target: spec.confirm.ID, + Outcome: outcome, + Fields: RedactParams(spec.params), + } + if werr := WriteAudit(stderr, auditFile, rec); werr != nil { + _, _ = fmt.Fprintf(stderr, "warning: audit record not fully written: %v\n", werr) + } + } return err } @@ -152,20 +179,56 @@ func runWriteE(opts *RootOptions, build func(args []string) (writeSpec, error)) Outcome: OutcomeFor(derr), Fields: RedactParams(spec.params), } - if werr := WriteAudit(stderr, auditFile, rec); werr != nil { - return UserError(werr, "audit") + if derr == nil && spec.createdID != nil && *spec.createdID != "" { + if rec.Fields == nil { + rec.Fields = map[string]string{} + } + rec.Fields["created_id"] = *spec.createdID } - + werr := WriteAudit(stderr, auditFile, rec) if derr != nil { + // The dispatch outcome outranks an audit-write failure: a KAS + // fault must keep its APIError classification (exit 2) even + // when the audit sink also broke. + if werr != nil { + _, _ = fmt.Fprintf(stderr, "warning: audit record not fully written: %v\n", werr) + } return APIError(derr, spec.action) } if rerr := Render(out, opts.Output, writeResult{Message: result}); rerr != nil { + if werr != nil { + _, _ = fmt.Fprintf(stderr, "warning: audit record not fully written: %v\n", werr) + } return UserError(rerr, "render") } + if werr != nil { + // The write itself succeeded (result already rendered above); + // a broken audit sink still exits non-zero because the trace + // contract could not be honoured. + return UserError(werr, "audit") + } return nil } } +// refusalOutcome maps a WriteResolver refusal error to its audit +// outcome: "declined" for an interactive no, "refused" for the +// non-TTY-without---yes abort, "aborted" for a prompt I/O failure. +// Any other error (or nil, the --dry-run case, which writes its own +// record) yields "" — no record. +func refusalOutcome(err error) string { + switch { + case errors.Is(err, ErrConfirmationDeclined): + return AuditOutcomeDeclined + case errors.Is(err, ErrConfirmationRequired): + return AuditOutcomeRefused + case errors.Is(err, ErrConfirmationAborted): + return AuditOutcomeAborted + default: + return "" + } +} + // writeResult wraps the human success line a write dispatch returns so // it renders through the same --output pipeline the read commands use: // table output stays the bare line (no header row), json/yaml emit a diff --git a/internal/cli/run_internal_test.go b/internal/cli/run_internal_test.go new file mode 100644 index 0000000..7c7479b --- /dev/null +++ b/internal/cli/run_internal_test.go @@ -0,0 +1,80 @@ +package cli + +// White-box tests for the runWriteE seam: the success-audit created_id +// correlation and the refusal→outcome mapping are internal wiring the +// cli_test package cannot reach (writeSpec and refusalOutcome are +// unexported), so they are pinned here. + +import ( + "bytes" + "context" + "errors" + "fmt" + "strings" + "testing" + + "github.com/spf13/cobra" + + "github.com/chmmou/kasapi-cli/internal/api" +) + +// TestRunWriteESuccessAuditCarriesCreatedID pins that a create action +// whose identifier KAS generates server-side surfaces that identifier +// in the success audit record (created_id), not only in the rendered +// success line. +func TestRunWriteESuccessAuditCarriesCreatedID(t *testing.T) { + t.Parallel() + opts := &RootOptions{ + Login: "w0000000", + AuthData: "x", + AuthType: "plain", + Output: FormatTable, + } + var createdID string + run := runWriteE(opts, func([]string) (writeSpec, error) { + return writeSpec{ + action: "add_ftpuser", + destructive: false, + confirm: ConfirmAction{Verb: "create", Resource: "ftp user", ID: "backup user"}, + params: map[string]any{"ftp_comment": "backup user"}, + createdID: &createdID, + dispatch: func(_ *api.Client, _ context.Context) (string, error) { + createdID = "f0000001" + return "created ftp user f0000001", nil + }, + }, nil + }) + cmd := &cobra.Command{} + cmd.SetContext(context.Background()) + var out, errb bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&errb) + if err := run(cmd, nil); err != nil { + t.Fatalf("runWriteE: %v", err) + } + if !strings.Contains(errb.String(), "created_id=f0000001") { + t.Errorf("audit line missing created_id=f0000001: %s", errb.String()) + } + if !strings.Contains(errb.String(), "outcome=success") { + t.Errorf("audit line missing outcome=success: %s", errb.String()) + } +} + +func TestRefusalOutcome(t *testing.T) { + t.Parallel() + cases := []struct { + err error + want string + }{ + {UserError(ErrConfirmationDeclined, ""), AuditOutcomeDeclined}, + {UserError(ErrConfirmationRequired, ""), AuditOutcomeRefused}, + {UserError(fmt.Errorf("%w: %w", ErrConfirmationAborted, errors.New("tty gone")), ""), AuditOutcomeAborted}, + {errors.New("unrelated"), ""}, + {nil, ""}, + } + for _, c := range cases { + if got := refusalOutcome(c.err); got != c.want { + t.Errorf("refusalOutcome(%v) = %q, want %q", c.err, got, c.want) + } + } +} diff --git a/internal/cli/sambausers.go b/internal/cli/sambausers.go index e812ce9..31c54b8 100644 --- a/internal/cli/sambausers.go +++ b/internal/cli/sambausers.go @@ -52,13 +52,12 @@ func newSambaUsersGetCmd(opts *RootOptions) *cobra.Command { } } -// sambauserWriteFlags binds the shared add_sambauser / update_sambauser -// request fields to a command. The same flag set serves both: add -// reads every value, update sends only the flags the user explicitly -// changed (see sambauserChangedFields). The password flag maps to a -// different KAS key per action (add_sambauser: samba_password, -// update_sambauser: samba_new_password) — see spec() and -// sambauserChangedFields. +// sambauserWriteFlags binds the add_sambauser request fields to the +// add command. update uses the disjoint sambauserUpdateFlags below — +// the same split the cronjob/ftpuser slices use — so update's --help +// does not describe add requirements. The password flag maps to +// add_sambauser's samba_password key (update sends +// samba_new_password) — see spec() and sambauserChangedFields. type sambauserWriteFlags struct { password string comment string @@ -67,9 +66,9 @@ type sambauserWriteFlags struct { func (f *sambauserWriteFlags) bind(cmd *cobra.Command) { fl := cmd.Flags() - fl.StringVar(&f.password, "password", "", "Samba password (required for add; new password for update)") - fl.StringVar(&f.comment, "comment", "", "user comment / label (required for add)") - fl.StringVar(&f.path, "path", "", "share path the user is granted (samba_path; required for add)") + fl.StringVar(&f.password, "password", "", "Samba password (required)") + fl.StringVar(&f.comment, "comment", "", "user comment / label (required)") + fl.StringVar(&f.path, "path", "", "share path the user is granted (samba_path; required)") } func (f *sambauserWriteFlags) spec() sambauser.Spec { @@ -97,16 +96,19 @@ func newSambaUsersAddCmd(opts *RootOptions) *cobra.Command { return writeSpec{}, fmt.Errorf("--path is required") } s := f.spec() + var createdID string return writeSpec{ action: "add_sambauser", destructive: false, confirm: ConfirmAction{Verb: "create", Resource: "samba user", ID: f.comment}, params: sambauser.AddParams(s), + createdID: &createdID, dispatch: func(c *api.Client, ctx context.Context) (string, error) { login, derr := sambauser.NewClient(c).Add(ctx, s) if derr != nil { return "", derr } + createdID = login return "created samba user " + login, nil }, }, nil @@ -116,6 +118,23 @@ func newSambaUsersAddCmd(opts *RootOptions) *cobra.Command { return cmd } +// sambauserUpdateFlags binds the update_sambauser mutable surface. +// Disjoint from sambauserWriteFlags so update's --help describes the +// flags as replacements instead of add requirements — the same split +// the cronjob/ftpuser slices use. +type sambauserUpdateFlags struct { + password string + comment string + path string +} + +func (f *sambauserUpdateFlags) bind(cmd *cobra.Command) { + fl := cmd.Flags() + fl.StringVar(&f.password, "password", "", "replacement Samba password (sent as samba_new_password)") + fl.StringVar(&f.comment, "comment", "", "replacement user comment / label") + fl.StringVar(&f.path, "path", "", "replacement share path the user is granted (samba_path)") +} + // sambauserChangedFields collects only the write flags the user // explicitly set into the update_sambauser field map (keyed on the // sambauser.Field* constants). Each field is a wholesale replacement @@ -124,7 +143,7 @@ func newSambaUsersAddCmd(opts *RootOptions) *cobra.Command { // ftpuser update uses. The password flag maps to samba_new_password // here (update_sambauser's key) rather than the add-only // samba_password. -func sambauserChangedFields(cmd *cobra.Command, f *sambauserWriteFlags) map[string]string { +func sambauserChangedFields(cmd *cobra.Command, f *sambauserUpdateFlags) map[string]string { fields := map[string]string{} if cmd.Flags().Changed("password") { fields[sambauser.FieldNewPassword] = f.password @@ -139,7 +158,7 @@ func sambauserChangedFields(cmd *cobra.Command, f *sambauserWriteFlags) map[stri } func newSambaUsersUpdateCmd(opts *RootOptions) *cobra.Command { - f := &sambauserWriteFlags{} + f := &sambauserUpdateFlags{} cmd := &cobra.Command{ Use: "update [password/path flags]", Short: "Replace mutable fields of a Samba user (update_sambauser)", diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 5da33e1..ffeb72f 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -163,6 +163,23 @@ func TestResolveUnknownProfile(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "missing") { t.Fatalf("expected unknown-profile error, got %v", err) } + if !errors.Is(err, config.ErrUnknownProfile) { + t.Errorf("err = %v, want errors.Is ErrUnknownProfile", err) + } +} + +// TestResolveProfileWithoutConfigIsUnknownProfile pins the sentinel on +// the nil-Config + --profile path so callers can errors.Is-branch on it +// like on the profile-not-in-file case. +func TestResolveProfileWithoutConfigIsUnknownProfile(t *testing.T) { + var cfg *config.Config + _, err := cfg.Resolve(config.Env{}, config.Override{Profile: "prod"}) + if err == nil || !strings.Contains(err.Error(), "no config file loaded") { + t.Fatalf("expected no-config profile error, got %v", err) + } + if !errors.Is(err, config.ErrUnknownProfile) { + t.Errorf("err = %v, want errors.Is ErrUnknownProfile", err) + } } func TestResolveMissingCredentials(t *testing.T) { @@ -176,6 +193,9 @@ func TestResolveMissingCredentials(t *testing.T) { if err == nil { t.Fatal("expected error for missing credentials") } + if !errors.Is(err, config.ErrMissingCredentials) { + t.Errorf("err = %v, want errors.Is ErrMissingCredentials", err) + } for _, want := range []string{"auth_data", "auth_type"} { if !strings.Contains(err.Error(), want) { t.Errorf("error %q does not mention %q", err, want) diff --git a/internal/config/credentials.go b/internal/config/credentials.go index 7f55f2e..d9e3933 100644 --- a/internal/config/credentials.go +++ b/internal/config/credentials.go @@ -1,11 +1,23 @@ package config import ( + "errors" "fmt" "os" "strings" ) +// ErrUnknownProfile is returned by Resolve when the requested profile +// (via --profile or default_profile) does not exist in the config file, +// or when --profile was given without any config file loaded. +// Callers branch with errors.Is instead of string-matching. +var ErrUnknownProfile = errors.New("config: profile not defined") + +// ErrMissingCredentials is returned by Resolve when the flag > env > +// config precedence still leaves login, auth_data, or auth_type empty. +// Callers branch with errors.Is instead of string-matching. +var ErrMissingCredentials = errors.New("config: missing credentials") + // Credentials are the resolved values for a single KAS API call. type Credentials struct { Login string @@ -61,12 +73,12 @@ func (c *Config) Resolve(env Env, ov Override) (Credentials, error) { if name != "" { p, ok := c.Profiles[name] if !ok { - return Credentials{}, fmt.Errorf("config: profile %q not defined", name) + return Credentials{}, fmt.Errorf("%w: %q", ErrUnknownProfile, name) } prof = p } } else if ov.Profile != "" { - return Credentials{}, fmt.Errorf("config: --profile %q given but no config file loaded", ov.Profile) + return Credentials{}, fmt.Errorf("%w: --profile %q given but no config file loaded", ErrUnknownProfile, ov.Profile) } cred := Credentials{ @@ -98,7 +110,7 @@ func (c Credentials) validate() error { missing = append(missing, "auth_type (--auth-type or KAS_AUTHTYPE)") } if len(missing) > 0 { - return fmt.Errorf("config: missing credentials: %s", strings.Join(missing, ", ")) + return fmt.Errorf("%w: %s", ErrMissingCredentials, strings.Join(missing, ", ")) } if c.AuthType != AuthPlain && c.AuthType != AuthSession { return fmt.Errorf("config: auth_type %q must be %q or %q", c.AuthType, AuthPlain, AuthSession) diff --git a/internal/cronjob/cronjob.go b/internal/cronjob/cronjob.go index cb43b19..f2eb83b 100644 --- a/internal/cronjob/cronjob.go +++ b/internal/cronjob/cronjob.go @@ -98,7 +98,7 @@ func NewClient(c Caller) *Client { Action: "get_cronjobs", Label: "cronjob", ArgName: "id", - FilterKey: "cronjob_id", + FilterKey: FieldID, Decoder: DecodeCronjobs, }, c: c, diff --git a/internal/cronjob/cronjob_test.go b/internal/cronjob/cronjob_test.go index dc26649..0f4b2d6 100644 --- a/internal/cronjob/cronjob_test.go +++ b/internal/cronjob/cronjob_test.go @@ -60,7 +60,7 @@ func TestDecodeCronjobs(t *testing.T) { func TestDecodeCronjobSingular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "cronjob/get_cronjob_response_success.xml") + resp := testutil.DecodeFixture(t, "cronjob/get_cronjobs_response_success_single.xml") got, err := cronjob.DecodeCronjobs(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeCronjobs: %v", err) @@ -135,7 +135,7 @@ func TestClientList(t *testing.T) { func TestClientGet(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "cronjob/get_cronjob_response_success.xml") + resp := testutil.DecodeFixture(t, "cronjob/get_cronjobs_response_success_single.xml") fc := &testutil.FakeCaller{Resp: resp} c, err := cronjob.NewClient(fc).Get(context.Background(), "325208") if err != nil { @@ -206,7 +206,7 @@ func TestCronjobListTabular(t *testing.T) { func TestCronjobTabular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "cronjob/get_cronjob_response_success.xml") + resp := testutil.DecodeFixture(t, "cronjob/get_cronjobs_response_success_single.xml") list, _ := cronjob.DecodeCronjobs(resp.Body.ReturnInfo) if len(list) != 1 { t.Fatalf("len = %d, want 1", len(list)) diff --git a/internal/cronjob/doc.go b/internal/cronjob/doc.go index bf02e97..2ffa4d1 100644 --- a/internal/cronjob/doc.go +++ b/internal/cronjob/doc.go @@ -1,4 +1,5 @@ // Package cronjob holds the domain types and use cases for the KAS cronjob -// endpoints (get_cronjobs, get_cronjob, add_cronjob, update_cronjob, -// delete_cronjob). See issues #11 and #13. +// endpoints (get_cronjobs, add_cronjob, update_cronjob, delete_cronjob). +// A single cronjob is a get_cronjobs call with the cronjob_id filter; +// there is no singular get action. See issues #11 and #13. package cronjob diff --git a/internal/cronjob/write.go b/internal/cronjob/write.go index 8fc234a..6c1f006 100644 --- a/internal/cronjob/write.go +++ b/internal/cronjob/write.go @@ -19,6 +19,10 @@ const ( deleteAction = "delete_cronjob" ) +// FieldID is the cronjob_id identifier key shared by the get filter, +// update_cronjob, and delete_cronjob. +const FieldID = "cronjob_id" + // The Field-prefixed constants are the KAS request keys update_cronjob // accepts besides the cronjob_id identifier; AddParams uses the same // keys. Each is an optional wholesale replacement on update: only the @@ -148,7 +152,7 @@ func (cl *Client) Update(ctx context.Context, id string, fields map[string]strin // (single source of truth, see AddParams): the cronjob_id identifier // plus every caller-supplied mutable field verbatim. func UpdateParams(id string, fields map[string]string) map[string]any { - params := map[string]any{"cronjob_id": id} + params := map[string]any{FieldID: id} for k, v := range fields { params[k] = v } @@ -169,5 +173,5 @@ func (cl *Client) Delete(ctx context.Context, id string) error { // DeleteParams builds the delete_cronjob KAS request parameter map // (single source of truth, see AddParams). func DeleteParams(id string) map[string]any { - return map[string]any{"cronjob_id": id} + return map[string]any{FieldID: id} } diff --git a/internal/cronjob/write_test.go b/internal/cronjob/write_test.go index d20380f..b74a14e 100644 --- a/internal/cronjob/write_test.go +++ b/internal/cronjob/write_test.go @@ -55,7 +55,7 @@ func TestClientAdd(t *testing.T) { // return the new id rather than wrapping ErrUnexpectedReturnString. func TestClientAddWarningStillSucceeds(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "cronjob/add_cronjob_response_warning.xml") + resp := testutil.DecodeFixture(t, "cronjob/add_cronjob_response_success_warning.xml") id, err := cronjob.NewClient(&testutil.FakeCaller{Resp: resp}).Add(context.Background(), sampleSpec()) if err != nil { t.Fatalf("Add (warning): %v", err) diff --git a/internal/database/database_test.go b/internal/database/database_test.go index 4248b4a..3e24d9e 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -48,7 +48,7 @@ func TestDecodeDatabases(t *testing.T) { func TestDecodeDatabaseSingular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "database/get_database_response_success.xml") + resp := testutil.DecodeFixture(t, "database/get_databases_response_success_single.xml") got, err := database.DecodeDatabases(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeDatabases: %v", err) @@ -86,7 +86,7 @@ func TestClientList(t *testing.T) { func TestClientGet(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "database/get_database_response_success.xml") + resp := testutil.DecodeFixture(t, "database/get_databases_response_success_single.xml") fc := &testutil.FakeCaller{Resp: resp} d, err := database.NewClient(fc).Get(context.Background(), "d0123460") if err != nil { @@ -151,7 +151,7 @@ func TestDatabaseListTabular(t *testing.T) { func TestDatabaseTabular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "database/get_database_response_success.xml") + resp := testutil.DecodeFixture(t, "database/get_databases_response_success_single.xml") list, _ := database.DecodeDatabases(resp.Body.ReturnInfo) if len(list) != 1 { t.Fatalf("len = %d, want 1", len(list)) diff --git a/internal/database/doc.go b/internal/database/doc.go index 5477d19..ba41ffd 100644 --- a/internal/database/doc.go +++ b/internal/database/doc.go @@ -1,4 +1,6 @@ // Package database holds the domain types and use cases for the KAS -// database endpoints (get_databases, get_database, add_database, -// update_database, delete_database). See issues #11 and #13. +// database endpoints (get_databases, add_database, update_database, +// delete_database — there is no singular get action; a single database is +// fetched via get_databases with the database_login filter). See issues +// #11 and #13. package database diff --git a/internal/ddns/ddns.go b/internal/ddns/ddns.go index 739a3b6..71915e4 100644 --- a/internal/ddns/ddns.go +++ b/internal/ddns/ddns.go @@ -33,8 +33,9 @@ type Caller = kasread.Caller // // `in_progress` is no longer omitempty for parity with the majority // of read modules (account, mailaccount, mailinglist, sambauser, -// ftpuser, database): every captured fixture row carries it and a -// stray empty-string on an older account is harmless. +// ftpuser, database). The captured ddns fixtures do not carry the key +// at all, so it decodes to an empty string — emitting that "" is +// harmless and matches what the other modules show on older accounts. type DDNSUser struct { Login string `json:"dyndns_login" yaml:"dyndns_login"` Password string `json:"dyndns_password,omitempty" yaml:"dyndns_password,omitempty"` diff --git a/internal/ddns/ddns_test.go b/internal/ddns/ddns_test.go index 945ec28..1aea422 100644 --- a/internal/ddns/ddns_test.go +++ b/internal/ddns/ddns_test.go @@ -53,7 +53,7 @@ func TestDecodeDDNSUsers(t *testing.T) { func TestDecodeDDNSUserSingular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "ddns/get_ddnsuser_response_success.xml") + resp := testutil.DecodeFixture(t, "ddns/get_ddnsusers_response_success_single.xml") got, err := ddns.DecodeDDNSUsers(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeDDNSUsers: %v", err) @@ -121,7 +121,7 @@ func TestClientList(t *testing.T) { func TestClientGet(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "ddns/get_ddnsuser_response_success.xml") + resp := testutil.DecodeFixture(t, "ddns/get_ddnsusers_response_success_single.xml") fc := &testutil.FakeCaller{Resp: resp} u, err := ddns.NewClient(fc).Get(context.Background(), "dyn0000002") if err != nil { @@ -160,7 +160,7 @@ func TestClientGetEmptyLogin(t *testing.T) { // array stripped down to zero entries. func TestClientGetNotFound(t *testing.T) { t.Parallel() - emptyResp := testutil.DecodeFixture(t, "ddns/get_ddnsuser_response_success.xml") + emptyResp := testutil.DecodeFixture(t, "ddns/get_ddnsusers_response_success_single.xml") emptyResp.Body.ReturnInfo.Array = nil c := ddns.NewClient(&testutil.FakeCaller{Resp: emptyResp}) _, err := c.Get(context.Background(), "ghost") @@ -206,7 +206,7 @@ func TestDDNSUserListTabular(t *testing.T) { func TestDDNSUserTabular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "ddns/get_ddnsuser_response_success.xml") + resp := testutil.DecodeFixture(t, "ddns/get_ddnsusers_response_success_single.xml") list, _ := ddns.DecodeDDNSUsers(resp.Body.ReturnInfo) if len(list) != 1 { t.Fatalf("len = %d, want 1", len(list)) diff --git a/internal/directoryprotection/directoryprotection.go b/internal/directoryprotection/directoryprotection.go index b337ba8..079dd9b 100644 --- a/internal/directoryprotection/directoryprotection.go +++ b/internal/directoryprotection/directoryprotection.go @@ -38,22 +38,22 @@ type DirectoryProtectionList []DirectoryProtection // even when no filter is set), so this module exposes one List // method that takes an optional path. type Client struct { - API Caller + c Caller } // NewClient returns a Client backed by the given Caller. -func NewClient(c Caller) *Client { return &Client{API: c} } +func NewClient(c Caller) *Client { return &Client{c: c} } // List calls get_directoryprotection and decodes the response into a // DirectoryProtectionList. An empty path returns every protected // directory; a non-empty path filters to that directory's user // entries (still a list, since multiple users per path are possible). -func (c *Client) List(ctx context.Context, path string) (DirectoryProtectionList, error) { +func (cl *Client) List(ctx context.Context, path string) (DirectoryProtectionList, error) { var params map[string]any if path != "" { params = map[string]any{"directory_path": path} } - resp, err := c.API.Call(ctx, "get_directoryprotection", params) + resp, err := cl.c.Call(ctx, "get_directoryprotection", params) if err != nil { return nil, err } diff --git a/internal/directoryprotection/directoryprotection_test.go b/internal/directoryprotection/directoryprotection_test.go index 0ecddae..78bfbb7 100644 --- a/internal/directoryprotection/directoryprotection_test.go +++ b/internal/directoryprotection/directoryprotection_test.go @@ -11,7 +11,7 @@ import ( func TestDecodeDirectoryProtections(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "directoryprotection/get_directoryprotections_response_success.xml") + resp := testutil.DecodeFixture(t, "directoryprotection/get_directoryprotection_response_success_all.xml") got, err := directoryprotection.DecodeDirectoryProtections(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeDirectoryProtections: %v", err) @@ -57,7 +57,7 @@ func TestDecodeDirectoryProtectionSingular(t *testing.T) { func TestClientListNoFilter(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "directoryprotection/get_directoryprotections_response_success.xml") + resp := testutil.DecodeFixture(t, "directoryprotection/get_directoryprotection_response_success_all.xml") fc := &testutil.FakeCaller{Resp: resp} list, err := directoryprotection.NewClient(fc).List(context.Background(), "") if err != nil { @@ -107,7 +107,7 @@ func TestClientPropagatesError(t *testing.T) { func TestDirectoryProtectionListTabular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "directoryprotection/get_directoryprotections_response_success.xml") + resp := testutil.DecodeFixture(t, "directoryprotection/get_directoryprotection_response_success_all.xml") list, _ := directoryprotection.DecodeDirectoryProtections(resp.Body.ReturnInfo) headers := list.TableHeaders() if headers[0] != "PATH" { diff --git a/internal/directoryprotection/doc.go b/internal/directoryprotection/doc.go index ab8c403..563f192 100644 --- a/internal/directoryprotection/doc.go +++ b/internal/directoryprotection/doc.go @@ -1,6 +1,6 @@ // Package directoryprotection holds the domain types and use cases for the -// KAS directory-protection endpoints (get_directoryprotections, -// get_directoryprotection, add_directoryprotection, -// update_directoryprotection, delete_directoryprotection). See issues #11 -// and #13. +// KAS directory-protection endpoints (get_directoryprotection, +// add_directoryprotection, update_directoryprotection, +// delete_directoryprotection — there is no plural get action; the list is +// the unfiltered get_directoryprotection call). See issues #11 and #13. package directoryprotection diff --git a/internal/directoryprotection/write.go b/internal/directoryprotection/write.go index ffa0e79..c575902 100644 --- a/internal/directoryprotection/write.go +++ b/internal/directoryprotection/write.go @@ -74,7 +74,7 @@ func (cl *Client) Add(ctx context.Context, s Spec) (string, error) { case s.Password == "": return "", errors.New("directoryprotection: add_directoryprotection requires a non-empty directory password") } - resp, err := kaswrite.Call(ctx, cl.API, "directoryprotection", addAction, AddParams(s)) + resp, err := kaswrite.Call(ctx, cl.c, "directoryprotection", addAction, AddParams(s)) if err != nil { return "", err } @@ -111,7 +111,7 @@ func (cl *Client) Update(ctx context.Context, path, user string, fields map[stri if len(fields) == 0 { return errors.New("directoryprotection: update_directoryprotection requires at least one field to change") } - _, err := kaswrite.Call(ctx, cl.API, "directoryprotection", updateAction, UpdateParams(path, user, fields)) + _, err := kaswrite.Call(ctx, cl.c, "directoryprotection", updateAction, UpdateParams(path, user, fields)) return err } @@ -142,7 +142,7 @@ func (cl *Client) Delete(ctx context.Context, path, user string) error { case user == "": return errors.New("directoryprotection: delete_directoryprotection requires a non-empty directory user") } - _, err := kaswrite.Call(ctx, cl.API, "directoryprotection", deleteAction, DeleteParams(path, user)) + _, err := kaswrite.Call(ctx, cl.c, "directoryprotection", deleteAction, DeleteParams(path, user)) return err } diff --git a/internal/dns/dns.go b/internal/dns/dns.go index 243f576..e6808ac 100644 --- a/internal/dns/dns.go +++ b/internal/dns/dns.go @@ -35,18 +35,18 @@ type RecordList []Record // Client groups the read endpoints scoped to DNS settings. type Client struct { - API Caller + c Caller } // NewClient returns a Client backed by the given Caller. -func NewClient(c Caller) *Client { return &Client{API: c} } +func NewClient(c Caller) *Client { return &Client{c: c} } // Settings calls get_dns_settings for the given zone host and decodes // the response into a RecordList. zoneHost is required (the zone the // records belong to, e.g. "example.com"). recordID is optional and // narrows the result to the single resource record with that ID — // leave it empty to list every record in the zone. -func (c *Client) Settings(ctx context.Context, zoneHost, recordID string) (RecordList, error) { +func (cl *Client) Settings(ctx context.Context, zoneHost, recordID string) (RecordList, error) { if zoneHost == "" { return nil, fmt.Errorf("dns: zone_host is required") } @@ -54,7 +54,7 @@ func (c *Client) Settings(ctx context.Context, zoneHost, recordID string) (Recor if recordID != "" { params["record_id"] = recordID } - resp, err := c.API.Call(ctx, "get_dns_settings", params) + resp, err := cl.c.Call(ctx, "get_dns_settings", params) if err != nil { return nil, err } diff --git a/internal/dns/dns_test.go b/internal/dns/dns_test.go index 79a5553..00df2de 100644 --- a/internal/dns/dns_test.go +++ b/internal/dns/dns_test.go @@ -75,7 +75,7 @@ func TestClientSettings(t *testing.T) { // zone_host+record_id fixture returns a one-element ReturnInfo array. func TestClientSettingsWithRecordID(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "dns/get_dns_settings_zone_host_and_record_id_response_success.xml") + resp := testutil.DecodeFixture(t, "dns/get_dns_settings_response_success_zone_host_and_record_id.xml") fc := &testutil.FakeCaller{Resp: resp} list, err := dns.NewClient(fc).Settings(context.Background(), "example.com", "110118416") if err != nil { diff --git a/internal/domain/domain_test.go b/internal/domain/domain_test.go index ec32b3c..39b7b1c 100644 --- a/internal/domain/domain_test.go +++ b/internal/domain/domain_test.go @@ -44,7 +44,7 @@ func TestDecodeDomains(t *testing.T) { func TestDecodeDomainSingular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "domain/get_domain_response_success.xml") + resp := testutil.DecodeFixture(t, "domain/get_domains_response_success_single.xml") got, err := domain.DecodeDomains(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeDomains: %v", err) @@ -126,7 +126,7 @@ func TestClientList(t *testing.T) { func TestClientGet(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "domain/get_domain_response_success.xml") + resp := testutil.DecodeFixture(t, "domain/get_domains_response_success_single.xml") fc := &testutil.FakeCaller{Resp: resp} d, err := domain.NewClient(fc).Get(context.Background(), "example.com") if err != nil { @@ -221,7 +221,7 @@ func TestTLDListTabular(t *testing.T) { func TestDomainTabularSummarisesPEM(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "domain/get_domain_response_success.xml") + resp := testutil.DecodeFixture(t, "domain/get_domains_response_success_single.xml") list, _ := domain.DecodeDomains(resp.Body.ReturnInfo) rows := list[0].TableRows() for _, row := range rows { diff --git a/internal/ftpuser/doc.go b/internal/ftpuser/doc.go index 3897b42..1490d68 100644 --- a/internal/ftpuser/doc.go +++ b/internal/ftpuser/doc.go @@ -1,7 +1,9 @@ // Package ftpuser holds the domain types and use cases for the KAS FTP-user -// endpoints (get_ftpusers, get_ftpuser, add_ftpuser, update_ftpuser, -// delete_ftpuser). See issues #11 and #13. +// endpoints (get_ftpusers, add_ftpuser, update_ftpuser, delete_ftpuser). +// A single user is a get_ftpusers call with the ftp_login filter; there +// is no singular get action. See issues #11 and #13. // -// Note: KAS docs spell the create action add_ftpusers (plural) while the -// fixture folder uses the singular form — verify against the live API. +// Note: KAS docs spell the create action add_ftpusers (plural); the +// captured request fixture (#119, verified against the live API) +// confirms the real action is the singular add_ftpuser. package ftpuser diff --git a/internal/ftpuser/ftpuser_test.go b/internal/ftpuser/ftpuser_test.go index e7239fc..f263e38 100644 --- a/internal/ftpuser/ftpuser_test.go +++ b/internal/ftpuser/ftpuser_test.go @@ -41,7 +41,7 @@ func TestDecodeFTPUsers(t *testing.T) { func TestDecodeFTPUserSingular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "ftpuser/get_ftpuser_response_success.xml") + resp := testutil.DecodeFixture(t, "ftpuser/get_ftpusers_response_success_single.xml") got, err := ftpuser.DecodeFTPUsers(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeFTPUsers: %v", err) @@ -68,7 +68,7 @@ func TestDecodeFTPUserSingular(t *testing.T) { func TestDecodeFTPUsersEmptyList(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "ftpuser/get_ftpuser_response_success_empty_list.xml") + resp := testutil.DecodeFixture(t, "ftpuser/get_ftpusers_response_success_empty_list.xml") got, err := ftpuser.DecodeFTPUsers(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeFTPUsers: %v", err) @@ -99,7 +99,7 @@ func TestClientList(t *testing.T) { func TestClientGet(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "ftpuser/get_ftpuser_response_success.xml") + resp := testutil.DecodeFixture(t, "ftpuser/get_ftpusers_response_success_single.xml") fc := &testutil.FakeCaller{Resp: resp} u, err := ftpuser.NewClient(fc).Get(context.Background(), "f0000001") if err != nil { @@ -126,7 +126,7 @@ func TestClientGetEmptyLogin(t *testing.T) { func TestClientGetNotFound(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "ftpuser/get_ftpuser_response_success_empty_list.xml") + resp := testutil.DecodeFixture(t, "ftpuser/get_ftpusers_response_success_empty_list.xml") c := ftpuser.NewClient(&testutil.FakeCaller{Resp: resp}) if _, err := c.Get(context.Background(), "missing"); err == nil { t.Errorf("Get on empty result err = nil, want not-found") @@ -164,7 +164,7 @@ func TestFTPUserListTabular(t *testing.T) { func TestFTPUserTabular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "ftpuser/get_ftpuser_response_success.xml") + resp := testutil.DecodeFixture(t, "ftpuser/get_ftpusers_response_success_single.xml") list, _ := ftpuser.DecodeFTPUsers(resp.Body.ReturnInfo) if len(list) != 1 { t.Fatalf("len = %d, want 1", len(list)) diff --git a/internal/mailaccount/doc.go b/internal/mailaccount/doc.go index edf53cf..08995ea 100644 --- a/internal/mailaccount/doc.go +++ b/internal/mailaccount/doc.go @@ -1,4 +1,6 @@ // Package mailaccount holds the domain types and use cases for the KAS -// mail-account endpoints (get_mailaccounts, get_mailaccount, add_mailaccount, -// update_mailaccount, delete_mailaccount). See issues #9 and #13. +// mail-account endpoints (get_mailaccounts, add_mailaccount, +// update_mailaccount, delete_mailaccount). A single account is a +// get_mailaccounts call with the mail_login filter; there is no +// singular get action. See issues #9 and #13. package mailaccount diff --git a/internal/mailaccount/mailaccount_test.go b/internal/mailaccount/mailaccount_test.go index c389f4f..0f1748d 100644 --- a/internal/mailaccount/mailaccount_test.go +++ b/internal/mailaccount/mailaccount_test.go @@ -43,7 +43,7 @@ func TestDecodeMailAccounts(t *testing.T) { func TestDecodeMailAccountSingular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "mailaccount/get_mailaccount_response_success.xml") + resp := testutil.DecodeFixture(t, "mailaccount/get_mailaccounts_response_success_single.xml") got, err := mailaccount.DecodeMailAccounts(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeMailAccounts: %v", err) @@ -84,7 +84,7 @@ func TestClientList(t *testing.T) { func TestClientGet(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "mailaccount/get_mailaccount_response_success.xml") + resp := testutil.DecodeFixture(t, "mailaccount/get_mailaccounts_response_success_single.xml") fc := &testutil.FakeCaller{Resp: resp} a, err := mailaccount.NewClient(fc).Get(context.Background(), "m0000001") if err != nil { @@ -148,7 +148,7 @@ func TestMailAccountListTabular(t *testing.T) { func TestMailAccountTabular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "mailaccount/get_mailaccount_response_success.xml") + resp := testutil.DecodeFixture(t, "mailaccount/get_mailaccounts_response_success_single.xml") list, _ := mailaccount.DecodeMailAccounts(resp.Body.ReturnInfo) rows := list[0].TableRows() if len(rows) == 0 { diff --git a/internal/mailforward/mailforward_test.go b/internal/mailforward/mailforward_test.go index 4314f30..d6985fc 100644 --- a/internal/mailforward/mailforward_test.go +++ b/internal/mailforward/mailforward_test.go @@ -37,7 +37,7 @@ func TestDecodeMailForwards(t *testing.T) { func TestDecodeMailForwardSingular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "mailforward/get_mailforward_response_success.xml") + resp := testutil.DecodeFixture(t, "mailforward/get_mailforwards_response_success_single.xml") got, err := mailforward.DecodeMailForwards(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeMailForwards: %v", err) @@ -71,7 +71,7 @@ func TestClientList(t *testing.T) { func TestClientGet(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "mailforward/get_mailforward_response_success.xml") + resp := testutil.DecodeFixture(t, "mailforward/get_mailforwards_response_success_single.xml") fc := &testutil.FakeCaller{Resp: resp} f, err := mailforward.NewClient(fc).Get(context.Background(), "from@example.de") if err != nil { diff --git a/internal/mailinglist/mailinglist_test.go b/internal/mailinglist/mailinglist_test.go index 9eec93d..80a4539 100644 --- a/internal/mailinglist/mailinglist_test.go +++ b/internal/mailinglist/mailinglist_test.go @@ -43,7 +43,7 @@ func TestDecodeMailingLists(t *testing.T) { func TestDecodeMailingListSingular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "mailinglist/get_mailinglist_response_success.xml") + resp := testutil.DecodeFixture(t, "mailinglist/get_mailinglists_response_success_single.xml") got, err := mailinglist.DecodeMailingLists(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeMailingLists: %v", err) @@ -102,7 +102,7 @@ func TestClientListEmpty(t *testing.T) { func TestClientGet(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "mailinglist/get_mailinglist_response_success.xml") + resp := testutil.DecodeFixture(t, "mailinglist/get_mailinglists_response_success_single.xml") fc := &testutil.FakeCaller{Resp: resp} m, err := mailinglist.NewClient(fc).Get(context.Background(), "announce-example-com") if err != nil { @@ -173,7 +173,7 @@ func TestMailingListListTabular(t *testing.T) { func TestMailingListSingularTabular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "mailinglist/get_mailinglist_response_success.xml") + resp := testutil.DecodeFixture(t, "mailinglist/get_mailinglists_response_success_single.xml") list, _ := mailinglist.DecodeMailingLists(resp.Body.ReturnInfo) if len(list) != 1 { t.Fatalf("len = %d, want 1", len(list)) diff --git a/internal/sambauser/sambauser_test.go b/internal/sambauser/sambauser_test.go index dab139c..f18b5bf 100644 --- a/internal/sambauser/sambauser_test.go +++ b/internal/sambauser/sambauser_test.go @@ -56,7 +56,7 @@ func TestClientList(t *testing.T) { func TestDecodeSambaUserSingular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "sambauser/get_sambauser_response_success.xml") + resp := testutil.DecodeFixture(t, "sambauser/get_sambausers_response_success_single.xml") got, err := sambauser.DecodeSambaUsers(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeSambaUsers: %v", err) @@ -75,7 +75,7 @@ func TestDecodeSambaUserSingular(t *testing.T) { func TestClientGet(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "sambauser/get_sambauser_response_success.xml") + resp := testutil.DecodeFixture(t, "sambauser/get_sambausers_response_success_single.xml") fc := &testutil.FakeCaller{Resp: resp} u, err := sambauser.NewClient(fc).Get(context.Background(), "s0000000") if err != nil { @@ -123,7 +123,7 @@ func TestClientPropagatesError(t *testing.T) { func TestSambaUserTabular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "sambauser/get_sambauser_response_success.xml") + resp := testutil.DecodeFixture(t, "sambauser/get_sambausers_response_success_single.xml") list, _ := sambauser.DecodeSambaUsers(resp.Body.ReturnInfo) if len(list) != 1 { t.Fatalf("len = %d, want 1", len(list)) diff --git a/internal/server/server.go b/internal/server/server.go index 518248f..220b19d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -32,15 +32,15 @@ type ServiceList []Service // Client groups the read endpoints scoped to the host server. type Client struct { - API Caller + c Caller } // NewClient returns a Client backed by the given Caller. -func NewClient(c Caller) *Client { return &Client{API: c} } +func NewClient(c Caller) *Client { return &Client{c: c} } // Information calls get_server_information and decodes the response. -func (c *Client) Information(ctx context.Context) (ServiceList, error) { - resp, err := c.API.Call(ctx, "get_server_information", nil) +func (cl *Client) Information(ctx context.Context) (ServiceList, error) { + resp, err := cl.c.Call(ctx, "get_server_information", nil) if err != nil { return nil, err } diff --git a/internal/session/store.go b/internal/session/store.go index 2989eb6..7683620 100644 --- a/internal/session/store.go +++ b/internal/session/store.go @@ -155,7 +155,10 @@ func (s *Store) Load(ctx context.Context, login string) (*Entry, error) { if !ok { return nil } - if !e.ExpiresAt.IsZero() && !s.now().Before(e.ExpiresAt) { + // A zero expires_at can only come from a hand-edited file — Save + // and Refresh always fill it. Treating it as never-expiring would + // make the token locally immortal, so it is expired instead. + if e.ExpiresAt.IsZero() || !s.now().Before(e.ExpiresAt) { return s.deleteLocked(login) } out = &e diff --git a/internal/session/store_test.go b/internal/session/store_test.go index 4ec71f1..b750c53 100644 --- a/internal/session/store_test.go +++ b/internal/session/store_test.go @@ -1,6 +1,7 @@ package session_test import ( + "os" "path/filepath" "runtime" "testing" @@ -125,6 +126,29 @@ func TestLoadDropsExpiredEntry(t *testing.T) { } } +// TestLoadDropsZeroExpiryEntry pins that a hand-edited entry without +// expires_at (Save/Refresh always fill it) is treated as expired, not +// as never-expiring — otherwise the token would be locally immortal. +func TestLoadDropsZeroExpiryEntry(t *testing.T) { + now := time.Date(2026, 5, 3, 12, 0, 0, 0, time.UTC) + path := filepath.Join(t.TempDir(), "sessions.toml") + if err := os.WriteFile(path, []byte("[sessions.w0]\ntoken = \"t\"\n"), 0o600); err != nil { + t.Fatalf("write sessions.toml: %v", err) + } + s, err := session.New(path) + if err != nil { + t.Fatalf("New: %v", err) + } + s.Now = func() time.Time { return now } + got, err := s.Load(t.Context(), "w0") + if err != nil { + t.Fatalf("Load: %v", err) + } + if got != nil { + t.Errorf("expected zero-expiry entry to be dropped, got %+v", got) + } +} + func TestDeleteRemovesEntryOnly(t *testing.T) { now := time.Date(2026, 5, 3, 12, 0, 0, 0, time.UTC) s := newStore(t, now) diff --git a/internal/softwareinstall/doc.go b/internal/softwareinstall/doc.go index fa09160..2f45add 100644 --- a/internal/softwareinstall/doc.go +++ b/internal/softwareinstall/doc.go @@ -1,5 +1,6 @@ // Package softwareinstall holds the domain types and use cases for the KAS -// software-install endpoints (get_softwareinstalls, get_softwareinstall, -// add_softwareinstall). The KAS API has no update or delete for this +// software-install endpoints (get_softwareinstall, add_softwareinstall — +// there is no plural get action; the list is the unfiltered +// get_softwareinstall call). The KAS API has no update or delete for this // resource. See issues #11 and #13. package softwareinstall diff --git a/internal/softwareinstall/softwareinstall_test.go b/internal/softwareinstall/softwareinstall_test.go index 0e35e72..c4d488e 100644 --- a/internal/softwareinstall/softwareinstall_test.go +++ b/internal/softwareinstall/softwareinstall_test.go @@ -12,7 +12,7 @@ import ( func TestDecodeSoftwareInstalls(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "softwareinstall/get_softwareinstalls_response_success.xml") + resp := testutil.DecodeFixture(t, "softwareinstall/get_softwareinstall_response_success_all.xml") got, err := softwareinstall.DecodeSoftwareInstalls(resp.Body.ReturnInfo) if err != nil { t.Fatalf("DecodeSoftwareInstalls: %v", err) @@ -75,7 +75,7 @@ func TestDecodeSoftwareInstallSingular(t *testing.T) { func TestClientList(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "softwareinstall/get_softwareinstalls_response_success.xml") + resp := testutil.DecodeFixture(t, "softwareinstall/get_softwareinstall_response_success_all.xml") fc := &testutil.FakeCaller{Resp: resp} list, err := softwareinstall.NewClient(fc).List(context.Background()) if err != nil { @@ -142,7 +142,7 @@ func TestClientPropagatesError(t *testing.T) { func TestSoftwareInstallListTabular(t *testing.T) { t.Parallel() - resp := testutil.DecodeFixture(t, "softwareinstall/get_softwareinstalls_response_success.xml") + resp := testutil.DecodeFixture(t, "softwareinstall/get_softwareinstall_response_success_all.xml") list, _ := softwareinstall.DecodeSoftwareInstalls(resp.Body.ReturnInfo) headers := list.TableHeaders() if headers[0] != "ID" { diff --git a/internal/ssl/fault_test.go b/internal/ssl/fault_test.go new file mode 100644 index 0000000..68cc952 --- /dev/null +++ b/internal/ssl/fault_test.go @@ -0,0 +1,18 @@ +package ssl_test + +import ( + "testing" + + "github.com/chmmou/kasapi-cli/internal/testutil" +) + +// The ssl write slice is not implemented yet (#125), but its captured +// fault fixtures already live in testdata/ssl/. Anchoring them via the +// shared testutil.AssertFaultFixtures keeps them bound to the KAS +// contract instead of rotting unreferenced until the slice lands. +func TestFaultFixturesDecodeToDocumentedCodes(t *testing.T) { + t.Parallel() + testutil.AssertFaultFixtures(t, "ssl", map[string]string{ + "update_ssl_response_failed_nothing_to_do.xml": "nothing_to_do", + }) +} diff --git a/internal/symlink/fault_test.go b/internal/symlink/fault_test.go new file mode 100644 index 0000000..4a87e54 --- /dev/null +++ b/internal/symlink/fault_test.go @@ -0,0 +1,19 @@ +package symlink_test + +import ( + "testing" + + "github.com/chmmou/kasapi-cli/internal/testutil" +) + +// The symlink write slice is not implemented yet (#125), but its +// captured fault fixtures already live in testdata/symlink/. Anchoring +// them via the shared testutil.AssertFaultFixtures keeps them bound to +// the KAS contract instead of rotting unreferenced until the slice +// lands. +func TestFaultFixturesDecodeToDocumentedCodes(t *testing.T) { + t.Parallel() + testutil.AssertFaultFixtures(t, "symlink", map[string]string{ + "add_symlink_response_failed_in_progress.xml": "in_progress", + }) +} diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index e01add1..1c079c1 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -64,12 +64,14 @@ func DecodeFixture(t *testing.T, relPath string) *soap.Response { // KAS contract: every testdata//*_response_failed_*.xml must // decode to a *soap.FaultError with a non-empty Fault.String, and each // entry in want (fixture filename -> expected fault code) must match -// exactly. It is the fixture<->contract anchor every module reuses so a -// captured fault fixture cannot silently drift from its documented KAS -// code. module is the testdata/ subdirectory (e.g. "ftpuser"); the -// empty string scans the shared top-level response_failed_*.xml set. -// want may be nil to assert only the universal invariant; pin a few -// representative documented codes there to also catch a code drift. +// exactly. A want key that matches no file on disk fails too, so a +// renamed or deleted fixture cannot leave a dead pin behind. It is the +// fixture<->contract anchor every module reuses so a captured fault +// fixture cannot silently drift from its documented KAS code. module +// is the testdata/ subdirectory (e.g. "ftpuser"); the empty string +// scans the shared top-level response_failed_*.xml set. want may be +// nil to assert only the universal invariant; pin a few representative +// documented codes there to also catch a code drift. func AssertFaultFixtures(t *testing.T, module string, want map[string]string) { t.Helper() dir := filepath.Join(RepoRoot(t), "testdata", module) @@ -78,6 +80,7 @@ func AssertFaultFixtures(t *testing.T, module string, want map[string]string) { t.Fatalf("read fixture dir %s: %v", dir, err) } seen := 0 + matched := map[string]bool{} for _, e := range entries { if e.IsDir() { continue @@ -90,6 +93,9 @@ func AssertFaultFixtures(t *testing.T, module string, want map[string]string) { continue } seen++ + if _, ok := want[name]; ok { + matched[name] = true + } //nolint:gosec // G304: fixture path is rooted at the repo testdata/ dir. f, oerr := os.Open(filepath.Join(dir, name)) if oerr != nil { @@ -112,6 +118,11 @@ func AssertFaultFixtures(t *testing.T, module string, want map[string]string) { if seen == 0 { t.Fatalf("no fault fixtures found for module %q", module) } + for name := range want { + if !matched[name] { + t.Errorf("want entry %s matches no fault fixture on disk (renamed or deleted?)", name) + } + } } // FakeCaller is a minimal stub for the Caller interface implemented by diff --git a/internal/transport/client.go b/internal/transport/client.go index a479d0f..4c94c9c 100644 --- a/internal/transport/client.go +++ b/internal/transport/client.go @@ -167,6 +167,15 @@ func (c *Client) doOnce(ctx context.Context, endpoint string, body []byte) ([]by resp, err := c.HTTPClient.Do(req) if err != nil { + // The caller's cancelled/expired ctx is a decision to stop, not + // a transient server condition — retrying would only burn a + // backoff sleep before failing on the same ctx again. The check + // is on ctx.Err(), not errors.Is(err, context.DeadlineExceeded): + // a per-attempt HTTPClient.Timeout error also matches the + // latter, and that one IS a transient condition worth retrying. + if ctx.Err() != nil { + return nil, fmt.Errorf("transport: post %s: %w", endpoint, err) + } return nil, &retryableError{err: fmt.Errorf("transport: post %s: %w", endpoint, err)} } defer func() { _ = resp.Body.Close() }() @@ -176,6 +185,9 @@ func (c *Client) doOnce(ctx context.Context, endpoint string, body []byte) ([]by // so the memory-exhaustion guard must be enforced at this read. respBody, err := io.ReadAll(io.LimitReader(resp.Body, soap.MaxResponseBytes+1)) if err != nil { + if ctx.Err() != nil { + return nil, fmt.Errorf("transport: read body: %w", err) + } return nil, &retryableError{err: fmt.Errorf("transport: read body: %w", err)} } if len(respBody) > soap.MaxResponseBytes { @@ -183,6 +195,20 @@ func (c *Client) doOnce(ctx context.Context, endpoint string, body []byte) ([]by } if resp.StatusCode >= 500 { + // A PHP SOAP server may deliver a SOAP fault with HTTP 500. Such + // a body must reach the decoder so the typed-fault path (auth + // refresh, flood fallback, exit-code classification) applies + // instead of three blind retries that discard the fault. The + // sniff matches the Fault element with any namespace prefix + // (SOAP-ENV:Fault, soap:Fault) and the prefix-less + // default-namespace form. It is a byte-level heuristic: a + // non-SOAP 5xx body that happens to contain a marker is handed + // to the decoder and fails there as a non-retryable decode error + // — accepted, because real gateway/proxy error pages carry + // neither marker and so stay retryable. + if bytes.Contains(respBody, []byte(":Fault")) || bytes.Contains(respBody, []byte("= 400 { diff --git a/internal/transport/client_test.go b/internal/transport/client_test.go index af78b6c..1320ad6 100644 --- a/internal/transport/client_test.go +++ b/internal/transport/client_test.go @@ -169,6 +169,56 @@ func TestDoRetriesOn5xx(t *testing.T) { } } +func TestDoPassesThroughSoapFaultOn5xx(t *testing.T) { + const faultBody = `` + + `SOAP-ENV:Server` + + `flood_protection` + var calls atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.WriteHeader(http.StatusInternalServerError) + _, _ = io.WriteString(w, faultBody) + })) + defer srv.Close() + + c := newClient(srv, newFakeClock()) + body, err := c.Do(context.Background(), srv.URL, []byte(sampleEnvelope)) + if err != nil { + t.Fatalf("Do: %v", err) + } + if string(body) != faultBody { + t.Errorf("body = %q, want the fault body passed through", body) + } + if calls.Load() != 1 { + t.Errorf("calls = %d, want 1 (fault body must not be retried)", calls.Load()) + } +} + +func TestDoPassesThroughDefaultNamespaceFaultOn5xx(t *testing.T) { + const faultBody = `` + + `Server` + + `flood_protection` + var calls atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + calls.Add(1) + w.WriteHeader(http.StatusInternalServerError) + _, _ = io.WriteString(w, faultBody) + })) + defer srv.Close() + + c := newClient(srv, newFakeClock()) + body, err := c.Do(context.Background(), srv.URL, []byte(sampleEnvelope)) + if err != nil { + t.Fatalf("Do: %v", err) + } + if string(body) != faultBody { + t.Errorf("body = %q, want the prefix-less fault body passed through", body) + } + if calls.Load() != 1 { + t.Errorf("calls = %d, want 1 (fault body must not be retried)", calls.Load()) + } +} + func TestDoStopsRetryAfterMax(t *testing.T) { var calls atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -374,19 +424,28 @@ func TestRecordDelayExtendsWhenNewDelayIsLonger(t *testing.T) { } func TestDoRespectsContextDeadlineDuringRequest(t *testing.T) { + var hits atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + hits.Add(1) <-r.Context().Done() })) defer srv.Close() c := newClient(srv, newFakeClock()) - c.MaxRetries = 0 ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() _, err := c.Do(ctx, srv.URL, nil) if err == nil { t.Fatal("expected error on cancelled context") } + if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("err = %v, want context.DeadlineExceeded preserved", err) + } + // The caller's cancellation is not a transient server condition: + // even with MaxRetries at its default, exactly one attempt runs. + if hits.Load() != 1 { + t.Errorf("requests = %d, want 1 (no retry on context cancellation)", hits.Load()) + } } // The 16-MB soap.MaxResponseBytes cap must bite at the transport read: @@ -418,3 +477,34 @@ func TestDoRejectsOversizedResponse(t *testing.T) { t.Errorf("requests = %d, want 1 (oversize must not be retried)", hits.Load()) } } + +// A per-attempt HTTPClient.Timeout error matches +// errors.Is(err, context.DeadlineExceeded) even though the caller's ctx +// is still alive; it is a transient slow-server condition and must stay +// retryable — only the caller's own cancellation suppresses the retry. +func TestDoRetriesOnClientTimeout(t *testing.T) { + var hits atomic.Int32 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if hits.Add(1) == 1 { + // Outlast the 100ms client timeout, then return so + // srv.Close does not wait on a stuck handler. + time.Sleep(500 * time.Millisecond) + return + } + _, _ = io.WriteString(w, "") + })) + defer srv.Close() + + c := newClient(srv, newFakeClock()) + c.HTTPClient.Timeout = 100 * time.Millisecond + resp, err := c.Do(context.Background(), srv.URL, []byte(sampleEnvelope)) + if err != nil { + t.Fatalf("Do: %v", err) + } + if string(resp) != "" { + t.Errorf("body = %q, want ", resp) + } + if hits.Load() != 2 { + t.Errorf("requests = %d, want 2 (client-timeout attempt retried once)", hits.Load()) + } +} diff --git a/internal/usage/usage.go b/internal/usage/usage.go index 98057d9..c4af89e 100644 --- a/internal/usage/usage.go +++ b/internal/usage/usage.go @@ -83,19 +83,19 @@ type TrafficList []Traffic // Client groups the read endpoints scoped to webspace + traffic // counters: get_space, get_space_usage, get_traffic. type Client struct { - API Caller + c Caller } // NewClient returns a Client backed by the given Caller. -func NewClient(c Caller) *Client { return &Client{API: c} } +func NewClient(c Caller) *Client { return &Client{c: c} } // Space calls get_space and decodes the response into SpaceList. // // The KAS API accepts optional show_subaccounts and show_details // parameters; both default to "Y" server-side. We omit them — callers // that need a different scope can call (*api.Client).Call directly. -func (c *Client) Space(ctx context.Context) (SpaceList, error) { - resp, err := c.API.Call(ctx, "get_space", nil) +func (cl *Client) Space(ctx context.Context) (SpaceList, error) { + resp, err := cl.c.Call(ctx, "get_space", nil) if err != nil { return nil, err } @@ -108,12 +108,12 @@ func (c *Client) Space(ctx context.Context) (SpaceList, error) { // SpaceUsage calls get_space_usage for the given directory and decodes // the response. An empty directory queries the document-root level. -func (c *Client) SpaceUsage(ctx context.Context, directory string) (SpaceUsageList, error) { +func (cl *Client) SpaceUsage(ctx context.Context, directory string) (SpaceUsageList, error) { var params map[string]any if directory != "" { params = map[string]any{"directory": directory} } - resp, err := c.API.Call(ctx, "get_space_usage", params) + resp, err := cl.c.Call(ctx, "get_space_usage", params) if err != nil { return nil, err } @@ -128,7 +128,7 @@ func (c *Client) SpaceUsage(ctx context.Context, directory string) (SpaceUsageLi // are optional; pass 0/0 to query the current month. month is encoded // as a zero-padded string ("01".."12") because KAS rejects "1" with a // syntax error. -func (c *Client) Traffic(ctx context.Context, year, month int) (TrafficList, error) { +func (cl *Client) Traffic(ctx context.Context, year, month int) (TrafficList, error) { var params map[string]any if year != 0 || month != 0 { params = make(map[string]any, 2) @@ -139,7 +139,7 @@ func (c *Client) Traffic(ctx context.Context, year, month int) (TrafficList, err params["month"] = fmt.Sprintf("%02d", month) } } - resp, err := c.API.Call(ctx, "get_traffic", params) + resp, err := cl.c.Call(ctx, "get_traffic", params) if err != nil { return nil, err } diff --git a/testdata/cronjob/add_cronjob_response_warning.xml b/testdata/cronjob/add_cronjob_response_success_warning.xml similarity index 100% rename from testdata/cronjob/add_cronjob_response_warning.xml rename to testdata/cronjob/add_cronjob_response_success_warning.xml diff --git a/testdata/cronjob/get_cronjob_request.xml b/testdata/cronjob/get_cronjobs_request_cronjob_id.xml similarity index 100% rename from testdata/cronjob/get_cronjob_request.xml rename to testdata/cronjob/get_cronjobs_request_cronjob_id.xml diff --git a/testdata/cronjob/get_cronjob_response_success.xml b/testdata/cronjob/get_cronjobs_response_success_single.xml similarity index 100% rename from testdata/cronjob/get_cronjob_response_success.xml rename to testdata/cronjob/get_cronjobs_response_success_single.xml diff --git a/testdata/database/get_database_request.xml b/testdata/database/get_databases_request_single.xml similarity index 100% rename from testdata/database/get_database_request.xml rename to testdata/database/get_databases_request_single.xml diff --git a/testdata/database/get_database_response_success.xml b/testdata/database/get_databases_response_success_single.xml similarity index 100% rename from testdata/database/get_database_response_success.xml rename to testdata/database/get_databases_response_success_single.xml diff --git a/testdata/ddns/get_ddnsuser_response_success.xml b/testdata/ddns/get_ddnsusers_response_success_single.xml similarity index 100% rename from testdata/ddns/get_ddnsuser_response_success.xml rename to testdata/ddns/get_ddnsusers_response_success_single.xml diff --git a/testdata/directoryprotection/get_directoryprotections_request.xml b/testdata/directoryprotection/get_directoryprotection_request_all.xml similarity index 100% rename from testdata/directoryprotection/get_directoryprotections_request.xml rename to testdata/directoryprotection/get_directoryprotection_request_all.xml diff --git a/testdata/directoryprotection/get_directoryprotections_response_success.xml b/testdata/directoryprotection/get_directoryprotection_response_success_all.xml similarity index 100% rename from testdata/directoryprotection/get_directoryprotections_response_success.xml rename to testdata/directoryprotection/get_directoryprotection_response_success_all.xml diff --git a/testdata/dns/get_dns_settings_zone_host_and_record_id_request.xml b/testdata/dns/get_dns_settings_request_zone_host_and_record_id.xml similarity index 100% rename from testdata/dns/get_dns_settings_zone_host_and_record_id_request.xml rename to testdata/dns/get_dns_settings_request_zone_host_and_record_id.xml diff --git a/testdata/dns/get_dns_settings_zone_host_and_record_id_response_success.xml b/testdata/dns/get_dns_settings_response_success_zone_host_and_record_id.xml similarity index 100% rename from testdata/dns/get_dns_settings_zone_host_and_record_id_response_success.xml rename to testdata/dns/get_dns_settings_response_success_zone_host_and_record_id.xml diff --git a/testdata/domain/get_domain_request.xml b/testdata/domain/get_domains_request_domain_name.xml similarity index 100% rename from testdata/domain/get_domain_request.xml rename to testdata/domain/get_domains_request_domain_name.xml diff --git a/testdata/domain/get_domain_response_success.xml b/testdata/domain/get_domains_response_success_single.xml similarity index 100% rename from testdata/domain/get_domain_response_success.xml rename to testdata/domain/get_domains_response_success_single.xml diff --git a/testdata/domain/get_topleveldomains_request.xml b/testdata/domain/get_topleveldomains_request.xml index c6cfda6..ee944fc 100644 --- a/testdata/domain/get_topleveldomains_request.xml +++ b/testdata/domain/get_topleveldomains_request.xml @@ -5,7 +5,7 @@ { "KasRequestParams": {}, - "kas_action": "get_subdomains", + "kas_action": "get_topleveldomains", "kas_auth_data": "REDACTED", "kas_auth_type": "session", "kas_login": "w0000000" diff --git a/testdata/domain/update_domain_request.xml b/testdata/domain/update_domain_request.xml index 8cf6d37..ad049cc 100644 --- a/testdata/domain/update_domain_request.xml +++ b/testdata/domain/update_domain_request.xml @@ -6,7 +6,7 @@ { "KasRequestParams": { "domain_name": "example.com", - "domain_path": "/example-new.com/", + "domain_path": "/example.net/", "redirect_status": "0", "php_version": "8.4", "is_active": "Y" diff --git a/testdata/domain/update_domain_response_success.xml b/testdata/domain/update_domain_response_success.xml index d464970..6fcbd74 100644 --- a/testdata/domain/update_domain_response_success.xml +++ b/testdata/domain/update_domain_response_success.xml @@ -23,7 +23,7 @@ domain_path - /example-new.com/ + /example.net/ redirect_status diff --git a/testdata/ftpuser/get_ftpuser_request.xml b/testdata/ftpuser/get_ftpusers_request_ftp_login.xml similarity index 100% rename from testdata/ftpuser/get_ftpuser_request.xml rename to testdata/ftpuser/get_ftpusers_request_ftp_login.xml diff --git a/testdata/ftpuser/get_ftpuser_response_success_empty_list.xml b/testdata/ftpuser/get_ftpusers_response_success_empty_list.xml similarity index 100% rename from testdata/ftpuser/get_ftpuser_response_success_empty_list.xml rename to testdata/ftpuser/get_ftpusers_response_success_empty_list.xml diff --git a/testdata/ftpuser/get_ftpuser_response_success.xml b/testdata/ftpuser/get_ftpusers_response_success_single.xml similarity index 100% rename from testdata/ftpuser/get_ftpuser_response_success.xml rename to testdata/ftpuser/get_ftpusers_response_success_single.xml diff --git a/testdata/mailaccount/get_mailaccount_request.xml b/testdata/mailaccount/get_mailaccounts_request_mail_login.xml similarity index 100% rename from testdata/mailaccount/get_mailaccount_request.xml rename to testdata/mailaccount/get_mailaccounts_request_mail_login.xml diff --git a/testdata/mailaccount/get_mailaccount_response_success.xml b/testdata/mailaccount/get_mailaccounts_response_success_single.xml similarity index 100% rename from testdata/mailaccount/get_mailaccount_response_success.xml rename to testdata/mailaccount/get_mailaccounts_response_success_single.xml diff --git a/testdata/mailforward/get_mailforward_request.xml b/testdata/mailforward/get_mailforwards_request_mail_forward.xml similarity index 100% rename from testdata/mailforward/get_mailforward_request.xml rename to testdata/mailforward/get_mailforwards_request_mail_forward.xml diff --git a/testdata/mailforward/get_mailforward_response_success.xml b/testdata/mailforward/get_mailforwards_response_success_single.xml similarity index 100% rename from testdata/mailforward/get_mailforward_response_success.xml rename to testdata/mailforward/get_mailforwards_response_success_single.xml diff --git a/testdata/mailinglist/add_mailinglist_response_failed_no_active_listen_owner_found.xml b/testdata/mailinglist/add_mailinglist_response_failed_no_active_listen_owner_found.xml index 5125efb..2224685 100644 --- a/testdata/mailinglist/add_mailinglist_response_failed_no_active_listen_owner_found.xml +++ b/testdata/mailinglist/add_mailinglist_response_failed_no_active_listen_owner_found.xml @@ -5,7 +5,7 @@ SOAP-ENV:Server no_active_listen_owner_found KasApi - info@example1.org + info@example.org \ No newline at end of file diff --git a/testdata/mailinglist/get_mailinglist_request.xml b/testdata/mailinglist/get_mailinglists_request_mailinglist_name.xml similarity index 100% rename from testdata/mailinglist/get_mailinglist_request.xml rename to testdata/mailinglist/get_mailinglists_request_mailinglist_name.xml diff --git a/testdata/mailinglist/get_mailinglist_response_success.xml b/testdata/mailinglist/get_mailinglists_response_success_single.xml similarity index 100% rename from testdata/mailinglist/get_mailinglist_response_success.xml rename to testdata/mailinglist/get_mailinglists_response_success_single.xml diff --git a/testdata/sambauser/get_sambauser_response_success.xml b/testdata/sambauser/get_sambausers_response_success_single.xml similarity index 100% rename from testdata/sambauser/get_sambauser_response_success.xml rename to testdata/sambauser/get_sambausers_response_success_single.xml diff --git a/testdata/softwareinstall/get_softwareinstalls_request.xml b/testdata/softwareinstall/get_softwareinstall_request_all.xml similarity index 100% rename from testdata/softwareinstall/get_softwareinstalls_request.xml rename to testdata/softwareinstall/get_softwareinstall_request_all.xml diff --git a/testdata/softwareinstall/get_softwareinstalls_response_success.xml b/testdata/softwareinstall/get_softwareinstall_response_success_all.xml similarity index 100% rename from testdata/softwareinstall/get_softwareinstalls_response_success.xml rename to testdata/softwareinstall/get_softwareinstall_response_success_all.xml