Skip to content

Commit db71a5e

Browse files
FIX (databases): Add support dashed databases for read only users creation
1 parent df78e29 commit db71a5e

File tree

2 files changed

+78
-3
lines changed

2 files changed

+78
-3
lines changed

backend/internal/features/databases/databases/postgresql/model.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,22 +424,22 @@ func (p *PostgresqlDatabase) CreateReadOnlyUser(
424424
// Step 2: Grant database connection privilege and revoke TEMP
425425
_, err = tx.Exec(
426426
ctx,
427-
fmt.Sprintf(`GRANT CONNECT ON DATABASE %s TO "%s"`, *p.Database, baseUsername),
427+
fmt.Sprintf(`GRANT CONNECT ON DATABASE "%s" TO "%s"`, *p.Database, baseUsername),
428428
)
429429
if err != nil {
430430
return "", "", fmt.Errorf("failed to grant connect privilege: %w", err)
431431
}
432432

433433
// Revoke TEMP privilege from PUBLIC role (like CREATE on public schema, TEMP is granted to PUBLIC by default)
434-
_, err = tx.Exec(ctx, fmt.Sprintf(`REVOKE TEMP ON DATABASE %s FROM PUBLIC`, *p.Database))
434+
_, err = tx.Exec(ctx, fmt.Sprintf(`REVOKE TEMP ON DATABASE "%s" FROM PUBLIC`, *p.Database))
435435
if err != nil {
436436
logger.Warn("Failed to revoke TEMP from PUBLIC", "error", err)
437437
}
438438

439439
// Also revoke from the specific user (belt and suspenders)
440440
_, err = tx.Exec(
441441
ctx,
442-
fmt.Sprintf(`REVOKE TEMP ON DATABASE %s FROM "%s"`, *p.Database, baseUsername),
442+
fmt.Sprintf(`REVOKE TEMP ON DATABASE "%s" FROM "%s"`, *p.Database, baseUsername),
443443
)
444444
if err != nil {
445445
logger.Warn("Failed to revoke TEMP privilege", "error", err, "username", baseUsername)

backend/internal/features/databases/databases/postgresql/readonly_user_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,81 @@ func Test_ReadOnlyUser_MultipleSchemas_AllAccessible(t *testing.T) {
246246
assert.NoError(t, err)
247247
}
248248

249+
func Test_CreateReadOnlyUser_DatabaseNameWithDash_Success(t *testing.T) {
250+
env := config.GetEnv()
251+
container := connectToPostgresContainer(t, env.TestPostgres16Port)
252+
defer container.DB.Close()
253+
254+
dashDbName := "test-db-with-dash"
255+
256+
_, err := container.DB.Exec(fmt.Sprintf(`DROP DATABASE IF EXISTS "%s"`, dashDbName))
257+
assert.NoError(t, err)
258+
259+
_, err = container.DB.Exec(fmt.Sprintf(`CREATE DATABASE "%s"`, dashDbName))
260+
assert.NoError(t, err)
261+
262+
defer func() {
263+
_, _ = container.DB.Exec(fmt.Sprintf(`DROP DATABASE IF EXISTS "%s"`, dashDbName))
264+
}()
265+
266+
dashDSN := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
267+
container.Host, container.Port, container.Username, container.Password, dashDbName)
268+
dashDB, err := sqlx.Connect("postgres", dashDSN)
269+
assert.NoError(t, err)
270+
defer dashDB.Close()
271+
272+
_, err = dashDB.Exec(`
273+
CREATE TABLE dash_test (
274+
id SERIAL PRIMARY KEY,
275+
data TEXT NOT NULL
276+
);
277+
INSERT INTO dash_test (data) VALUES ('test1'), ('test2');
278+
`)
279+
assert.NoError(t, err)
280+
281+
pgModel := &PostgresqlDatabase{
282+
Version: tools.GetPostgresqlVersionEnum("16"),
283+
Host: container.Host,
284+
Port: container.Port,
285+
Username: container.Username,
286+
Password: container.Password,
287+
Database: &dashDbName,
288+
IsHttps: false,
289+
}
290+
291+
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
292+
ctx := context.Background()
293+
294+
username, password, err := pgModel.CreateReadOnlyUser(ctx, logger, nil, uuid.New())
295+
assert.NoError(t, err)
296+
assert.NotEmpty(t, username)
297+
assert.NotEmpty(t, password)
298+
assert.True(t, strings.HasPrefix(username, "postgresus-"))
299+
300+
readOnlyDSN := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
301+
container.Host, container.Port, username, password, dashDbName)
302+
readOnlyConn, err := sqlx.Connect("postgres", readOnlyDSN)
303+
assert.NoError(t, err)
304+
defer readOnlyConn.Close()
305+
306+
var count int
307+
err = readOnlyConn.Get(&count, "SELECT COUNT(*) FROM dash_test")
308+
assert.NoError(t, err)
309+
assert.Equal(t, 2, count)
310+
311+
_, err = readOnlyConn.Exec("INSERT INTO dash_test (data) VALUES ('should-fail')")
312+
assert.Error(t, err)
313+
assert.Contains(t, err.Error(), "permission denied")
314+
315+
_, err = dashDB.Exec(fmt.Sprintf(`DROP OWNED BY "%s" CASCADE`, username))
316+
if err != nil {
317+
t.Logf("Warning: Failed to drop owned objects: %v", err)
318+
}
319+
320+
_, err = dashDB.Exec(fmt.Sprintf(`DROP USER IF EXISTS "%s"`, username))
321+
assert.NoError(t, err)
322+
}
323+
249324
func Test_CreateReadOnlyUser_Supabase_UserCanReadButNotWrite(t *testing.T) {
250325
env := config.GetEnv()
251326

0 commit comments

Comments
 (0)