Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prepare for new dedicated models (e.g. folders) #204

Merged
merged 1 commit into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading