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

Add onepassword_item_share Resource Type #205

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
47 changes: 47 additions & 0 deletions docs/resources/item_share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "onepassword_item_share Resource - onepassword"
subcategory: ""
description: |-
A 1Password Item Share URL.

Note: Sharing at item is only supported by the 1Password CLI, and therefore, only by service and user accounts.Attempting to create this resource when using 1Password Connect Server will result in an error.
---

# onepassword_item_share (Resource)

A 1Password Item Share URL.

> **Note:** Sharing at item is only supported by the 1Password CLI, and therefore, only by service and user accounts.
Attempting to create this resource when using 1Password Connect Server will result in an error.

## Example Usage

```terraform
resource "onepassword_item_share" "example" {
vault = "your-vault-id"
item = "your-item-id"

emails = "[email protected],[email protected]"
expires_in = "3d"
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `item` (String) The UUID of the item. Item identifiers are unique within a specific vault.
- `vault` (String) The UUID of the vault the item is in.

### Optional

- `emails` (String) Comma-separated list of emails to allow to access the item
- `expires_in` (String) The time until the share expires. Must be a number followed by a time unit (s, m, h, d, w). Not valid when `view_once` is set to `true`
- `view_once` (Boolean) Whether the share should be viewable only once. Not valid when `expires_in` is set

### Read-Only

- `id` (String) The Terraform resource identifier for this item in the format `vaults/<vault_id>/items/<item_id>`.
- `share_url` (String) The item share URL
7 changes: 7 additions & 0 deletions examples/resources/onepassword_item_share/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
resource "onepassword_item_share" "example" {
vault = "your-vault-id"
item = "your-item-id"

emails = "[email protected],[email protected]"
expires_in = "3d"
}
31 changes: 31 additions & 0 deletions internal/onepassword/cli/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,37 @@ func (op *OP) delete(ctx context.Context, item *onepassword.Item, vaultUuid stri
return nil, op.execJson(ctx, nil, nil, p("item"), p("delete"), p(item.ID), f("vault", vaultUuid))
}

func (op *OP) ShareItem(ctx context.Context, itemUuid string, vaultUuid string, emails string, expires_in string, view_once bool) (*string, error) {
versionErr := op.checkCliVersion(ctx)
if versionErr != nil {
return nil, versionErr
}
return op.share(ctx, itemUuid, vaultUuid, emails, expires_in, view_once)
}

func (op *OP) share(ctx context.Context, itemUuid string, vaultUuid string, emails string, expires_in string, view_once bool) (*string, error) {
args := []opArg{p("item"), p("share"), p(itemUuid), f("vault", vaultUuid)}

if emails != "" {
args = append(args, f("emails", emails))
}

if view_once {
args = append(args, p("--view-once"))
} else if expires_in != "" {
args = append(args, f("expires-in", expires_in))
}

result, err := op.execRaw(ctx, nil, args...)

if err != nil {
return nil, err
}

res := strings.TrimSpace(string(result))
return &res, nil
}

func (op *OP) GetFileContent(ctx context.Context, file *onepassword.File, itemUuid, vaultUuid string) ([]byte, error) {
versionErr := op.checkCliVersion(ctx)
if versionErr != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/onepassword/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Client interface {
CreateItem(ctx context.Context, item *onepassword.Item, vaultUuid string) (*onepassword.Item, error)
UpdateItem(ctx context.Context, item *onepassword.Item, vaultUuid string) (*onepassword.Item, error)
DeleteItem(ctx context.Context, item *onepassword.Item, vaultUuid string) error
ShareItem(ctx context.Context, itemUuid string, vaultUuid string, emails string, expires_in string, view_once bool) (*string, error)
GetFileContent(ctx context.Context, file *onepassword.File, itemUUid, vaultUuid string) ([]byte, error)
}

Expand Down
5 changes: 5 additions & 0 deletions internal/onepassword/connect/connect_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package connect

import (
"context"
"errors"

"github.com/1Password/connect-sdk-go/connect"
"github.com/1Password/connect-sdk-go/onepassword"
Expand Down Expand Up @@ -39,6 +40,10 @@ func (c *Client) DeleteItem(_ context.Context, item *onepassword.Item, vaultUuid
return c.connectClient.DeleteItem(item, vaultUuid)
}

func (w *Client) ShareItem(_ context.Context, itemUuid string, vaultUUID string, emails string, expires_in string, view_once bool) (*string, error) {
return nil, errors.New("ShareItem is not implemented in Connect Server")
}

func (w *Client) GetFileContent(_ context.Context, file *onepassword.File, itemUUID, vaultUUID string) ([]byte, error) {
return w.connectClient.GetFileContent(file)
}
Expand Down
210 changes: 210 additions & 0 deletions internal/provider/onepassword_item_share_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package provider

import (
"context"
"fmt"
"regexp"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"

"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"

"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"

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

// Ensure provider defined types fully satisfy framework interfaces.
var _ resource.Resource = &OnePasswordItemShareResource{}
var _ resource.ResourceWithImportState = &OnePasswordItemShareResource{}

func NewOnePasswordItemShareResource() resource.Resource {
return &OnePasswordItemShareResource{}
}

// OnePasswordItemShareResource defines the resource implementation.
type OnePasswordItemShareResource struct {
client onepassword.Client
}

// OnePasswordItemShareResourceModel describes the resource data model.
type OnePasswordItemShareResourceModel struct {
ID types.String `tfsdk:"id"`
Item types.String `tfsdk:"item"`
Vault types.String `tfsdk:"vault"`
Emails types.String `tfsdk:"emails"`
ExpiresIn types.String `tfsdk:"expires_in"`
ViewOnce types.Bool `tfsdk:"view_once"`
ShareURL types.String `tfsdk:"share_url"`
}

func (r *OnePasswordItemShareResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_item_share"
}

func (r *OnePasswordItemShareResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
// This description is used by the documentation generator and the language server.
MarkdownDescription: `
A 1Password Item Share URL.

