Skip to content

Commit

Permalink
implement support for filters in data sources
Browse files Browse the repository at this point in the history
  • Loading branch information
maxlaverse committed Apr 18, 2024
1 parent e618936 commit 88a569c
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 54 deletions.
4 changes: 2 additions & 2 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ tffmt:
terraform fmt -recursive examples

docs:
go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
find docs -type f -exec sed -i '' '/INTERNAL USE/d' {} \;
go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@v0.19.0
find docs -type f -name '*.md' -exec sed -i '' '/INTERNAL USE/d' {} \;

clean:
rm internal/provider/.bitwarden/data.json || true
10 changes: 6 additions & 4 deletions docs/data-sources/item_login.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,24 @@ data "bitwarden_item_login" "database_credentials" {
<!-- schema generated by tfplugindocs -->
## Schema

### Required
### Optional

- `collection_ids` (List of String) Identifier of the collections the item belongs to.
- `folder_id` (String) Identifier of the folder.
- `id` (String) Identifier.
- `organization_id` (String) Identifier of the organization.
- `search` (String) Occurence to search for.
- `url` (String) URL to filter results by.

### Read-Only

- `attachments` (List of Object) List of item attachments. (see [below for nested schema](#nestedatt--attachments))
- `collection_ids` (List of String) Identifier of the collections the item belongs to.
- `creation_date` (String) Date the item was created.
- `deleted_date` (String) Date the item was deleted.
- `favorite` (Boolean) Mark as a Favorite to have item appear at the top of your Vault in the UI.
- `field` (List of Object, Sensitive) Extra fields. (see [below for nested schema](#nestedatt--field))
- `folder_id` (String) Identifier of the folder.
- `name` (String) Name.
- `notes` (String, Sensitive) Notes.
- `organization_id` (String) Identifier of the organization.
- `password` (String, Sensitive) Login password.
- `reprompt` (Boolean) Require master password “re-prompt” when displaying secret in the UI.
- `revision_date` (String) Last time the item was updated.
Expand Down
10 changes: 6 additions & 4 deletions docs/data-sources/item_secure_note.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,24 @@ data "bitwarden_item_secure_note" "ssh_notes" {
<!-- schema generated by tfplugindocs -->
## Schema

### Required
### Optional

- `collection_ids` (List of String) Identifier of the collections the item belongs to.
- `folder_id` (String) Identifier of the folder.
- `id` (String) Identifier.
- `organization_id` (String) Identifier of the organization.
- `search` (String) Occurence to search for.
- `url` (String) URL to filter results by.

### Read-Only

- `attachments` (List of Object) List of item attachments. (see [below for nested schema](#nestedatt--attachments))
- `collection_ids` (List of String) Identifier of the collections the item belongs to.
- `creation_date` (String) Date the item was created.
- `deleted_date` (String) Date the item was deleted.
- `favorite` (Boolean) Mark as a Favorite to have item appear at the top of your Vault in the UI.
- `field` (List of Object, Sensitive) Extra fields. (see [below for nested schema](#nestedatt--field))
- `folder_id` (String) Identifier of the folder.
- `name` (String) Name.
- `notes` (String, Sensitive) Notes.
- `organization_id` (String) Identifier of the organization.
- `reprompt` (Boolean) Require master password “re-prompt” when displaying secret in the UI.
- `revision_date` (String) Last time the item was updated.

Expand Down
2 changes: 1 addition & 1 deletion docs/resources/item_login.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ resource "bitwarden_item_login" "administrative-user" {
- `favorite` (Boolean) Mark as a Favorite to have item appear at the top of your Vault in the UI.
- `field` (Block List) Extra fields. (see [below for nested schema](#nestedblock--field))
- `folder_id` (String) Identifier of the folder.
- `id` (String) Identifier.
- `notes` (String, Sensitive) Notes.
- `organization_id` (String) Identifier of the organization.
- `password` (String, Sensitive) Login password.
Expand All @@ -67,7 +68,6 @@ resource "bitwarden_item_login" "administrative-user" {
- `attachments` (List of Object) List of item attachments. (see [below for nested schema](#nestedatt--attachments))
- `creation_date` (String) Date the item was created.
- `deleted_date` (String) Date the item was deleted.
- `id` (String) Identifier.
- `revision_date` (String) Last time the item was updated.

<a id="nestedblock--field"></a>
Expand Down
2 changes: 1 addition & 1 deletion docs/resources/item_secure_note.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ EOT
- `favorite` (Boolean) Mark as a Favorite to have item appear at the top of your Vault in the UI.
- `field` (Block List) Extra fields. (see [below for nested schema](#nestedblock--field))
- `folder_id` (String) Identifier of the folder.
- `id` (String) Identifier.
- `notes` (String, Sensitive) Notes.
- `organization_id` (String) Identifier of the organization.
- `reprompt` (Boolean) Require master password “re-prompt” when displaying secret in the UI.
Expand All @@ -64,7 +65,6 @@ EOT
- `attachments` (List of Object) List of item attachments. (see [below for nested schema](#nestedatt--attachments))
- `creation_date` (String) Date the item was created.
- `deleted_date` (String) Date the item was deleted.
- `id` (String) Identifier.
- `revision_date` (String) Last time the item was updated.

<a id="nestedblock--field"></a>
Expand Down
32 changes: 29 additions & 3 deletions internal/bitwarden/bw/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ type Client interface {
CreateObject(Object) (*Object, error)
EditObject(Object) (*Object, error)
GetAttachment(itemId, attachmentId string) ([]byte, error)
GetObject(objType, itemId string) (*Object, error)
GetObject(objType, itemOrSearch string) (*Object, error)
GetSessionKey() string
HasSessionKey() bool
ListObjects(objType string, options ...ListObjectsOption) ([]Object, error)
LoginWithAPIKey(password, clientId, clientSecret string) error
LoginWithPassword(username, password string) error
Logout() error
Expand Down Expand Up @@ -137,8 +138,8 @@ func (c *client) EditObject(obj Object) (*Object, error) {
return &obj, nil
}

func (c *client) GetObject(objType, itemId string) (*Object, error) {
out, err := c.cmdWithSession("get", objType, itemId).Run()
func (c *client) GetObject(objType, itemOrSearch string) (*Object, error) {
out, err := c.cmdWithSession("get", objType, itemOrSearch).Run()
if err != nil {
return nil, remapError(err)
}
Expand All @@ -165,6 +166,31 @@ func (c *client) GetSessionKey() string {
return c.sessionKey
}

// ListObjects returns objects of a given type matching given filters.
func (c *client) ListObjects(objType string, options ...ListObjectsOption) ([]Object, error) {
args := []string{
"list",
objType,
}

for _, applyOption := range options {
applyOption(&args)
}

out, err := c.cmdWithSession(args...).Run()
if err != nil {
return nil, remapError(err)
}

var obj []Object
err = json.Unmarshal(out, &obj)
if err != nil {
return nil, newUnmarshallError(err, "list object", out)
}

return obj, nil
}

// LoginWithPassword logs in using a password and retrieves the session key,
// allowing authenticated requests using the client.
func (c *client) LoginWithPassword(username, password string) error {
Expand Down
34 changes: 34 additions & 0 deletions internal/bitwarden/bw/client_options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package bw

type ListObjectsOption func(args *[]string)
type ListObjectsOptionGenerator func(id string) ListObjectsOption

func WithCollectionID(id string) ListObjectsOption {
return func(args *[]string) {
*args = append(*args, "--collectionid", id)
}
}

func WithFolderID(id string) ListObjectsOption {
return func(args *[]string) {
*args = append(*args, "--folderid", id)
}
}

func WithOrganizationID(id string) ListObjectsOption {
return func(args *[]string) {
*args = append(*args, "--organizationid", id)
}
}

func WithSearch(search string) ListObjectsOption {
return func(args *[]string) {
*args = append(*args, "--search", search)
}
}

func WithUrl(url string) ListObjectsOption {
return func(args *[]string) {
*args = append(*args, "--url", url)
}
}
24 changes: 24 additions & 0 deletions internal/provider/data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,27 @@ func readDataSource(attrObject bw.ObjectType, attrType bw.ItemType) schema.ReadC
return objectRead(ctx, d, meta)
}
}

func listOptionsFromData(d *schema.ResourceData) []bw.ListObjectsOption {
filters := []bw.ListObjectsOption{}

filterMap := map[string]bw.ListObjectsOptionGenerator{
attributeFilterSearch: bw.WithSearch,
attributeFilterCollectionId: bw.WithCollectionID,
attributeFilterFolderID: bw.WithFolderID,
attributeFilterOrganizationID: bw.WithOrganizationID,
attributeFilterURL: bw.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
}
14 changes: 14 additions & 0 deletions internal/provider/data_source_item_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ func TestAccDataSourceItemLoginDeleted(t *testing.T) {
Config: tfConfigProvider() + tfConfigResourceItemLoginSmall(),
Check: getObjectID("bitwarden_item_login.foo", &objectID),
},
{
Config: tfConfigProvider() + tfConfigDataItemLoginWithURLFilter("https://start_with/something"),
Check: getObjectID("bitwarden_item_login.foo", &objectID),
},
{
Config: tfConfigProvider() + tfConfigResourceItemLoginSmall() + tfConfigDataItemLoginWithId(objectID),
PreConfig: func() {
Expand All @@ -74,6 +78,16 @@ data "bitwarden_item_login" "foo_data" {
`, id)
}

func tfConfigDataItemLoginWithURLFilter(url string) string {
return fmt.Sprintf(`
data "bitwarden_item_login" "foo_data" {
provider = bitwarden
url = "%s"
}
`, url)
}

func tfConfigDataItemLogin() string {
return `
data "bitwarden_item_login" "foo_data" {
Expand Down
43 changes: 42 additions & 1 deletion internal/provider/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package provider
import (
"context"
"errors"
"fmt"
"log"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand All @@ -15,6 +16,10 @@ func objectCreate(ctx context.Context, d *schema.ResourceData, meta interface{})
}

func objectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
if _, areFiltersSet := d.GetOk(attributeID); !areFiltersSet {
return diag.FromErr(objectSearch(d, meta))
}

return diag.FromErr(objectOperation(ctx, d, func(secret bw.Object) (*bw.Object, error) {
obj, err := meta.(bw.Client).GetObject(string(secret.Object), secret.ID)

Expand All @@ -23,10 +28,46 @@ func objectRead(ctx context.Context, d *schema.ResourceData, meta interface{}) d
if obj != nil && obj.DeletedDate != nil {
return nil, errors.New("object is soft deleted")
}

if obj != nil && obj.ID != secret.ID {
return nil, errors.New("returned object ID does not match requested object ID")
}
return obj, err
}))
}

func objectSearch(d *schema.ResourceData, meta interface{}) error {
objType, ok := d.GetOk(attributeObject)
if !ok {
return fmt.Errorf("BUG: object type not set in the resource data")
}

objs, err := meta.(bw.Client).ListObjects(fmt.Sprintf("%ss", objType), 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 {
log.Print("[WARN] Too many objects found:")
for _, obj := range objs {
log.Printf("[WARN] * %s (%s)", obj.Name, obj.ID)
}
return fmt.Errorf("too many objects found")
}

obj := objs[0]

// 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 errors.New("object is soft deleted")
}

return objectDataFromStruct(d, &obj)
}

func objectReadIgnoreMissing(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
err := objectOperation(ctx, d, func(secret bw.Object) (*bw.Object, error) {
return meta.(bw.Client).GetObject(string(secret.Object), secret.ID)
Expand Down Expand Up @@ -57,7 +98,7 @@ func objectDelete(ctx context.Context, d *schema.ResourceData, meta interface{})
}))
}

func objectOperation(ctx context.Context, d *schema.ResourceData, operation func(secret bw.Object) (*bw.Object, error)) error {
func objectOperation(_ context.Context, d *schema.ResourceData, operation func(secret bw.Object) (*bw.Object, error)) error {
obj, err := operation(objectStructFromData(d))
if err != nil {
return err
Expand Down
27 changes: 22 additions & 5 deletions internal/provider/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ func loginSchema(schemaType schemaTypeEnum) map[string]*schema.Schema {

func baseSchema(schemaType schemaTypeEnum) map[string]*schema.Schema {

return map[string]*schema.Schema{
base := map[string]*schema.Schema{
/*
* Attributes that can be required
*/
attributeID: {
Description: descriptionIdentifier,
Type: schema.TypeString,
Computed: schemaType == Resource,
Required: schemaType == DataSource,
Optional: true,
},
attributeName: {
Description: descriptionName,
Expand All @@ -73,7 +73,7 @@ func baseSchema(schemaType schemaTypeEnum) map[string]*schema.Schema {
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Computed: schemaType == DataSource,
Optional: schemaType == Resource,
Optional: true,
},
attributeFavorite: {
Description: descriptionFavorite,
Expand Down Expand Up @@ -121,7 +121,7 @@ func baseSchema(schemaType schemaTypeEnum) map[string]*schema.Schema {
Description: descriptionFolderID,
Type: schema.TypeString,
Computed: schemaType == DataSource,
Optional: schemaType == Resource,
Optional: true,
},

attributeNotes: {
Expand All @@ -135,7 +135,7 @@ func baseSchema(schemaType schemaTypeEnum) map[string]*schema.Schema {
Description: descriptionOrganizationID,
Type: schema.TypeString,
Computed: schemaType == DataSource,
Optional: schemaType == Resource,
Optional: true,
},
attributeReprompt: {
Description: descriptionReprompt,
Expand Down Expand Up @@ -181,6 +181,23 @@ func baseSchema(schemaType schemaTypeEnum) map[string]*schema.Schema {
Computed: true,
},
}

if schemaType == DataSource {
base[attributeFilterSearch] = &schema.Schema{
Description: descriptionFilterSearch,
Type: schema.TypeString,
Optional: true,
Computed: true,
}

base[attributeFilterURL] = &schema.Schema{
Description: descriptionFilterURL,
Type: schema.TypeString,
Optional: true,
Computed: true,
}
}
return base
}

func uriElem() *schema.Resource {
Expand Down
Loading

0 comments on commit 88a569c

Please sign in to comment.