Skip to content
This repository has been archived by the owner on Feb 16, 2023. It is now read-only.

Commit

Permalink
Merge pull request #396 from secrethub/eddy/395-cli-2
Browse files Browse the repository at this point in the history
Add support for 1Password CLI 2 in the migration
  • Loading branch information
edif2008 authored Feb 15, 2022
2 parents 3962e85 + cd7d62a commit d29a942
Show file tree
Hide file tree
Showing 4 changed files with 305 additions and 111 deletions.
115 changes: 115 additions & 0 deletions internals/onepassword/cli_v1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package onepassword

import (
"encoding/base64"
"encoding/json"
"fmt"
)

type OPV1CLI struct{}

func (op *OPV1CLI) IsV2() bool {
return false
}

func (op *OPV1CLI) CreateVault(name string) error {
_, err := execOP("create", "vault", name)
if err != nil {
return fmt.Errorf("could not create vault '%s': %s", name, err)
}
return nil
}

func (op *OPV1CLI) CreateItem(vault string, template *ItemTemplate, title string) error {
jsonTemplate, err := json.Marshal(template)
if err != nil {
return err
}

encodedTemplate := base64.RawURLEncoding.EncodeToString(jsonTemplate)

_, err = execOP("create", "item", "apicredential", "--vault="+vault, encodedTemplate, "title="+title)
return err
}

func (op *OPV1CLI) SetField(vault, item, field, value string) error {
_, err := execOP("edit", "item", item, fmt.Sprintf(`%s=%s`, field, value), "--vault="+vault)
if err != nil {
return fmt.Errorf("could not set field '%s'.'%s'.'%s'", vault, item, field)
}
return nil
}

// GetFields returns a title-to-value map of the fields from the first section of the given 1Password item.
// The rest of the fields are ignored as the migration tool only stores information in the first
// section of each item.
func (op *OPV1CLI) GetFields(vault, item string) (map[string]string, error) {
opItem := struct {
Details ItemTemplate `json:"details"`
}{}
opItemJSON, err := execOP("get", "item", item, "--vault="+vault)
if err != nil {
return nil, fmt.Errorf("could not get item '%s'.'%s' from 1Password: %s", vault, item, err)
}
err = json.Unmarshal(opItemJSON, &opItem)
if err != nil {
return nil, fmt.Errorf("unexpected format of 1Password item in `op get item` command output: %s", err)
}

fields := make(map[string]string, len(opItem.Details.Sections[0].Fields))
for _, field := range opItem.Details.Sections[0].Fields {
fields[field.Title] = field.Value
}
return fields, nil
}

func (op *OPV1CLI) ExistsVault(vaultName string) (bool, error) {
vaultsBytes, err := execOP("list", "vaults")
if err != nil {
return false, fmt.Errorf("could not list vaults: %s", err)
}

vaultsJSON := make([]struct {
UUID string `json:"uuid"`
Name string `json:"name"`
}, 0)

err = json.Unmarshal(vaultsBytes, &vaultsJSON)
if err != nil {
return false, fmt.Errorf("unexpected format of `op list vaults`: %s", vaultsBytes)
}

for _, vault := range vaultsJSON {
if vault.Name == vaultName {
return true, nil
}
}

return false, nil
}

func (op *OPV1CLI) ExistsItemInVault(vault string, itemName string) (bool, error) {
itemsBytes, err := execOP("list", "items", "--vault", vault)
if err != nil {
return false, fmt.Errorf("could not list items in vault %s: %s", vault, err)
}

itemsJSON := make([]struct {
Overview struct {
Title string `json:"title"`
} `json:"overview"`
}, 0)

err = json.Unmarshal(itemsBytes, &itemsJSON)
if err != nil {
return false, fmt.Errorf("unexpected format of `op list items`: %s", itemsBytes)
}

for _, item := range itemsJSON {
if item.Overview.Title == itemName {
return true, nil
}
}

return false, nil
}
134 changes: 134 additions & 0 deletions internals/onepassword/cli_v2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package onepassword

