Skip to content

Commit 8d29da8

Browse files
authored
Merge pull request #276 from 1Password/new/redis
new(redis-cli): Add support for Redis CLI with password as environment variable and command-line flags
2 parents 4f3af47 + 9139722 commit 8d29da8

11 files changed

Lines changed: 449 additions & 0 deletions

File tree

plugins/redis/plugin.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package redis
2+
3+
import (
4+
"github.com/1Password/shell-plugins/sdk"
5+
"github.com/1Password/shell-plugins/sdk/schema"
6+
)
7+
8+
func New() schema.Plugin {
9+
return schema.Plugin{
10+
Name: "redis",
11+
Platform: schema.PlatformInfo{
12+
Name: "Redis",
13+
Homepage: sdk.URL("https://redis.io/"),
14+
},
15+
Credentials: []schema.CredentialType{
16+
UserCredentials(),
17+
},
18+
Executables: []schema.Executable{
19+
RedisCLI(),
20+
},
21+
}
22+
}

plugins/redis/provisioner.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package redis
2+
3+
import (
4+
"context"
5+
6+
"github.com/1Password/shell-plugins/sdk"
7+
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
8+
)
9+
10+
type redisArgsProvisioner struct {
11+
}
12+
13+
func redisProvisioner() sdk.Provisioner {
14+
return redisArgsProvisioner{}
15+
}
16+
17+
// Redis CLI flags that, when already supplied by the user, signal that we
18+
// should not provision the corresponding field from the 1Password item.
19+
var (
20+
hostFlags = []string{"-h"}
21+
portFlags = []string{"-p"}
22+
userFlags = []string{"--user"}
23+
passwordFlags = []string{"-a", "--pass"}
24+
)
25+
26+
func (p redisArgsProvisioner) Provision(ctx context.Context, in sdk.ProvisionInput, out *sdk.ProvisionOutput) {
27+
suppliedFlags := flagSet(out.CommandLine)
28+
29+
// The password is passed via an environment variable so it never appears in
30+
// the process's argument list. Skip it if the user already authenticated on
31+
// the command line.
32+
if value, ok := in.ItemFields[fieldname.Password]; ok && !containsAny(suppliedFlags, passwordFlags) {
33+
out.AddEnvVar("REDISCLI_AUTH", value)
34+
}
35+
36+
// Collect the flags to inject first, then prepend them in a single pass.
37+
// Mutating out.CommandLine while ranging over it risks index-out-of-range
38+
// panics and stale reads, so we never modify it during inspection.
39+
var injected []string
40+
if value, ok := in.ItemFields[fieldname.Host]; ok && !containsAny(suppliedFlags, hostFlags) {
41+
injected = append(injected, "-h", value)
42+
}
43+
if value, ok := in.ItemFields[fieldname.Port]; ok && !containsAny(suppliedFlags, portFlags) {
44+
injected = append(injected, "-p", value)
45+
}
46+
if value, ok := in.ItemFields[fieldname.Username]; ok && !containsAny(suppliedFlags, userFlags) {
47+
injected = append(injected, "--user", value)
48+
}
49+
50+
if len(injected) > 0 {
51+
out.CommandLine = prependArgs(out.CommandLine, injected)
52+
}
53+
}
54+
55+
func (p redisArgsProvisioner) Deprovision(ctx context.Context, in sdk.DeprovisionInput, out *sdk.DeprovisionOutput) {
56+
// Nothing to do here: credentials get wiped automatically when the process exits.
57+
}
58+
59+
func (p redisArgsProvisioner) Description() string {
60+
return "Provision redis secrets as command-line arguments and the password as an environment variable."
61+
}
62+
63+
// flagSet returns the set of arguments present on the command line, excluding
64+
// the executable name at index 0.
65+
func flagSet(commandLine []string) map[string]bool {
66+
set := make(map[string]bool, len(commandLine))
67+
for i, arg := range commandLine {
68+
if i == 0 {
69+
continue
70+
}
71+
set[arg] = true
72+
}
73+
return set
74+
}
75+
76+
func containsAny(set map[string]bool, flags []string) bool {
77+
for _, f := range flags {
78+
if set[f] {
79+
return true
80+
}
81+
}
82+
return false
83+
}
84+
85+
// prependArgs inserts args immediately after the executable name (index 0),
86+
// leaving the rest of the user-supplied command line intact.
87+
func prependArgs(commandLine []string, args []string) []string {
88+
if len(commandLine) == 0 {
89+
return append([]string{}, args...)
90+
}
91+
result := make([]string, 0, len(commandLine)+len(args))
92+
result = append(result, commandLine[0])
93+
result = append(result, args...)
94+
result = append(result, commandLine[1:]...)
95+
return result
96+
}

