From 6c482c2f195d06cf0c0870e2e19d3ee1a9bff7ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priscila=20Sol=C3=ADs=20Garc=C3=ADa?= Date: Tue, 9 May 2023 11:32:51 -0600 Subject: [PATCH 1/2] Add fields to each items default section This commit adds the ability to set a list of fields for any type of 1Password item. In addition to this, some additional updates were made to support this: - Include "note_value" in the resource as it was previously set in the data source - Delete "purpose" from the fields as it can cause a collision with the note_value argument - Fix bug where it wasn't capturing deleted fields from an existing item - Add "secure_note" category --- onepassword/data_source_onepassword_item.go | 96 ++++--- .../data_source_onepassword_item_test.go | 97 ++++++-- onepassword/resource_onepassword_item.go | 235 ++++++++++++------ onepassword/resource_onepassword_item_test.go | 35 ++- 4 files changed, 336 insertions(+), 127 deletions(-) diff --git a/onepassword/data_source_onepassword_item.go b/onepassword/data_source_onepassword_item.go index 770a9122..15b7fb17 100644 --- a/onepassword/data_source_onepassword_item.go +++ b/onepassword/data_source_onepassword_item.go @@ -13,6 +13,35 @@ import ( func dataSourceOnepasswordItem() *schema.Resource { exactlyOneOfUUIDAndTitle := []string{"uuid", "title"} + fieldSchema := map[string]*schema.Schema{ + "id": { + Description: fieldIDDescription, + Type: schema.TypeString, + Computed: true, + }, + "label": { + Description: fieldLabelDescription, + Type: schema.TypeString, + Computed: true, + }, + "purpose": { + Description: fmt.Sprintf(enumDescription, fieldPurposeDescription, fieldPurposes), + Type: schema.TypeString, + Computed: true, + }, + "type": { + Description: fmt.Sprintf(enumDescription, fieldTypeDescription, fieldTypes), + Type: schema.TypeString, + Computed: true, + }, + "value": { + Description: fieldValueDescription, + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + } + return &schema.Resource{ Description: "Use this data source to get details of an item by its vault uuid and either the title or the uuid of the item.", Read: dataSourceOnepasswordItemRead, @@ -95,6 +124,17 @@ func dataSourceOnepasswordItem() *schema.Resource { Optional: true, Sensitive: true, }, + "field": { + Description: fieldsDescription, + Type: schema.TypeList, + Computed: true, + Optional: true, + MinItems: 0, + Elem: &schema.Resource{ + Description: fieldDescription, + Schema: fieldSchema, + }, + }, "section": { Description: sectionsDescription, Type: schema.TypeList, @@ -120,34 +160,7 @@ func dataSourceOnepasswordItem() *schema.Resource { MinItems: 0, Elem: &schema.Resource{ Description: fieldDescription, - Schema: map[string]*schema.Schema{ - "id": { - Description: fieldIDDescription, - Type: schema.TypeString, - Computed: true, - }, - "label": { - Description: fieldLabelDescription, - Type: schema.TypeString, - Computed: true, - }, - "purpose": { - Description: fmt.Sprintf(enumDescription, fieldPurposeDescription, fieldPurposes), - Type: schema.TypeString, - Computed: true, - }, - "type": { - Description: fmt.Sprintf(enumDescription, fieldTypeDescription, fieldTypes), - Type: schema.TypeString, - Computed: true, - }, - "value": { - Description: fieldValueDescription, - Type: schema.TypeString, - Computed: true, - Sensitive: true, - }, - }, + Schema: fieldSchema, }, }, }, @@ -193,7 +206,6 @@ func dataSourceOnepasswordItemRead(data *schema.ResourceData, meta interface{}) dataField := map[string]interface{}{} dataField["id"] = f.ID dataField["label"] = f.Label - dataField["purpose"] = f.Purpose dataField["type"] = f.Type dataField["value"] = f.Value @@ -207,20 +219,36 @@ func dataSourceOnepasswordItemRead(data *schema.ResourceData, meta interface{}) data.Set("section", dataSections) + fields := []interface{}{} for _, f := range item.Fields { - switch f.Purpose { - case "USERNAME": + switch f.Label { + case "username": data.Set("username", f.Value) - case "PASSWORD": + case "password": data.Set("password", f.Value) - case "NOTES": + case "hostname": + data.Set("hostname", f.Value) + case "database": + data.Set("database", f.Value) + case "port": + data.Set("port", f.Value) + case "type": + data.Set("type", f.Value) + case "notesPlain": data.Set("note_value", f.Value) default: if f.Section == nil { - data.Set(strings.ToLower(f.Label), f.Value) + field := make(map[string]interface{}) + field["id"] = f.ID + field["label"] = f.Label + field["purpose"] = f.Purpose + field["type"] = f.Type + field["value"] = f.Value + fields = append(fields, field) } } } + data.Set("field", fields) return nil } diff --git a/onepassword/data_source_onepassword_item_test.go b/onepassword/data_source_onepassword_item_test.go index 3eb42043..f7dbf9af 100644 --- a/onepassword/data_source_onepassword_item_test.go +++ b/onepassword/data_source_onepassword_item_test.go @@ -75,6 +75,30 @@ func TestDataSourceOnePasswordItemReadWithSections(t *testing.T) { compareItemToSource(t, dataSourceData, expectedItem) } +func TestDataSourceOnePasswordItemReadWithFields(t *testing.T) { + expectedItem := generateItem() + meta := &testClient{ + GetItemFunc: func(uuid string, vaultUUID string) (*onepassword.Item, error) { + return expectedItem, nil + }, + } + expectedItem.Fields = append(expectedItem.Fields, &onepassword.ItemField{ + ID: "98765", + Type: "CONCEALED", + Label: "Secret Field", + Value: "Very secret", + }) + + dataSourceData := generateDataSource(t, expectedItem) + dataSourceData.Set("uuid", expectedItem.ID) + + err := dataSourceOnepasswordItemRead(dataSourceData, meta) + if err != nil { + t.Errorf("Unexpected error occured") + } + compareItemToSource(t, dataSourceData, expectedItem) +} + func compareItemToSource(t *testing.T, dataSourceData *schema.ResourceData, item *onepassword.Item) { if dataSourceData.Get("uuid") != item.ID { t.Errorf("Expected uuid to be %v got %v", item.ID, dataSourceData.Get("uuid")) @@ -94,33 +118,53 @@ func compareItemToSource(t *testing.T, dataSourceData *schema.ResourceData, item } compareStringSlice(t, getTags(dataSourceData), item.Tags) + predefinedFields := []string{"username", "password", "hostname", "database", "port", "type"} for _, f := range item.Fields { path := f.Label - if f.Section != nil { - sectionIndex := 0 - fieldIndex := 0 - sections := dataSourceData.Get("section").([]interface{}) - - for i, section := range sections { - s := section.(map[string]interface{}) - if s["label"] == f.Section.Label || - (f.Section.ID != "" && s["id"] == f.Section.ID) { - sectionIndex = i - sectionFields := dataSourceData.Get(fmt.Sprintf("section.%d.field", i)).([]interface{}) - - for j, field := range sectionFields { - df := field.(map[string]interface{}) - if df["label"] == f.Label { - fieldIndex = j + if !contains(t, predefinedFields, f.Label) { + if f.Section != nil { + sectionIndex := 0 + fieldIndex := 0 + sections := dataSourceData.Get("section").([]interface{}) + + for i, section := range sections { + s := section.(map[string]interface{}) + if s["label"] == f.Section.Label || + (f.Section.ID != "" && s["id"] == f.Section.ID) { + sectionIndex = i + sectionFields := dataSourceData.Get(fmt.Sprintf("section.%d.field", i)).([]interface{}) + + for j, field := range sectionFields { + df := field.(map[string]interface{}) + if df["label"] == f.Label { + fieldIndex = j + } } } } - } - if len(sections) > 0 { - path = fmt.Sprintf("section.%d.field.%d.value", sectionIndex, fieldIndex) + if len(sections) > 0 { + path = fmt.Sprintf("section.%d.field.%d.value", sectionIndex, fieldIndex) + } + } else { + fieldIndex := 0 + fields := dataSourceData.Get("field").([]interface{}) + + for i, field := range fields { + df := field.(map[string]interface{}) + if df["label"] == f.Label { + fieldIndex = i + } + } + + if len(fields) > 0 { + path = fmt.Sprintf("field.%d.value", fieldIndex) + } } } + if f.Label == "notesPlain" { + path = "note_value" + } if dataSourceData.Get(path) != f.Value { t.Errorf("Expected field %v to be %v got %v", f.Label, f.Value, dataSourceData.Get(path)) } @@ -176,6 +220,10 @@ func generateFields() []*onepassword.ItemField { Label: "type", Value: "test_type", }, + { + Label: "notesPlain", + Value: "test_note", + }, } return fields } @@ -192,3 +240,14 @@ func compareStringSlice(t *testing.T, actual, expected []string) { } } } + +func contains(t *testing.T, s []string, e string) bool { + t.Helper() + + for _, a := range s { + if a == e { + return true + } + } + return false +} diff --git a/onepassword/resource_onepassword_item.go b/onepassword/resource_onepassword_item.go index e493b9e8..0d5d4621 100644 --- a/onepassword/resource_onepassword_item.go +++ b/onepassword/resource_onepassword_item.go @@ -34,6 +34,7 @@ const ( sectionLabelDescription = "The label for the section." sectionFieldsDescription = "A list of custom fields in the section." + fieldsDescription = "A list of custom fields in an item" fieldDescription = "A custom field." fieldIDDescription = "A unique identifier for the field." fieldLabelDescription = "The label for the field." @@ -51,7 +52,7 @@ const ( enumDescription = "%s One of %q" ) -var categories = []string{"login", "password", "database"} +var categories = []string{"login", "password", "database", "secure_note"} var dbTypes = []string{"db2", "filemaker", "msaccess", "mssql", "mysql", "oracle", "postgresql", "sqlite", "other"} var fieldPurposes = []string{"USERNAME", "PASSWORD", "NOTES"} var fieldTypes = []string{"STRING", "EMAIL", "CONCEALED", "URL", "OTP", "DATE", "MONTH_YEAR", "MENU"} @@ -100,6 +101,34 @@ func resourceOnepasswordItem() *schema.Resource { Optional: true, } + fieldSchema := map[string]*schema.Schema{ + "id": { + Description: fieldIDDescription, + Type: schema.TypeString, + Computed: true, + }, + "label": { + Description: fieldLabelDescription, + Type: schema.TypeString, + Required: true, + }, + "type": { + Description: fmt.Sprintf(enumDescription, fieldTypeDescription, fieldTypes), + Type: schema.TypeString, + Default: "STRING", + Optional: true, + ValidateFunc: validation.StringInSlice(fieldTypes, true), + }, + "value": { + Description: fieldValueDescription, + Type: schema.TypeString, + Optional: true, + Computed: true, + Sensitive: true, + }, + "password_recipe": passwordRecipe, + } + return &schema.Resource{ Description: "A 1Password item.", Create: resourceOnepasswordItemCreate, @@ -185,6 +214,21 @@ func resourceOnepasswordItem() *schema.Resource { Sensitive: true, Computed: true, }, + "note_value": { + Description: noteValueDescription, + Type: schema.TypeString, + Optional: true, + }, + "field": { + Description: fieldsDescription, + Type: schema.TypeList, + Optional: true, + MinItems: 0, + Elem: &schema.Resource{ + Description: fieldDescription, + Schema: fieldSchema, + }, + }, "section": { Description: sectionsDescription, Type: schema.TypeList, @@ -210,40 +254,7 @@ func resourceOnepasswordItem() *schema.Resource { MinItems: 0, Elem: &schema.Resource{ Description: fieldDescription, - Schema: map[string]*schema.Schema{ - "id": { - Description: fieldIDDescription, - Type: schema.TypeString, - Optional: true, - Computed: true, - }, - "label": { - Description: fieldLabelDescription, - Type: schema.TypeString, - Required: true, - }, - "purpose": { - Description: fmt.Sprintf(enumDescription, fieldPurposeDescription, fieldPurposes), - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice(fieldPurposes, true), - }, - "type": { - Description: fmt.Sprintf(enumDescription, fieldTypeDescription, fieldTypes), - Type: schema.TypeString, - Default: "STRING", - Optional: true, - ValidateFunc: validation.StringInSlice(fieldTypes, true), - }, - "value": { - Description: fieldValueDescription, - Type: schema.TypeString, - Optional: true, - Computed: true, - Sensitive: true, - }, - "password_recipe": passwordRecipe, - }, + Schema: fieldSchema, }, }, }, @@ -352,9 +363,9 @@ func itemToData(item *onepassword.Item, data *schema.ResourceData) { data.Set("category", strings.ToLower(string(item.Category))) dataSections := data.Get("section").([]interface{}) + sections := []interface{}{} for _, s := range item.Sections { section := map[string]interface{}{} - newSection := true // Check for existing section state for i := 0; i < len(dataSections); i++ { @@ -364,38 +375,37 @@ func itemToData(item *onepassword.Item, data *schema.ResourceData) { if (s.ID != "" && s.ID == existingID) || s.Label == existingLabel { section = existingSection - newSection = false } } section["id"] = s.ID section["label"] = s.Label + sections = append(sections, section) - existingFields := []interface{}{} + fields := []interface{}{} + var dataFields []interface{} if section["field"] != nil { - existingFields = section["field"].([]interface{}) + dataFields = section["field"].([]interface{}) } for _, f := range item.Fields { if f.Section != nil && f.Section.ID == s.ID { - dataField := map[string]interface{}{} - newField := true + field := make(map[string]interface{}) // Check for existing field state - for i := 0; i < len(existingFields); i++ { - existingField := existingFields[i].(map[string]interface{}) + for i := 0; i < len(dataFields); i++ { + existingField := dataFields[i].(map[string]interface{}) existingID := existingField["id"].(string) existingLabel := existingField["label"].(string) if (f.ID != "" && f.ID == existingID) || f.Label == existingLabel { - dataField = existingFields[i].(map[string]interface{}) - newField = false + field = existingField } } - dataField["id"] = f.ID - dataField["label"] = f.Label - dataField["purpose"] = f.Purpose - dataField["type"] = f.Type - dataField["value"] = f.Value + field["id"] = f.ID + field["label"] = f.Label + field["type"] = f.Type + field["value"] = f.Value + fields = append(fields, field) if f.Recipe != nil { charSets := map[string]bool{} @@ -409,35 +419,72 @@ func itemToData(item *onepassword.Item, data *schema.ResourceData) { "digits": charSets["digits"], "symbols": charSets["symbols"], } - dataField["password_recipe"] = dataRecipe - } - - if newField { - existingFields = append(existingFields, dataField) + field["password_recipe"] = dataRecipe } } } - section["field"] = existingFields + section["field"] = fields - if newSection { - dataSections = append(dataSections, section) - } } + data.Set("section", sections) - data.Set("section", dataSections) - + dataFields := data.Get("field").([]interface{}) + fields := []interface{}{} for _, f := range item.Fields { - switch f.Purpose { - case "USERNAME": - data.Set("username", f.Value) - case "PASSWORD": - data.Set("password", f.Value) - default: - if f.Section == nil { - data.Set(f.Label, f.Value) + if f.Section == nil { + field := make(map[string]interface{}) + + for i := 0; i < len(dataFields); i++ { + existingField := dataFields[i].(map[string]interface{}) + existingID := existingField["id"].(string) + existingLabel := existingField["label"].(string) + + if (f.ID != "" && f.ID == existingID) || f.Label == existingLabel { + field = existingField + } + } + + // Determine if this is a generic field or one of the predefined fields set in the provider + switch f.ID { + case "username": + data.Set("username", f.Value) + case "password": + data.Set("password", f.Value) + case "hostname": + data.Set("hostname", f.Value) + case "database": + data.Set("database", f.Value) + case "port": + data.Set("port", f.Value) + case "database_type": + data.Set("type", f.Value) + case "notesPlain": + data.Set("note_value", f.Value) + default: + field["id"] = f.ID + field["label"] = f.Label + field["type"] = f.Type + field["value"] = f.Value + fields = append(fields, field) + } + + if f.Recipe != nil { + charSets := map[string]bool{} + for _, s := range f.Recipe.CharacterSets { + charSets[strings.ToLower(s)] = true + } + + dataRecipe := map[string]interface{}{ + "length": f.Recipe.Length, + "letters": charSets["letters"], + "digits": charSets["digits"], + "symbols": charSets["symbols"], + } + field["password_recipe"] = dataRecipe } } } + data.Set("field", fields) } func dataToItem(data *schema.ResourceData) (*onepassword.Item, error) { @@ -538,6 +585,55 @@ func dataToItem(data *schema.ResourceData) (*onepassword.Item, error) { Value: data.Get("type").(string), }, } + case "secure_note": + item.Category = onepassword.SecureNote + } + + if data.Get("note_value") != nil { + note := onepassword.ItemField{ + ID: "notesPlain", + Label: "notesPlain", + Purpose: "NOTES", + Type: "STRING", + Value: data.Get("note_value").(string), + } + item.Fields = append(item.Fields, ¬e) + } + + fields := data.Get("field").([]interface{}) + for i := 0; i < len(fields); i++ { + field, ok := fields[i].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("Unable to parse field: %v", fields[i]) + } + fid, err := uuid.GenerateUUID() + if err != nil { + return nil, fmt.Errorf("Unable to generate a field id: %w", err) + } + + if field["id"].(string) != "" { + fid = field["id"].(string) + } else { + field["id"] = fid + } + + f := &onepassword.ItemField{ + ID: fid, + Type: field["type"].(string), + Label: field["label"].(string), + Value: field["value"].(string), + } + + recipe, err := parseGeneratorRecipe(field["password_recipe"].([]interface{})) + if err != nil { + return nil, err + } + + if recipe != nil { + addRecipe(f, recipe) + } + + item.Fields = append(item.Fields, f) } sections := data.Get("section").([]interface{}) @@ -574,7 +670,6 @@ func dataToItem(data *schema.ResourceData) (*onepassword.Item, error) { Section: s, ID: field["id"].(string), Type: field["type"].(string), - Purpose: field["purpose"].(string), Label: field["label"].(string), Value: field["value"].(string), } diff --git a/onepassword/resource_onepassword_item_test.go b/onepassword/resource_onepassword_item_test.go index aa6e877d..0b7aea15 100644 --- a/onepassword/resource_onepassword_item_test.go +++ b/onepassword/resource_onepassword_item_test.go @@ -26,9 +26,10 @@ var schemaKeys = []string{ func TestResourceItemToDataDataToTitem(t *testing.T) { resources := map[string]*schema.ResourceData{ - "login": generateResourceLoginItem(t), - "database": generateResourceDatabaseItem(t), - "password": generateResourcePasswordItem(t), + "login": generateResourceLoginItem(t), + "database": generateResourceDatabaseItem(t), + "password": generateResourcePasswordItem(t), + "secure_note": generateResourceSecureNoteItem(t), } for name, resource := range resources { t.Run(name, func(t *testing.T) { @@ -104,6 +105,26 @@ func TestAddSectionsToItem(t *testing.T) { testCRUDForItem(t, item) } +func TestAddFieldsToItem(t *testing.T) { + item := generateResourceLoginItem(t) + + fields := []interface{}{ + map[string]interface{}{ + "label": "secret value", + "type": "CONCEALED", + "value": "secret", + }, + map[string]interface{}{ + "label": "user", + "value": "root", + }, + } + + item.Set("field", fields) + + testCRUDForItem(t, item) +} + func testCRUDForItem(t *testing.T, itemToCreate *schema.ResourceData) { meta := &testClient{ GetItemFunc: getItem, @@ -133,7 +154,7 @@ func testCRUDForItem(t *testing.T, itemToCreate *schema.ResourceData) { itemToCreate.Set("password", "new_password") err = resourceOnepasswordItemUpdate(itemToCreate, meta) if err != nil { - t.Errorf("Unexpected error occured when deleting item") + t.Errorf("Unexpected error occured when updating item") } err = resourceOnepasswordItemRead(itemRead, meta) if err != nil { @@ -222,6 +243,12 @@ func generateResourcePasswordItem(t *testing.T) *schema.ResourceData { return resourceData } +func generateResourceSecureNoteItem(t *testing.T) *schema.ResourceData { + resourceData := generateBaseItem(t) + resourceData.Set("category", "secure_note") + return resourceData +} + func generateBaseItem(t *testing.T) *schema.ResourceData { resourceData := schema.TestResourceDataRaw(t, resourceOnepasswordItem().Schema, nil) resourceData.Set("uuid", "79841a98-dd4a-4c34-8be5-32dca20a7328") From 6f1254dcbd916902194b357e307c2dd351da9728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priscila=20Sol=C3=ADs=20Garc=C3=ADa?= Date: Tue, 9 May 2023 11:53:19 -0600 Subject: [PATCH 2/2] update docs --- docs/data-sources/item.md | 15 ++++++++++++++- docs/resources/item.md | 39 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/data-sources/item.md b/docs/data-sources/item.md index 623a2295..2f1882a5 100644 --- a/docs/data-sources/item.md +++ b/docs/data-sources/item.md @@ -32,8 +32,9 @@ data "onepassword_item" "example" { ### Read-only -- **category** (String, Read-only) The category of the item. One of ["login" "password" "database"] +- **category** (String, Read-only) The category of the item. One of ["login" "password" "database" "secure_note"] - **database** (String, Read-only) (Only applies to the database category) The name of the database. +- **field** (Block List) A list of custom fields in an item (see [below for nested schema](#nestedblock--field)) - **hostname** (String, Read-only) (Only applies to the database category) The address where the database can be found - **id** (String, Read-only) The Terraform resource identifier for this item in the format `vaults//items/` - **password** (String, Read-only) Password for this item. @@ -44,6 +45,18 @@ data "onepassword_item" "example" { - **url** (String, Read-only) The primary URL for the item. - **username** (String, Read-only) Username for this item. + +### Nested Schema for `field` + +Read-only: + +- **id** (String, Read-only) A unique identifier for the field. +- **label** (String, Read-only) The label for the field. +- **purpose** (String, Read-only) Purpose indicates this is a special field: a username, password, or notes field. One of ["USERNAME" "PASSWORD" "NOTES"] +- **type** (String, Read-only) The type of value stored in the field. One of ["STRING" "EMAIL" "CONCEALED" "URL" "OTP" "DATE" "MONTH_YEAR" "MENU"] +- **value** (String, Read-only) The value of the field. + + ### Nested Schema for `section` diff --git a/docs/resources/item.md b/docs/resources/item.md index 8ab57483..973ec30f 100644 --- a/docs/resources/item.md +++ b/docs/resources/item.md @@ -54,9 +54,11 @@ resource "onepassword_item" "demo_db" { ### Optional -- **category** (String, Optional) The category of the item. One of ["login" "password" "database"] +- **category** (String, Optional) The category of the item. One of ["login" "password" "database" "secure_note"] - **database** (String, Optional) (Only applies to the database category) The name of the database. +- **field** (Block List) A list of custom fields in an item (see [below for nested schema](#nestedblock--field)) - **hostname** (String, Optional) (Only applies to the database category) The address where the database can be found +- **note_value** (String, Optional) Secure Note value. - **password** (String, Optional) Password for this item. - **password_recipe** (Block List, Max: 1) Password for this item. (see [below for nested schema](#nestedblock--password_recipe)) - **port** (String, Optional) (Only applies to the database category) The port the database is listening on. @@ -72,6 +74,35 @@ resource "onepassword_item" "demo_db" { - **id** (String, Read-only) The Terraform resource identifier for this item in the format `vaults//items/`. - **uuid** (String, Read-only) The UUID of the item. Item identifiers are unique within a specific vault. + +### Nested Schema for `field` + +Required: + +- **label** (String, Required) The label for the field. + +Optional: + +- **password_recipe** (Block List, Max: 1) Password for this item. (see [below for nested schema](#nestedblock--field--password_recipe)) +- **type** (String, Optional) The type of value stored in the field. One of ["STRING" "EMAIL" "CONCEALED" "URL" "OTP" "DATE" "MONTH_YEAR" "MENU"] +- **value** (String, Optional) The value of the field. + +Read-only: + +- **id** (String, Read-only) A unique identifier for the field. + + +### Nested Schema for `field.password_recipe` + +Optional: + +- **digits** (Boolean, Optional) Use digits [0-9] when generating the password. +- **length** (Number, Optional) The length of the password to be generated. +- **letters** (Boolean, Optional) Use letters [a-zA-Z] when generating the password. +- **symbols** (Boolean, Optional) Use symbols [!@.-_*] when generating the password. + + + ### Nested Schema for `password_recipe` @@ -107,12 +138,14 @@ Required: Optional: -- **id** (String, Optional) A unique identifier for the field. - **password_recipe** (Block List, Max: 1) Password for this item. (see [below for nested schema](#nestedblock--section--field--password_recipe)) -- **purpose** (String, Optional) Purpose indicates this is a special field: a username, password, or notes field. One of ["USERNAME" "PASSWORD" "NOTES"] - **type** (String, Optional) The type of value stored in the field. One of ["STRING" "EMAIL" "CONCEALED" "URL" "OTP" "DATE" "MONTH_YEAR" "MENU"] - **value** (String, Optional) The value of the field. +Read-only: + +- **id** (String, Read-only) A unique identifier for the field. + ### Nested Schema for `section.field.password_recipe`