diff --git a/cmd/initialise/verify_database.go b/cmd/initialise/verify_database.go index 3e3bea9ef..62fbd9b40 100644 --- a/cmd/initialise/verify_database.go +++ b/cmd/initialise/verify_database.go @@ -2,6 +2,7 @@ package initialise import ( "context" + "database/sql" _ "embed" "fmt" @@ -37,6 +38,17 @@ The user provided by flags needs privileges to func VerifyDatabase(databaseName string) func(context.Context, *database.DB) error { return func(ctx context.Context, db *database.DB) error { + var currentDatabase string + err := db.QueryRowContext(ctx, func(r *sql.Row) error { + return r.Scan(¤tDatabase) + }, "SELECT current_database()") + if err != nil { + return fmt.Errorf("unable to get current database: %w", err) + } + if currentDatabase == databaseName { + logging.WithFields("database", databaseName).Info("database is same as config.database.postgres.admin.ExistingDatabase, skipping creation") + return nil + } logging.WithFields("database", databaseName).Info("verify database") return exec(ctx, db, fmt.Sprintf(databaseStmt, databaseName), []string{dbAlreadyExistsCode}) diff --git a/cmd/initialise/verify_database_test.go b/cmd/initialise/verify_database_test.go index 1899605e4..62ae26186 100644 --- a/cmd/initialise/verify_database_test.go +++ b/cmd/initialise/verify_database_test.go @@ -1,8 +1,8 @@ package initialise import ( - "context" "database/sql" + "database/sql/driver" "errors" "testing" ) @@ -27,6 +27,9 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, + expectQuery("SELECT current_database()", nil, []string{"current_database"}, [][]driver.Value{ + {"postgres"}, + }), expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", sql.ErrTxDone), ), database: "zitadel", @@ -37,6 +40,9 @@ func Test_verifyDB(t *testing.T) { name: "doesn't exists, create successful", args: args{ db: prepareDB(t, + expectQuery("SELECT current_database()", nil, []string{"current_database"}, [][]driver.Value{ + {"postgres"}, + }), expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", nil), ), database: "zitadel", @@ -47,16 +53,31 @@ func Test_verifyDB(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, + expectQuery("SELECT current_database()", nil, []string{"current_database"}, [][]driver.Value{ + {"postgres"}, + }), expectExec("-- replace zitadel with the name of the database\nCREATE DATABASE \"zitadel\"", nil), ), database: "zitadel", }, targetErr: nil, }, + { + name: "same database, skip creation", + args: args{ + db: prepareDB(t, + expectQuery("SELECT current_database()", nil, []string{"current_database"}, [][]driver.Value{ + {"zitadel"}, + }), + ), + database: "zitadel", + }, + targetErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := VerifyDatabase(tt.args.database)(context.Background(), tt.args.db.db); !errors.Is(err, tt.targetErr) { + if err := VerifyDatabase(tt.args.database)(t.Context(), tt.args.db.db); !errors.Is(err, tt.targetErr) { t.Errorf("verifyDB() error = %v, want: %v", err, tt.targetErr) } if err := tt.args.db.mock.ExpectationsWereMet(); err != nil { diff --git a/cmd/initialise/verify_grant.go b/cmd/initialise/verify_grant.go index 27f0bd4d0..5f87135d7 100644 --- a/cmd/initialise/verify_grant.go +++ b/cmd/initialise/verify_grant.go @@ -2,6 +2,7 @@ package initialise import ( "context" + "database/sql" _ "embed" "fmt" @@ -32,6 +33,17 @@ Prerequisites: func VerifyGrant(databaseName, username string) func(context.Context, *database.DB) error { return func(ctx context.Context, db *database.DB) error { + var currentUser string + err := db.QueryRowContext(ctx, func(r *sql.Row) error { + return r.Scan(¤tUser) + }, "SELECT current_user") + if err != nil { + return fmt.Errorf("unable to get current user: %w", err) + } + if currentUser == username { + logging.WithFields("username", username).Info("config.database.postgres.user.username is same as config.database.postgres.admin.username, skipping grant") + return nil + } logging.WithFields("user", username, "database", databaseName).Info("verify grant") return exec(ctx, db, fmt.Sprintf(grantStmt, databaseName, username), nil) diff --git a/cmd/initialise/verify_grant_test.go b/cmd/initialise/verify_grant_test.go index 3cab9099f..f71fb1d04 100644 --- a/cmd/initialise/verify_grant_test.go +++ b/cmd/initialise/verify_grant_test.go @@ -3,11 +3,18 @@ package initialise import ( "context" "database/sql" + "database/sql/driver" "errors" "testing" ) func Test_verifyGrant(t *testing.T) { + err := ReadStmts() + if err != nil { + t.Errorf("unable to read stmts: %v", err) + t.FailNow() + } + type args struct { db db database string @@ -22,6 +29,9 @@ func Test_verifyGrant(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"admin"}, + }), expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", sql.ErrTxDone), ), database: "zitadel", @@ -33,6 +43,9 @@ func Test_verifyGrant(t *testing.T) { name: "correct", args: args{ db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"admin"}, + }), expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", nil), ), database: "zitadel", @@ -44,6 +57,9 @@ func Test_verifyGrant(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"admin"}, + }), expectExec("GRANT ALL ON DATABASE \"zitadel\" TO \"zitadel-user\"", nil), ), database: "zitadel", @@ -51,6 +67,19 @@ func Test_verifyGrant(t *testing.T) { }, targetErr: nil, }, + { + name: "same user, skip grant", + args: args{ + db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"zitadel-user"}, + }), + ), + database: "zitadel", + username: "zitadel-user", + }, + targetErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cmd/initialise/verify_user.go b/cmd/initialise/verify_user.go index 3adca93e5..bf6e98306 100644 --- a/cmd/initialise/verify_user.go +++ b/cmd/initialise/verify_user.go @@ -2,6 +2,7 @@ package initialise import ( "context" + "database/sql" _ "embed" "fmt" @@ -37,6 +38,17 @@ The user provided by flags needs privileges to func VerifyUser(username, password string) func(context.Context, *database.DB) error { return func(ctx context.Context, db *database.DB) error { + var currentUser string + err := db.QueryRowContext(ctx, func(r *sql.Row) error { + return r.Scan(¤tUser) + }, "SELECT current_user") + if err != nil { + return fmt.Errorf("unable to get current user: %w", err) + } + if currentUser == username { + logging.WithFields("username", username).Info("config.database.postgres.user.username is same as config.database.postgres.admin.username, skipping create user") + return nil + } logging.WithFields("username", username).Info("verify user") if password != "" { diff --git a/cmd/initialise/verify_user_test.go b/cmd/initialise/verify_user_test.go index 40cde5baa..442543265 100644 --- a/cmd/initialise/verify_user_test.go +++ b/cmd/initialise/verify_user_test.go @@ -3,6 +3,7 @@ package initialise import ( "context" "database/sql" + "database/sql/driver" "errors" "testing" ) @@ -28,6 +29,9 @@ func Test_verifyUser(t *testing.T) { name: "doesn't exists, create fails", args: args{ db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"postgres"}, + }), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", sql.ErrTxDone), ), username: "zitadel-user", @@ -39,6 +43,9 @@ func Test_verifyUser(t *testing.T) { name: "correct without password", args: args{ db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"postgres"}, + }), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\"", nil), ), username: "zitadel-user", @@ -50,6 +57,9 @@ func Test_verifyUser(t *testing.T) { name: "correct with password", args: args{ db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"postgres"}, + }), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", @@ -61,6 +71,9 @@ func Test_verifyUser(t *testing.T) { name: "already exists", args: args{ db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"postgres"}, + }), expectExec("-- replace zitadel-user with the name of the user\nCREATE USER \"zitadel-user\" WITH PASSWORD 'password'", nil), ), username: "zitadel-user", @@ -68,11 +81,23 @@ func Test_verifyUser(t *testing.T) { }, targetErr: nil, }, + { + name: "same user, skip create", + args: args{ + db: prepareDB(t, + expectQuery("SELECT current_user", nil, []string{"current_user"}, [][]driver.Value{ + {"zitadel-user"}, + }), + ), + username: "zitadel-user", + }, + targetErr: nil, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := VerifyUser(tt.args.username, tt.args.password)(context.Background(), tt.args.db.db); !errors.Is(err, tt.targetErr) { - t.Errorf("VerifyGrant() error = %v, want: %v", err, tt.targetErr) + t.Errorf("VerifyUser() error = %v, want: %v", err, tt.targetErr) } if err := tt.args.db.mock.ExpectationsWereMet(); err != nil { t.Error(err) diff --git a/docs/docs/self-hosting/manage/configure/configure.mdx b/docs/docs/self-hosting/manage/configure/configure.mdx index c68f716d6..621a71b35 100644 --- a/docs/docs/self-hosting/manage/configure/configure.mdx +++ b/docs/docs/self-hosting/manage/configure/configure.mdx @@ -12,6 +12,7 @@ import Login from "./_login.md"; import CodeBlock from "@theme/CodeBlock"; import DefaultsYamlSource from "!!raw-loader!./defaults.yaml"; import StepsYamlSource from "!!raw-loader!./steps.yaml"; +import SkipSuperUserDatabaseQueries from "!!raw-loader!./example-skip-superuser-queries.yaml"; This guide assumes you are familiar with [running ZITADEL using the least amount of configuration possible](/docs/self-hosting/deploy/overview). @@ -47,6 +48,18 @@ By using the FirstInstance section, you can overwrite the [DefaultInstance confi {StepsYamlSource} +### Pre-existing Database and User + +By default, when you run `zitadel init` or `zitadel start-from-init`, ZITADEL checks if the specified database and user exist. If they don't, ZITADEL creates them and grants the necessary permissions. + +If you manage the database and user manually, you can instruct ZITADEL to skip these checks. This is useful if the ZITADEL user has limited database permissions, for example, if it is not a `SUPERUSER`. This scenario is most common on cloud providers. +To skip the checks, you must ensure the database objects (database, user, grant) exist before running the commands and then set the `Database.postgres.Admin.ExistingDatabase` field in your configuration as shown below. This tells ZITADEL to connect to the existing database as an admin user, bypassing the creation and permission steps. + +
+ skip-superuser-queries-config.yaml + {SkipSuperUserDatabaseQueries} +
+ ### Multiple configuration files ZITADEL merges configuration files when multiple `--config` and `--steps` flags are provided. diff --git a/docs/docs/self-hosting/manage/configure/example-skip-superuser-queries.yaml b/docs/docs/self-hosting/manage/configure/example-skip-superuser-queries.yaml new file mode 100644 index 000000000..26e85f9d9 --- /dev/null +++ b/docs/docs/self-hosting/manage/configure/example-skip-superuser-queries.yaml @@ -0,0 +1,10 @@ +Database: + postgres: + Database: existing_database + # Admin credentials should be the same as the application user credentials + Admin: + Username: zitadel-user + ExistingDatabase: existing_database # this is used to connect the admin to the previously created database + # Application user credentials + User: + Username: zitadel-user