From 808e488f99a60e640b2ee90894a12179782e35c1 Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Tue, 26 Apr 2022 15:03:54 -0400 Subject: [PATCH 1/9] Fix race condition on duplicate entity alias creation The creation of entity aliases should be serialized, since we want to handle the case when the user is attempting to create duplicate entity aliases during the same Teraform execution. A duplicate entity alias shares the same mount accessor and name with another entity alias. The fix is to lock the /identity/entity-alias/id path on all create, update, and delete operations. This allows the provider to reliably detect the duplicate entity alias condition, and prevent duplicate alias creation. Other fixes: - all CRUD functions are of the *Context type, and can return meaning diag.Diagnostics - created a new entity package under internal/identity/entity with some helper functions --- vault/resource_identity_entity_alias.go | 230 +++++++++++++------ vault/resource_identity_entity_alias_test.go | 153 +++++++++++- 2 files changed, 315 insertions(+), 68 deletions(-) diff --git a/vault/resource_identity_entity_alias.go b/vault/resource_identity_entity_alias.go index 3676fe05d..fb39bae2c 100644 --- a/vault/resource_identity_entity_alias.go +++ b/vault/resource_identity_entity_alias.go @@ -1,9 +1,12 @@ package vault import ( + "context" "fmt" "log" + "strings" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/vault/api" ) @@ -12,11 +15,10 @@ const identityEntityAliasPath = "/identity/entity-alias" func identityEntityAliasResource() *schema.Resource { return &schema.Resource{ - Create: identityEntityAliasCreate, - Update: identityEntityAliasUpdate, - Read: identityEntityAliasRead, - Delete: identityEntityAliasDelete, - Exists: identityEntityAliasExists, + CreateContext: identityEntityAliasCreate, + UpdateContext: identityEntityAliasUpdate, + ReadContext: identityEntityAliasRead, + DeleteContext: identityEntityAliasDelete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, @@ -51,7 +53,10 @@ func identityEntityAliasResource() *schema.Resource { } } -func identityEntityAliasCreate(d *schema.ResourceData, meta interface{}) error { +func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + path := identityEntityAliasPath + vaultMutexKV.Lock(path) + defer vaultMutexKV.Unlock(path) client := meta.(*api.Client) name := d.Get("name").(string) @@ -59,8 +64,6 @@ func identityEntityAliasCreate(d *schema.ResourceData, meta interface{}) error { canonicalID := d.Get("canonical_id").(string) customMetadata := d.Get("custom_metadata").(map[string]interface{}) - path := identityEntityAliasPath - data := map[string]interface{}{ "name": name, "mount_accessor": mountAccessor, @@ -68,39 +71,106 @@ func identityEntityAliasCreate(d *schema.ResourceData, meta interface{}) error { "custom_metadata": customMetadata, } - resp, err := client.Logical().Write(path, data) + diags := diag.Diagnostics{} + + var duplicates []string + /* + aliases, err := getEntityAliasesByName(client, name) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("Failed to get entity aliases, err=%s", err), + }) + + return diags + } + + for _, config := range aliases { + if config.Data["mount_accessor"].(string) == mountAccessor { + duplicates = append(duplicates, config.Data["id"].(string)) + } + } + */ + a, err := getEntityAliasesByMountAccessor(client, mountAccessor) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("Failed to get entity aliases by mount accessor, err=%s", err), + }) + + return diags + } + + for _, alias := range a { + if v, ok := alias["name"]; ok && v.(string) == name { + duplicates = append(duplicates, alias["id"].(string)) + } + } + + if len(duplicates) > 0 { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf( + "entity alias %q already exists for mount accessor %q, "+ + "ids=%q", name, mountAccessor, strings.Join(duplicates, ",")), + Detail: "In the case where this error occurred during the creation of more than one alias, " + + "it may be necessary to assign a unique alias name to each of affected resources and " + + "then rerun the apply. After a successful apply the desired original alias names can then be " + + "reassigned", + }) + + return diags + } + resp, err := client.Logical().Write(path, data) if err != nil { - return fmt.Errorf("error writing IdentityEntityAlias to %q: %s", name, err) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf( + "error writing IdentityEntityAlias to %q: %s", name, err), + }) + + return diags } if resp == nil { - aliasIDMsg := "Unable to determine alias id." + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf( + "unexpected empty response during entity alias creation name=%q", name), + }) - if aliasID, err := findAliasID(client, canonicalID, name, mountAccessor); err == nil { - aliasIDMsg = fmt.Sprintf("Alias resource ID %q may be imported.", aliasID) - } + return diags - return fmt.Errorf("IdentityEntityAlias %q already exists. %s", name, aliasIDMsg) } log.Printf("[DEBUG] Wrote IdentityEntityAlias %q", name) d.SetId(resp.Data["id"].(string)) - return identityEntityAliasRead(d, meta) + return identityEntityAliasRead(ctx, d, meta) } -func identityEntityAliasUpdate(d *schema.ResourceData, meta interface{}) error { +func identityEntityAliasUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vaultMutexKV.Lock(identityEntityAliasPath) + defer vaultMutexKV.Unlock(identityEntityAliasPath) + client := meta.(*api.Client) id := d.Id() log.Printf("[DEBUG] Updating IdentityEntityAlias %q", id) path := identityEntityAliasIDPath(id) + diags := diag.Diagnostics{} + resp, err := client.Logical().Read(path) if err != nil { - return fmt.Errorf("error updating IdentityEntityAlias %q: %s", id, err) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("error updating IdentityEntityAlias %q: %s", id, err), + }) + + return diags } data := map[string]interface{}{ @@ -124,104 +194,130 @@ func identityEntityAliasUpdate(d *schema.ResourceData, meta interface{}) error { _, err = client.Logical().Write(path, data) if err != nil { - return fmt.Errorf("error updating IdentityEntityAlias %q: %s", id, err) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("error updating IdentityEntityAlias %q: %s", id, err), + }) + + return diags } log.Printf("[DEBUG] Updated IdentityEntityAlias %q", id) - return identityEntityAliasRead(d, meta) + return identityEntityAliasRead(ctx, d, meta) } -func identityEntityAliasRead(d *schema.ResourceData, meta interface{}) error { +func identityEntityAliasRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*api.Client) id := d.Id() path := identityEntityAliasIDPath(id) - log.Printf("[DEBUG] Reading IdentityEntityAlias %s from %q", id, path) + diags := diag.Diagnostics{} + + log.Printf("[DEBUG] Reading IdentityEntityAlias %q from %q", id, path) resp, err := client.Logical().Read(path) if err != nil { - return fmt.Errorf("error reading IdentityEntityAlias %q: %s", id, err) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("error reading IdentityEntityAlias %q: %s", id, err), + }) + + return diags } log.Printf("[DEBUG] Read IdentityEntityAlias %s", id) if resp == nil { log.Printf("[WARN] IdentityEntityAlias %q not found, removing from state", id) d.SetId("") - return nil + + return diags } d.SetId(resp.Data["id"].(string)) for _, k := range []string{"name", "mount_accessor", "canonical_id", "custom_metadata"} { if err := d.Set(k, resp.Data[k]); err != nil { - return fmt.Errorf("error setting state key %q on IdentityEntityAlias %q: err=%q", k, id, err) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("error setting state key %q on IdentityEntityAlias %q: err=%q", k, id, err), + }) + + return diags } } - return nil + + return diags } -func identityEntityAliasDelete(d *schema.ResourceData, meta interface{}) error { +func identityEntityAliasDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + vaultMutexKV.Lock(identityEntityAliasPath) + defer vaultMutexKV.Unlock(identityEntityAliasPath) client := meta.(*api.Client) id := d.Id() path := identityEntityAliasIDPath(id) + diags := diag.Diagnostics{} + log.Printf("[DEBUG] Deleting IdentityEntityAlias %q", id) _, err := client.Logical().Delete(path) if err != nil { - return fmt.Errorf("error IdentityEntityAlias %q", id) + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("error IdentityEntityAlias %q", id), + }) + return diags } log.Printf("[DEBUG] Deleted IdentityEntityAlias %q", id) - return nil + return diags } -func identityEntityAliasExists(d *schema.ResourceData, meta interface{}) (bool, error) { - client := meta.(*api.Client) - id := d.Id() - - path := identityEntityAliasIDPath(id) - key := id - - // use the name if no ID is set - if len(id) == 0 { - key = d.Get("name").(string) - path = identityEntityAliasNamePath(key) +func getEntityAliasesByName(client *api.Client, name string) ([]*api.Secret, error) { + resp, err := client.Logical().List(identityEntityAliasPath + "/id") + if resp == nil || err != nil { + return nil, err } - log.Printf("[DEBUG] Checking if IdentityEntityAlias %q exists", key) - resp, err := client.Logical().Read(path) - if err != nil { - return true, fmt.Errorf("error checking if IdentityEntityAlias %q exists: %s", key, err) - } - log.Printf("[DEBUG] Checked if IdentityEntityAlias %q exists", key) - - return resp != nil, nil -} + var result []*api.Secret + for _, id := range resp.Data["keys"].([]interface{}) { + config, err := client.Logical().Read(identityEntityAliasIDPath(id.(string))) + if err != nil || config == nil { + continue + } -func identityEntityAliasNamePath(name string) string { - return fmt.Sprintf("%s/name/%s", identityEntityAliasPath, name) -} + if config.Data["name"].(string) == name { + result = append(result, config) + } + } -func identityEntityAliasIDPath(id string) string { - return fmt.Sprintf("%s/id/%s", identityEntityAliasPath, id) + return result, err } -func findAliasID(client *api.Client, canonicalID, name, mountAccessor string) (string, error) { - path := identityEntityIDPath(canonicalID) - - resp, err := client.Logical().Read(path) - if err != nil { - return "", fmt.Errorf("error reading entity aliases: %s", err) +func getEntityAliasesByMountAccessor(client *api.Client, accessor string) ([]map[string]interface{}, error) { + resp, err := client.Logical().List(identityEntityPath + "/id") + if resp == nil || err != nil { + return nil, err } - if resp != nil { - aliases := resp.Data["aliases"].([]interface{}) - for _, aliasRaw := range aliases { - alias := aliasRaw.(map[string]interface{}) - if alias["name"] == name && alias["mount_accessor"] == mountAccessor { - return alias["id"].(string), nil + result := make([]map[string]interface{}, 0) + for _, id := range resp.Data["keys"].([]interface{}) { + config, err := client.Logical().Read(identityEntityIDPath(id.(string))) + if err != nil || config == nil { + continue + } + + if aliases, ok := config.Data["aliases"]; ok { + for _, v := range aliases.([]interface{}) { + alias := v.(map[string]interface{}) + if alias["mount_accessor"].(string) == accessor { + result = append(result, alias) + } } } } - return "", fmt.Errorf("unable to determine alias ID. canonical ID: %q name: %q mountAccessor: %q", canonicalID, name, mountAccessor) + return result, err +} + +func identityEntityAliasIDPath(id string) string { + return fmt.Sprintf("%s/id/%s", identityEntityAliasPath, id) } diff --git a/vault/resource_identity_entity_alias_test.go b/vault/resource_identity_entity_alias_test.go index d461de2a9..6a07a3b1e 100644 --- a/vault/resource_identity_entity_alias_test.go +++ b/vault/resource_identity_entity_alias_test.go @@ -33,9 +33,160 @@ func TestAccIdentityEntityAlias(t *testing.T) { resource.TestCheckResourceAttrPair(nameEntityAlias, "mount_accessor", nameGithubA, "accessor"), ), }, + { + ResourceName: nameEntityAlias, + ImportState: true, + ImportStateVerify: true, + }, { Config: testAccIdentityEntityAliasConfig(entity, true, false), - ExpectError: regexp.MustCompile(`IdentityEntityAlias.*already exists.*may be imported`), + ExpectError: regexp.MustCompile(`entity alias .+ already exists`), + }, + }, + }) +} + +func TestAccIdentityEntityAliasDuplicateFlow(t *testing.T) { + namePrefix := acctest.RandomWithPrefix("test-duplicate-flow") + alias := acctest.RandomWithPrefix("alias") + + configTmpl := fmt.Sprintf(` +variable name_prefix { + default = "%s" +} + +variable entity_alias_name_1 { + default = "%%s" +} +variable entity_alias_name_2 { + default = "%%s" +} + +resource "vault_auth_backend" "test" { + path = "cert/${var.name_prefix}" + type = "cert" +} + +resource "vault_cert_auth_backend_role" "test" { + name = var.name_prefix + backend = vault_auth_backend.test.path + certificate = < Date: Thu, 28 Apr 2022 15:37:13 -0400 Subject: [PATCH 2/9] Move some identity/entity specific code to its own package --- internal/identity/entity/entity.go | 71 ++++++++++++++++++ vault/resource_identity_entity.go | 19 ++--- vault/resource_identity_entity_alias.go | 73 +++---------------- vault/resource_identity_entity_alias_test.go | 3 +- vault/resource_identity_entity_policies.go | 5 +- .../resource_identity_entity_policies_test.go | 5 +- vault/resource_identity_entity_test.go | 21 +++--- 7 files changed, 108 insertions(+), 89 deletions(-) create mode 100644 internal/identity/entity/entity.go diff --git a/internal/identity/entity/entity.go b/internal/identity/entity/entity.go new file mode 100644 index 000000000..f825129b5 --- /dev/null +++ b/internal/identity/entity/entity.go @@ -0,0 +1,71 @@ +package entity + +import ( + "fmt" + + "github.com/hashicorp/vault/api" +) + +const ( + IdentityEntityAliasPath = "/identity/entity-alias" + IdentityEntityPath = "/identity/entity" +) + +// GetAliasesByName +func GetAliasesByName(client *api.Client, name string) ([]*api.Secret, error) { + resp, err := client.Logical().List(IdentityEntityAliasPath + "/id") + if resp == nil || err != nil { + return nil, err + } + + var result []*api.Secret + for _, id := range resp.Data["keys"].([]interface{}) { + config, err := client.Logical().Read(AliasIDPath(id.(string))) + if err != nil || config == nil { + continue + } + + if config.Data["name"].(string) == name { + result = append(result, config) + } + } + + return result, err +} + +// GetAliasesByMountAccessor +func GetAliasesByMountAccessor(client *api.Client, accessor string) ([]map[string]interface{}, error) { + resp, err := client.Logical().List(IdentityEntityPath + "/id") + if resp == nil || err != nil { + return nil, err + } + + result := make([]map[string]interface{}, 0) + for _, id := range resp.Data["keys"].([]interface{}) { + config, err := client.Logical().Read(IDPath(id.(string))) + if err != nil || config == nil { + continue + } + + if aliases, ok := config.Data["aliases"]; ok { + for _, v := range aliases.([]interface{}) { + alias := v.(map[string]interface{}) + if alias["mount_accessor"].(string) == accessor { + result = append(result, alias) + } + } + } + } + + return result, err +} + +// AliasIDPath +func AliasIDPath(id string) string { + return fmt.Sprintf("%s/id/%s", IdentityEntityAliasPath, id) +} + +// IDPath +func IDPath(id string) string { + return fmt.Sprintf("%s/id/%s", IdentityEntityPath, id) +} diff --git a/vault/resource_identity_entity.go b/vault/resource_identity_entity.go index e5364686d..5757d1425 100644 --- a/vault/resource_identity_entity.go +++ b/vault/resource_identity_entity.go @@ -8,11 +8,10 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/vault/api" + "github.com/hashicorp/terraform-provider-vault/internal/identity/entity" "github.com/hashicorp/terraform-provider-vault/util" ) -const identityEntityPath = "/identity/entity" - var errEntityNotFound = errors.New("entity not found") func identityEntityResource() *schema.Resource { @@ -112,7 +111,7 @@ func identityEntityCreate(d *schema.ResourceData, meta interface{}) error { name := d.Get("name").(string) - path := identityEntityPath + path := entity.IdentityEntityPath data := map[string]interface{}{ "name": name, @@ -148,7 +147,7 @@ func identityEntityUpdate(d *schema.ResourceData, meta interface{}) error { id := d.Id() log.Printf("[DEBUG] Updating IdentityEntity %q", id) - path := identityEntityIDPath(id) + path := entity.IDPath(id) vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) @@ -198,7 +197,7 @@ func identityEntityDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*api.Client) id := d.Id() - path := identityEntityIDPath(id) + path := entity.IDPath(id) vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) @@ -217,7 +216,7 @@ func identityEntityExists(d *schema.ResourceData, meta interface{}) (bool, error client := meta.(*api.Client) id := d.Id() - path := identityEntityIDPath(id) + path := entity.IDPath(id) key := id // use the name if no ID is set @@ -237,11 +236,7 @@ func identityEntityExists(d *schema.ResourceData, meta interface{}) (bool, error } func identityEntityNamePath(name string) string { - return fmt.Sprintf("%s/name/%s", identityEntityPath, name) -} - -func identityEntityIDPath(id string) string { - return fmt.Sprintf("%s/id/%s", identityEntityPath, id) + return fmt.Sprintf("%s/name/%s", entity.IdentityEntityPath, name) } func readIdentityEntityPolicies(client *api.Client, entityID string) ([]interface{}, error) { @@ -257,7 +252,7 @@ func readIdentityEntityPolicies(client *api.Client, entityID string) ([]interfac } func readIdentityEntity(client *api.Client, entityID string, retry bool) (*api.Secret, error) { - path := identityEntityIDPath(entityID) + path := entity.IDPath(entityID) log.Printf("[DEBUG] Reading Entity %q from %q", entityID, path) return readEntity(client, path, retry) diff --git a/vault/resource_identity_entity_alias.go b/vault/resource_identity_entity_alias.go index fb39bae2c..2a990b518 100644 --- a/vault/resource_identity_entity_alias.go +++ b/vault/resource_identity_entity_alias.go @@ -9,9 +9,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/vault/api" -) -const identityEntityAliasPath = "/identity/entity-alias" + "github.com/hashicorp/terraform-provider-vault/internal/identity/entity" +) func identityEntityAliasResource() *schema.Resource { return &schema.Resource{ @@ -54,7 +54,7 @@ func identityEntityAliasResource() *schema.Resource { } func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - path := identityEntityAliasPath + path := entity.IdentityEntityAliasPath vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) client := meta.(*api.Client) @@ -91,7 +91,7 @@ func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta } } */ - a, err := getEntityAliasesByMountAccessor(client, mountAccessor) + a, err := entity.GetAliasesByMountAccessor(client, mountAccessor) if err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, @@ -152,14 +152,14 @@ func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta } func identityEntityAliasUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - vaultMutexKV.Lock(identityEntityAliasPath) - defer vaultMutexKV.Unlock(identityEntityAliasPath) + vaultMutexKV.Lock(entity.IdentityEntityAliasPath) + defer vaultMutexKV.Unlock(entity.IdentityEntityAliasPath) client := meta.(*api.Client) id := d.Id() log.Printf("[DEBUG] Updating IdentityEntityAlias %q", id) - path := identityEntityAliasIDPath(id) + path := entity.AliasIDPath(id) diags := diag.Diagnostics{} @@ -210,7 +210,7 @@ func identityEntityAliasRead(ctx context.Context, d *schema.ResourceData, meta i client := meta.(*api.Client) id := d.Id() - path := identityEntityAliasIDPath(id) + path := entity.AliasIDPath(id) diags := diag.Diagnostics{} @@ -248,12 +248,12 @@ func identityEntityAliasRead(ctx context.Context, d *schema.ResourceData, meta i } func identityEntityAliasDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - vaultMutexKV.Lock(identityEntityAliasPath) - defer vaultMutexKV.Unlock(identityEntityAliasPath) + vaultMutexKV.Lock(entity.IdentityEntityAliasPath) + defer vaultMutexKV.Unlock(entity.IdentityEntityAliasPath) client := meta.(*api.Client) id := d.Id() - path := identityEntityAliasIDPath(id) + path := entity.AliasIDPath(id) diags := diag.Diagnostics{} @@ -270,54 +270,3 @@ func identityEntityAliasDelete(ctx context.Context, d *schema.ResourceData, meta return diags } - -func getEntityAliasesByName(client *api.Client, name string) ([]*api.Secret, error) { - resp, err := client.Logical().List(identityEntityAliasPath + "/id") - if resp == nil || err != nil { - return nil, err - } - - var result []*api.Secret - for _, id := range resp.Data["keys"].([]interface{}) { - config, err := client.Logical().Read(identityEntityAliasIDPath(id.(string))) - if err != nil || config == nil { - continue - } - - if config.Data["name"].(string) == name { - result = append(result, config) - } - } - - return result, err -} - -func getEntityAliasesByMountAccessor(client *api.Client, accessor string) ([]map[string]interface{}, error) { - resp, err := client.Logical().List(identityEntityPath + "/id") - if resp == nil || err != nil { - return nil, err - } - - result := make([]map[string]interface{}, 0) - for _, id := range resp.Data["keys"].([]interface{}) { - config, err := client.Logical().Read(identityEntityIDPath(id.(string))) - if err != nil || config == nil { - continue - } - - if aliases, ok := config.Data["aliases"]; ok { - for _, v := range aliases.([]interface{}) { - alias := v.(map[string]interface{}) - if alias["mount_accessor"].(string) == accessor { - result = append(result, alias) - } - } - } - } - - return result, err -} - -func identityEntityAliasIDPath(id string) string { - return fmt.Sprintf("%s/id/%s", identityEntityAliasPath, id) -} diff --git a/vault/resource_identity_entity_alias_test.go b/vault/resource_identity_entity_alias_test.go index 6a07a3b1e..4818b8d4d 100644 --- a/vault/resource_identity_entity_alias_test.go +++ b/vault/resource_identity_entity_alias_test.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/vault/api" + "github.com/hashicorp/terraform-provider-vault/internal/identity/entity" "github.com/hashicorp/terraform-provider-vault/testutil" ) @@ -233,7 +234,7 @@ func testAccCheckIdentityEntityAliasDestroy(s *terraform.State) error { if rs.Type != "vault_identity_entity_alias" { continue } - secret, err := client.Logical().Read(identityEntityAliasIDPath(rs.Primary.ID)) + secret, err := client.Logical().Read(entity.AliasIDPath(rs.Primary.ID)) if err != nil { return fmt.Errorf("error checking for identity entity %q: %s", rs.Primary.ID, err) } diff --git a/vault/resource_identity_entity_policies.go b/vault/resource_identity_entity_policies.go index 1c9b14f5a..44fd05364 100644 --- a/vault/resource_identity_entity_policies.go +++ b/vault/resource_identity_entity_policies.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/vault/api" + "github.com/hashicorp/terraform-provider-vault/internal/identity/entity" "github.com/hashicorp/terraform-provider-vault/util" ) @@ -54,7 +55,7 @@ func identityEntityPoliciesUpdate(d *schema.ResourceData, meta interface{}) erro id := d.Get("entity_id").(string) log.Printf("[DEBUG] Updating IdentityEntityPolicies %q", id) - path := identityEntityIDPath(id) + path := entity.IDPath(id) vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) @@ -137,7 +138,7 @@ func identityEntityPoliciesDelete(d *schema.ResourceData, meta interface{}) erro id := d.Get("entity_id").(string) log.Printf("[DEBUG] Deleting IdentityEntityPolicies %q", id) - path := identityEntityIDPath(id) + path := entity.IDPath(id) vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) diff --git a/vault/resource_identity_entity_policies_test.go b/vault/resource_identity_entity_policies_test.go index 47de88bee..924242e1d 100644 --- a/vault/resource_identity_entity_policies_test.go +++ b/vault/resource_identity_entity_policies_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/vault/api" + "github.com/hashicorp/terraform-provider-vault/internal/identity/entity" "github.com/hashicorp/terraform-provider-vault/testutil" "github.com/hashicorp/terraform-provider-vault/util" ) @@ -122,7 +123,7 @@ func testAccIdentityEntityPoliciesCheckAttrs(resource string) resource.TestCheck id := instanceState.ID - path := identityEntityIDPath(id) + path := entity.IDPath(id) client := testProvider.Meta().(*api.Client) resp, err := client.Logical().Read(path) if err != nil { @@ -217,7 +218,7 @@ func testAccIdentityEntityPoliciesCheckLogical(resource string, policies []strin id := instanceState.ID - path := identityEntityIDPath(id) + path := entity.IDPath(id) client := testProvider.Meta().(*api.Client) resp, err := client.Logical().Read(path) if err != nil { diff --git a/vault/resource_identity_entity_test.go b/vault/resource_identity_entity_test.go index a648d4931..e8352a5c9 100644 --- a/vault/resource_identity_entity_test.go +++ b/vault/resource_identity_entity_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/vault/api" + "github.com/hashicorp/terraform-provider-vault/internal/identity/entity" "github.com/hashicorp/terraform-provider-vault/testutil" ) @@ -119,7 +120,7 @@ func testAccCheckIdentityEntityDestroy(s *terraform.State) error { if rs.Type != "vault_identity_entity" { continue } - secret, err := client.Logical().Read(identityEntityIDPath(rs.Primary.ID)) + secret, err := client.Logical().Read(entity.IDPath(rs.Primary.ID)) if err != nil { return fmt.Errorf("error checking for identity entity %q: %s", rs.Primary.ID, err) } @@ -144,7 +145,7 @@ func testAccIdentityEntityCheckAttrs() resource.TestCheckFunc { id := instanceState.ID - path := identityEntityIDPath(id) + path := entity.IDPath(id) client := testProvider.Meta().(*api.Client) resp, err := client.Logical().Read(path) if err != nil { @@ -308,7 +309,7 @@ func TestReadEntity(t *testing.T) { }, { name: "retry-exhausted-default-max-404", - path: identityEntityIDPath("retry-exhausted-default-max-404"), + path: entity.IDPath("retry-exhausted-default-max-404"), retryHandler: &testRetryHandler{ okAtCount: 0, retryStatus: http.StatusNotFound, @@ -316,11 +317,11 @@ func TestReadEntity(t *testing.T) { maxRetries: DefaultMaxHTTPRetriesCCC, expectedRetries: DefaultMaxHTTPRetriesCCC, wantError: fmt.Errorf(`%w: %q`, errEntityNotFound, - identityEntityIDPath("retry-exhausted-default-max-404")), + entity.IDPath("retry-exhausted-default-max-404")), }, { name: "retry-exhausted-default-max-412", - path: identityEntityIDPath("retry-exhausted-default-max-412"), + path: entity.IDPath("retry-exhausted-default-max-412"), retryHandler: &testRetryHandler{ okAtCount: 0, retryStatus: http.StatusPreconditionFailed, @@ -328,11 +329,11 @@ func TestReadEntity(t *testing.T) { maxRetries: DefaultMaxHTTPRetriesCCC, expectedRetries: DefaultMaxHTTPRetriesCCC, wantError: fmt.Errorf(`failed reading %q`, - identityEntityIDPath("retry-exhausted-default-max-412")), + entity.IDPath("retry-exhausted-default-max-412")), }, { name: "retry-exhausted-custom-max-404", - path: identityEntityIDPath("retry-exhausted-custom-max-404"), + path: entity.IDPath("retry-exhausted-custom-max-404"), retryHandler: &testRetryHandler{ okAtCount: 0, retryStatus: http.StatusNotFound, @@ -340,11 +341,11 @@ func TestReadEntity(t *testing.T) { maxRetries: 5, expectedRetries: 5, wantError: fmt.Errorf(`%w: %q`, errEntityNotFound, - identityEntityIDPath("retry-exhausted-custom-max-404")), + entity.IDPath("retry-exhausted-custom-max-404")), }, { name: "retry-exhausted-custom-max-412", - path: identityEntityIDPath("retry-exhausted-custom-max-412"), + path: entity.IDPath("retry-exhausted-custom-max-412"), retryHandler: &testRetryHandler{ okAtCount: 0, retryStatus: http.StatusPreconditionFailed, @@ -352,7 +353,7 @@ func TestReadEntity(t *testing.T) { maxRetries: 5, expectedRetries: 5, wantError: fmt.Errorf(`failed reading %q`, - identityEntityIDPath("retry-exhausted-custom-max-412")), + entity.IDPath("retry-exhausted-custom-max-412")), }, } From 400f139dedb610f18786e45c3bda5e0affcb12e3 Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Thu, 28 Apr 2022 17:24:01 -0400 Subject: [PATCH 3/9] Provide a single function for finding entity aliases --- go.mod | 1 + internal/identity/entity/entity.go | 59 ++++++++++++++----------- vault/resource_identity_entity_alias.go | 29 +++--------- 3 files changed, 42 insertions(+), 47 deletions(-) diff --git a/go.mod b/go.mod index a36774012..534cfbac1 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/hashicorp/vault/api v1.3.2-0.20211222220726-b046cd9f80eb github.com/hashicorp/vault/sdk v0.3.1-0.20211214161113-fcc5f22bea02 github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/mapstructure v1.4.2 github.com/opencontainers/image-spec v1.0.2 // indirect golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 diff --git a/internal/identity/entity/entity.go b/internal/identity/entity/entity.go index f825129b5..016aaaff3 100644 --- a/internal/identity/entity/entity.go +++ b/internal/identity/entity/entity.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/hashicorp/vault/api" + "github.com/mitchellh/mapstructure" ) const ( @@ -11,36 +12,34 @@ const ( IdentityEntityPath = "/identity/entity" ) -// GetAliasesByName -func GetAliasesByName(client *api.Client, name string) ([]*api.Secret, error) { - resp, err := client.Logical().List(IdentityEntityAliasPath + "/id") - if resp == nil || err != nil { - return nil, err - } - - var result []*api.Secret - for _, id := range resp.Data["keys"].([]interface{}) { - config, err := client.Logical().Read(AliasIDPath(id.(string))) - if err != nil || config == nil { - continue - } - - if config.Data["name"].(string) == name { - result = append(result, config) - } - } +type Alias struct { + CanonicalId string `mapstructure:"canonical_id"` + CreationTime string `mapstructure:"creation_time"` + CustomMetadata map[string]interface{} `mapstructure:"custom_metadata"` + ID string `mapstructure:"id"` + LastUpdateTime string `mapstructure:"last_update_time"` + Local bool `mapstructure:"local"` + MergedFromCanonicalIds interface{} `mapstructure:"merged_from_canonical_ids"` + Metadata interface{} `mapstructure:"metadata"` + MountAccessor string `mapstructure:"mount_accessor"` + MountPath string `mapstructure:"mount_path"` + MountType string `mapstructure:"mount_type"` + Name string `mapstructure:"name"` +} - return result, err +// FindAliasParams +type FindAliasParams struct { + Name string + MountAccessor string } -// GetAliasesByMountAccessor -func GetAliasesByMountAccessor(client *api.Client, accessor string) ([]map[string]interface{}, error) { +func FindAliases(client *api.Client, params *FindAliasParams) ([]*Alias, error) { resp, err := client.Logical().List(IdentityEntityPath + "/id") if resp == nil || err != nil { return nil, err } - result := make([]map[string]interface{}, 0) + var result []*Alias for _, id := range resp.Data["keys"].([]interface{}) { config, err := client.Logical().Read(IDPath(id.(string))) if err != nil || config == nil { @@ -49,10 +48,20 @@ func GetAliasesByMountAccessor(client *api.Client, accessor string) ([]map[strin if aliases, ok := config.Data["aliases"]; ok { for _, v := range aliases.([]interface{}) { - alias := v.(map[string]interface{}) - if alias["mount_accessor"].(string) == accessor { - result = append(result, alias) + var a Alias + if err := mapstructure.Decode(v, &a); err != nil { + return nil, err } + + if params.Name != "" && a.Name != params.Name { + continue + } + + if params.MountAccessor != "" && a.MountAccessor != params.MountAccessor { + continue + } + + result = append(result, &a) } } } diff --git a/vault/resource_identity_entity_alias.go b/vault/resource_identity_entity_alias.go index 2a990b518..eae30c760 100644 --- a/vault/resource_identity_entity_alias.go +++ b/vault/resource_identity_entity_alias.go @@ -74,24 +74,11 @@ func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta diags := diag.Diagnostics{} var duplicates []string - /* - aliases, err := getEntityAliasesByName(client, name) - if err != nil { - diags = append(diags, diag.Diagnostic{ - Severity: diag.Error, - Summary: fmt.Sprintf("Failed to get entity aliases, err=%s", err), - }) - - return diags - } - for _, config := range aliases { - if config.Data["mount_accessor"].(string) == mountAccessor { - duplicates = append(duplicates, config.Data["id"].(string)) - } - } - */ - a, err := entity.GetAliasesByMountAccessor(client, mountAccessor) + aliases, err := entity.FindAliases(client, &entity.FindAliasParams{ + Name: name, + MountAccessor: mountAccessor, + }) if err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, @@ -101,13 +88,11 @@ func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta return diags } - for _, alias := range a { - if v, ok := alias["name"]; ok && v.(string) == name { - duplicates = append(duplicates, alias["id"].(string)) + if len(aliases) > 0 { + for _, alias := range aliases { + duplicates = append(duplicates, alias.ID) } - } - if len(duplicates) > 0 { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: fmt.Sprintf( From f4ed49160ad56bf0ce5350376b4cb12f2458730c Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Fri, 29 Apr 2022 10:36:25 -0400 Subject: [PATCH 4/9] Update FindAliases() to return on a entity ID read error --- internal/identity/entity/entity.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/identity/entity/entity.go b/internal/identity/entity/entity.go index 016aaaff3..b8c1161cc 100644 --- a/internal/identity/entity/entity.go +++ b/internal/identity/entity/entity.go @@ -42,7 +42,11 @@ func FindAliases(client *api.Client, params *FindAliasParams) ([]*Alias, error) var result []*Alias for _, id := range resp.Data["keys"].([]interface{}) { config, err := client.Logical().Read(IDPath(id.(string))) - if err != nil || config == nil { + if err != nil { + return nil, err + } + + if config == nil { continue } From 82f1332a68c76034aceba73120adb7da934a000b Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Fri, 29 Apr 2022 17:10:56 -0400 Subject: [PATCH 5/9] Add tests for FindAliases --- internal/identity/entity/entity.go | 78 +++-- internal/identity/entity/entity_test.go | 275 ++++++++++++++++++ testutil/testutil.go | 20 ++ vault/resource_identity_entity.go | 12 +- vault/resource_identity_entity_alias.go | 16 +- vault/resource_identity_entity_alias_test.go | 2 +- vault/resource_identity_entity_policies.go | 4 +- .../resource_identity_entity_policies_test.go | 4 +- vault/resource_identity_entity_test.go | 41 +-- 9 files changed, 379 insertions(+), 73 deletions(-) create mode 100644 internal/identity/entity/entity_test.go diff --git a/internal/identity/entity/entity.go b/internal/identity/entity/entity.go index b8c1161cc..4cc155815 100644 --- a/internal/identity/entity/entity.go +++ b/internal/identity/entity/entity.go @@ -8,40 +8,70 @@ import ( ) const ( - IdentityEntityAliasPath = "/identity/entity-alias" - IdentityEntityPath = "/identity/entity" + RootEntityPath = "/identity/entity" + RootEntityIDPath = RootEntityPath + "/id" + RootAliasPath = RootEntityPath + "-alias" + RootAliasIDPath = RootAliasPath + "/id" ) +// Entity represents a Vault identity entity +type Entity struct { + Aliases []*Alias `mapstructure:"aliases" json:"aliases,omitempty"` + CreationTime string `mapstructure:"creation_time" json:"creation_time"` + DirectGroupIds []interface{} `mapstructure:"direct_group_ids" json:"direct_group_ids,omitempty"` + Disabled bool `mapstructure:"disabled" json:"disabled,omitempty"` + GroupIds []interface{} `mapstructure:"group_ids" json:"group_ids,omitempty"` + ID string `mapstructure:"id" json:"id,omitempty"` + InheritedGroupIds []interface{} `mapstructure:"inherited_group_ids" json:"inherited_group_ids,omitempty"` + LastUpdateTime string `mapstructure:"last_update_time" json:"last_update_time"` + MergedEntityIds interface{} `mapstructure:"merged_entity_ids" json:"merged_entity_ids,omitempty"` + Metadata interface{} `mapstructure:"metadata" json:"metadata,omitempty"` + MfaSecrets struct{} `mapstructure:"mfa_secrets" json:"mfa_secrets"` + Name string `mapstructure:"name" json:"name,omitempty"` + NamespaceId string `mapstructure:"namespace_id" json:"namespace_id,omitempty"` + Policies []string `mapstructure:"policies" json:"policies,omitempty"` +} + +// Alias represents a Vault identity entity alias as it is associated to its Entity. type Alias struct { - CanonicalId string `mapstructure:"canonical_id"` - CreationTime string `mapstructure:"creation_time"` - CustomMetadata map[string]interface{} `mapstructure:"custom_metadata"` - ID string `mapstructure:"id"` - LastUpdateTime string `mapstructure:"last_update_time"` - Local bool `mapstructure:"local"` - MergedFromCanonicalIds interface{} `mapstructure:"merged_from_canonical_ids"` - Metadata interface{} `mapstructure:"metadata"` - MountAccessor string `mapstructure:"mount_accessor"` - MountPath string `mapstructure:"mount_path"` - MountType string `mapstructure:"mount_type"` - Name string `mapstructure:"name"` + CanonicalId string `mapstructure:"canonical_id" json:"canonical_id,omitempty"` + CreationTime string `mapstructure:"creation_time" json:"creation_time,omitempty"` + CustomMetadata map[string]interface{} `mapstructure:"custom_metadata" json:"custom_metadata,omitempty"` + ID string `mapstructure:"id" json:"id,omitempty"` + LastUpdateTime string `mapstructure:"last_update_time" json:"last_update_time,omitempty"` + Local bool `mapstructure:"local" json:"local,omitempty"` + MergedFromCanonicalIds interface{} `mapstructure:"merged_from_canonical_ids" json:"merged_from_canonical_ids,omitempty"` + Metadata interface{} `mapstructure:"metadata" json:"metadata,omitempty"` + MountAccessor string `mapstructure:"mount_accessor" json:"mount_accessor,omitempty"` + MountPath string `mapstructure:"mount_path" json:"mount_path,omitempty"` + MountType string `mapstructure:"mount_type" json:"mount_type,omitempty"` + Name string `mapstructure:"name" json:"name,omitempty"` } // FindAliasParams type FindAliasParams struct { - Name string + // Name to constrain the search to. + Name string + // MountAccessor to constrain the search to. MountAccessor string } +// FindAliases for the given FindAliasParams. func FindAliases(client *api.Client, params *FindAliasParams) ([]*Alias, error) { - resp, err := client.Logical().List(IdentityEntityPath + "/id") + resp, err := client.Logical().List(RootEntityIDPath) if resp == nil || err != nil { return nil, err } + entityIDs, ok := resp.Data["keys"] + if !ok || entityIDs == nil { + return nil, nil + } + var result []*Alias - for _, id := range resp.Data["keys"].([]interface{}) { - config, err := client.Logical().Read(IDPath(id.(string))) + + for _, id := range entityIDs.([]interface{}) { + config, err := client.Logical().Read(JoinEntityID(id.(string))) if err != nil { return nil, err } @@ -73,12 +103,12 @@ func FindAliases(client *api.Client, params *FindAliasParams) ([]*Alias, error) return result, err } -// AliasIDPath -func AliasIDPath(id string) string { - return fmt.Sprintf("%s/id/%s", IdentityEntityAliasPath, id) +// JoinAliasID to the root alias ID path. +func JoinAliasID(id string) string { + return fmt.Sprintf("%s/%s", RootAliasIDPath, id) } -// IDPath -func IDPath(id string) string { - return fmt.Sprintf("%s/id/%s", IdentityEntityPath, id) +// JoinEntityID to the root entity ID path. +func JoinEntityID(id string) string { + return fmt.Sprintf("%s/%s", RootEntityIDPath, id) } diff --git a/internal/identity/entity/entity_test.go b/internal/identity/entity/entity_test.go new file mode 100644 index 000000000..1023788f6 --- /dev/null +++ b/internal/identity/entity/entity_test.go @@ -0,0 +1,275 @@ +package entity + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "regexp" + "testing" + + "github.com/hashicorp/vault/api" + + "github.com/hashicorp/terraform-provider-vault/testutil" +) + +type testFindAliasHandler struct { + requests int + retryStatus int + wantErrOnList bool + wantErrOnRead bool + entities []*Entity +} + +func (t *testFindAliasHandler) handler() http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + t.requests++ + + wantPath := "/v1" + RootEntityIDPath + pathRE := regexp.MustCompile(fmt.Sprintf(`^%s/(.+)`, wantPath)) + path := req.URL.Path + values := req.URL.Query() + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusBadRequest) + return + } + + var data map[string]interface{} + if path == wantPath && values.Get("list") == "true" { + if t.wantErrOnList { + w.WriteHeader(http.StatusInternalServerError) + return + } + + var ids []interface{} + for _, e := range t.entities { + ids = append(ids, e.ID) + } + data = map[string]interface{}{ + "keys": ids, + } + } else { + if t.wantErrOnRead { + w.WriteHeader(http.StatusInternalServerError) + return + } + + match := pathRE.FindStringSubmatch(path) + if len(match) > 1 { + id := match[1] + for _, e := range t.entities { + if e.ID == id { + b, err := json.Marshal(e) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + if err := json.Unmarshal(b, &data); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + } + } + + } + } + + m, err := json.Marshal( + &api.Secret{ + Data: data, + }, + ) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + w.Write(m) + } +} + +func TestFindAliases(t *testing.T) { + aliasBob := &Alias{ + Name: "bob", + MountAccessor: "CC417368-0C63-407A-93AD-2D76A72F58E2", + } + + aliasAlice := &Alias{ + Name: "alice", + MountAccessor: "CC417368-0C63-407A-93AD-2D76A72F58E3", + } + tests := []struct { + name string + params *FindAliasParams + want []*Alias + findHandler *testFindAliasHandler + wantErr bool + }{ + { + name: "empty", + params: &FindAliasParams{}, + findHandler: &testFindAliasHandler{ + entities: []*Entity{}, + }, + want: nil, + wantErr: false, + }, + { + name: "all", + params: &FindAliasParams{}, + findHandler: &testFindAliasHandler{ + entities: []*Entity{ + { + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", + Aliases: []*Alias{ + aliasBob, + }, + }, + { + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932B", + Aliases: []*Alias{ + aliasAlice, + }, + }, + }, + }, + want: []*Alias{ + aliasBob, + aliasAlice, + }, + wantErr: false, + }, + { + name: "name-only", + params: &FindAliasParams{ + Name: aliasBob.Name, + }, + findHandler: &testFindAliasHandler{ + entities: []*Entity{ + { + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", + Aliases: []*Alias{ + aliasBob, + }, + }, + { + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932B", + Aliases: []*Alias{ + aliasAlice, + }, + }, + }, + }, + want: []*Alias{ + aliasBob, + }, + wantErr: false, + }, + { + name: "name-and-mount-accessor", + params: &FindAliasParams{ + Name: aliasBob.Name, + MountAccessor: aliasBob.MountAccessor, + }, + findHandler: &testFindAliasHandler{ + entities: []*Entity{ + { + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", + Aliases: []*Alias{ + aliasAlice, + }, + }, + { + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", + Aliases: []*Alias{ + aliasBob, + }, + }, + }, + }, + want: []*Alias{ + aliasBob, + }, + wantErr: false, + }, + { + name: "mount-accessor-mismatch", + params: &FindAliasParams{ + Name: aliasBob.Name, + MountAccessor: aliasAlice.MountAccessor, + }, + findHandler: &testFindAliasHandler{ + entities: []*Entity{ + { + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", + Aliases: []*Alias{ + aliasAlice, + }, + }, + { + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", + Aliases: []*Alias{ + aliasBob, + }, + }, + }, + }, + want: nil, + wantErr: false, + }, + { + name: "error-on-list", + params: &FindAliasParams{ + Name: aliasAlice.Name, + MountAccessor: aliasBob.MountAccessor, + }, + findHandler: &testFindAliasHandler{ + wantErrOnList: true, + }, + want: nil, + wantErr: true, + }, + { + name: "error-on-read", + params: &FindAliasParams{}, + findHandler: &testFindAliasHandler{ + entities: []*Entity{ + { + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", + Aliases: []*Alias{ + aliasAlice, + }, + }, + }, + wantErrOnRead: true, + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := tt.findHandler + + config, ln := testutil.TestHTTPServer(t, r.handler()) + defer ln.Close() + + config.Address = fmt.Sprintf("http://%s", ln.Addr()) + c, err := api.NewClient(config) + if err != nil { + t.Fatal(err) + } + + got, err := FindAliases(c, tt.params) + if (err != nil) != tt.wantErr { + t.Errorf("FindAliases() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("FindAliases() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/testutil/testutil.go b/testutil/testutil.go index 71a002cad..96da012ad 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/http" "os" "reflect" @@ -13,6 +14,7 @@ import ( "github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/vault/api" "github.com/mitchellh/go-homedir" ) @@ -269,3 +271,21 @@ func (c *ghRESTClient) do(method, path string, v interface{}) error { } return nil } + +// testHTTPServer creates a test HTTP server that handles requests until +// the listener returned is closed. +// XXX: copied from github.com/hashicorp/vault/api/client_test.go +func TestHTTPServer(t *testing.T, handler http.Handler) (*api.Config, net.Listener) { + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("err: %s", err) + } + + server := &http.Server{Handler: handler} + go server.Serve(ln) + + config := api.DefaultConfig() + config.Address = fmt.Sprintf("http://%s", ln.Addr()) + + return config, ln +} diff --git a/vault/resource_identity_entity.go b/vault/resource_identity_entity.go index 5757d1425..209125920 100644 --- a/vault/resource_identity_entity.go +++ b/vault/resource_identity_entity.go @@ -111,7 +111,7 @@ func identityEntityCreate(d *schema.ResourceData, meta interface{}) error { name := d.Get("name").(string) - path := entity.IdentityEntityPath + path := entity.RootEntityPath data := map[string]interface{}{ "name": name, @@ -147,7 +147,7 @@ func identityEntityUpdate(d *schema.ResourceData, meta interface{}) error { id := d.Id() log.Printf("[DEBUG] Updating IdentityEntity %q", id) - path := entity.IDPath(id) + path := entity.JoinEntityID(id) vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) @@ -197,7 +197,7 @@ func identityEntityDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*api.Client) id := d.Id() - path := entity.IDPath(id) + path := entity.JoinEntityID(id) vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) @@ -216,7 +216,7 @@ func identityEntityExists(d *schema.ResourceData, meta interface{}) (bool, error client := meta.(*api.Client) id := d.Id() - path := entity.IDPath(id) + path := entity.JoinEntityID(id) key := id // use the name if no ID is set @@ -236,7 +236,7 @@ func identityEntityExists(d *schema.ResourceData, meta interface{}) (bool, error } func identityEntityNamePath(name string) string { - return fmt.Sprintf("%s/name/%s", entity.IdentityEntityPath, name) + return fmt.Sprintf("%s/name/%s", entity.RootEntityPath, name) } func readIdentityEntityPolicies(client *api.Client, entityID string) ([]interface{}, error) { @@ -252,7 +252,7 @@ func readIdentityEntityPolicies(client *api.Client, entityID string) ([]interfac } func readIdentityEntity(client *api.Client, entityID string, retry bool) (*api.Secret, error) { - path := entity.IDPath(entityID) + path := entity.JoinEntityID(entityID) log.Printf("[DEBUG] Reading Entity %q from %q", entityID, path) return readEntity(client, path, retry) diff --git a/vault/resource_identity_entity_alias.go b/vault/resource_identity_entity_alias.go index eae30c760..372d1e3cf 100644 --- a/vault/resource_identity_entity_alias.go +++ b/vault/resource_identity_entity_alias.go @@ -54,7 +54,7 @@ func identityEntityAliasResource() *schema.Resource { } func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - path := entity.IdentityEntityAliasPath + path := entity.RootAliasPath vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) client := meta.(*api.Client) @@ -137,14 +137,14 @@ func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta } func identityEntityAliasUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - vaultMutexKV.Lock(entity.IdentityEntityAliasPath) - defer vaultMutexKV.Unlock(entity.IdentityEntityAliasPath) + vaultMutexKV.Lock(entity.RootAliasPath) + defer vaultMutexKV.Unlock(entity.RootAliasPath) client := meta.(*api.Client) id := d.Id() log.Printf("[DEBUG] Updating IdentityEntityAlias %q", id) - path := entity.AliasIDPath(id) + path := entity.JoinAliasID(id) diags := diag.Diagnostics{} @@ -195,7 +195,7 @@ func identityEntityAliasRead(ctx context.Context, d *schema.ResourceData, meta i client := meta.(*api.Client) id := d.Id() - path := entity.AliasIDPath(id) + path := entity.JoinAliasID(id) diags := diag.Diagnostics{} @@ -233,12 +233,12 @@ func identityEntityAliasRead(ctx context.Context, d *schema.ResourceData, meta i } func identityEntityAliasDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - vaultMutexKV.Lock(entity.IdentityEntityAliasPath) - defer vaultMutexKV.Unlock(entity.IdentityEntityAliasPath) + vaultMutexKV.Lock(entity.RootAliasPath) + defer vaultMutexKV.Unlock(entity.RootAliasPath) client := meta.(*api.Client) id := d.Id() - path := entity.AliasIDPath(id) + path := entity.JoinAliasID(id) diags := diag.Diagnostics{} diff --git a/vault/resource_identity_entity_alias_test.go b/vault/resource_identity_entity_alias_test.go index 4818b8d4d..92308aa6a 100644 --- a/vault/resource_identity_entity_alias_test.go +++ b/vault/resource_identity_entity_alias_test.go @@ -234,7 +234,7 @@ func testAccCheckIdentityEntityAliasDestroy(s *terraform.State) error { if rs.Type != "vault_identity_entity_alias" { continue } - secret, err := client.Logical().Read(entity.AliasIDPath(rs.Primary.ID)) + secret, err := client.Logical().Read(entity.JoinAliasID(rs.Primary.ID)) if err != nil { return fmt.Errorf("error checking for identity entity %q: %s", rs.Primary.ID, err) } diff --git a/vault/resource_identity_entity_policies.go b/vault/resource_identity_entity_policies.go index 44fd05364..a4dc11fe3 100644 --- a/vault/resource_identity_entity_policies.go +++ b/vault/resource_identity_entity_policies.go @@ -55,7 +55,7 @@ func identityEntityPoliciesUpdate(d *schema.ResourceData, meta interface{}) erro id := d.Get("entity_id").(string) log.Printf("[DEBUG] Updating IdentityEntityPolicies %q", id) - path := entity.IDPath(id) + path := entity.JoinEntityID(id) vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) @@ -138,7 +138,7 @@ func identityEntityPoliciesDelete(d *schema.ResourceData, meta interface{}) erro id := d.Get("entity_id").(string) log.Printf("[DEBUG] Deleting IdentityEntityPolicies %q", id) - path := entity.IDPath(id) + path := entity.JoinEntityID(id) vaultMutexKV.Lock(path) defer vaultMutexKV.Unlock(path) diff --git a/vault/resource_identity_entity_policies_test.go b/vault/resource_identity_entity_policies_test.go index 924242e1d..f0043510c 100644 --- a/vault/resource_identity_entity_policies_test.go +++ b/vault/resource_identity_entity_policies_test.go @@ -123,7 +123,7 @@ func testAccIdentityEntityPoliciesCheckAttrs(resource string) resource.TestCheck id := instanceState.ID - path := entity.IDPath(id) + path := entity.JoinEntityID(id) client := testProvider.Meta().(*api.Client) resp, err := client.Logical().Read(path) if err != nil { @@ -218,7 +218,7 @@ func testAccIdentityEntityPoliciesCheckLogical(resource string, policies []strin id := instanceState.ID - path := entity.IDPath(id) + path := entity.JoinEntityID(id) client := testProvider.Meta().(*api.Client) resp, err := client.Logical().Read(path) if err != nil { diff --git a/vault/resource_identity_entity_test.go b/vault/resource_identity_entity_test.go index e8352a5c9..385a23ae6 100644 --- a/vault/resource_identity_entity_test.go +++ b/vault/resource_identity_entity_test.go @@ -3,7 +3,6 @@ package vault import ( "encoding/json" "fmt" - "net" "net/http" "reflect" "strconv" @@ -120,7 +119,7 @@ func testAccCheckIdentityEntityDestroy(s *terraform.State) error { if rs.Type != "vault_identity_entity" { continue } - secret, err := client.Logical().Read(entity.IDPath(rs.Primary.ID)) + secret, err := client.Logical().Read(entity.JoinEntityID(rs.Primary.ID)) if err != nil { return fmt.Errorf("error checking for identity entity %q: %s", rs.Primary.ID, err) } @@ -145,7 +144,7 @@ func testAccIdentityEntityCheckAttrs() resource.TestCheckFunc { id := instanceState.ID - path := entity.IDPath(id) + path := entity.JoinEntityID(id) client := testProvider.Meta().(*api.Client) resp, err := client.Logical().Read(path) if err != nil { @@ -309,7 +308,7 @@ func TestReadEntity(t *testing.T) { }, { name: "retry-exhausted-default-max-404", - path: entity.IDPath("retry-exhausted-default-max-404"), + path: entity.JoinEntityID("retry-exhausted-default-max-404"), retryHandler: &testRetryHandler{ okAtCount: 0, retryStatus: http.StatusNotFound, @@ -317,11 +316,11 @@ func TestReadEntity(t *testing.T) { maxRetries: DefaultMaxHTTPRetriesCCC, expectedRetries: DefaultMaxHTTPRetriesCCC, wantError: fmt.Errorf(`%w: %q`, errEntityNotFound, - entity.IDPath("retry-exhausted-default-max-404")), + entity.JoinEntityID("retry-exhausted-default-max-404")), }, { name: "retry-exhausted-default-max-412", - path: entity.IDPath("retry-exhausted-default-max-412"), + path: entity.JoinEntityID("retry-exhausted-default-max-412"), retryHandler: &testRetryHandler{ okAtCount: 0, retryStatus: http.StatusPreconditionFailed, @@ -329,11 +328,11 @@ func TestReadEntity(t *testing.T) { maxRetries: DefaultMaxHTTPRetriesCCC, expectedRetries: DefaultMaxHTTPRetriesCCC, wantError: fmt.Errorf(`failed reading %q`, - entity.IDPath("retry-exhausted-default-max-412")), + entity.JoinEntityID("retry-exhausted-default-max-412")), }, { name: "retry-exhausted-custom-max-404", - path: entity.IDPath("retry-exhausted-custom-max-404"), + path: entity.JoinEntityID("retry-exhausted-custom-max-404"), retryHandler: &testRetryHandler{ okAtCount: 0, retryStatus: http.StatusNotFound, @@ -341,11 +340,11 @@ func TestReadEntity(t *testing.T) { maxRetries: 5, expectedRetries: 5, wantError: fmt.Errorf(`%w: %q`, errEntityNotFound, - entity.IDPath("retry-exhausted-custom-max-404")), + entity.JoinEntityID("retry-exhausted-custom-max-404")), }, { name: "retry-exhausted-custom-max-412", - path: entity.IDPath("retry-exhausted-custom-max-412"), + path: entity.JoinEntityID("retry-exhausted-custom-max-412"), retryHandler: &testRetryHandler{ okAtCount: 0, retryStatus: http.StatusPreconditionFailed, @@ -353,7 +352,7 @@ func TestReadEntity(t *testing.T) { maxRetries: 5, expectedRetries: 5, wantError: fmt.Errorf(`failed reading %q`, - entity.IDPath("retry-exhausted-custom-max-412")), + entity.JoinEntityID("retry-exhausted-custom-max-412")), }, } @@ -366,7 +365,7 @@ func TestReadEntity(t *testing.T) { r := tt.retryHandler - config, ln := testHTTPServer(t, r.handler()) + config, ln := testutil.TestHTTPServer(t, r.handler()) defer ln.Close() config.Address = fmt.Sprintf("http://%s", ln.Addr()) @@ -474,21 +473,3 @@ func (t *testRetryHandler) handler() http.HandlerFunc { } } } - -// testHTTPServer creates a test HTTP server that handles requests until -// the listener returned is closed. -// XXX: copied from github.com/hashicorp/vault/api/client_test.go -func testHTTPServer(t *testing.T, handler http.Handler) (*api.Config, net.Listener) { - ln, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatalf("err: %s", err) - } - - server := &http.Server{Handler: handler} - go server.Serve(ln) - - config := api.DefaultConfig() - config.Address = fmt.Sprintf("http://%s", ln.Addr()) - - return config, ln -} From 6d53f10e1961f92d9189207e8bfb9850137c2f12 Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Fri, 29 Apr 2022 17:57:18 -0400 Subject: [PATCH 6/9] Fix duplicate entity ID --- internal/identity/entity/entity_test.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/identity/entity/entity_test.go b/internal/identity/entity/entity_test.go index 1023788f6..10f8ba940 100644 --- a/internal/identity/entity/entity_test.go +++ b/internal/identity/entity/entity_test.go @@ -91,6 +91,8 @@ func (t *testFindAliasHandler) handler() http.HandlerFunc { } func TestFindAliases(t *testing.T) { + t.Parallel() + aliasBob := &Alias{ Name: "bob", MountAccessor: "CC417368-0C63-407A-93AD-2D76A72F58E2", @@ -179,10 +181,14 @@ func TestFindAliases(t *testing.T) { ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", Aliases: []*Alias{ aliasAlice, + { + Name: aliasBob.Name, + MountAccessor: aliasAlice.MountAccessor, + }, }, }, { - ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932B", Aliases: []*Alias{ aliasBob, }, @@ -209,7 +215,7 @@ func TestFindAliases(t *testing.T) { }, }, { - ID: "C6D3410E-86AF-4A10-9282-4B1E9773932A", + ID: "C6D3410E-86AF-4A10-9282-4B1E9773932B", Aliases: []*Alias{ aliasBob, }, From 458582cb1817af93550b724d8d92eac922511641 Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Sat, 30 Apr 2022 11:09:12 -0400 Subject: [PATCH 7/9] Lock on the alias path and mount accessor - factor out common lock/unlock functions --- vault/resource_identity_entity_alias.go | 32 +++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/vault/resource_identity_entity_alias.go b/vault/resource_identity_entity_alias.go index 372d1e3cf..f240b3f01 100644 --- a/vault/resource_identity_entity_alias.go +++ b/vault/resource_identity_entity_alias.go @@ -54,11 +54,13 @@ func identityEntityAliasResource() *schema.Resource { } func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - path := entity.RootAliasPath - vaultMutexKV.Lock(path) - defer vaultMutexKV.Unlock(path) + lock, unlock := getEntityAliasLockFuncs(d) + lock() + defer unlock() + client := meta.(*api.Client) + path := entity.RootAliasPath name := d.Get("name").(string) mountAccessor := d.Get("mount_accessor").(string) canonicalID := d.Get("canonical_id").(string) @@ -137,8 +139,9 @@ func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta } func identityEntityAliasUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - vaultMutexKV.Lock(entity.RootAliasPath) - defer vaultMutexKV.Unlock(entity.RootAliasPath) + lock, unlock := getEntityAliasLockFuncs(d) + lock() + defer unlock() client := meta.(*api.Client) id := d.Id() @@ -233,8 +236,10 @@ func identityEntityAliasRead(ctx context.Context, d *schema.ResourceData, meta i } func identityEntityAliasDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - vaultMutexKV.Lock(entity.RootAliasPath) - defer vaultMutexKV.Unlock(entity.RootAliasPath) + lock, unlock := getEntityAliasLockFuncs(d) + lock() + defer unlock() + client := meta.(*api.Client) id := d.Id() @@ -255,3 +260,16 @@ func identityEntityAliasDelete(ctx context.Context, d *schema.ResourceData, meta return diags } + +func getEntityAliasLockFuncs(d *schema.ResourceData) (func(), func()) { + mountAccessor := d.Get("mount_accessor").(string) + lockKey := strings.Join([]string{entity.RootAliasIDPath, mountAccessor}, "/") + lock := func() { + vaultMutexKV.Lock(lockKey) + } + + unlock := func() { + vaultMutexKV.Unlock(lockKey) + } + return lock, unlock +} From a3c4434b1261abbac455cff1afac97be8c8fb105 Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Mon, 2 May 2022 16:00:14 -0400 Subject: [PATCH 8/9] Update error and log messages --- vault/resource_identity_entity_alias.go | 29 +++++++++++++------------ 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/vault/resource_identity_entity_alias.go b/vault/resource_identity_entity_alias.go index f240b3f01..75deb6569 100644 --- a/vault/resource_identity_entity_alias.go +++ b/vault/resource_identity_entity_alias.go @@ -114,7 +114,7 @@ func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta diags = append(diags, diag.Diagnostic{ Severity: diag.Error, Summary: fmt.Sprintf( - "error writing IdentityEntityAlias to %q: %s", name, err), + "error writing entity alias to %q: %s", name, err), }) return diags @@ -131,7 +131,7 @@ func identityEntityAliasCreate(ctx context.Context, d *schema.ResourceData, meta } - log.Printf("[DEBUG] Wrote IdentityEntityAlias %q", name) + log.Printf("[DEBUG] Wrote entity alias %q", name) d.SetId(resp.Data["id"].(string)) @@ -146,7 +146,7 @@ func identityEntityAliasUpdate(ctx context.Context, d *schema.ResourceData, meta client := meta.(*api.Client) id := d.Id() - log.Printf("[DEBUG] Updating IdentityEntityAlias %q", id) + log.Printf("[DEBUG] Updating entity alias %q", id) path := entity.JoinAliasID(id) diags := diag.Diagnostics{} @@ -155,7 +155,7 @@ func identityEntityAliasUpdate(ctx context.Context, d *schema.ResourceData, meta if err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, - Summary: fmt.Sprintf("error updating IdentityEntityAlias %q: %s", id, err), + Summary: fmt.Sprintf("error reading entity alias %q: %s", id, err), }) return diags @@ -184,12 +184,12 @@ func identityEntityAliasUpdate(ctx context.Context, d *schema.ResourceData, meta if err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, - Summary: fmt.Sprintf("error updating IdentityEntityAlias %q: %s", id, err), + Summary: fmt.Sprintf("error updating entity alias %q: %s", id, err), }) return diags } - log.Printf("[DEBUG] Updated IdentityEntityAlias %q", id) + log.Printf("[DEBUG] Updated entity alias %q", id) return identityEntityAliasRead(ctx, d, meta) } @@ -202,19 +202,19 @@ func identityEntityAliasRead(ctx context.Context, d *schema.ResourceData, meta i diags := diag.Diagnostics{} - log.Printf("[DEBUG] Reading IdentityEntityAlias %q from %q", id, path) + log.Printf("[DEBUG] Reading entity alias %q from %q", id, path) resp, err := client.Logical().Read(path) if err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, - Summary: fmt.Sprintf("error reading IdentityEntityAlias %q: %s", id, err), + Summary: fmt.Sprintf("error reading entity alias %q: %s", id, err), }) return diags } - log.Printf("[DEBUG] Read IdentityEntityAlias %s", id) + log.Printf("[DEBUG] Read entity alias %s", id) if resp == nil { - log.Printf("[WARN] IdentityEntityAlias %q not found, removing from state", id) + log.Printf("[WARN] entity alias %q not found, removing from state", id) d.SetId("") return diags @@ -225,7 +225,7 @@ func identityEntityAliasRead(ctx context.Context, d *schema.ResourceData, meta i if err := d.Set(k, resp.Data[k]); err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, - Summary: fmt.Sprintf("error setting state key %q on IdentityEntityAlias %q: err=%q", k, id, err), + Summary: fmt.Sprintf("error setting state key %q on entity alias %q: err=%q", k, id, err), }) return diags @@ -247,16 +247,17 @@ func identityEntityAliasDelete(ctx context.Context, d *schema.ResourceData, meta diags := diag.Diagnostics{} - log.Printf("[DEBUG] Deleting IdentityEntityAlias %q", id) + baseMsg := fmt.Sprintf("entity alias ID %q on mount_accessor %q", id, d.Get("mount_accessor")) + log.Printf("[INFO] Deleting %s", baseMsg) _, err := client.Logical().Delete(path) if err != nil { diags = append(diags, diag.Diagnostic{ Severity: diag.Error, - Summary: fmt.Sprintf("error IdentityEntityAlias %q", id), + Summary: fmt.Sprintf("failed deleting %s, err=%s", baseMsg, err), }) return diags } - log.Printf("[DEBUG] Deleted IdentityEntityAlias %q", id) + log.Printf("[INFO] Successfully deleted %s", baseMsg) return diags } From 91dd8422b13d53d10846f3893296e120ede879fa Mon Sep 17 00:00:00 2001 From: Ben Ash Date: Mon, 2 May 2022 16:45:33 -0400 Subject: [PATCH 9/9] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57019de05..655c0cfdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ BUGS: * `resource/pki_secret_backend_root_sign_intermediate`: Ensure that the `certificate_bundle`, and `ca_chain` do not contain duplicate certificates. ([#1428](https://github.com/hashicorp/terraform-provider-vault/pull/1428)) +* `resource/identity_entity_alias`: Serialize create, update, and delete operations in order to prevent alias + mismatches. + ([#1429](https://github.com/hashicorp/terraform-provider-vault/pull/1429)) ## 3.5.0 (April 20, 2022) FEATURES: