Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Whole-codebase re-review follow-up (fourth pass, Med + Low findings):
- The `mail lists update` `--config-file` / `--subscriber` blobs are
now elided from the audit record **by key** (`config` /
`subscriber`), independent of the multi-line/oversized shape
heuristic: a single-line list config short enough to pass that
heuristic could previously reach both audit sinks verbatim,
including an embedded cleartext list password —
`docs/usage/destructive-writes.md` already promised these blobs
never do.
- `kasapi-cli help <nonsense>` now exits 1 like every other unknown
command: cobra's stock help command returns nil for an
unresolvable topic, so a typo'd topic read as success to scripts.
`help`, `help <group>`, and `help <group> <subcommand>` keep
printing help with exit 0.
- The last `get_<singular>` fixture missed by the third-pass rename
sweep, `testdata/account/get_account_response_success.xml`
(embedding the real plural action `get_accounts`), was renamed to
`get_accounts_response_success_single.xml`.
- `config show` / `config path` / `config list-profiles` and
`sessions delete` now document in their `--help` that their output
is plain text and the global `--output` format flag has no effect
there, instead of silently ignoring it.
- The `mail accounts update` and `ddnsusers update` `--help` texts
now describe every field flag as a replacement value (the
convention the cronjob/ftpuser/sambauser/database update commands
already follow), instead of reusing `add`'s wording on all but the
password flag.
- The white-box `runWriteE` created_id test no longer couples
to host state — it pins `KAS_AUDIT_LOG` and points `--config` at a
nonexistent temp path so it cannot read the developer's real
config or append to a real audit log.

- Whole-codebase re-review follow-up (third pass, High + Med + Low
findings):
- Group commands (`mail`, `accounts`, `config`, ...) invoked with an
Expand Down
6 changes: 6 additions & 0 deletions docs/cli/kasapi-cli_config_list-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

List configured profiles and their auth_type (auth_data redacted)

### Synopsis

List configured profiles alphabetically, one per line, with the default profile marked "*" and auth_data never shown.

The output is plain text; the global --output format flag has no effect here.

```
kasapi-cli config list-profiles [flags]
```
Expand Down
6 changes: 6 additions & 0 deletions docs/cli/kasapi-cli_config_path.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Print the resolved config-file path

### Synopsis

Print the resolved config-file path.

The output is a single plain-text line; the global --output format flag has no effect here.

```
kasapi-cli config path [flags]
```
Expand Down
6 changes: 6 additions & 0 deletions docs/cli/kasapi-cli_config_show.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

Print the resolved effective config (auth_data redacted)

### Synopsis

Print the resolved effective config: config path, profiles, and the config+env+flag credential resolution result, with auth_data redacted.

The output is plain key: value text; the global --output format flag has no effect here.

