Skip to content

Commit

Permalink
prepare for new dedicated models (e.g. folders) (#204)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxlaverse authored Dec 18, 2024
1 parent 20e95bc commit a59ce68
Show file tree
Hide file tree
Showing 24 changed files with 338 additions and 294 deletions.
21 changes: 18 additions & 3 deletions internal/bitwarden/bwcli/password_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions internal/bitwarden/client_options.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package bitwarden

import "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models"

type ListObjectsOptionGenerator func(id string) ListObjectsOption
type ListObjectsFilterOptions struct {
CollectionFilter string
FolderFilter string
OrganizationFilter string
SearchFilter string
UrlFilter string
ItemType models.ItemType
}

func (f *ListObjectsFilterOptions) IsValid() bool {
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions internal/bitwarden/embedded/password_manager_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions internal/bitwarden/models/password_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions internal/provider/data_source_folder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
2 changes: 1 addition & 1 deletion internal/provider/data_source_item_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
2 changes: 1 addition & 1 deletion internal/provider/data_source_item_secure_note.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
3 changes: 1 addition & 2 deletions internal/provider/data_source_org_collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
3 changes: 1 addition & 2 deletions internal/provider/data_source_organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}
40 changes: 39 additions & 1 deletion internal/provider/operation_folder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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) {
Expand All @@ -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))
}
86 changes: 85 additions & 1 deletion internal/provider/operation_generic.go
Original file line number Diff line number Diff line change
@@ -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{
Expand Down
Loading

0 comments on commit a59ce68

Please sign in to comment.