diff --git a/plugins/localxpose/access_token.go b/plugins/localxpose/access_token.go new file mode 100644 index 00000000..de25beba --- /dev/null +++ b/plugins/localxpose/access_token.go @@ -0,0 +1,95 @@ +package localxpose + +import ( + "context" + + "github.com/99designs/keyring" + + "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" +) + +const ( + serviceName = "https://loclx.io" + keyringKeyName = "token" + tokenFilePath = "~/.localxpose/.access" +) + +func AccessToken() schema.CredentialType { + return schema.CredentialType{ + Name: credname.AccessToken, + DocsURL: sdk.URL("https://localxpose.io/docs"), + ManagementURL: sdk.URL("https://localxpose.io/dashboard/access"), + Fields: []schema.CredentialField{ + { + Name: fieldname.AccessToken, + MarkdownDescription: "Token used to authenticate to LocalXpose.", + Secret: true, + Composition: &schema.ValueComposition{ + Length: 40, + Charset: schema.Charset{ + Uppercase: true, + Lowercase: true, + Digits: true, + }, + }, + }, + }, + DefaultProvisioner: provision.EnvVars(defaultEnvVarMapping), + Importer: importer.TryAll( + importer.TryEnvVarPair(defaultEnvVarMapping), + TryOSKeyring(), + TryAccessTokenFile(), + )} +} + +var defaultEnvVarMapping = map[string]sdk.FieldName{ + "LX_ACCESS_TOKEN": fieldname.AccessToken, +} + +func TryAccessTokenFile() sdk.Importer { + return importer.TryFile(tokenFilePath, func(ctx context.Context, contents importer.FileContents, in sdk.ImportInput, out *sdk.ImportAttempt) { + if contents.ToString() == "" { + return + } + out.AddCandidate(sdk.ImportCandidate{ + Fields: map[sdk.FieldName]string{ + fieldname.AccessToken: contents.ToString(), + }, + }) + }) +} + +func TryOSKeyring() sdk.Importer { + return func(ctx context.Context, in sdk.ImportInput, out *sdk.ImportOutput) { + availableBackends := keyring.AvailableBackends() + if len(availableBackends) == 0 { + return + } + for _, backendType := range availableBackends { + attempt := out.NewAttempt(importer.SourceOther(string(backendType), "")) + openKeyring, err := keyring.Open(keyring.Config{ + KeychainTrustApplication: true, + ServiceName: serviceName, + }) + if err != nil { + attempt.AddError(err) + return + } + key, err := openKeyring.Get(keyringKeyName) + if err != nil { + attempt.AddError(err) + continue + } + attempt.AddCandidate(sdk.ImportCandidate{ + Fields: map[sdk.FieldName]string{ + fieldname.AccessToken: string(key.Data), + }, + }) + } + } +} diff --git a/plugins/localxpose/access_token_test.go b/plugins/localxpose/access_token_test.go new file mode 100644 index 00000000..5af40bb6 --- /dev/null +++ b/plugins/localxpose/access_token_test.go @@ -0,0 +1,53 @@ +package localxpose + +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{ + fieldname.AccessToken: "PROVISIONERqLtcqQ8a3oRVfK5tiHzDOhEXAMPLE", + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "LX_ACCESS_TOKEN": "PROVISIONERqLtcqQ8a3oRVfK5tiHzDOhEXAMPLE", + }, + }, + }, + }) +} + +func TestAccessTokenImporter(t *testing.T) { + expectedFields := map[sdk.FieldName]string{ + fieldname.AccessToken: "31QJpgl8FB9qLtcqQ8a3oRVfK5tiHzDOhEXAMPLE", + } + + plugintest.TestImporter(t, AccessToken().Importer, map[string]plugintest.ImportCase{ + "environment": { + Environment: map[string]string{ + "LX_ACCESS_TOKEN": "31QJpgl8FB9qLtcqQ8a3oRVfK5tiHzDOhEXAMPLE", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: expectedFields, + }, + }, + }, + "file": { + Files: map[string]string{ + "~/.localxpose/.access": plugintest.LoadFixture(t, "localxpose.access"), + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: expectedFields, + }, + }, + }, + }) +} diff --git a/plugins/localxpose/localxpose.go b/plugins/localxpose/localxpose.go new file mode 100644 index 00000000..f1f347f6 --- /dev/null +++ b/plugins/localxpose/localxpose.go @@ -0,0 +1,36 @@ +package localxpose + +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 LocalXposeCLI() schema.Executable { + return schema.Executable{ + Name: "LocalXpose CLI", + Runs: []string{"loclx"}, + DocsURL: sdk.URL("https://localxpose.io/docs/cli"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + needsauth.NotWhenContainsArgs("account", "login"), // skip 1Password authentication for "loclx account login" and its subcommands + needsauth.NotWhenContainsArgs("a", "login"), // skip 1Password authentication for "loclx account login" and its subcommands + needsauth.NotWhenContainsArgs("account", "logout"), // skip 1Password authentication for "loclx account logout" and its subcommands + needsauth.NotWhenContainsArgs("a", "logout"), // skip 1Password authentication for "loclx account logout" and its subcommands + needsauth.NotWhenContainsArgs("service", "restart"), // skip 1Password authentication for "loclx service restart" and its subcommands + needsauth.NotWhenContainsArgs("service", "start"), // skip 1Password authentication for "loclx service start" and its subcommands + needsauth.NotWhenContainsArgs("service", "status"), // skip 1Password authentication for "loclx service status" and its subcommands + needsauth.NotWhenContainsArgs("service", "stop"), // skip 1Password authentication for "loclx service stop" and its subcommands + needsauth.NotWhenContainsArgs("service", "uninstall"), // skip 1Password authentication for "loclx service uninstall" and its subcommands + needsauth.NotWhenContainsArgs("setting"), // skip 1Password authentication for "loclx setting" and its subcommands + needsauth.NotWhenContainsArgs("update"), // skip 1Password authentication for "loclx update" and its subcommands + needsauth.NotWithoutArgs(), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.AccessToken, + }, + }, + } +} diff --git a/plugins/localxpose/plugin.go b/plugins/localxpose/plugin.go new file mode 100644 index 00000000..9625383b --- /dev/null +++ b/plugins/localxpose/plugin.go @@ -0,0 +1,22 @@ +package localxpose + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "localxpose", + Platform: schema.PlatformInfo{ + Name: "LocalXpose", + Homepage: sdk.URL("https://localxpose.io"), + }, + Credentials: []schema.CredentialType{ + AccessToken(), + }, + Executables: []schema.Executable{ + LocalXposeCLI(), + }, + } +} diff --git a/plugins/localxpose/test-fixtures/localxpose.access b/plugins/localxpose/test-fixtures/localxpose.access new file mode 100644 index 00000000..73dda4e8 --- /dev/null +++ b/plugins/localxpose/test-fixtures/localxpose.access @@ -0,0 +1 @@ +31QJpgl8FB9qLtcqQ8a3oRVfK5tiHzDOhEXAMPLE \ No newline at end of file