Skip to content

Commit b146cfe

Browse files
committed
postgresgrant: New controller, supporting PostgreSQL GRANTs.
Preliminary support for GRANTs. Note that this is currently experimental, and operates in non-authoritative mode. It will only GRANT but not REVOKE previously granted privileges.
1 parent 3e18647 commit b146cfe

30 files changed

+1425
-20
lines changed

PROJECT

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,16 @@ resources:
2525
kind: PostgresPublication
2626
path: github.com/glints-dev/postgres-config-operator/api/v1alpha1
2727
version: v1alpha1
28+
- api:
29+
crdVersion: v1
30+
namespaced: true
31+
controller: true
32+
domain: glints.com
33+
group: postgres
34+
kind: PostgresGrant
35+
path: github.com/glints-dev/postgres-config-operator/api/v1alpha1
36+
version: v1alpha1
37+
webhooks:
38+
validation: true
39+
webhookVersion: v1
2840
version: "3"

api/v1alpha1/postgresgrant_types.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
Copyright 2021.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha1
18+
19+
import (
20+
"github.com/jackc/pgx/v4"
21+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
22+
)
23+
24+
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
25+
26+
// PostgresGrantSpec defines the desired state of PostgresGrant
27+
type PostgresGrantSpec struct {
28+
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
29+
// Important: Run "make" to regenerate code after modifying this file
30+
31+
// PostgresRef is a reference to the PostgreSQL server to configure
32+
PostgresRef PostgresRef `json:"postgresRef"`
33+
34+
// Role to grant privileges to
35+
Role string `json:"role"`
36+
37+
// Tables is the list of tables to grant privileges for
38+
Tables []PostgresIdentifier `json:"tables,omitempty"`
39+
40+
// TablePrivileges is the list of privileges to grant
41+
// Must be one of: SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER, ALL
42+
TablePrivileges []string `json:"tablePrivileges,omitempty"`
43+
44+
// Sequences is the list of sequences to grant privileges for
45+
Sequences []PostgresIdentifier `json:"sequences,omitempty"`
46+
47+
// SequencePrivileges is the list of privileges to grant
48+
// Must be one of: USAGE, SELECT, UPDATE, ALL
49+
SequencePrivileges []string `json:"sequencePrivileges,omitempty"`
50+
51+
// Databases is the list of databases to grant privileges for
52+
Databases []string `json:"databases,omitempty"`
53+
54+
// DatabasePrivileges is the list of privileges to grant
55+
// Must be one of: CREATE, CONNECT, TEMPORARY, TEMP, ALL
56+
DatabasePrivileges []string `json:"databasePrivileges,omitempty"`
57+
58+
// Schemas is the list of schemas to grant privileges for
59+
Schemas []string `json:"schemas,omitempty"`
60+
61+
// SchemaPrivileges is the list of privileges to grant
62+
// Must be one of: CREATE, USAGE, ALL
63+
SchemaPrivileges []string `json:"schemaPrivileges,omitempty"`
64+
65+
// Functions is the list of functions to grant privileges for
66+
Functions []PostgresIdentifier `json:"functions,omitempty"`
67+
68+
// FunctionPrivileges is the list of privileges to grant
69+
// Must be one of: EXECUTE, ALL
70+
FunctionPrivileges []string `json:"functionPrivileges,omitempty"`
71+
}
72+
73+
// PostgresGrantStatus defines the observed state of PostgresGrant
74+
type PostgresGrantStatus struct {
75+
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
76+
// Important: Run "make" to regenerate code after modifying this file
77+
}
78+
79+
//+kubebuilder:object:root=true
80+
//+kubebuilder:subresource:status
81+
82+
// PostgresGrant is the Schema for the postgresgrants API
83+
type PostgresGrant struct {
84+
metav1.TypeMeta `json:",inline"`
85+
metav1.ObjectMeta `json:"metadata,omitempty"`
86+
87+
Spec PostgresGrantSpec `json:"spec,omitempty"`
88+
Status PostgresGrantStatus `json:"status,omitempty"`
89+
}
90+
91+
// PrivilegesForType returns the list of privileges defined for a given object
92+
// type within the grant. If the object type is invalid, nil is returned.
93+
func (g *PostgresGrant) PrivilegesForType(o ObjectType) []string {
94+
switch o {
95+
case ObjectTypeTable:
96+
return g.Spec.TablePrivileges
97+
case ObjectTypeSequence:
98+
return g.Spec.SequencePrivileges
99+
case ObjectTypeFunction:
100+
return g.Spec.FunctionPrivileges
101+
case ObjectTypeSchema:
102+
return g.Spec.SchemaPrivileges
103+
case ObjectTypeDatabase:
104+
return g.Spec.DatabasePrivileges
105+
}
106+
107+
return nil
108+
}
109+
110+
// IdentifiersForType returns the list of identifiers defined for a given object
111+
// type within the grant. If the object type is invalid, nil is returned.
112+
func (g *PostgresGrant) IdentifiersForType(o ObjectType) []pgx.Identifier {
113+
var identifiers []pgx.Identifier
114+
switch o {
115+
case ObjectTypeTable:
116+
for _, table := range g.Spec.Tables {
117+
identifiers = append(identifiers, pgx.Identifier{table.Schema, table.Name})
118+
}
119+
case ObjectTypeSequence:
120+
for _, sequence := range g.Spec.Sequences {
121+
identifiers = append(identifiers, pgx.Identifier{sequence.Schema, sequence.Name})
122+
}
123+
case ObjectTypeFunction:
124+
for _, function := range g.Spec.Functions {
125+
identifiers = append(identifiers, pgx.Identifier{function.Schema, function.Name})
126+
}
127+
case ObjectTypeSchema:
128+
for _, schema := range g.Spec.Schemas {
129+
identifiers = append(identifiers, pgx.Identifier{schema})
130+
}
131+
case ObjectTypeDatabase:
132+
for _, database := range g.Spec.Databases {
133+
identifiers = append(identifiers, pgx.Identifier{database})
134+
}
135+
}
136+
137+
return identifiers
138+
}
139+
140+
//+kubebuilder:object:root=true
141+
142+
// PostgresGrantList contains a list of PostgresGrant
143+
type PostgresGrantList struct {
144+
metav1.TypeMeta `json:",inline"`
145+
metav1.ListMeta `json:"metadata,omitempty"`
146+
Items []PostgresGrant `json:"items"`
147+
}
148+
149+
func init() {
150+
SchemeBuilder.Register(&PostgresGrant{}, &PostgresGrantList{})
151+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package v1alpha1
2+
3+
import (
4+
"net/http"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
"k8s.io/apimachinery/pkg/api/errors"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
)
11+
12+
var _ = Describe("PostgresGrant: Webhooks", func() {
13+
type testCase struct {
14+
Name string
15+
Spec PostgresGrantSpec
16+
ErrExpected bool
17+
ExpectedCode int32
18+
ExpectedReason metav1.StatusReason
19+
}
20+
21+
testCases := []testCase{
22+
{
23+
Name: "should fail to validate if ALL is specified along other operations",
24+
Spec: PostgresGrantSpec{
25+
Tables: []PostgresIdentifier{
26+
{Name: "jobs", Schema: "public"},
27+
},
28+
TablePrivileges: []string{"SELECT", "ALL"},
29+
},
30+
ExpectedCode: http.StatusForbidden,
31+
ExpectedReason: "\"ALL\" cannot be specified alongside other privileges",
32+
},
33+
{
34+
Name: "should fail to validate with duplicate table privileges",
35+
Spec: PostgresGrantSpec{
36+
Tables: []PostgresIdentifier{
37+
{Name: "jobs", Schema: "public"},
38+
},
39+
TablePrivileges: []string{"SELECT", "SELECT"},
40+
},
41+
ExpectedCode: http.StatusForbidden,
42+
ExpectedReason: "duplicate table privilege \"SELECT\"",
43+
},
44+
{
45+
Name: "should fail to validate with invalid table privileges",
46+
Spec: PostgresGrantSpec{
47+
Tables: []PostgresIdentifier{
48+
{Name: "jobs", Schema: "public"},
49+
},
50+
TablePrivileges: []string{"DUMMY"},
51+
},
52+
ExpectedCode: http.StatusForbidden,
53+
ExpectedReason: "invalid table privilege \"DUMMY\"",
54+
},
55+
}
56+
57+
It("should succeed validation with valid privilege declaration", func() {
58+
59+
grant := &PostgresGrant{
60+
ObjectMeta: metav1.ObjectMeta{
61+
Name: "grant-test",
62+
Namespace: "default",
63+
},
64+
Spec: PostgresGrantSpec{
65+
Tables: []PostgresIdentifier{
66+
{Name: "jobs", Schema: "public"},
67+
},
68+
TablePrivileges: []string{"SELECT", "UPDATE"},
69+
},
70+
}
71+
72+
err := k8sClient.Create(ctx, grant)
73+
Expect(err).NotTo(HaveOccurred())
74+
})
75+
76+
for _, testCase := range testCases {
77+
testCase := testCase
78+
It(testCase.Name, func() {
79+
grant := &PostgresGrant{
80+
ObjectMeta: metav1.ObjectMeta{
81+
Name: "grant-test",
82+
Namespace: "default",
83+
},
84+
Spec: testCase.Spec,
85+
}
86+
87+
err := k8sClient.Create(ctx, grant)
88+
Expect(err).To(HaveOccurred())
89+
90+
statusErr, ok := err.(*errors.StatusError)
91+
Expect(ok).To(BeTrue())
92+
93+
Expect(statusErr.ErrStatus.Code).To(BeEquivalentTo(testCase.ExpectedCode))
94+
Expect(statusErr.ErrStatus.Reason).To(Equal(testCase.ExpectedReason))
95+
})
96+
}
97+
})

0 commit comments

Comments
 (0)