From a40313ecd9861820886ee79ac4bc9638c82143f2 Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:42:30 -0800 Subject: [PATCH 1/6] implement shell plugin for authenticating with MotherDuck when using duckdb --- plugins/motherduck/access_token.go | 40 ++++++++++++++++++ plugins/motherduck/access_token_test.go | 55 +++++++++++++++++++++++++ plugins/motherduck/duckdb.go | 24 +++++++++++ plugins/motherduck/plugin.go | 22 ++++++++++ 4 files changed, 141 insertions(+) create mode 100644 plugins/motherduck/access_token.go create mode 100644 plugins/motherduck/access_token_test.go create mode 100644 plugins/motherduck/duckdb.go create mode 100644 plugins/motherduck/plugin.go diff --git a/plugins/motherduck/access_token.go b/plugins/motherduck/access_token.go new file mode 100644 index 00000000..5dfcd29e --- /dev/null +++ b/plugins/motherduck/access_token.go @@ -0,0 +1,40 @@ +package motherduck + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/importer" + "github.com/1Password/shell-plugins/sdk/provision" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func AccessToken() schema.CredentialType { + return schema.CredentialType{ + Name: credname.AccessToken, + DocsURL: sdk.URL("https://motherduck.com/docs/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/#authentication-using-an-access-token"), + ManagementURL: sdk.URL("https://app.motherduck.com/settings/general"), + Fields: []schema.CredentialField{ + { + Name: fieldname.Token, + MarkdownDescription: "Token used to authenticate to MotherDuck.", + Secret: true, + Composition: &schema.ValueComposition{ + Length: 405, + Charset: schema.Charset{ + Uppercase: true, + Lowercase: true, + Digits: true, + }, + }, + }, + }, + DefaultProvisioner: provision.EnvVars(defaultEnvVarMapping), + Importer: importer.TryAll( + importer.TryEnvVarPair(defaultEnvVarMapping), + )} +} + +var defaultEnvVarMapping = map[string]sdk.FieldName{ + "motherduck_token": fieldname.Token, +} diff --git a/plugins/motherduck/access_token_test.go b/plugins/motherduck/access_token_test.go new file mode 100644 index 00000000..51c3248a --- /dev/null +++ b/plugins/motherduck/access_token_test.go @@ -0,0 +1,55 @@ +package motherduck + +import ( + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func TestAccessTokenProvisioner(t *testing.T) { + plugintest.TestProvisioner(t, AccessToken().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "default": { + ItemFields: map[sdk.FieldName]string{ // TODO: Check if this is correct + fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "MOTHERDUCK_TOKEN": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + }, + }, + }, + }) +} + +func TestAccessTokenImporter(t *testing.T) { + plugintest.TestImporter(t, AccessToken().Importer, map[string]plugintest.ImportCase{ + "environment": { + Environment: map[string]string{ // TODO: Check if this is correct + "MOTHERDUCK_TOKEN": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + }, + }, + }, + }, + // TODO: If you implemented a config file importer, add a test file example in motherduck/test-fixtures + // and fill the necessary details in the test template below. + "config file": { + Files: map[string]string{ + // "~/path/to/config.yml": plugintest.LoadFixture(t, "config.yml"), + }, + ExpectedCandidates: []sdk.ImportCandidate{ + // { + // Fields: map[sdk.FieldName]string{ + // fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + // }, + // }, + }, + }, + }) +} diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go new file mode 100644 index 00000000..cd79b0b5 --- /dev/null +++ b/plugins/motherduck/duckdb.go @@ -0,0 +1,24 @@ +package motherduck + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" +) + +func DuckDBCLI() schema.Executable { + return schema.Executable{ + Name: "DuckDB CLI", + Runs: []string{"duckdb"}, + DocsURL: sdk.URL("https://duckdb.org/docs/api/cli/overview"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.AccessToken, + }, + }, + } +} diff --git a/plugins/motherduck/plugin.go b/plugins/motherduck/plugin.go new file mode 100644 index 00000000..08673d2a --- /dev/null +++ b/plugins/motherduck/plugin.go @@ -0,0 +1,22 @@ +package motherduck + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "motherduck", + Platform: schema.PlatformInfo{ + Name: "MotherDuck", + Homepage: sdk.URL("https://motherduck.com"), // TODO: Check if this is correct + }, + Credentials: []schema.CredentialType{ + AccessToken(), + }, + Executables: []schema.Executable{ + DuckDBCLI(), + }, + } +} From f1ce2381aa284261494e1ca4e897783bf86f481b Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:05:55 -0800 Subject: [PATCH 2/6] Don't use plugin if any of the args contain motherduck_token= --- plugins/motherduck/duckdb.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go index cd79b0b5..0e2a464e 100644 --- a/plugins/motherduck/duckdb.go +++ b/plugins/motherduck/duckdb.go @@ -1,12 +1,46 @@ package motherduck import ( + "strings" + "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/needsauth" "github.com/1Password/shell-plugins/sdk/schema" "github.com/1Password/shell-plugins/sdk/schema/credname" ) +func NotWhenAnyArgsContain(argsSequence ...string) sdk.NeedsAuthentication { + return func(in sdk.NeedsAuthenticationInput) bool { + if len(argsSequence) == 0 { + return true + } + + if len(argsSequence) > len(in.CommandArgs) { + return true + } + + for i := range in.CommandArgs { + if i+len(argsSequence) > len(in.CommandArgs) { + return true + } + + matches := true + for i, argsToCompare := range in.CommandArgs[i : i+len(argsSequence)] { + if !strings.Contains(argsToCompare, argsSequence[i]) { + matches = false + } + } + + // If the argsToSkip are found in the command-line args, return that the command + // does not not require authentication + if matches { + return false + } + } + return true + } +} + func DuckDBCLI() schema.Executable { return schema.Executable{ Name: "DuckDB CLI", @@ -14,6 +48,7 @@ func DuckDBCLI() schema.Executable { DocsURL: sdk.URL("https://duckdb.org/docs/api/cli/overview"), NeedsAuth: needsauth.IfAll( needsauth.NotForHelpOrVersion(), + NotWhenAnyArgsContain("motherduck_token="), ), Uses: []schema.CredentialUsage{ { From e08177cde68451a1a6471cb0815bb986f092fe73 Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:06:22 -0800 Subject: [PATCH 3/6] remove test for config file --- plugins/motherduck/access_token_test.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/plugins/motherduck/access_token_test.go b/plugins/motherduck/access_token_test.go index 51c3248a..1c2d9488 100644 --- a/plugins/motherduck/access_token_test.go +++ b/plugins/motherduck/access_token_test.go @@ -2,12 +2,12 @@ package motherduck import ( "testing" - + "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/plugintest" "github.com/1Password/shell-plugins/sdk/schema/fieldname" ) - + func TestAccessTokenProvisioner(t *testing.T) { plugintest.TestProvisioner(t, AccessToken().DefaultProvisioner, map[string]plugintest.ProvisionCase{ "default": { @@ -37,19 +37,5 @@ func TestAccessTokenImporter(t *testing.T) { }, }, }, - // TODO: If you implemented a config file importer, add a test file example in motherduck/test-fixtures - // and fill the necessary details in the test template below. - "config file": { - Files: map[string]string{ - // "~/path/to/config.yml": plugintest.LoadFixture(t, "config.yml"), - }, - ExpectedCandidates: []sdk.ImportCandidate{ - // { - // Fields: map[sdk.FieldName]string{ - // fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", - // }, - // }, - }, - }, }) } From 00e7531ef0bb96a5d680be4d251dff3ce53769ff Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:23:49 -0700 Subject: [PATCH 4/6] - update management url - length is not fixed so remove - token can include . and _ --- plugins/motherduck/access_token.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/motherduck/access_token.go b/plugins/motherduck/access_token.go index 5dfcd29e..234f15e3 100644 --- a/plugins/motherduck/access_token.go +++ b/plugins/motherduck/access_token.go @@ -13,18 +13,18 @@ func AccessToken() schema.CredentialType { return schema.CredentialType{ Name: credname.AccessToken, DocsURL: sdk.URL("https://motherduck.com/docs/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/#authentication-using-an-access-token"), - ManagementURL: sdk.URL("https://app.motherduck.com/settings/general"), + ManagementURL: sdk.URL("https://app.motherduck.com/settings/tokens"), Fields: []schema.CredentialField{ { Name: fieldname.Token, MarkdownDescription: "Token used to authenticate to MotherDuck.", Secret: true, Composition: &schema.ValueComposition{ - Length: 405, Charset: schema.Charset{ Uppercase: true, Lowercase: true, Digits: true, + Specific: []rune{'.', '_'}, }, }, }, From 3cf454404f5deeb7df4e1b357aaecb66c51224a7 Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:25:36 -0700 Subject: [PATCH 5/6] replace NotWhenAnyArgsContain with helper function specific to motherduck. Defer to environment variable or provided token value if either is set. --- plugins/motherduck/duckdb.go | 55 ++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go index 0e2a464e..85618fa1 100644 --- a/plugins/motherduck/duckdb.go +++ b/plugins/motherduck/duckdb.go @@ -1,6 +1,7 @@ package motherduck import ( + "os" "strings" "github.com/1Password/shell-plugins/sdk" @@ -9,36 +10,28 @@ import ( "github.com/1Password/shell-plugins/sdk/schema/credname" ) -func NotWhenAnyArgsContain(argsSequence ...string) sdk.NeedsAuthentication { - return func(in sdk.NeedsAuthenticationInput) bool { - if len(argsSequence) == 0 { - return true - } - - if len(argsSequence) > len(in.CommandArgs) { - return true - } - - for i := range in.CommandArgs { - if i+len(argsSequence) > len(in.CommandArgs) { - return true - } - - matches := true - for i, argsToCompare := range in.CommandArgs[i : i+len(argsSequence)] { - if !strings.Contains(argsToCompare, argsSequence[i]) { - matches = false - } - } - - // If the argsToSkip are found in the command-line args, return that the command - // does not not require authentication - if matches { - return false - } - } - return true - } +// The plugin is only invoked if: +// - environment variable motherduck_token is not set +// - connection string contains 'md:' and does not contain 'motherduck_token=' +func ForMotherDuckButTokenNotSet() sdk.NeedsAuthentication { + return func(in sdk.NeedsAuthenticationInput) bool { + // If environment variables are already set, we don't need to authenticate + if envValue := os.Getenv("motherduck_token"); envValue != "" { + return false + } + + // Otherwise, check if the command uses MotherDuck + if len(in.CommandArgs) == 0 { + return false + } + + for _, arg := range in.CommandArgs { + if strings.Contains(arg, "md:") && !strings.Contains(arg, "motherduck_token=") { + return true + } + } + return false + } } func DuckDBCLI() schema.Executable { @@ -48,7 +41,7 @@ func DuckDBCLI() schema.Executable { DocsURL: sdk.URL("https://duckdb.org/docs/api/cli/overview"), NeedsAuth: needsauth.IfAll( needsauth.NotForHelpOrVersion(), - NotWhenAnyArgsContain("motherduck_token="), + ForMotherDuckButTokenNotSet(), ), Uses: []schema.CredentialUsage{ { From 14818e7538077291d7d41e4c432fa9932fffc545 Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:25:42 -0700 Subject: [PATCH 6/6] remove todo --- plugins/motherduck/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/motherduck/plugin.go b/plugins/motherduck/plugin.go index 08673d2a..8de964c6 100644 --- a/plugins/motherduck/plugin.go +++ b/plugins/motherduck/plugin.go @@ -10,7 +10,7 @@ func New() schema.Plugin { Name: "motherduck", Platform: schema.PlatformInfo{ Name: "MotherDuck", - Homepage: sdk.URL("https://motherduck.com"), // TODO: Check if this is correct + Homepage: sdk.URL("https://motherduck.com"), }, Credentials: []schema.CredentialType{ AccessToken(),