import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
)

type OPV2CLI struct{}

func (op *OPV2CLI) IsV2() bool {
return true
}

func (op *OPV2CLI) CreateVault(name string) error {
_, err := execOP("vault", "create", name)
if err != nil {
return fmt.Errorf("could not create vault '%s': %s", name, err)
}
return nil
}

func (op *OPV2CLI) CreateItem(vault string, template *ItemTemplate, title string) error {
jsonTemplate, err := json.Marshal(template)
if err != nil {
return err
}

tempJSONFile, err := ioutil.TempFile(os.TempDir(), "jsonTemplate-")
if err != nil {
return err
}
defer os.Remove(tempJSONFile.Name())

if _, err = tempJSONFile.Write(jsonTemplate); err != nil {
return err
}

_, err = execOP("item", "create", "--category=apicredential", "--vault="+vault, "--template="+tempJSONFile.Name(), "--title="+title)
if err != nil {
return err
}

err = tempJSONFile.Close()
return err
}

func (op *OPV2CLI) SetField(vault, item, field, value string) error {
_, err := execOP("item", "edit", item, fmt.Sprintf(`%s=%s`, field, value), "--vault="+vault)
if err != nil {
return fmt.Errorf("could not set field '%s'.'%s'.'%s'", vault, item, field)
}
return nil
}

// GetFields returns a title-to-value map of the fields from the first section of the given 1Password item.
// The rest of the fields are ignored as the migration tool only stores information in the first
// section of each item.
func (op *OPV2CLI) GetFields(vault, item string) (map[string]string, error) {
opItem := struct {
Fields []v2ItemFieldTemplate `json:"fields"`
}{}
opItemJSON, err := execOP("item", "get", item, "--vault="+vault, "--format=json")
if err != nil {
return nil, fmt.Errorf("could not get item '%s'.'%s' from 1Password: %s", vault, item, err)
}
err = json.Unmarshal(opItemJSON, &opItem)
if err != nil {
return nil, fmt.Errorf("unexpected format of 1Password item in `op get item` command output: %s", err)
}

fields := make(map[string]string, len(opItem.Fields))
for _, field := range opItem.Fields {
fields[field.Label] = field.Value
}
return fields, nil
}

type v2ItemFieldTemplate struct {
ID string `json:"id"`
Type string `json:"type"`
Label string `json:"label"`
Value string `json:"value"`
}

func (op *OPV2CLI) ExistsVault(vaultName string) (bool, error) {
vaultsBytes, err := execOP("vault", "list", "--format=json")
if err != nil {
return false, fmt.Errorf("could not list vaults: %s", err)
}

vaultsJSON := make([]struct {
ID string `json:"id"`
Name string `json:"name"`
}, 0)

err = json.Unmarshal(vaultsBytes, &vaultsJSON)
if err != nil {
return false, fmt.Errorf("unexpected format of `op list vaults`: %s", vaultsBytes)
}

for _, vault := range vaultsJSON {
if vault.Name == vaultName {
return true, nil
}
}

return false, nil
}

func (op *OPV2CLI) ExistsItemInVault(vault string, itemName string) (bool, error) {
itemsBytes, err := execOP("item", "list", "--vault="+vault, "--format=json")
if err != nil {
return false, fmt.Errorf("could not list items in vault %s: %s", vault, err)
}

itemsJSON := make([]struct {
Title string `json:"title"`
}, 0)

err = json.Unmarshal(itemsBytes, &itemsJSON)
if err != nil {
return false, fmt.Errorf("unexpected format of `op list items`: %s", itemsBytes)
}

for _, item := range itemsJSON {
if item.Title == itemName {
return true, nil
}
}

return false, nil
}
111 changes: 17 additions & 94 deletions internals/onepassword/onepassword.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package onepassword

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
Expand All @@ -14,55 +13,30 @@ import (
"github.com/mitchellh/go-homedir"
)

