diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 998b2e82..cea424f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,12 +50,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: true - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} @@ -83,7 +83,7 @@ jobs: # We could run 'make lint' but we prefer this action because it leaves # 'annotations' (i.e. it comments on PRs to point out linter violations). - name: Lint - uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 + uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9.2.1 with: version: ${{ env.GOLANGCI_VERSION }} @@ -94,12 +94,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: true - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} @@ -144,7 +144,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: true @@ -152,7 +152,7 @@ jobs: run: git fetch --prune --unshallow - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} @@ -181,7 +181,7 @@ jobs: run: make -j2 test - name: Publish Unit Test Coverage - uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: use_oidc: true flags: unittests @@ -194,17 +194,17 @@ jobs: steps: - name: Setup QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 with: platforms: all - name: Setup Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 with: version: ${{ env.DOCKER_BUILDX_VERSION }} - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: true @@ -212,7 +212,7 @@ jobs: run: git fetch --prune --unshallow - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} @@ -254,12 +254,12 @@ jobs: steps: - name: Setup QEMU - uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0 + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 with: platforms: all - name: Setup Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 with: version: ${{ env.DOCKER_BUILDX_VERSION }} @@ -272,7 +272,7 @@ jobs: password: ${{ secrets.XPKG_TOKEN }} - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: true @@ -280,7 +280,7 @@ jobs: run: git fetch --prune --unshallow - name: Setup Go - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7c0dccc3..7c175032 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,14 +29,14 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: submodules: true - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 with: languages: go - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/tag.yml b/.github/workflows/tag.yml index 8eab397c..04957289 100644 --- a/.github/workflows/tag.yml +++ b/.github/workflows/tag.yml @@ -16,7 +16,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Create Tag uses: negz/create-tag@39bae1e0932567a58c20dea5a1a0d18358503320 # v1 diff --git a/apis/cluster/postgresql/v1alpha1/grant_types.go b/apis/cluster/postgresql/v1alpha1/grant_types.go index 325701f3..21f542e6 100644 --- a/apis/cluster/postgresql/v1alpha1/grant_types.go +++ b/apis/cluster/postgresql/v1alpha1/grant_types.go @@ -302,6 +302,7 @@ type Routine struct { } // GrantParameters define the desired state of a PostgreSQL grant instance. +// +kubebuilder:validation:XValidation:rule="!has(self.withInherit) || has(self.memberOf) || has(self.memberOfRef) || has(self.memberOfSelector)",message="withInherit may only be set on memberOf grants" type GrantParameters struct { // Privileges to be granted. // See https://www.postgresql.org/docs/current/sql-grant.html for available privileges. @@ -406,6 +407,15 @@ type GrantParameters struct { // +optional // +kubebuilder:validation:items:Pattern:=^[a-zA-Z_][a-zA-Z0-9_$]*$ ForeignServers []string `json:"foreignServers,omitempty"` + + // WithInherit controls whether the grantee automatically inherits the privileges + // of the granted role. When set to false, emits WITH INHERIT FALSE (PostgreSQL 16+), + // granting membership without automatic privilege inheritance. Only valid when + // memberOf is set. When omitted, PostgreSQL's default behavior (inherit true) applies. + // Note: this field is only evaluated when non-nil. Removing it from the manifest + // does NOT revert the database-side setting; set withInherit: true explicitly to revert. + // +optional + WithInherit *bool `json:"withInherit,omitempty"` } // A GrantStatus represents the observed state of a Grant. diff --git a/apis/cluster/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/cluster/postgresql/v1alpha1/zz_generated.deepcopy.go index dedd46c2..05241d2e 100644 --- a/apis/cluster/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/cluster/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -633,6 +633,11 @@ func (in *GrantParameters) DeepCopyInto(out *GrantParameters) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.WithInherit != nil { + in, out := &in.WithInherit, &out.WithInherit + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrantParameters. diff --git a/apis/namespaced/postgresql/v1alpha1/grant_types.go b/apis/namespaced/postgresql/v1alpha1/grant_types.go index cbfe9f27..99004214 100644 --- a/apis/namespaced/postgresql/v1alpha1/grant_types.go +++ b/apis/namespaced/postgresql/v1alpha1/grant_types.go @@ -304,6 +304,7 @@ type Routine struct { } // GrantParameters define the desired state of a PostgreSQL grant instance. +// +kubebuilder:validation:XValidation:rule="!has(self.withInherit) || has(self.memberOf) || has(self.memberOfRef) || has(self.memberOfSelector)",message="withInherit may only be set on memberOf grants" type GrantParameters struct { // Privileges to be granted. // See https://www.postgresql.org/docs/current/sql-grant.html for available privileges. @@ -408,6 +409,15 @@ type GrantParameters struct { // +optional // +kubebuilder:validation:items:Pattern:=^[a-zA-Z_][a-zA-Z0-9_$]*$ ForeignServers []string `json:"foreignServers,omitempty"` + + // WithInherit controls whether the grantee automatically inherits the privileges + // of the granted role. When set to false, emits WITH INHERIT FALSE (PostgreSQL 16+), + // granting membership without automatic privilege inheritance. Only valid when + // memberOf is set. When omitted, PostgreSQL's default behavior (inherit true) applies. + // Note: this field is only evaluated when non-nil. Removing it from the manifest + // does NOT revert the database-side setting; set withInherit: true explicitly to revert. + // +optional + WithInherit *bool `json:"withInherit,omitempty"` } // A GrantStatus represents the observed state of a Grant. diff --git a/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go b/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go index 7a62990c..fa54e26b 100644 --- a/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go +++ b/apis/namespaced/postgresql/v1alpha1/zz_generated.deepcopy.go @@ -750,6 +750,11 @@ func (in *GrantParameters) DeepCopyInto(out *GrantParameters) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.WithInherit != nil { + in, out := &in.WithInherit, &out.WithInherit + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrantParameters. diff --git a/cluster/images/provider-sql/Dockerfile b/cluster/images/provider-sql/Dockerfile index 3edf2763..0e6fe119 100644 --- a/cluster/images/provider-sql/Dockerfile +++ b/cluster/images/provider-sql/Dockerfile @@ -1,4 +1,4 @@ -FROM gcr.io/distroless/static:nonroot@sha256:e3f945647ffb95b5839c07038d64f9811adf17308b9121d8a2b87b6a22a80a39 +FROM gcr.io/distroless/static:nonroot@sha256:963fa6c544fe5ce420f1f54fb88b6fb01479f054c8056d0f74cc2c6000df5240 ARG TARGETOS ARG TARGETARCH diff --git a/examples/cluster/postgresql/grant-with-inherit-false.yaml b/examples/cluster/postgresql/grant-with-inherit-false.yaml new file mode 100644 index 00000000..24442840 --- /dev/null +++ b/examples/cluster/postgresql/grant-with-inherit-false.yaml @@ -0,0 +1,25 @@ +# Demonstrates WITH INHERIT FALSE on a role membership grant (PostgreSQL 16+). +# +# Use case: RDS IAM database authentication with a shared admin role. +# +# 1. An "admin" role holds rds_iam membership so that migration tools +# (Flyway, Liquibase, Atlas) can authenticate via IAM tokens. +# 2. The RDS master user needs membership in "admin" in order to run +# ALTER DEFAULT PRIVILEGES FOR ROLE admin — but must NOT inherit +# the rds_iam privilege, which would break its password authentication. +# +# GRANT admin TO master_user WITH INHERIT FALSE satisfies both requirements: +# the master user can SET ROLE admin (for ALTER DEFAULT PRIVILEGES) while +# retaining its own password-based login. +--- +apiVersion: postgresql.sql.crossplane.io/v1alpha1 +kind: Grant +metadata: + name: grant-master-user-membership-no-inherit +spec: + forProvider: + withInherit: false + roleRef: + name: master-user + memberOfRef: + name: admin-role diff --git a/go.mod b/go.mod index 4295bebd..f5047592 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/crossplane/crossplane-runtime/v2 v2.2.1 github.com/crossplane/crossplane-tools v0.0.0-20250731192036-00d407d8b7ec github.com/crossplane/upjet/v2 v2.2.0 - github.com/go-sql-driver/mysql v1.9.3 + github.com/go-sql-driver/mysql v1.10.0 github.com/google/go-cmp v0.7.0 github.com/lib/pq v1.12.3 github.com/microsoft/go-mssqldb v1.10.0 @@ -22,7 +22,7 @@ require ( require ( dario.cat/mergo v1.0.2 // indirect - filippo.io/edwards25519 v1.1.1 // indirect + filippo.io/edwards25519 v1.2.0 // indirect github.com/agext/levenshtein v1.2.3 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/antchfx/htmlquery v1.2.4 // indirect diff --git a/go.sum b/go.sum index 1ae620e6..12632af9 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= -filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw= -filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= +filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= @@ -115,8 +115,8 @@ github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxE github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw= +github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= diff --git a/package/crds/postgresql.sql.crossplane.io_grants.yaml b/package/crds/postgresql.sql.crossplane.io_grants.yaml index 516fb06a..810a7ec5 100644 --- a/package/crds/postgresql.sql.crossplane.io_grants.yaml +++ b/package/crds/postgresql.sql.crossplane.io_grants.yaml @@ -462,6 +462,15 @@ spec: pattern: ^[a-zA-Z_][a-zA-Z0-9_$]*$ type: string type: array + withInherit: + description: |- + WithInherit controls whether the grantee automatically inherits the privileges + of the granted role. When set to false, emits WITH INHERIT FALSE (PostgreSQL 16+), + granting membership without automatic privilege inheritance. Only valid when + memberOf is set. When omitted, PostgreSQL's default behavior (inherit true) applies. + Note: this field is only evaluated when non-nil. Removing it from the manifest + does NOT revert the database-side setting; set withInherit: true explicitly to revert. + type: boolean withOption: description: |- WithOption allows an option to be set on the grant. @@ -472,6 +481,10 @@ spec: - GRANT type: string type: object + x-kubernetes-validations: + - message: withInherit may only be set on memberOf grants + rule: '!has(self.withInherit) || has(self.memberOf) || has(self.memberOfRef) + || has(self.memberOfSelector)' managementPolicies: default: - '*' diff --git a/package/crds/postgresql.sql.m.crossplane.io_grants.yaml b/package/crds/postgresql.sql.m.crossplane.io_grants.yaml index 7dc8c04a..6d140dc4 100644 --- a/package/crds/postgresql.sql.m.crossplane.io_grants.yaml +++ b/package/crds/postgresql.sql.m.crossplane.io_grants.yaml @@ -472,6 +472,15 @@ spec: pattern: ^[a-zA-Z_][a-zA-Z0-9_$]*$ type: string type: array + withInherit: + description: |- + WithInherit controls whether the grantee automatically inherits the privileges + of the granted role. When set to false, emits WITH INHERIT FALSE (PostgreSQL 16+), + granting membership without automatic privilege inheritance. Only valid when + memberOf is set. When omitted, PostgreSQL's default behavior (inherit true) applies. + Note: this field is only evaluated when non-nil. Removing it from the manifest + does NOT revert the database-side setting; set withInherit: true explicitly to revert. + type: boolean withOption: description: |- WithOption allows an option to be set on the grant. @@ -482,6 +491,10 @@ spec: - GRANT type: string type: object + x-kubernetes-validations: + - message: withInherit may only be set on memberOf grants + rule: '!has(self.withInherit) || has(self.memberOf) || has(self.memberOfRef) + || has(self.memberOfSelector)' managementPolicies: default: - '*' diff --git a/pkg/controller/cluster/postgresql/grant/reconciler.go b/pkg/controller/cluster/postgresql/grant/reconciler.go index 81c716b1..802bc3e9 100644 --- a/pkg/controller/cluster/postgresql/grant/reconciler.go +++ b/pkg/controller/cluster/postgresql/grant/reconciler.go @@ -61,6 +61,8 @@ const ( errInvalidParams = "invalid parameters for grant type %s" errGetServerVersion = "cannot get server version" errMemberOfWithDatabaseOrPrivileges = "cannot set privileges or database in the same grant as memberOf" + errWithInheritOnlyForMemberOf = "withInherit is only valid for memberOf grants" + errInheritRequiresPG16 = "withInherit requires PostgreSQL 16 or later (server version %d)" maxConcurrency = 5 ) @@ -280,6 +282,9 @@ func createGrantQueriesWithVersion(gp v1alpha1.GrantParameters, ql *[]xsql.Query case v1alpha1.RoleForeignServer: return createForeignServerGrantQueries(gp, ql, ro) case v1alpha1.RoleMember: + if gp.WithInherit != nil && serverVersion < 160000 { + return errors.Errorf(errInheritRequiresPG16, serverVersion) + } return createMemberGrantQueries(gp, ql, ro) case v1alpha1.RoleRoutine: return createRoutineGrantQueries(gp, ql, ro) @@ -303,12 +308,33 @@ func createMemberGrantQueries(gp v1alpha1.GrantParameters, ql *[]xsql.Query, ro *ql = append(*ql, xsql.Query{String: fmt.Sprintf("REVOKE %s FROM %s", mo, ro)}, xsql.Query{String: fmt.Sprintf("GRANT %s TO %s %s", mo, ro, - withOption(gp.WithOption), + membershipWithClauses(gp.WithOption, gp.WithInherit), )}, ) return nil } +// membershipWithClauses builds the WITH clause for role membership GRANTs, +// combining the optional ADMIN/SET option and the optional INHERIT flag. +// On PostgreSQL 16+, multiple options are comma-separated: WITH ADMIN OPTION, INHERIT FALSE. +func membershipWithClauses(option *v1alpha1.GrantOption, inherit *bool) string { + var parts []string + if option != nil { + parts = append(parts, fmt.Sprintf("%s OPTION", string(*option))) + } + if inherit != nil { + if *inherit { + parts = append(parts, "INHERIT TRUE") + } else { + parts = append(parts, "INHERIT FALSE") + } + } + if len(parts) == 0 { + return "" + } + return "WITH " + strings.Join(parts, ", ") +} + func createRoutineGrantQueries(gp v1alpha1.GrantParameters, ql *[]xsql.Query, ro string) error { if gp.Database == nil || gp.Schema == nil || len(gp.Routines) < 1 || gp.Role == nil || len(gp.Privileges) < 1 { return errors.Errorf(errInvalidParams, v1alpha1.RoleRoutine) @@ -603,6 +629,10 @@ func resolveGrantType(gp v1alpha1.GrantParameters) (v1alpha1.GrantType, error) { return v1alpha1.RoleMember, nil } + if gp.WithInherit != nil { + return "", errors.New(errWithInheritOnlyForMemberOf) + } + return gp.IdentifyGrantType() } @@ -771,6 +801,9 @@ func selectGrantQueryWithVersion(gp v1alpha1.GrantParameters, q *xsql.Query, ser case v1alpha1.RoleForeignServer: return selectForeignServerGrantQuery(gp, q) case v1alpha1.RoleMember: + if gp.WithInherit != nil && serverVersion < 160000 { + return errors.Errorf(errInheritRequiresPG16, serverVersion) + } return selectMemberGrantQuery(gp, q) case v1alpha1.RoleRoutine: return selectRoutineGrantQuery(gp, q) @@ -791,17 +824,27 @@ func selectMemberGrantQuery(gp v1alpha1.GrantParameters, q *xsql.Query) error { // A simpler query would use ::regrole to cast the roleid and member oids // to their role names, but that throws an error for nonexistent roles // rather than returning false. - q.String = "SELECT EXISTS(SELECT 1 FROM pg_auth_members m " + - "INNER JOIN pg_roles mo ON m.roleid = mo.oid " + - "INNER JOIN pg_roles r ON m.member = r.oid " + - "WHERE r.rolname=$1 AND mo.rolname=$2 AND " + - "m.admin_option = $3)" - q.Parameters = []interface{}{ gp.Role, gp.MemberOf, ao, } + + if gp.WithInherit != nil { + // inherit_option is a pg_auth_members column added in PostgreSQL 16. + q.String = "SELECT EXISTS(SELECT 1 FROM pg_auth_members m " + + "INNER JOIN pg_roles mo ON m.roleid = mo.oid " + + "INNER JOIN pg_roles r ON m.member = r.oid " + + "WHERE r.rolname=$1 AND mo.rolname=$2 AND " + + "m.admin_option = $3 AND m.inherit_option = $4)" + q.Parameters = append(q.Parameters, *gp.WithInherit) + } else { + q.String = "SELECT EXISTS(SELECT 1 FROM pg_auth_members m " + + "INNER JOIN pg_roles mo ON m.roleid = mo.oid " + + "INNER JOIN pg_roles r ON m.member = r.oid " + + "WHERE r.rolname=$1 AND mo.rolname=$2 AND " + + "m.admin_option = $3)" + } return nil } diff --git a/pkg/controller/cluster/postgresql/grant/reconciler_test.go b/pkg/controller/cluster/postgresql/grant/reconciler_test.go index 1ccabe70..cf667da6 100644 --- a/pkg/controller/cluster/postgresql/grant/reconciler_test.go +++ b/pkg/controller/cluster/postgresql/grant/reconciler_test.go @@ -195,7 +195,8 @@ func TestObserve(t *testing.T) { gog := v1alpha1.GrantOptionGrant type fields struct { - db xsql.DB + db xsql.DB + serverVersion int } type args struct { @@ -632,6 +633,140 @@ func TestObserve(t *testing.T) { err: nil, }, }, + "SuccessRoleMembershipWithInheritNil": { + reason: "WithInherit nil should produce a 3-parameter query (no inherit_option filter)", + fields: fields{ + serverVersion: 160000, + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + if len(q.Parameters) != 3 { + return fmt.Errorf("expected 3 query parameters, got %d", len(q.Parameters)) + } + bv := dest[0].(*bool) + *bv = true + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + "SuccessRoleMembershipWithInheritFalse": { + reason: "WithInherit false should produce a 4-parameter query with $4 == false", + fields: fields{ + serverVersion: 160000, + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + if len(q.Parameters) != 4 { + return fmt.Errorf("expected 4 query parameters, got %d", len(q.Parameters)) + } + inheritParam, ok := q.Parameters[3].(bool) + if !ok || inheritParam != false { + return fmt.Errorf("expected $4 to be false, got %v", q.Parameters[3]) + } + bv := dest[0].(*bool) + *bv = true + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + "SuccessRoleMembershipWithInheritTrue": { + reason: "WithInherit true should produce a 4-parameter query with $4 == true", + fields: fields{ + serverVersion: 160000, + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + if len(q.Parameters) != 4 { + return fmt.Errorf("expected 4 query parameters, got %d", len(q.Parameters)) + } + inheritParam, ok := q.Parameters[3].(bool) + if !ok || inheritParam != true { + return fmt.Errorf("expected $4 to be true, got %v", q.Parameters[3]) + } + bv := dest[0].(*bool) + *bv = true + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(true), + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + "ErrRoleMembershipWithInheritOnPG15": { + reason: "WithInherit on a server older than PostgreSQL 16 should return an error before any query is run", + fields: fields{ + serverVersion: 150000, + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + return fmt.Errorf("Scan should not be called when server version is below 16") + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + err: errors.Errorf(errInheritRequiresPG16, 150000), + }, + }, } for name, tc := range cases { @@ -640,7 +775,7 @@ func TestObserve(t *testing.T) { if db == nil { db = mockDB{} } - e := external{db: db} + e := external{db: db, serverVersion: tc.fields.serverVersion} got, err := e.Observe(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\ne.Observe(...): -want error, +got error:\n%s\n", tc.reason, diff) @@ -657,7 +792,8 @@ func TestCreate(t *testing.T) { goa := v1alpha1.GrantOptionAdmin type fields struct { - db xsql.DB + db xsql.DB + serverVersion int } type args struct { @@ -935,6 +1071,127 @@ func TestCreate(t *testing.T) { err: nil, }, }, + "RoleMembershipWithInheritNil": { + reason: "WithInherit nil should produce a GRANT with no WITH clause", + fields: fields{ + serverVersion: 160000, + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + if len(ql) != 2 { + return fmt.Errorf("expected 2 queries, got %d", len(ql)) + } + grantSQL := ql[1].String + if strings.Contains(grantSQL, "INHERIT") { + return fmt.Errorf("expected no INHERIT clause, got: %s", grantSQL) + } + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "RoleMembershipWithInheritFalse": { + reason: "WithInherit false should produce a GRANT with WITH INHERIT FALSE", + fields: fields{ + serverVersion: 160000, + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + if len(ql) != 2 { + return fmt.Errorf("expected 2 queries, got %d", len(ql)) + } + grantSQL := ql[1].String + if !strings.Contains(grantSQL, "WITH INHERIT FALSE") { + return fmt.Errorf("expected WITH INHERIT FALSE in grant SQL, got: %s", grantSQL) + } + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "RoleMembershipWithInheritFalseAndAdminOption": { + reason: "WithInherit false and WithOption ADMIN should produce WITH ADMIN OPTION, INHERIT FALSE", + fields: fields{ + serverVersion: 160000, + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + if len(ql) != 2 { + return fmt.Errorf("expected 2 queries, got %d", len(ql)) + } + grantSQL := ql[1].String + if !strings.Contains(grantSQL, "WITH ADMIN OPTION, INHERIT FALSE") { + return fmt.Errorf("expected WITH ADMIN OPTION, INHERIT FALSE in grant SQL, got: %s", grantSQL) + } + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithOption: &goa, + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "ErrRoleMembershipWithInheritOnPG15": { + reason: "WithInherit on a server older than PostgreSQL 16 should return an error before any query is run", + fields: fields{ + serverVersion: 150000, + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + return fmt.Errorf("ExecTx should not be called when server version is below 16") + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Errorf(errInheritRequiresPG16, 150000), errCreateGrant), + }, + }, } for name, tc := range cases { @@ -943,7 +1200,7 @@ func TestCreate(t *testing.T) { if db == nil { db = mockDB{} } - e := external{db: db} + e := external{db: db, serverVersion: tc.fields.serverVersion} got, err := e.Create(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\ne.Create(...): -want error, +got error:\n%s\n", tc.reason, diff) diff --git a/pkg/controller/namespaced/postgresql/grant/reconciler.go b/pkg/controller/namespaced/postgresql/grant/reconciler.go index b1908f22..28205462 100644 --- a/pkg/controller/namespaced/postgresql/grant/reconciler.go +++ b/pkg/controller/namespaced/postgresql/grant/reconciler.go @@ -56,6 +56,8 @@ const ( errInvalidParams = "invalid parameters for grant type %s" errGetServerVersion = "cannot get server version" errMemberOfWithDatabaseOrPrivileges = "cannot set privileges or database in the same grant as memberOf" + errWithInheritOnlyForMemberOf = "withInherit is only valid for memberOf grants" + errInheritRequiresPG16 = "withInherit requires PostgreSQL 16 or later (server version %d)" maxConcurrency = 5 ) @@ -238,6 +240,9 @@ func createGrantQueriesWithVersion(gp v1alpha1.GrantParameters, ql *[]xsql.Query case v1alpha1.RoleForeignServer: return createForeignServerGrantQueries(gp, ql, ro) case v1alpha1.RoleMember: + if gp.WithInherit != nil && serverVersion < 160000 { + return errors.Errorf(errInheritRequiresPG16, serverVersion) + } return createMemberGrantQueries(gp, ql, ro) case v1alpha1.RoleRoutine: return createRoutineGrantQueries(gp, ql, ro) @@ -261,12 +266,33 @@ func createMemberGrantQueries(gp v1alpha1.GrantParameters, ql *[]xsql.Query, ro *ql = append(*ql, xsql.Query{String: fmt.Sprintf("REVOKE %s FROM %s", mo, ro)}, xsql.Query{String: fmt.Sprintf("GRANT %s TO %s %s", mo, ro, - withOption(gp.WithOption), + membershipWithClauses(gp.WithOption, gp.WithInherit), )}, ) return nil } +// membershipWithClauses builds the WITH clause for role membership GRANTs, +// combining the optional ADMIN/SET option and the optional INHERIT flag. +// On PostgreSQL 16+, multiple options are comma-separated: WITH ADMIN OPTION, INHERIT FALSE. +func membershipWithClauses(option *v1alpha1.GrantOption, inherit *bool) string { + var parts []string + if option != nil { + parts = append(parts, fmt.Sprintf("%s OPTION", string(*option))) + } + if inherit != nil { + if *inherit { + parts = append(parts, "INHERIT TRUE") + } else { + parts = append(parts, "INHERIT FALSE") + } + } + if len(parts) == 0 { + return "" + } + return "WITH " + strings.Join(parts, ", ") +} + func createRoutineGrantQueries(gp v1alpha1.GrantParameters, ql *[]xsql.Query, ro string) error { if gp.Database == nil || gp.Schema == nil || len(gp.Routines) < 1 || gp.Role == nil || len(gp.Privileges) < 1 { return errors.Errorf(errInvalidParams, v1alpha1.RoleRoutine) @@ -561,6 +587,10 @@ func resolveGrantType(gp v1alpha1.GrantParameters) (v1alpha1.GrantType, error) { return v1alpha1.RoleMember, nil } + if gp.WithInherit != nil { + return "", errors.New(errWithInheritOnlyForMemberOf) + } + return gp.IdentifyGrantType() } @@ -729,6 +759,9 @@ func selectGrantQueryWithVersion(gp v1alpha1.GrantParameters, q *xsql.Query, ser case v1alpha1.RoleForeignServer: return selectForeignServerGrantQuery(gp, q) case v1alpha1.RoleMember: + if gp.WithInherit != nil && serverVersion < 160000 { + return errors.Errorf(errInheritRequiresPG16, serverVersion) + } return selectMemberGrantQuery(gp, q) case v1alpha1.RoleRoutine: return selectRoutineGrantQuery(gp, q) @@ -749,17 +782,27 @@ func selectMemberGrantQuery(gp v1alpha1.GrantParameters, q *xsql.Query) error { // A simpler query would use ::regrole to cast the roleid and member oids // to their role names, but that throws an error for nonexistent roles // rather than returning false. - q.String = "SELECT EXISTS(SELECT 1 FROM pg_auth_members m " + - "INNER JOIN pg_roles mo ON m.roleid = mo.oid " + - "INNER JOIN pg_roles r ON m.member = r.oid " + - "WHERE r.rolname=$1 AND mo.rolname=$2 AND " + - "m.admin_option = $3)" - q.Parameters = []interface{}{ gp.Role, gp.MemberOf, ao, } + + if gp.WithInherit != nil { + // inherit_option is a pg_auth_members column added in PostgreSQL 16. + q.String = "SELECT EXISTS(SELECT 1 FROM pg_auth_members m " + + "INNER JOIN pg_roles mo ON m.roleid = mo.oid " + + "INNER JOIN pg_roles r ON m.member = r.oid " + + "WHERE r.rolname=$1 AND mo.rolname=$2 AND " + + "m.admin_option = $3 AND m.inherit_option = $4)" + q.Parameters = append(q.Parameters, *gp.WithInherit) + } else { + q.String = "SELECT EXISTS(SELECT 1 FROM pg_auth_members m " + + "INNER JOIN pg_roles mo ON m.roleid = mo.oid " + + "INNER JOIN pg_roles r ON m.member = r.oid " + + "WHERE r.rolname=$1 AND mo.rolname=$2 AND " + + "m.admin_option = $3)" + } return nil } diff --git a/pkg/controller/namespaced/postgresql/grant/reconciler_test.go b/pkg/controller/namespaced/postgresql/grant/reconciler_test.go index 92857b2c..633096ec 100644 --- a/pkg/controller/namespaced/postgresql/grant/reconciler_test.go +++ b/pkg/controller/namespaced/postgresql/grant/reconciler_test.go @@ -21,6 +21,7 @@ import ( "database/sql" "fmt" "sort" + "strings" "testing" "github.com/crossplane-contrib/provider-sql/apis/namespaced/postgresql/v1alpha1" @@ -253,7 +254,8 @@ func TestObserve(t *testing.T) { gog := v1alpha1.GrantOptionGrant type fields struct { - db xsql.DB + db xsql.DB + serverVersion int } type args struct { @@ -681,6 +683,140 @@ func TestObserve(t *testing.T) { err: nil, }, }, + "SuccessRoleMembershipWithInheritNil": { + reason: "WithInherit nil should produce a 3-parameter query (no inherit_option filter)", + fields: fields{ + serverVersion: 160000, + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + if len(q.Parameters) != 3 { + return fmt.Errorf("expected 3 query parameters, got %d", len(q.Parameters)) + } + bv := dest[0].(*bool) + *bv = true + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + "SuccessRoleMembershipWithInheritFalse": { + reason: "WithInherit false should produce a 4-parameter query with $4 == false", + fields: fields{ + serverVersion: 160000, + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + if len(q.Parameters) != 4 { + return fmt.Errorf("expected 4 query parameters, got %d", len(q.Parameters)) + } + inheritParam, ok := q.Parameters[3].(bool) + if !ok || inheritParam != false { + return fmt.Errorf("expected $4 to be false, got %v", q.Parameters[3]) + } + bv := dest[0].(*bool) + *bv = true + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + "SuccessRoleMembershipWithInheritTrue": { + reason: "WithInherit true should produce a 4-parameter query with $4 == true", + fields: fields{ + serverVersion: 160000, + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + if len(q.Parameters) != 4 { + return fmt.Errorf("expected 4 query parameters, got %d", len(q.Parameters)) + } + inheritParam, ok := q.Parameters[3].(bool) + if !ok || inheritParam != true { + return fmt.Errorf("expected $4 to be true, got %v", q.Parameters[3]) + } + bv := dest[0].(*bool) + *bv = true + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(true), + }, + }, + }, + }, + want: want{ + o: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: true, + }, + err: nil, + }, + }, + "ErrRoleMembershipWithInheritOnPG15": { + reason: "WithInherit on a server older than PostgreSQL 16 should return an error before any query is run", + fields: fields{ + serverVersion: 150000, + db: mockDB{ + MockScan: func(ctx context.Context, q xsql.Query, dest ...interface{}) error { + return fmt.Errorf("Scan should not be called when server version is below 16") + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + err: errors.Errorf(errInheritRequiresPG16, 150000), + }, + }, } for name, tc := range cases { @@ -689,7 +825,7 @@ func TestObserve(t *testing.T) { if db == nil { db = mockDB{} } - e := external{db: db} + e := external{db: db, serverVersion: tc.fields.serverVersion} got, err := e.Observe(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\ne.Observe(...): -want error, +got error:\n%s\n", tc.reason, diff) @@ -706,7 +842,8 @@ func TestCreate(t *testing.T) { goa := v1alpha1.GrantOptionAdmin type fields struct { - db xsql.DB + db xsql.DB + serverVersion int } type args struct { @@ -975,6 +1112,127 @@ func TestCreate(t *testing.T) { err: nil, }, }, + "RoleMembershipWithInheritNil": { + reason: "WithInherit nil should produce a GRANT with no WITH clause", + fields: fields{ + serverVersion: 160000, + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + if len(ql) != 2 { + return fmt.Errorf("expected 2 queries, got %d", len(ql)) + } + grantSQL := ql[1].String + if strings.Contains(grantSQL, "INHERIT") { + return fmt.Errorf("expected no INHERIT clause, got: %s", grantSQL) + } + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "RoleMembershipWithInheritFalse": { + reason: "WithInherit false should produce a GRANT with WITH INHERIT FALSE", + fields: fields{ + serverVersion: 160000, + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + if len(ql) != 2 { + return fmt.Errorf("expected 2 queries, got %d", len(ql)) + } + grantSQL := ql[1].String + if !strings.Contains(grantSQL, "WITH INHERIT FALSE") { + return fmt.Errorf("expected WITH INHERIT FALSE in grant SQL, got: %s", grantSQL) + } + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "RoleMembershipWithInheritFalseAndAdminOption": { + reason: "WithInherit false and WithOption ADMIN should produce WITH ADMIN OPTION, INHERIT FALSE", + fields: fields{ + serverVersion: 160000, + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + if len(ql) != 2 { + return fmt.Errorf("expected 2 queries, got %d", len(ql)) + } + grantSQL := ql[1].String + if !strings.Contains(grantSQL, "WITH ADMIN OPTION, INHERIT FALSE") { + return fmt.Errorf("expected WITH ADMIN OPTION, INHERIT FALSE in grant SQL, got: %s", grantSQL) + } + return nil + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithOption: &goa, + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + err: nil, + }, + }, + "ErrRoleMembershipWithInheritOnPG15": { + reason: "WithInherit on a server older than PostgreSQL 16 should return an error before any query is run", + fields: fields{ + serverVersion: 150000, + db: &mockDB{ + MockExecTx: func(ctx context.Context, ql []xsql.Query) error { + return fmt.Errorf("ExecTx should not be called when server version is below 16") + }, + }, + }, + args: args{ + mg: &v1alpha1.Grant{ + Spec: v1alpha1.GrantSpec{ + ForProvider: v1alpha1.GrantParameters{ + Role: ptr.To("testrole"), + MemberOf: ptr.To("parentrole"), + WithInherit: ptr.To(false), + }, + }, + }, + }, + want: want{ + err: errors.Wrap(errors.Errorf(errInheritRequiresPG16, 150000), errCreateGrant), + }, + }, } for name, tc := range cases { @@ -983,7 +1241,7 @@ func TestCreate(t *testing.T) { if db == nil { db = mockDB{} } - e := external{db: db} + e := external{db: db, serverVersion: tc.fields.serverVersion} got, err := e.Create(tc.args.ctx, tc.args.mg) if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { t.Errorf("\n%s\ne.Create(...): -want error, +got error:\n%s\n", tc.reason, diff)