squadron/internal/template/onepassword.go
Kevin Franklin Kim 4b88ffbd15
feat: bump linter
2025-10-10 11:54:31 +02:00

336 lines
9.0 KiB
Go

package template
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strings"
"sync"
"text/template"
"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
"github.com/pkg/errors"
"github.com/pterm/pterm"
)
var (
onePasswordCache map[string]map[string]string
onePasswordUUID = regexp.MustCompile(`^[a-z0-9]{26}$`)
)
var ErrOnePasswordNotSignedIn = errors.New("not signed in")
func onePasswordConnectGet(client connect.Client, vaultUUID, itemUUID string) (map[string]string, error) {
var item *onepassword.Item
if onePasswordUUID.MatchString(itemUUID) {
if v, err := client.GetItem(itemUUID, vaultUUID); err != nil {
return nil, err
} else {
item = v
}
} else {
if v, err := client.GetItemByTitle(itemUUID, vaultUUID); err != nil {
return nil, err
} else {
item = v
}
}
ret := map[string]string{}
for _, f := range item.Fields {
ret[f.Label] = f.Value
}
return ret, nil
}
func onePasswordConnectGetDocument(client connect.Client, vaultUUID, itemUUID string) (string, error) {
var item *onepassword.Item
if onePasswordUUID.MatchString(itemUUID) {
if v, err := client.GetItem(itemUUID, vaultUUID); err != nil {
return "", err
} else {
item = v
}
} else {
if v, err := client.GetItemByTitle(itemUUID, vaultUUID); err != nil {
return "", err
} else {
item = v
}
}
if item.Category != onepassword.Document {
return "", errors.Errorf("unexpected document type: %s", item.Category)
} else if len(item.Files) != 0 {
return "", errors.Errorf("unexpected document files length: %d", len(item.Files))
}
res, err := client.GetFileContent(item.Files[0])
if err != nil {
return "", err
}
return strings.Trim(string(res), "\n"), nil
}
var onePasswordGetLock sync.Mutex
func onePasswordGet(ctx context.Context, account, vaultUUID, itemUUID string) (map[string]string, error) {
onePasswordGetLock.Lock()
defer onePasswordGetLock.Unlock()
var v struct {
Vault struct {
ID string `json:"id"`
} `json:"vault"`
Fields []struct {
ID string `json:"id"`
Type string `json:"type"` // CONCEALED, STRING
Label string `json:"label"`
Value any `json:"value"`
} `json:"fields"`
}
if res, err := exec.CommandContext(ctx, "op", "item", "get", itemUUID, "--vault", vaultUUID, "--account", account, "--format", "json").CombinedOutput(); err != nil && strings.Contains(string(res), "You are not currently signed in") {
return nil, ErrOnePasswordNotSignedIn
} else if err != nil {
return nil, errors.Wrap(err, string(res))
} else if err := json.Unmarshal(res, &v); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal secret")
} else if v.Vault.ID != vaultUUID {
return nil, errors.Errorf("wrong vault UUID %s for item %s", vaultUUID, itemUUID)
} else {
ret := map[string]string{}
aliases := map[string]string{
"notesPlain": "notes",
}
for _, field := range v.Fields {
if alias, ok := aliases[field.Label]; ok {
ret[alias] = fmt.Sprintf("%v", field.Value)
} else {
ret[field.Label] = fmt.Sprintf("%v", field.Value)
}
}
return ret, nil
}
}
var onePasswordGetDocumentLock sync.Mutex
func onePasswordGetDocument(ctx context.Context, account, vaultUUID, itemUUID string) (string, error) {
onePasswordGetDocumentLock.Lock()
defer onePasswordGetDocumentLock.Unlock()
res, err := exec.CommandContext(ctx, "op", "document", "get", itemUUID, "--vault", vaultUUID, "--account", account).CombinedOutput()
if err != nil && strings.Contains(string(res), "You are not currently signed in") {
return "", ErrOnePasswordNotSignedIn
} else if err != nil {
return "", errors.Wrap(err, string(res))
}
return strings.Trim(string(res), "\n"), nil
}
func onePasswordSignIn(ctx context.Context, account string) error {
fmt.Println("Your templates includes a call to 1Password, please sign in to retrieve your session token:")
// create command
cmd := exec.CommandContext(ctx, "op", "signin", account, "--raw")
// use multi writer to handle password prompt
var stdoutBuf bytes.Buffer
cmd.Stdout = io.MultiWriter(os.Stdout, &stdoutBuf)
cmd.Stdin = os.Stdin
// start the process and wait till it's finished
if err := cmd.Start(); err != nil {
return err
} else if err := cmd.Wait(); err != nil {
return err
}
if token := strings.TrimSuffix(stdoutBuf.String(), "\n"); token == "" {
fmt.Printf("Failed to login into your '%s' account! Please refer to the manual:\n", account)
fmt.Println("https://support.1password.com/command-line-getting-started/#set-up-the-command-line-tool")
return errors.New("failed to retrieve 1password session token")
} else if err := os.Setenv(fmt.Sprintf("OP_SESSION_%s", account), token); err != nil {
return err
} else {
fmt.Println("NOTE: If you want to skip this step, run:")
fmt.Printf("export OP_SESSION_%s=%s\n", account, token)
}
return nil
}
func isConnect() bool {
return os.Getenv("OP_CONNECT_HOST") != "" && os.Getenv("OP_CONNECT_TOKEN") != ""
}
func isServiceAccount() bool {
return os.Getenv("OP_SERVICE_ACCOUNT_TOKEN") != ""
}
var onePasswordInitLock sync.Mutex
func onePasswordInit(ctx context.Context, account string) error {
onePasswordInitLock.Lock()
defer onePasswordInitLock.Unlock()
// validate cache
if onePasswordCache != nil {
return nil
}
onePasswordCache = map[string]map[string]string{}
// validate env
if isConnect() || isServiceAccount() {
return nil
}
// validate executeable
if _, err := exec.LookPath("op"); err != nil {
pterm.Warning.Println("Your templates includes a call to 1Password, please install it:")
pterm.Warning.Println("https://support.1password.com/command-line-getting-started/#set-up-the-command-line-tool")
return errors.Wrap(err, "failed to lookup op")
}
// validate auth
if _, err := exec.CommandContext(ctx, "op", "account", "get", "--account", account).CombinedOutput(); err == nil {
return nil
}
// validate auth env
if os.Getenv(fmt.Sprintf("OP_SESSION_%s", account)) == "" {
if err := onePasswordSignIn(ctx, account); err != nil {
return errors.Wrap(err, "failed to sign in")
}
}
return nil
}
func onePassword(ctx context.Context, templateVars any, errorOnMissing bool) func(account, vaultUUID, itemUUID, field string) (string, error) {
return func(account, vaultUUID, itemUUID, field string) (string, error) {
// init
if err := onePasswordInit(ctx, account); err != nil {
return "", err
}
// render uuid & field params
if value, err := onePasswordRender("op", itemUUID, templateVars, errorOnMissing); err != nil {
return "", err
} else {
itemUUID = value
}
if value, err := onePasswordRender("op", field, templateVars, errorOnMissing); err != nil {
return "", err
} else {
field = value
}
// create cache key
cacheKey := strings.Join([]string{account, vaultUUID, itemUUID}, "#")
if _, ok := onePasswordCache[cacheKey]; !ok {
if isConnect() {
client, err := connect.NewClientFromEnvironment()
if err != nil {
return "", err
}
if res, err := onePasswordConnectGet(client, vaultUUID, itemUUID); err != nil {
return "", err
} else {
onePasswordCache[cacheKey] = res
}
} else {
if res, err := onePasswordGet(ctx, account, vaultUUID, itemUUID); err != nil {
return "", err
} else {
onePasswordCache[cacheKey] = res
}
}
}
if value, ok := onePasswordCache[cacheKey][field]; !ok {
return "", nil
} else {
return value, nil
}
}
}
func onePasswordDocument(ctx context.Context, templateVars any, errorOnMissing bool) func(account, vaultUUID, itemUUID string) (string, error) {
return func(account, vaultUUID, itemUUID string) (string, error) {
// init
if err := onePasswordInit(ctx, account); err != nil {
return "", err
}
// render uuid & field params
if value, err := onePasswordRender("op", itemUUID, templateVars, errorOnMissing); err != nil {
return "", err
} else {
itemUUID = value
}
// create cache key
cacheKey := strings.Join([]string{account, vaultUUID, itemUUID}, "#")
if _, ok := onePasswordCache[cacheKey]; !ok {
if isConnect() {
if client, err := connect.NewClientFromEnvironment(); err != nil {
return "", err
} else if res, err := onePasswordConnectGetDocument(client, vaultUUID, itemUUID); err != nil {
return "", err
} else {
onePasswordCache[cacheKey] = map[string]string{"document": res}
}
} else {
if res, err := onePasswordGetDocument(ctx, account, vaultUUID, itemUUID); err != nil {
return "", err
} else {
onePasswordCache[cacheKey] = map[string]string{"document": res}
}
}
}
if value, ok := onePasswordCache[cacheKey]["document"]; !ok {
return "", nil
} else {
return value, nil
}
}
}
func onePasswordRender(name, text string, data any, errorOnMissing bool) (string, error) {
var opts []string
if !errorOnMissing {
opts = append(opts, "missingkey=error")
}
out := bytes.NewBuffer([]byte{})
if uuidTpl, err := template.New(name).Option(opts...).Parse(text); err != nil {
return "", err
} else if err := uuidTpl.Execute(out, data); err != nil {
return "", err
}
return out.String(), nil
}