From a59ce685ead7bd0f11c9125cf2e0500d2a44c6f4 Mon Sep 17 00:00:00 2001 From: Maxime Lagresle Date: Wed, 18 Dec 2024 08:28:29 +0100 Subject: [PATCH] prepare for new dedicated models (e.g. folders) (#204) --- internal/bitwarden/bwcli/password_manager.go | 21 ++- internal/bitwarden/client_options.go | 9 + .../embedded/password_manager_base.go | 7 + internal/bitwarden/models/password_manager.go | 1 + internal/provider/data_source_folder.go | 3 +- internal/provider/data_source_item_login.go | 2 +- .../provider/data_source_item_secure_note.go | 2 +- .../provider/data_source_org_collection.go | 3 +- internal/provider/data_source_organization.go | 3 +- internal/provider/operation_folder.go | 40 +++- internal/provider/operation_generic.go | 86 ++++++++- internal/provider/operation_item.go | 76 +++++++- internal/provider/operation_object.go | 173 ------------------ internal/provider/operation_org_collection.go | 39 +++- internal/provider/operation_organization.go | 27 +++ internal/provider/operation_project.go | 49 +---- internal/provider/operation_secret.go | 49 +---- internal/provider/resource_folder.go | 6 +- internal/provider/resource_item_login.go | 10 +- .../provider/resource_item_secure_note.go | 10 +- internal/provider/resource_org_collection.go | 6 +- internal/provider/resource_project.go | 2 +- internal/provider/resource_secret.go | 2 +- .../transformation/transformation_object.go | 6 + 24 files changed, 338 insertions(+), 294 deletions(-) delete mode 100644 internal/provider/operation_object.go create mode 100644 internal/provider/operation_organization.go diff --git a/internal/bitwarden/bwcli/password_manager.go b/internal/bitwarden/bwcli/password_manager.go index c054f98..32d3b8d 100644 --- a/internal/bitwarden/bwcli/password_manager.go +++ b/internal/bitwarden/bwcli/password_manager.go @@ -177,6 +177,8 @@ func (c *client) GetObject(ctx context.Context, obj models.Object) (*models.Obje obj.ID, } + desiredObjType := obj.Type + if obj.Object == models.ObjectTypeOrgCollection { args = append(args, "--organizationid", obj.OrganizationID) } @@ -191,6 +193,10 @@ func (c *client) GetObject(ctx context.Context, obj models.Object) (*models.Obje return nil, newUnmarshallError(err, args[0:2], out) } + if desiredObjType > 0 && obj.Type != desiredObjType { + return nil, models.ErrItemTypeMismatch + } + return &obj, nil } @@ -221,13 +227,22 @@ func (c *client) ListObjects(ctx context.Context, objType models.ObjectType, opt return nil, remapError(err) } - var obj []models.Object - err = json.Unmarshal(out, &obj) + var objs []models.Object + err = json.Unmarshal(out, &objs) if err != nil { return nil, newUnmarshallError(err, args[0:2], out) } - return obj, nil + filters := bitwarden.ListObjectsOptionsToFilterOptions(options...) + filteredObj := []models.Object{} + for _, obj := range objs { + if filters.ItemType != 0 && obj.Type != filters.ItemType { + continue + } + filteredObj = append(filteredObj, obj) + } + + return filteredObj, nil } // LoginWithPassword logs in using a password and retrieves the session key, diff --git a/internal/bitwarden/client_options.go b/internal/bitwarden/client_options.go index 80e52af..3a6da1a 100644 --- a/internal/bitwarden/client_options.go +++ b/internal/bitwarden/client_options.go @@ -1,5 +1,7 @@ package bitwarden +import "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + type ListObjectsOptionGenerator func(id string) ListObjectsOption type ListObjectsFilterOptions struct { CollectionFilter string @@ -7,6 +9,7 @@ type ListObjectsFilterOptions struct { OrganizationFilter string SearchFilter string UrlFilter string + ItemType models.ItemType } func (f *ListObjectsFilterOptions) IsValid() bool { @@ -27,6 +30,12 @@ func WithFolderID(id string) ListObjectsOption { } } +func WithItemType(itemType int) ListObjectsOption { + return func(f *ListObjectsFilterOptions) { + f.ItemType = models.ItemType(itemType) + } +} + func WithOrganizationID(id string) ListObjectsOption { return func(f *ListObjectsFilterOptions) { f.OrganizationFilter = id diff --git a/internal/bitwarden/embedded/password_manager_base.go b/internal/bitwarden/embedded/password_manager_base.go index 1a96548..f57cdeb 100644 --- a/internal/bitwarden/embedded/password_manager_base.go +++ b/internal/bitwarden/embedded/password_manager_base.go @@ -62,6 +62,9 @@ func (v *baseVault) getObject(_ context.Context, obj models.Object) (*models.Obj if !ok || obj.DeletedDate != nil { return nil, models.ErrObjectNotFound } + if obj.Type > 0 && storedObj.Type != obj.Type { + return nil, models.ErrItemTypeMismatch + } return &storedObj, nil } @@ -658,6 +661,10 @@ func objMatchFilter(ctx context.Context, obj models.Object, filters bitwarden.Li } } + if filters.ItemType > 0 && obj.Object == models.ObjectTypeItem && obj.Type != filters.ItemType { + return false + } + if len(filters.UrlFilter) > 0 { matchUrl := false for _, u := range obj.Login.URIs { diff --git a/internal/bitwarden/models/password_manager.go b/internal/bitwarden/models/password_manager.go index 578f79d..70b5380 100644 --- a/internal/bitwarden/models/password_manager.go +++ b/internal/bitwarden/models/password_manager.go @@ -12,6 +12,7 @@ var ( ErrAlreadyLoggedIn = errors.New("you are already logged in") ErrWrongMasterPassword = errors.New("invalid master password") ErrLoggedOut = errors.New("please login first") + ErrItemTypeMismatch = errors.New("returned object type does not match requested object type") ) type ItemType int diff --git a/internal/provider/data_source_folder.go b/internal/provider/data_source_folder.go index 28ffa37..674ad69 100644 --- a/internal/provider/data_source_folder.go +++ b/internal/provider/data_source_folder.go @@ -2,14 +2,13 @@ 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 dataSourceFolder() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing folder.", - ReadContext: withPasswordManager(opObjectRead(models.ObjectTypeFolder)), + ReadContext: withPasswordManager(opFolderRead), 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 fad5246..599be8d 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(opItemRead(models.ObjectTypeItem, models.ItemTypeLogin)), + ReadContext: withPasswordManager(opItemRead(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 ee1049e..f7d9ea5 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(opItemRead(models.ObjectTypeItem, models.ItemTypeSecureNote)), + ReadContext: withPasswordManager(opItemRead(models.ItemTypeSecureNote)), Schema: dataSourceItemSecureNoteSchema, } } diff --git a/internal/provider/data_source_org_collection.go b/internal/provider/data_source_org_collection.go index 447ad7c..f329ee3 100644 --- a/internal/provider/data_source_org_collection.go +++ b/internal/provider/data_source_org_collection.go @@ -2,14 +2,13 @@ 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 dataSourceOrgCollection() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing organization collection.", - ReadContext: withPasswordManager(opObjectRead(models.ObjectTypeOrgCollection)), + ReadContext: withPasswordManager(opOrganizationCollectionRead), Schema: schema_definition.OrgCollectionSchema(schema_definition.DataSource), } } diff --git a/internal/provider/data_source_organization.go b/internal/provider/data_source_organization.go index 2b6196c..3202f2d 100644 --- a/internal/provider/data_source_organization.go +++ b/internal/provider/data_source_organization.go @@ -2,14 +2,13 @@ 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 dataSourceOrganization() *schema.Resource { return &schema.Resource{ Description: "Use this data source to get information on an existing organization.", - ReadContext: withPasswordManager(opObjectRead(models.ObjectTypeOrganization)), + ReadContext: withPasswordManager(opOrganizationRead), Schema: schema_definition.OrganizationSchema(), } } diff --git a/internal/provider/operation_folder.go b/internal/provider/operation_folder.go index c3ef9e7..dc07fb6 100644 --- a/internal/provider/operation_folder.go +++ b/internal/provider/operation_folder.go @@ -8,6 +8,7 @@ import ( "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" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/transformation" ) func opFolderCreate(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { @@ -16,7 +17,15 @@ func opFolderCreate(ctx context.Context, d *schema.ResourceData, bwClient bitwar return diag.FromErr(err) } - return objectCreate(ctx, d, bwClient) + return diag.FromErr(applyOperation(ctx, d, bwClient.CreateObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} + +func opFolderDelete(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 diag.FromErr(applyOperation(ctx, d, withNilReturn(bwClient.DeleteObject), transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) } func opFolderImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { @@ -27,3 +36,32 @@ func opFolderImport(ctx context.Context, d *schema.ResourceData, meta interface{ } return []*schema.ResourceData{d}, nil } + +func opFolderRead(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, models.ObjectTypeFolder) + if err != nil { + return diag.FromErr(err) + } + if _, idProvided := d.GetOk(schema_definition.AttributeID); !idProvided { + return diag.FromErr(searchOperation(ctx, d, bwClient.ListObjects, transformation.ObjectDataFromStruct)) + } + + return diag.FromErr(applyOperation(ctx, d, bwClient.GetObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} + +func opFolderReadIgnoreMissing(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 ignoreMissing(ctx, d, applyOperation(ctx, d, bwClient.GetObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} + +func opFolderUpdate(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 diag.FromErr(applyOperation(ctx, d, bwClient.EditObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} diff --git a/internal/provider/operation_generic.go b/internal/provider/operation_generic.go index 3e86eed..c6e127a 100644 --- a/internal/provider/operation_generic.go +++ b/internal/provider/operation_generic.go @@ -1,6 +1,90 @@ package provider -import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +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/models" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/transformation" +) + +type applyOperationFn[T any] func(ctx context.Context, secret T) (*T, error) +type deleteOperationFn[T any] func(context.Context, T) error +type listOperationFn[T any] func(ctx context.Context, objType models.ObjectType, options ...bitwarden.ListObjectsOption) ([]T, error) + +// TransformationOperation +type schemaToObjectTransformation[T any] func(ctx context.Context, d *schema.ResourceData) T +type objectToSchemaTransformation[T any] func(ctx context.Context, d *schema.ResourceData, obj *T) error + +func applyOperation[T any](ctx context.Context, d *schema.ResourceData, clientOperation applyOperationFn[T], fromSchemaToObj schemaToObjectTransformation[T], fromObjToSchema objectToSchemaTransformation[T]) error { + obj, err := clientOperation(ctx, fromSchemaToObj(ctx, d)) + if err != nil { + return err + } + + return fromObjToSchema(ctx, d, obj) +} + +func searchOperation[T any](ctx context.Context, d *schema.ResourceData, clientOperation listOperationFn[T], fromObjToSchema objectToSchemaTransformation[T]) error { + objType, ok := d.GetOk(schema_definition.AttributeObject) + if !ok { + return fmt.Errorf("BUG: object type not set in the resource data") + } + + objs, err := clientOperation(ctx, models.ObjectType(objType.(string)), transformation.ListOptionsFromData(d)...) + if err != nil { + return err + } + + if len(objs) == 0 { + return fmt.Errorf("no object found matching the filter") + } else if len(objs) > 1 { + tflog.Warn(ctx, "Too many objects found", map[string]interface{}{"objects": objs}) + + 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. + switch object := any(obj).(type) { + case *models.Object: + if object.DeletedDate != nil { + return errors.New("object is soft deleted") + } + } + + return fromObjToSchema(ctx, d, &obj) +} + +func withNilReturn[T any](operation deleteOperationFn[T]) func(ctx context.Context, secret T) (*T, error) { + return func(ctx context.Context, secret T) (*T, error) { + return nil, operation(ctx, secret) + } +} + +func ignoreMissing(ctx context.Context, d *schema.ResourceData, err error) diag.Diagnostics { + 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 resourceImporter(stateContext schema.StateContextFunc) *schema.ResourceImporter { return &schema.ResourceImporter{ diff --git a/internal/provider/operation_item.go b/internal/provider/operation_item.go index bcf1676..d547ffe 100644 --- a/internal/provider/operation_item.go +++ b/internal/provider/operation_item.go @@ -8,14 +8,84 @@ import ( "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" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/transformation" ) -func opItemRead(attrObject models.ObjectType, attrType models.ItemType) passwordManagerOperation { +func opItemCreate(attrType models.ItemType) passwordManagerOperation { return func(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { - err := d.Set(schema_definition.AttributeType, attrType) + err := d.Set(schema_definition.AttributeObject, models.ObjectTypeItem) if err != nil { return diag.FromErr(err) } - return opObjectRead(attrObject)(ctx, d, bwClient) + + err = d.Set(schema_definition.AttributeType, attrType) + if err != nil { + return diag.FromErr(err) + } + + return diag.FromErr(applyOperation(ctx, d, bwClient.CreateObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) + } +} + +func opItemDelete(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + err := d.Set(schema_definition.AttributeObject, models.ObjectTypeItem) + if err != nil { + return diag.FromErr(err) + } + return diag.FromErr(applyOperation(ctx, d, withNilReturn(bwClient.DeleteObject), transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} + +func opItemImport(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, models.ObjectTypeItem) + 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 opItemRead(attrType models.ItemType) passwordManagerOperation { + return func(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + err := d.Set(schema_definition.AttributeObject, models.ObjectTypeItem) + if err != nil { + return diag.FromErr(err) + } + + err = d.Set(schema_definition.AttributeType, attrType) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(d.Get(schema_definition.AttributeID).(string)) + err = d.Set(schema_definition.AttributeObject, models.ObjectTypeItem) + if err != nil { + return diag.FromErr(err) + } + if _, idProvided := d.GetOk(schema_definition.AttributeID); !idProvided { + return diag.FromErr(searchOperation(ctx, d, bwClient.ListObjects, transformation.ObjectDataFromStruct)) + } + return diag.FromErr(applyOperation(ctx, d, bwClient.GetObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) + } +} + +func opItemReadIgnoreMissing(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + err := d.Set(schema_definition.AttributeObject, models.ObjectTypeItem) + if err != nil { + return diag.FromErr(err) + } + return ignoreMissing(ctx, d, applyOperation(ctx, d, bwClient.GetObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} + +func opItemUpdate(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { + err := d.Set(schema_definition.AttributeObject, models.ObjectTypeItem) + if err != nil { + return diag.FromErr(err) } + return diag.FromErr(applyOperation(ctx, d, bwClient.EditObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) } diff --git a/internal/provider/operation_object.go b/internal/provider/operation_object.go deleted file mode 100644 index 236ceae..0000000 --- a/internal/provider/operation_object.go +++ /dev/null @@ -1,173 +0,0 @@ -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" - "github.com/maxlaverse/terraform-provider-bitwarden/internal/transformation" -) - -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)), transformation.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 transformation.ObjectDataFromStruct(ctx, d, &obj) -} - -func objectOperation(ctx context.Context, d *schema.ResourceData, operation objectOperationFunc) error { - obj, err := operation(ctx, transformation.ObjectStructFromData(ctx, d)) - if err != nil { - return err - } - - return transformation.ObjectDataFromStruct(ctx, d, obj) -} diff --git a/internal/provider/operation_org_collection.go b/internal/provider/operation_org_collection.go index 3a34bce..0d85902 100644 --- a/internal/provider/operation_org_collection.go +++ b/internal/provider/operation_org_collection.go @@ -10,6 +10,7 @@ import ( "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" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/transformation" ) func opOrganizationCollectionCreate(ctx context.Context, d *schema.ResourceData, bwClient bitwarden.PasswordManager) diag.Diagnostics { @@ -17,8 +18,15 @@ func opOrganizationCollectionCreate(ctx context.Context, d *schema.ResourceData, if err != nil { return diag.FromErr(err) } + return diag.FromErr(applyOperation(ctx, d, bwClient.CreateObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} - return objectCreate(ctx, d, bwClient) +func opOrganizationCollectionDelete(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 diag.FromErr(applyOperation(ctx, d, withNilReturn(bwClient.DeleteObject), transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) } func opOrganizationCollectionImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { @@ -35,3 +43,32 @@ func opOrganizationCollectionImport(ctx context.Context, d *schema.ResourceData, return []*schema.ResourceData{d}, nil } + +func opOrganizationCollectionRead(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, models.ObjectTypeOrgCollection) + if err != nil { + return diag.FromErr(err) + } + if _, idProvided := d.GetOk(schema_definition.AttributeID); !idProvided { + return diag.FromErr(searchOperation(ctx, d, bwClient.ListObjects, transformation.ObjectDataFromStruct)) + } + + return diag.FromErr(applyOperation(ctx, d, bwClient.GetObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} + +func opOrganizationCollectionReadIgnoreMissing(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 ignoreMissing(ctx, d, applyOperation(ctx, d, bwClient.GetObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} + +func opOrganizationCollectionUpdate(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 diag.FromErr(applyOperation(ctx, d, bwClient.EditObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} diff --git a/internal/provider/operation_organization.go b/internal/provider/operation_organization.go new file mode 100644 index 0000000..da4ac32 --- /dev/null +++ b/internal/provider/operation_organization.go @@ -0,0 +1,27 @@ +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" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/transformation" +) + +func opOrganizationRead(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, models.ObjectTypeOrganization) + if err != nil { + return diag.FromErr(err) + } + + if _, idProvided := d.GetOk(schema_definition.AttributeID); !idProvided { + return diag.FromErr(searchOperation(ctx, d, bwClient.ListObjects, transformation.ObjectDataFromStruct)) + } + + return diag.FromErr(applyOperation(ctx, d, bwClient.GetObject, transformation.ObjectStructFromData, transformation.ObjectDataFromStruct)) +} diff --git a/internal/provider/operation_project.go b/internal/provider/operation_project.go index 007f7ca..0ed6789 100644 --- a/internal/provider/operation_project.go +++ b/internal/provider/operation_project.go @@ -2,29 +2,20 @@ 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" "github.com/maxlaverse/terraform-provider-bitwarden/internal/transformation" ) -type projectOperationFunc func(ctx context.Context, secret models.Project) (*models.Project, error) - -func opProjectCreate() secretsManagerOperation { - return func(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return diag.FromErr(projectOperation(ctx, d, bwsClient.CreateProject)) - } +func opProjectCreate(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { + return diag.FromErr(applyOperation(ctx, d, bwsClient.CreateProject, transformation.ProjectStructFromData, transformation.ProjectDataFromStruct)) } 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) - })) + return diag.FromErr(applyOperation(ctx, d, withNilReturn(bwsClient.DeleteProject), transformation.ProjectStructFromData, transformation.ProjectDataFromStruct)) } func opProjectImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { @@ -34,41 +25,13 @@ func opProjectImport(ctx context.Context, d *schema.ResourceData, meta interface 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 { - if project.ID != projectReq.ID { - return nil, errors.New("returned project ID does not match requested project ID") - } - } - - return project, err - })) + return diag.FromErr(applyOperation(ctx, d, bwsClient.GetProject, transformation.ProjectStructFromData, transformation.ProjectDataFromStruct)) } 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 errors.Is(err, models.ErrObjectNotFound) { - d.SetId("") - tflog.Warn(ctx, "Project not found, removing from state") - return diag.Diagnostics{} - } - - return diag.FromErr(err) + return ignoreMissing(ctx, d, applyOperation(ctx, d, bwsClient.GetProject, transformation.ProjectStructFromData, transformation.ProjectDataFromStruct)) } func opProjectUpdate(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return diag.FromErr(projectOperation(ctx, d, bwsClient.EditProject)) -} - -func projectOperation(ctx context.Context, d *schema.ResourceData, operation projectOperationFunc) error { - project, err := operation(ctx, transformation.ProjectStructFromData(ctx, d)) - if err != nil { - return err - } - - return transformation.ProjectDataFromStruct(ctx, d, project) + return diag.FromErr(applyOperation(ctx, d, bwsClient.EditProject, transformation.ProjectStructFromData, transformation.ProjectDataFromStruct)) } diff --git a/internal/provider/operation_secret.go b/internal/provider/operation_secret.go index 341a498..7fdf6f6 100644 --- a/internal/provider/operation_secret.go +++ b/internal/provider/operation_secret.go @@ -2,30 +2,21 @@ 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/models" "github.com/maxlaverse/terraform-provider-bitwarden/internal/schema_definition" "github.com/maxlaverse/terraform-provider-bitwarden/internal/transformation" ) -type secretOperationFunc func(ctx context.Context, secret models.Secret) (*models.Secret, error) - -func opSecretCreate() secretsManagerOperation { - return func(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return diag.FromErr(secretOperation(ctx, d, bwsClient.CreateSecret)) - } +func opSecretCreate(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { + return diag.FromErr(applyOperation(ctx, d, bwsClient.CreateSecret, transformation.SecretStructFromData, transformation.SecretDataFromStruct)) } 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) - })) + return diag.FromErr(applyOperation(ctx, d, withNilReturn(bwsClient.DeleteSecret), transformation.SecretStructFromData, transformation.SecretDataFromStruct)) } func opSecretImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { @@ -39,34 +30,15 @@ func opSecretRead(ctx context.Context, d *schema.ResourceData, bwsClient bitward return diag.FromErr(secretSearch(ctx, d, bwsClient)) } - return diag.FromErr(secretOperation(ctx, d, func(ctx context.Context, secretReq models.Secret) (*models.Secret, error) { - secret, err := bwsClient.GetSecret(ctx, secretReq) - if secret != nil { - if secret.ID != secretReq.ID { - return nil, errors.New("returned secret ID does not match requested secret ID") - } - } - - return secret, err - })) + return diag.FromErr(applyOperation(ctx, d, bwsClient.GetSecret, transformation.SecretStructFromData, transformation.SecretDataFromStruct)) } 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) + return ignoreMissing(ctx, d, applyOperation(ctx, d, bwsClient.GetSecret, transformation.SecretStructFromData, transformation.SecretDataFromStruct)) } func opSecretUpdate(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) diag.Diagnostics { - return diag.FromErr(secretOperation(ctx, d, bwsClient.EditSecret)) + return diag.FromErr(applyOperation(ctx, d, bwsClient.EditSecret, transformation.SecretStructFromData, transformation.SecretDataFromStruct)) } func secretSearch(ctx context.Context, d *schema.ResourceData, bwsClient bitwarden.SecretsManager) error { @@ -82,12 +54,3 @@ func secretSearch(ctx context.Context, d *schema.ResourceData, bwsClient bitward return transformation.SecretDataFromStruct(ctx, d, secret) } - -func secretOperation(ctx context.Context, d *schema.ResourceData, operation secretOperationFunc) error { - secret, err := operation(ctx, transformation.SecretStructFromData(ctx, d)) - if err != nil { - return err - } - - return transformation.SecretDataFromStruct(ctx, d, secret) -} diff --git a/internal/provider/resource_folder.go b/internal/provider/resource_folder.go index 349700c..fd108f3 100644 --- a/internal/provider/resource_folder.go +++ b/internal/provider/resource_folder.go @@ -10,9 +10,9 @@ func resourceFolder() *schema.Resource { Description: "Manages a folder.", CreateContext: withPasswordManager(opFolderCreate), - ReadContext: withPasswordManager(opObjectReadIgnoreMissing), - UpdateContext: withPasswordManager(opObjectUpdate), - DeleteContext: withPasswordManager(opObjectDelete), + ReadContext: withPasswordManager(opFolderReadIgnoreMissing), + UpdateContext: withPasswordManager(opFolderUpdate), + DeleteContext: withPasswordManager(opFolderDelete), Importer: resourceImporter(opFolderImport), Schema: schema_definition.FolderSchema(schema_definition.Resource), diff --git a/internal/provider/resource_item_login.go b/internal/provider/resource_item_login.go index 8b01747..f1d4019 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(opObjectCreate(models.ObjectTypeItem, models.ItemTypeLogin)), - ReadContext: withPasswordManager(opObjectReadIgnoreMissing), - UpdateContext: withPasswordManager(opObjectUpdate), - DeleteContext: withPasswordManager(opObjectDelete), - Importer: resourceImporter(opObjectImport(models.ObjectTypeItem, models.ItemTypeLogin)), + CreateContext: withPasswordManager(opItemCreate(models.ItemTypeLogin)), + ReadContext: withPasswordManager(opItemReadIgnoreMissing), + UpdateContext: withPasswordManager(opItemUpdate), + DeleteContext: withPasswordManager(opItemDelete), + Importer: resourceImporter(opItemImport(models.ItemTypeLogin)), Schema: dataSourceItemSecureNoteSchema, } } diff --git a/internal/provider/resource_item_secure_note.go b/internal/provider/resource_item_secure_note.go index 4f67afb..0f70e02 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(opObjectCreate(models.ObjectTypeItem, models.ItemTypeSecureNote)), - ReadContext: withPasswordManager(opObjectReadIgnoreMissing), - UpdateContext: withPasswordManager(opObjectUpdate), - DeleteContext: withPasswordManager(opObjectDelete), - Importer: resourceImporter(opObjectImport(models.ObjectTypeItem, models.ItemTypeSecureNote)), + CreateContext: withPasswordManager(opItemCreate(models.ItemTypeSecureNote)), + ReadContext: withPasswordManager(opItemReadIgnoreMissing), + UpdateContext: withPasswordManager(opItemUpdate), + DeleteContext: withPasswordManager(opItemDelete), + Importer: resourceImporter(opItemImport(models.ItemTypeSecureNote)), Schema: dataSourceItemSecureNoteSchema, } } diff --git a/internal/provider/resource_org_collection.go b/internal/provider/resource_org_collection.go index ffc0c8b..58c3ef6 100644 --- a/internal/provider/resource_org_collection.go +++ b/internal/provider/resource_org_collection.go @@ -10,9 +10,9 @@ func resourceOrgCollection() *schema.Resource { Description: "Manages an organization collection.", CreateContext: withPasswordManager(opOrganizationCollectionCreate), - ReadContext: withPasswordManager(opObjectReadIgnoreMissing), - UpdateContext: withPasswordManager(opObjectUpdate), - DeleteContext: withPasswordManager(opObjectDelete), + ReadContext: withPasswordManager(opOrganizationCollectionReadIgnoreMissing), + UpdateContext: withPasswordManager(opOrganizationCollectionUpdate), + DeleteContext: withPasswordManager(opOrganizationCollectionDelete), Importer: resourceImporter(opOrganizationCollectionImport), Schema: schema_definition.OrgCollectionSchema(schema_definition.Resource), diff --git a/internal/provider/resource_project.go b/internal/provider/resource_project.go index 21d7a77..abdd355 100644 --- a/internal/provider/resource_project.go +++ b/internal/provider/resource_project.go @@ -10,7 +10,7 @@ func resourceProject() *schema.Resource { return &schema.Resource{ Description: "Manages a Project.", - CreateContext: withSecretsManager(opProjectCreate()), + CreateContext: withSecretsManager(opProjectCreate), ReadContext: withSecretsManager(opProjectReadIgnoreMissing), UpdateContext: withSecretsManager(opProjectUpdate), DeleteContext: withSecretsManager(opProjectDelete), diff --git a/internal/provider/resource_secret.go b/internal/provider/resource_secret.go index ec29808..c87d3d4 100644 --- a/internal/provider/resource_secret.go +++ b/internal/provider/resource_secret.go @@ -10,7 +10,7 @@ func resourceSecret() *schema.Resource { return &schema.Resource{ Description: "Manages a secret.", - CreateContext: withSecretsManager(opSecretCreate()), + CreateContext: withSecretsManager(opSecretCreate), ReadContext: withSecretsManager(opSecretReadIgnoreMissing), UpdateContext: withSecretsManager(opSecretUpdate), DeleteContext: withSecretsManager(opSecretDelete), diff --git a/internal/transformation/transformation_object.go b/internal/transformation/transformation_object.go index 599a59d..f10a016 100644 --- a/internal/transformation/transformation_object.go +++ b/internal/transformation/transformation_object.go @@ -389,5 +389,11 @@ func ListOptionsFromData(d *schema.ResourceData) []bitwarden.ListObjectsOption { filters = append(filters, optionFunc(v)) } } + + itemType, ok := d.GetOk(schema_definition.AttributeType) + if ok { + filters = append(filters, bitwarden.WithItemType(itemType.(int))) + } + return filters }