Add support for DB Adv TTL Mgmt (#2011)

* add support for schedule-based roles
This commit is contained in:
Milena Zlaticanin 2023-09-26 11:09:09 -07:00 committed by GitHub
parent 8adb474d0b
commit 2653605832
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 263 additions and 110 deletions

View file

@ -16,16 +16,19 @@ services:
VAULT_TOKEN: "TEST"
MYSQL_URL: "vault:vault@tcp(mysql:3306)/"
# to run this container on Mac M1 you need to
# export DOCKER_DEFAULT_PLATFORM=linux/amd64
# to run acceptance tests locally you need to
# export MYSQL_URL=root:mysql@tcp(localhost:3306)/
# to run the docker container
# docker compose up -d mysql
mysql:
image: mysql:5.7
image: docker.mirror.hashicorp.services/mysql:latest
command: --default-authentication-plugin=mysql_native_password
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: "root"
MYSQL_DATABASE: "main"
MYSQL_USER: "vault"
MYSQL_PASSWORD: "vault"
MYSQL_ROOT_PASSWORD: "mysql"
elastic:
image: elasticsearch:7.17.10
@ -95,4 +98,4 @@ services:
ports:
- "9042:9042"
volumes:
- ./testdata/cassandra.yaml:/etc/cassandra/cassandra.yaml
- ./testdata/cassandra.yaml:/etc/cassandra/cassandra.yaml

View file

@ -352,6 +352,9 @@ const (
FieldCredentialType = "credential_type"
FieldFilename = "filename"
FieldDefault = "default"
FieldRotationStatements = "rotation_statements"
FieldRotationSchedule = "rotation_schedule"
FieldRotationWindow = "rotation_window"
FieldKubernetesCACert = "kubernetes_ca_cert"
FieldDisableLocalCAJWT = "disable_local_ca_jwt"
FieldKubernetesHost = "kubernetes_host"

View file

@ -4,8 +4,10 @@
package vault
import (
"encoding/json"
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-provider-vault/internal/consts"
"log"
"regexp"
"strings"
@ -20,39 +22,44 @@ var (
databaseSecretBackendStaticRoleNameFromPathRegex = regexp.MustCompile("^.+/static-roles/(.+$)")
)
var staticRoleFields = []string{
consts.FieldRotationPeriod,
consts.FieldRotationStatements,
consts.FieldDBName,
}
func databaseSecretBackendStaticRoleResource() *schema.Resource {
return &schema.Resource{
Create: databaseSecretBackendStaticRoleWrite,
Read: provider.ReadWrapper(databaseSecretBackendStaticRoleRead),
Update: databaseSecretBackendStaticRoleWrite,
Delete: databaseSecretBackendStaticRoleDelete,
Exists: databaseSecretBackendStaticRoleExists,
CreateContext: databaseSecretBackendStaticRoleWrite,
ReadContext: provider.ReadContextWrapper(databaseSecretBackendStaticRoleRead),
UpdateContext: databaseSecretBackendStaticRoleWrite,
DeleteContext: databaseSecretBackendStaticRoleDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
"name": {
consts.FieldName: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Unique name for the static role.",
},
"backend": {
consts.FieldBackend: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The path of the Database Secret Backend the role belongs to.",
},
"username": {
consts.FieldUsername: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "The database username that this role corresponds to.",
},
"rotation_period": {
consts.FieldRotationPeriod: {
Type: schema.TypeInt,
Required: true,
Optional: true,
Description: "The amount of time Vault should wait before rotating the password, in seconds.",
ValidateFunc: func(v interface{}, k string) (ws []string, errs []error) {
value := v.(int)
@ -62,13 +69,24 @@ func databaseSecretBackendStaticRoleResource() *schema.Resource {
return
},
},
"db_name": {
consts.FieldRotationSchedule: {
Type: schema.TypeString,
Optional: true,
Description: "A cron-style string that will define the schedule on which rotations should occur.",
},
consts.FieldRotationWindow: {
Type: schema.TypeInt,
Optional: true,
Description: "The amount of time in seconds in which the rotations are allowed to occur starting " +
"from a given rotation_schedule.",
},
consts.FieldDBName: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Database connection to use for this role.",
},
"rotation_statements": {
consts.FieldRotationStatements: {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
@ -78,43 +96,56 @@ func databaseSecretBackendStaticRoleResource() *schema.Resource {
}
}
func databaseSecretBackendStaticRoleWrite(d *schema.ResourceData, meta interface{}) error {
func databaseSecretBackendStaticRoleWrite(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
return diag.FromErr(e)
}
backend := d.Get("backend").(string)
name := d.Get("name").(string)
backend := d.Get(consts.FieldBackend).(string)
name := d.Get(consts.FieldName).(string)
path := databaseSecretBackendStaticRolePath(backend, name)
data := map[string]interface{}{
"username": d.Get("username"),
"rotation_period": d.Get("rotation_period"),
"db_name": d.Get("db_name"),
"username": d.Get(consts.FieldUsername),
"db_name": d.Get(consts.FieldDBName),
"rotation_statements": []string{},
}
if v, ok := d.GetOkExists("rotation_statements"); ok && v != "" {
data["rotation_statements"] = v
useAPIVer115 := provider.IsAPISupported(meta, provider.VaultVersion115)
if useAPIVer115 {
if v, ok := d.GetOk(consts.FieldRotationSchedule); ok && v != "" {
data[consts.FieldRotationSchedule] = v
}
if v, ok := d.GetOk(consts.FieldRotationWindow); ok && v != "" {
data[consts.FieldRotationWindow] = v
}
}
if v, ok := d.GetOk(consts.FieldRotationStatements); ok && v != "" {
data[consts.FieldRotationStatements] = v
}
if v, ok := d.GetOk(consts.FieldRotationPeriod); ok && v != "" {
data[consts.FieldRotationPeriod] = v
}
log.Printf("[DEBUG] Creating static role %q on database backend %q", name, backend)
_, err := client.Logical().Write(path, data)
if err != nil {
return fmt.Errorf("error creating static role %q for backend %q: %s", name, backend, err)
return diag.Errorf("error creating static role %q for backend %q: %s", name, backend, err)
}
log.Printf("[DEBUG] Created static role %q on AWS backend %q", name, backend)
d.SetId(path)
return databaseSecretBackendStaticRoleRead(d, meta)
return databaseSecretBackendStaticRoleRead(ctx, d, meta)
}
func databaseSecretBackendStaticRoleRead(d *schema.ResourceData, meta interface{}) error {
func databaseSecretBackendStaticRoleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
return diag.FromErr(e)
}
path := d.Id()
@ -123,20 +154,20 @@ func databaseSecretBackendStaticRoleRead(d *schema.ResourceData, meta interface{
if err != nil {
log.Printf("[WARN] Removing database static role %q because its ID is invalid", path)
d.SetId("")
return fmt.Errorf("invalid static role ID %q: %s", path, err)
return diag.Errorf("invalid static role ID %q: %s", path, err)
}
backend, err := databaseSecretBackendStaticRoleBackendFromPath(path)
if err != nil {
log.Printf("[WARN] Removing database static role %q because its ID is invalid", path)
d.SetId("")
return fmt.Errorf("invalid static role ID %q: %s", path, err)
return diag.Errorf("invalid static role ID %q: %s", path, err)
}
log.Printf("[DEBUG] Reading static role from %q", path)
role, err := client.Logical().Read(path)
if err != nil {
return fmt.Errorf("error reading static role %q: %s", path, err)
return diag.Errorf("error reading static role %q: %s", path, err)
}
log.Printf("[DEBUG] Read static role from %q", path)
if role == nil {
@ -145,67 +176,55 @@ func databaseSecretBackendStaticRoleRead(d *schema.ResourceData, meta interface{
return nil
}
d.Set("backend", backend)
d.Set("name", name)
d.Set("username", role.Data["username"])
d.Set("db_name", role.Data["db_name"])
if v, ok := role.Data["rotation_period"]; ok {
n, err := v.(json.Number).Int64()
if err != nil {
return fmt.Errorf("unexpected value %q for rotation_period of %q", v, path)
}
d.Set("rotation_period", n)
if err := d.Set(consts.FieldBackend, backend); err != nil {
return diag.FromErr(err)
}
var rotation []string
if rotationStr, ok := role.Data["rotation_statements"].(string); ok {
rotation = append(rotation, rotationStr)
} else if rotations, ok := role.Data["rotation_statements"].([]interface{}); ok {
for _, cr := range rotations {
rotation = append(rotation, cr.(string))
if err := d.Set(consts.FieldName, name); err != nil {
return diag.FromErr(err)
}
if err := d.Set(consts.FieldUsername, role.Data[consts.FieldUsername]); err != nil {
return diag.FromErr(err)
}
useAPIVer115 := provider.IsAPISupported(meta, provider.VaultVersion115)
if useAPIVer115 {
if err := d.Set(consts.FieldRotationSchedule, role.Data[consts.FieldRotationSchedule]); err != nil {
return diag.FromErr(err)
}
if err := d.Set(consts.FieldRotationWindow, role.Data[consts.FieldRotationWindow]); err != nil {
return diag.FromErr(err)
}
}
err = d.Set("rotation_statements", rotation)
if err != nil {
return fmt.Errorf("unexpected value %q for rotation_statements of %s: %s", rotation, path, err)
for _, k := range staticRoleFields {
if v, ok := role.Data[k]; ok {
if err := d.Set(k, v); err != nil {
return diag.FromErr(err)
}
}
}
return nil
}
func databaseSecretBackendStaticRoleDelete(d *schema.ResourceData, meta interface{}) error {
func databaseSecretBackendStaticRoleDelete(_ context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client, e := provider.GetClient(d, meta)
if e != nil {
return e
return diag.FromErr(e)
}
path := d.Id()
log.Printf("[DEBUG] Deleting static role %q", path)
_, err := client.Logical().Delete(path)
if err != nil {
return fmt.Errorf("error deleting static role %q: %s", path, err)
return diag.Errorf("error deleting static role %q: %s", path, err)
}
log.Printf("[DEBUG] Deleted static role %q", path)
return nil
}
func databaseSecretBackendStaticRoleExists(d *schema.ResourceData, meta interface{}) (bool, error) {
client, e := provider.GetClient(d, meta)
if e != nil {
return false, e
}
path := d.Id()
log.Printf("[DEBUG] Checking if %q exists", path)
role, 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 role != nil, nil
}
func databaseSecretBackendStaticRolePath(backend, name string) string {
return strings.Trim(backend, "/") + "/static-roles/" + strings.Trim(name, "/")
}

View file

@ -20,14 +20,13 @@ import (
)
func TestAccDatabaseSecretBackendStaticRole_import(t *testing.T) {
connURL := os.Getenv("MYSQL_URL")
if connURL == "" {
t.Skip("MYSQL_URL not set")
}
connURL := testutil.SkipTestEnvUnset(t, "MYSQL_URL")[0]
backend := acctest.RandomWithPrefix("tf-test-db")
username := acctest.RandomWithPrefix("user")
dbName := acctest.RandomWithPrefix("db")
name := acctest.RandomWithPrefix("staticrole")
resourceName := "vault_database_secret_backend_static_role.test"
if err := createTestUser(connURL, username); err != nil {
t.Fatal(err)
@ -39,13 +38,13 @@ func TestAccDatabaseSecretBackendStaticRole_import(t *testing.T) {
CheckDestroy: testAccDatabaseSecretBackendStaticRoleCheckDestroy,
Steps: []resource.TestStep{
{
Config: testAccDatabaseSecretBackendStaticRoleConfig_basic(name, username, dbName, backend, connURL),
Config: testAccDatabaseSecretBackendStaticRoleConfig_rotationPeriod(name, username, dbName, backend, connURL),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "name", name),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "backend", backend),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "username", username),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "db_name", dbName),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "rotation_period", "3600"),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "backend", backend),
resource.TestCheckResourceAttr(resourceName, "username", username),
resource.TestCheckResourceAttr(resourceName, "db_name", dbName),
resource.TestCheckResourceAttr(resourceName, "rotation_period", "3600"),
),
},
{
@ -57,15 +56,14 @@ func TestAccDatabaseSecretBackendStaticRole_import(t *testing.T) {
})
}
func TestAccDatabaseSecretBackendStaticRole_basic(t *testing.T) {
connURL := os.Getenv("MYSQL_URL")
if connURL == "" {
t.Skip("MYSQL_URL not set")
}
func TestAccDatabaseSecretBackendStaticRole_rotationPeriod(t *testing.T) {
connURL := testutil.SkipTestEnvUnset(t, "MYSQL_URL")[0]
backend := acctest.RandomWithPrefix("tf-test-db")
name := acctest.RandomWithPrefix("staticrole")
username := acctest.RandomWithPrefix("user")
dbName := acctest.RandomWithPrefix("db")
name := acctest.RandomWithPrefix("staticrole")
resourceName := "vault_database_secret_backend_static_role.test"
if err := createTestUser(connURL, username); err != nil {
t.Fatal(err)
@ -77,24 +75,74 @@ func TestAccDatabaseSecretBackendStaticRole_basic(t *testing.T) {
CheckDestroy: testAccDatabaseSecretBackendStaticRoleCheckDestroy,
Steps: []resource.TestStep{
{
Config: testAccDatabaseSecretBackendStaticRoleConfig_basic(name, username, dbName, backend, connURL),
Config: testAccDatabaseSecretBackendStaticRoleConfig_rotationPeriod(name, username, dbName, backend, connURL),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "name", name),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "backend", backend),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "username", username),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "db_name", dbName),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "rotation_period", "3600"),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "backend", backend),
resource.TestCheckResourceAttr(resourceName, "username", username),
resource.TestCheckResourceAttr(resourceName, "db_name", dbName),
resource.TestCheckResourceAttr(resourceName, "rotation_period", "3600"),
),
},
{
Config: testAccDatabaseSecretBackendStaticRoleConfig_updated(name, username, dbName, backend, connURL),
Config: testAccDatabaseSecretBackendStaticRoleConfig_updatedRotationPeriod(name, username, dbName, backend, connURL),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "name", name),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "backend", backend),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "username", username),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "db_name", dbName),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "rotation_period", "1800"),
resource.TestCheckResourceAttr("vault_database_secret_backend_static_role.test", "rotation_statements.0", "SELECT 1;"),
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "backend", backend),
resource.TestCheckResourceAttr(resourceName, "username", username),
resource.TestCheckResourceAttr(resourceName, "db_name", dbName),
resource.TestCheckResourceAttr(resourceName, "rotation_period", "1800"),
resource.TestCheckResourceAttr(resourceName, "rotation_statements.0", "SELECT 1;"),
),
},
},
})
}
func TestAccDatabaseSecretBackendStaticRole_rotationSchedule(t *testing.T) {
connURL := os.Getenv("MYSQL_URL")
if connURL == "" {
t.Skip("MYSQL_URL not set")
}
backend := acctest.RandomWithPrefix("tf-test-db")
username := acctest.RandomWithPrefix("username")
dbName := acctest.RandomWithPrefix("db")
name := acctest.RandomWithPrefix("static-role")
resourceName := "vault_database_secret_backend_static_role.test"
if err := createTestUser(connURL, username); err != nil {
t.Fatal(err)
}
resource.Test(t, resource.TestCase{
Providers: testProviders,
PreCheck: func() {
testutil.TestAccPreCheck(t)
SkipIfAPIVersionLT(t, testProvider.Meta(), provider.VaultVersion115)
},
CheckDestroy: testAccDatabaseSecretBackendStaticRoleCheckDestroy,
Steps: []resource.TestStep{
{
Config: testAccDatabaseSecretBackendStaticRoleConfig_rotationSchedule(name, username, dbName, backend, connURL),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "backend", backend),
resource.TestCheckResourceAttr(resourceName, "username", username),
resource.TestCheckResourceAttr(resourceName, "db_name", dbName),
resource.TestCheckResourceAttr(resourceName, "rotation_schedule", "* * * * *"),
resource.TestCheckResourceAttr(resourceName, "rotation_window", "3600"),
),
},
{
Config: testAccDatabaseSecretBackendStaticRoleConfig_updatedRotationSchedule(name, username, dbName, backend, connURL),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(resourceName, "name", name),
resource.TestCheckResourceAttr(resourceName, "backend", backend),
resource.TestCheckResourceAttr(resourceName, "username", username),
resource.TestCheckResourceAttr(resourceName, "db_name", dbName),
resource.TestCheckResourceAttr(resourceName, "rotation_schedule", "*/30 * * * *"),
resource.TestCheckResourceAttr(resourceName, "rotation_window", "14400"),
resource.TestCheckResourceAttr(resourceName, "rotation_statements.0", "SELECT 1;"),
),
},
},
@ -117,7 +165,7 @@ func testAccDatabaseSecretBackendStaticRoleCheckDestroy(s *terraform.State) erro
return err
}
if secret != nil {
return fmt.Errorf("sttatic role %q still exists", rs.Primary.ID)
return fmt.Errorf("static role %q still exists", rs.Primary.ID)
}
}
return nil
@ -155,7 +203,65 @@ func createTestUser(connURL, username string) error {
return nil
}
func testAccDatabaseSecretBackendStaticRoleConfig_basic(name, username, db, path, connURL string) string {
func testAccDatabaseSecretBackendStaticRoleConfig_rotationSchedule(name, username, db, path, connURL string) string {
return fmt.Sprintf(`
resource "vault_mount" "db" {
path = "%s"
type = "database"
}
resource "vault_database_secret_backend_connection" "test" {
backend = vault_mount.db.path
name = "%s"
allowed_roles = ["*"]
mysql {
connection_url = "%s"
}
}
resource "vault_database_secret_backend_static_role" "test" {
backend = vault_mount.db.path
db_name = vault_database_secret_backend_connection.test.name
name = "%s"
username = "%s"
rotation_schedule = "* * * * *"
rotation_window = 3600
rotation_statements = ["ALTER USER '{{username}}'@'localhost' IDENTIFIED BY '{{password}}';"]
}
`, path, db, connURL, name, username)
}
func testAccDatabaseSecretBackendStaticRoleConfig_updatedRotationSchedule(name, username, db, path, connURL string) string {
return fmt.Sprintf(`
resource "vault_mount" "db" {
path = "%s"
type = "database"
}
resource "vault_database_secret_backend_connection" "test" {
backend = vault_mount.db.path
name = "%s"
allowed_roles = ["*"]
mysql {
connection_url = "%s"
}
}
resource "vault_database_secret_backend_static_role" "test" {
backend = vault_mount.db.path
db_name = vault_database_secret_backend_connection.test.name
name = "%s"
username = "%s"
rotation_schedule = "*/30 * * * *"
rotation_window = 14400
rotation_statements = ["SELECT 1;"]
}
`, path, db, connURL, name, username)
}
func testAccDatabaseSecretBackendStaticRoleConfig_rotationPeriod(name, username, db, path, connURL string) string {
return fmt.Sprintf(`
resource "vault_mount" "db" {
path = "%s"
@ -183,7 +289,7 @@ resource "vault_database_secret_backend_static_role" "test" {
`, path, db, connURL, name, username)
}
func testAccDatabaseSecretBackendStaticRoleConfig_updated(name, username, db, path, connURL string) string {
func testAccDatabaseSecretBackendStaticRoleConfig_updatedRotationPeriod(name, username, db, path, connURL string) string {
return fmt.Sprintf(`
resource "vault_mount" "db" {
path = "%s"

View file

@ -30,14 +30,26 @@ resource "vault_database_secret_backend_connection" "postgres" {
}
}
resource "vault_database_secret_backend_static_role" "static_role" {
# configure a static role with period-based rotations
resource "vault_database_secret_backend_static_role" "period_role" {
backend = vault_mount.db.path
name = "my-static-role"
name = "my-period-role"
db_name = vault_database_secret_backend_connection.postgres.name
username = "example"
rotation_period = "3600"
rotation_statements = ["ALTER USER \"{{name}}\" WITH PASSWORD '{{password}}';"]
}
# configure a static role with schedule-based rotations
resource "vault_database_secret_backend_static_role" "schedule_role" {
backend = vault_mount.db.path
name = "my-schedule-role"
db_name = vault_database_secret_backend_connection.postgres.name
username = "example"
rotation_schedule = "0 0 * * SAT"
rotation_window = "172800"
rotation_statements = ["ALTER USER \"{{name}}\" WITH PASSWORD '{{password}}';"]
}
```
## Argument Reference
@ -57,7 +69,17 @@ The following arguments are supported:
* `username` - (Required) The database username that this static role corresponds to.
* `rotation_period` - (Required) The amount of time Vault should wait before rotating the password, in seconds.
* `rotation_period` - The amount of time Vault should wait before rotating the password, in seconds.
Mutually exclusive with `rotation_schedule`.
* `rotation_schedule` - A cron-style string that will define the schedule on which rotations should occur.
Mutually exclusive with `rotation_period`.
**Warning**: The `rotation_period` and `rotation_schedule` fields are
mutually exclusive. One of them must be set but not both.
* `rotation_window` - (Optional) The amount of time, in seconds, in which rotations are allowed to occur starting
from a given `rotation_schedule`.
* `rotation_statements` - (Optional) Database statements to execute to rotate the password for the configured database user.