Skip to content

Commit 990631f

Browse files
committed
Reject invalid Postgres user specs
The PASSWORD option was never effective. CEL validation rules, available in beta since Kubernetes 1.25, can detect and reject values that the RE2 pattern validation of "github.com/go-openapi" could not. Issue: PGO-1094 See: https://pkg.go.dev/github.com/go-openapi/validate@v0.24.0#hdr-Known_limitations
1 parent 88ac6e6 commit 990631f

File tree

5 files changed

+149
-0
lines changed

5 files changed

+149
-0
lines changed

config/crd/bases/postgres-operator.crunchydata.com_postgresclusters.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15080,8 +15080,14 @@ spec:
1508015080
options:
1508115081
description: 'ALTER ROLE options except for PASSWORD. This field
1508215082
is ignored for the "postgres" user. More info: https://www.postgresql.org/docs/current/role-attributes.html'
15083+
maxLength: 200
1508315084
pattern: ^[^;]*$
1508415085
type: string
15086+
x-kubernetes-validations:
15087+
- message: cannot assign password
15088+
rule: '!self.matches("(?i:PASSWORD)")'
15089+
- message: cannot contain comments
15090+
rule: '!self.matches("(?:--|/[*]|[*]/)")'
1508515091
password:
1508615092
description: Properties of the password generated for this user.
1508715093
properties:
@@ -15102,6 +15108,7 @@ spec:
1510215108
required:
1510315109
- name
1510415110
type: object
15111+
maxItems: 64
1510515112
type: array
1510615113
x-kubernetes-list-map-keys:
1510715114
- name

internal/controller/postgrescluster/postgres_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,8 @@ func TestValidatePostgresUsers(t *testing.T) {
679679
assert.Equal(t, len(recorder.Events), 0)
680680
})
681681