func CreateVault(name string) error {
_, err := execOP("create", "vault", name)
if err != nil {
return fmt.Errorf("could not create vault '%s': %s", name, err)
}
return nil
type OPCLI interface {
IsV2() bool
CreateVault(name string) error
CreateItem(vault string, template *ItemTemplate, title string) error
SetField(vault, item, field, value string) error
GetFields(vault, item string) (map[string]string, error)
ExistsVault(vaultName string) (bool, error)
ExistsItemInVault(vault string, itemName string) (bool, error)
}

func CreateItem(vault string, template *ItemTemplate, title string) error {
jsonTemplate, err := json.Marshal(template)
func GetOPClient() (OPCLI, error) {
out, err := execOP("--version")
if err != nil {
return err
return nil, err
}

encodedTemplate := base64.RawURLEncoding.EncodeToString(jsonTemplate)
version := strings.TrimSpace(string(out))

_, err = execOP("create", "item", "apicredential", "--vault="+vault, encodedTemplate, "title="+title)
return err
}

func SetField(vault, item, field, value string) error {
_, err := execOP("edit", "item", item, fmt.Sprintf(`%s=%s`, field, value), "--vault="+vault)
if err != nil {
return fmt.Errorf("could not set field '%s'.'%s'.'%s'", vault, item, field)
if strings.HasPrefix(version, "2.") {
return &OPV2CLI{}, nil
} else if strings.HasPrefix(version, "1.") {
return &OPV1CLI{}, nil
}
return nil
}

// GetFields returns a title-to-value map of the fields from the first section of the given 1Password item.
// The rest of the fields are ignored as the migration tool only stores information in the first
// section of each item.
func GetFields(vault, item string) (map[string]string, error) {
opItem := struct {
Details ItemTemplate `json:"details"`
}{}
opItemJSON, err := execOP("get", "item", item, "--vault="+vault)
if err != nil {
return nil, fmt.Errorf("could not get item '%s'.'%s' from 1Password: %s", vault, item, err)
}
err = json.Unmarshal(opItemJSON, &opItem)
if err != nil {
return nil, fmt.Errorf("unexpected format of 1Password item in `op get item` command output: %s", err)
}

fields := make(map[string]string, len(opItem.Details.Sections[0].Fields))
for _, field := range opItem.Details.Sections[0].Fields {
fields[field.Title] = field.Value
}
return fields, nil
return nil, fmt.Errorf("1password: op version not recognized")
}

func NewItemTemplate() *ItemTemplate {
Expand Down Expand Up @@ -202,54 +176,3 @@ func GetSignInAddress() (string, error) {

return "", fmt.Errorf("unexpected format of 1password config file at %s: missing account entry for latest used account", path)
}

func ExistsVault(vaultName string) (bool, error) {
vaultsBytes, err := execOP("list", "vaults")
if err != nil {
return false, fmt.Errorf("could not list vaults: %s", err)
}

vaultsJSON := make([]struct {
UUID string `json:"uuid"`
Name string `json:"name"`
}, 0)

err = json.Unmarshal(vaultsBytes, &vaultsJSON)
if err != nil {
return false, fmt.Errorf("unexpected format of `op list vaults`: %s", vaultsBytes)
}

for _, vault := range vaultsJSON {
if vault.Name == vaultName {
return true, nil
}
}

return false, nil
}

func ExistsItemInVault(vault string, itemName string) (bool, error) {
itemsBytes, err := execOP("list", "items", "--vault", vault)
if err != nil {
return false, fmt.Errorf("could not list items in vault %s: %s", vault, err)
}

itemsJSON := make([]struct {
Overview struct {
Title string `json:"title"`
} `json:"overview"`
}, 0)

err = json.Unmarshal(itemsBytes, &itemsJSON)
if err != nil {
return false, fmt.Errorf("unexpected format of `op list items`: %s", itemsBytes)
}

for _, item := range itemsJSON {
if item.Overview.Title == itemName {
return true, nil
}
}

return false, nil
}
Loading

0 comments on commit d29a942

Please sign in to comment.