-
Notifications
You must be signed in to change notification settings - Fork 9
/
secrets.go
138 lines (111 loc) · 3.68 KB
/
secrets.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// Copyright 2022 Namespace Labs Inc; All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
package onepassword
import (
"bytes"
"context"
"fmt"
"os"
"os/exec"
"sync"
"time"
"github.com/spf13/pflag"
"namespacelabs.dev/foundation/framework/secrets"
"namespacelabs.dev/foundation/framework/secrets/combined"
"namespacelabs.dev/foundation/internal/console"
"namespacelabs.dev/foundation/internal/fnerrors"
"namespacelabs.dev/foundation/std/cfg"
"namespacelabs.dev/foundation/std/tasks"
"namespacelabs.dev/foundation/universe/onepassword"
)
const cmdTimeout = time.Minute
var (
readSerially bool
)
func SetupFlags(flags *pflag.FlagSet) {
flags.BoolVar(&readSerially, "one_password_read_serially", false, "If specified, read secrets from 1Password serially.")
_ = flags.MarkHidden("one_password_read_serially")
}
func Register() {
p := &provider{}
combined.RegisterSecretsProvider(func(ctx context.Context, _ cfg.Configuration, _ secrets.SecretIdentifier, cfg *onepassword.Secret) ([]byte, error) {
if cfg.SecretReference == "" {
return nil, fnerrors.BadInputError("invalid 1Password secret configuration: missing field secret_reference")
}
return p.Read(ctx, cfg.SecretReference)
})
}
type provider struct {
once sync.Once
initErr error
mu sync.Mutex
}
func (p *provider) Read(ctx context.Context, ref string) ([]byte, error) {
if readSerially {
p.mu.Lock()
defer p.mu.Unlock()
}
var data []byte
p.once.Do(func() {
// If no account is configured, `op read` does not fail but waits for user input.
// Hence, we ensure on the first read that a user account is indeed configured.
if err := ensureAccount(ctx); err != nil {
p.initErr = err
return
}
// Do the first read serially, so that the user ends up with only one approval popup.
res, err := read(ctx, ref)
if err != nil {
p.initErr = err
return
}
data = res
// XXX hack!
// The first read succeeds and unlocks the 1Password client.
// Still, the second read can fail with `error initializing client: account is not signed in` if issued too quickly :(
time.Sleep(time.Second)
})
// The only writes to p.initErr are inside p.once which is already done at this point.
if p.initErr != nil {
return nil, p.initErr
}
if data != nil {
// First read does not need to repeat.
return data, nil
}
return read(ctx, ref)
}
func ensureAccount(ctx context.Context) error {
return tasks.Return0(ctx, tasks.Action("1password.ensure"), func(ctx context.Context) error {
if os.Getenv("OP_SERVICE_ACCOUNT_TOKEN") != "" {
return nil
}
// Handle manual logins.
c := exec.CommandContext(ctx, "op", "account", "list")
var b bytes.Buffer
c.Stdout = &b
c.Stderr = console.Output(ctx, "1Password")
if err := c.Run(); err != nil {
return fnerrors.InvocationError("1Password", "failed to invoke %q: %w", c.String(), err)
} else if b.String() == "" {
return fnerrors.InvocationError("1Password", "no 1Password account configured")
}
fmt.Fprintf(console.Debug(ctx), "Configured 1Password accounts:\n%s\n", b.String())
return nil
})
}
func read(ctx context.Context, ref string) ([]byte, error) {
return tasks.Return(ctx, tasks.Action("1password.read").Arg("ref", ref), func(ctx context.Context) ([]byte, error) {
c := exec.CommandContext(ctx, "op", "read", ref)
var b bytes.Buffer
c.Stdout = &b
c.Stderr = console.Output(ctx, "1Password")
if err := c.Run(); err != nil {
return nil, fnerrors.InvocationError("1Password", "failed to invoke %q: %w", c.String(), err)
}
// `\n` is added by `op read`.
data := bytes.TrimSuffix(b.Bytes(), []byte{'\n'})
return data, nil
})
}