682+
// See [internal/testing/validation.TestPostgresUserOptions]
683+
682684
t.Run("NoComments", func(t *testing.T) {
683685
cluster := v1beta1.NewPostgresCluster()
684686
cluster.Name = "pg1"
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*
2+
Copyright 2021 - 2024 Crunchy Data Solutions, Inc.
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
*/
15+
16+
package validation
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"testing"
22+
23+
"gotest.tools/v3/assert"
24+
apierrors "k8s.io/apimachinery/pkg/api/errors"
25+
"sigs.k8s.io/controller-runtime/pkg/client"
26+
"sigs.k8s.io/yaml"
27+
28+
"github.com/crunchydata/postgres-operator/internal/testing/cmp"
29+
"github.com/crunchydata/postgres-operator/internal/testing/require"
30+
"github.com/crunchydata/postgres-operator/pkg/apis/postgres-operator.crunchydata.com/v1beta1"
31+
)
32+
33+
func TestPostgresUserOptions(t *testing.T) {
34+
ctx := context.Background()
35+
cc := require.Kubernetes(t)
36+
t.Parallel()
37+
38+
namespace := require.Namespace(t, cc)
39+
base := v1beta1.NewPostgresCluster()
40+
41+
// Start with a bunch of required fields.
42+
assert.NilError(t, yaml.Unmarshal([]byte(`{
43+
postgresVersion: 16,
44+
backups: {
45+
pgbackrest: {
46+
repos: [{ name: repo1 }],
47+
},
48+
},
49+
instances: [{
50+
dataVolumeClaimSpec: {
51+
accessModes: [ReadWriteOnce],
52+
resources: { requests: { storage: 1Mi } },
53+
},
54+
}],
55+
}`), &base.Spec))
56+
57+
base.Namespace = namespace.Name
58+
base.Name = "postgres-user-options"
59+
60+
assert.NilError(t, cc.Create(ctx, base.DeepCopy(), client.DryRunAll),
61+
"expected this base cluster to be valid")
62+
63+
// See [internal/controller/postgrescluster.TestValidatePostgresUsers]
64+
65+
t.Run("NoComments", func(t *testing.T) {
66+
cluster := base.DeepCopy()
67+
cluster.Spec.Users = []v1beta1.PostgresUserSpec{
68+
{Name: "dashes", Options: "ANY -- comment"},
69+
{Name: "block-open", Options: "/* asdf"},
70+
{Name: "block-close", Options: " qw */ rt"},
71+
}
72+
73+
err := cc.Create(ctx, cluster, client.DryRunAll)
74+
assert.Assert(t, apierrors.IsInvalid(err))
75+
assert.ErrorContains(t, err, "cannot contain comments")
76+
77+
//nolint:errorlint // This is a test, and a panic is unlikely.
78+
status := err.(apierrors.APIStatus).Status()
79+
assert.Assert(t, status.Details != nil)
80+
assert.Equal(t, len(status.Details.Causes), 3)
81+
82+
for i, cause := range status.Details.Causes {
83+
assert.Equal(t, cause.Field, fmt.Sprintf("spec.users[%d].options", i))
84+
assert.Assert(t, cmp.Contains(cause.Message, "cannot contain comments"))
85+
}
86+
})
87+
88+
t.Run("NoPassword", func(t *testing.T) {
89+
cluster := base.DeepCopy()
90+
cluster.Spec.Users = []v1beta1.PostgresUserSpec{
91+
{Name: "uppercase", Options: "SUPERUSER PASSWORD ''"},
92+
{Name: "lowercase", Options: "password 'asdf'"},
93+
}
94+
95+
err := cc.Create(ctx, cluster, client.DryRunAll)
96+
assert.Assert(t, apierrors.IsInvalid(err))
97+
assert.ErrorContains(t, err, "cannot assign password")
98+
99+
//nolint:errorlint // This is a test, and a panic is unlikely.
100+
status := err.(apierrors.APIStatus).Status()
101+
assert.Assert(t, status.Details != nil)
102+
assert.Equal(t, len(status.Details.Causes), 2)
103+
104+
for i, cause := range status.Details.Causes {
105+
assert.Equal(t, cause.Field, fmt.Sprintf("spec.users[%d].options", i))
106+
assert.Assert(t, cmp.Contains(cause.Message, "cannot assign password"))
107+
}
108+
})
109+
110+
t.Run("NoTerminators", func(t *testing.T) {
111+
cluster := base.DeepCopy()
112+
cluster.Spec.Users = []v1beta1.PostgresUserSpec{
113+
{Name: "semicolon", Options: "some ;where"},
114+
}
115+
116+
err := cc.Create(ctx, cluster, client.DryRunAll)
117+
assert.Assert(t, apierrors.IsInvalid(err))
118+
assert.ErrorContains(t, err, "should match")
119+
120+
//nolint:errorlint // This is a test, and a panic is unlikely.
121+
status := err.(apierrors.APIStatus).Status()
122+
assert.Assert(t, status.Details != nil)
123+
assert.Equal(t, len(status.Details.Causes), 1)
124+
assert.Equal(t, status.Details.Causes[0].Field, "spec.users[0].options")
125+
})
126+
127+
t.Run("Valid", func(t *testing.T) {
128+
cluster := base.DeepCopy()
129+
cluster.Spec.Users = []v1beta1.PostgresUserSpec{
130+
{Name: "normal", Options: "CREATEDB valid until '2006-01-02'"},
131+
{Name: "very-full", Options: "NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOLOGIN NOREPLICATION NOBYPASSRLS CONNECTION LIMIT 5"},
132+
}
133+
134+
assert.NilError(t, cc.Create(ctx, cluster, client.DryRunAll))
135+
})
136+
}

pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgres_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ type PostgresUserSpec struct {
6060
// ALTER ROLE options except for PASSWORD. This field is ignored for the
6161
// "postgres" user.
6262
// More info: https://www.postgresql.org/docs/current/role-attributes.html
63+
// +kubebuilder:validation:MaxLength=200
6364
// +kubebuilder:validation:Pattern=`^[^;]*$`
65+
// +kubebuilder:validation:XValidation:rule=`!self.matches("(?i:PASSWORD)")`,message="cannot assign password"
66+
// +kubebuilder:validation:XValidation:rule=`!self.matches("(?:--|/[*]|[*]/)")`,message="cannot contain comments"
6467
// +optional
6568
Options string `json:"options,omitempty"`
6669

pkg/apis/postgres-operator.crunchydata.com/v1beta1/postgrescluster_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ type PostgresClusterSpec struct {
175175
// from this list does NOT drop the user nor revoke their access.
176176
// +listType=map
177177
// +listMapKey=name
178+
// +kubebuilder:validation:MaxItems=64
178179
// +optional
179180
Users []PostgresUserSpec `json:"users,omitempty"`
180181

0 commit comments

Comments
 (0)