> **Note:** Sharing at item is only supported by the 1Password CLI, and therefore, only by service and user accounts.
Attempting to create this resource when using 1Password Connect Server will result in an error.`,

Attributes: map[string]schema.Attribute{
// "id": schema.StringAttribute{
// MarkdownDescription: terraformItemIDDescription,
// Computed: true,
// },
"id": schema.StringAttribute{
MarkdownDescription: terraformItemIDDescription,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"item": schema.StringAttribute{
MarkdownDescription: itemUUIDDescription,
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"vault": schema.StringAttribute{
MarkdownDescription: vaultUUIDDescription,
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"emails": schema.StringAttribute{
MarkdownDescription: "Comma-separated list of emails to allow to access the item",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,},\s*)*([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$`),
"must be a comma-separated list of valid email addresses",
),
},
},
"expires_in": schema.StringAttribute{
MarkdownDescription: "The time until the share expires. Must be a number followed by a time unit (s, m, h, d, w). Not valid when `view_once` is set to `true`",
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.RegexMatches(
regexp.MustCompile(`^\d+[smhdw]$`),
"must be a number followed by a time unit (s, m, h, d, w)",
),
},
},
"view_once": schema.BoolAttribute{
MarkdownDescription: "Whether the share should be viewable only once. Not valid when `expires_in` is set",
Optional: true,
Computed: true,
Default: booldefault.StaticBool(false),
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
},
"share_url": schema.StringAttribute{
MarkdownDescription: "The item share URL",
Computed: true,
},
},
}
}

func (r *OnePasswordItemShareResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}

client, ok := req.ProviderData.(onepassword.Client)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected onepassword.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)

return
}

r.client = client
}

func (r *OnePasswordItemShareResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data OnePasswordItemShareResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

shareUrl, err := r.client.ShareItem(ctx, data.Item.ValueString(), data.Vault.ValueString(), data.Emails.ValueString(), data.ExpiresIn.ValueString(), data.ViewOnce.ValueBool())
if err != nil {
resp.Diagnostics.AddError("1Password Item Share create error", fmt.Sprintf("Error creating 1Password item share, got error %s", err))
return
}

data.ShareURL = setStringValue(*shareUrl)

data.ID = setStringValue(fmt.Sprintf("vault/%s/item/%s/share/%s/%s/%t", data.Item.ValueString(), data.Vault.ValueString(), data.Emails.ValueString(), data.ExpiresIn.ValueString(), data.ViewOnce.ValueBool()))

// Write logs using the tflog package
// Documentation: https://terraform.io/plugin/log
tflog.Trace(ctx, "created a resource")

// Save data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

// The 1Password Item Share resource does not support reading, updating, deleting, or importing.
//
// These methods are implemented to satisfy the interface requirements, but they are not used, and no warnings will be output.
func (r *OnePasswordItemShareResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
}

func (r *OnePasswordItemShareResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
}

func (r *OnePasswordItemShareResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
}

func (r *OnePasswordItemShareResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
}

func (r *OnePasswordItemShareResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
var data OnePasswordItemShareResourceModel

resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

if data.ViewOnce.ValueBool() && !data.ExpiresIn.IsNull() {
resp.Diagnostics.AddAttributeError(
path.Root("attribute_two"),
"Conflicting Attributes",
"The expiration cannot be set when the share is only viewable once",
)
}
}
3 changes: 3 additions & 0 deletions internal/provider/onepassword_item_share_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package provider

// Tests are not written for op cli-only commands yet, and item sharing is not supported in Connect Server.
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ func (p *OnePasswordProvider) Configure(ctx context.Context, req provider.Config
func (p *OnePasswordProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewOnePasswordItemResource,
NewOnePasswordItemShareResource,
}
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading