Skip to content

Commit

Permalink
Merge pull request #119 from 1Password/feat/biometric-unlock-with-ser…
Browse files Browse the repository at this point in the history
…vice-accounts

Authenticate 1Password CLI with biometric unlock using user account
  • Loading branch information
volodymyrZotov authored Dec 11, 2023
2 parents ac9d006 + b83c776 commit d5367ac
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 50 deletions.
58 changes: 42 additions & 16 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,52 @@ description: |-
Use the 1Password Terraform provider to reference, create, or update items in your existing vaults using [1Password Secrets Automation](https://1password.com/secrets).

The 1Password Terraform provider supports using both [1Password Connect Server](https://developer.1password.com/docs/secrets-automation/#1password-connect-server)
and [1Password Service Accounts](https://developer.1password.com/docs/secrets-automation/#1password-service-accounts).
To use a service account token, you must install [1Password CLI](https://developer.1password.com/docs/cli) on the machine running Terraform. Refer to the
and [1Password CLI](https://developer.1password.com/docs/cli).

You must install [1Password CLI](https://developer.1password.com/docs/cli) on the machine running Terraform to use it. Refer to the
[Terraform documentation](https://developer.hashicorp.com/terraform/cloud-docs/run/install-software#only-install-standalone-binaries) to learn how to install 1Password CLI on Terraform Cloud.

## Use with 1Password CLI

Retry mechanism is implemented when using the provider with 1Password CLI. The reason for having a retry mechanism is that 1Password doesn't allow parallel modification on the items located in the same vault.
Note that each retry fast forwards to the [service account rate limit](https://developer.1password.com/docs/service-accounts/rate-limits/) if use with service account.

It's recommended to limit the number of parallel resource operations. It can be done by using `-parallelism=n` flag when running `terraform apply`, where `n` is the number of parallel resource operations (the default is `10`).
```
terraform apply `-parallelism=n`
```

### Authenticate CLI with service account

To authenticate CLI with service account, set `service_account_token` in the provider configuration.

### Authenticate the CLI with user account using biometric unlock

To authenticate the CLI with user account using biometric unlock:
1. [Turn on the app integration](https://developer.1password.com/docs/cli/app-integration/#step-1-turn-on-the-app-integration)
2. In the terminal run `op account ls` to find sign-in address or account ID. It will print similar output in the console:
```
URL EMAIL USER ID
acme.dev.com [email protected] HERE_WILL_BE_REAL_USER_ID
acme.prod.com [email protected] HERE_WILL_BE_REAL_USER_ID
```
3. Set `account` in the provider configuration with the `URL` or `USER ID` value from the previous step.
4. When the biometric unlock popup appears while running terraform command, [authenticate it using fingerprint or password](https://developer.1password.com/docs/cli/app-integration/#step-2-enter-any-command-to-sign-in).

## Use with 1Password Connect

To use the provider with 1Password Connect you need to
1. [Deploy your Connect server](https://developer.1password.com/docs/connect/get-started#deployment)
2. Set `url` and `token` in the provider configuration.

## Example Usage

```terraform
provider "onepassword" {
url = "http://localhost:8080"
token = "CONNECT_TOKEN"
service_account_token = "SERVICE_ACCOUNT_TOKEN"
account = "ACCOUNT_ID_OR_SIGN_IN_ADDRESS"
op_cli_path = "OP_CLI_PATH"
}
```
Expand All @@ -30,17 +65,8 @@ provider "onepassword" {

### Optional

- `op_cli_path` (String) The path to the 1Password CLI binary. Can also be sourced from OP_CLI_PATH. Defaults to `op`. Only used when setting a `service_account_token`.
- `service_account_token` (String) A valid token for your 1Password Service Account. Can also be sourced from OP_SERVICE_ACCOUNT_TOKEN. Either this or `token` must be set.
- `token` (String) A valid token for your 1Password Connect API. Can also be sourced from OP_CONNECT_TOKEN. Either this or `service_account_token` must be set.
- `url` (String) 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. Can be omitted, if service_account_token is set.

## Use with service accounts:
Retry mechanism is implemented when using provider with service accounts. Each retry fast forwards to the [service account rate limit](https://developer.1password.com/docs/service-accounts/rate-limits/).

It's recommended to limit the number of parallel resource operations. It can be done by using `-parallelism=n` flag when running `terraform apply`, where `n` is the number of parallel resource operations (the default is `10`).
```
terraform apply `-parallelism=n`
```

The reason of having retry mechanism is that 1Password doesn't allow parallel modification on the items located in the same vault.
- `account` (String) A valid account's sign-in address or ID to use biometrics unlock. Can also be sourced from OP_ACCOUNT. Must be set to use with biometric unlock.
- `op_cli_path` (String) The path to the 1Password CLI binary. Can also be sourced from OP_CLI_PATH. Defaults to `op`.
- `service_account_token` (String) A valid token for your 1Password Service Account. Can also be sourced from OP_SERVICE_ACCOUNT_TOKEN. Must be set to use with 1Password service account.
- `token` (String) A valid token for your 1Password Connect API. Can also be sourced from OP_CONNECT_TOKEN. Must be set to use with 1Password Connect server.
- `url` (String) 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. Must be set to use with 1Password Connect server.
1 change: 1 addition & 0 deletions examples/provider/provider.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ provider "onepassword" {
url = "http://localhost:8080"
token = "CONNECT_TOKEN"
service_account_token = "SERVICE_ACCOUNT_TOKEN"
account = "ACCOUNT_ID_OR_SIGN_IN_ADDRESS"
op_cli_path = "OP_CLI_PATH"
}
23 changes: 16 additions & 7 deletions onepassword/cli/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import (
type OP struct {
binaryPath string
serviceAccountToken string
account string
}

func New(serviceAccountToken, binaryPath string) *OP {
func New(serviceAccountToken, binaryPath, account string) *OP {
return &OP{
binaryPath: binaryPath,
serviceAccountToken: serviceAccountToken,
account: account,
}
}

Expand Down Expand Up @@ -154,11 +156,7 @@ func (op *OP) delete(ctx context.Context, item *onepassword.Item, vaultUuid stri
}
item.Vault.ID = vaultUuid

err := op.execJson(ctx, nil, nil, p("item"), p("delete"), p(item.ID), f("vault", vaultUuid))
if err != nil {
return nil, err
}
return item, err
return nil, op.execJson(ctx, nil, nil, p("item"), p("delete"), p(item.ID), f("vault", vaultUuid))
}

func (op *OP) execJson(ctx context.Context, dst any, stdin []byte, args ...opArg) error {
Expand All @@ -174,18 +172,29 @@ func (op *OP) execJson(ctx context.Context, dst any, stdin []byte, args ...opArg

func (op *OP) execRaw(ctx context.Context, stdin []byte, args ...opArg) ([]byte, error) {
var cmdArgs []string

if op.account != "" {
args = append(args, f("account", op.account))
}

for _, arg := range args {
cmdArgs = append(cmdArgs, arg.format())
}

cmd := exec.CommandContext(ctx, op.binaryPath, cmdArgs...)
cmd.Env = append(cmd.Environ(),
"OP_SERVICE_ACCOUNT_TOKEN="+op.serviceAccountToken,
"OP_FORMAT=json",
"OP_INTEGRATION_NAME=terraform-provider-connect",
"OP_INTEGRATION_ID=GO",
//"OP_INTEGRATION_BUILDNUMBER="+version.ProviderVersion, // causes bad request errors from CLI
)
if op.serviceAccountToken != "" {
cmd.Env = append(cmd.Env, "OP_SERVICE_ACCOUNT_TOKEN="+op.serviceAccountToken)
}
if op.account != "" {
cmd.Env = append(cmd.Env, "OP_BIOMETRIC_UNLOCK_ENABLED=true")
}

if stdin != nil {
cmd.Stdin = bytes.NewReader(stdin)
}
Expand Down
55 changes: 38 additions & 17 deletions onepassword/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,31 @@ func Provider() *schema.Provider {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("OP_CONNECT_HOST", nil),
Description: "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. Can be omitted, if service_account_token is set.",
Description: "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. Must be set to use with 1Password Connect server.",
},
"token": {
Type: schema.TypeString,
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. Either this or `service_account_token` must be set.",
Description: "A valid token for your 1Password Connect API. Can also be sourced from OP_CONNECT_TOKEN. Must be set to use with 1Password Connect server.",
},
"service_account_token": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("OP_SERVICE_ACCOUNT_TOKEN", nil),
Description: "A valid token for your 1Password Service Account. Can also be sourced from OP_SERVICE_ACCOUNT_TOKEN. Either this or `token` must be set.",
Description: "A valid token for your 1Password Service Account. Can also be sourced from OP_SERVICE_ACCOUNT_TOKEN. Must be set to use with 1Password service account.",
},
"account": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("OP_ACCOUNT", nil),
Description: "A valid account's sign-in address or ID to use biometrics unlock. Can also be sourced from OP_ACCOUNT. Must be set to use with biometric unlock.",
},
"op_cli_path": {
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("OP_CLI_PATH", "op"),
Description: "The path to the 1Password CLI binary. Can also be sourced from OP_CLI_PATH. Defaults to `op`. Only used when setting a `service_account_token`.",
Description: "The path to the 1Password CLI binary. Can also be sourced from OP_CLI_PATH. Defaults to `op`.",
},
},
DataSourcesMap: map[string]*schema.Resource{
Expand All @@ -78,6 +84,7 @@ func Provider() *schema.Provider {
url = d.Get("url").(string)
token = d.Get("token").(string)
serviceAccountToken = d.Get("service_account_token").(string)
account = d.Get("account").(string)
opCliPath = d.Get("op_cli_path").(string)
)

Expand All @@ -86,34 +93,48 @@ func Provider() *schema.Provider {
// 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 serviceAccountToken != "" {
if serviceAccountToken != "" || account != "" {
if token != "" || url != "" {
return nil, diag.Errorf("Either Connect credentials (\"token\" and \"url\") or Service Account (\"service_account_token\") credentials can be set. Both are set. Please unset one of them.")
return nil, diag.Errorf("Either Connect credentials (\"token\" and \"url\") or 1Password CLI (\"service_account_token\" or \"account\") credentials can be set. Both are set. Please unset one of them.")
}
if opCliPath == "" {
return nil, diag.Errorf("Path to op CLI binary is not set. Either leave empty, provide the \"op_cli_path\" field in the provider configuration, or set the OP_CLI_PATH environment variable.")
}

op := cli.New(serviceAccountToken, opCliPath)

cliVersion, err := op.GetVersion(ctx)
if err != nil {
return nil, diag.FromErr(fmt.Errorf("failed to get version of op CLI: %w", err))
}
if cliVersion.LessThan(semver.MustParse(minimumOpCliVersion)) {
return nil, diag.Errorf("Current 1Password CLI version is \"%s\". Please upgrade to at least \"%s\".", cliVersion, minimumOpCliVersion)
if serviceAccountToken != "" && account != "" {
return nil, diag.Errorf("\"service_account_token\" and \"account\" are set. Please unset one of them to use the provider with 1Password CLI.")
}

return (Client)(op), nil
return initializeCLI(ctx, serviceAccountToken, account, opCliPath)
} else if token != "" && url != "" {
return connectctx.Wrap(connect.NewClientWithUserAgent(url, token, providerUserAgent)), nil
} else {
return nil, diag.Errorf("Invalid provider configuration. Either Connect credentials (\"token\" and \"url\") or Service Account (\"service_account_token\") credentials should be set.")
return nil, diag.Errorf("Invalid provider configuration. Either Connect credentials (\"token\" and \"url\") or Service Account (\"service_account_token\" or \"account\") credentials should be set.")
}
}
return provider
}

// initializeCLI initializes CLI to use either with service account or with user account
// service account takes preference if both are set
func initializeCLI(ctx context.Context, serviceAccountToken, account, opCliPath string) (Client, diag.Diagnostics) {
op := cli.New("", opCliPath, account)

// override OP to use service account token
if serviceAccountToken != "" {
op = cli.New(serviceAccountToken, opCliPath, "")
}

cliVersion, err := op.GetVersion(ctx)
if err != nil {
return nil, diag.FromErr(fmt.Errorf("failed to get version of op CLI: %w", err))
}
if cliVersion.LessThan(semver.MustParse(minimumOpCliVersion)) {
return nil, diag.Errorf("Current 1Password CLI version is \"%s\". Please upgrade to at least \"%s\".", cliVersion, minimumOpCliVersion)
}

return op, nil
}

// Client is a subset of connect.Client with context added.
type Client interface {
GetVault(ctx context.Context, uuid string) (*onepassword.Vault, error)
Expand Down
44 changes: 34 additions & 10 deletions templates/index.md.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,46 @@ description: |-
Use the 1Password Terraform provider to reference, create, or update items in your existing vaults using [1Password Secrets Automation](https://1password.com/secrets).

The 1Password Terraform provider supports using both [1Password Connect Server](https://developer.1password.com/docs/secrets-automation/#1password-connect-server)
and [1Password Service Accounts](https://developer.1password.com/docs/secrets-automation/#1password-service-accounts).
To use a service account token, you must install [1Password CLI](https://developer.1password.com/docs/cli) on the machine running Terraform. Refer to the
[Terraform documentation](https://developer.hashicorp.com/terraform/cloud-docs/run/install-software#only-install-standalone-binaries) to learn how to install 1Password CLI on Terraform Cloud.

## Example Usage
and [1Password CLI](https://developer.1password.com/docs/cli).

{{tffile "examples/provider/provider.tf"}}
You must install [1Password CLI](https://developer.1password.com/docs/cli) on the machine running Terraform to use it. Refer to the
[Terraform documentation](https://developer.hashicorp.com/terraform/cloud-docs/run/install-software#only-install-standalone-binaries) to learn how to install 1Password CLI on Terraform Cloud.

{{ .SchemaMarkdown | trimspace }}
## Use with 1Password CLI

## Use with service accounts:
Retry mechanism is implemented when using provider with service accounts. Each retry fast forwards to the [service account rate limit](https://developer.1password.com/docs/service-accounts/rate-limits/).
Retry mechanism is implemented when using the provider with 1Password CLI. The reason for having a retry mechanism is that 1Password doesn't allow parallel modification on the items located in the same vault.
Note that each retry fast forwards to the [service account rate limit](https://developer.1password.com/docs/service-accounts/rate-limits/) if use with service account.

It's recommended to limit the number of parallel resource operations. It can be done by using `-parallelism=n` flag when running `terraform apply`, where `n` is the number of parallel resource operations (the default is `10`).
```
terraform apply `-parallelism=n`
```

The reason of having retry mechanism is that 1Password doesn't allow parallel modification on the items located in the same vault.
### Authenticate CLI with service account

To authenticate CLI with service account, set `service_account_token` in the provider configuration.

### Authenticate the CLI with user account using biometric unlock

To authenticate the CLI with user account using biometric unlock:
1. [Turn on the app integration](https://developer.1password.com/docs/cli/app-integration/#step-1-turn-on-the-app-integration)
2. In the terminal run `op account ls` to find sign-in address or account ID. It will print similar output in the console:
```
URL EMAIL USER ID
acme.dev.com [email protected] HERE_WILL_BE_REAL_USER_ID
acme.prod.com [email protected] HERE_WILL_BE_REAL_USER_ID
```
3. Set `account` in the provider configuration with the `URL` or `USER ID` value from the previous step.
4. When the biometric unlock popup appears while running terraform command, [authenticate it using fingerprint or password](https://developer.1password.com/docs/cli/app-integration/#step-2-enter-any-command-to-sign-in).

## Use with 1Password Connect

To use the provider with 1Password Connect you need to
1. [Deploy your Connect server](https://developer.1password.com/docs/connect/get-started#deployment)
2. Set `url` and `token` in the provider configuration.

## Example Usage

{{tffile "examples/provider/provider.tf"}}

{{ .SchemaMarkdown | trimspace }}

0 comments on commit d5367ac

Please sign in to comment.