Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OP CLI Basic Support #63

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,22 @@ endif
ifneq ($(WORKTREE_CLEAN), 0)
@echo "[ERROR] Uncommitted changes found in worktree. Address them and try again."; exit 1;
endif

## Local Installation
# Example Provider For Local Testing
# terraform {
# required_providers {
# onepassword = {
# source = "terraform.example.com/local/onepassword"
# version = "~> 1.0.2"
# }
# }
#}
local: VERSION = "1.0.2"
local: PLUGIN_PATH = ~/.terraform.d/plugins/terraform.example.com/local/onepassword/${VERSION}/darwin_amd64
local: build
mkdir -p $(PLUGIN_PATH)
cp ./dist/terraform-provider-onepassword $(PLUGIN_PATH)
rm ./examples/cli/.terraform.lock.hcl
rm -r ./examples/cli/.terraform
cd ./examples/cli/ && terraform init
25 changes: 22 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,37 @@ Use the 1Password Connect Terraform Provider to reference, create, or update ite

## Example Usage

Connecting to 1Password Connect

```terraform
provider "onepassword" {
url = "http://localhost:8080"
}
```

Connecting to 1Password local CLI

```terraform
provider "onepassword" {
account = "username"
password = "password"
}
```

## Schema

### Required
### Optional

- **token** (String, Optional) A valid token for your 1Password Connect API. Can also be sourced from OP_CONNECT_TOKEN.

### Optional

- **url** (String, Optional) The HTTP(S) URL where your 1Password Connect API can be found. Must be provided through the OP_CONNECT_HOST environment variable if this attribute is not set.

### Optional

- **token** (String, Required) A valid token for your 1Password Connect API. Can also be sourced from OP_CONNECT_TOKEN.
- **account** (String, Optional) Account to use for the 1Password CLI. Can also be sourced from OP_ACCOUNT

### Optional

- **url** (String, Optional) The HTTP(S) URL where your 1Password Connect API can be found. Must be provided through the the OP_CONNECT_HOST environment variable if this attribute is not set.
- **password** (String, Optional) Password to use for the 1Password CLI. Can also be sourced from OP_PASSWORD
30 changes: 30 additions & 0 deletions examples/cli/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
variable "op_account" {
type = string
}
variable "op_password" {
type = string
}

terraform {
required_providers {
onepassword = {
source = "terraform.example.com/local/onepassword"
version = "~> 1.0.2"
}
}
}


provider "onepassword" {
account = var.op_account
password = var.op_password
}

data "onepassword_item" "item" {
vault = "Private"
title = "Example"
}

output "password" {
value = data.onepassword_item.item.section
}
82 changes: 71 additions & 11 deletions onepassword/provider.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package onepassword

import (
"context"
"fmt"
"os/exec"

"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/terraform-provider-onepassword/version"
"github.com/1Password/terraform-provider-onepassword/opcli"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/1Password/terraform-provider-onepassword/version"
)

