diff --git a/postgresql/config.go b/postgresql/config.go index c4ed30e8..57205cd4 100644 --- a/postgresql/config.go +++ b/postgresql/config.go @@ -41,6 +41,7 @@ const ( featurePubWithoutTruncate featureFunction featureServer + featureSecurityLabel ) var ( @@ -112,6 +113,8 @@ var ( featureServer: semver.MustParseRange(">=10.0.0"), featureDatabaseOwnerRole: semver.MustParseRange(">=15.0.0"), + + featureSecurityLabel: semver.MustParseRange(">=11.0.0"), } ) diff --git a/postgresql/provider.go b/postgresql/provider.go index 7f15c92e..eeda3ce1 100644 --- a/postgresql/provider.go +++ b/postgresql/provider.go @@ -199,6 +199,7 @@ func Provider() *schema.Provider { "postgresql_function": resourcePostgreSQLFunction(), "postgresql_server": resourcePostgreSQLServer(), "postgresql_user_mapping": resourcePostgreSQLUserMapping(), + "postgresql_security_label": resourcePostgreSQLSecurityLabel(), }, DataSourcesMap: map[string]*schema.Resource{ diff --git a/postgresql/resource_postgresql_security_label.go b/postgresql/resource_postgresql_security_label.go new file mode 100644 index 00000000..a9a48022 --- /dev/null +++ b/postgresql/resource_postgresql_security_label.go @@ -0,0 +1,182 @@ +package postgresql + +import ( + "bytes" + "database/sql" + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/lib/pq" +) + +const ( + securityLabelObjectNameAttr = "object_name" + securityLabelObjectTypeAttr = "object_type" + securityLabelProviderAttr = "label_provider" + securityLabelLabelAttr = "label" +) + +func resourcePostgreSQLSecurityLabel() *schema.Resource { + return &schema.Resource{ + Create: PGResourceFunc(resourcePostgreSQLSecurityLabelCreate), + Read: PGResourceFunc(resourcePostgreSQLSecurityLabelRead), + Update: PGResourceFunc(resourcePostgreSQLSecurityLabelUpdate), + Delete: PGResourceFunc(resourcePostgreSQLSecurityLabelDelete), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + securityLabelObjectNameAttr: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the existing object to apply the security label to", + }, + securityLabelObjectTypeAttr: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The type of the existing object to apply the security label to", + }, + securityLabelProviderAttr: { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The provider to apply the security label for", + }, + securityLabelLabelAttr: { + Type: schema.TypeString, + Required: true, + ForceNew: false, + Description: "The label to be applied", + }, + }, + } +} + +func resourcePostgreSQLSecurityLabelCreate(db *DBConnection, d *schema.ResourceData) error { + if !db.featureSupported(featureSecurityLabel) { + return fmt.Errorf( + "Security Label is not supported for this Postgres version (%s)", + db.version, + ) + } + log.Printf("[WARN] PostgreSQL security label Create") + label := d.Get(securityLabelLabelAttr).(string) + if err := resourcePostgreSQLSecurityLabelUpdateImpl(db, d, pq.QuoteLiteral(label)); err != nil { + return err + } + + d.SetId(generateSecurityLabelID(d)) + + return resourcePostgreSQLSecurityLabelReadImpl(db, d) +} + +func resourcePostgreSQLSecurityLabelUpdateImpl(db *DBConnection, d *schema.ResourceData, label string) error { + b := bytes.NewBufferString("SECURITY LABEL ") + + objectType := d.Get(securityLabelObjectTypeAttr).(string) + objectName := d.Get(securityLabelObjectNameAttr).(string) + provider := d.Get(securityLabelProviderAttr).(string) + fmt.Fprint(b, " FOR ", pq.QuoteIdentifier(provider)) + fmt.Fprint(b, " ON ", objectType, pq.QuoteIdentifier(objectName)) + fmt.Fprint(b, " IS ", label) + + if _, err := db.Exec(b.String()); err != nil { + log.Printf("[WARN] PostgreSQL security label Create failed %s", err) + return err + } + return nil +} + +func resourcePostgreSQLSecurityLabelRead(db *DBConnection, d *schema.ResourceData) error { + if !db.featureSupported(featureSecurityLabel) { + return fmt.Errorf( + "Security Label is not supported for this Postgres version (%s)", + db.version, + ) + } + log.Printf("[WARN] PostgreSQL security label Read") + + return resourcePostgreSQLSecurityLabelReadImpl(db, d) +} + +func resourcePostgreSQLSecurityLabelReadImpl(db *DBConnection, d *schema.ResourceData) error { + objectType := d.Get(securityLabelObjectTypeAttr).(string) + objectName := d.Get(securityLabelObjectNameAttr).(string) + provider := d.Get(securityLabelProviderAttr).(string) + + txn, err := startTransaction(db.client, "") + if err != nil { + return err + } + defer deferredRollback(txn) + + query := "SELECT objtype, objname, provider, label FROM pg_seclabels WHERE objtype = $1 and objname = $2 and provider = $3" + row := db.QueryRow(query, objectType, objectName, provider) + + var label string + err = row.Scan(&objectType, &objectName, &provider, &label) + switch { + case err == sql.ErrNoRows: + log.Printf("[WARN] PostgreSQL security label for (%s '%s') with provider %s not found", objectType, objectName, provider) + d.SetId("") + return nil + case err != nil: + return fmt.Errorf("Error reading security label: %w", err) + } + + d.Set(securityLabelObjectTypeAttr, objectType) + d.Set(securityLabelObjectNameAttr, objectName) + d.Set(securityLabelProviderAttr, provider) + d.Set(securityLabelLabelAttr, label) + d.SetId(generateSecurityLabelID(d)) + + return nil +} + +func resourcePostgreSQLSecurityLabelDelete(db *DBConnection, d *schema.ResourceData) error { + if !db.featureSupported(featureSecurityLabel) { + return fmt.Errorf( + "Security Label is not supported for this Postgres version (%s)", + db.version, + ) + } + log.Printf("[WARN] PostgreSQL security label Delete") + + if err := resourcePostgreSQLSecurityLabelUpdateImpl(db, d, "NULL"); err != nil { + return err + } + + d.SetId("") + + return nil +} + +func resourcePostgreSQLSecurityLabelUpdate(db *DBConnection, d *schema.ResourceData) error { + if !db.featureSupported(featureServer) { + return fmt.Errorf( + "Security Label is not supported for this Postgres version (%s)", + db.version, + ) + } + log.Printf("[WARN] PostgreSQL security label Update") + + label := d.Get(securityLabelLabelAttr).(string) + if err := resourcePostgreSQLSecurityLabelUpdateImpl(db, d, pq.QuoteLiteral(label)); err != nil { + return err + } + + return resourcePostgreSQLSecurityLabelReadImpl(db, d) +} + +func generateSecurityLabelID(d *schema.ResourceData) string { + return strings.Join([]string{ + d.Get(securityLabelProviderAttr).(string), + d.Get(securityLabelObjectTypeAttr).(string), + d.Get(securityLabelObjectNameAttr).(string), + }, ".") +} diff --git a/postgresql/resource_postgresql_security_label_test.go b/postgresql/resource_postgresql_security_label_test.go new file mode 100644 index 00000000..8b7872aa --- /dev/null +++ b/postgresql/resource_postgresql_security_label_test.go @@ -0,0 +1,211 @@ +package postgresql + +import ( + "database/sql" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestAccPostgresqlSecurityLabel_Basic(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testCheckCompatibleVersion(t, featureSecurityLabel) + testSuperuserPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPostgresqlSecurityLabelDestroy, + Steps: []resource.TestStep{ + { + Config: testAccPostgresqlSecurityLabelConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlSecurityLabelExists("postgresql_security_label.test_label"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "object_type", "role"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "object_name", "security_label_test_role"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "label_provider", "dummy"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "label", "secret"), + ), + }, + }, + }) +} + +func TestAccPostgresqlSecurityLabel_Update(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testCheckCompatibleVersion(t, featureSecurityLabel) + testSuperuserPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPostgresqlSecurityLabelDestroy, + Steps: []resource.TestStep{ + { + Config: testAccPostgresqlSecurityLabelConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlSecurityLabelExists("postgresql_security_label.test_label"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "object_type", "role"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "object_name", "security_label_test_role"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "label_provider", "dummy"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "label", "secret"), + ), + }, + { + Config: testAccPostgresqlSecurityLabelChanges2, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlSecurityLabelExists("postgresql_security_label.test_label"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "label", "top secret"), + ), + }, + { + Config: testAccPostgresqlSecurityLabelChanges3, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlSecurityLabelExists("postgresql_security_label.test_label"), + resource.TestCheckResourceAttr( + "postgresql_security_label.test_label", "object_name", "security_label_test_role2"), + ), + }, + }, + }) +} + +func checkSecurityLabelExists(txn *sql.Tx, objectType string, objectName string, provider string) (bool, error) { + var _rez bool + err := txn.QueryRow("SELECT TRUE FROM pg_seclabels WHERE objtype = $1 AND objname = $2 AND provider = $3", objectType, objectName, provider).Scan(&_rez) + switch { + case err == sql.ErrNoRows: + return false, nil + case err != nil: + return false, fmt.Errorf("Error reading info about security label: %s", err) + } + + return true, nil +} + +func testAccCheckPostgresqlSecurityLabelDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*Client) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "postgresql_security_label" { + continue + } + + txn, err := startTransaction(client, "") + if err != nil { + return err + } + defer deferredRollback(txn) + + splitted := strings.Split(rs.Primary.ID, ".") + exists, err := checkSecurityLabelExists(txn, splitted[1], splitted[2], splitted[0]) + + if err != nil { + return fmt.Errorf("Error checking security label%s", err) + } + + if exists { + return fmt.Errorf("Security label still exists after destroy") + } + } + + return nil +} + +func testAccCheckPostgresqlSecurityLabelExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Resource not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + + objectType, ok := rs.Primary.Attributes[securityLabelObjectTypeAttr] + if !ok { + return fmt.Errorf("No Attribute for object type is set") + } + + objectName, ok := rs.Primary.Attributes[securityLabelObjectNameAttr] + if !ok { + return fmt.Errorf("No Attribute for object name is set") + } + + provider, ok := rs.Primary.Attributes[securityLabelProviderAttr] + + client := testAccProvider.Meta().(*Client) + txn, err := startTransaction(client, "") + if err != nil { + return err + } + defer deferredRollback(txn) + + exists, err := checkSecurityLabelExists(txn, objectType, objectName, provider) + + if err != nil { + return fmt.Errorf("Error checking security label%s", err) + } + + if !exists { + return fmt.Errorf("Security label not found") + } + + return nil + } +} + +var testAccPostgresqlSecurityLabelConfig = ` +resource "postgresql_role" "test_role" { + name = "security_label_test_role" + login = true + create_database = true +} +resource "postgresql_security_label" "test_label" { + object_type = "role" + object_name = postgresql_role.test_role.name + label_provider = "dummy" + label = "secret" +} +` + +var testAccPostgresqlSecurityLabelChanges2 = ` +resource "postgresql_role" "test_role" { + name = "security_label_test_role" + login = true + create_database = true +} +resource "postgresql_security_label" "test_label" { + object_type = "role" + object_name = postgresql_role.test_role.name + label_provider = "dummy" + label = "top secret" +} +` + +var testAccPostgresqlSecurityLabelChanges3 = ` +resource "postgresql_role" "test_role" { + name = "security_label_test_role2" + login = true + create_database = true +} +resource "postgresql_security_label" "test_label" { + object_type = "role" + object_name = postgresql_role.test_role.name + label_provider = "dummy" + label = "top secret" +} +`