From 4272ea6fe1954a39edf80750d66d9f40b0ef721a Mon Sep 17 00:00:00 2001 From: Silvan Date: Wed, 16 Feb 2022 13:30:49 +0100 Subject: [PATCH] fix: init sub commands (#3218) * fix(init): add sub commands * fix(init): admin user in config, test(init): verify functions * refactor: config, remove second commands * refactor: init steps * chore: fix link in readme * chore: numerate sql files * Update cmd/admin/initialise/sql/README.md Co-authored-by: Livio Amstutz * Update cmd/admin/initialise/sql/README.md Co-authored-by: Livio Amstutz * fix(init): remove unused index * user * fix database username in defaults.yaml Co-authored-by: Livio Amstutz --- cmd/admin/initialise/config.go | 21 +++- cmd/admin/initialise/helper.go | 28 +++++ cmd/admin/initialise/init.go | 57 +++------ cmd/admin/initialise/init_test.go | 82 +++++++++++++ cmd/admin/initialise/sql/01_user.sql | 1 + cmd/admin/initialise/sql/02_database.sql | 2 + cmd/admin/initialise/sql/03_grant_user.sql | 3 + cmd/admin/initialise/sql/04_eventstore.sql | 1 + cmd/admin/initialise/sql/05_projections.sql | 1 + .../sql/06_enable_hash_sharded_indexes.sql | 1 + cmd/admin/initialise/sql/07_events_table.sql | 24 ++++ cmd/admin/initialise/sql/README.md | 14 +++ cmd/admin/initialise/verify_database.go | 39 +++---- cmd/admin/initialise/verify_database_test.go | 80 +++++++++++++ cmd/admin/initialise/verify_grant.go | 38 +++--- cmd/admin/initialise/verify_grant_test.go | 92 +++++++++++++++ cmd/admin/initialise/verify_user.go | 37 +++--- cmd/admin/initialise/verify_user_test.go | 109 ++++++++++++++++++ cmd/admin/initialise/verify_zitadel.go | 91 ++++----------- cmd/admin/initialise/verify_zitadel_test.go | 73 ++++++++++++ cmd/defaults.yaml | 18 +-- internal/database/config.go | 12 +- 22 files changed, 639 insertions(+), 185 deletions(-) create mode 100644 cmd/admin/initialise/helper.go create mode 100644 cmd/admin/initialise/init_test.go create mode 100644 cmd/admin/initialise/sql/01_user.sql create mode 100644 cmd/admin/initialise/sql/02_database.sql create mode 100644 cmd/admin/initialise/sql/03_grant_user.sql create mode 100644 cmd/admin/initialise/sql/04_eventstore.sql create mode 100644 cmd/admin/initialise/sql/05_projections.sql create mode 100644 cmd/admin/initialise/sql/06_enable_hash_sharded_indexes.sql create mode 100644 cmd/admin/initialise/sql/07_events_table.sql create mode 100644 cmd/admin/initialise/sql/README.md create mode 100644 cmd/admin/initialise/verify_database_test.go create mode 100644 cmd/admin/initialise/verify_grant_test.go create mode 100644 cmd/admin/initialise/verify_user_test.go create mode 100644 cmd/admin/initialise/verify_zitadel_test.go diff --git a/cmd/admin/initialise/config.go b/cmd/admin/initialise/config.go index cb44ef60b6..0677fe4913 100644 --- a/cmd/admin/initialise/config.go +++ b/cmd/admin/initialise/config.go @@ -3,5 +3,24 @@ package initialise import "github.com/caos/zitadel/internal/database" type Config struct { - Database database.Config + Database database.Config + AdminUser database.User +} + +func adminConfig(config Config) database.Config { + adminConfig := config.Database + adminConfig.Username = config.AdminUser.Username + adminConfig.Password = config.AdminUser.Password + adminConfig.SSL.Cert = config.AdminUser.SSL.Cert + adminConfig.SSL.Key = config.AdminUser.SSL.Key + if config.AdminUser.SSL.RootCert != "" { + adminConfig.SSL.RootCert = config.AdminUser.SSL.RootCert + } + if config.AdminUser.SSL.Mode != "" { + adminConfig.SSL.Mode = config.AdminUser.SSL.Mode + } + //use default database because the zitadel database might not exist + adminConfig.Database = "" + + return adminConfig } diff --git a/cmd/admin/initialise/helper.go b/cmd/admin/initialise/helper.go new file mode 100644 index 0000000000..45a66de03f --- /dev/null +++ b/cmd/admin/initialise/helper.go @@ -0,0 +1,28 @@ +package initialise + +import ( + "database/sql" +) + +func exists(query string, args ...interface{}) func(*sql.DB) (exists bool, err error) { + return func(db *sql.DB) (exists bool, err error) { + row := db.QueryRow("SELECT EXISTS("+query+")", args...) + err = row.Scan(&exists) + return exists, err + } +} + +func exec(stmt string, args ...interface{}) func(*sql.DB) error { + return func(db *sql.DB) error { + _, err := db.Exec(stmt, args...) + return err + } +} + +func verify(db *sql.DB, checkExists func(*sql.DB) (bool, error), create func(*sql.DB) error) error { + exists, err := checkExists(db) + if exists || err != nil { + return err + } + return create(db) +} diff --git a/cmd/admin/initialise/init.go b/cmd/admin/initialise/init.go index 6bd461a236..e8af457b14 100644 --- a/cmd/admin/initialise/init.go +++ b/cmd/admin/initialise/init.go @@ -13,20 +13,6 @@ import ( _ "github.com/lib/pq" ) -var ( - user string - password string - sslCert string - sslKey string -) - -const ( - userFlag = "user" - passwordFlag = "password" - sslCertFlag = "ssl-cert" - sslKeyFlag = "ssl-key" -) - func New() *cobra.Command { cmd := &cobra.Command{ Use: "init", @@ -42,52 +28,39 @@ The user provided by flags needs priviledge to - grant all rights of the ZITADEL database to the user created if not yet set `, RunE: func(cmd *cobra.Command, args []string) error { - config := new(Config) - if err := viper.Unmarshal(config); err != nil { + config := Config{} + if err := viper.Unmarshal(&config); err != nil { return err } - return initialise(config, verifyUser, verifyDB, verifyGrant) + if err := initialise(config, + verifyUser(config.Database), + verifyDatabase(config.Database), + verifyGrant(config.Database), + ); err != nil { + return err + } + + return verifyZitadel(config.Database) }, } - cmd.PersistentFlags().StringVar(&password, passwordFlag, "", "password of the the provided user") - cmd.PersistentFlags().StringVar(&sslCert, sslCertFlag, "", "ssl cert from the provided user") - cmd.PersistentFlags().StringVar(&sslKey, sslKeyFlag, "", "ssl key from the provided user") - cmd.PersistentFlags().StringVar(&user, userFlag, "", "(required) the user to check if the database, user and grants exists and create if not") - cmd.MarkPersistentFlagRequired(userFlag) - cmd.AddCommand(newZitadel(), newDatabase(), newUser(), newGrant()) - return cmd } -func adminConfig(config database.Config) database.Config { - adminConfig := config - adminConfig.User = user - adminConfig.Password = password - adminConfig.SSL.Cert = sslCert - adminConfig.SSL.Key = sslKey - - return adminConfig -} - -func initialise(config *Config, steps ...func(*sql.DB, database.Config) error) error { +func initialise(config Config, steps ...func(*sql.DB) error) error { logging.Info("initialization started") - db, err := database.Connect(adminConfig(config.Database)) + db, err := database.Connect(adminConfig(config)) if err != nil { return err } for _, step := range steps { - if err = step(db, config.Database); err != nil { + if err = step(db); err != nil { return err } } - if err = db.Close(); err != nil { - return err - } - - return verifyZitadel(config.Database) + return db.Close() } diff --git a/cmd/admin/initialise/init_test.go b/cmd/admin/initialise/init_test.go new file mode 100644 index 0000000000..0d995c29ea --- /dev/null +++ b/cmd/admin/initialise/init_test.go @@ -0,0 +1,82 @@ +package initialise + +import ( + "database/sql" + "database/sql/driver" + "regexp" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +type db struct { + mock sqlmock.Sqlmock + db *sql.DB +} + +func prepareDB(t *testing.T, expectations ...expectation) db { + t.Helper() + client, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("unable to create sql mock: %v", err) + } + for _, expectation := range expectations { + expectation(mock) + } + return db{ + mock: mock, + db: client, + } +} + +type expectation func(m sqlmock.Sqlmock) + +func expectExists(query string, value bool, args ...driver.Value) expectation { + return func(m sqlmock.Sqlmock) { + m.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(args...).WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(value)) + } +} + +func expectQueryErr(query string, err error, args ...driver.Value) expectation { + return func(m sqlmock.Sqlmock) { + m.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(args...).WillReturnError(err) + } +} + +func expectExec(stmt string, err error, args ...driver.Value) expectation { + return func(m sqlmock.Sqlmock) { + query := m.ExpectExec(regexp.QuoteMeta(stmt)).WithArgs(args...) + if err != nil { + query.WillReturnError(err) + return + } + query.WillReturnResult(sqlmock.NewResult(1, 1)) + } +} + +func expectBegin(err error) expectation { + return func(m sqlmock.Sqlmock) { + query := m.ExpectBegin() + if err != nil { + query.WillReturnError(err) + } + } +} + +func expectCommit(err error) expectation { + return func(m sqlmock.Sqlmock) { + query := m.ExpectCommit() + if err != nil { + query.WillReturnError(err) + } + } +} + +func expectRollback(err error) expectation { + return func(m sqlmock.Sqlmock) { + query := m.ExpectRollback() + if err != nil { + query.WillReturnError(err) + } + } +} diff --git a/cmd/admin/initialise/sql/01_user.sql b/cmd/admin/initialise/sql/01_user.sql new file mode 100644 index 0000000000..967c7f2b7d --- /dev/null +++ b/cmd/admin/initialise/sql/01_user.sql @@ -0,0 +1 @@ +CREATE USER $1 WITH PASSWORD $2 \ No newline at end of file diff --git a/cmd/admin/initialise/sql/02_database.sql b/cmd/admin/initialise/sql/02_database.sql new file mode 100644 index 0000000000..50c599e3b5 --- /dev/null +++ b/cmd/admin/initialise/sql/02_database.sql @@ -0,0 +1,2 @@ +-- replace %[1]s with the name of the database +CREATE DATABASE %[1]s \ No newline at end of file diff --git a/cmd/admin/initialise/sql/03_grant_user.sql b/cmd/admin/initialise/sql/03_grant_user.sql new file mode 100644 index 0000000000..e218323934 --- /dev/null +++ b/cmd/admin/initialise/sql/03_grant_user.sql @@ -0,0 +1,3 @@ +-- replace the first %[1]s with the database +-- replace the second \%[2]s with the user +GRANT ALL ON DATABASE %[1]s TO %[2]s \ No newline at end of file diff --git a/cmd/admin/initialise/sql/04_eventstore.sql b/cmd/admin/initialise/sql/04_eventstore.sql new file mode 100644 index 0000000000..e62237d680 --- /dev/null +++ b/cmd/admin/initialise/sql/04_eventstore.sql @@ -0,0 +1 @@ +CREATE SCHEMA eventstore \ No newline at end of file diff --git a/cmd/admin/initialise/sql/05_projections.sql b/cmd/admin/initialise/sql/05_projections.sql new file mode 100644 index 0000000000..ed68342c49 --- /dev/null +++ b/cmd/admin/initialise/sql/05_projections.sql @@ -0,0 +1 @@ +CREATE SCHEMA projections \ No newline at end of file diff --git a/cmd/admin/initialise/sql/06_enable_hash_sharded_indexes.sql b/cmd/admin/initialise/sql/06_enable_hash_sharded_indexes.sql new file mode 100644 index 0000000000..f68c5ed574 --- /dev/null +++ b/cmd/admin/initialise/sql/06_enable_hash_sharded_indexes.sql @@ -0,0 +1 @@ +SET experimental_enable_hash_sharded_indexes = on \ No newline at end of file diff --git a/cmd/admin/initialise/sql/07_events_table.sql b/cmd/admin/initialise/sql/07_events_table.sql new file mode 100644 index 0000000000..4a2497ab92 --- /dev/null +++ b/cmd/admin/initialise/sql/07_events_table.sql @@ -0,0 +1,24 @@ +CREATE TABLE eventstore.events ( + id UUID DEFAULT gen_random_uuid() + , event_type TEXT NOT NULL + , aggregate_type TEXT NOT NULL + , aggregate_id TEXT NOT NULL + , aggregate_version TEXT NOT NULL + , event_sequence BIGINT NOT NULL + , previous_aggregate_sequence BIGINT + , previous_aggregate_type_sequence INT8 + , creation_date TIMESTAMPTZ NOT NULL DEFAULT now() + , event_data JSONB + , editor_user TEXT NOT NULL + , editor_service TEXT NOT NULL + , resource_owner TEXT NOT NULL + + , PRIMARY KEY (event_sequence DESC) USING HASH WITH BUCKET_COUNT = 10 + , INDEX agg_type_agg_id (aggregate_type, aggregate_id) + , INDEX agg_type (aggregate_type) + , INDEX agg_type_seq (aggregate_type, event_sequence DESC) + STORING (id, event_type, aggregate_id, aggregate_version, previous_aggregate_sequence, creation_date, event_data, editor_user, editor_service, resource_owner, previous_aggregate_type_sequence) + , INDEX max_sequence (aggregate_type, aggregate_id, event_sequence DESC) + , CONSTRAINT previous_sequence_unique UNIQUE (previous_aggregate_sequence DESC) + , CONSTRAINT prev_agg_type_seq_unique UNIQUE(previous_aggregate_type_sequence) +) \ No newline at end of file diff --git a/cmd/admin/initialise/sql/README.md b/cmd/admin/initialise/sql/README.md new file mode 100644 index 0000000000..26e5113a6e --- /dev/null +++ b/cmd/admin/initialise/sql/README.md @@ -0,0 +1,14 @@ +# SQL initialisation + +The sql-files in this folder initialize the ZITADEL database and user. These objects need to exist before ZITADEL is able to set and start up. + +## files + +- 01_user.sql: create the user zitadel uses to connect to the database +- 02_database.sql: create the database for zitadel +- 03_grant_user.sql: grants the user created before to have full access to its database. The user needs full access to the database because zitadel makes ddl/dml on runtime +- 04_eventstore.sql: creates the schema needed for eventsourcing +- 05_projections.sql: creates the schema needed to read the data +- files 06_enable_hash_sharded_indexes.sql and 07_events_table.sql must run in the same session + - 06_enable_hash_sharded_indexes.sql enables the [hash sharded index](https://www.cockroachlabs.com/docs/stable/hash-sharded-indexes.html) feature for this session + - 07_events_table.sql creates the table for eventsourcing diff --git a/cmd/admin/initialise/verify_database.go b/cmd/admin/initialise/verify_database.go index eeb54f5888..32722202f4 100644 --- a/cmd/admin/initialise/verify_database.go +++ b/cmd/admin/initialise/verify_database.go @@ -2,13 +2,21 @@ package initialise import ( "database/sql" + _ "embed" + "fmt" - "github.com/caos/logging" "github.com/caos/zitadel/internal/database" "github.com/spf13/cobra" "github.com/spf13/viper" ) +var ( + searchDatabase = "SELECT database_name FROM [show databases] WHERE database_name = $1" + + //go:embed sql/02_database.sql + databaseStmt string +) + func newDatabase() *cobra.Command { return &cobra.Command{ Use: "database", @@ -24,31 +32,20 @@ The user provided by flags needs priviledge to - grant all rights of the ZITADEL database to the user created if not yet set `, RunE: func(cmd *cobra.Command, args []string) error { - config := new(Config) - if err := viper.Unmarshal(config); err != nil { + config := Config{} + if err := viper.Unmarshal(&config); err != nil { return err } - return initialise(config, verifyDB) + return initialise(config, verifyDatabase(config.Database)) }, } } -func verifyDB(db *sql.DB, config database.Config) error { - logging.Info("verify database") - exists, err := existsDatabase(db, config) - if exists || err != nil { - return err +func verifyDatabase(config database.Config) func(*sql.DB) error { + return func(db *sql.DB) error { + return verify(db, + exists(searchDatabase, config.Database), + exec(fmt.Sprintf(databaseStmt, config.Database)), + ) } - return createDatabase(db, config) -} - -func existsDatabase(db *sql.DB, config database.Config) (exists bool, err error) { - row := db.QueryRow("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", config.Database) - err = row.Scan(&exists) - return exists, err -} - -func createDatabase(db *sql.DB, config database.Config) error { - _, err := db.Exec("CREATE DATABASE " + config.Database) - return err } diff --git a/cmd/admin/initialise/verify_database_test.go b/cmd/admin/initialise/verify_database_test.go new file mode 100644 index 0000000000..aaf9dca4b0 --- /dev/null +++ b/cmd/admin/initialise/verify_database_test.go @@ -0,0 +1,80 @@ +package initialise + +import ( + "database/sql" + "errors" + "testing" + + "github.com/caos/zitadel/internal/database" +) + +func Test_verifyDB(t *testing.T) { + type args struct { + db db + config database.Config + } + tests := []struct { + name string + args args + targetErr error + }{ + { + name: "exists fails", + args: args{ + db: prepareDB(t, expectQueryErr("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", sql.ErrConnDone, "zitadel")), + config: database.Config{ + Database: "zitadel", + }, + }, + targetErr: sql.ErrConnDone, + }, + { + name: "doesn't exists, create fails", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", false, "zitadel"), + expectExec("CREATE DATABASE zitadel", sql.ErrTxDone), + ), + config: database.Config{ + Database: "zitadel", + }, + }, + targetErr: sql.ErrTxDone, + }, + { + name: "doesn't exists, create successful", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", false, "zitadel"), + expectExec("CREATE DATABASE zitadel", nil), + ), + config: database.Config{ + Database: "zitadel", + }, + }, + targetErr: nil, + }, + { + name: "already exists", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT database_name FROM [show databases] WHERE database_name = $1)", true, "zitadel"), + ), + config: database.Config{ + Database: "zitadel", + }, + }, + targetErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := verifyDatabase(tt.args.config)(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 { + t.Error(err) + } + }) + } +} diff --git a/cmd/admin/initialise/verify_grant.go b/cmd/admin/initialise/verify_grant.go index bf62e4ea7e..84240cf17b 100644 --- a/cmd/admin/initialise/verify_grant.go +++ b/cmd/admin/initialise/verify_grant.go @@ -2,6 +2,8 @@ package initialise import ( "database/sql" + _ "embed" + "fmt" "github.com/caos/logging" "github.com/caos/zitadel/internal/database" @@ -9,6 +11,12 @@ import ( "github.com/spf13/viper" ) +var ( + searchGrant = "SELECT * FROM [SHOW GRANTS ON DATABASE %s] where grantee = $1 AND privilege_type = 'ALL'" + //go:embed sql/03_grant_user.sql + grantStmt string +) + func newGrant() *cobra.Command { return &cobra.Command{ Use: "grant", @@ -19,31 +27,21 @@ Prereqesits: - cockroachdb `, RunE: func(cmd *cobra.Command, args []string) error { - config := new(Config) - if err := viper.Unmarshal(config); err != nil { + config := Config{} + if err := viper.Unmarshal(&config); err != nil { return err } - return initialise(config, verifyGrant) + return initialise(config, verifyGrant(config.Database)) }, } } -func verifyGrant(db *sql.DB, config database.Config) error { - logging.Info("verify grant") - exists, err := hasGrant(db, config) - if exists || err != nil { - return err +func verifyGrant(config database.Config) func(*sql.DB) error { + return func(db *sql.DB) error { + logging.WithFields("user", config.Username).Info("verify grant") + return verify(db, + exists(fmt.Sprintf(searchGrant, config.Database), config.Username), + exec(fmt.Sprintf(grantStmt, config.Database, config.Username)), + ) } - return grant(db, config) -} - -func hasGrant(db *sql.DB, config database.Config) (has bool, err error) { - row := db.QueryRow("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE "+config.Database+"] where grantee = $1 AND privilege_type = 'ALL')", config.User) - err = row.Scan(&has) - return has, err -} - -func grant(db *sql.DB, config database.Config) error { - _, err := db.Exec("GRANT ALL ON DATABASE " + config.Database + " TO " + config.User) - return err } diff --git a/cmd/admin/initialise/verify_grant_test.go b/cmd/admin/initialise/verify_grant_test.go new file mode 100644 index 0000000000..1455c77c82 --- /dev/null +++ b/cmd/admin/initialise/verify_grant_test.go @@ -0,0 +1,92 @@ +package initialise + +import ( + "database/sql" + "errors" + "testing" + + "github.com/caos/zitadel/internal/database" +) + +func Test_verifyGrant(t *testing.T) { + type args struct { + db db + config database.Config + } + tests := []struct { + name string + args args + targetErr error + }{ + { + name: "exists fails", + args: args{ + db: prepareDB(t, expectQueryErr("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE zitadel] where grantee = $1 AND privilege_type = 'ALL'", sql.ErrConnDone, "zitadel-user")), + config: database.Config{ + Database: "zitadel", + User: database.User{ + Username: "zitadel-user", + }, + }, + }, + targetErr: sql.ErrConnDone, + }, + { + name: "doesn't exists, create fails", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE zitadel] where grantee = $1 AND privilege_type = 'ALL'", false, "zitadel-user"), + expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", sql.ErrTxDone), + ), + config: database.Config{ + Database: "zitadel", + User: database.User{ + Username: "zitadel-user", + }, + }, + }, + targetErr: sql.ErrTxDone, + }, + { + name: "correct", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE zitadel] where grantee = $1 AND privilege_type = 'ALL'", false, "zitadel-user"), + expectExec("GRANT ALL ON DATABASE zitadel TO zitadel-user", nil), + ), + config: database.Config{ + Database: "zitadel", + User: database.User{ + Username: "zitadel-user", + }, + }, + }, + targetErr: nil, + }, + { + name: "already exists", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT * FROM [SHOW GRANTS ON DATABASE zitadel] where grantee = $1 AND privilege_type = 'ALL'", true, "zitadel-user"), + ), + config: database.Config{ + Database: "zitadel", + User: database.User{ + Username: "zitadel-user", + }, + }, + }, + targetErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := verifyGrant(tt.args.config)(tt.args.db.db); !errors.Is(err, tt.targetErr) { + t.Errorf("verifyGrant() error = %v, want: %v", err, tt.targetErr) + } + if err := tt.args.db.mock.ExpectationsWereMet(); err != nil { + t.Error(err) + } + }) + } +} diff --git a/cmd/admin/initialise/verify_user.go b/cmd/admin/initialise/verify_user.go index 51d980b9a9..93cab468ab 100644 --- a/cmd/admin/initialise/verify_user.go +++ b/cmd/admin/initialise/verify_user.go @@ -2,6 +2,7 @@ package initialise import ( "database/sql" + _ "embed" "github.com/caos/logging" "github.com/caos/zitadel/internal/database" @@ -9,6 +10,12 @@ import ( "github.com/spf13/viper" ) +var ( + searchUser = "SELECT username FROM [show roles] WHERE username = $1" + //go:embed sql/01_user.sql + createUserStmt string +) + func newUser() *cobra.Command { return &cobra.Command{ Use: "user", @@ -24,31 +31,21 @@ The user provided by flags needs priviledge to - grant all rights of the ZITADEL database to the user created if not yet set `, RunE: func(cmd *cobra.Command, args []string) error { - config := new(Config) - if err := viper.Unmarshal(config); err != nil { + config := Config{} + if err := viper.Unmarshal(&config); err != nil { return err } - return initialise(config, verifyUser) + return initialise(config, verifyUser(config.Database)) }, } } -func verifyUser(db *sql.DB, config database.Config) error { - logging.Info("verify user") - exists, err := existsUser(db, config) - if exists || err != nil { - return err +func verifyUser(config database.Config) func(*sql.DB) error { + return func(db *sql.DB) error { + logging.WithFields("username", config.Username).Info("verify user") + return verify(db, + exists(searchUser, config.Username), + exec(createUserStmt, config.Username, &sql.NullString{String: config.Password, Valid: config.Password != ""}), + ) } - return createUser(db, config) -} - -func existsUser(db *sql.DB, config database.Config) (exists bool, err error) { - row := db.QueryRow("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", config.User) - err = row.Scan(&exists) - return exists, err -} - -func createUser(db *sql.DB, config database.Config) error { - _, err := db.Exec("CREATE USER $1 WITH PASSWORD $2", config.User, &sql.NullString{String: config.Password, Valid: config.Password != ""}) - return err } diff --git a/cmd/admin/initialise/verify_user_test.go b/cmd/admin/initialise/verify_user_test.go new file mode 100644 index 0000000000..7330c60369 --- /dev/null +++ b/cmd/admin/initialise/verify_user_test.go @@ -0,0 +1,109 @@ +package initialise + +import ( + "database/sql" + "errors" + "testing" + + "github.com/caos/zitadel/internal/database" +) + +func Test_verifyUser(t *testing.T) { + type args struct { + db db + config database.Config + } + tests := []struct { + name string + args args + targetErr error + }{ + { + name: "exists fails", + args: args{ + db: prepareDB(t, expectQueryErr("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", sql.ErrConnDone, "zitadel-user")), + config: database.Config{ + Database: "zitadel", + User: database.User{ + Username: "zitadel-user", + }, + }, + }, + targetErr: sql.ErrConnDone, + }, + { + name: "doesn't exists, create fails", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", false, "zitadel-user"), + expectExec("CREATE USER $1 WITH PASSWORD $2", sql.ErrTxDone, "zitadel-user", nil), + ), + config: database.Config{ + Database: "zitadel", + User: database.User{ + Username: "zitadel-user", + }, + }, + }, + targetErr: sql.ErrTxDone, + }, + { + name: "correct without password", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", false, "zitadel-user"), + expectExec("CREATE USER $1 WITH PASSWORD $2", nil, "zitadel-user", nil), + ), + config: database.Config{ + Database: "zitadel", + User: database.User{ + Username: "zitadel-user", + }, + }, + }, + targetErr: nil, + }, + { + name: "correct with password", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", false, "zitadel-user"), + expectExec("CREATE USER $1 WITH PASSWORD $2", nil, "zitadel-user", "password"), + ), + config: database.Config{ + Database: "zitadel", + User: database.User{ + Username: "zitadel-user", + Password: "password", + }, + }, + }, + targetErr: nil, + }, + { + name: "already exists", + args: args{ + db: prepareDB(t, + expectExists("SELECT EXISTS(SELECT username FROM [show roles] WHERE username = $1)", true, "zitadel-user"), + ), + config: database.Config{ + Database: "zitadel", + User: database.User{ + Username: "zitadel-user", + }, + }, + }, + targetErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := verifyUser(tt.args.config)(tt.args.db.db); !errors.Is(err, tt.targetErr) { + t.Errorf("verifyGrant() error = %v, want: %v", err, tt.targetErr) + } + if err := tt.args.db.mock.ExpectationsWereMet(); err != nil { + t.Error(err) + } + }) + } +} diff --git a/cmd/admin/initialise/verify_zitadel.go b/cmd/admin/initialise/verify_zitadel.go index 6d735d7ee8..51c5c7b43c 100644 --- a/cmd/admin/initialise/verify_zitadel.go +++ b/cmd/admin/initialise/verify_zitadel.go @@ -2,8 +2,8 @@ package initialise import ( "database/sql" + _ "embed" - "github.com/caos/logging" "github.com/caos/zitadel/internal/database" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -12,7 +12,19 @@ import ( const ( eventstoreSchema = "eventstore" projectionsSchema = "projections" - eventsTable = "events" +) + +var ( + searchEventsTable = "SELECT table_name FROM [SHOW TABLES] WHERE table_name = 'events'" + searchSchema = "SELECT schema_name FROM [SHOW SCHEMAS] WHERE schema_name = $1" + //go:embed sql/06_enable_hash_sharded_indexes.sql + enableHashShardedIdx string + //go:embed sql/07_events_table.sql + createEventsStmt string + //go:embed sql/05_projections.sql + createProjectionsStmt string + //go:embed sql/04_eventstore.sql + createEventstoreStmt string ) func newZitadel() *cobra.Command { @@ -29,7 +41,7 @@ Prereqesits: if err := viper.Unmarshal(config); err != nil { return err } - return initialise(config, verifyUser) + return verifyZitadel(config.Database) }, } } @@ -40,93 +52,32 @@ func verifyZitadel(config database.Config) error { return err } - if err := verifySchema(db, config, projectionsSchema); err != nil { + if err := verify(db, exists(searchSchema, projectionsSchema), exec(createProjectionsStmt)); err != nil { return err } - if err := verifySchema(db, config, eventstoreSchema); err != nil { + if err := verify(db, exists(searchSchema, eventstoreSchema), exec(createEventstoreStmt)); err != nil { return err } - if err := verifyEvents(db, config); err != nil { + if err := verify(db, exists(searchSchema, projectionsSchema), createEvents); err != nil { return err } return db.Close() } -func verifySchema(db *sql.DB, config database.Config, schema string) error { - logging.WithFields("schema", schema).Info("verify schema") - exists, err := existsSchema(db, config, schema) - if exists || err != nil { - return err - } - return createSchema(db, config, schema) -} - -func existsSchema(db *sql.DB, config database.Config, schema string) (exists bool, err error) { - row := db.QueryRow("SELECT EXISTS(SELECT schema_name FROM [SHOW SCHEMAS] WHERE schema_name = $1)", schema) - err = row.Scan(&exists) - return exists, err -} - -func createSchema(db *sql.DB, config database.Config, schema string) error { - _, err := db.Exec("CREATE SCHEMA " + schema) - return err -} - -func verifyEvents(db *sql.DB, config database.Config) error { - logging.Info("verify events table") - - exists, err := existsEvents(db, config) - if exists || err != nil { - return err - } - return createEvents(db, config) -} - -func existsEvents(db *sql.DB, config database.Config) (exists bool, err error) { - row := db.QueryRow("SELECT EXISTS(SELECT table_name FROM [SHOW TABLES] WHERE table_name = $1)", eventsTable) - err = row.Scan(&exists) - return exists, err -} - -func createEvents(db *sql.DB, config database.Config) error { +func createEvents(db *sql.DB) error { tx, err := db.Begin() if err != nil { return err } - if _, err = tx.Exec("SET experimental_enable_hash_sharded_indexes = on"); err != nil { + if _, err = tx.Exec(enableHashShardedIdx); err != nil { tx.Rollback() return err } - stmt := `CREATE TABLE eventstore.events ( - id UUID DEFAULT gen_random_uuid() - , event_type TEXT NOT NULL - , aggregate_type TEXT NOT NULL - , aggregate_id TEXT NOT NULL - , aggregate_version TEXT NOT NULL - , event_sequence BIGINT NOT NULL - , previous_aggregate_sequence BIGINT - , previous_aggregate_type_sequence INT8 - , creation_date TIMESTAMPTZ NOT NULL DEFAULT now() - , event_data JSONB - , editor_user TEXT NOT NULL - , editor_service TEXT NOT NULL - , resource_owner TEXT NOT NULL - - , PRIMARY KEY (event_sequence DESC) USING HASH WITH BUCKET_COUNT = 10 - , INDEX agg_type_agg_id (aggregate_type, aggregate_id) - , INDEX agg_type (aggregate_type) - , INDEX agg_type_seq (aggregate_type, event_sequence DESC) - STORING (id, event_type, aggregate_id, aggregate_version, previous_aggregate_sequence, creation_date, event_data, editor_user, editor_service, resource_owner, previous_aggregate_type_sequence) - , INDEX changes_idx (aggregate_type, aggregate_id, creation_date) USING HASH WITH BUCKET_COUNT = 10 - , INDEX max_sequence (aggregate_type, aggregate_id, event_sequence DESC) - , CONSTRAINT previous_sequence_unique UNIQUE (previous_aggregate_sequence DESC) - , CONSTRAINT prev_agg_type_seq_unique UNIQUE(previous_aggregate_type_sequence) -)` - if _, err = tx.Exec(stmt); err != nil { + if _, err = tx.Exec(createEventsStmt); err != nil { tx.Rollback() return err } diff --git a/cmd/admin/initialise/verify_zitadel_test.go b/cmd/admin/initialise/verify_zitadel_test.go new file mode 100644 index 0000000000..6f6c73a49a --- /dev/null +++ b/cmd/admin/initialise/verify_zitadel_test.go @@ -0,0 +1,73 @@ +package initialise + +import ( + "database/sql" + "errors" + "testing" +) + +func Test_verifyEvents(t *testing.T) { + type args struct { + db db + } + tests := []struct { + name string + args args + targetErr error + }{ + { + name: "unable to begin", + args: args{ + db: prepareDB(t, + expectBegin(sql.ErrConnDone), + ), + }, + targetErr: sql.ErrConnDone, + }, + { + name: "hash sharded indexes fails", + args: args{ + db: prepareDB(t, + expectBegin(nil), + expectExec("SET experimental_enable_hash_sharded_indexes = on", sql.ErrNoRows), + expectRollback(nil), + ), + }, + targetErr: sql.ErrNoRows, + }, + { + name: "create table fails", + args: args{ + db: prepareDB(t, + expectBegin(nil), + expectExec("SET experimental_enable_hash_sharded_indexes = on", nil), + expectExec(createEventsStmt, sql.ErrNoRows), + expectRollback(nil), + ), + }, + targetErr: sql.ErrNoRows, + }, + { + name: "correct", + args: args{ + db: prepareDB(t, + expectBegin(nil), + expectExec("SET experimental_enable_hash_sharded_indexes = on", nil), + expectExec(createEventsStmt, nil), + expectCommit(nil), + ), + }, + targetErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := createEvents(tt.args.db.db); !errors.Is(err, tt.targetErr) { + t.Errorf("createEvents() error = %v, want: %v", err, tt.targetErr) + } + if err := tt.args.db.mock.ExpectationsWereMet(); err != nil { + t.Error(err) + } + }) + } +} diff --git a/cmd/defaults.yaml b/cmd/defaults.yaml index 3f5a56b4f9..322bf220b4 100644 --- a/cmd/defaults.yaml +++ b/cmd/defaults.yaml @@ -11,18 +11,22 @@ ExternalSecure: true Database: Host: localhost Port: 26257 - User: zitadel Database: zitadel - Password: "" MaxOpenConns: 20 MaxConnLifetime: 30m MaxConnIdleTime: 30m Options: "" - SSL: - Mode: diabled - RootCert: "" - Cert: "" - Key: "" + User: + Username: zitadel + Password: "" + SSL: + Mode: diabled + RootCert: "" + Cert: "" + Key: "" + +AdminUser: + Username: root Projections: Config: diff --git a/internal/database/config.go b/internal/database/config.go index 284da731d0..cdef060459 100644 --- a/internal/database/config.go +++ b/internal/database/config.go @@ -14,19 +14,23 @@ const ( type Config struct { Host string Port string - User string - Password string Database string - SSL SSL MaxOpenConns uint32 MaxConnLifetime time.Duration MaxConnIdleTime time.Duration + User //Additional options to be appended as options= //The value will be taken as is. Multiple options are space separated. Options string } +type User struct { + Username string + Password string + SSL SSL +} + type SSL struct { // type of connection security Mode string @@ -57,7 +61,7 @@ func (c Config) String() string { fields := []string{ "host=" + c.Host, "port=" + c.Port, - "user=" + c.User, + "user=" + c.Username, "dbname=" + c.Database, "application_name=zitadel", "sslmode=" + c.SSL.Mode,