From 5276986f8e1989515c8a33a3538285645546e85d Mon Sep 17 00:00:00 2001 From: Maxime Lagresle Date: Tue, 17 Dec 2024 10:01:30 +0100 Subject: [PATCH] regroup operations and transformations --- internal/provider/data_source.go | 42 ----- internal/provider/data_source_attachment.go | 2 +- internal/provider/data_source_folder.go | 2 +- internal/provider/data_source_item_login.go | 2 +- .../provider/data_source_item_secure_note.go | 2 +- .../provider/data_source_org_collection.go | 2 +- internal/provider/data_source_organization.go | 2 +- internal/provider/data_source_project.go | 2 +- internal/provider/data_source_secret.go | 2 +- ...{attachment.go => operation_attachment.go} | 100 +++++----- internal/provider/operation_folder.go | 29 +++ internal/provider/operation_generic.go | 9 + internal/provider/operation_item.go | 21 +++ internal/provider/operation_object.go | 172 ++++++++++++++++++ internal/provider/operation_org_collection.go | 37 ++++ .../{project.go => operation_project.go} | 84 +++------ .../{secret.go => operation_secret.go} | 55 +++--- internal/provider/resource.go | 79 -------- internal/provider/resource_attachment.go | 42 +---- internal/provider/resource_folder.go | 33 +--- internal/provider/resource_item_login.go | 10 +- .../provider/resource_item_secure_note.go | 10 +- internal/provider/resource_org_collection.go | 41 +---- internal/provider/resource_project.go | 10 +- internal/provider/resource_secret.go | 10 +- .../provider/transformation_attachment.go | 32 ++++ .../{object.go => transformation_object.go} | 140 +++----------- internal/provider/transformation_project.go | 45 +++++ internal/provider/transformation_secret.go | 72 ++++++++ 29 files changed, 586 insertions(+), 503 deletions(-) delete mode 100644 internal/provider/data_source.go rename internal/provider/{attachment.go => operation_attachment.go} (76%) create mode 100644 internal/provider/operation_folder.go create mode 100644 internal/provider/operation_generic.go create mode 100644 internal/provider/operation_item.go create mode 100644 internal/provider/operation_object.go create mode 100644 internal/provider/operation_org_collection.go rename internal/provider/{project.go => operation_project.go} (54%) rename internal/provider/{secret.go => operation_secret.go} (81%) delete mode 100644 internal/provider/resource.go create mode 100644 internal/provider/transformation_attachment.go rename internal/provider/{object.go => transformation_object.go} (78%) create mode 100644 internal/provider/transformation_project.go create mode 100644 internal/provider/transformation_secret.go diff --git a/internal/provider/data_source.go b/internal/provider/data_source.go deleted file mode 100644 index cfaed79..0000000 --- a/internal/provider/data_source.go +++ /dev/null @@ -1,42 +0,0 @@ -package provider - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" -) - -func resourceReadDataSourceItem(attrObject models.ObjectType, attrType models.ItemType) passwordManagerOperation { - return func(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - err := d.Set(schema_definition.AttributeType, attrType) - if err != nil { - return diag.FromErr(err) - } - return resourceReadDataSourceObject(attrObject)(ctx, d, bwClient) - } -} - -func resourceReadDataSourceObject(objType models.ObjectType) passwordManagerOperation { - return func(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - d.SetId(d.Get(schema_definition.AttributeID).(string)) - err := d.Set(schema_definition.AttributeObject, objType) - if err != nil { - return diag.FromErr(err) - } - return objectRead(ctx, d, bwClient) - } -} - -func resourceReadDataSourceSecret(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - d.SetId(d.Get(schema_definition.AttributeID).(string)) - return secretRead(ctx, d, bwsClient) -} - -func resourceReadDataSourceProject(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - d.SetId(d.Get(schema_definition.AttributeID).(string)) - return projectRead(ctx, d, bwsClient) -} diff --git a/internal/provider/data_source_attachment.go b/internal/provider/data_source_attachment.go index 5d028c5..c55d290 100644 --- a/internal/provider/data_source_attachment.go +++ b/internal/provider/data_source_attachment.go @@ -8,7 +8,7 @@ import ( func dataSourceAttachment() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get the content on an existing item's attachment.", - ReadContext: withPasswordManager(resourceReadDataSourceAttachment), + ReadContext: withPasswordManager(opAttachmentRead), Schema: map[string]*schema.Schema{ schema_definition.AttributeID: { Description: schema_definition.DescriptionIdentifier, diff --git a/internal/provider/data_source_folder.go b/internal/provider/data_source_folder.go index 130c6eb..28ffa37 100644 --- a/internal/provider/data_source_folder.go +++ b/internal/provider/data_source_folder.go @@ -9,7 +9,7 @@ import ( func dataSourceFolder() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing folder.", - ReadContext: withPasswordManager(resourceReadDataSourceObject(models.ObjectTypeFolder)), + ReadContext: withPasswordManager(opObjectRead(models.ObjectTypeFolder)), Schema: schema_definition.FolderSchema(schema_definition.DataSource), } } diff --git a/internal/provider/data_source_item_login.go b/internal/provider/data_source_item_login.go index 80d67c6..fad5246 100644 --- a/internal/provider/data_source_item_login.go +++ b/internal/provider/data_source_item_login.go @@ -14,7 +14,7 @@ func dataSourceItemLogin() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing login item.", - ReadContext: withPasswordManager(resourceReadDataSourceItem(models.ObjectTypeItem, models.ItemTypeLogin)), + ReadContext: withPasswordManager(opItemRead(models.ObjectTypeItem, models.ItemTypeLogin)), Schema: dataSourceItemLoginSchema, } } diff --git a/internal/provider/data_source_item_secure_note.go b/internal/provider/data_source_item_secure_note.go index b4f66fe..ee1049e 100644 --- a/internal/provider/data_source_item_secure_note.go +++ b/internal/provider/data_source_item_secure_note.go @@ -11,7 +11,7 @@ func dataSourceItemSecureNote() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing secure note item.", - ReadContext: withPasswordManager(resourceReadDataSourceItem(models.ObjectTypeItem, models.ItemTypeSecureNote)), + ReadContext: withPasswordManager(opItemRead(models.ObjectTypeItem, models.ItemTypeSecureNote)), Schema: dataSourceItemSecureNoteSchema, } } diff --git a/internal/provider/data_source_org_collection.go b/internal/provider/data_source_org_collection.go index e0a0c3e..447ad7c 100644 --- a/internal/provider/data_source_org_collection.go +++ b/internal/provider/data_source_org_collection.go @@ -9,7 +9,7 @@ import ( func dataSourceOrgCollection() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing organization collection.", - ReadContext: withPasswordManager(resourceReadDataSourceObject(models.ObjectTypeOrgCollection)), + ReadContext: withPasswordManager(opObjectRead(models.ObjectTypeOrgCollection)), Schema: schema_definition.OrgCollectionSchema(schema_definition.DataSource), } } diff --git a/internal/provider/data_source_organization.go b/internal/provider/data_source_organization.go index da38d30..2b6196c 100644 --- a/internal/provider/data_source_organization.go +++ b/internal/provider/data_source_organization.go @@ -9,7 +9,7 @@ import ( func dataSourceOrganization() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing organization.", - ReadContext: withPasswordManager(resourceReadDataSourceObject(models.ObjectTypeOrganization)), + ReadContext: withPasswordManager(opObjectRead(models.ObjectTypeOrganization)), Schema: schema_definition.OrganizationSchema(), } } diff --git a/internal/provider/data_source_project.go b/internal/provider/data_source_project.go index cb2c3ed..68b1501 100644 --- a/internal/provider/data_source_project.go +++ b/internal/provider/data_source_project.go @@ -10,7 +10,7 @@ func dataSourceProject() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing project.", - ReadContext: withSecretsManager(resourceReadDataSourceProject), + ReadContext: withSecretsManager(opProjectRead), Schema: dataSourceProjectSchema, } } diff --git a/internal/provider/data_source_secret.go b/internal/provider/data_source_secret.go index 6c46393..3803f18 100644 --- a/internal/provider/data_source_secret.go +++ b/internal/provider/data_source_secret.go @@ -10,7 +10,7 @@ func dataSourceSecret() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing secret.", - ReadContext: withSecretsManager(resourceReadDataSourceSecret), + ReadContext: withSecretsManager(opSecretRead), Schema: dataSourceSecretSchema, } } diff --git a/internal/provider/attachment.go b/internal/provider/operation_attachment.go similarity index 76% rename from internal/provider/attachment.go rename to internal/provider/operation_attachment.go index 2bb2c78..520eec2 100644 --- a/internal/provider/attachment.go +++ b/internal/provider/operation_attachment.go @@ -5,9 +5,12 @@ import ( "crypto/sha1" "encoding/hex" "errors" + "fmt" "io" "os" + "strings" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" @@ -15,7 +18,7 @@ import ( "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" ) -func resourceCreateAttachment(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { +func opAttachmentCreate(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { itemId := d.Get(schema_definition.AttributeAttachmentItemID).(string) existingAttachments, err := listExistingAttachments(ctx, bwClient, itemId) @@ -50,74 +53,59 @@ func resourceCreateAttachment(ctx context.Context, d *schema.ResourceData, bwCli return diag.FromErr(attachmentDataFromStruct(d, attachmentsAdded[0])) } -func resourceReadAttachment(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { +func opAttachmentDelete(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { itemId := d.Get(schema_definition.AttributeAttachmentItemID).(string) - obj, err := bwClient.GetObject(ctx, models.Object{ID: itemId, Object: models.ObjectTypeItem}) - if err != nil { - // If the item is not found, we can't simply consider the attachment as - // deleted, because we won't have an item to attach it to. - // This means we don't need a special handling for NotFound errors and - // should just return whatever we get. - return diag.FromErr(err) - } + return diag.FromErr(bwClient.DeleteAttachment(ctx, itemId, d.Id())) +} - for _, attachment := range obj.Attachments { - if attachment.ID == d.Id() { - return diag.FromErr(attachmentDataFromStruct(d, attachment)) - } +func opAttachmentImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + split := strings.Split(d.Id(), "/") + if len(split) != 2 { + return nil, fmt.Errorf("invalid ID specified, should be in the format /: '%s'", d.Id()) } - - // If the item exists but the attachment is not found, we consider the - // attachment as deleted. - d.SetId("") - return diag.Diagnostics{} + d.SetId(split[0]) + d.Set(schema_definition.AttributeAttachmentItemID, split[1]) + return []*schema.ResourceData{d}, nil } -func resourceDeleteAttachment(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { +func opAttachmentRead(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { itemId := d.Get(schema_definition.AttributeAttachmentItemID).(string) - return diag.FromErr(bwClient.DeleteAttachment(ctx, itemId, d.Id())) -} - -func attachmentDataFromStruct(d *schema.ResourceData, attachment models.Attachment) error { - d.SetId(attachment.ID) - - err := d.Set(schema_definition.AttributeAttachmentFileName, attachment.FileName) - if err != nil { - return err - } + attachmentId := d.Get(schema_definition.AttributeID).(string) - err = d.Set(schema_definition.AttributeAttachmentSize, attachment.Size) - if err != nil { - return err - } - err = d.Set(schema_definition.AttributeAttachmentSizeName, attachment.SizeName) + content, err := bwClient.GetAttachment(ctx, itemId, attachmentId) if err != nil { - return err + return diag.FromErr(err) } - err = d.Set(schema_definition.AttributeAttachmentURL, attachment.Url) - if err != nil { - return err - } + d.SetId(attachmentId) - return nil + return diag.FromErr(d.Set(schema_definition.AttributeAttachmentContent, string(content))) } -func resourceReadDataSourceAttachment(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { +func opAttachmentReadIgnoreMissing(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { itemId := d.Get(schema_definition.AttributeAttachmentItemID).(string) - attachmentId := d.Get(schema_definition.AttributeID).(string) - - content, err := bwClient.GetAttachment(ctx, itemId, attachmentId) + obj, err := bwClient.GetObject(ctx, models.Object{ID: itemId, Object: models.ObjectTypeItem}) if err != nil { + // If the item is not found, we can't simply consider the attachment as + // deleted, because we won't have an item to attach it to. + // This means we don't need a special handling for NotFound errors and + // should just return whatever we get. return diag.FromErr(err) } - d.SetId(attachmentId) + for _, attachment := range obj.Attachments { + if attachment.ID == d.Id() { + return diag.FromErr(attachmentDataFromStruct(d, attachment)) + } + } - return diag.FromErr(d.Set(schema_definition.AttributeAttachmentContent, string(content))) + // If the item exists but the attachment is not found, we consider the + // attachment as deleted. + d.SetId("") + return diag.Diagnostics{} } func listExistingAttachments(ctx context.Context, client bitwarden.PasswordManager, itemId string) ([]models.Attachment, error) { @@ -176,3 +164,21 @@ func contentSha1Sum(content string) (string, error) { return hex.EncodeToString(outputChecksum[:]), nil } + +func contentHash(val interface{}) string { + hash, _ := contentSha1Sum(val.(string)) + return hash +} + +func fileHashComputable(val interface{}, _ cty.Path) diag.Diagnostics { + _, err := fileSha1Sum(val.(string)) + if err != nil { + return diag.FromErr(fmt.Errorf("unable to compute hash of file: %w", err)) + } + return diag.Diagnostics{} +} + +func fileHash(val interface{}) string { + hash, _ := fileSha1Sum(val.(string)) + return hash +} diff --git a/internal/provider/operation_folder.go b/internal/provider/operation_folder.go new file mode 100644 index 0000000..c3ef9e7 --- /dev/null +++ b/internal/provider/operation_folder.go @@ -0,0 +1,29 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" +) + +func opFolderCreate(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + err := d.Set(schema_definition.AttributeObject, models.ObjectTypeFolder) + if err != nil { + return diag.FromErr(err) + } + + return objectCreate(ctx, d, bwClient) +} + +func opFolderImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.SetId(d.Id()) + err := d.Set(schema_definition.AttributeObject, models.ObjectTypeFolder) + if err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} diff --git a/internal/provider/operation_generic.go b/internal/provider/operation_generic.go new file mode 100644 index 0000000..3e86eed --- /dev/null +++ b/internal/provider/operation_generic.go @@ -0,0 +1,9 @@ +package provider + +import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + +func resourceImporter(stateContext schema.StateContextFunc) *schema.ResourceImporter { + return &schema.ResourceImporter{ + StateContext: stateContext, + } +} diff --git a/internal/provider/operation_item.go b/internal/provider/operation_item.go new file mode 100644 index 0000000..bcf1676 --- /dev/null +++ b/internal/provider/operation_item.go @@ -0,0 +1,21 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" +) + +func opItemRead(attrObject models.ObjectType, attrType models.ItemType) passwordManagerOperation { + return func(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + err := d.Set(schema_definition.AttributeType, attrType) + if err != nil { + return diag.FromErr(err) + } + return opObjectRead(attrObject)(ctx, d, bwClient) + } +} diff --git a/internal/provider/operation_object.go b/internal/provider/operation_object.go new file mode 100644 index 0000000..9c024ef --- /dev/null +++ b/internal/provider/operation_object.go @@ -0,0 +1,172 @@ +package provider + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/bwcli" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" +) + +type objectOperationFunc func(ctx context.Context, secret models.Object) (*models.Object, error) + +func opObjectCreate(attrObject models.ObjectType, attrType models.ItemType) passwordManagerOperation { + return func(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + err := d.Set(schema_definition.AttributeObject, attrObject) + if err != nil { + return diag.FromErr(err) + } + err = d.Set(schema_definition.AttributeType, attrType) + if err != nil { + return diag.FromErr(err) + } + + return objectCreate(ctx, d, bwClient) + } +} + +func opObjectDelete(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + return diag.FromErr(objectOperation(ctx, d, func(ctx context.Context, secret models.Object) (*models.Object, error) { + return nil, bwClient.DeleteObject(ctx, secret) + })) +} + +func opObjectImport(attrObject models.ObjectType, attrType models.ItemType) schema.StateContextFunc { + return func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.SetId(d.Id()) + err := d.Set(schema_definition.AttributeObject, attrObject) + if err != nil { + return nil, err + } + err = d.Set(schema_definition.AttributeType, attrType) + if err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil + } +} + +func opObjectRead(objType models.ObjectType) passwordManagerOperation { + return func(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + d.SetId(d.Get(schema_definition.AttributeID).(string)) + err := d.Set(schema_definition.AttributeObject, objType) + if err != nil { + return diag.FromErr(err) + } + return objectRead(ctx, d, bwClient) + } +} + +func opObjectReadIgnoreMissing(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + err := objectOperation(ctx, d, func(ctx context.Context, secret models.Object) (*models.Object, error) { + return bwClient.GetObject(ctx, secret) + }) + + if errors.Is(err, models.ErrObjectNotFound) { + d.SetId("") + tflog.Warn(ctx, "Object not found, removing from state") + return diag.Diagnostics{} + } + + if _, exists := d.GetOk(schema_definition.AttributeDeletedDate); exists { + d.SetId("") + tflog.Warn(ctx, "Object was soft deleted, removing from state") + return diag.Diagnostics{} + } + + return diag.FromErr(err) +} + +func opObjectUpdate(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + return diag.FromErr(objectOperation(ctx, d, bwClient.EditObject)) +} + +func objectCreate(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + return diag.FromErr(objectOperation(ctx, d, bwClient.CreateObject)) +} + +func objectRead(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + if _, idProvided := d.GetOk(schema_definition.AttributeID); !idProvided { + return diag.FromErr(objectSearch(ctx, d, bwClient)) + } + + return diag.FromErr(objectOperation(ctx, d, func(ctx context.Context, secret models.Object) (*models.Object, error) { + obj, err := bwClient.GetObject(ctx, secret) + if obj != nil { + // If the object exists but is marked as soft deleted, we return an error, because relying + // on an object in the 'trash' sounds like a bad idea. + if obj.DeletedDate != nil { + return nil, errors.New("object is soft deleted") + } + + if obj.ID != secret.ID { + return nil, errors.New("returned object ID does not match requested object ID") + } + + if obj.Type != secret.Type { + return nil, errors.New("returned object type does not match requested object type") + } + } + + return obj, err + })) +} + +func objectSearch(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) error { + objType, ok := d.GetOk(schema_definition.AttributeObject) + if !ok { + return fmt.Errorf("BUG: object type not set in the resource data") + } + + objs, err := bwClient.ListObjects(ctx, models.ObjectType(objType.(string)), listOptionsFromData(d)...) + if err != nil { + return err + } + + // If the object is an item, also filter by type to avoid returning a login when a secure note is expected. + if models.ObjectType(objType.(string)) == models.ObjectTypeItem { + itemType, ok := d.GetOk(schema_definition.AttributeType) + if !ok { + return fmt.Errorf("BUG: item type not set in the resource data") + } + + objs = bwcli.FilterObjectsByType(objs, models.ItemType(itemType.(int))) + } + + if len(objs) == 0 { + return fmt.Errorf("no object found matching the filter") + } else if len(objs) > 1 { + objects := []string{} + for _, obj := range objs { + objects = append(objects, fmt.Sprintf("%s (%s)", obj.Name, obj.ID)) + } + tflog.Warn(ctx, "Too many objects found", map[string]interface{}{"objects": objects}) + + return fmt.Errorf("too many objects found") + } + + obj := objs[0] + + // If the object exists but is marked as soft deleted, we return an error. This shouldn't happen + // in theory since we never pass the --trash flag to the Bitwarden CLI when listing objects. + if obj.DeletedDate != nil { + return errors.New("object is soft deleted") + } + + return objectDataFromStruct(ctx, d, &obj) +} + +func objectOperation(ctx context.Context, d *schema.ResourceData, operation objectOperationFunc) error { + obj, err := operation(ctx, objectStructFromData(ctx, d)) + if err != nil { + return err + } + + return objectDataFromStruct(ctx, d, obj) +} diff --git a/internal/provider/operation_org_collection.go b/internal/provider/operation_org_collection.go new file mode 100644 index 0000000..3a34bce --- /dev/null +++ b/internal/provider/operation_org_collection.go @@ -0,0 +1,37 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" +) + +func opOrganizationCollectionCreate(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + err := d.Set(schema_definition.AttributeObject, models.ObjectTypeOrgCollection) + if err != nil { + return diag.FromErr(err) + } + + return objectCreate(ctx, d, bwClient) +} + +func opOrganizationCollectionImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + split := strings.Split(d.Id(), "/") + if len(split) != 2 { + return nil, fmt.Errorf("invalid ID specified, should be in the format /: '%s'", d.Id()) + } + d.SetId(split[1]) + d.Set(schema_definition.AttributeOrganizationID, split[0]) + err := d.Set(schema_definition.AttributeObject, models.ObjectTypeOrgCollection) + if err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/internal/provider/project.go b/internal/provider/operation_project.go similarity index 54% rename from internal/provider/project.go rename to internal/provider/operation_project.go index 9e4adc7..a5eab5f 100644 --- a/internal/provider/project.go +++ b/internal/provider/operation_project.go @@ -14,40 +14,25 @@ import ( type projectOperationFunc func(ctx context.Context, secret models.Project) (*models.Project, error) -func resourceCreateProject() secretsManagerOperation { +func opProjectCreate() secretsManagerOperation { return func(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return projectCreate(ctx, d, bwsClient) + return diag.FromErr(projectOperation(ctx, d, bwsClient.CreateProject)) } } -func resourceReadProjectIgnoreMissing(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - err := projectOperation(ctx, d, func(ctx context.Context, project models.Project) (*models.Project, error) { - return bwsClient.GetProject(ctx, project) - }) - - if errors.Is(err, models.ErrObjectNotFound) { - d.SetId("") - tflog.Warn(ctx, "Project not found, removing from state") - return diag.Diagnostics{} - } - - return diag.FromErr(err) -} - -func resourceUpdateProject(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return diag.FromErr(projectOperation(ctx, d, bwsClient.EditProject)) -} -func resourceDeleteProject(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { +func opProjectDelete(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { return diag.FromErr(projectOperation(ctx, d, func(ctx context.Context, project models.Project) (*models.Project, error) { return nil, bwsClient.DeleteProject(ctx, project) })) } -func projectCreate(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return diag.FromErr(projectOperation(ctx, d, bwsClient.CreateProject)) +func opProjectImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.SetId(d.Id()) + return []*schema.ResourceData{d}, nil } -func projectRead(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { +func opProjectRead(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { + d.SetId(d.Get(schema_definition.AttributeID).(string)) return diag.FromErr(projectOperation(ctx, d, func(ctx context.Context, projectReq models.Project) (*models.Project, error) { project, err := bwsClient.GetProject(ctx, projectReq) if project != nil { @@ -60,52 +45,29 @@ func projectRead(ctx context.Context, d *schema.ResourceData, bwsClient bitwarde })) } -func projectOperation(ctx context.Context, d *schema.ResourceData, operation projectOperationFunc) error { - project, err := operation(ctx, projectStructFromData(ctx, d)) - if err != nil { - return err - } - - return projectDataFromStruct(ctx, d, project) -} - -func projectStructFromData(_ context.Context, d *schema.ResourceData) models.Project { - var project models.Project - - project.ID = d.Id() - if v, ok := d.Get(schema_definition.AttributeName).(string); ok { - project.Name = v - } +func opProjectReadIgnoreMissing(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { + err := projectOperation(ctx, d, func(ctx context.Context, project models.Project) (*models.Project, error) { + return bwsClient.GetProject(ctx, project) + }) - if v, ok := d.Get(schema_definition.AttributeOrganizationID).(string); ok { - project.OrganizationID = v + if errors.Is(err, models.ErrObjectNotFound) { + d.SetId("") + tflog.Warn(ctx, "Project not found, removing from state") + return diag.Diagnostics{} } - return project + return diag.FromErr(err) } -func projectDataFromStruct(_ context.Context, d *schema.ResourceData, project *models.Project) error { - if project == nil { - // Project has been deleted - return nil - } - - d.SetId(project.ID) - - err := d.Set(schema_definition.AttributeName, project.Name) - if err != nil { - return err - } +func opProjectUpdate(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { + return diag.FromErr(projectOperation(ctx, d, bwsClient.EditProject)) +} - err = d.Set(schema_definition.AttributeOrganizationID, project.OrganizationID) +func projectOperation(ctx context.Context, d *schema.ResourceData, operation projectOperationFunc) error { + project, err := operation(ctx, projectStructFromData(ctx, d)) if err != nil { return err } - return nil -} - -func resourceImportProject(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - d.SetId(d.Id()) - return []*schema.ResourceData{d}, nil + return projectDataFromStruct(ctx, d, project) } diff --git a/internal/provider/secret.go b/internal/provider/operation_secret.go similarity index 81% rename from internal/provider/secret.go rename to internal/provider/operation_secret.go index 69a51fc..acd06fc 100644 --- a/internal/provider/secret.go +++ b/internal/provider/operation_secret.go @@ -15,41 +15,25 @@ import ( type secretOperationFunc func(ctx context.Context, secret models.Secret) (*models.Secret, error) -func resourceCreateSecret() secretsManagerOperation { +func opSecretCreate() secretsManagerOperation { return func(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return secretCreate(ctx, d, bwsClient) + return diag.FromErr(secretOperation(ctx, d, bwsClient.CreateSecret)) } } -func resourceReadSecretIgnoreMissing(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - err := secretOperation(ctx, d, func(ctx context.Context, secret models.Secret) (*models.Secret, error) { - return bwsClient.GetSecret(ctx, secret) - }) - - if errors.Is(err, models.ErrObjectNotFound) { - d.SetId("") - tflog.Warn(ctx, "Secret not found, removing from state") - return diag.Diagnostics{} - } - - return diag.FromErr(err) -} - -func resourceUpdateSecret(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return diag.FromErr(secretOperation(ctx, d, bwsClient.EditSecret)) -} - -func resourceDeleteSecret(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { +func opSecretDelete(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { return diag.FromErr(secretOperation(ctx, d, func(ctx context.Context, secret models.Secret) (*models.Secret, error) { return nil, bwsClient.DeleteSecret(ctx, secret) })) } -func secretCreate(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return diag.FromErr(secretOperation(ctx, d, bwsClient.CreateSecret)) +func opSecretImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + d.SetId(d.Id()) + return []*schema.ResourceData{d}, nil } -func secretRead(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { +func opSecretRead(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { + d.SetId(d.Get(schema_definition.AttributeID).(string)) if _, idProvided := d.GetOk(schema_definition.AttributeID); !idProvided { return diag.FromErr(secretSearch(ctx, d, bwsClient)) } @@ -66,6 +50,24 @@ func secretRead(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden })) } +func opSecretReadIgnoreMissing(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { + err := secretOperation(ctx, d, func(ctx context.Context, secret models.Secret) (*models.Secret, error) { + return bwsClient.GetSecret(ctx, secret) + }) + + if errors.Is(err, models.ErrObjectNotFound) { + d.SetId("") + tflog.Warn(ctx, "Secret not found, removing from state") + return diag.Diagnostics{} + } + + return diag.FromErr(err) +} + +func opSecretUpdate(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { + return diag.FromErr(secretOperation(ctx, d, bwsClient.EditSecret)) +} + func secretSearch(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) error { secretKey, ok := d.GetOk(schema_definition.AttributeKey) if !ok { @@ -151,8 +153,3 @@ func secretDataFromStruct(_ context.Context, d *schema.ResourceData, secret *mod return nil } - -func resourceImportSecret(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - d.SetId(d.Id()) - return []*schema.ResourceData{d}, nil -} diff --git a/internal/provider/resource.go b/internal/provider/resource.go deleted file mode 100644 index 7f5be32..0000000 --- a/internal/provider/resource.go +++ /dev/null @@ -1,79 +0,0 @@ -package provider - -import ( - "context" - "errors" - - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" -) - -func resourceCreateObject(attrObject models.ObjectType, attrType models.ItemType) passwordManagerOperation { - return func(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - err := d.Set(schema_definition.AttributeObject, attrObject) - if err != nil { - return diag.FromErr(err) - } - err = d.Set(schema_definition.AttributeType, attrType) - if err != nil { - return diag.FromErr(err) - } - - return objectCreate(ctx, d, bwClient) - } -} - -func resourceReadObjectIgnoreMissing(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - err := objectOperation(ctx, d, func(ctx context.Context, secret models.Object) (*models.Object, error) { - return bwClient.GetObject(ctx, secret) - }) - - if errors.Is(err, models.ErrObjectNotFound) { - d.SetId("") - tflog.Warn(ctx, "Object not found, removing from state") - return diag.Diagnostics{} - } - - if _, exists := d.GetOk(schema_definition.AttributeDeletedDate); exists { - d.SetId("") - tflog.Warn(ctx, "Object was soft deleted, removing from state") - return diag.Diagnostics{} - } - - return diag.FromErr(err) -} - -func resourceUpdateObject(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - return diag.FromErr(objectOperation(ctx, d, bwClient.EditObject)) -} - -func resourceDeleteObject(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - return diag.FromErr(objectOperation(ctx, d, func(ctx context.Context, secret models.Object) (*models.Object, error) { - return nil, bwClient.DeleteObject(ctx, secret) - })) -} - -func resourceImportObject(attrObject models.ObjectType, attrType models.ItemType) schema.StateContextFunc { - return func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - d.SetId(d.Id()) - err := d.Set(schema_definition.AttributeObject, attrObject) - if err != nil { - return nil, err - } - err = d.Set(schema_definition.AttributeType, attrType) - if err != nil { - return nil, err - } - return []*schema.ResourceData{d}, nil - } -} - -func resourceImporter(stateContext schema.StateContextFunc) *schema.ResourceImporter { - return &schema.ResourceImporter{ - StateContext: stateContext, - } -} diff --git a/internal/provider/resource_attachment.go b/internal/provider/resource_attachment.go index e7ac028..ca6479c 100644 --- a/internal/provider/resource_attachment.go +++ b/internal/provider/resource_attachment.go @@ -1,12 +1,6 @@ package provider import ( - "context" - "fmt" - "strings" - - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" ) @@ -55,39 +49,11 @@ func resourceAttachment() *schema.Resource { return &schema.Resource{ Description: "Manages an item attachment.", - CreateContext: withPasswordManager(resourceCreateAttachment), - ReadContext: withPasswordManager(resourceReadAttachment), - DeleteContext: withPasswordManager(resourceDeleteAttachment), - Importer: resourceImporter(resourceImportAttachment), + CreateContext: withPasswordManager(opAttachmentCreate), + ReadContext: withPasswordManager(opAttachmentReadIgnoreMissing), + DeleteContext: withPasswordManager(opAttachmentDelete), + Importer: resourceImporter(opAttachmentImport), Schema: resourceAttachmentSchema, } } - -func resourceImportAttachment(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - split := strings.Split(d.Id(), "/") - if len(split) != 2 { - return nil, fmt.Errorf("invalid ID specified, should be in the format /: '%s'", d.Id()) - } - d.SetId(split[0]) - d.Set(schema_definition.AttributeAttachmentItemID, split[1]) - return []*schema.ResourceData{d}, nil -} - -func contentHash(val interface{}) string { - hash, _ := contentSha1Sum(val.(string)) - return hash -} - -func fileHashComputable(val interface{}, _ cty.Path) diag.Diagnostics { - _, err := fileSha1Sum(val.(string)) - if err != nil { - return diag.FromErr(fmt.Errorf("unable to compute hash of file: %w", err)) - } - return diag.Diagnostics{} -} - -func fileHash(val interface{}) string { - hash, _ := fileSha1Sum(val.(string)) - return hash -} diff --git a/internal/provider/resource_folder.go b/internal/provider/resource_folder.go index f7f2c4f..349700c 100644 --- a/internal/provider/resource_folder.go +++ b/internal/provider/resource_folder.go @@ -1,12 +1,7 @@ package provider import ( - "context" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" ) @@ -14,30 +9,12 @@ func resourceFolder() *schema.Resource { return &schema.Resource{ Description: "Manages a folder.", - CreateContext: withPasswordManager(resourceCreateFolder), - ReadContext: withPasswordManager(resourceReadObjectIgnoreMissing), - UpdateContext: withPasswordManager(resourceUpdateObject), - DeleteContext: withPasswordManager(resourceDeleteObject), - Importer: resourceImporter(resourceImportFolder), + CreateContext: withPasswordManager(opFolderCreate), + ReadContext: withPasswordManager(opObjectReadIgnoreMissing), + UpdateContext: withPasswordManager(opObjectUpdate), + DeleteContext: withPasswordManager(opObjectDelete), + Importer: resourceImporter(opFolderImport), Schema: schema_definition.FolderSchema(schema_definition.Resource), } } - -func resourceCreateFolder(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - err := d.Set(schema_definition.AttributeObject, models.ObjectTypeFolder) - if err != nil { - return diag.FromErr(err) - } - - return objectCreate(ctx, d, bwClient) -} - -func resourceImportFolder(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - d.SetId(d.Id()) - err := d.Set(schema_definition.AttributeObject, models.ObjectTypeFolder) - if err != nil { - return nil, err - } - return []*schema.ResourceData{d}, nil -} diff --git a/internal/provider/resource_item_login.go b/internal/provider/resource_item_login.go index ee783ec..8b01747 100644 --- a/internal/provider/resource_item_login.go +++ b/internal/provider/resource_item_login.go @@ -14,11 +14,11 @@ func resourceItemLogin() *schema.Resource { return &schema.Resource{ Description: "Manages a login item.", - CreateContext: withPasswordManager(resourceCreateObject(models.ObjectTypeItem, models.ItemTypeLogin)), - ReadContext: withPasswordManager(resourceReadObjectIgnoreMissing), - UpdateContext: withPasswordManager(resourceUpdateObject), - DeleteContext: withPasswordManager(resourceDeleteObject), - Importer: resourceImporter(resourceImportObject(models.ObjectTypeItem, models.ItemTypeLogin)), + CreateContext: withPasswordManager(opObjectCreate(models.ObjectTypeItem, models.ItemTypeLogin)), + ReadContext: withPasswordManager(opObjectReadIgnoreMissing), + UpdateContext: withPasswordManager(opObjectUpdate), + DeleteContext: withPasswordManager(opObjectDelete), + Importer: resourceImporter(opObjectImport(models.ObjectTypeItem, models.ItemTypeLogin)), Schema: dataSourceItemSecureNoteSchema, } } diff --git a/internal/provider/resource_item_secure_note.go b/internal/provider/resource_item_secure_note.go index 7e3e42c..4f67afb 100644 --- a/internal/provider/resource_item_secure_note.go +++ b/internal/provider/resource_item_secure_note.go @@ -11,11 +11,11 @@ func resourceItemSecureNote() *schema.Resource { return &schema.Resource{ Description: "Manages a secure note item.", - CreateContext: withPasswordManager(resourceCreateObject(models.ObjectTypeItem, models.ItemTypeSecureNote)), - ReadContext: withPasswordManager(resourceReadObjectIgnoreMissing), - UpdateContext: withPasswordManager(resourceUpdateObject), - DeleteContext: withPasswordManager(resourceDeleteObject), - Importer: resourceImporter(resourceImportObject(models.ObjectTypeItem, models.ItemTypeSecureNote)), + CreateContext: withPasswordManager(opObjectCreate(models.ObjectTypeItem, models.ItemTypeSecureNote)), + ReadContext: withPasswordManager(opObjectReadIgnoreMissing), + UpdateContext: withPasswordManager(opObjectUpdate), + DeleteContext: withPasswordManager(opObjectDelete), + Importer: resourceImporter(opObjectImport(models.ObjectTypeItem, models.ItemTypeSecureNote)), Schema: dataSourceItemSecureNoteSchema, } } diff --git a/internal/provider/resource_org_collection.go b/internal/provider/resource_org_collection.go index 637f465..ffc0c8b 100644 --- a/internal/provider/resource_org_collection.go +++ b/internal/provider/resource_org_collection.go @@ -1,14 +1,7 @@ package provider import ( - "context" - "fmt" - "strings" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" ) @@ -16,36 +9,12 @@ func resourceOrgCollection() *schema.Resource { return &schema.Resource{ Description: "Manages an organization collection.", - CreateContext: withPasswordManager(resourceCreateOrgCollection), - ReadContext: withPasswordManager(resourceReadObjectIgnoreMissing), - UpdateContext: withPasswordManager(resourceUpdateObject), - DeleteContext: withPasswordManager(resourceDeleteObject), - Importer: resourceImporter(resourceImportOrgCollection), + CreateContext: withPasswordManager(opOrganizationCollectionCreate), + ReadContext: withPasswordManager(opObjectReadIgnoreMissing), + UpdateContext: withPasswordManager(opObjectUpdate), + DeleteContext: withPasswordManager(opObjectDelete), + Importer: resourceImporter(opOrganizationCollectionImport), Schema: schema_definition.OrgCollectionSchema(schema_definition.Resource), } } - -func resourceCreateOrgCollection(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - err := d.Set(schema_definition.AttributeObject, models.ObjectTypeOrgCollection) - if err != nil { - return diag.FromErr(err) - } - - return objectCreate(ctx, d, bwClient) -} - -func resourceImportOrgCollection(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - split := strings.Split(d.Id(), "/") - if len(split) != 2 { - return nil, fmt.Errorf("invalid ID specified, should be in the format /: '%s'", d.Id()) - } - d.SetId(split[1]) - d.Set(schema_definition.AttributeOrganizationID, split[0]) - err := d.Set(schema_definition.AttributeObject, models.ObjectTypeOrgCollection) - if err != nil { - return nil, err - } - - return []*schema.ResourceData{d}, nil -} diff --git a/internal/provider/resource_project.go b/internal/provider/resource_project.go index 71acdbd..21d7a77 100644 --- a/internal/provider/resource_project.go +++ b/internal/provider/resource_project.go @@ -10,11 +10,11 @@ func resourceProject() *schema.Resource { return &schema.Resource{ Description: "Manages a Project.", - CreateContext: withSecretsManager(resourceCreateProject()), - ReadContext: withSecretsManager(resourceReadProjectIgnoreMissing), - UpdateContext: withSecretsManager(resourceUpdateProject), - DeleteContext: withSecretsManager(resourceDeleteProject), + CreateContext: withSecretsManager(opProjectCreate()), + ReadContext: withSecretsManager(opProjectReadIgnoreMissing), + UpdateContext: withSecretsManager(opProjectUpdate), + DeleteContext: withSecretsManager(opProjectDelete), Schema: resourceProjectSchema, - Importer: resourceImporter(resourceImportProject), + Importer: resourceImporter(opProjectImport), } } diff --git a/internal/provider/resource_secret.go b/internal/provider/resource_secret.go index 5fef2d1..ec29808 100644 --- a/internal/provider/resource_secret.go +++ b/internal/provider/resource_secret.go @@ -10,11 +10,11 @@ func resourceSecret() *schema.Resource { return &schema.Resource{ Description: "Manages a secret.", - CreateContext: withSecretsManager(resourceCreateSecret()), - ReadContext: withSecretsManager(resourceReadSecretIgnoreMissing), - UpdateContext: withSecretsManager(resourceUpdateSecret), - DeleteContext: withSecretsManager(resourceDeleteSecret), + CreateContext: withSecretsManager(opSecretCreate()), + ReadContext: withSecretsManager(opSecretReadIgnoreMissing), + UpdateContext: withSecretsManager(opSecretUpdate), + DeleteContext: withSecretsManager(opSecretDelete), Schema: resourceSecretSchema, - Importer: resourceImporter(resourceImportSecret), + Importer: resourceImporter(opSecretImport), } } diff --git a/internal/provider/transformation_attachment.go b/internal/provider/transformation_attachment.go new file mode 100644 index 0000000..cd14572 --- /dev/null +++ b/internal/provider/transformation_attachment.go @@ -0,0 +1,32 @@ +package provider + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" +) + +func attachmentDataFromStruct(d *schema.ResourceData, attachment models.Attachment) error { + d.SetId(attachment.ID) + + err := d.Set(schema_definition.AttributeAttachmentFileName, attachment.FileName) + if err != nil { + return err + } + + err = d.Set(schema_definition.AttributeAttachmentSize, attachment.Size) + if err != nil { + return err + } + err = d.Set(schema_definition.AttributeAttachmentSizeName, attachment.SizeName) + if err != nil { + return err + } + + err = d.Set(schema_definition.AttributeAttachmentURL, attachment.Url) + if err != nil { + return err + } + + return nil +} diff --git a/internal/provider/object.go b/internal/provider/transformation_object.go similarity index 78% rename from internal/provider/object.go rename to internal/provider/transformation_object.go index ba1ed38..129255f 100644 --- a/internal/provider/object.go +++ b/internal/provider/transformation_object.go @@ -2,129 +2,14 @@ package provider import ( "context" - "errors" - "fmt" "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/bwcli" "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" ) -type objectOperationFunc func(ctx context.Context, secret models.Object) (*models.Object, error) - -func objectCreate(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - return diag.FromErr(objectOperation(ctx, d, bwClient.CreateObject)) -} - -func objectRead(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - if _, idProvided := d.GetOk(schema_definition.AttributeID); !idProvided { - return diag.FromErr(objectSearch(ctx, d, bwClient)) - } - - return diag.FromErr(objectOperation(ctx, d, func(ctx context.Context, secret models.Object) (*models.Object, error) { - obj, err := bwClient.GetObject(ctx, secret) - if obj != nil { - // If the object exists but is marked as soft deleted, we return an error, because relying - // on an object in the 'trash' sounds like a bad idea. - if obj.DeletedDate != nil { - return nil, errors.New("object is soft deleted") - } - - if obj.ID != secret.ID { - return nil, errors.New("returned object ID does not match requested object ID") - } - - if obj.Type != secret.Type { - return nil, errors.New("returned object type does not match requested object type") - } - } - - return obj, err - })) -} - -func objectSearch(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) error { - objType, ok := d.GetOk(schema_definition.AttributeObject) - if !ok { - return fmt.Errorf("BUG: object type not set in the resource data") - } - - objs, err := bwClient.ListObjects(ctx, models.ObjectType(objType.(string)), listOptionsFromData(d)...) - if err != nil { - return err - } - - // If the object is an item, also filter by type to avoid returning a login when a secure note is expected. - if models.ObjectType(objType.(string)) == models.ObjectTypeItem { - itemType, ok := d.GetOk(schema_definition.AttributeType) - if !ok { - return fmt.Errorf("BUG: item type not set in the resource data") - } - - objs = bwcli.FilterObjectsByType(objs, models.ItemType(itemType.(int))) - } - - if len(objs) == 0 { - return fmt.Errorf("no object found matching the filter") - } else if len(objs) > 1 { - objects := []string{} - for _, obj := range objs { - objects = append(objects, fmt.Sprintf("%s (%s)", obj.Name, obj.ID)) - } - tflog.Warn(ctx, "Too many objects found", map[string]interface{}{"objects": objects}) - - return fmt.Errorf("too many objects found") - } - - obj := objs[0] - - // If the object exists but is marked as soft deleted, we return an error. This shouldn't happen - // in theory since we never pass the --trash flag to the Bitwarden CLI when listing objects. - if obj.DeletedDate != nil { - return errors.New("object is soft deleted") - } - - return objectDataFromStruct(ctx, d, &obj) -} - -func listOptionsFromData(d *schema.ResourceData) []bitwarden.ListObjectsOption { - filters := []bitwarden.ListObjectsOption{} - - filterMap := map[string]bitwarden.ListObjectsOptionGenerator{ - schema_definition.AttributeFilterSearch: bitwarden.WithSearch, - schema_definition.AttributeFilterCollectionId: bitwarden.WithCollectionID, - schema_definition.AttributeOrganizationID: bitwarden.WithOrganizationID, - schema_definition.AttributeFilterFolderID: bitwarden.WithFolderID, - schema_definition.AttributeFilterOrganizationID: bitwarden.WithOrganizationID, - schema_definition.AttributeFilterURL: bitwarden.WithUrl, - } - - for attribute, optionFunc := range filterMap { - v, ok := d.GetOk(attribute) - if !ok { - continue - } - - if v, ok := v.(string); ok && len(v) > 0 { - filters = append(filters, optionFunc(v)) - } - } - return filters -} - -func objectOperation(ctx context.Context, d *schema.ResourceData, operation objectOperationFunc) error { - obj, err := operation(ctx, objectStructFromData(ctx, d)) - if err != nil { - return err - } - - return objectDataFromStruct(ctx, d, obj) -} - func objectDataFromStruct(ctx context.Context, d *schema.ResourceData, obj *models.Object) error { if obj == nil { // Object has been deleted @@ -481,3 +366,28 @@ const ( URIMatchRegExp URIMatchStr = "regexp" URIMatchNever URIMatchStr = "never" ) + +func listOptionsFromData(d *schema.ResourceData) []bitwarden.ListObjectsOption { + filters := []bitwarden.ListObjectsOption{} + + filterMap := map[string]bitwarden.ListObjectsOptionGenerator{ + schema_definition.AttributeFilterSearch: bitwarden.WithSearch, + schema_definition.AttributeFilterCollectionId: bitwarden.WithCollectionID, + schema_definition.AttributeOrganizationID: bitwarden.WithOrganizationID, + schema_definition.AttributeFilterFolderID: bitwarden.WithFolderID, + schema_definition.AttributeFilterOrganizationID: bitwarden.WithOrganizationID, + schema_definition.AttributeFilterURL: bitwarden.WithUrl, + } + + for attribute, optionFunc := range filterMap { + v, ok := d.GetOk(attribute) + if !ok { + continue + } + + if v, ok := v.(string); ok && len(v) > 0 { + filters = append(filters, optionFunc(v)) + } + } + return filters +} diff --git a/internal/provider/transformation_project.go b/internal/provider/transformation_project.go new file mode 100644 index 0000000..b816c30 --- /dev/null +++ b/internal/provider/transformation_project.go @@ -0,0 +1,45 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" +) + +func projectStructFromData(_ context.Context, d *schema.ResourceData) models.Project { + var project models.Project + + project.ID = d.Id() + if v, ok := d.Get(schema_definition.AttributeName).(string); ok { + project.Name = v + } + + if v, ok := d.Get(schema_definition.AttributeOrganizationID).(string); ok { + project.OrganizationID = v + } + + return project +} + +func projectDataFromStruct(_ context.Context, d *schema.ResourceData, project *models.Project) error { + if project == nil { + // Project has been deleted + return nil + } + + d.SetId(project.ID) + + err := d.Set(schema_definition.AttributeName, project.Name) + if err != nil { + return err + } + + err = d.Set(schema_definition.AttributeOrganizationID, project.OrganizationID) + if err != nil { + return err + } + + return nil +} diff --git a/internal/provider/transformation_secret.go b/internal/provider/transformation_secret.go new file mode 100644 index 0000000..cd225ef --- /dev/null +++ b/internal/provider/transformation_secret.go @@ -0,0 +1,72 @@ +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" +) + +func secretStructFromData(_ context.Context, d *schema.ResourceData) models.Secret { + var secret models.Secret + + secret.ID = d.Id() + if v, ok := d.Get(schema_definition.AttributeKey).(string); ok { + secret.Key = v + } + + if v, ok := d.Get(schema_definition.AttributeValue).(string); ok { + secret.Value = v + } + + if v, ok := d.Get(schema_definition.AttributeNote).(string); ok { + secret.Note = v + } + + if v, ok := d.Get(schema_definition.AttributeOrganizationID).(string); ok { + secret.OrganizationID = v + } + + if v, ok := d.Get(schema_definition.AttributeProjectID).(string); ok { + secret.ProjectID = v + } + + return secret +} + +func secretDataFromStruct(_ context.Context, d *schema.ResourceData, secret *models.Secret) error { + if secret == nil { + // Secret has been deleted + return nil + } + + d.SetId(secret.ID) + + err := d.Set(schema_definition.AttributeKey, secret.Key) + if err != nil { + return err + } + + err = d.Set(schema_definition.AttributeValue, secret.Value) + if err != nil { + return err + } + + err = d.Set(schema_definition.AttributeNote, secret.Note) + if err != nil { + return err + } + + err = d.Set(schema_definition.AttributeOrganizationID, secret.OrganizationID) + if err != nil { + return err + } + + err = d.Set(schema_definition.AttributeProjectID, secret.ProjectID) + if err != nil { + return err + } + + return nil +}