plugins/redis/redis_cli.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package redis
2+
3+
import (
4+
"github.com/1Password/shell-plugins/sdk"
5+
"github.com/1Password/shell-plugins/sdk/needsauth"
6+
"github.com/1Password/shell-plugins/sdk/schema"
7+
"github.com/1Password/shell-plugins/sdk/schema/credname"
8+
)
9+
10+
func RedisCLI() schema.Executable {
11+
return schema.Executable{
12+
Name: "Redis CLI",
13+
Runs: []string{"redis-cli"},
14+
DocsURL: sdk.URL("https://redis.io/docs/ui/cli"),
15+
NeedsAuth: needsauth.IfAll(
16+
needsauth.NotWhenContainsArgs("--help"),
17+
needsauth.NotForVersion(),
18+
),
19+
Uses: []schema.CredentialUsage{
20+
{
21+
Name: credname.UserCredentials,
22+
Provisioner: redisProvisioner(),
23+
},
24+
},
25+
}
26+
}

plugins/redis/user_credentials.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package redis
2+
3+
import (
4+
"github.com/1Password/shell-plugins/sdk"
5+
"github.com/1Password/shell-plugins/sdk/importer"
6+
"github.com/1Password/shell-plugins/sdk/provision"
7+
"github.com/1Password/shell-plugins/sdk/schema"
8+
"github.com/1Password/shell-plugins/sdk/schema/credname"
9+
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
10+
)
11+
12+
func UserCredentials() schema.CredentialType {
13+
return schema.CredentialType{
14+
Name: credname.UserCredentials,
15+
DocsURL: sdk.URL("https://redis.io/docs/ui/cli/#host-port-password-and-database"),
16+
Fields: []schema.CredentialField{
17+
{
18+
Name: fieldname.Password,
19+
MarkdownDescription: "Password used to authenticate to Redis server.",
20+
Secret: true,
21+
Composition: &schema.ValueComposition{
22+
Charset: schema.Charset{
23+
Uppercase: true,
24+
Lowercase: true,
25+
Digits: true,
26+
},
27+
},
28+
},
29+
{
30+
Name: fieldname.Username,
31+
MarkdownDescription: "Username used to authenticate to Redis server.",
32+
Secret: false,
33+
Optional: true,
34+
Composition: &schema.ValueComposition{
35+
Charset: schema.Charset{
36+
Uppercase: true,
37+
Lowercase: true,
38+
Digits: true,
39+
},
40+
},
41+
},
42+
{
43+
Name: fieldname.Host,
44+
MarkdownDescription: "Host address for the Redis server.",
45+
Secret: false,
46+
Optional: true,
47+
Composition: &schema.ValueComposition{
48+
Charset: schema.Charset{
49+
Lowercase: true,
50+
Symbols: true,
51+
Digits: true,
52+
},
53+
},
54+
},
55+
{
56+
Name: fieldname.Port,
57+
MarkdownDescription: "Port for the Redis server.",
58+
Secret: false,
59+
Optional: true,
60+
Composition: &schema.ValueComposition{
61+
Charset: schema.Charset{
62+
Digits: true,
63+
},
64+
},
65+
},
66+
},
67+
DefaultProvisioner: provision.EnvVars(defaultEnvVarMapping),
68+
Importer: importer.TryAll(
69+
importer.TryEnvVarPair(defaultEnvVarMapping),
70+
)}
71+
}
72+
73+
var defaultEnvVarMapping = map[string]sdk.FieldName{
74+
"REDISCLI_AUTH": fieldname.Password,
75+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package redis
2+
3+
import (
4+
"testing"
5+
6+
"github.com/1Password/shell-plugins/sdk"
7+
"github.com/1Password/shell-plugins/sdk/plugintest"
8+
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
9+
)
10+
11+
func TestUserCredentialsProvisioner(t *testing.T) {
12+
plugintest.TestProvisioner(t, redisProvisioner(), map[string]plugintest.ProvisionCase{
13+
"default": {
14+
ItemFields: map[sdk.FieldName]string{
15+
fieldname.Password: "pjtxpc2gaddifapjvalggspojexample",
16+
fieldname.Username: "example",
17+
fieldname.Host: "127.0.0.1",
18+
fieldname.Port: "6379",
19+
},
20+
CommandLine: []string{"redis-cli"},
21+
ExpectedOutput: sdk.ProvisionOutput{
22+
Environment: map[string]string{
23+
"REDISCLI_AUTH": "pjtxpc2gaddifapjvalggspojexample",
24+
},
25+
CommandLine: []string{"redis-cli", "-h", "127.0.0.1", "-p", "6379", "--user", "example"},
26+
},
27+
},
28+
// User supplies some flags themselves: we must not duplicate them, but
29+
// should still provision the fields they left out (and keep their args).
30+
"user-supplied flags are respected": {
31+
ItemFields: map[sdk.FieldName]string{
32+
fieldname.Password: "pjtxpc2gaddifapjvalggspojexample",
33+
fieldname.Username: "example",
34+
fieldname.Host: "127.0.0.1",
35+
fieldname.Port: "6379",
36+
},
37+
CommandLine: []string{"redis-cli", "-h", "myhost", "PING"},
38+
ExpectedOutput: sdk.ProvisionOutput{
39+
Environment: map[string]string{
40+
"REDISCLI_AUTH": "pjtxpc2gaddifapjvalggspojexample",
41+
},
42+
CommandLine: []string{"redis-cli", "-p", "6379", "--user", "example", "-h", "myhost", "PING"},
43+
},
44+
},
45+
// A recognized flag as the final token used to cause an index-out-of-range
46+
// panic; ensure it is handled gracefully.
47+
"recognized flag as last token": {
48+
ItemFields: map[sdk.FieldName]string{
49+
fieldname.Password: "pjtxpc2gaddifapjvalggspojexample",
50+
fieldname.Host: "127.0.0.1",
51+
},
52+
CommandLine: []string{"redis-cli", "-h"},
53+
ExpectedOutput: sdk.ProvisionOutput{
54+
Environment: map[string]string{
55+
"REDISCLI_AUTH": "pjtxpc2gaddifapjvalggspojexample",
56+
},
57+
CommandLine: []string{"redis-cli", "-h"},
58+
},
59+
},
60+
// User authenticates on the command line: don't also set the env var.
61+
"password flag skips env var": {
62+
ItemFields: map[sdk.FieldName]string{
63+
fieldname.Password: "pjtxpc2gaddifapjvalggspojexample",
64+
},
65+
CommandLine: []string{"redis-cli", "-a", "mypassword"},
66+
ExpectedOutput: sdk.ProvisionOutput{
67+
CommandLine: []string{"redis-cli", "-a", "mypassword"},
68+
},
69+
},
70+
})
71+
}
72+
func TestDefaultUserCredentialsProvisioner(t *testing.T) {
73+
plugintest.TestProvisioner(t, UserCredentials().DefaultProvisioner, map[string]plugintest.ProvisionCase{
74+
"default": {
75+
ItemFields: map[sdk.FieldName]string{
76+
fieldname.Password: "pjtxpc2gaddifapjvalggspojexample",
77+
},
78+
ExpectedOutput: sdk.ProvisionOutput{
79+
Environment: map[string]string{
80+
"REDISCLI_AUTH": "pjtxpc2gaddifapjvalggspojexample",
81+
},
82+
},
83+
},
84+
})
85+
}
86+
87+
func TestUserCredentialsImporter(t *testing.T) {
88+
plugintest.TestImporter(t, UserCredentials().Importer, map[string]plugintest.ImportCase{
89+
"environment": {
90+
Environment: map[string]string{
91+
"REDISCLI_AUTH": "pjtxpc2gaddifapjvalggspojexample",
92+
},
93+
ExpectedCandidates: []sdk.ImportCandidate{
94+
{
95+
Fields: map[sdk.FieldName]string{
96+
fieldname.Password: "pjtxpc2gaddifapjvalggspojexample",
97+
},
98+
},
99+
},
100+
},
101+
})
102+
}

plugins/rediscloud/api_key.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package rediscloud
2+
3+
import (
4+
"github.com/1Password/shell-plugins/sdk"
5+
"github.com/1Password/shell-plugins/sdk/importer"
6+
"github.com/1Password/shell-plugins/sdk/provision"
7+
"github.com/1Password/shell-plugins/sdk/schema"
8+
"github.com/1Password/shell-plugins/sdk/schema/credname"
9+
"github.com/1Password/shell-plugins/sdk/schema/fieldname"
10+
)
11+
12+
func APIKey() schema.CredentialType {
13+
return schema.CredentialType{
14+
Name: credname.APIKey,
15+
DocsURL: sdk.URL("https://docs.redis.com/latest/rc/api/get-started/manage-api-keys/"),
16+
ManagementURL: sdk.URL("https://app.redislabs.com/#/access-management/api-keys"),
17+
Fields: []schema.CredentialField{
18+
{
19+
Name: fieldname.AccountKey,
20+
MarkdownDescription: "API Account key (also known as Access Key, or just API Key) to authenticate to Redis Enterprise Cloud.",
21+
Secret: true,
22+
Composition: &schema.ValueComposition{
23+
Charset: schema.Charset{
24+
Uppercase: true,
25+
Lowercase: true,
26+
Digits: true,
27+
},
28+
},
29+
},
30+
{
31+
Name: fieldname.UserKey,
32+
MarkdownDescription: "API user key (also known as Secret Key) to authenticate to Redis Enterprise Cloud.",
33+
Secret: true,
34+
Composition: &schema.ValueComposition{
35+
Charset: schema.Charset{
36+
Uppercase: true,
37+
Lowercase: true,
38+
Digits: true,
39+
},
40+
},
41+
},
42+
},
43+
DefaultProvisioner: provision.EnvVars(envVarMappingForRedisCloud),
44+
Importer: importer.TryAll(
45+
importer.TryEnvVarPair(envVarMappingForRedisCloud),
46+
)}
47+
}
48+
49+
var envVarMappingForRedisCloud = map[string]sdk.FieldName{
50+
"REDISCLOUD_ACCESS_KEY": fieldname.AccountKey,
51+
"REDISCLOUD_SECRET_KEY": fieldname.UserKey,
52+
}

0 commit comments

Comments
 (0)