Skip to content

Commit f5d54fb

Browse files
authored
fix: grant PostgREST role switching on PostgreSQL 16+ (#2071)
1 parent 94128d2 commit f5d54fb

3 files changed

Lines changed: 150 additions & 0 deletions

File tree

functions/postgrest.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,14 @@ BEGIN
1212
GRANT SELECT ON ALL TABLES IN SCHEMA public TO postgrest_anon;
1313
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO postgrest_anon;
1414
END IF;
15+
16+
IF current_setting('server_version_num')::int >= 160000
17+
AND NOT EXISTS (
18+
SELECT FROM pg_catalog.pg_roles
19+
WHERE rolname = current_user AND rolsuper
20+
)
21+
THEN
22+
EXECUTE format('GRANT %I TO %I WITH SET TRUE, INHERIT FALSE', 'postgrest_api', current_user);
23+
EXECUTE format('GRANT %I TO %I WITH SET TRUE, INHERIT FALSE', 'postgrest_anon', current_user);
24+
END IF;
1525
END $$;

migrate/postgrest_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package migrate
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"fmt"
7+
"io"
8+
"net"
9+
"path/filepath"
10+
"time"
11+
12+
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
13+
"github.com/flanksource/duty/api"
14+
"github.com/flanksource/duty/functions"
15+
_ "github.com/lib/pq"
16+
ginkgo "github.com/onsi/ginkgo/v2"
17+
. "github.com/onsi/gomega"
18+
)
19+
20+
var _ = ginkgo.Describe("PostgREST role bootstrap", func() {
21+
versions := []struct {
22+
name string
23+
version embeddedpostgres.PostgresVersion
24+
}{
25+
{name: "PostgreSQL 15", version: embeddedpostgres.V15},
26+
{name: "PostgreSQL 17", version: embeddedpostgres.V17},
27+
}
28+
29+
for _, version := range versions {
30+
ginkgo.It("allows a CREATEROLE user to SET ROLE on "+version.name, ginkgo.Label("slow"), func() {
31+
listener, err := net.Listen("tcp", "127.0.0.1:0")
32+
Expect(err).NotTo(HaveOccurred())
33+
port := uint32(listener.Addr().(*net.TCPAddr).Port)
34+
Expect(listener.Close()).To(Succeed())
35+
36+
postgres := embeddedpostgres.NewDatabase(embeddedpostgres.DefaultConfig().
37+
Version(version.version).
38+
Port(port).
39+
Username("postgres").
40+
Password("postgres").
41+
Database("postgres").
42+
RuntimePath(filepath.Join(ginkgo.GinkgoT().TempDir(), "postgres")).
43+
StartTimeout(time.Minute).
44+
Logger(io.Discard))
45+
Expect(postgres.Start()).To(Succeed())
46+
defer func() {
47+
Expect(postgres.Stop()).To(Succeed())
48+
}()
49+
50+
adminDB, err := sql.Open("postgres", fmt.Sprintf("postgres://postgres:postgres@localhost:%d/postgres?sslmode=disable", port))
51+
Expect(err).NotTo(HaveOccurred())
52+
defer adminDB.Close()
53+
54+
_, err = adminDB.Exec(`
55+
CREATE ROLE duty_bootstrap LOGIN PASSWORD 'duty_bootstrap' CREATEROLE;
56+
GRANT CREATE ON DATABASE postgres TO duty_bootstrap;
57+
GRANT USAGE, CREATE ON SCHEMA public TO duty_bootstrap;
58+
`)
59+
Expect(err).NotTo(HaveOccurred())
60+
61+
bootstrapURL := fmt.Sprintf("postgres://duty_bootstrap:duty_bootstrap@localhost:%d/postgres?sslmode=disable", port)
62+
bootstrapDB, err := sql.Open("postgres", bootstrapURL)
63+
Expect(err).NotTo(HaveOccurred())
64+
defer bootstrapDB.Close()
65+
66+
scripts, err := functions.GetFunctions()
67+
Expect(err).NotTo(HaveOccurred())
68+
postgrestSQL, ok := scripts["postgrest.sql"]
69+
Expect(ok).To(BeTrue())
70+
config := api.Config{
71+
ConnectionString: bootstrapURL,
72+
Postgrest: api.PostgrestConfig{
73+
DBRole: "postgrest_api",
74+
AnonDBRole: "postgrest_anon",
75+
},
76+
}
77+
78+
for range 2 {
79+
_, err = bootstrapDB.Exec(postgrestSQL)
80+
Expect(err).NotTo(HaveOccurred())
81+
Expect(grantPostgrestRolesToCurrentUser(bootstrapDB, config)).To(Succeed())
82+
}
83+
84+
conn, err := bootstrapDB.Conn(context.Background())
85+
Expect(err).NotTo(HaveOccurred())
86+
defer conn.Close()
87+
for _, role := range []string{"postgrest_api", "postgrest_anon"} {
88+
_, err = conn.ExecContext(context.Background(), "SET ROLE "+role)
89+
Expect(err).NotTo(HaveOccurred())
90+
var currentRole string
91+
Expect(conn.QueryRowContext(context.Background(), "SELECT current_role").Scan(&currentRole)).To(Succeed())
92+
Expect(currentRole).To(Equal(role))
93+
_, err = conn.ExecContext(context.Background(), "RESET ROLE")
94+
Expect(err).NotTo(HaveOccurred())
95+
}
96+
97+
if version.version == embeddedpostgres.V17 {
98+
for _, role := range []string{"postgrest_api", "postgrest_anon"} {
99+
var adminOption, setOption, inheritOption bool
100+
Expect(adminDB.QueryRow(`
101+
SELECT bool_or(admin_option), bool_or(set_option), bool_or(inherit_option)
102+
FROM pg_auth_members membership
103+
JOIN pg_roles granted_role ON granted_role.oid = membership.roleid
104+
JOIN pg_roles member_role ON member_role.oid = membership.member
105+
WHERE granted_role.rolname = $1 AND member_role.rolname = 'duty_bootstrap'
106+
`, role).Scan(&adminOption, &setOption, &inheritOption)).To(Succeed())
107+
Expect(adminOption).To(BeTrue())
108+
Expect(setOption).To(BeTrue())
109+
Expect(inheritOption).To(BeFalse())
110+
}
111+
}
112+
113+
_, err = adminDB.Exec(postgrestSQL)
114+
Expect(err).NotTo(HaveOccurred())
115+
var superuserMemberships int
116+
Expect(adminDB.QueryRow(`
117+
SELECT count(*)
118+
FROM pg_auth_members membership
119+
JOIN pg_roles granted_role ON granted_role.oid = membership.roleid
120+
JOIN pg_roles member_role ON member_role.oid = membership.member
121+
WHERE granted_role.rolname IN ('postgrest_api', 'postgrest_anon')
122+
AND member_role.rolname = 'postgres'
123+
`).Scan(&superuserMemberships)).To(Succeed())
124+
Expect(superuserMemberships).To(BeZero())
125+
})
126+
}
127+
})

migrate/suite_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package migrate
2+
3+
import (
4+
"testing"
5+
6+
ginkgo "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestMigrate(t *testing.T) {
11+
RegisterFailHandler(ginkgo.Fail)
12+
ginkgo.RunSpecs(t, "Migrate Suite")
13+
}

0 commit comments

Comments
 (0)