diff --git a/.github/workflows/golangci.yml b/.github/workflows/golangci.yml index b52b2499..c35ca4ca 100644 --- a/.github/workflows/golangci.yml +++ b/.github/workflows/golangci.yml @@ -10,16 +10,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3.0.1 + uses: actions/checkout@v4 - name: Setup Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: 1.20.x + go-version: 1.23.x - run: go mod vendor - name: Run golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - version: v1.53 + version: v1.61 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07539634..1d98f821 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,28 +20,28 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Unshallow run: git fetch --prune --unshallow - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: '1.23' - name: Import GPG key id: import_gpg - uses: paultyng/ghaction-import-gpg@v2.1.0 - env: - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - PASSPHRASE: ${{ secrets.PASSPHRASE }} + uses: crazy-max/ghaction-import-gpg@v6.1.0 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.PASSPHRASE }} - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 + uses: goreleaser/goreleaser-action@v6 with: version: latest - args: release --rm-dist + args: release --clean env: GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d1b63bc..b0cf3441 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,19 +12,19 @@ jobs: strategy: matrix: - pgversion: [15, 14, 13, 12, 11] + pgversion: [16, 15, 14, 13, 12] env: PGVERSION: ${{ matrix.pgversion }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: '1.23' - name: test run: make test diff --git a/.goreleaser.yml b/.goreleaser.yml index 34fef1d2..8594bb3e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,5 +1,6 @@ # Visit https://goreleaser.com for documentation on how to customize this # behavior. +version: 2 before: hooks: # this is just an example and not a requirement for provider building/publishing @@ -47,8 +48,8 @@ signs: - "${signature}" - "--detach-sign" - "${artifact}" -release: - # Visit your project's GitHub Releases page to publish this release. - #draft: true +release: {} + # If you want to manually examine the release before its live, uncomment this line: + # draft: true changelog: - skip: true + disable: true diff --git a/go.mod b/go.mod index 5d4290d5..eebe8c4a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/terraform-providers/terraform-provider-postgresql -go 1.20 +go 1.23 + +toolchain go1.23.2 require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1 diff --git a/go.sum b/go.sum index 58a1700c..caf9f3c1 100644 --- a/go.sum +++ b/go.sum @@ -768,6 +768,7 @@ github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkE github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.44.314 h1:d/5Jyk/Fb+PBd/4nzQg0JuC2W4A0knrDIzBgK/ggAow= +github.com/aws/aws-sdk-go v1.44.314/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v1.18.1/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2 v1.20.0 h1:INUDpYLt4oiPOJl0XwZDK2OVAVf0Rzo+MGVTv9f+gy8= github.com/aws/aws-sdk-go-v2 v1.20.0/go.mod h1:uWOr0m0jDsiWw8nnXiqZ+YG6LdvAlGYDLLf2NmHZoy4= @@ -894,6 +895,7 @@ github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -964,7 +966,9 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-pkcs11 v0.2.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY= github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE= +github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk= github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk= +github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -1136,6 +1140,7 @@ github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= @@ -1157,6 +1162,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -1243,6 +1249,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= @@ -1253,6 +1260,7 @@ github.com/sean-/postgresql-acl v0.0.0-20161225120419-d10489e5d217 h1:zBCJK2lWcz github.com/sean-/postgresql-acl v0.0.0-20161225120419-d10489e5d217/go.mod h1:9onFewqXpqEn8CNOdWUVX4aUSxsnkRfuXXbXS4JqcGY= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -1540,6 +1548,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1972,6 +1981,7 @@ google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mR google.golang.org/genproto v0.0.0-20230629202037-9506855d4529/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= google.golang.org/genproto v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:O9kGHb51iE/nOGvQaDUuadVYqovW56s5emA88lQnj6Y= google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf h1:v5Cf4E9+6tawYrs/grq1q1hFpGtzlGFzgWHqwt6NFiU= +google.golang.org/genproto v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234020-1aefcd67740a/go.mod h1:ts19tUU+Z0ZShN1y3aPyq2+O3d5FUNNgT6FtOzmrNn8= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230526203410-71b5a4ffd15e/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= @@ -1979,6 +1989,7 @@ google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go. google.golang.org/genproto/googleapis/api v0.0.0-20230629202037-9506855d4529/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/genproto/googleapis/api v0.0.0-20230706204954-ccb25ca9f130/go.mod h1:mPBs5jNgx2GuQGvFwUvVKqtn6HsUw9nP64BedgvqEsQ= google.golang.org/genproto/googleapis/api v0.0.0-20230731193218-e0aa005b6bdf h1:xkVZ5FdZJF4U82Q/JS+DcZA83s/GRVL+QrFMlexk9Yo= +google.golang.org/genproto/googleapis/api v0.0.0-20230731193218-e0aa005b6bdf/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:ylj+BE99M198VPbBh6A8d9n3w8fChvyLK3wwBOjXBFA= google.golang.org/genproto/googleapis/bytestream v0.0.0-20230711160842-782d3b101e98/go.mod h1:3QoBVwTHkXbY1oRGzlhwhOykfcATQN43LJ6iT8Wy8kE= google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234015-3fc162c6f38a/go.mod h1:xURIpW9ES5+/GZhnV6beoEtxQrnkRGIfP5VQG2tCBLc= diff --git a/postgresql/config.go b/postgresql/config.go index 17668c0e..fd9cbfad 100644 --- a/postgresql/config.go +++ b/postgresql/config.go @@ -44,6 +44,8 @@ const ( featurePubWithoutTruncate featureFunction featureServer + featureCreateRoleSelfGrant + featureSecurityLabel ) var ( @@ -115,6 +117,11 @@ var ( featureServer: semver.MustParseRange(">=10.0.0"), featureDatabaseOwnerRole: semver.MustParseRange(">=15.0.0"), + + // New privileges rules in version 16 + // https://www.postgresql.org/docs/16/release-16.html#RELEASE-16-PRIVILEGES + featureCreateRoleSelfGrant: semver.MustParseRange(">=16.0.0"), + featureSecurityLabel: semver.MustParseRange(">=11.0.0"), } ) diff --git a/postgresql/helpers.go b/postgresql/helpers.go index a5778fc1..5349d4e8 100644 --- a/postgresql/helpers.go +++ b/postgresql/helpers.go @@ -57,11 +57,25 @@ func pqQuoteLiteral(in string) string { func isMemberOfRole(db QueryAble, role, member string) (bool, error) { var _rez int + setOption := true + err := db.QueryRow( - "SELECT 1 FROM pg_auth_members WHERE pg_get_userbyid(roleid) = $1 AND pg_get_userbyid(member) = $2", - role, member, + "SELECT 1 FROM information_schema.columns WHERE table_name='pg_auth_members' AND column_name = 'set_option'", ).Scan(&_rez) + switch { + case err == sql.ErrNoRows: + setOption = false + case err != nil: + return false, fmt.Errorf("could not read setOption column: %w", err) + } + + query := "SELECT 1 FROM pg_auth_members WHERE pg_get_userbyid(roleid) = $1 AND pg_get_userbyid(member) = $2" + if setOption { + query += " AND set_option" + } + + err = db.QueryRow(query, role, member).Scan(&_rez) switch { case err == sql.ErrNoRows: return false, nil @@ -268,6 +282,30 @@ func validatePrivileges(d *schema.ResourceData) error { return nil } +func resourcePrivilegesEqual(granted *schema.Set, d *schema.ResourceData) bool { + objectType := d.Get("object_type").(string) + wanted := d.Get("privileges").(*schema.Set) + + if granted.Equal(wanted) { + return true + } + + if !wanted.Contains("ALL") { + return false + } + + // implicit check: e.g. for object_type schema -> ALL == ["CREATE", "USAGE"] + log.Printf("The wanted privilege is 'ALL'. therefore, we will check if the current privileges are ALL implicitely") + implicits := []interface{}{} + for _, p := range allowedPrivileges[objectType] { + if p != "ALL" { + implicits = append(implicits, p) + } + } + wantedSet := schema.NewSet(schema.HashString, implicits) + return granted.Equal(wantedSet) +} + func pgArrayToSet(arr pq.ByteaArray) *schema.Set { s := make([]interface{}, len(arr)) for i, v := range arr { diff --git a/postgresql/helpers_test.go b/postgresql/helpers_test.go index 6b60d806..11538618 100644 --- a/postgresql/helpers_test.go +++ b/postgresql/helpers_test.go @@ -1,6 +1,7 @@ package postgresql import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "testing" "github.com/stretchr/testify/assert" @@ -45,3 +46,78 @@ func TestQuoteTableName(t *testing.T) { }) } } + +func TestArePrivilegesEqual(t *testing.T) { + + type PrivilegesTestObject struct { + d *schema.ResourceData + granted *schema.Set + wanted *schema.Set + assertion bool + } + + tt := []PrivilegesTestObject{ + { + buildResourceData("database", t), + buildPrivilegesSet("CONNECT", "CREATE", "TEMPORARY"), + buildPrivilegesSet("ALL"), + true, + }, + { + buildResourceData("database", t), + buildPrivilegesSet("CREATE", "USAGE"), + buildPrivilegesSet("USAGE"), + false, + }, + { + buildResourceData("table", t), + buildPrivilegesSet("SELECT", "INSERT", "UPDATE", "DELETE", "TRUNCATE", "REFERENCES", "TRIGGER"), + buildPrivilegesSet("ALL"), + true, + }, + { + buildResourceData("table", t), + buildPrivilegesSet("SELECT"), + buildPrivilegesSet("SELECT, INSERT"), + false, + }, + { + buildResourceData("schema", t), + buildPrivilegesSet("CREATE", "USAGE"), + buildPrivilegesSet("ALL"), + true, + }, + { + buildResourceData("schema", t), + buildPrivilegesSet("CREATE"), + buildPrivilegesSet("ALL"), + false, + }, + } + + for _, configuration := range tt { + err := configuration.d.Set("privileges", configuration.wanted) + assert.NoError(t, err) + equal := resourcePrivilegesEqual(configuration.granted, configuration.d) + assert.Equal(t, configuration.assertion, equal) + } +} + +func buildPrivilegesSet(grants ...interface{}) *schema.Set { + return schema.NewSet(schema.HashString, grants) +} + +func buildResourceData(objectType string, t *testing.T) *schema.ResourceData { + var testSchema = map[string]*schema.Schema{ + "object_type": {Type: schema.TypeString}, + "privileges": { + Type: schema.TypeSet, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + } + + m := make(map[string]any) + m["object_type"] = objectType + return schema.TestResourceDataRaw(t, testSchema, m) +} diff --git a/postgresql/provider.go b/postgresql/provider.go index d420757a..5f3e368f 100644 --- a/postgresql/provider.go +++ b/postgresql/provider.go @@ -3,6 +3,8 @@ package postgresql import ( "context" "fmt" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/sts" "os" "github.com/Azure/azure-sdk-for-go/sdk/azcore" @@ -104,6 +106,13 @@ func Provider() *schema.Provider { Description: "AWS region to use for IAM auth", }, + "aws_rds_iam_provider_role_arn": { + Type: schema.TypeString, + Optional: true, + Default: "", + Description: "AWS IAM role to assume for IAM auth", + }, + "azure_identity_auth": { Type: schema.TypeBool, Optional: true, @@ -228,6 +237,7 @@ func Provider() *schema.Provider { "postgresql_function": resourcePostgreSQLFunction(), "postgresql_server": resourcePostgreSQLServer(), "postgresql_user_mapping": resourcePostgreSQLUserMapping(), + "postgresql_security_label": resourcePostgreSQLSecurityLabel(), }, DataSourcesMap: map[string]*schema.Resource{ @@ -247,7 +257,7 @@ func validateExpectedVersion(v interface{}, key string) (warnings []string, erro return } -func getRDSAuthToken(region string, profile string, username string, host string, port int) (string, error) { +func getRDSAuthToken(region string, profile string, role string, username string, host string, port int) (string, error) { endpoint := fmt.Sprintf("%s:%d", host, port) ctx := context.Background() @@ -266,6 +276,32 @@ func getRDSAuthToken(region string, profile string, username string, host string return "", err } + if role != "" { + stsClient := sts.NewFromConfig(awscfg) + roleInput := &sts.AssumeRoleInput{ + RoleArn: aws.String(role), + RoleSessionName: aws.String("TerraformPostgresqlProvider"), + } + + roleOutput, err := stsClient.AssumeRole(ctx, roleInput) + if err != nil { + return "", fmt.Errorf("could not assume AWS role: %w", err) + } + + awscfg, err = awsConfig.LoadDefaultConfig(ctx, + awsConfig.WithCredentialsProvider( + aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider( + *roleOutput.Credentials.AccessKeyId, + *roleOutput.Credentials.SecretAccessKey, + *roleOutput.Credentials.SessionToken, + )), + ), + ) + if err != nil { + return "", fmt.Errorf("could not load AWS default config: %w", err) + } + } + token, err := auth.BuildAuthToken(ctx, endpoint, awscfg.Region, username, awscfg.Credentials) return token, err @@ -343,8 +379,9 @@ func providerConfigure(d *schema.ResourceData) (interface{}, error) { if d.Get("aws_rds_iam_auth").(bool) { profile := d.Get("aws_rds_iam_profile").(string) region := d.Get("aws_rds_iam_region").(string) + role := d.Get("aws_rds_iam_provider_role_arn").(string) var err error - password, err = getRDSAuthToken(region, profile, username, host, port) + password, err = getRDSAuthToken(region, profile, role, username, host, port) if err != nil { return nil, err } diff --git a/postgresql/resource_postgresql_database.go b/postgresql/resource_postgresql_database.go index 9cc9da55..6cabadf7 100644 --- a/postgresql/resource_postgresql_database.go +++ b/postgresql/resource_postgresql_database.go @@ -14,16 +14,17 @@ import ( ) const ( - dbAllowConnsAttr = "allow_connections" - dbCTypeAttr = "lc_ctype" - dbCollationAttr = "lc_collate" - dbConnLimitAttr = "connection_limit" - dbEncodingAttr = "encoding" - dbIsTemplateAttr = "is_template" - dbNameAttr = "name" - dbOwnerAttr = "owner" - dbTablespaceAttr = "tablespace_name" - dbTemplateAttr = "template" + dbAllowConnsAttr = "allow_connections" + dbCTypeAttr = "lc_ctype" + dbCollationAttr = "lc_collate" + dbConnLimitAttr = "connection_limit" + dbEncodingAttr = "encoding" + dbIsTemplateAttr = "is_template" + dbNameAttr = "name" + dbOwnerAttr = "owner" + dbTablespaceAttr = "tablespace_name" + dbTemplateAttr = "template" + dbAlterObjectOwnership = "alter_object_ownership" ) func resourcePostgreSQLDatabase() *schema.Resource { @@ -102,6 +103,12 @@ func resourcePostgreSQLDatabase() *schema.Resource { Computed: true, Description: "If true, then this database can be cloned by any user with CREATEDB privileges", }, + dbAlterObjectOwnership: { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If true, the owner of already existing objects will change if the owner changes", + }, }, } } @@ -393,6 +400,10 @@ func resourcePostgreSQLDatabaseUpdate(db *DBConnection, d *schema.ResourceData) return err } + if err := setAlterOwnership(db, d); err != nil { + return err + } + if err := setDBOwner(db, d); err != nil { return err } @@ -468,6 +479,7 @@ func setDBOwner(db *DBConnection, d *schema.ResourceData) error { } dbName := d.Get(dbNameAttr).(string) + sql := fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(owner)) if _, err := db.Exec(sql); err != nil { return fmt.Errorf("Error updating database OWNER: %w", err) @@ -476,6 +488,60 @@ func setDBOwner(db *DBConnection, d *schema.ResourceData) error { return err } +func setAlterOwnership(db *DBConnection, d *schema.ResourceData) error { + if !d.HasChange(dbOwnerAttr) && !d.HasChange(dbAlterObjectOwnership) { + return nil + } + owner := d.Get(dbOwnerAttr).(string) + if owner == "" { + return nil + } + + alterOwnership := d.Get(dbAlterObjectOwnership).(bool) + if !alterOwnership { + return nil + } + currentUser := db.client.config.getDatabaseUsername() + + dbName := d.Get(dbNameAttr).(string) + + lockTxn, err := startTransaction(db.client, dbName) + if err := pgLockRole(lockTxn, currentUser); err != nil { + return err + } + defer deferredRollback(lockTxn) + + currentOwner, err := getDatabaseOwner(db, dbName) + if err != nil { + return fmt.Errorf("Error getting current database OWNER: %w", err) + } + + newOwner := d.Get(dbOwnerAttr).(string) + + if currentOwner == newOwner { + return nil + } + + currentOwnerGranted, err := grantRoleMembership(db, currentOwner, currentUser) + if err != nil { + return err + } + if currentOwnerGranted { + defer func() { + _, err = revokeRoleMembership(db, currentOwner, currentUser) + }() + } + sql := fmt.Sprintf("REASSIGN OWNED BY %s TO %s", pq.QuoteIdentifier(currentOwner), pq.QuoteIdentifier(newOwner)) + if _, err := lockTxn.Exec(sql); err != nil { + return fmt.Errorf("Error reassigning objects owned by '%s': %w", currentOwner, err) + } + + if err := lockTxn.Commit(); err != nil { + return fmt.Errorf("error committing reassign: %w", err) + } + return nil +} + func setDBTablespace(db QueryAble, d *schema.ResourceData) error { if !d.HasChange(dbTablespaceAttr) { return nil diff --git a/postgresql/resource_postgresql_database_test.go b/postgresql/resource_postgresql_database_test.go index bf8d255f..f49ae1aa 100644 --- a/postgresql/resource_postgresql_database_test.go +++ b/postgresql/resource_postgresql_database_test.go @@ -43,6 +43,8 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) { "postgresql_database.default_opts", "connection_limit", "-1"), resource.TestCheckResourceAttr( "postgresql_database.default_opts", "is_template", "false"), + resource.TestCheckResourceAttr( + "postgresql_database.default_opts", "alter_object_ownership", "false"), resource.TestCheckResourceAttr( "postgresql_database.modified_opts", "owner", "myrole"), @@ -62,6 +64,8 @@ func TestAccPostgresqlDatabase_Basic(t *testing.T) { "postgresql_database.modified_opts", "connection_limit", "10"), resource.TestCheckResourceAttr( "postgresql_database.modified_opts", "is_template", "true"), + resource.TestCheckResourceAttr( + "postgresql_database.modified_opts", "alter_object_ownership", "true"), resource.TestCheckResourceAttr( "postgresql_database.pathological_opts", "owner", "myrole"), @@ -266,21 +270,94 @@ resource postgresql_database "test_db" { }) } +// Test the case where the owned objects by the previous database owner are altered. +func TestAccPostgresqlDatabase_AlterObjectOwnership(t *testing.T) { + skipIfNotAcc(t) + + const ( + databaseSuffix = "ownership" + tableName = "testtable1" + previous_owner = "previous_owner" + new_owner = "new_owner" + ) + + databaseName := fmt.Sprintf("%s_%s", dbNamePrefix, databaseSuffix) + + config := getTestConfig(t) + dsn := config.connStr("postgres") + + for _, role := range []string{previous_owner, new_owner} { + dbExecute( + t, dsn, + fmt.Sprintf("CREATE ROLE %s;", role), + ) + defer func(role string) { + dbExecute(t, dsn, fmt.Sprintf("DROP ROLE %s", role)) + }(role) + + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testSuperuserPreCheck(t) + }, + Providers: testAccProviders, + CheckDestroy: testAccCheckPostgresqlDatabaseDestroy, + Steps: []resource.TestStep{ + { + Config: ` +resource postgresql_database "test_db" { + name = "tf_tests_db_ownership" + owner = "previous_owner" + alter_object_ownership = true +} +`, + Check: func(*terraform.State) error { + // To test default privileges, we need to create a table + // after having apply the state. + _ = createTestTables(t, databaseSuffix, []string{tableName}, previous_owner) + return nil + }, + }, + { + Config: ` +resource postgresql_database "test_db" { + name = "tf_tests_db_ownership" + owner = "new_owner" + alter_object_ownership = true +} +`, + Check: resource.ComposeTestCheckFunc( + testAccCheckPostgresqlDatabaseExists("postgresql_database.test_db"), + resource.TestCheckResourceAttr("postgresql_database.test_db", "name", databaseName), + resource.TestCheckResourceAttr("postgresql_database.test_db", "owner", new_owner), + resource.TestCheckResourceAttr("postgresql_database.test_db", "alter_object_ownership", "true"), + + checkTableOwnership(t, config.connStr(databaseName), new_owner, tableName), + ), + }, + }, + }) + +} + func checkUserMembership( t *testing.T, dsn, member, role string, shouldHaveRole bool, ) resource.TestCheckFunc { return func(s *terraform.State) error { - db, err := sql.Open("postgres", dsn) + client := testAccProvider.Meta().(*Client) + db, err := client.Connect() if err != nil { t.Fatalf("could to create connection pool: %v", err) } - defer db.Close() var _rez int - err = db.QueryRow(` - SELECT 1 FROM pg_auth_members - WHERE pg_get_userbyid(roleid) = $1 AND pg_get_userbyid(member) = $2 - `, role, member).Scan(&_rez) + query := "SELECT 1 FROM pg_auth_members WHERE pg_get_userbyid(roleid) = $1 AND pg_get_userbyid(member) = $2" + if db.featureSupported(featureCreateRoleSelfGrant) { + query += " AND (set_option OR inherit_option)" + } + err = db.QueryRow(query, role, member).Scan(&_rez) switch { case err == sql.ErrNoRows: @@ -306,6 +383,38 @@ func checkUserMembership( } } +func checkTableOwnership( + t *testing.T, dsn, owner, tableName string, +) resource.TestCheckFunc { + return func(s *terraform.State) error { + db, err := sql.Open("postgres", dsn) + if err != nil { + t.Fatalf("could not create connection pool: %v", err) + } + defer db.Close() + + var _rez int + + err = db.QueryRow(` + SELECT 1 FROM pg_tables + WHERE tablename = $1 AND tableowner = $2 + `, tableName, owner).Scan(&_rez) + + switch { + case err == sql.ErrNoRows: + return fmt.Errorf( + "User %s should be owner of %s but is not", owner, tableName, + ) + case err != nil: + t.Fatalf("Error checking table ownership. %v", err) + + } + + return nil + + } +} + func testAccCheckPostgresqlDatabaseDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*Client) @@ -396,6 +505,7 @@ resource "postgresql_database" "default_opts" { lc_ctype = "C" connection_limit = -1 is_template = false + alter_object_ownership = false } resource "postgresql_database" "modified_opts" { @@ -407,6 +517,7 @@ resource "postgresql_database" "modified_opts" { lc_ctype = "en_US.UTF-8" connection_limit = 10 is_template = true + alter_object_ownership = true } resource "postgresql_database" "pathological_opts" { diff --git a/postgresql/resource_postgresql_default_privileges.go b/postgresql/resource_postgresql_default_privileges.go index 30be8314..124b3511 100644 --- a/postgresql/resource_postgresql_default_privileges.go +++ b/postgresql/resource_postgresql_default_privileges.go @@ -268,7 +268,11 @@ func readRoleDefaultPrivileges(txn *sql.Tx, d *schema.ResourceData) error { } privilegesSet := pgArrayToSet(privileges) - d.Set("privileges", privilegesSet) + privilegesEqual := resourcePrivilegesEqual(privilegesSet, d) + + if !privilegesEqual { + d.Set("privileges", privilegesSet) + } d.SetId(generateDefaultPrivilegesID(d)) return nil diff --git a/postgresql/resource_postgresql_grant.go b/postgresql/resource_postgresql_grant.go index 43fddb47..ecdae0f3 100644 --- a/postgresql/resource_postgresql_grant.go +++ b/postgresql/resource_postgresql_grant.go @@ -34,12 +34,12 @@ var objectTypes = map[string]string{ "schema": "n", } +type ResourceSchemeGetter func(string) interface{} + func resourcePostgreSQLGrant() *schema.Resource { return &schema.Resource{ Create: PGResourceFunc(resourcePostgreSQLGrantCreate), - // Since all of this resource's arguments force a recreation - // there's no need for an Update function - // Update: + Update: PGResourceFunc(resourcePostgreSQLGrantUpdate), Read: PGResourceFunc(resourcePostgreSQLGrantRead), Delete: PGResourceFunc(resourcePostgreSQLGrantDelete), @@ -88,7 +88,6 @@ func resourcePostgreSQLGrant() *schema.Resource { "privileges": { Type: schema.TypeSet, Required: true, - ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, Description: "The list of privileges to grant", @@ -129,6 +128,14 @@ func resourcePostgreSQLGrantRead(db *DBConnection, d *schema.ResourceData) error } func resourcePostgreSQLGrantCreate(db *DBConnection, d *schema.ResourceData) error { + return resourcePostgreSQLGrantCreateOrUpdate(db, d, false) +} + +func resourcePostgreSQLGrantUpdate(db *DBConnection, d *schema.ResourceData) error { + return resourcePostgreSQLGrantCreateOrUpdate(db, d, true) +} + +func resourcePostgreSQLGrantCreateOrUpdate(db *DBConnection, d *schema.ResourceData, usePrevious bool) error { if err := validateFeatureSupport(db, d); err != nil { return fmt.Errorf("feature is not supported: %v", err) } @@ -187,7 +194,7 @@ func resourcePostgreSQLGrantCreate(db *DBConnection, d *schema.ResourceData) err // Revoke all privileges before granting otherwise reducing privileges will not work. // We just have to revoke them in the same transaction so the role will not lost its // privileges between the revoke and grant statements. - if err := revokeRolePrivileges(txn, d); err != nil { + if err := revokeRolePrivileges(txn, d, usePrevious); err != nil { return err } if err := grantRolePrivileges(txn, d); err != nil { @@ -243,7 +250,7 @@ func resourcePostgreSQLGrantDelete(db *DBConnection, d *schema.ResourceData) err } if err := withRolesGranted(txn, owners, func() error { - return revokeRolePrivileges(txn, d) + return revokeRolePrivileges(txn, d, false) }); err != nil { return err } @@ -255,7 +262,7 @@ func resourcePostgreSQLGrantDelete(db *DBConnection, d *schema.ResourceData) err return nil } -func readDatabaseRolePriviges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error { +func readDatabaseRolePrivileges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error { dbName := d.Get("database").(string) query := ` SELECT array_agg(privilege_type) @@ -269,12 +276,14 @@ WHERE grantee = $2 if err := txn.QueryRow(query, dbName, roleOID).Scan(&privileges); err != nil { return fmt.Errorf("could not read privileges for database %s: %w", dbName, err) } - - d.Set("privileges", pgArrayToSet(privileges)) + granted := pgArrayToSet(privileges) + if !resourcePrivilegesEqual(granted, d) { + return d.Set("privileges", granted) + } return nil } -func readSchemaRolePriviges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error { +func readSchemaRolePrivileges(txn *sql.Tx, d *schema.ResourceData, roleOID uint32) error { dbName := d.Get("schema").(string) query := ` SELECT array_agg(privilege_type) @@ -289,7 +298,10 @@ WHERE grantee = $2 return fmt.Errorf("could not read privileges for schema %s: %w", dbName, err) } - d.Set("privileges", pgArrayToSet(privileges)) + granted := pgArrayToSet(privileges) + if !resourcePrivilegesEqual(granted, d) { + return d.Set("privileges", granted) + } return nil } @@ -309,7 +321,10 @@ WHERE grantee = $2 return fmt.Errorf("could not read privileges for foreign data wrapper %s: %w", fdwName, err) } - d.Set("privileges", pgArrayToSet(privileges)) + granted := pgArrayToSet(privileges) + if !resourcePrivilegesEqual(granted, d) { + return d.Set("privileges", granted) + } return nil } @@ -329,7 +344,10 @@ WHERE grantee = $2 return fmt.Errorf("could not read privileges for foreign server %s: %w", srvName, err) } - d.Set("privileges", pgArrayToSet(privileges)) + granted := pgArrayToSet(privileges) + if !resourcePrivilegesEqual(granted, d) { + return d.Set("privileges", granted) + } return nil } @@ -426,10 +444,10 @@ func readRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error { switch objectType { case "database": - return readDatabaseRolePriviges(txn, d, roleOID) + return readDatabaseRolePrivileges(txn, d, roleOID) case "schema": - return readSchemaRolePriviges(txn, d, roleOID) + return readSchemaRolePrivileges(txn, d, roleOID) case "foreign_data_wrapper": return readForeignDataWrapperRolePrivileges(txn, d, roleOID) @@ -502,8 +520,7 @@ GROUP BY pg_class.relname } privilegesSet := pgArrayToSet(privileges) - - if !privilegesSet.Equal(d.Get("privileges").(*schema.Set)) { + if !resourcePrivilegesEqual(privilegesSet, d) { // If any object doesn't have the same privileges as saved in the state, // we return its privileges to force an update. log.Printf( @@ -589,40 +606,40 @@ func createGrantQuery(d *schema.ResourceData, privileges []string) string { return query } -func createRevokeQuery(d *schema.ResourceData) string { +func createRevokeQuery(getter ResourceSchemeGetter) string { var query string - switch strings.ToUpper(d.Get("object_type").(string)) { + switch strings.ToUpper(getter("object_type").(string)) { case "DATABASE": query = fmt.Sprintf( "REVOKE ALL PRIVILEGES ON DATABASE %s FROM %s", - pq.QuoteIdentifier(d.Get("database").(string)), - pq.QuoteIdentifier(d.Get("role").(string)), + pq.QuoteIdentifier(getter("database").(string)), + pq.QuoteIdentifier(getter("role").(string)), ) case "SCHEMA": query = fmt.Sprintf( "REVOKE ALL PRIVILEGES ON SCHEMA %s FROM %s", - pq.QuoteIdentifier(d.Get("schema").(string)), - pq.QuoteIdentifier(d.Get("role").(string)), + pq.QuoteIdentifier(getter("schema").(string)), + pq.QuoteIdentifier(getter("role").(string)), ) case "FOREIGN_DATA_WRAPPER": - fdwName := d.Get("objects").(*schema.Set).List()[0] + fdwName := getter("objects").(*schema.Set).List()[0] query = fmt.Sprintf( "REVOKE ALL PRIVILEGES ON FOREIGN DATA WRAPPER %s FROM %s", pq.QuoteIdentifier(fdwName.(string)), - pq.QuoteIdentifier(d.Get("role").(string)), + pq.QuoteIdentifier(getter("role").(string)), ) case "FOREIGN_SERVER": - srvName := d.Get("objects").(*schema.Set).List()[0] + srvName := getter("objects").(*schema.Set).List()[0] query = fmt.Sprintf( "REVOKE ALL PRIVILEGES ON FOREIGN SERVER %s FROM %s", pq.QuoteIdentifier(srvName.(string)), - pq.QuoteIdentifier(d.Get("role").(string)), + pq.QuoteIdentifier(getter("role").(string)), ) case "COLUMN": - objects := d.Get("objects").(*schema.Set) - columns := d.Get("columns").(*schema.Set) - privileges := d.Get("privileges").(*schema.Set) + objects := getter("objects").(*schema.Set) + columns := getter("columns").(*schema.Set) + privileges := getter("privileges").(*schema.Set) if privileges.Len() == 0 || columns.Len() == 0 { // No privileges to revoke, so don't revoke anything query = "SELECT NULL" @@ -631,13 +648,13 @@ func createRevokeQuery(d *schema.ResourceData) string { "REVOKE %s (%s) ON TABLE %s FROM %s", setToPgIdentSimpleList(privileges), setToPgIdentListWithoutSchema(columns), - setToPgIdentList(d.Get("schema").(string), objects), - pq.QuoteIdentifier(d.Get("role").(string)), + setToPgIdentList(getter("schema").(string), objects), + pq.QuoteIdentifier(getter("role").(string)), ) } case "TABLE", "SEQUENCE", "FUNCTION", "PROCEDURE", "ROUTINE": - objects := d.Get("objects").(*schema.Set) - privileges := d.Get("privileges").(*schema.Set) + objects := getter("objects").(*schema.Set) + privileges := getter("privileges").(*schema.Set) if objects.Len() > 0 { if privileges.Len() > 0 { // Revoking specific privileges instead of all privileges @@ -645,24 +662,24 @@ func createRevokeQuery(d *schema.ResourceData) string { query = fmt.Sprintf( "REVOKE %s ON %s %s FROM %s", setToPgIdentSimpleList(privileges), - strings.ToUpper(d.Get("object_type").(string)), - setToPgIdentList(d.Get("schema").(string), objects), - pq.QuoteIdentifier(d.Get("role").(string)), + strings.ToUpper(getter("object_type").(string)), + setToPgIdentList(getter("schema").(string), objects), + pq.QuoteIdentifier(getter("role").(string)), ) } else { query = fmt.Sprintf( "REVOKE ALL PRIVILEGES ON %s %s FROM %s", - strings.ToUpper(d.Get("object_type").(string)), - setToPgIdentList(d.Get("schema").(string), objects), - pq.QuoteIdentifier(d.Get("role").(string)), + strings.ToUpper(getter("object_type").(string)), + setToPgIdentList(getter("schema").(string), objects), + pq.QuoteIdentifier(getter("role").(string)), ) } } else { query = fmt.Sprintf( "REVOKE ALL PRIVILEGES ON ALL %sS IN SCHEMA %s FROM %s", - strings.ToUpper(d.Get("object_type").(string)), - pq.QuoteIdentifier(d.Get("schema").(string)), - pq.QuoteIdentifier(d.Get("role").(string)), + strings.ToUpper(getter("object_type").(string)), + pq.QuoteIdentifier(getter("schema").(string)), + pq.QuoteIdentifier(getter("role").(string)), ) } } @@ -687,8 +704,21 @@ func grantRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error { return err } -func revokeRolePrivileges(txn *sql.Tx, d *schema.ResourceData) error { - query := createRevokeQuery(d) +func revokeRolePrivileges(txn *sql.Tx, d *schema.ResourceData, usePrevious bool) error { + getter := d.Get + + if usePrevious { + getter = func(name string) interface{} { + if d.HasChange(name) { + old, _ := d.GetChange(name) + return old + } + + return d.Get(name) + } + } + + query := createRevokeQuery(getter) if len(query) == 0 { // Query is empty, don't run anything return nil diff --git a/postgresql/resource_postgresql_grant_test.go b/postgresql/resource_postgresql_grant_test.go index a81c95bc..4b62180b 100644 --- a/postgresql/resource_postgresql_grant_test.go +++ b/postgresql/resource_postgresql_grant_test.go @@ -293,7 +293,7 @@ func TestCreateRevokeQuery(t *testing.T) { } for _, c := range cases { - out := createRevokeQuery(c.resource) + out := createRevokeQuery(c.resource.Get) if out != c.expected { t.Fatalf("Error matching output and expected: %#v vs %#v", out, c.expected) } @@ -1139,6 +1139,83 @@ resource "postgresql_grant" "test" { }) } +func TestAccPostgresqlImplicitGrants(t *testing.T) { + skipIfNotAcc(t) + + dbSuffix, teardown := setupTestDatabase(t, true, true) + defer teardown() + + testTables := []string{"test_schema.test_table"} + createTestTables(t, dbSuffix, testTables, "") + + dbName, roleName := getTestDBNames(dbSuffix) + + // create a TF config with placeholder for privileges + // it will be filled in each step. + var testGrant = fmt.Sprintf(` + resource "postgresql_grant" "test" { + database = "%s" + role = "%s" + schema = "test_schema" + object_type = "table" + objects = ["test_table"] + privileges = %%s + } + `, dbName, roleName) + + var testCheckTableGrants = func(grants ...string) resource.TestCheckFunc { + return func(*terraform.State) error { + return testCheckTablesPrivileges(t, dbName, roleName, []string{testTables[0]}, grants) + } + } + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testCheckCompatibleVersion(t, featurePrivileges) + }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testGrant, `["ALL"]`), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "postgresql_grant.test", "id", fmt.Sprintf("%s_%s_test_schema_table_test_table", roleName, dbName), + ), + resource.TestCheckResourceAttr("postgresql_grant.test", "objects.#", "1"), + resource.TestCheckResourceAttr("postgresql_grant.test", "objects.0", "test_table"), + testCheckTableGrants("SELECT", "INSERT", "UPDATE", "DELETE"), + ), + }, + { + Config: fmt.Sprintf(testGrant, `["SELECT"]`), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_grant.test", "objects.#", "1"), + resource.TestCheckResourceAttr("postgresql_grant.test", "objects.0", "test_table"), + testCheckTableGrants("SELECT"), + ), + }, + { + // Empty list means that privileges will be applied on all tables. + Config: fmt.Sprintf(testGrant, `["SELECT", "INSERT", "UPDATE", "DELETE"]`), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_grant.test", "objects.#", "1"), + resource.TestCheckResourceAttr("postgresql_grant.test", "objects.0", "test_table"), + testCheckTableGrants("SELECT", "INSERT", "UPDATE", "DELETE"), + ), + }, + { + Config: fmt.Sprintf(testGrant, `[]`), + Destroy: true, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("postgresql_grant.test", "objects.#", "1"), + resource.TestCheckResourceAttr("postgresql_grant.test", "objects.0", "test_table"), + testCheckTableGrants(""), + ), + }, + }, + }) +} + func TestAccPostgresqlGrantSchema(t *testing.T) { // create a TF config with placeholder for privileges // it will be filled in each step. diff --git a/postgresql/resource_postgresql_role_test.go b/postgresql/resource_postgresql_role_test.go index ef502f00..fc958840 100644 --- a/postgresql/resource_postgresql_role_test.go +++ b/postgresql/resource_postgresql_role_test.go @@ -212,7 +212,16 @@ resource "postgresql_role" "test_role" { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) - testCheckCompatibleVersion(t, featurePrivileges) + client := testAccProvider.Meta().(*Client) + db, err := client.Connect() + if err != nil { + t.Fatalf("could connect to database: %v", err) + } + // Requires >= 9 and <16 + // We disable this test for >= pg16 as it makes no sense with the new createRoleSelfGrant feature + if !db.featureSupported(featurePrivileges) || db.featureSupported(featureCreateRoleSelfGrant) { + t.Skipf("Skip extension tests for Postgres %s", db.version) + } }, Providers: testAccProviders, CheckDestroy: testAccCheckPostgresqlRoleDestroy, diff --git a/postgresql/resource_postgresql_security_label.go b/postgresql/resource_postgresql_security_label.go new file mode 100644 index 00000000..626d87fd --- /dev/null +++ b/postgresql/resource_postgresql_security_label.go @@ -0,0 +1,198 @@ +package postgresql + +import ( + "bytes" + "database/sql" + "fmt" + "log" + "regexp" + "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("[DEBUG] 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 fmt.Errorf("could not create security label: %w", 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("[DEBUG] 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, provider, objname, label FROM pg_seclabels WHERE objtype = $1 and objname = $2 and provider = $3" + row := db.QueryRow(query, objectType, quoteIdentifier(objectName), quoteIdentifier(provider)) + + var label, newObjectName, newProvider string + err = row.Scan(&objectType, &newProvider, &newObjectName, &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) + } + + if quoteIdentifier(objectName) != newObjectName || quoteIdentifier(provider) != newProvider { + // In reality, this should never happen, but if it does, we want to make sure that the state is in sync with the remote system + // This will trigger a TF error saying that the provider has a bug if it ever happens + objectName = newObjectName + provider = newProvider + } + 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("[DEBUG] 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("[DEBUG] 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), + }, ".") +} + +func quoteIdentifier(s string) string { + var result = s + re := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`) + if !re.MatchString(s) || s != strings.ToLower(s) { + result = pq.QuoteIdentifier(s) + } + return result +} diff --git a/postgresql/resource_postgresql_security_label_test.go b/postgresql/resource_postgresql_security_label_test.go new file mode 100644 index 00000000..f1701795 --- /dev/null +++ b/postgresql/resource_postgresql_security_label_test.go @@ -0,0 +1,214 @@ +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, quoteIdentifier(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] + if !ok { + return fmt.Errorf("No Attribute for security provider is set") + } + + 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" +} +` diff --git a/postgresql/resource_postgresql_user_mapping.go b/postgresql/resource_postgresql_user_mapping.go index 15b9136c..891ef2b0 100644 --- a/postgresql/resource_postgresql_user_mapping.go +++ b/postgresql/resource_postgresql_user_mapping.go @@ -114,6 +114,13 @@ func resourcePostgreSQLUserMappingReadImpl(db *DBConnection, d *schema.ResourceD var userMappingOptions []string query := "SELECT umoptions FROM information_schema._pg_user_mappings WHERE authorization_identifier = $1 and foreign_server_name = $2" err = txn.QueryRow(query, username, serverName).Scan(pq.Array(&userMappingOptions)) + + if err != sql.ErrNoRows && err != nil { + // Fallback to pg_user_mappings table if information_schema._pg_user_mappings is not available + query := "SELECT umoptions FROM pg_user_mappings WHERE usename = $1 and srvname = $2" + err = txn.QueryRow(query, username, serverName).Scan(pq.Array(&userMappingOptions)) + } + switch { case err == sql.ErrNoRows: log.Printf("[WARN] PostgreSQL user mapping (%s) for server (%s) not found", username, serverName) diff --git a/tests/build/Dockerfile b/tests/build/Dockerfile new file mode 100644 index 00000000..60efcd79 --- /dev/null +++ b/tests/build/Dockerfile @@ -0,0 +1,9 @@ +ARG PGVERSION +FROM postgres:${PGVERSION:-latest} + +ARG PGVERSION +RUN apt-get update && apt-get install -y build-essential postgresql-server-dev-${PGVERSION:-all} +RUN dpkg -l |grep postgresql +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..6eb18a01 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -2,7 +2,10 @@ version: "3" services: postgres: - image: postgres:${PGVERSION:-latest} + build: + context: build + args: + - PGVERSION=${PGVERSION} user: postgres command: - "postgres" @@ -10,6 +13,8 @@ services: - "wal_level=logical" - "-c" - "max_replication_slots=10" + - "-c" + - "shared_preload_libraries=/opt/dummy_seclabel/dummy_seclabel" environment: POSTGRES_PASSWORD: ${PGPASSWORD} ports: diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index 0d19ccf9..9a60bdc2 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -183,6 +183,7 @@ The following arguments are supported: from the environment (or the given profile, see `aws_rds_iam_profile`) * `aws_rds_iam_profile` - (Optional) The AWS IAM Profile to use while using AWS RDS IAM Auth. * `aws_rds_iam_region` - (Optional) The AWS region to use while using AWS RDS IAM Auth. +* `aws_rds_iam_provider_role_arn` - (Optional) AWS IAM role to assume while using AWS RDS IAM Auth. * `azure_identity_auth` - (Optional) If set to `true`, call the Azure OAuth token endpoint for temporary token * `azure_tenant_id` - (Optional) (Required if `azure_identity_auth` is `true`) Azure tenant ID [read more](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config.html) * `azure_environment` - (Optional) The Azure Cloud environment. Possible values are `public`, `usgovernment` and `china`. Defaults to `public`. diff --git a/website/docs/r/postgresql_database.html.markdown b/website/docs/r/postgresql_database.html.markdown index 5110e0f5..d2715288 100644 --- a/website/docs/r/postgresql_database.html.markdown +++ b/website/docs/r/postgresql_database.html.markdown @@ -17,12 +17,13 @@ within a PostgreSQL server instance. ```hcl resource "postgresql_database" "my_db" { - name = "my_db" - owner = "my_role" - template = "template0" - lc_collate = "C" - connection_limit = -1 - allow_connections = true + name = "my_db" + owner = "my_role" + template = "template0" + lc_collate = "C" + connection_limit = -1 + allow_connections = true + alter_object_ownership = true } ``` @@ -82,6 +83,14 @@ resource "postgresql_database" "my_db" { force the creation of a new resource as this value can only be changed when a database is created. +* `alter_object_ownership` - (Optional) If `true`, the change of the database + `owner` will also include a reassignment of the ownership of preexisting + objects like tables or sequences from the previous owner to the new one. + If set to `false` (the default), then the previous database `owner` will still + hold the ownership of the objects in that database. To alter existing objects in + the database, you must be a direct or indirect member of the specified role, or + the username in the provider must be superuser. + ## Import Example `postgresql_database` supports importing resources. Supposing the following diff --git a/website/docs/r/postgresql_default_privileges.html.markdown b/website/docs/r/postgresql_default_privileges.html.markdown index 8c995370..9bd36828 100644 --- a/website/docs/r/postgresql_default_privileges.html.markdown +++ b/website/docs/r/postgresql_default_privileges.html.markdown @@ -28,17 +28,31 @@ resource "postgresql_default_privileges" "read_only_tables" { ## Argument Reference -* `role` - (Required) The name of the role to which grant default privileges on. +* `role` - (Required) The role that will automatically be granted the specified privileges on new objects created by the owner. * `database` - (Required) The database to grant default privileges for this role. -* `owner` - (Required) Role for which apply default privileges (You can change default privileges only for objects that will be created by yourself or by roles that you are a member of). +* `owner` - (Required) Specifies the role that creates objects for which the default privileges will be applied. * `schema` - (Optional) The database schema to set default privileges for this role. * `object_type` - (Required) The PostgreSQL object type to set the default privileges on (one of: table, sequence, function, type, schema). -* `privileges` - (Required) The list of privileges to apply as default privileges. An empty list could be provided to revoke all default privileges for this role. +* `privileges` - (Required) List of privileges (e.g., SELECT, INSERT, UPDATE, DELETE) to grant on new objects created by the owner. An empty list could be provided to revoke all default privileges for this role. ## Examples -Revoke default privileges for functions for "public" role: +### Grant default privileges for tables to "current_role" role: + +```hcl +resource "postgresql_default_privileges" "grant_table_privileges" { + database = postgresql_database.example_db.name + role = "current_role" + owner = "owner_role" + schema = "public" + object_type = "table" + privileges = ["SELECT", "INSERT", "UPDATE"] +} +``` +Whenever the `owner_role` creates a new table in the `public` schema, the `current_role` is automatically granted SELECT, INSERT, and UPDATE privileges on that table. + +### Revoke default privileges for functions for "public" role: ```hcl resource "postgresql_default_privileges" "revoke_public" { 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..64d4960f --- /dev/null +++ b/website/docs/r/postgresql_security_label.html.markdown @@ -0,0 +1,42 @@ +--- +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. + +## Import + +Security label is an attribute that can be added multiple times, so no import is needed, simply apply again. 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 +