```
kasapi-cli config show [flags]
```
Expand Down
6 changes: 3 additions & 3 deletions docs/cli/kasapi-cli_ddnsusers_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ kasapi-cli ddnsusers update <dyndns-login> [password/target/dual-stack flags] [f

```
--comment string replacement user comment / label
--dual-stack enable dual-stack (Y) for this user; pass --dual-stack=false to disable
--dual-stack replacement dual-stack setting: Y when set, --dual-stack=false sends N
-h, --help help for update
--password string replacement DDNS password
--target-ipv4 string IPv4 target (undocumented but verified)
--target-ipv6 string IPv6 target (undocumented but verified)
--target-ipv4 string replacement IPv4 target (undocumented but verified)
--target-ipv6 string replacement IPv6 target (undocumented but verified)
```

### Options inherited from parent commands
Expand Down
30 changes: 15 additions & 15 deletions docs/cli/kasapi-cli_mail_accounts_update.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ kasapi-cli mail accounts update <mail-login> [field flags] [flags]
### Options

```
--active string mailbox status (Y|N)
--allow-nets string restrict access to these IP/CIDR networks
--copy-address string BCC copy recipient address(es)
--active string replacement mailbox status (Y|N)
--allow-nets string replacement IP/CIDR network access restriction (an explicitly empty value clears it)
--copy-address string replacement BCC copy recipient address(es)
-h, --help help for update
--password string replacement mailbox password (sent as mail_new_password)
--responder string auto-responder: "N", "Y", or a "<start>|<end>" timestamp range
--responder-content-type string auto-responder body format (html|text)
--responder-displayname string auto-responder sender display name
--responder-text string auto-responder message body
--sender-alias string permitted FROM alias address(es)
--webmail-autologin string allow KAS-to-webmail auto-login (Y|N)
--xlist-archiv string XLIST archive folder name
--xlist-drafts string XLIST drafts folder name
--xlist-enabled string enable XLIST special-folder mapping (Y|N)
--xlist-sent string XLIST sent-items folder name
--xlist-spam string XLIST spam folder name
--xlist-trash string XLIST trash folder name
--responder string replacement auto-responder setting: "N", "Y", or a "<start>|<end>" timestamp range
--responder-content-type string replacement auto-responder body format (html|text)
--responder-displayname string replacement auto-responder sender display name
--responder-text string replacement auto-responder message body
--sender-alias string replacement permitted FROM alias address(es)
--webmail-autologin string replacement KAS-to-webmail auto-login setting (Y|N)
--xlist-archiv string replacement XLIST archive folder name
--xlist-drafts string replacement XLIST drafts folder name
--xlist-enabled string replacement XLIST special-folder mapping setting (Y|N)
--xlist-sent string replacement XLIST sent-items folder name
--xlist-spam string replacement XLIST spam folder name
--xlist-trash string replacement XLIST trash folder name
```

### Options inherited from parent commands
Expand Down
2 changes: 2 additions & 0 deletions docs/cli/kasapi-cli_sessions_delete.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Invalidate the resolved profile's cached session token, both server-side (kas_ac

Acts on the *currently cached* token only; it never bootstraps a fresh token just to delete it. Idempotent: a missing or already-invalid session is reported and exits 0. No confirmation prompt — deleting a session merely forces a re-authentication on the next session-mode call.

The output is plain text; the global --output format flag has no effect here.

```
kasapi-cli sessions delete [flags]
```
Expand Down
9 changes: 5 additions & 4 deletions docs/usage/destructive-writes.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,11 @@ additionally appends the same record as one JSON object per line

Secret request parameters (`auth_data`, `*password`, `*token`,
`*secret`, …) are replaced with `<redacted>` in **both** sinks and never
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 `<elided N bytes>`. Read commands produce no
audit record.
written. The `mail lists update` `--config-file` / `--subscriber` blobs
(which can contain the list password) are always elided to
`<elided N bytes>`, keyed on the parameter name and independent of
their size; any other multi-line or oversized value is elided the same
way. Read commands produce no audit record.

## `--dry-run`: preview without dispatching

Expand Down
6 changes: 3 additions & 3 deletions internal/account/account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func TestClientList(t *testing.T) {

func TestClientGet(t *testing.T) {
t.Parallel()
resp := testutil.DecodeFixture(t, "account/get_account_response_success.xml")
resp := testutil.DecodeFixture(t, "account/get_accounts_response_success_single.xml")
fc := &testutil.FakeCaller{Resp: resp}
got, err := account.NewClient(fc).Get(context.Background(), "w0000001")
if err != nil {
Expand Down Expand Up @@ -190,7 +190,7 @@ func TestClientGetNotFound(t *testing.T) {
t.Parallel()
// Synthesise an empty array response by reusing the singular
// fixture but with the array stripped to zero entries.
emptyResp := testutil.DecodeFixture(t, "account/get_account_response_success.xml")
emptyResp := testutil.DecodeFixture(t, "account/get_accounts_response_success_single.xml")
emptyResp.Body.ReturnInfo.Array = nil
c := account.NewClient(&testutil.FakeCaller{Resp: emptyResp})
if _, err := c.Get(context.Background(), "wXXXXXXX"); err == nil {
Expand Down Expand Up @@ -230,7 +230,7 @@ func TestAccountListTabular(t *testing.T) {

func TestAccountTabular(t *testing.T) {
t.Parallel()
resp := testutil.DecodeFixture(t, "account/get_account_response_success.xml")
resp := testutil.DecodeFixture(t, "account/get_accounts_response_success_single.xml")
accs, _ := account.DecodeAccounts(resp.Body.ReturnInfo)
if len(accs) != 1 {
t.Fatalf("len = %d, want 1", len(accs))
Expand Down
26 changes: 19 additions & 7 deletions internal/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ var auditSecretParams = map[string]struct{}{

const auditRedacted = "<redacted>"

// auditBlobParams are wholesale-replacement blob keys
// (update_mailinglist's config / subscriber) whose content is always
// elided by key, independent of the size/shape heuristic below: the
// list config can embed the list password in cleartext, and a
// single-line config under maxAuditValueLen would otherwise reach both
// audit sinks verbatim.
var auditBlobParams = map[string]struct{}{
"config": {},
"subscriber": {},
}

// maxAuditValueLen caps how long a single parameter value may be before
// RedactParams elides it. Normal write parameters (names, hosts, Y/N
// toggles) are far shorter; only wholesale blobs like the mailing-list
Expand Down Expand Up @@ -106,12 +117,12 @@ func redactParam(key string) bool {

// RedactParams converts a KAS request/response parameter map into the
// string map stored on AuditRecord.Fields, replacing every secret value
// (see redactParam) with auditRedacted. Multi-line or oversized values
// (mailing-list config / subscriber blobs sent wholesale by
// update_mailinglist) are elided to a "<elided N bytes>" marker: the
// list config can carry the list password in cleartext, so the blob
// content must never reach either audit sink. Non-string values are
// rendered with %v. A nil/empty map yields nil so the field is omitted.
// (see redactParam) with auditRedacted. Wholesale blob keys
// (auditBlobParams) and any multi-line or oversized value are elided to
// a "<elided N bytes>" marker: the mailing-list config can carry the
// list password in cleartext, so the blob content must never reach
// either audit sink. Non-string values are rendered with %v. A
// nil/empty map yields nil so the field is omitted.
func RedactParams(params map[string]any) map[string]string {
if len(params) == 0 {
return nil
Expand All @@ -123,7 +134,8 @@ func RedactParams(params map[string]any) map[string]string {
continue
}
s := fmt.Sprintf("%v", v)
if strings.ContainsAny(s, "\n\r") || len(s) > maxAuditValueLen {
_, blob := auditBlobParams[strings.ToLower(k)]
if blob || strings.ContainsAny(s, "\n\r") || len(s) > maxAuditValueLen {
s = fmt.Sprintf("<elided %d bytes>", len(s))
}
out[k] = s
Expand Down
28 changes: 17 additions & 11 deletions internal/cli/audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,23 +54,29 @@ func TestRedactParams(t *testing.T) {
}
}

// Multi-line or oversized parameter values (the wholesale mailing-list
// config / subscriber blobs of update_mailinglist) must never reach the
// audit sinks verbatim: the list config can carry the list password in
// cleartext.
// The wholesale mailing-list config / subscriber blobs of
// update_mailinglist must never reach the audit sinks verbatim: the
// list config can carry the list password in cleartext. They are
// elided by key — a single-line config short enough to pass the
// size/shape heuristic must still be elided — and any other multi-line
// or oversized value is elided by shape.
func TestRedactParamsElidesBlobs(t *testing.T) {
t.Parallel()
got := cli.RedactParams(map[string]any{
"config": "line1\npassword secret123\n",
"subscriber": "a@x.de\rb@x.de",
"long": strings.Repeat("x", 300),
"config": "keep_password secret123", // single-line, short: elided by key
"subscriber": "a@x.de", // single-line, short: elided by key
"notes": "line1\nline2", // elided by shape (multi-line)
"long": strings.Repeat("x", 300), // elided by shape (oversized)
"comment": "short stays",
})
if got["config"] != "<elided 25 bytes>" {
t.Errorf("config = %q, want <elided 25 bytes>", got["config"])
if got["config"] != "<elided 23 bytes>" {
t.Errorf("config = %q, want <elided 23 bytes>", got["config"])
}
if got["subscriber"] != "<elided 13 bytes>" {
t.Errorf("subscriber = %q, want <elided 13 bytes>", got["subscriber"])
if got["subscriber"] != "<elided 6 bytes>" {
t.Errorf("subscriber = %q, want <elided 6 bytes>", got["subscriber"])
}
if got["notes"] != "<elided 11 bytes>" {
t.Errorf("notes = %q, want <elided 11 bytes>", got["notes"])
}
if got["long"] != "<elided 300 bytes>" {
t.Errorf("long = %q, want <elided 300 bytes>", got["long"])
Expand Down
18 changes: 15 additions & 3 deletions internal/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ func newConfigPathCmd(opts *RootOptions) *cobra.Command {
return &cobra.Command{
Use: "path",
Short: "Print the resolved config-file path",
Args: cobra.NoArgs,
Long: "Print the resolved config-file path.\n\n" +
"The output is a single plain-text line; the global --output " +
"format flag has no effect here.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
path, err := resolveConfigPath(opts.ConfigPath)
if err != nil {
Expand All @@ -66,7 +69,12 @@ func newConfigShowCmd(opts *RootOptions) *cobra.Command {
return &cobra.Command{
Use: "show",
Short: "Print the resolved effective config (auth_data redacted)",
Args: cobra.NoArgs,
Long: "Print the resolved effective config: config path, profiles, " +
"and the config+env+flag credential resolution result, with " +
"auth_data redacted.\n\n" +
"The output is plain key: value text; the global --output " +
"format flag has no effect here.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return runConfigShow(opts, cmd.OutOrStdout())
},
Expand Down Expand Up @@ -508,7 +516,11 @@ func newConfigListProfilesCmd(opts *RootOptions) *cobra.Command {
return &cobra.Command{
Use: "list-profiles",
Short: "List configured profiles and their auth_type (auth_data redacted)",
Args: cobra.NoArgs,
Long: "List configured profiles alphabetically, one per line, with " +
"the default profile marked \"*\" and auth_data never shown.\n\n" +
"The output is plain text; the global --output format flag has " +
"no effect here.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return runConfigListProfiles(opts.ConfigPath, cmd.OutOrStdout())
},
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/ddnsusers.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ func (f *ddnsuserUpdateFlags) bind(cmd *cobra.Command) {
fl := cmd.Flags()
fl.StringVar(&f.password, "password", "", "replacement DDNS password")
fl.StringVar(&f.comment, "comment", "", "replacement user comment / label")
fl.StringVar(&f.targetIPv4, "target-ipv4", "", "IPv4 target (undocumented but verified)")
fl.StringVar(&f.targetIPv6, "target-ipv6", "", "IPv6 target (undocumented but verified)")
fl.BoolVar(&f.dualStack, "dual-stack", false, "enable dual-stack (Y) for this user; pass --dual-stack=false to disable")
fl.StringVar(&f.targetIPv4, "target-ipv4", "", "replacement IPv4 target (undocumented but verified)")
fl.StringVar(&f.targetIPv6, "target-ipv6", "", "replacement IPv6 target (undocumented but verified)")
fl.BoolVar(&f.dualStack, "dual-stack", false, "replacement dual-stack setting: Y when set, --dual-stack=false sends N")
}

// ddnsuserUpdateChangedFields collects only the flags the user
Expand Down
30 changes: 15 additions & 15 deletions internal/cli/mail.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,21 +465,21 @@ type mailAccountUpdateFlags struct {
func (f *mailAccountUpdateFlags) bind(cmd *cobra.Command) {
fl := cmd.Flags()
fl.StringVar(&f.password, "password", "", "replacement mailbox password (sent as mail_new_password)")
fl.StringVar(&f.active, "active", "", "mailbox status (Y|N)")
fl.StringVar(&f.webmailAutologin, "webmail-autologin", "", "allow KAS-to-webmail auto-login (Y|N)")
fl.StringVar(&f.responder, "responder", "", `auto-responder: "N", "Y", or a "<start>|<end>" timestamp range`)
fl.StringVar(&f.responderContentType, "responder-content-type", "", "auto-responder body format (html|text)")
fl.StringVar(&f.responderDisplayName, "responder-displayname", "", "auto-responder sender display name")
fl.StringVar(&f.responderText, "responder-text", "", "auto-responder message body")
fl.StringVar(&f.copyAddress, "copy-address", "", "BCC copy recipient address(es)")
fl.StringVar(&f.senderAlias, "sender-alias", "", "permitted FROM alias address(es)")
fl.StringVar(&f.xlistEnabled, "xlist-enabled", "", "enable XLIST special-folder mapping (Y|N)")
fl.StringVar(&f.xlistSent, "xlist-sent", "", "XLIST sent-items folder name")
fl.StringVar(&f.xlistDrafts, "xlist-drafts", "", "XLIST drafts folder name")
fl.StringVar(&f.xlistTrash, "xlist-trash", "", "XLIST trash folder name")
fl.StringVar(&f.xlistSpam, "xlist-spam", "", "XLIST spam folder name")
fl.StringVar(&f.xlistArchiv, "xlist-archiv", "", "XLIST archive folder name")
fl.StringVar(&f.allowNets, "allow-nets", "", "restrict access to these IP/CIDR networks")
fl.StringVar(&f.active, "active", "", "replacement mailbox status (Y|N)")
fl.StringVar(&f.webmailAutologin, "webmail-autologin", "", "replacement KAS-to-webmail auto-login setting (Y|N)")
fl.StringVar(&f.responder, "responder", "", `replacement auto-responder setting: "N", "Y", or a "<start>|<end>" timestamp range`)
fl.StringVar(&f.responderContentType, "responder-content-type", "", "replacement auto-responder body format (html|text)")
fl.StringVar(&f.responderDisplayName, "responder-displayname", "", "replacement auto-responder sender display name")
fl.StringVar(&f.responderText, "responder-text", "", "replacement auto-responder message body")
fl.StringVar(&f.copyAddress, "copy-address", "", "replacement BCC copy recipient address(es)")
fl.StringVar(&f.senderAlias, "sender-alias", "", "replacement permitted FROM alias address(es)")
fl.StringVar(&f.xlistEnabled, "xlist-enabled", "", "replacement XLIST special-folder mapping setting (Y|N)")
fl.StringVar(&f.xlistSent, "xlist-sent", "", "replacement XLIST sent-items folder name")
fl.StringVar(&f.xlistDrafts, "xlist-drafts", "", "replacement XLIST drafts folder name")
fl.StringVar(&f.xlistTrash, "xlist-trash", "", "replacement XLIST trash folder name")
fl.StringVar(&f.xlistSpam, "xlist-spam", "", "replacement XLIST spam folder name")
fl.StringVar(&f.xlistArchiv, "xlist-archiv", "", "replacement XLIST archive folder name")
fl.StringVar(&f.allowNets, "allow-nets", "", "replacement IP/CIDR network access restriction (an explicitly empty value clears it)")
}

// mailAccountUpdateChangedFields collects only the flags the user
Expand Down
Loading
Loading