From 472c2a03b40ac2d8e75cfab2a4859ed6271249b9 Mon Sep 17 00:00:00 2001 From: Yong Wen Chua Date: Sat, 31 Jul 2021 08:31:10 +0800 Subject: [PATCH] Add resource for GCP Static Account (#1094) * Refactor some GCP functions out to a separate file * Add GCP Static account resource * Rename Binding type * Add tests * Update * Update prefix Ref https://github.com/hashicorp/vault-plugin-secrets-gcp/pull/107/commits/8fba665423e0f328670ccc579a5537998ffd8f94 * Add docs * Apply suggestions from code review Co-authored-by: Austin Gebauer <34121980+austingebauer@users.noreply.github.com> * Rename function * Update vault/resource_gcp_secret_static_account.go Co-authored-by: Ben Ash <32777270+benashz@users.noreply.github.com> * Add some comments Co-authored-by: Austin Gebauer <34121980+austingebauer@users.noreply.github.com> Co-authored-by: Ben Ash <32777270+benashz@users.noreply.github.com> --- go.mod | 1 + vault/gcp.go | 99 +++++ vault/provider.go | 4 + vault/resource_gcp_secret_roleset.go | 99 +---- vault/resource_gcp_secret_roleset_test.go | 12 +- vault/resource_gcp_secret_static_account.go | 267 ++++++++++++ ...resource_gcp_secret_static_account_test.go | 397 ++++++++++++++++++ .../docs/r/gcp_secret_static_account.html.md | 81 ++++ website/vault.erb | 4 + 9 files changed, 864 insertions(+), 100 deletions(-) create mode 100644 vault/gcp.go create mode 100644 vault/resource_gcp_secret_static_account.go create mode 100644 vault/resource_gcp_secret_static_account_test.go create mode 100644 website/docs/r/gcp_secret_static_account.html.md diff --git a/go.mod b/go.mod index c59d860ec..cbdfc9978 100644 --- a/go.mod +++ b/go.mod @@ -22,4 +22,5 @@ require ( github.com/hashicorp/vault/sdk v0.2.1 github.com/mitchellh/go-homedir v1.1.0 github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 ) diff --git a/vault/gcp.go b/vault/gcp.go new file mode 100644 index 000000000..cbb79c99d --- /dev/null +++ b/vault/gcp.go @@ -0,0 +1,99 @@ +package vault + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" +) + +/// GCPBinding is used to generate the HCL binding format that GCP Secret Engine Requires +/// `Resource` is the self-link of a GCP resource +/// Roles is a list of IAM roles to be assigned to an entity for that resource. +type GCPBinding struct { + Resource string + Roles []string +} + +func gcpSecretFlattenBinding(v interface{}) interface{} { + transformed := schema.NewSet(gcpSecretBindingHash, []interface{}{}) + if v == nil { + return transformed + } + + rawBindings := v.((map[string]interface{})) + for resource, roles := range rawBindings { + transformed.Add(map[string]interface{}{ + "resource": resource, + "roles": schema.NewSet(schema.HashString, roles.([]interface{})), + }) + } + + return transformed +} + +func gcpSecretBindingHash(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["resource"].(string))) + + // We need to make sure to sort the strings below so that we always + // generate the same hash code no matter what is in the set. + if v, ok := m["roles"]; ok { + vs := v.(*schema.Set).List() + s := make([]string, len(vs)) + for i, raw := range vs { + s[i] = raw.(string) + } + sort.Strings(s) + + for _, v := range s { + buf.WriteString(fmt.Sprintf("%s-", v)) + } + } + return hashcode.String(buf.String()) +} + +func gcpSecretRenderBinding(binding *GCPBinding) string { + output := fmt.Sprintf("resource \"%s\" {\n", binding.Resource) + output = fmt.Sprintf("%s roles = %s\n", output, policyRenderListOfStrings(binding.Roles)) + return fmt.Sprintf("%s}\n", output) +} + +func gcpSecretRenderBindings(bindings []*GCPBinding) string { + var output string + + for i, binding := range bindings { + if i == 0 { + output = fmt.Sprintf("%s", gcpSecretRenderBinding(binding)) + } else { + output = fmt.Sprintf("%s\n\n%s", output, gcpSecretRenderBinding(binding)) + } + } + + return output +} + +func gcpSecretRenderBindingsFromData(v interface{}) string { + rawBindings := v.(*schema.Set).List() + + bindings := make([]*GCPBinding, len(rawBindings)) + + for i, binding := range rawBindings { + rawRoles := binding.(map[string]interface{})["roles"].(*schema.Set).List() + roles := make([]string, len(rawRoles)) + for j, role := range rawRoles { + roles[j] = role.(string) + } + + binding := &GCPBinding{ + Resource: binding.(map[string]interface{})["resource"].(string), + Roles: roles, + } + bindings[i] = binding + } + + return gcpSecretRenderBindings(bindings) +} diff --git a/vault/provider.go b/vault/provider.go index 4c1bfefbe..319755eda 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -427,6 +427,10 @@ var ( Resource: gcpSecretRolesetResource(), PathInventory: []string{"/gcp/roleset/{name}"}, }, + "vault_gcp_secret_static_account": { + Resource: gcpSecretStaticAccountResource(), + PathInventory: []string{"/gcp/static-account/{name}"}, + }, "vault_cert_auth_backend_role": { Resource: certAuthBackendRoleResource(), PathInventory: []string{"/auth/cert/certs/{name}"}, diff --git a/vault/resource_gcp_secret_roleset.go b/vault/resource_gcp_secret_roleset.go index 9f080da58..0c2afa080 100644 --- a/vault/resource_gcp_secret_roleset.go +++ b/vault/resource_gcp_secret_roleset.go @@ -1,15 +1,12 @@ package vault import ( - "bytes" "fmt" "log" "regexp" - "sort" "strings" "github.com/hashicorp/terraform-plugin-sdk/helper/customdiff" - "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/vault/api" ) @@ -19,11 +16,6 @@ var ( gcpSecretRolesetNameFromPathRegex = regexp.MustCompile("^.+/roleset/(.+)$") ) -type Binding struct { - Resource string - Roles []string -} - func gcpSecretRolesetResource() *schema.Resource { return &schema.Resource{ Create: gcpSecretRolesetCreate, @@ -76,7 +68,7 @@ func gcpSecretRolesetResource() *schema.Resource { "binding": { Type: schema.TypeSet, Required: true, - Set: gcpSecretRolesetBindingHash, + Set: gcpSecretBindingHash, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "resource": { @@ -107,8 +99,8 @@ func gcpSecretRolesetResource() *schema.Resource { // Due to https://github.com/hashicorp/terraform/issues/17411 // we cannot use d.HasChange("binding") directly oldBinding, newBinding := d.GetChange("binding") - oldHcl := renderBindingsFromData(oldBinding) - newHcl := renderBindingsFromData(newBinding) + oldHcl := gcpSecretRenderBindingsFromData(oldBinding) + newHcl := gcpSecretRenderBindingsFromData(newBinding) return d.HasChange("token_scopes") || oldHcl != newHcl }), @@ -191,7 +183,7 @@ func gcpSecretRolesetRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error reading %s for GCP Secrets backend roleset %q", "project", path) } - if err := d.Set("binding", gcpSecretRolesetFlattenBinding(resp.Data["bindings"])); err != nil { + if err := d.Set("binding", gcpSecretFlattenBinding(resp.Data["bindings"])); err != nil { return fmt.Errorf("error reading %s for GCP Secrets backend roleset %q", "binding", path) } @@ -230,23 +222,6 @@ func gcpSecretRolesetDelete(d *schema.ResourceData, meta interface{}) error { return nil } -func gcpSecretRolesetFlattenBinding(v interface{}) interface{} { - if v == nil { - return v - } - - rawBindings := v.((map[string]interface{})) - transformed := schema.NewSet(gcpSecretRolesetBindingHash, []interface{}{}) - for resource, roles := range rawBindings { - transformed.Add(map[string]interface{}{ - "resource": resource, - "roles": schema.NewSet(schema.HashString, roles.([]interface{})), - }) - } - - return transformed -} - func gcpSecretRolesetUpdateFields(d *schema.ResourceData, data map[string]interface{}) { if v, ok := d.GetOk("secret_type"); ok { data["secret_type"] = v.(string) @@ -261,7 +236,7 @@ func gcpSecretRolesetUpdateFields(d *schema.ResourceData, data map[string]interf } if v, ok := d.GetOk("binding"); ok { - bindingsHCL := renderBindingsFromData(v) + bindingsHCL := gcpSecretRenderBindingsFromData(v) log.Printf("[DEBUG] Rendered GCP Secrets backend roleset bindings HCL:\n%s", bindingsHCL) data["bindings"] = bindingsHCL } @@ -279,28 +254,6 @@ func gcpSecretRolesetExists(d *schema.ResourceData, meta interface{}) (bool, err return secret != nil, nil } -func gcpSecretRolesetBindingHash(v interface{}) int { - var buf bytes.Buffer - m := v.(map[string]interface{}) - buf.WriteString(fmt.Sprintf("%s-", m["resource"].(string))) - - // We need to make sure to sort the strings below so that we always - // generate the same hash code no matter what is in the set. - if v, ok := m["roles"]; ok { - vs := v.(*schema.Set).List() - s := make([]string, len(vs)) - for i, raw := range vs { - s[i] = raw.(string) - } - sort.Strings(s) - - for _, v := range s { - buf.WriteString(fmt.Sprintf("%s-", v)) - } - } - return hashcode.String(buf.String()) -} - func gcpSecretRolesetPath(backend, roleset string) string { return strings.Trim(backend, "/") + "/roleset/" + strings.Trim(roleset, "/") } @@ -326,45 +279,3 @@ func gcpSecretRoleSetdRolesetNameFromPath(path string) (string, error) { } return res[1], nil } - -func renderBinding(binding *Binding) string { - output := fmt.Sprintf("resource \"%s\" {\n", binding.Resource) - output = fmt.Sprintf("%s roles = %s\n", output, policyRenderListOfStrings(binding.Roles)) - return fmt.Sprintf("%s}\n", output) -} - -func renderBindings(bindings []*Binding) string { - var output string - - for i, binding := range bindings { - if i == 0 { - output = fmt.Sprintf("%s", renderBinding(binding)) - } else { - output = fmt.Sprintf("%s\n\n%s", output, renderBinding(binding)) - } - } - - return output -} - -func renderBindingsFromData(v interface{}) string { - rawBindings := v.(*schema.Set).List() - - bindings := make([]*Binding, len(rawBindings)) - - for i, binding := range rawBindings { - rawRoles := binding.(map[string]interface{})["roles"].(*schema.Set).List() - roles := make([]string, len(rawRoles)) - for j, role := range rawRoles { - roles[j] = role.(string) - } - - binding := &Binding{ - Resource: binding.(map[string]interface{})["resource"].(string), - Roles: roles, - } - bindings[i] = binding - } - - return renderBindings(bindings) -} diff --git a/vault/resource_gcp_secret_roleset_test.go b/vault/resource_gcp_secret_roleset_test.go index 292686007..8a4d11906 100644 --- a/vault/resource_gcp_secret_roleset_test.go +++ b/vault/resource_gcp_secret_roleset_test.go @@ -57,10 +57,10 @@ func TestGCPSecretRoleset(t *testing.T) { ), }, { - ResourceName: "vault_gcp_secret_backend.test", + ResourceName: "vault_gcp_secret_roleset.test", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"credentials"}, + ImportStateVerifyIgnore: []string{}, }, { Config: updatedConfig, @@ -218,9 +218,9 @@ func testGCPSecretRoleset_attrs(backend, roleset string) resource.TestCheckFunc return fmt.Errorf("expected %s to have %d entries in state, has %d", "binding", remoteLength, localBindingsLength) } - flattenedBindings := gcpSecretRolesetFlattenBinding(remoteBindings).(*schema.Set) + flattenedBindings := gcpSecretFlattenBinding(remoteBindings).(*schema.Set) for _, remoteBinding := range flattenedBindings.List() { - bindingHash := strconv.Itoa(gcpSecretRolesetBindingHash(remoteBinding)) + bindingHash := strconv.Itoa(gcpSecretBindingHash(remoteBinding)) remoteResource := remoteBinding.(map[string]interface{})["resource"].(string) localResource := instanceState.Attributes["binding."+bindingHash+".resource"] @@ -336,7 +336,7 @@ resource "vault_gcp_secret_roleset" "test" { binding["resource"] = resource binding["roles"] = schema.NewSet(schema.HashString, roles) - return terraform, gcpSecretRolesetBindingHash(binding) + return terraform, gcpSecretBindingHash(binding) } func testGCPSecretRoleset_service_account_key(backend, roleset, credentials, project, role string) (string, int) { @@ -369,5 +369,5 @@ resource "vault_gcp_secret_roleset" "test" { binding["resource"] = resource binding["roles"] = schema.NewSet(schema.HashString, roles) - return terraform, gcpSecretRolesetBindingHash(binding) + return terraform, gcpSecretBindingHash(binding) } diff --git a/vault/resource_gcp_secret_static_account.go b/vault/resource_gcp_secret_static_account.go new file mode 100644 index 000000000..1cf5d14b4 --- /dev/null +++ b/vault/resource_gcp_secret_static_account.go @@ -0,0 +1,267 @@ +package vault + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/vault/api" +) + +var ( + gcpSecretStaticAccountBackendFromPathRegex = regexp.MustCompile("^(.+)/static-account/.+$") + gcpSecretStaticAccountNameFromPathRegex = regexp.MustCompile("^.+/static-account/(.+)$") +) + +func gcpSecretStaticAccountResource() *schema.Resource { + return &schema.Resource{ + Create: gcpSecretStaticAccountCreate, + Read: gcpSecretStaticAccountRead, + Update: gcpSecretStaticAccountUpdate, + Delete: gcpSecretStaticAccountDelete, + Exists: gcpSecretStaticAccountExists, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "backend": { + Type: schema.TypeString, + Required: true, + Description: "Path where the GCP secrets engine is mounted.", + ForceNew: true, + // standardise on no beginning or trailing slashes + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "static_account": { + Type: schema.TypeString, + Required: true, + Description: "Name of the Static Account to create", + ForceNew: true, + }, + "secret_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + Description: "Type of secret generated for this static account. Defaults to `access_token`. Accepted values: `access_token`, `service_account_key`", + }, + "service_account_email": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Email of the GCP service account.", + }, + "token_scopes": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Optional: true, + Description: "List of OAuth scopes to assign to `access_token` secrets generated under this static account (`access_token` static accounts only) ", + }, + "binding": { + Type: schema.TypeSet, + Optional: true, + Set: gcpSecretBindingHash, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "resource": { + Type: schema.TypeString, + Required: true, + Description: "Resource name", + }, + "roles": { + Type: schema.TypeSet, + Required: true, + Description: "List of roles to apply to the resource", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + }, + "service_account_project": { + Type: schema.TypeString, + Computed: true, + Description: "Project of the GCP Service Account managed by this static account", + }, + }, + } +} + +func gcpSecretStaticAccountCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + backend := d.Get("backend").(string) + staticAccount := d.Get("static_account").(string) + + path := gcpSecretStaticAccountPath(backend, staticAccount) + + log.Printf("[DEBUG] Writing GCP Secrets backend static account %q", path) + + data := map[string]interface{}{} + gcpSecretStaticAccountUpdateFields(d, data) + d.SetId(path) + _, err := client.Logical().Write(path, data) + if err != nil { + d.SetId("") + return fmt.Errorf("error writing GCP Secrets backend static account %q: %s", path, err) + } + log.Printf("[DEBUG] Wrote GCP Secrets backend static account %q", path) + + return gcpSecretStaticAccountRead(d, meta) +} + +func gcpSecretStaticAccountRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + backend, err := gcpSecretStaticAccountBackendFromPath(path) + if err != nil { + return fmt.Errorf("invalid path %q for GCP secrets backend static account: %s", path, err) + } + + staticAccount, err := gcpSecretStaticAccountNameFromPath(path) + if err != nil { + return fmt.Errorf("invalid path %q for GCP Secrets backend static account: %s", path, err) + } + + log.Printf("[DEBUG] Reading GCP Secrets backend static account %q", path) + resp, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("error reading GCP Secrets backend static account %q: %s", path, err) + } + + log.Printf("[DEBUG] Read GCP Secrets backend static account %q", path) + if resp == nil { + log.Printf("[WARN] GCP Secrets backend static account %q not found, removing from state", path) + d.SetId("") + return nil + } + + if err := d.Set("backend", backend); err != nil { + return err + } + if err := d.Set("static_account", staticAccount); err != nil { + return err + } + + for _, k := range []string{"secret_type", "token_scopes", "service_account_email", "service_account_project"} { + v, ok := resp.Data[k] + if ok { + if err := d.Set(k, v); err != nil { + return fmt.Errorf("error reading %s for GCP Secrets backend static account %q: %q", k, path, err) + } + } + } + + var binding interface{} + if v, ok := resp.Data["bindings"]; ok && v != "" { + binding = gcpSecretFlattenBinding(v) + } else { + binding = gcpSecretFlattenBinding(nil) + } + if err := d.Set("binding", binding); err != nil { + return fmt.Errorf("error reading %s for GCP Secrets backend static account %q", "binding", path) + } + + return nil +} + +func gcpSecretStaticAccountUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + data := map[string]interface{}{} + gcpSecretStaticAccountUpdateFields(d, data) + + log.Printf("[DEBUG] Updating GCP Secrets backend static account %q", path) + + _, err := client.Logical().Write(path, data) + if err != nil { + return fmt.Errorf("error updating GCP Secrets backend static account %q: %s", path, err) + } + log.Printf("[DEBUG] Updated GCP Secrets backend static account %q", path) + + return gcpSecretStaticAccountRead(d, meta) +} + +func gcpSecretStaticAccountDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + path := d.Id() + + log.Printf("[DEBUG] Deleting GCP secrets backend static account %q", path) + _, err := client.Logical().Delete(path) + if err != nil { + return fmt.Errorf("error deleting GCP secrets backend static account %q", path) + } + log.Printf("[DEBUG] Deleted GCP secrets backend static account %q", path) + + return nil +} + +func gcpSecretStaticAccountUpdateFields(d *schema.ResourceData, data map[string]interface{}) { + if v, ok := d.GetOk("service_account_email"); ok { + data["service_account_email"] = v.(string) + } + + if v, ok := d.GetOk("secret_type"); ok { + data["secret_type"] = v.(string) + } + + if v, ok := d.GetOk("token_scopes"); ok && d.Get("secret_type").(string) == "access_token" { + data["token_scopes"] = v.(*schema.Set).List() + } + + if v, ok := d.GetOk("binding"); ok { + bindingsHCL := gcpSecretRenderBindingsFromData(v) + log.Printf("[DEBUG] Rendered GCP Secrets backend static account bindings HCL:\n%s", bindingsHCL) + data["bindings"] = bindingsHCL + } else { + data["bindings"] = "" + } +} + +func gcpSecretStaticAccountExists(d *schema.ResourceData, meta interface{}) (bool, error) { + client := meta.(*api.Client) + path := d.Id() + log.Printf("[DEBUG] Checking if %q exists", path) + secret, err := client.Logical().Read(path) + if err != nil { + return true, fmt.Errorf("error checking if %q exists: %s", path, err) + } + log.Printf("[DEBUG] Checked if %q exists", path) + return secret != nil, nil +} + +func gcpSecretStaticAccountPath(backend, staticAccount string) string { + return strings.Trim(backend, "/") + "/static-account/" + strings.Trim(staticAccount, "/") +} + +func gcpSecretStaticAccountBackendFromPath(path string) (string, error) { + if !gcpSecretStaticAccountBackendFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no backend found") + } + res := gcpSecretStaticAccountBackendFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for backend", len(res)) + } + return res[1], nil +} + +func gcpSecretStaticAccountNameFromPath(path string) (string, error) { + if !gcpSecretStaticAccountNameFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no static account found") + } + res := gcpSecretStaticAccountNameFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for role", len(res)) + } + return res[1], nil +} diff --git a/vault/resource_gcp_secret_static_account_test.go b/vault/resource_gcp_secret_static_account_test.go new file mode 100644 index 000000000..f7a35bb24 --- /dev/null +++ b/vault/resource_gcp_secret_static_account_test.go @@ -0,0 +1,397 @@ +package vault + +import ( + "encoding/json" + "fmt" + "log" + "strconv" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/vault/api" + "golang.org/x/oauth2/google" +) + +// This test requires that you pass credentials for a user or service account having the IAM rights +// listed at https://www.vaultproject.io/docs/secrets/gcp/index.html for the project you are testing +// on. The credentials must also allow setting IAM permissions on the project being tested. +func TestGCPSecretStaticAccount(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-gcp") + staticAccount := acctest.RandomWithPrefix("tf-test") + credentials, project := getTestGCPCreds(t) + + // We will use the provided key as the static account + conf, err := google.JWTConfigFromJSON([]byte(credentials), "https://www.googleapis.com/auth/cloud-platform") + if err != nil { + t.Fatalf("error decoding GCP Credentials: %v", err) + } + serviceAccountEmail := conf.Email + + noBindings := testGCPSecretStaticAccount_accessToken(backend, staticAccount, credentials, serviceAccountEmail, project) + + initialRole := "roles/viewer" + initialConfig, initialHash := testGCPSecretStaticAccount_accessTokenBinding(backend, staticAccount, credentials, serviceAccountEmail, project, initialRole) + + updatedRole := "roles/browser" + updatedConfig, updatedHash := testGCPSecretStaticAccount_accessTokenBinding(backend, staticAccount, credentials, serviceAccountEmail, project, updatedRole) + + keyConfig, keyHash := testGCPSecretStaticAccount_serviceAccountKey(backend, staticAccount, credentials, serviceAccountEmail, project, updatedRole) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: testGCPSecretStaticAccountDestroy, + Steps: []resource.TestStep{ + { + Config: noBindings, + Check: resource.ComposeTestCheckFunc( + testGCPSecretStaticAccount_attrs(backend, staticAccount), + resource.TestCheckResourceAttr("vault_gcp_secret_backend.test", "path", backend), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "backend", backend), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "static_account", staticAccount), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "secret_type", "access_token"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "service_account_email", serviceAccountEmail), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "service_account_project", project), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "token_scopes.#", "1"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "token_scopes.2400041053", "https://www.googleapis.com/auth/cloud-platform"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "binding.#", "0"), + ), + }, + { + Config: initialConfig, + Check: resource.ComposeTestCheckFunc( + testGCPSecretStaticAccount_attrs(backend, staticAccount), + resource.TestCheckResourceAttr("vault_gcp_secret_backend.test", "path", backend), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "backend", backend), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "static_account", staticAccount), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "secret_type", "access_token"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "service_account_email", serviceAccountEmail), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "service_account_project", project), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "token_scopes.#", "1"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "token_scopes.2400041053", "https://www.googleapis.com/auth/cloud-platform"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "binding.#", "1"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", fmt.Sprintf("binding.%d.resource", initialHash), fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", project)), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", fmt.Sprintf("binding.%d.roles.#", initialHash), "1"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", fmt.Sprintf("binding.%d.roles.3993311253", initialHash), initialRole), + ), + }, + { + ResourceName: "vault_gcp_secret_static_account.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{}, + }, + { + Config: updatedConfig, + Check: resource.ComposeTestCheckFunc( + testGCPSecretStaticAccount_attrs(backend, staticAccount), + resource.TestCheckResourceAttr("vault_gcp_secret_backend.test", "path", backend), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "backend", backend), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "static_account", staticAccount), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "secret_type", "access_token"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "service_account_email", serviceAccountEmail), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "service_account_project", project), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "token_scopes.#", "1"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "token_scopes.2400041053", "https://www.googleapis.com/auth/cloud-platform"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "binding.#", "1"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", fmt.Sprintf("binding.%d.resource", updatedHash), fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", project)), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", fmt.Sprintf("binding.%d.roles.#", updatedHash), "1"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", fmt.Sprintf("binding.%d.roles.2133424675", updatedHash), updatedRole), + ), + }, + { + Config: keyConfig, + Check: resource.ComposeTestCheckFunc( + testGCPSecretStaticAccount_attrs(backend, staticAccount), + resource.TestCheckResourceAttr("vault_gcp_secret_backend.test", "path", backend), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "backend", backend), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "static_account", staticAccount), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "secret_type", "service_account_key"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "service_account_email", serviceAccountEmail), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "service_account_project", project), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", "binding.#", "1"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", fmt.Sprintf("binding.%d.resource", keyHash), fmt.Sprintf("//cloudresourcemanager.googleapis.com/projects/%s", project)), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", fmt.Sprintf("binding.%d.roles.#", keyHash), "1"), + resource.TestCheckResourceAttr("vault_gcp_secret_static_account.test", fmt.Sprintf("binding.%d.roles.2133424675", keyHash), updatedRole), + ), + }, + }, + }) +} + +func testGCPSecretStaticAccount_attrs(backend, staticAccount string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resourceState := s.Modules[0].Resources["vault_gcp_secret_static_account.test"] + if resourceState == nil { + return fmt.Errorf("resource not found in state") + } + + instanceState := resourceState.Primary + if instanceState == nil { + return fmt.Errorf("resource not found in state") + } + + endpoint := instanceState.ID + + if endpoint != backend+"/static-account/"+staticAccount { + return fmt.Errorf("expected ID to be %q, got %q instead", backend+"/static-account/"+staticAccount, endpoint) + } + + client := testProvider.Meta().(*api.Client) + resp, err := client.Logical().Read(endpoint) + if err != nil { + return fmt.Errorf("%q doesn't exist", endpoint) + } + + attrs := map[string]string{ + "secret_type": "secret_type", + "service_account_project": "service_account_project", + "token_scopes": "token_scopes", + "service_account_email": "service_account_email", + } + for stateAttr, apiAttr := range attrs { + if resp.Data[apiAttr] == nil && instanceState.Attributes[stateAttr] == "" { + continue + } + var match bool + switch resp.Data[apiAttr].(type) { + case json.Number: + apiData, err := resp.Data[apiAttr].(json.Number).Int64() + if err != nil { + return fmt.Errorf("expected API field %s to be an int, was %q", apiAttr, resp.Data[apiAttr]) + } + stateData, err := strconv.ParseInt(instanceState.Attributes[stateAttr], 10, 64) + if err != nil { + return fmt.Errorf("expected state field %s to be an int, was %q", stateAttr, instanceState.Attributes[stateAttr]) + } + match = apiData == stateData + case bool: + if _, ok := resp.Data[apiAttr]; !ok && instanceState.Attributes[stateAttr] == "" { + match = true + } else { + stateData, err := strconv.ParseBool(instanceState.Attributes[stateAttr]) + if err != nil { + return fmt.Errorf("expected state field %s to be a bool, was %q", stateAttr, instanceState.Attributes[stateAttr]) + } + match = resp.Data[apiAttr] == stateData + } + case []interface{}: + apiData := resp.Data[apiAttr].([]interface{}) + length := instanceState.Attributes[stateAttr+".#"] + if length == "" { + if len(resp.Data[apiAttr].([]interface{})) != 0 { + return fmt.Errorf("expected state field %s to have %d entries, had 0", stateAttr, len(apiData)) + } + match = true + } else { + count, err := strconv.Atoi(length) + if err != nil { + return fmt.Errorf("expected %s.# to be a number, got %q", stateAttr, instanceState.Attributes[stateAttr+".#"]) + } + if count != len(apiData) { + return fmt.Errorf("expected %s to have %d entries in state, has %d", stateAttr, len(apiData), count) + } + + for i := 0; i < count; i++ { + found := false + for stateKey, stateValue := range instanceState.Attributes { + if strings.HasPrefix(stateKey, stateAttr) { + if apiData[i] == stateValue { + found = true + } + } + } + if !found { + return fmt.Errorf("Expected item %d of %s (%s in state) of %q to be in state but wasn't", i, apiAttr, stateAttr, apiData[i]) + } + } + match = true + } + default: + match = resp.Data[apiAttr] == instanceState.Attributes[stateAttr] + } + if !match { + return fmt.Errorf("expected %s (%s in state) of %q to be %q, got %q", apiAttr, stateAttr, endpoint, instanceState.Attributes[stateAttr], resp.Data[apiAttr]) + } + } + + roleHashFunction := schema.HashSchema(&schema.Schema{ + Type: schema.TypeString, + }) + + // Bindings need to be tested separately + remoteBindings := resp.Data["bindings"] // map[string]interface {} + localBindingsLengthRaw := instanceState.Attributes["binding.#"] + if localBindingsLengthRaw == "" { + return fmt.Errorf("cannot find bindings from state") + } + localBindingsLength, err := strconv.Atoi(localBindingsLengthRaw) + if err != nil { + return fmt.Errorf("expected binding.# to be a number, got %q", localBindingsLengthRaw) + } + + var remoteLength int + if remoteBindings == nil { + remoteLength = 0 + } else { + remoteLength = len(remoteBindings.(map[string]interface{})) + } + if localBindingsLength != remoteLength { + return fmt.Errorf("expected %s to have %d entries in state, has %d", "binding", remoteLength, localBindingsLength) + } + + flattenedBindings := gcpSecretFlattenBinding(remoteBindings).(*schema.Set) + for _, remoteBinding := range flattenedBindings.List() { + bindingHash := strconv.Itoa(gcpSecretBindingHash(remoteBinding)) + + remoteResource := remoteBinding.(map[string]interface{})["resource"].(string) + localResource := instanceState.Attributes["binding."+bindingHash+".resource"] + if localResource == "" { + return fmt.Errorf("expected to find binding for resource %s in state, but didn't", remoteResource) + } + if localResource != remoteResource { + return fmt.Errorf("expected to find binding for resource %s in state, but found %s instead", remoteResource, localResource) + } + + // Check Roles + remoteRoles := remoteBinding.(map[string]interface{})["roles"].(*schema.Set) + localRolesCountRaw := instanceState.Attributes["binding."+bindingHash+".roles.#"] + if localRolesCountRaw == "" { + return fmt.Errorf("cannot find role counts for the binding for resource %s", remoteResource) + } + localRolesCount, err := strconv.Atoi(localRolesCountRaw) + if err != nil { + return fmt.Errorf("expected binding.%s.roles.# to be a number, got %q", remoteResource, localRolesCountRaw) + } + if remoteRoles.Len() != localRolesCount { + return fmt.Errorf("expected %d roles for binding for resource %s but got %d instead", remoteRoles.Len(), remoteResource, localRolesCount) + } + + for _, remoteRole := range remoteRoles.List() { + roleHash := strconv.Itoa(roleHashFunction(remoteRole.(string))) + log.Printf("[DEBUG] Path to look for %s for %s", "binding."+bindingHash+".roles."+roleHash, remoteRole.(string)) + localRole := instanceState.Attributes["binding."+bindingHash+".roles."+roleHash] + if localRole == "" { + return fmt.Errorf("expected to find role %s for binding for resource %s in state, but didn't", remoteRole.(string), remoteResource) + } + + if localRole != remoteRole.(string) { + return fmt.Errorf("expected to find role %s for binding for resource %s in state, but found %s instead", remoteRole.(string), remoteResource, localRole) + } + } + } + return nil + } +} + +func testGCPSecretStaticAccountDestroy(s *terraform.State) error { + client := testProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_gcp_secret_static_account" { + continue + } + secret, err := client.Logical().Read(rs.Primary.ID) + if err != nil { + return fmt.Errorf("error checking for GCP Secrets StaticAccount %q: %s", rs.Primary.ID, err) + } + if secret != nil { + return fmt.Errorf("GCP Secrets StaticAccount %q still exists", rs.Primary.ID) + } + } + return nil +} + +func testGCPSecretStaticAccount_accessToken(backend, staticAccount, credentials, serviceAccountEmail, project string) string { + return fmt.Sprintf(` +resource "vault_gcp_secret_backend" "test" { + path = "%s" + credentials = <vault_gcp_secret_roleset + > + vault_gcp_secret_static_account + + > vault_generic_endpoint