From 8cf2e1310e272b4b57efdefc189fa16d62b0e212 Mon Sep 17 00:00:00 2001 From: Jared Bunting Date: Wed, 18 Oct 2023 13:06:05 -0400 Subject: [PATCH 1/3] Add SECURITY LABEL provider to acceptance test postgres db. --- tests/build/Dockerfile | 6 ++ tests/build/dummy_seclabel/Makefile | 13 ++++ .../dummy_seclabel/dummy_seclabel--1.0.sql | 8 +++ tests/build/dummy_seclabel/dummy_seclabel.c | 60 +++++++++++++++++++ .../dummy_seclabel/dummy_seclabel.control | 4 ++ tests/docker-compose.yml | 5 +- 6 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 tests/build/Dockerfile create mode 100644 tests/build/dummy_seclabel/Makefile create mode 100644 tests/build/dummy_seclabel/dummy_seclabel--1.0.sql create mode 100644 tests/build/dummy_seclabel/dummy_seclabel.c create mode 100644 tests/build/dummy_seclabel/dummy_seclabel.control diff --git a/tests/build/Dockerfile b/tests/build/Dockerfile new file mode 100644 index 00000000..e132f836 --- /dev/null +++ b/tests/build/Dockerfile @@ -0,0 +1,6 @@ +FROM postgres:${PGVERSION:-latest} + +RUN apt-get update && apt-get install -y build-essential postgresql-server-dev-all +COPY dummy_seclabel /opt/dummy_seclabel +WORKDIR /opt/dummy_seclabel +RUN make diff --git a/tests/build/dummy_seclabel/Makefile b/tests/build/dummy_seclabel/Makefile new file mode 100644 index 00000000..3447a688 --- /dev/null +++ b/tests/build/dummy_seclabel/Makefile @@ -0,0 +1,13 @@ +# src/test/modules/dummy_seclabel/Makefile + +MODULES = dummy_seclabel +PGFILEDESC = "dummy_seclabel - regression testing of the SECURITY LABEL statement" + +EXTENSION = dummy_seclabel +DATA = dummy_seclabel--1.0.sql + +REGRESS = dummy_seclabel + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) diff --git a/tests/build/dummy_seclabel/dummy_seclabel--1.0.sql b/tests/build/dummy_seclabel/dummy_seclabel--1.0.sql new file mode 100644 index 00000000..5939e930 --- /dev/null +++ b/tests/build/dummy_seclabel/dummy_seclabel--1.0.sql @@ -0,0 +1,8 @@ +/* src/test/modules/dummy_seclabel/dummy_seclabel--1.0.sql */ + +-- complain if script is sourced in psql, rather than via CREATE EXTENSION +\echo Use "CREATE EXTENSION dummy_seclabel" to load this file. \quit + +CREATE FUNCTION dummy_seclabel_dummy() + RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/tests/build/dummy_seclabel/dummy_seclabel.c b/tests/build/dummy_seclabel/dummy_seclabel.c new file mode 100644 index 00000000..fea8d679 --- /dev/null +++ b/tests/build/dummy_seclabel/dummy_seclabel.c @@ -0,0 +1,60 @@ +/* + * dummy_seclabel.c + * + * Dummy security label provider. + * + * This module does not provide anything worthwhile from a security + * perspective, but allows regression testing independent of platform-specific + * features like SELinux. + * + * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + */ +#include "postgres.h" + +#include "commands/seclabel.h" +#include "fmgr.h" +#include "miscadmin.h" +#include "utils/rel.h" + +PG_MODULE_MAGIC; + +PG_FUNCTION_INFO_V1(dummy_seclabel_dummy); + +static void +dummy_object_relabel(const ObjectAddress *object, const char *seclabel) +{ + if (seclabel == NULL || + strcmp(seclabel, "unclassified") == 0 || + strcmp(seclabel, "classified") == 0) + return; + + if (strcmp(seclabel, "secret") == 0 || + strcmp(seclabel, "top secret") == 0) + { + if (!superuser()) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("only superuser can set '%s' label", seclabel))); + return; + } + ereport(ERROR, + (errcode(ERRCODE_INVALID_NAME), + errmsg("'%s' is not a valid security label", seclabel))); +} + +void +_PG_init(void) +{ + register_label_provider("dummy", dummy_object_relabel); +} + +/* + * This function is here just so that the extension is not completely empty + * and the dynamic library is loaded when CREATE EXTENSION runs. + */ +Datum +dummy_seclabel_dummy(PG_FUNCTION_ARGS) +{ + PG_RETURN_VOID(); +} diff --git a/tests/build/dummy_seclabel/dummy_seclabel.control b/tests/build/dummy_seclabel/dummy_seclabel.control new file mode 100644 index 00000000..8c372728 --- /dev/null +++ b/tests/build/dummy_seclabel/dummy_seclabel.control @@ -0,0 +1,4 @@ +comment = 'Test code for SECURITY LABEL feature' +default_version = '1.0' +module_pathname = '$libdir/dummy_seclabel' +relocatable = true diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 177994bf..6c212946 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -2,7 +2,8 @@ version: "3" services: postgres: - image: postgres:${PGVERSION:-latest} + build: build +# image: postgres:${PGVERSION:-latest} user: postgres command: - "postgres" @@ -10,6 +11,8 @@ services: - "wal_level=logical" - "-c" - "max_replication_slots=10" + - "-c" + - "shared_preload_libraries=/opt/dummy_seclabel/dummy_seclabel" environment: POSTGRES_PASSWORD: ${PGPASSWORD} ports: From 9f98aa091849b8d46f435d8e8bbf098afdf5ed99 Mon Sep 17 00:00:00 2001 From: Jared Bunting Date: Wed, 18 Oct 2023 13:06:23 -0400 Subject: [PATCH 2/3] Add security label resource --- postgresql/config.go | 3 + postgresql/provider.go | 1 + .../resource_postgresql_security_label.go | 182 +++++++++++++++ ...resource_postgresql_security_label_test.go | 211 ++++++++++++++++++ 4 files changed, 397 insertions(+) create mode 100644 postgresql/resource_postgresql_security_label.go create mode 100644 postgresql/resource_postgresql_security_label_test.go 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" +} +` From 63519b7aa4c4f5e1ab5b9963054f080f84023cb1 Mon Sep 17 00:00:00 2001 From: Piet Schrijver Date: Thu, 18 Apr 2024 11:36:32 +0200 Subject: [PATCH 3/3] Add documentation for the security label resource --- .../r/postgresql_security_label.html.markdown | 38 +++++++++++++++++++ website/postgresql.erb | 3 ++ 2 files changed, 41 insertions(+) create mode 100644 website/docs/r/postgresql_security_label.html.markdown diff --git a/website/docs/r/postgresql_security_label.html.markdown b/website/docs/r/postgresql_security_label.html.markdown new file mode 100644 index 00000000..36515723 --- /dev/null +++ b/website/docs/r/postgresql_security_label.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "postgresql" +page_title: "PostgreSQL: postgresql_grant" +sidebar_current: "docs-postgresql-resource-postgresql_grant" +description: |- + Creates and manages privileges given to a user for a database schema. +--- + +# postgresql\_security\_label + +The ``postgresql_security_label`` resource creates and manages security labels. + +See [PostgreSQL documentation](https://www.postgresql.org/docs/current/sql-security-label.html) + +~> **Note:** This resource needs Postgresql version 11 or above. + +## Usage + +```hcl +resource "postgresql_role" "my_role" { + name = "my_role" + login = true +} + +resource "postgresql_security_label" "workload" { + object_type = "role" + object_name = postgresql_role.my_role.name + label_provider = "pgaadauth" + label = "aadauth,oid=00000000-0000-0000-0000-000000000000,type=service" +} +``` + +## Argument Reference + +* `object_type` - (Required) The PostgreSQL object type to apply this security label to. +* `object_name` - (Required) The name of the object to be labeled. Names of objects that reside in schemas (tables, functions, etc.) can be schema-qualified. +* `label_provider` - (Required) The name of the provider with which this label is to be associated. +* `label` - (Required) The value of the security label. diff --git a/website/postgresql.erb b/website/postgresql.erb index 5b525a9e..b48358f2 100644 --- a/website/postgresql.erb +++ b/website/postgresql.erb @@ -52,6 +52,9 @@ > postgresql_user_mapping + > + postgresql_security_label +