diff --git a/util/util.go b/util/util.go index 84eaf968c..229d169f4 100644 --- a/util/util.go +++ b/util/util.go @@ -136,6 +136,20 @@ func GetTestADCreds(t *testing.T) (string, string, string) { return adBindDN, adBindPass, adURL } +func GetTestNomadCreds(t *testing.T) (string, string) { + address := os.Getenv("NOMAD_ADDR") + token := os.Getenv("NOMAD_TOKEN") + + if address == "" { + t.Skip("NOMAD_ADDR not set") + } + if token == "" { + t.Skip("NOMAD_TOKEN not set") + } + + return address, token +} + func TestCheckResourceAttrJSON(name, key, expectedValue string) resource.TestCheckFunc { return func(s *terraform.State) error { resourceState, ok := s.RootModule().Resources[name] diff --git a/vault/data_source_nomad_credentials.go b/vault/data_source_nomad_credentials.go new file mode 100644 index 000000000..cf32d192b --- /dev/null +++ b/vault/data_source_nomad_credentials.go @@ -0,0 +1,71 @@ +package vault + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/vault/api" +) + +func nomadAccessCredentialsDataSource() *schema.Resource { + return &schema.Resource{ + Read: readNomadCredsResource, + Schema: map[string]*schema.Schema{ + "backend": { + Type: schema.TypeString, + Required: true, + Description: "Nomad secret backend to generate tokens from.", + }, + "role": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the role.", + }, + "accessor_id": { + Type: schema.TypeString, + Computed: true, + Description: "The public identifier for a specific token. It can be used to look up information about a token or to revoke a token.", + }, + "secret_id": { + Type: schema.TypeString, + Computed: true, + Description: "Used to make requests to Nomad and should be kept private.", + }, + }, + } +} + +func readNomadCredsResource(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + backend := d.Get("backend").(string) + role := d.Get("role").(string) + path := fmt.Sprintf("%s/creds/%s", backend, role) + + secret, err := client.Logical().Read(path) + if err != nil { + return fmt.Errorf("error reading from Vault: %s", err) + } + log.Printf("[DEBUG] Read %q from Vault", path) + + if secret == nil { + return fmt.Errorf("no role found at %q", path) + } + + accessorID := secret.Data["accessor_id"].(string) + if accessorID == "" { + return fmt.Errorf("accessor_id is not set in response") + } + + secretID := secret.Data["secret_id"].(string) + if secretID == "" { + return fmt.Errorf("secret_id is not set in response") + } + + d.SetId(accessorID) + d.Set("accessor_id", accessorID) + d.Set("secret_id", secretID) + + return nil +} diff --git a/vault/data_source_nomad_credentials_test.go b/vault/data_source_nomad_credentials_test.go new file mode 100644 index 000000000..9fb7ef656 --- /dev/null +++ b/vault/data_source_nomad_credentials_test.go @@ -0,0 +1,96 @@ +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-provider-vault/util" +) + +func TestAccDataSourceNomadAccessCredentialsClientBasic(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-nomad") + address, token := util.GetTestNomadCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { util.TestAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceNomadAccessCredentialsConfig(backend, address, token, "test"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("data.vault_nomad_access_token.token", "secret_id"), + resource.TestCheckResourceAttrSet("data.vault_nomad_access_token.token", "accessor_id"), + ), + }, + }, + }) +} + +func TestAccDataSourceNomadAccessCredentialsManagementBasic(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-nomad") + address, token := util.GetTestNomadCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { util.TestAccPreCheck(t) }, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceNomadAccessCredentialsManagementConfig(backend, address, token, "test"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("data.vault_nomad_access_token.token", "secret_id"), + resource.TestCheckResourceAttrSet("data.vault_nomad_access_token.token", "accessor_id"), + ), + }, + }, + }) +} + +func testAccDataSourceNomadAccessCredentialsConfig(backend, address, token, role string) string { + return fmt.Sprintf(` +resource "vault_nomad_secret_backend" "config" { + backend = "%s" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + address = "%s" + token = "%s" +} + +resource "vault_nomad_secret_role" "test" { + backend = vault_nomad_secret_backend.config.backend + role = "%s" + policies = ["reaodnly"] +} + +data "vault_nomad_access_token" "token" { + backend = vault_nomad_secret_backend.config.backend + role = vault_nomad_secret_role.test.role +} +`, backend, address, token, role) +} + +func testAccDataSourceNomadAccessCredentialsManagementConfig(backend, address, token, role string) string { + return fmt.Sprintf(` +resource "vault_nomad_secret_backend" "config" { + backend = "%s" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + address = "%s" + token = "%s" +} + +resource "vault_nomad_secret_role" "test" { + backend = vault_nomad_secret_backend.config.backend + role = "%s" + type = "management" +} + +data "vault_nomad_access_token" "token" { + backend = vault_nomad_secret_backend.config.backend + role = vault_nomad_secret_role.test.role +} +`, backend, address, token, role) +} diff --git a/vault/provider.go b/vault/provider.go index 26bfae830..614d904da 100644 --- a/vault/provider.go +++ b/vault/provider.go @@ -231,6 +231,10 @@ var ( Resource: adAccessCredentialsDataSource(), PathInventory: []string{"/ad/creds/{role}"}, }, + "vault_nomad_access_token": { + Resource: nomadAccessCredentialsDataSource(), + PathInventory: []string{"/nomad/creds/{role}"}, + }, "vault_aws_access_credentials": { Resource: awsAccessCredentialsDataSource(), PathInventory: []string{"/aws/creds"}, @@ -466,6 +470,18 @@ var ( Resource: ldapAuthBackendGroupResource(), PathInventory: []string{"/auth/ldap/groups/{name}"}, }, + "vault_nomad_secret_backend": { + Resource: nomadSecretAccessBackendResource(), + PathInventory: []string{ + "/nomad", + "/nomad/config/access", + "/nomad/config/lease", + }, + }, + "vault_nomad_secret_role": { + Resource: nomadSecretBackendRoleResource(), + PathInventory: []string{"/nomad/role/{role}"}, + }, "vault_policy": { Resource: policyResource(), PathInventory: []string{"/sys/policy/{name}"}, diff --git a/vault/resource_nomad_secret_backend.go b/vault/resource_nomad_secret_backend.go new file mode 100644 index 000000000..6b2e4bf1b --- /dev/null +++ b/vault/resource_nomad_secret_backend.go @@ -0,0 +1,362 @@ +package vault + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-provider-vault/util" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/vault/api" +) + +func nomadSecretAccessBackendResource() *schema.Resource { + fields := map[string]*schema.Schema{ + "backend": { + Type: schema.TypeString, + Default: "nomad", + ForceNew: true, + Optional: true, + Description: "The mount path for the Nomad backend.", + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "address": { + Type: schema.TypeString, + Optional: true, + Description: `Specifies the address of the Nomad instance, provided as "protocol://host:port" like "http://127.0.0.1:4646".`, + }, + "ca_cert": { + Type: schema.TypeString, + Optional: true, + Description: `CA certificate to use when verifying Nomad server certificate, must be x509 PEM encoded.`, + }, + "client_cert": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: `Client certificate used for Nomad's TLS communication, must be x509 PEM encoded and if this is set you need to also set client_key.`, + }, + "client_key": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: `Client key used for Nomad's TLS communication, must be x509 PEM encoded and if this is set you need to also set client_cert.`, + }, + "default_lease_ttl_seconds": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: `Default lease duration for secrets in seconds.`, + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: `Human-friendly description of the mount for the backend.`, + }, + "local": { + Type: schema.TypeBool, + Required: false, + Optional: true, + Description: `Mark the secrets engine as local-only. Local engines are not replicated or removed by replication. Tolerance duration to use when checking the last rotation time.`, + }, + "max_lease_ttl_seconds": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Maximum possible lease duration for secrets in seconds.", + }, + "max_token_name_length": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: `Specifies the maximum length to use for the name of the Nomad token generated with Generate Credential. If omitted, 0 is used and ignored, defaulting to the max value allowed by the Nomad version.`, + }, + "max_ttl": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Maximum possible lease duration for secrets in seconds.", + }, + "token": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + Description: `Specifies the Nomad Management token to use.`, + }, + "ttl": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + Description: "Maximum possible lease duration for secrets in seconds.", + }, + } + return &schema.Resource{ + Create: createNomadAccessConfigResource, + Update: updateNomadAccessConfigResource, + Read: readNomadAccessConfigResource, + Delete: deleteNomadAccessConfigResource, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: fields, + } +} + +func createNomadAccessConfigResource(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + backend := d.Get("backend").(string) + description := d.Get("description").(string) + defaultTTL := d.Get("default_lease_ttl_seconds").(int) + local := d.Get("local").(bool) + maxTTL := d.Get("max_lease_ttl_seconds").(int) + + log.Printf("[DEBUG] Mounting Nomad backend at %q", backend) + err := client.Sys().Mount(backend, &api.MountInput{ + Type: "nomad", + Description: description, + Local: local, + Config: api.MountConfigInput{ + DefaultLeaseTTL: fmt.Sprintf("%ds", defaultTTL), + MaxLeaseTTL: fmt.Sprintf("%ds", maxTTL), + }, + }) + if err != nil { + return fmt.Errorf("error mounting to %q: %s", backend, err) + } + + log.Printf("[DEBUG] Mounted Nomad backend at %q", backend) + d.SetId(backend) + + data := map[string]interface{}{} + if v, ok := d.GetOkExists("address"); ok { + data["address"] = v + } + + if v, ok := d.GetOkExists("ca_cert"); ok { + data["ca_cert"] = v + } + + if v, ok := d.GetOkExists("client_cert"); ok { + data["client_cert"] = v + } + + if v, ok := d.GetOkExists("client_key"); ok { + data["client_key"] = v + } + + if v, ok := d.GetOkExists("max_token_name_length"); ok { + data["max_token_name_length"] = v + } + + if v, ok := d.GetOkExists("token"); ok { + data["token"] = v + } + + configPath := fmt.Sprintf("%s/config/access", backend) + log.Printf("[DEBUG] Writing %q", configPath) + if _, err := client.Logical().Write(configPath, data); err != nil { + return fmt.Errorf("error writing %q: %s", configPath, err) + } + + dataLease := map[string]interface{}{} + if v, ok := d.GetOkExists("max_ttl"); ok { + dataLease["max_ttl"] = v + } + + if v, ok := d.GetOkExists("ttl"); ok { + dataLease["ttl"] = v + } + + configLeasePath := fmt.Sprintf("%s/config/lease", backend) + log.Printf("[DEBUG] Writing %q", configLeasePath) + if _, err := client.Logical().Write(configLeasePath, dataLease); err != nil { + return fmt.Errorf("error writing %q: %s", configLeasePath, err) + } + + log.Printf("[DEBUG] Wrote %q", configLeasePath) + return readNomadAccessConfigResource(d, meta) +} + +func readNomadAccessConfigResource(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + + path := d.Id() + log.Printf("[DEBUG] Reading %q", path) + + mountResp, err := client.Sys().MountConfig(path) + if err != nil && util.Is404(err) { + log.Printf("[WARN] %q not found, removing from state", path) + d.SetId("") + return nil + } else if err != nil { + return fmt.Errorf("error reading %q: %s", path, err) + } + + d.Set("backend", d.Id()) + d.Set("default_lease_ttl_seconds", mountResp.DefaultLeaseTTL) + d.Set("max_lease_ttl_seconds", mountResp.MaxLeaseTTL) + + configPath := fmt.Sprintf("%s/config/access", d.Id()) + log.Printf("[DEBUG] Reading %q", configPath) + + resp, err := client.Logical().Read(configPath) + if err != nil { + return fmt.Errorf("error reading %q: %s", configPath, err) + } + log.Printf("[DEBUG] Read %q", configPath) + if resp == nil { + log.Printf("[WARN] %q not found, removing from state", configPath) + d.SetId("") + return nil + } + + if val, ok := resp.Data["address"]; ok { + if err := d.Set("address", val); err != nil { + return fmt.Errorf("error setting state key 'address': %s", err) + } + } + + if val, ok := resp.Data["ca_cert"]; ok { + if err := d.Set("ca_cert", val); err != nil { + return fmt.Errorf("error setting state key 'ca_cert': %s", err) + } + } + + if val, ok := resp.Data["client_cert"]; ok { + if err := d.Set("client_cert", val); err != nil { + return fmt.Errorf("error setting state key 'client_cert': %s", err) + } + } + + if val, ok := resp.Data["client_key"]; ok { + if err := d.Set("client_key", val); err != nil { + return fmt.Errorf("error setting state key 'client_key': %s", err) + } + } + + if val, ok := resp.Data["max_token_name_length"]; ok { + if err := d.Set("max_token_name_length", val); err != nil { + return fmt.Errorf("error setting state key 'max_token_name_length': %s", err) + } + } + + configLeasePath := fmt.Sprintf("%s/config/lease", d.Id()) + log.Printf("[DEBUG] Reading %q", configLeasePath) + + resp, err = client.Logical().Read(configLeasePath) + if err != nil { + return fmt.Errorf("error reading %q: %s", configLeasePath, err) + } + log.Printf("[DEBUG] Read %q", configLeasePath) + if resp == nil { + log.Printf("[WARN] %q not found, removing from state", configLeasePath) + d.SetId("") + return nil + } + + if val, ok := resp.Data["max_ttl"]; ok { + if err := d.Set("max_ttl", val); err != nil { + return fmt.Errorf("error setting state key 'max_ttl': %s", err) + } + } + + if val, ok := resp.Data["ttl"]; ok { + if err := d.Set("ttl", val); err != nil { + return fmt.Errorf("error setting state key 'ttl': %s", err) + } + } + + return nil +} + +func updateNomadAccessConfigResource(d *schema.ResourceData, meta interface{}) error { + backend := d.Id() + + client := meta.(*api.Client) + tune := api.MountConfigInput{} + data := map[string]interface{}{} + + if d.HasChange("default_lease_ttl_seconds") || d.HasChange("max_lease_ttl_seconds") { + tune.DefaultLeaseTTL = fmt.Sprintf("%ds", d.Get("default_lease_ttl_seconds")) + tune.MaxLeaseTTL = fmt.Sprintf("%ds", d.Get("max_lease_ttl_seconds")) + + log.Printf("[DEBUG] Updating mount lease TTLs for %q", backend) + err := client.Sys().TuneMount(backend, tune) + if err != nil { + return fmt.Errorf("error updating mount TTLs for %q: %s", backend, err) + } + log.Printf("[DEBUG] Updated lease TTLs for %q", backend) + } + + configPath := fmt.Sprintf("%s/config/access", backend) + log.Printf("[DEBUG] Updating %q", configPath) + + if raw, ok := d.GetOk("address"); ok { + data["address"] = raw + } + + if raw, ok := d.GetOk("ca_cert"); ok { + data["ca_cert"] = raw + } + + if raw, ok := d.GetOk("client_cert"); ok { + data["client_cert"] = raw + } + + if raw, ok := d.GetOk("client_key"); ok { + data["client_key"] = raw + } + + if raw, ok := d.GetOk("max_token_name_length"); ok { + data["max_token_name_length"] = raw + } + + if raw, ok := d.GetOk("token"); ok { + data["token"] = raw + } + + if _, err := client.Logical().Write(configPath, data); err != nil { + return fmt.Errorf("error updating access config %q: %s", configPath, err) + } + log.Printf("[DEBUG] Updated %q", configPath) + + configLeasePath := fmt.Sprintf("%s/config/lease", backend) + log.Printf("[DEBUG] Updating %q", configLeasePath) + + dataLease := map[string]interface{}{} + + if raw, ok := d.GetOk("max_ttl"); ok { + dataLease["max_ttl"] = raw + } + + if raw, ok := d.GetOk("ttl"); ok { + dataLease["ttl"] = raw + } + + if _, err := client.Logical().Write(configLeasePath, dataLease); err != nil { + return fmt.Errorf("error updating lease config %q: %s", configLeasePath, err) + } + + log.Printf("[DEBUG] Updated %q", configLeasePath) + return readNomadAccessConfigResource(d, meta) +} + +func deleteNomadAccessConfigResource(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + vaultPath := d.Id() + log.Printf("[DEBUG] Unmounting Nomad backend %q", vaultPath) + + err := client.Sys().Unmount(vaultPath) + if err != nil && util.Is404(err) { + log.Printf("[WARN] %q not found, removing from state", vaultPath) + d.SetId("") + return fmt.Errorf("error unmounting Nomad backend from %q: %s", vaultPath, err) + } else if err != nil { + return fmt.Errorf("error unmounting Nomad backend from %q: %s", vaultPath, err) + } + log.Printf("[DEBUG] Unmounted Nomad backend %q", vaultPath) + return nil +} diff --git a/vault/resource_nomad_secret_backend_test.go b/vault/resource_nomad_secret_backend_test.go new file mode 100644 index 000000000..0e2759f51 --- /dev/null +++ b/vault/resource_nomad_secret_backend_test.go @@ -0,0 +1,101 @@ +package vault + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform-provider-vault/util" + "github.com/hashicorp/vault/api" +) + +func TestAccNomadSecretBackend(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-nomad") + address, token := util.GetTestNomadCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { util.TestAccPreCheck(t) }, + PreventPostDestroyRefresh: true, + CheckDestroy: testAccNomadSecretBackendCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testNomadSecretBackendConfig(backend, address, token, 60, 30, 3600, 7200), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "backend", backend), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "description", "test description"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "default_lease_ttl_seconds", "3600"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "max_lease_ttl_seconds", "7200"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "address", address), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "max_ttl", "60"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "ttl", "30"), + ), + }, + { + Config: testNomadSecretBackendConfig(backend, "foobar", token, 90, 60, 7200, 14400), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "backend", backend), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "description", "test description"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "default_lease_ttl_seconds", "7200"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "max_lease_ttl_seconds", "14400"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "address", "foobar"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "max_ttl", "90"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "ttl", "60"), + ), + }, + { + Config: testNomadSecretBackendConfig(backend, "foobar", token, 0, 0, -1, -1), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "backend", backend), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "description", "test description"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "default_lease_ttl_seconds", "-1"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "max_lease_ttl_seconds", "-1"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "address", "foobar"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "max_ttl", "0"), + resource.TestCheckResourceAttr("vault_nomad_secret_backend.test", "ttl", "0"), + ), + }, + }, + }) +} + +func testAccNomadSecretBackendCheckDestroy(s *terraform.State) error { + client := testProvider.Meta().(*api.Client) + + mounts, err := client.Sys().ListMounts() + if err != nil { + return err + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_nomad_secret_backend" { + continue + } + for backend, mount := range mounts { + backend = strings.Trim(backend, "/") + rsBackend := strings.Trim(rs.Primary.Attributes["backend"], "/") + if mount.Type == "nomad" && backend == rsBackend { + return fmt.Errorf("Mount %q still exists", rsBackend) + } + } + } + return nil +} + +func testNomadSecretBackendConfig(backend, address, token string, maxTTL, ttl, defaultLease, maxLease int) string { + return fmt.Sprintf(` +resource "vault_nomad_secret_backend" "test" { + backend = "%s" + description = "test description" + address = "%s" + token = "%s" + max_ttl = "%d" + ttl = "%d" + default_lease_ttl_seconds = "%d" + max_lease_ttl_seconds = "%d" +} +`, backend, address, token, maxTTL, ttl, defaultLease, maxLease) +} diff --git a/vault/resource_nomad_secret_role.go b/vault/resource_nomad_secret_role.go new file mode 100644 index 000000000..5eceffc7e --- /dev/null +++ b/vault/resource_nomad_secret_role.go @@ -0,0 +1,239 @@ +package vault + +import ( + "fmt" + "log" + "regexp" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-provider-vault/util" + "github.com/hashicorp/vault/api" +) + +var ( + nomadSecretBackendFromRolePathRegex = regexp.MustCompile("^(.+)/role/.+$") + nomadSecretBackendRoleNameFromPathRegex = regexp.MustCompile("^.+/role/(.+$)") +) + +func nomadSecretBackendRoleResource() *schema.Resource { + fields := map[string]*schema.Schema{ + "backend": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The mount path for the Nomad backend.", + StateFunc: func(v interface{}) string { + return strings.Trim(v.(string), "/") + }, + }, + "role": { + Type: schema.TypeString, + Required: true, + Description: `Name of the role.`, + ForceNew: true, + }, + "global": { + Type: schema.TypeBool, + Computed: true, + Optional: true, + Description: `Specifies if the token should be global.`, + }, + "policies": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + Optional: true, + Description: `Comma separated list of Nomad policies the token is going to be created against. These need to be created beforehand in Nomad.`, + }, + "type": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: `Specifies the type of token to create when using this role. Valid values are "client" or "management".`, + }, + } + return &schema.Resource{ + Create: createNomadRoleResource, + Update: updateNomadRoleResource, + Read: readNomadRoleResource, + Delete: deleteNomadRoleResource, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: fields, + } +} + +func createNomadRoleResource(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + backend := d.Get("backend").(string) + role := d.Get("role").(string) + roleType := d.Get("type").(string) + + if roleType == "" { + roleType = "client" + } + + rolePath := fmt.Sprintf("%s/role/%s", backend, role) + + log.Printf("[DEBUG] Creating %q", rolePath) + + data := map[string]interface{}{} + data["type"] = roleType + + if v, ok := d.GetOkExists("global"); ok { + data["global"] = v + } + + // Policies are required if role type is 'client', so setting up + // to enforce that here. + if v, ok := d.GetOkExists("policies"); ok { + if roleType == "client" { + data["policies"] = v + } + } + + if roleType == "client" && data["policies"] == nil { + return fmt.Errorf("error creating role %s: policies are required when role type is 'client'", role) + } + + // Policies not supported when role type is 'management' + if roleType == "management" && data["policies"] != nil { + return fmt.Errorf("error creating role %s: policies should be empty when using management tokens", role) + } + + log.Printf("[DEBUG] Writing %q", rolePath) + if _, err := client.Logical().Write(rolePath, data); err != nil { + return fmt.Errorf("error writing %q: %s", rolePath, err) + } + d.SetId(rolePath) + log.Printf("[DEBUG] Wrote %q", rolePath) + return readNomadRoleResource(d, meta) +} + +func readNomadRoleResource(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + rolePath := d.Id() + log.Printf("[DEBUG] Reading %q", rolePath) + + backend, err := nomadSecretBackendFromRolePath(rolePath) + if err != nil { + return fmt.Errorf("invalid role ID for backend %q: %s", rolePath, err) + } + d.Set("backend", backend) + + roleName, err := nomadSecretBackendRoleNameFromPath(rolePath) + if err != nil { + return fmt.Errorf("invalid role ID %q: %s", rolePath, err) + } + d.Set("role", roleName) + + resp, err := client.Logical().Read(rolePath) + if err != nil { + return fmt.Errorf("error reading %q: %s", rolePath, err) + } + log.Printf("[DEBUG] Read %q", rolePath) + + if resp == nil { + log.Printf("[WARN] %q not found, removing from state", rolePath) + d.SetId("") + return nil + } + + if val, ok := resp.Data["global"]; ok { + if err := d.Set("global", val); err != nil { + return fmt.Errorf("error setting state key 'global': %s", err) + } + } + + if val, ok := resp.Data["policies"]; ok { + if err := d.Set("policies", val); err != nil { + return fmt.Errorf("error setting state key 'policies': %s", err) + } + } + + if val, ok := resp.Data["type"]; ok { + if err := d.Set("type", val); err != nil { + return fmt.Errorf("error setting state key 'type': %s", err) + } + } + + return nil +} + +func updateNomadRoleResource(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + rolePath := d.Id() + roleType := d.Get("type").(string) + if roleType == "" { + roleType = "client" + } + + roleName, err := nomadSecretBackendRoleNameFromPath(rolePath) + if err != nil { + return fmt.Errorf("invalid role ID %q: %s", rolePath, err) + } + + log.Printf("[DEBUG] Updating %q", rolePath) + + data := map[string]interface{}{} + data["type"] = roleType + + if raw, ok := d.GetOk("global"); ok { + data["global"] = raw + } + if raw, ok := d.GetOk("policies"); ok { + if roleType == "client" { + data["policies"] = raw + } + } + + if roleType == "client" && data["policies"] == "" { + return fmt.Errorf("error updating role %s: policies are required when role type is 'client'", roleName) + } + + if _, err := client.Logical().Write(rolePath, data); err != nil { + return fmt.Errorf("error updating role %q: %s", rolePath, err) + } + log.Printf("[DEBUG] Updated %q", rolePath) + return readNomadRoleResource(d, meta) +} + +func deleteNomadRoleResource(d *schema.ResourceData, meta interface{}) error { + client := meta.(*api.Client) + rolePath := d.Id() + log.Printf("[DEBUG] Deleting %q", rolePath) + + if _, err := client.Logical().Delete(rolePath); err != nil && !util.Is404(err) { + return fmt.Errorf("error deleting %q: %s", rolePath, err) + } else if err != nil { + log.Printf("[DEBUG] %q not found, removing from state", rolePath) + d.SetId("") + return nil + } + log.Printf("[DEBUG] Deleted template auth backend role %q", rolePath) + return nil +} + +func nomadSecretBackendRoleNameFromPath(path string) (string, error) { + if !nomadSecretBackendRoleNameFromPathRegex.MatchString(path) { + return "", fmt.Errorf("no name found") + } + res := nomadSecretBackendRoleNameFromPathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for name", len(res)) + } + return res[1], nil +} + +func nomadSecretBackendFromRolePath(path string) (string, error) { + if !nomadSecretBackendFromRolePathRegex.MatchString(path) { + return "", fmt.Errorf("no backend found") + } + res := nomadSecretBackendFromRolePathRegex.FindStringSubmatch(path) + if len(res) != 2 { + return "", fmt.Errorf("unexpected number of matches (%d) for backend", len(res)) + } + return res[1], nil +} diff --git a/vault/resource_nomad_secret_role_test.go b/vault/resource_nomad_secret_role_test.go new file mode 100644 index 000000000..55dfe1304 --- /dev/null +++ b/vault/resource_nomad_secret_role_test.go @@ -0,0 +1,144 @@ +package vault + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/hashicorp/terraform-provider-vault/util" + "github.com/hashicorp/vault/api" +) + +func TestAccNomadSecretBackendRoleClientBasic(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-nomad") + address, token := util.GetTestNomadCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { util.TestAccPreCheck(t) }, + CheckDestroy: testAccNomadSecretBackendRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testNomadSecretBackendRoleClientConfig(backend, address, token, "bob", "readonly", true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "role", "bob"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "policies.#", "1"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "policies.0", "readonly"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "global", "true"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "type", "client"), + ), + }, + }, + }) +} + +func TestAccNomadSecretBackendRoleManagementBasic(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-nomad") + address, token := util.GetTestNomadCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { util.TestAccPreCheck(t) }, + CheckDestroy: testAccNomadSecretBackendRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testNomadSecretBackendRoleManagementConfig(backend, address, token, "bob", false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "role", "bob"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "policies.#", "0"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "global", "false"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "type", "management"), + ), + }, + }, + }) +} + +func TestAccNomadSecretBackendRoleImport(t *testing.T) { + backend := acctest.RandomWithPrefix("tf-test-nomad") + address, token := util.GetTestNomadCreds(t) + + resource.Test(t, resource.TestCase{ + Providers: testProviders, + PreCheck: func() { util.TestAccPreCheck(t) }, + CheckDestroy: testAccADSecretBackendRoleCheckDestroy, + Steps: []resource.TestStep{ + { + Config: testNomadSecretBackendRoleClientConfig(backend, address, token, "bob", "readonly", true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "role", "bob"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "policies.#", "1"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "policies.0", "readonly"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "global", "true"), + resource.TestCheckResourceAttr("vault_nomad_secret_role.test", "type", "client"), + ), + }, + { + ResourceName: "vault_nomad_secret_role.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccNomadSecretBackendRoleCheckDestroy(s *terraform.State) error { + client := testProvider.Meta().(*api.Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "vault_nomad_secret_role" { + continue + } + secret, err := client.Logical().Read(rs.Primary.ID) + if err != nil { + return err + } + if secret != nil { + return fmt.Errorf("role %q still exists", rs.Primary.ID) + } + } + return nil +} + +func testNomadSecretBackendRoleClientConfig(backend, address, token, role, policies string, global bool) string { + return fmt.Sprintf(` +resource "vault_nomad_secret_backend" "config" { + backend = "%s" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + address = "%s" + token = "%s" +} + +resource "vault_nomad_secret_role" "test" { + backend = vault_nomad_secret_backend.config.backend + role = "%s" + type = "client" + policies = ["%s"] + global = "%t" +} +`, backend, address, token, role, policies, global) +} + +func testNomadSecretBackendRoleManagementConfig(backend, address, token, role string, global bool) string { + return fmt.Sprintf(` +resource "vault_nomad_secret_backend" "config" { + backend = "%s" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + address = "%s" + token = "%s" +} + +resource "vault_nomad_secret_role" "test" { + backend = vault_nomad_secret_backend.config.backend + role = "%s" + type = "management" + global = "%t" +} +`, backend, address, token, role, global) +} diff --git a/website/docs/d/nomad_access_token.html.md b/website/docs/d/nomad_access_token.html.md new file mode 100644 index 000000000..bceb17bbc --- /dev/null +++ b/website/docs/d/nomad_access_token.html.md @@ -0,0 +1,64 @@ +--- +layout: "vault" +page_title: "Vault: vault_nomad_access_token data source" +sidebar_current: "docs-vault-datasource-nomad-access-token" +description: |- + Generates tokens for Nomad. +--- + +# vault\_nomad\_access\_token + +Generates tokens for Nomad. + +~> **Important** All data retrieved from Vault will be +written in cleartext to state file generated by Terraform, will appear in +the console output when Terraform runs, and may be included in plan files +if secrets are interpolated into any resource attributes. +Protect these artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +```hcl +resource "vault_nomad_secret_backend" "config" { + backend = "nomad" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + address = "https://127.0.0.1:4646" + token = "ae20ceaa-..." +} + +resource "vault_nomad_secret_role" "test" { + backend = vault_nomad_secret_backend.config.backend + role = "test" + type = "client" + policies = ["readonly"] +} + +data "vault_nomad_access_token" "token" { + backend = vault_nomad_secret_backend.config.backend + role = vault_nomad_secret_role.test.role + depends_on = [vault_nomad_secret_role.test] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `backend` - (Required) The path to the Nomad secret backend to +read credentials from, with no leading or trailing `/`s. + +* `role` - (Required) The name of the Nomad secret backend role to generate +a token for, with no leading or trailing `/`s. + +## Attributes Reference + +In addition to the arguments above, the following attributes are exported: + +* `accessor_id` - The public identifier for a specific token. It can be used +to look up information about a token or to revoke a token. + +* `secret_id` - The token to be used when making requests to Nomad and should be kept private. \ No newline at end of file diff --git a/website/docs/r/nomad_secret_backend.html.md b/website/docs/r/nomad_secret_backend.html.md new file mode 100644 index 000000000..598265b0d --- /dev/null +++ b/website/docs/r/nomad_secret_backend.html.md @@ -0,0 +1,82 @@ +--- +layout: "vault" +page_title: "Vault: vault_nomad_secret_backend resource" +sidebar_current: "docs-vault-resource-nomad-secret-backend" +description: |- + Creates a Nomad secret backend for Vault. +--- + +# vault\_nomad\_secret\_backend + +Creates a Nomad Secret Backend for Vault. The Nomad secret backend for Vault +generates Nomad ACL tokens dynamically based on pre-existing Nomad ACL policies. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +```hcl +resource "vault_nomad_secret_backend" "config" { + backend = "nomad" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + max_ttl = "240" + address = "https://127.0.0.1:4646" + token = "ae20ceaa-... + ttl = "120" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `backend` - (Optional) The unique path this backend should be mounted at. Must +not begin or end with a `/`. Defaults to `nomad`. + +* `address` - (Optional) Specifies the address of the Nomad instance, provided +as "protocol://host:port" like "http://127.0.0.1:4646". + +* `ca_cert` - (Optional) CA certificate to use when verifying the Nomad server certificate, must be +x509 PEM encoded. + +* `client_cert` - (Optional) Client certificate to provide to the Nomad server, must be x509 PEM encoded. + +* `client_key` - (Optional) Client certificate key to provide to the Nomad server, must be x509 PEM encoded. + +* `default_lease_ttl_seconds` - (Optional) Default lease duration for secrets in seconds. + +* `description` - (Optional) Human-friendly description of the mount for the Active Directory backend. + +* `local` - (Optional) Mark the secrets engine as local-only. Local engines are not replicated or removed by +replication.Tolerance duration to use when checking the last rotation time. + +* `max_token_name_length` - (Optional) Specifies the maximum length to use for the name of the Nomad token +generated with Generate Credential. If omitted, 0 is used and ignored, defaulting to the max value allowed +by the Nomad version. + +* `max_ttl` - (Optional) Maximum possible lease duration for secrets in seconds. + +* `token` - (Optional) Specifies the Nomad Management token to use. + +* `ttl` - (Optional) Specifies the ttl of the lease for the generated token. + + + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +Nomad secret backend can be imported using the `backend`, e.g. + +``` +$ terraform import vault_nomad_secret_backend.nomad nomad +``` diff --git a/website/docs/r/nomad_secret_role.html.md b/website/docs/r/nomad_secret_role.html.md new file mode 100644 index 000000000..de5fefb7c --- /dev/null +++ b/website/docs/r/nomad_secret_role.html.md @@ -0,0 +1,72 @@ +--- +layout: "vault" +page_title: "Vault: vault_nomad_secret_role resource" +sidebar_current: "docs-vault-resource-nomad-secret-role" +description: |- + Creates a Nomad role. +--- + +# vault\_nomad\_secret\_role + +Creates a Vault role for a Nomad token. This role configures how generated tokens +will function. + +~> **Important** All data provided in the resource configuration will be +written in cleartext to state and plan files generated by Terraform, and +will appear in the console output when Terraform runs. Protect these +artifacts accordingly. See +[the main provider documentation](../index.html) +for more details. + +## Example Usage + +```hcl +resource "vault_nomad_secret_backend" "config" { + backend = "nomad" + description = "test description" + default_lease_ttl_seconds = "3600" + max_lease_ttl_seconds = "7200" + address = "https://127.0.0.1:4646" + token = "ae20ceaa-..." +} + +resource "vault_nomad_secret_role" "test" { + backend = vault_nomad_secret_backend.config.backend + role = "test" + type = "client" + policies = ["readonly"] +} +``` + +## Argument Reference + +The following arguments are supported: + +* `backend` - (Optional) The unique path this backend should be mounted at. Must +not begin or end with a `/`. Defaults to `nomad`. + +* `role` - (Required) The name to identify this role within the backend. +Must be unique within the backend. + +* `global` - (Optional) Specifies if the generated token should be global. Defaults to +false. + +* `policies` - (Optional) List of policies attached to the generated token. This setting is only used +when `type` is 'client'. + +* `type` - (Optional) Specifies the type of token to create when using this role. Valid +settings are 'client' and 'management'. Defaults to 'client'. + + + +## Attributes Reference + +No additional attributes are exported by this resource. + +## Import + +Nomad secret role can be imported using the `backend`, e.g. + +``` +$ terraform import vault_nomad_secret_role.bob nomad/role/bob +```