mirror of
https://github.com/foomo/squadron.git
synced 2025-10-16 12:35:42 +00:00
336 lines
9.0 KiB
Go
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
|
|
}
|