const (
Expand Down Expand Up @@ -33,6 +38,18 @@ func Provider() *schema.Provider {
providerUserAgent := fmt.Sprintf(terraformProviderUserAgent, version.ProviderVersion)
provider := &schema.Provider{
Schema: map[string]*schema.Schema{
"account": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("OP_ACCOUNT", nil),
Description: "The account to execute the command by account shorthand, sign-in address, account UUID, or user UUID.",
},
"password": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("OP_PASSWORD", nil),
Description: "The password to interact with the CLI",
},
"url": {
Type: schema.TypeString,
Optional: true,
Expand All @@ -41,7 +58,7 @@ func Provider() *schema.Provider {
},
"token": {
Type: schema.TypeString,
Required: true,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("OP_CONNECT_TOKEN", nil),
Description: "A valid token for your 1Password Connect API. Can also be sourced from OP_CONNECT_TOKEN.",
},
Expand All @@ -54,20 +71,63 @@ func Provider() *schema.Provider {
"onepassword_item": resourceOnepasswordItem(),
},
}
provider.ConfigureFunc = func(d *schema.ResourceData) (interface{}, error) {
provider.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
var op bool
url := d.Get("url").(string)
token := d.Get("token").(string)
account := d.Get("account").(string)
password := d.Get("password").(string)

// This is not handled by setting Required to true because Terraform does not handle
// multiple required attributes well. If only one is set in the provider configuration,
// the other one is prompted for, but Terraform then forgets the value for the one that
// is defined in the code. This confusing user-experience can be avoided by handling the
// requirement of one of the attributes manually.
if url == "" {
return nil, fmt.Errorf("URL for Connect API is not set. Either provide the \"url\" field in the provider configuration or set the OP_CONNECT_HOST environment variable")
if _, err := exec.LookPath("op"); err == nil {
op = true
}

return connect.NewClientWithUserAgent(url, token, providerUserAgent), nil
if url != "" || token != "" {
if url == "" {
return nil, diag.Diagnostics{{
Severity: diag.Error,
Summary: "URL for Connect API is not set",
Detail: "Either provide the \"url\" field in the provider configuration or set the OP_CONNECT_HOST environment variable",
}}
}
if token == "" {
return nil, diag.Diagnostics{{
Severity: diag.Error,
Summary: "TOKEN for Connect API is not set",
Detail: "Either provide the \"token\" field in the provider configuration or set the OP_CONNECT_TOKEN environment variable",
}}
}
return connect.NewClientWithUserAgent(url, token, providerUserAgent), nil
} else if account == "" {
return nil, diag.Diagnostics{{
Severity: diag.Error,
Summary: "ACCOUNT is not set",
Detail: "Either provide the \"account\" field in the provider configuration or set the OP_ACCOUNT environment variable",
}}
} else if !op {
return nil, diag.Diagnostics{{
Severity: diag.Error,
Summary: "op executable not found",
Detail: "Please ensure you have the 1password-cli >= 2.0.0 installed in your $PATH.",
}}
} else if password == "" {
return nil, diag.Diagnostics{{
Severity: diag.Error,
Summary: "Password is not set",
Detail: "Provide the OP_PASSWORD environment variable.",
}}
} else {
provider, err := opcli.NewCLIClient(account, password)
if err != nil {
return nil, diag.Diagnostics{{
Severity: diag.Error,
Summary: "Could not initialize CLI provider",
Detail: err.Error(),
}}
}

return provider, nil
}
}
return provider
}
83 changes: 83 additions & 0 deletions opcli/cli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package opcli

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"

"github.com/1Password/connect-sdk-go/onepassword"
)

type OnePasswordCLI struct {
account string
token string
}

func (cli OnePasswordCLI) GetItem(item, vault string) (*onepassword.Item, error) {
output, err := cli.command(
defaultOnePasswordPath,
"item", "get", item,
"--vault", vault,
"--format", "json",
)
if err != nil {
return nil, err
}

var value *onepassword.Item
err = json.Unmarshal(output, &value)
if err != nil {
return nil, fmt.Errorf("failed to unmarshlal: %v data: %v", err, string(output))
}
return value, nil
}

func (cli OnePasswordCLI) command(name string, args ...string) (output []byte, err error) {
if cli.token == "" {
return nil, errors.New("OP client not authenticated")
}
args = append(args,
"--session", cli.token,
"--account", cli.account,
"--no-color",
)
cmd := exec.Command(name, args...)
var stdout, stdin, stderr bytes.Buffer

cmd.Stdin = &stdin
cmd.Stdout = &stdout
cmd.Stderr = &stderr

err = cmd.Run()
if err != nil {
stdout.Write(stderr.Bytes())
return nil, fmt.Errorf(string(stdout.Bytes()))
}
return stdout.Bytes(), nil
}

func getOnePasswordSessionToken(account, password string) (string, error) {
cmd := exec.Command(defaultOnePasswordPath,
"signin",
"--account", account,
"--raw",
)
var stdout, stdin, stderr bytes.Buffer
cmd.Env = os.Environ()

stdin.Write([]byte(password))
cmd.Stdin = &stdin
cmd.Stdout = &stdout
cmd.Stderr = &stderr

err := cmd.Run()
if err != nil {
stdout.Write(stderr.Bytes())
return "", fmt.Errorf("failed to sign in: %v", string(stdout.Bytes()))
}

return string(stdout.Bytes()), nil
}
Loading