Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
061ecc69dc | ||
|
|
8c54e0ff75 |
@@ -251,6 +251,12 @@ func NewConfigCommand() *cobra.Command {
|
||||
Use: "config",
|
||||
Short: "Manage the configuration file",
|
||||
Long: "Commands for creating, editing, and querying the vaultik config file.",
|
||||
// cobra checks Args only on a command that has a Run: without
|
||||
// one, a mistyped subcommand printed help and exited 0.
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
_ = cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(newConfigInitCommand())
|
||||
|
||||
@@ -15,6 +15,12 @@ func NewDatabaseCommand() *cobra.Command {
|
||||
Use: "database",
|
||||
Short: "Manage the local state database",
|
||||
Long: `Commands for managing the local SQLite state database.`,
|
||||
// cobra checks Args only on a command that has a Run: without
|
||||
// one, a mistyped subcommand printed help and exited 0.
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
_ = cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package cli //nolint:testpackage // shares runEntry and the command-word constants
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestEntryGroupRejectsUnknownSubcommand runs each group command with a
|
||||
// mistyped subcommand, as a crontab line might carry one. It must fail
|
||||
// with exit 1 and name the word, as `vaultik bogus` does at the top
|
||||
// level, instead of printing the group's help and exiting 0 having done
|
||||
// nothing. The group name alone still prints its help.
|
||||
//
|
||||
//nolint:paralleltest // replaces os.Args, os.Stdout and os.Stderr
|
||||
func TestEntryGroupRejectsUnknownSubcommand(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
group string
|
||||
mistyped string
|
||||
}{
|
||||
{group: cmdSnapshot, mistyped: "craete"},
|
||||
{group: "config", mistyped: "inti"},
|
||||
{group: cmdRemote, mistyped: "ifno"},
|
||||
{group: "database", mistyped: "delte"},
|
||||
{group: "completion", mistyped: "nosuchshell"},
|
||||
} {
|
||||
t.Run(testCase.group, func(t *testing.T) {
|
||||
code, stdout, stderr := runEntry(t,
|
||||
testCase.group, testCase.mistyped)
|
||||
|
||||
assert.Equal(t, 1, code)
|
||||
assert.Empty(t, stdout)
|
||||
assert.Contains(t, stderr,
|
||||
`unknown command "`+testCase.mistyped+`"`)
|
||||
|
||||
code, stdout, _ = runEntry(t, testCase.group)
|
||||
|
||||
assert.Equal(t, 0, code)
|
||||
assert.Contains(t, stdout,
|
||||
programName+" "+testCase.group+" [command]")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,12 @@ func NewRemoteCommand() *cobra.Command {
|
||||
Use: "remote",
|
||||
Short: "Remote storage management commands",
|
||||
Long: "Commands for inspecting and managing remote storage",
|
||||
// cobra checks Args only on a command that has a Run: without
|
||||
// one, a mistyped subcommand printed help and exited 0.
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
_ = cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
|
||||
@@ -74,6 +74,21 @@ on the source system.`,
|
||||
NewDatabaseCommand(),
|
||||
)
|
||||
|
||||
// cobra adds the completion command only when Execute runs, and with
|
||||
// no Run, so it skipped its own cobra.NoArgs and a mistyped shell name
|
||||
// printed help and exited 0. Adding it here lets it have a Run. Its
|
||||
// scripts go to os.Stdout as it is now: a later SetOut does not
|
||||
// redirect them.
|
||||
cmd.InitDefaultCompletionCmd()
|
||||
|
||||
for _, sub := range cmd.Commands() {
|
||||
if sub.Name() == "completion" {
|
||||
sub.Run = func(completion *cobra.Command, _ []string) {
|
||||
_ = completion.Help()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ func NewSnapshotCommand() *cobra.Command {
|
||||
Use: "snapshot",
|
||||
Short: "Snapshot management commands",
|
||||
Long: "Commands for creating, listing, and managing snapshots",
|
||||
// cobra checks Args only on a command that has a Run: without
|
||||
// one, a mistyped subcommand printed help and exited 0.
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, _ []string) {
|
||||
_ = cmd.Help()
|
||||
},
|
||||
}
|
||||
|
||||
// Add subcommands
|
||||
|
||||
+14
-7
@@ -141,6 +141,19 @@ func parseS3URL(rawURL string) (*URL, error) {
|
||||
// remote name and any query parameter at all are rejected rather than
|
||||
// silently ignored.
|
||||
func parseRcloneURL(rawURL string) (*URL, error) {
|
||||
// The remote name runs up to the first "/", "?" or "#". It never
|
||||
// contains a comma; one starts rclone's remote,param=value form,
|
||||
// whose values can be credentials. This is checked before url.Parse,
|
||||
// whose error for such a name can print part of it.
|
||||
remote := strings.TrimPrefix(rawURL, "rclone://")
|
||||
if end := strings.IndexAny(remote, "/?#"); end >= 0 {
|
||||
remote = remote[:end]
|
||||
}
|
||||
|
||||
if strings.Contains(remote, ",") {
|
||||
return nil, ErrRcloneRemoteParams
|
||||
}
|
||||
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return nil, wrapParseError(err)
|
||||
@@ -150,17 +163,11 @@ func parseRcloneURL(rawURL string) (*URL, error) {
|
||||
return nil, ErrURLCredentials
|
||||
}
|
||||
|
||||
remote := u.Host
|
||||
remote = u.Host
|
||||
if remote == "" {
|
||||
return nil, ErrMissingRemote
|
||||
}
|
||||
|
||||
// An rclone remote name never contains a comma; one starts rclone's
|
||||
// remote,param=value form, whose values can be credentials.
|
||||
if strings.Contains(remote, ",") {
|
||||
return nil, ErrRcloneRemoteParams
|
||||
}
|
||||
|
||||
err = rejectUnknownParams(u.Query())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -218,22 +218,49 @@ func TestParseStorageURLRejectsCredentials(t *testing.T) {
|
||||
|
||||
// TestParseStorageURLRejectsRcloneRemoteParams checks that an rclone
|
||||
// remote name carrying parameters (rclone's "remote,param=value" form,
|
||||
// which can hold a credential) is rejected without echoing the value.
|
||||
// which can hold a credential) is rejected without echoing the value,
|
||||
// including the URLs that url.Parse itself refuses.
|
||||
func TestParseStorageURLRejectsRcloneRemoteParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const secret = "topsecret"
|
||||
|
||||
raw := "rclone://gdrive,client_secret=" + secret + "/backups"
|
||||
|
||||
_, err := storage.ParseStorageURL(raw)
|
||||
if !errors.Is(err, storage.ErrRcloneRemoteParams) {
|
||||
t.Fatalf("ParseStorageURL(%q) error = %v, want %v",
|
||||
raw, err, storage.ErrRcloneRemoteParams)
|
||||
cases := []struct {
|
||||
name string
|
||||
raw string
|
||||
secrets []string // must not appear in the error message
|
||||
}{
|
||||
{
|
||||
name: "configured remote",
|
||||
raw: "rclone://gdrive,client_secret=topsecret/backups",
|
||||
secrets: []string{"topsecret"},
|
||||
},
|
||||
{
|
||||
name: "remote defined inline",
|
||||
raw: "rclone://:s3,access_key_id=KEY,secret_access_key=SECRET/bucket",
|
||||
secrets: []string{"KEY", "SECRET"},
|
||||
},
|
||||
{
|
||||
name: "colon in a parameter value",
|
||||
raw: "rclone://gdrive,client_secret=ab:cd/backups",
|
||||
secrets: []string{"cd"},
|
||||
},
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), secret) {
|
||||
t.Errorf("error message leaked %q: %v", secret, err.Error())
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := storage.ParseStorageURL(tc.raw)
|
||||
if !errors.Is(err, storage.ErrRcloneRemoteParams) {
|
||||
t.Fatalf("ParseStorageURL(%q) error = %v, want %v",
|
||||
tc.raw, err, storage.ErrRcloneRemoteParams)
|
||||
}
|
||||
|
||||
for _, secret := range tc.secrets {
|
||||
if strings.Contains(err.Error(), secret) {
|
||||
t.Errorf("error message leaked %q: %v", secret, err.Error())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user