diff --git a/api/admin/user.go b/api/admin/user.go index e8729b628..324e8912e 100644 --- a/api/admin/user.go +++ b/api/admin/user.go @@ -45,7 +45,7 @@ func AllUsers(c *gin.Context) { logrus.Info("Admin: reading all users") // send API call to capture all users - u, err := database.FromContext(c).GetUserList() + u, err := database.FromContext(c).ListUsers() if err != nil { retErr := fmt.Errorf("unable to capture all users: %w", err) diff --git a/api/authenticate.go b/api/authenticate.go index 4f14465b0..0a518b697 100644 --- a/api/authenticate.go +++ b/api/authenticate.go @@ -99,7 +99,7 @@ func Authenticate(c *gin.Context) { } // send API call to capture the user logging in - u, err := database.FromContext(c).GetUserName(newUser.GetName()) + u, err := database.FromContext(c).GetUserForName(newUser.GetName()) // create a new user account if len(u.GetName()) == 0 || err != nil { // create unique id for the user @@ -314,7 +314,7 @@ func AuthenticateToken(c *gin.Context) { } // check if the user exists - u, err = database.FromContext(c).GetUserName(u.GetName()) + u, err = database.FromContext(c).GetUserForName(u.GetName()) if err != nil { retErr := fmt.Errorf("user %s not found", u.GetName()) diff --git a/api/metrics.go b/api/metrics.go index 4c94cb4c4..90d499b75 100644 --- a/api/metrics.go +++ b/api/metrics.go @@ -74,7 +74,7 @@ func CustomMetrics(c *gin.Context) { // helper function to get the totals of resource types. func recordGauges(c *gin.Context) { // send API call to capture the total number of users - u, err := database.FromContext(c).GetUserCount() + u, err := database.FromContext(c).CountUsers() if err != nil { logrus.Errorf("unable to get count of all users: %v", err) } diff --git a/api/user.go b/api/user.go index 87e0922f1..f22108c58 100644 --- a/api/user.go +++ b/api/user.go @@ -87,7 +87,7 @@ func CreateUser(c *gin.Context) { } // send API call to capture the created user - user, _ := database.FromContext(c).GetUserName(input.GetName()) + user, _ := database.FromContext(c).GetUserForName(input.GetName()) c.JSON(http.StatusCreated, user) } @@ -172,18 +172,8 @@ func GetUsers(c *gin.Context) { // ensure per_page isn't above or below allowed values perPage = util.MaxInt(1, util.MinInt(100, perPage)) - // send API call to capture the total number of users - t, err := database.FromContext(c).GetUserCount() - if err != nil { - retErr := fmt.Errorf("unable to get users count: %w", err) - - util.HandleError(c, http.StatusInternalServerError, retErr) - - return - } - // send API call to capture the list of users - users, err := database.FromContext(c).GetUserLiteList(page, perPage) + users, t, err := database.FromContext(c).ListLiteUsers(page, perPage) if err != nil { retErr := fmt.Errorf("unable to get users: %w", err) @@ -311,7 +301,7 @@ func UpdateCurrentUser(c *gin.Context) { } // send API call to capture the updated user - u, err = database.FromContext(c).GetUserName(u.GetName()) + u, err = database.FromContext(c).GetUserForName(u.GetName()) if err != nil { retErr := fmt.Errorf("unable to get updated user %s: %w", u.GetName(), err) @@ -363,7 +353,7 @@ func GetUser(c *gin.Context) { }).Infof("reading user %s", user) // send API call to capture the user - u, err := database.FromContext(c).GetUserName(user) + u, err := database.FromContext(c).GetUserForName(user) if err != nil { retErr := fmt.Errorf("unable to get user %s: %w", user, err) @@ -548,7 +538,7 @@ func UpdateUser(c *gin.Context) { } // send API call to capture the user - u, err = database.FromContext(c).GetUserName(user) + u, err = database.FromContext(c).GetUserForName(user) if err != nil { retErr := fmt.Errorf("unable to get user %s: %w", user, err) @@ -584,7 +574,7 @@ func UpdateUser(c *gin.Context) { } // send API call to capture the updated user - u, _ = database.FromContext(c).GetUserName(user) + u, _ = database.FromContext(c).GetUserForName(user) c.JSON(http.StatusOK, u) } @@ -633,7 +623,7 @@ func DeleteUser(c *gin.Context) { }).Infof("deleting user %s", user) // send API call to capture the user - u, err := database.FromContext(c).GetUserName(user) + u, err := database.FromContext(c).GetUserForName(user) if err != nil { retErr := fmt.Errorf("unable to get user %s: %w", user, err) @@ -643,7 +633,7 @@ func DeleteUser(c *gin.Context) { } // send API call to remove the user - err = database.FromContext(c).DeleteUser(u.GetID()) + err = database.FromContext(c).DeleteUser(u) if err != nil { retErr := fmt.Errorf("unable to delete user %s: %w", u.GetName(), err) diff --git a/database/pipeline/index.go b/database/pipeline/index.go index ef8bb4f20..506fddfa8 100644 --- a/database/pipeline/index.go +++ b/database/pipeline/index.go @@ -15,8 +15,8 @@ ON pipelines (repo_id); ` ) -// CreateIndexes creates the indexes for the pipelines table in the database. -func (e *engine) CreateIndexes() error { +// CreatePipelineIndexes creates the indexes for the pipelines table in the database. +func (e *engine) CreatePipelineIndexes() error { e.logger.Tracef("creating indexes for pipelines table in the database") // create the repo_id column index for the pipelines table diff --git a/database/pipeline/index_test.go b/database/pipeline/index_test.go index 75e54f90e..1fa77b7b0 100644 --- a/database/pipeline/index_test.go +++ b/database/pipeline/index_test.go @@ -10,7 +10,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" ) -func TestPipeline_Engine_CreateIndexes(t *testing.T) { +func TestPipeline_Engine_CreatePipelineIndexes(t *testing.T) { // setup types _postgres, _mock := testPostgres(t) defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() @@ -41,18 +41,18 @@ func TestPipeline_Engine_CreateIndexes(t *testing.T) { // run tests for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err := test.database.CreateIndexes() + err := test.database.CreatePipelineIndexes() if test.failure { if err == nil { - t.Errorf("CreateIndexes for %s should have returned err", test.name) + t.Errorf("CreatePipelineIndexes for %s should have returned err", test.name) } return } if err != nil { - t.Errorf("CreateIndexes for %s returned err: %v", test.name, err) + t.Errorf("CreatePipelineIndexes for %s returned err: %v", test.name, err) } }) } diff --git a/database/pipeline/pipeline.go b/database/pipeline/pipeline.go index d6978fad0..2a305d8e4 100644 --- a/database/pipeline/pipeline.go +++ b/database/pipeline/pipeline.go @@ -67,13 +67,13 @@ func New(opts ...EngineOpt) (*engine, error) { } // create the pipelines table - err := e.CreateTable(e.client.Config.Dialector.Name()) + err := e.CreatePipelineTable(e.client.Config.Dialector.Name()) if err != nil { return nil, fmt.Errorf("unable to create %s table: %w", constants.TablePipeline, err) } // create the indexes for the pipelines table - err = e.CreateIndexes() + err = e.CreatePipelineIndexes() if err != nil { return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TablePipeline, err) } diff --git a/database/pipeline/service.go b/database/pipeline/service.go index a34e75a28..8ff5ec8d5 100644 --- a/database/pipeline/service.go +++ b/database/pipeline/service.go @@ -17,10 +17,10 @@ type PipelineService interface { // // https://en.wikipedia.org/wiki/Data_definition_language - // CreateIndexes creates the indexes for the pipelines table. - CreateIndexes() error - // CreateTable defines a function that creates the pipelines table. - CreateTable(string) error + // CreatePipelineIndexes defines a function that creates the indexes for the pipelines table. + CreatePipelineIndexes() error + // CreatePipelineTable defines a function that creates the pipelines table. + CreatePipelineTable(string) error // Pipeline Data Manipulation Language Functions // diff --git a/database/pipeline/table.go b/database/pipeline/table.go index b4c222e7b..bc463da68 100644 --- a/database/pipeline/table.go +++ b/database/pipeline/table.go @@ -56,8 +56,8 @@ pipelines ( ` ) -// CreateTable creates the pipelines table in the database. -func (e *engine) CreateTable(driver string) error { +// CreatePipelineTable creates the pipelines table in the database. +func (e *engine) CreatePipelineTable(driver string) error { e.logger.Tracef("creating pipelines table in the database") // handle the driver provided to create the table diff --git a/database/pipeline/table_test.go b/database/pipeline/table_test.go index c199f98b5..5b05e4313 100644 --- a/database/pipeline/table_test.go +++ b/database/pipeline/table_test.go @@ -10,7 +10,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" ) -func TestPipeline_Engine_CreateTable(t *testing.T) { +func TestPipeline_Engine_CreatePipelineTable(t *testing.T) { // setup types _postgres, _mock := testPostgres(t) defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() @@ -41,18 +41,18 @@ func TestPipeline_Engine_CreateTable(t *testing.T) { // run tests for _, test := range tests { t.Run(test.name, func(t *testing.T) { - err := test.database.CreateTable(test.name) + err := test.database.CreatePipelineTable(test.name) if test.failure { if err == nil { - t.Errorf("CreateTable for %s should have returned err", test.name) + t.Errorf("CreatePipelineTable for %s should have returned err", test.name) } return } if err != nil { - t.Errorf("CreateTable for %s returned err: %v", test.name, err) + t.Errorf("CreatePipelineTable for %s returned err: %v", test.name, err) } }) } diff --git a/database/postgres/ddl/user.go b/database/postgres/ddl/user.go deleted file mode 100644 index b6fec0794..000000000 --- a/database/postgres/ddl/user.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateUserTable represents a query to - // create the users table for Vela. - CreateUserTable = ` -CREATE TABLE -IF NOT EXISTS -users ( - id SERIAL PRIMARY KEY, - name VARCHAR(250), - refresh_token VARCHAR(500), - token VARCHAR(500), - hash VARCHAR(500), - favorites VARCHAR(5000), - active BOOLEAN, - admin BOOLEAN, - UNIQUE(name) -); -` - - // CreateUserRefreshIndex represents a query to create an - // index on the users table for the refresh_token column. - CreateUserRefreshIndex = ` -CREATE INDEX -IF NOT EXISTS -users_refresh -ON users (refresh_token); -` -) diff --git a/database/postgres/dml/user.go b/database/postgres/dml/user.go deleted file mode 100644 index 9febce658..000000000 --- a/database/postgres/dml/user.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListUsers represents a query to - // list all users in the database. - ListUsers = ` -SELECT * -FROM users; -` - - // ListLiteUsers represents a query to - // list all lite users in the database. - ListLiteUsers = ` -SELECT id, name -FROM users -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectUser represents a query to select - // a user for an id in the database. - SelectUser = ` -SELECT * -FROM users -WHERE id = ? -LIMIT 1; -` - - // SelectUserName represents a query to select - // a user for a name in the database. - SelectUserName = ` -SELECT * -FROM users -WHERE name = ? -LIMIT 1; -` - - // SelectUsersCount represents a query to select - // the count of users in the database. - SelectUsersCount = ` -SELECT count(*) as count -FROM users; -` - - // SelectRefreshToken represents a query to select - // a user for a refresh_token in the database. - // - // nolint: gosec // ignore false positive - SelectRefreshToken = ` -SELECT * -FROM users -WHERE refresh_token = ? -LIMIT 1; -` - - // DeleteUser represents a query to - // remove a user from the database. - DeleteUser = ` -DELETE -FROM users -WHERE id = ?; -` -) diff --git a/database/postgres/postgres.go b/database/postgres/postgres.go index c45b954a2..cedb34c49 100644 --- a/database/postgres/postgres.go +++ b/database/postgres/postgres.go @@ -11,6 +11,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" "github.com/go-vela/server/database/pipeline" "github.com/go-vela/server/database/postgres/ddl" + "github.com/go-vela/server/database/user" "github.com/go-vela/types/constants" "github.com/sirupsen/logrus" @@ -37,11 +38,15 @@ type ( } client struct { - config *config + config *config + // https://pkg.go.dev/gorm.io/gorm#DB Postgres *gorm.DB // https://pkg.go.dev/github.com/sirupsen/logrus#Entry Logger *logrus.Entry + // https://pkg.go.dev/github.com/go-vela/server/database/pipeline#PipelineService pipeline.PipelineService + // https://pkg.go.dev/github.com/go-vela/server/database/user#UserService + user.UserService } ) @@ -140,6 +145,8 @@ func NewTest() (*client, sqlmock.Sqlmock, error) { _mock.ExpectExec(pipeline.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(pipeline.CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(user.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(user.CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) // create the new mock Postgres database client // @@ -262,12 +269,6 @@ func createTables(c *client) error { return fmt.Errorf("unable to create %s table: %w", constants.TableStep, err) } - // create the users table - err = c.Postgres.Exec(ddl.CreateUserTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %w", constants.TableUser, err) - } - // create the workers table err = c.Postgres.Exec(ddl.CreateWorkerTable).Error if err != nil { @@ -342,12 +343,6 @@ func createIndexes(c *client) error { return fmt.Errorf("unable to create secrets_type_org index for the %s table: %w", constants.TableSecret, err) } - // create the users_refresh index for the users table - err = c.Postgres.Exec(ddl.CreateUserRefreshIndex).Error - if err != nil { - return fmt.Errorf("unable to create users_refresh index for the %s table: %w", constants.TableUser, err) - } - // create the workers_hostname_address index for the workers table err = c.Postgres.Exec(ddl.CreateWorkerHostnameAddressIndex).Error if err != nil { @@ -374,5 +369,18 @@ func createServices(c *client) error { return err } + // create the database agnostic user service + // + // https://pkg.go.dev/github.com/go-vela/server/database/user#New + c.UserService, err = user.New( + user.WithClient(c.Postgres), + user.WithEncryptionKey(c.config.EncryptionKey), + user.WithLogger(c.Logger), + user.WithSkipCreation(c.config.SkipCreation), + ) + if err != nil { + return err + } + return nil } diff --git a/database/postgres/postgres_test.go b/database/postgres/postgres_test.go index fd614af9a..e66919618 100644 --- a/database/postgres/postgres_test.go +++ b/database/postgres/postgres_test.go @@ -12,6 +12,7 @@ import ( "github.com/DATA-DOG/go-sqlmock" "github.com/go-vela/server/database/pipeline" "github.com/go-vela/server/database/postgres/ddl" + "github.com/go-vela/server/database/user" ) func TestPostgres_New(t *testing.T) { @@ -80,7 +81,6 @@ func TestPostgres_setupDatabase(t *testing.T) { _mock.ExpectExec(ddl.CreateSecretTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateServiceTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateStepTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateUserTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateWorkerTable).WillReturnResult(sqlmock.NewResult(1, 1)) // ensure the mock expects the index queries @@ -94,7 +94,6 @@ func TestPostgres_setupDatabase(t *testing.T) { _mock.ExpectExec(ddl.CreateSecretTypeOrgRepo).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateSecretTypeOrgTeam).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateSecretTypeOrg).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateWorkerHostnameAddressIndex).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(pipeline.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(pipeline.CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) @@ -165,7 +164,6 @@ func TestPostgres_createTables(t *testing.T) { _mock.ExpectExec(ddl.CreateSecretTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateServiceTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateStepTable).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateUserTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateWorkerTable).WillReturnResult(sqlmock.NewResult(1, 1)) tests := []struct { @@ -215,7 +213,6 @@ func TestPostgres_createIndexes(t *testing.T) { _mock.ExpectExec(ddl.CreateSecretTypeOrgRepo).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateSecretTypeOrgTeam).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateSecretTypeOrg).WillReturnResult(sqlmock.NewResult(1, 1)) - _mock.ExpectExec(ddl.CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(ddl.CreateWorkerHostnameAddressIndex).WillReturnResult(sqlmock.NewResult(1, 1)) tests := []struct { @@ -257,6 +254,8 @@ func TestPostgres_createServices(t *testing.T) { // ensure the mock expects the index queries _mock.ExpectExec(pipeline.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) _mock.ExpectExec(pipeline.CreateRepoIDIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(user.CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(user.CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) tests := []struct { failure bool diff --git a/database/postgres/user.go b/database/postgres/user.go deleted file mode 100644 index ca36ae49c..000000000 --- a/database/postgres/user.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "errors" - "fmt" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetUser gets a user by unique ID from the database. -func (c *client) GetUser(id int64) (*library.User, error) { - c.Logger.Tracef("getting user %d from the database", id) - - // variable to store query results - u := new(database.User) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableUser). - Raw(dml.SelectUser, id). - Scan(u) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err := u.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %d: %v", id, err) - - // return the unencrypted user - return u.ToLibrary(), result.Error - } - - // return the decrypted user - return u.ToLibrary(), result.Error -} - -// GetUserName gets a user by name from the database. -func (c *client) GetUserName(name string) (*library.User, error) { - c.Logger.WithFields(logrus.Fields{ - "user": name, - }).Tracef("getting user %s from the database", name) - - // variable to store query results - u := new(database.User) - - // send query to the database and store result in variable - result := c.Postgres. - Table(constants.TableUser). - Raw(dml.SelectUserName, name). - Scan(u) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err := u.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %s: %v", name, err) - - // return the unencrypted user - return u.ToLibrary(), result.Error - } - - // return the decrypted user - return u.ToLibrary(), result.Error -} - -// CreateUser creates a new user in the database. -// -// nolint: dupl // ignore similar code with update -func (c *client) CreateUser(u *library.User) error { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("creating user %s in the database", u.GetName()) - - // cast to database type - // - // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary - user := database.UserFromLibrary(u) - - // validate the necessary fields are populated - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate - err := user.Validate() - if err != nil { - return err - } - - // encrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt - err = user.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt user %s: %w", u.GetName(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableUser). - Create(user).Error -} - -// UpdateUser updates a user in the database. -// -// nolint: dupl // ignore similar code with create -func (c *client) UpdateUser(u *library.User) error { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("updating user %s in the database", u.GetName()) - - // cast to database type - // - // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary - user := database.UserFromLibrary(u) - - // validate the necessary fields are populated - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate - err := user.Validate() - if err != nil { - return err - } - - // encrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt - err = user.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt user %s: %w", u.GetName(), err) - } - - // send query to the database - return c.Postgres. - Table(constants.TableUser). - Save(user).Error -} - -// DeleteUser deletes a user by unique ID from the database. -func (c *client) DeleteUser(id int64) error { - c.Logger.Tracef("deleting user %d from the database", id) - - // send query to the database - return c.Postgres. - Table(constants.TableUser). - Exec(dml.DeleteUser, id).Error -} diff --git a/database/postgres/user_count.go b/database/postgres/user_count.go deleted file mode 100644 index 9b41092ab..000000000 --- a/database/postgres/user_count.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" -) - -// GetUserCount gets a count of all users from the database. -func (c *client) GetUserCount() (int64, error) { - c.Logger.Trace("getting count of users from the database") - - // variable to store query results - var u int64 - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableUser). - Raw(dml.SelectUsersCount). - Pluck("count", &u).Error - - return u, err -} diff --git a/database/postgres/user_count_test.go b/database/postgres/user_count_test.go deleted file mode 100644 index 55a0bbaf4..000000000 --- a/database/postgres/user_count_test.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetUserCount(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetToken("bar") - _userOne.SetHash("baz") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetToken("foo") - _userTwo.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectUsersCount).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUserCount() - - if test.failure { - if err == nil { - t.Errorf("GetUserCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/user_list.go b/database/postgres/user_list.go deleted file mode 100644 index 0dfb227f4..000000000 --- a/database/postgres/user_list.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" -) - -// GetUserList gets a list of all users from the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) GetUserList() ([]*library.User, error) { - c.Logger.Trace("listing users from the database") - - // variable to store query results - u := new([]database.User) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableUser). - Raw(dml.ListUsers). - Scan(u).Error - if err != nil { - return nil, err - } - - // variable we want to return - users := []*library.User{} - // iterate through all query results - for _, user := range *u { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := user - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary - users = append(users, tmp.ToLibrary()) - } - - return users, nil -} - -// GetUserLiteList gets a lite list of all users from the database. -func (c *client) GetUserLiteList(page, perPage int) ([]*library.User, error) { - c.Logger.Trace("listing lite users from the database") - - // variable to store query results - u := new([]database.User) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Postgres. - Table(constants.TableUser). - Raw(dml.ListLiteUsers, perPage, offset). - Scan(u).Error - - // variable we want to return - users := []*library.User{} - // iterate through all query results - for _, user := range *u { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := user - - // convert query result to library type - users = append(users, tmp.ToLibrary()) - } - - return users, err -} diff --git a/database/postgres/user_list_test.go b/database/postgres/user_list_test.go deleted file mode 100644 index d83b835b2..000000000 --- a/database/postgres/user_list_test.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetUserList(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetToken("bar") - _userOne.SetHash("baz") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetToken("foo") - _userTwo.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListUsers).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}, - ).AddRow(1, "foo", "", "bar", "baz", "{}", false, false). - AddRow(2, "bar", "", "foo", "baz", "{}", false, false) - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.User - }{ - { - failure: false, - want: []*library.User{_userOne, _userTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUserList() - - if test.failure { - if err == nil { - t.Errorf("GetUserList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserList is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_GetUserLiteList(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetFavorites(nil) - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetFavorites(nil) - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.ListLiteUsers, 1, 10).Statement - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id", "name"}).AddRow(1, "foo").AddRow(2, "bar") - - // ensure the mock expects the query - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - want []*library.User - }{ - { - failure: false, - want: []*library.User{_userOne, _userTwo}, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUserLiteList(1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetUserLiteList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserLiteList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserLiteList is %v, want %v", got, test.want) - } - } -} diff --git a/database/postgres/user_test.go b/database/postgres/user_test.go deleted file mode 100644 index f17aed522..000000000 --- a/database/postgres/user_test.go +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package postgres - -import ( - "reflect" - "testing" - - sqlmock "github.com/DATA-DOG/go-sqlmock" - - "github.com/go-vela/server/database/postgres/dml" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -func TestPostgres_Client_GetUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Raw(dml.SelectUser, 1).Statement - - // create expected return in mock - _rows := sqlmock.NewRows( - []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}, - ).AddRow(1, "foo", "", "bar", "baz", "{}", false, false) - - // ensure the mock expects the query for test case 1 - _mock.ExpectQuery(_query.SQL.String()).WillReturnRows(_rows) - // ensure the mock expects the error for test case 2 - _mock.ExpectQuery(_query.SQL.String()).WillReturnError(gorm.ErrRecordNotFound) - - // setup tests - tests := []struct { - failure bool - want *library.User - }{ - { - failure: false, - want: _user, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - got, err := _database.GetUser(1) - - if test.failure { - if err == nil { - t.Errorf("GetUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUser returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUser is %v, want %v", got, test.want) - } - } -} - -func TestPostgres_Client_CreateUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // create expected return in mock - _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) - - // ensure the mock expects the query - _mock.ExpectQuery(`INSERT INTO "users" ("name","refresh_token","token","hash","favorites","active","admin","id") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`). - WithArgs("foo", AnyArgument{}, AnyArgument{}, AnyArgument{}, "{}", false, false, 1). - WillReturnRows(_rows) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.CreateUser(_user) - - if test.failure { - if err == nil { - t.Errorf("CreateUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateUser returned err: %v", err) - } - } -} - -func TestPostgres_Client_UpdateUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // ensure the mock expects the query - _mock.ExpectExec(`UPDATE "users" SET "name"=$1,"refresh_token"=$2,"token"=$3,"hash"=$4,"favorites"=$5,"active"=$6,"admin"=$7 WHERE "id" = $8`). - WithArgs("foo", AnyArgument{}, AnyArgument{}, AnyArgument{}, "{}", false, false, 1). - WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.UpdateUser(_user) - - if test.failure { - if err == nil { - t.Errorf("UpdateUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateUser returned err: %v", err) - } - } -} - -func TestPostgres_Client_DeleteUser(t *testing.T) { - // setup types - // setup the test database client - _database, _mock, err := NewTest() - if err != nil { - t.Errorf("unable to create new postgres test database: %v", err) - } - - defer func() { _sql, _ := _database.Postgres.DB(); _sql.Close() }() - - // capture the current expected SQL query - // - // https://gorm.io/docs/sql_builder.html#DryRun-Mode - _query := _database.Postgres.Session(&gorm.Session{DryRun: true}).Exec(dml.DeleteUser, 1).Statement - - // ensure the mock expects the query - _mock.ExpectExec(_query.SQL.String()).WillReturnResult(sqlmock.NewResult(1, 1)) - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - err := _database.DeleteUser(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteUser returned err: %v", err) - } - } -} - -// testUser is a test helper function to create a -// library User type with all fields set to their -// zero values. -func testUser() *library.User { - i64 := int64(0) - str := "" - arr := []string{} - b := false - - return &library.User{ - ID: &i64, - Name: &str, - RefreshToken: &str, - Token: &str, - Hash: &str, - Favorites: &arr, - Active: &b, - Admin: &b, - } -} diff --git a/database/service.go b/database/service.go index 2c4f880cd..785f65742 100644 --- a/database/service.go +++ b/database/service.go @@ -6,6 +6,7 @@ package database import ( "github.com/go-vela/server/database/pipeline" + "github.com/go-vela/server/database/user" "github.com/go-vela/types/library" ) @@ -239,39 +240,16 @@ type Service interface { // deletes a step by unique ID. DeleteService(int64) error - // User Database Interface Functions - - // GetUser defines a function that - // gets a user by unique ID. - GetUser(int64) (*library.User, error) - // GetUserName defines a function that - // gets a user by name. - GetUserName(string) (*library.User, error) - // GetUserList defines a function that - // gets a list of all users. - GetUserList() ([]*library.User, error) - // GetUserCount defines a function that - // gets the count of users. - GetUserCount() (int64, error) - // GetUserLiteList defines a function - // that gets a lite list of users. - GetUserLiteList(int, int) ([]*library.User, error) - // CreateUser defines a function that - // creates a new user. - CreateUser(*library.User) error - // UpdateUser defines a function that - // updates a user. - UpdateUser(*library.User) error - // DeleteUser defines a function that - // deletes a user by unique ID. - DeleteUser(int64) error + // UserService provides the interface for functionality + // related to users stored in the database. + user.UserService // Worker Database Interface Functions // GetWorker defines a function that // gets a worker by hostname. GetWorker(string) (*library.Worker, error) - // GetWorkerAddress defines a function that + // GetWorkerByAddress defines a function that // gets a worker by address. GetWorkerByAddress(string) (*library.Worker, error) // GetWorkerList defines a function that diff --git a/database/sqlite/ddl/user.go b/database/sqlite/ddl/user.go deleted file mode 100644 index 3f8d77e90..000000000 --- a/database/sqlite/ddl/user.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package ddl - -const ( - // CreateUserTable represents a query to - // create the users table for Vela. - CreateUserTable = ` -CREATE TABLE -IF NOT EXISTS -users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT, - refresh_token TEXT, - token TEXT, - hash TEXT, - favorites TEXT, - active BOOLEAN, - admin BOOLEAN, - UNIQUE(name) -); -` - - // CreateUserRefreshIndex represents a query to create an - // index on the users table for the refresh_token column. - CreateUserRefreshIndex = ` -CREATE INDEX -IF NOT EXISTS -users_refresh -ON users (refresh_token); -` -) diff --git a/database/sqlite/dml/user.go b/database/sqlite/dml/user.go deleted file mode 100644 index 9febce658..000000000 --- a/database/sqlite/dml/user.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package dml - -const ( - // ListUsers represents a query to - // list all users in the database. - ListUsers = ` -SELECT * -FROM users; -` - - // ListLiteUsers represents a query to - // list all lite users in the database. - ListLiteUsers = ` -SELECT id, name -FROM users -ORDER BY id DESC -LIMIT ? -OFFSET ?; -` - - // SelectUser represents a query to select - // a user for an id in the database. - SelectUser = ` -SELECT * -FROM users -WHERE id = ? -LIMIT 1; -` - - // SelectUserName represents a query to select - // a user for a name in the database. - SelectUserName = ` -SELECT * -FROM users -WHERE name = ? -LIMIT 1; -` - - // SelectUsersCount represents a query to select - // the count of users in the database. - SelectUsersCount = ` -SELECT count(*) as count -FROM users; -` - - // SelectRefreshToken represents a query to select - // a user for a refresh_token in the database. - // - // nolint: gosec // ignore false positive - SelectRefreshToken = ` -SELECT * -FROM users -WHERE refresh_token = ? -LIMIT 1; -` - - // DeleteUser represents a query to - // remove a user from the database. - DeleteUser = ` -DELETE -FROM users -WHERE id = ?; -` -) diff --git a/database/sqlite/sqlite.go b/database/sqlite/sqlite.go index 61935ff56..32f476f16 100644 --- a/database/sqlite/sqlite.go +++ b/database/sqlite/sqlite.go @@ -10,6 +10,7 @@ import ( "github.com/go-vela/server/database/pipeline" "github.com/go-vela/server/database/sqlite/ddl" + "github.com/go-vela/server/database/user" "github.com/go-vela/types/constants" "github.com/sirupsen/logrus" @@ -37,10 +38,14 @@ type ( client struct { config *config + // https://pkg.go.dev/gorm.io/gorm#DB Sqlite *gorm.DB // https://pkg.go.dev/github.com/sirupsen/logrus#Entry Logger *logrus.Entry + // https://pkg.go.dev/github.com/go-vela/server/database/pipeline#PipelineService pipeline.PipelineService + // https://pkg.go.dev/github.com/go-vela/server/database/user#UserService + user.UserService } ) @@ -258,12 +263,6 @@ func createTables(c *client) error { return fmt.Errorf("unable to create %s table: %w", constants.TableStep, err) } - // create the users table - err = c.Sqlite.Exec(ddl.CreateUserTable).Error - if err != nil { - return fmt.Errorf("unable to create %s table: %w", constants.TableUser, err) - } - // create the workers table err = c.Sqlite.Exec(ddl.CreateWorkerTable).Error if err != nil { @@ -338,12 +337,6 @@ func createIndexes(c *client) error { return fmt.Errorf("unable to create secrets_type_org index for the %s table: %w", constants.TableSecret, err) } - // create the users_refresh index for the users table - err = c.Sqlite.Exec(ddl.CreateUserRefreshIndex).Error - if err != nil { - return fmt.Errorf("unable to create users_refresh index for the %s table: %w", constants.TableUser, err) - } - // create the workers_hostname_address index for the workers table err = c.Sqlite.Exec(ddl.CreateWorkerHostnameAddressIndex).Error if err != nil { @@ -370,5 +363,18 @@ func createServices(c *client) error { return err } + // create the database agnostic user service + // + // https://pkg.go.dev/github.com/go-vela/server/database/user#New + c.UserService, err = user.New( + user.WithClient(c.Sqlite), + user.WithEncryptionKey(c.config.EncryptionKey), + user.WithLogger(c.Logger), + user.WithSkipCreation(c.config.SkipCreation), + ) + if err != nil { + return err + } + return nil } diff --git a/database/sqlite/sqlite_test.go b/database/sqlite/sqlite_test.go index 713ce897d..6b31f874a 100644 --- a/database/sqlite/sqlite_test.go +++ b/database/sqlite/sqlite_test.go @@ -145,7 +145,7 @@ func TestSqlite_createTables(t *testing.T) { } } -func TestPostgres_createIndexes(t *testing.T) { +func TestSqlite_createIndexes(t *testing.T) { // setup types // setup the test database client _database, err := NewTest() @@ -180,3 +180,39 @@ func TestPostgres_createIndexes(t *testing.T) { } } } + +func TestSqlite_createServices(t *testing.T) { + // setup types + // setup the test database client + _database, err := NewTest() + if err != nil { + t.Errorf("unable to create new sqlite test database: %v", err) + } + + defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() + + tests := []struct { + failure bool + }{ + { + failure: false, + }, + } + + // run tests + for _, test := range tests { + err := createServices(_database) + + if test.failure { + if err == nil { + t.Errorf("createServices should have returned err") + } + + continue + } + + if err != nil { + t.Errorf("createServices returned err: %v", err) + } + } +} diff --git a/database/sqlite/user.go b/database/sqlite/user.go deleted file mode 100644 index 033291854..000000000 --- a/database/sqlite/user.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "errors" - "fmt" - - "github.com/sirupsen/logrus" - - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" - - "gorm.io/gorm" -) - -// GetUser gets a user by unique ID from the database. -func (c *client) GetUser(id int64) (*library.User, error) { - c.Logger.Tracef("getting user %d from the database", id) - - // variable to store query results - u := new(database.User) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableUser). - Raw(dml.SelectUser, id). - Scan(u) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err := u.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %d: %v", id, err) - - // return the unencrypted user - return u.ToLibrary(), result.Error - } - - // return the decrypted user - return u.ToLibrary(), result.Error -} - -// GetUserName gets a user by name from the database. -func (c *client) GetUserName(name string) (*library.User, error) { - c.Logger.WithFields(logrus.Fields{ - "user": name, - }).Tracef("getting user %s from the database", name) - - // variable to store query results - u := new(database.User) - - // send query to the database and store result in variable - result := c.Sqlite. - Table(constants.TableUser). - Raw(dml.SelectUserName, name). - Scan(u) - - // check if the query returned a record not found error or no rows were returned - if errors.Is(result.Error, gorm.ErrRecordNotFound) || result.RowsAffected == 0 { - return nil, gorm.ErrRecordNotFound - } - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err := u.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %s: %v", name, err) - - // return the unencrypted user - return u.ToLibrary(), result.Error - } - - // return the decrypted user - return u.ToLibrary(), result.Error -} - -// CreateUser creates a new user in the database. -// -// nolint: dupl // ignore similar code with update -func (c *client) CreateUser(u *library.User) error { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("creating user %s in the database", u.GetName()) - - // cast to database type - // - // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary - user := database.UserFromLibrary(u) - - // validate the necessary fields are populated - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate - err := user.Validate() - if err != nil { - return err - } - - // encrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt - err = user.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt user %s: %w", u.GetName(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableUser). - Create(user).Error -} - -// UpdateUser updates a user in the database. -// -// nolint: dupl // ignore similar code with create -func (c *client) UpdateUser(u *library.User) error { - c.Logger.WithFields(logrus.Fields{ - "user": u.GetName(), - }).Tracef("updating user %s in the database", u.GetName()) - - // cast to database type - // - // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary - user := database.UserFromLibrary(u) - - // validate the necessary fields are populated - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate - err := user.Validate() - if err != nil { - return err - } - - // encrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt - err = user.Encrypt(c.config.EncryptionKey) - if err != nil { - return fmt.Errorf("unable to encrypt user %s: %w", u.GetName(), err) - } - - // send query to the database - return c.Sqlite. - Table(constants.TableUser). - Save(user).Error -} - -// DeleteUser deletes a user by unique ID from the database. -func (c *client) DeleteUser(id int64) error { - c.Logger.Tracef("deleting user %d from the database", id) - - // send query to the database - return c.Sqlite. - Table(constants.TableUser). - Exec(dml.DeleteUser, id).Error -} diff --git a/database/sqlite/user_count_test.go b/database/sqlite/user_count_test.go deleted file mode 100644 index a991f7f8b..000000000 --- a/database/sqlite/user_count_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the user table - err = _database.Sqlite.Exec(ddl.CreateUserTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableUser, err) - } -} - -func TestSqlite_Client_GetUserCount(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetToken("bar") - _userOne.SetHash("baz") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetToken("foo") - _userTwo.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want int64 - }{ - { - failure: false, - want: 2, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - // create the users in the database - err := _database.CreateUser(_userOne) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - err = _database.CreateUser(_userTwo) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - got, err := _database.GetUserCount() - - if test.failure { - if err == nil { - t.Errorf("GetUserCount should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserCount returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserCount is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/user_list.go b/database/sqlite/user_list.go deleted file mode 100644 index d90863d6b..000000000 --- a/database/sqlite/user_list.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "github.com/go-vela/server/database/sqlite/dml" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/database" - "github.com/go-vela/types/library" -) - -// GetUserList gets a list of all users from the database. -// -// nolint: dupl // ignore false positive of duplicate code -func (c *client) GetUserList() ([]*library.User, error) { - c.Logger.Trace("listing users from the database") - - // variable to store query results - u := new([]database.User) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableUser). - Raw(dml.ListUsers). - Scan(u).Error - if err != nil { - return nil, err - } - - // variable we want to return - users := []*library.User{} - // iterate through all query results - for _, user := range *u { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := user - - // decrypt the fields for the user - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt - err = tmp.Decrypt(c.config.EncryptionKey) - if err != nil { - // ensures that the change is backwards compatible - // by logging the error instead of returning it - // which allows us to fetch unencrypted users - c.Logger.Errorf("unable to decrypt user %d: %v", tmp.ID.Int64, err) - } - - // convert query result to library type - // - // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary - users = append(users, tmp.ToLibrary()) - } - - return users, nil -} - -// GetUserLiteList gets a lite list of all users from the database. -func (c *client) GetUserLiteList(page, perPage int) ([]*library.User, error) { - c.Logger.Trace("listing lite users from the database") - - // variable to store query results - u := new([]database.User) - // calculate offset for pagination through results - offset := perPage * (page - 1) - - // send query to the database and store result in variable - err := c.Sqlite. - Table(constants.TableUser). - Raw(dml.ListLiteUsers, perPage, offset). - Scan(u).Error - - // variable we want to return - users := []*library.User{} - // iterate through all query results - for _, user := range *u { - // https://golang.org/doc/faq#closures_and_goroutines - tmp := user - - // convert query result to library type - users = append(users, tmp.ToLibrary()) - } - - return users, err -} diff --git a/database/sqlite/user_list_test.go b/database/sqlite/user_list_test.go deleted file mode 100644 index 4a626f988..000000000 --- a/database/sqlite/user_list_test.go +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "log" - "reflect" - "testing" - - "github.com/go-vela/server/database/sqlite/ddl" - "github.com/go-vela/types/constants" - "github.com/go-vela/types/library" -) - -func init() { - // setup the test database client - _database, err := NewTest() - if err != nil { - log.Fatalf("unable to create new sqlite test database: %v", err) - } - - // create the user table - err = _database.Sqlite.Exec(ddl.CreateUserTable).Error - if err != nil { - log.Fatalf("unable to create %s table: %v", constants.TableUser, err) - } -} - -func TestSqlite_Client_GetUserList(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - _userOne.SetToken("bar") - _userOne.SetHash("baz") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - _userTwo.SetToken("foo") - _userTwo.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.User - }{ - { - failure: false, - want: []*library.User{_userOne, _userTwo}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - for _, user := range test.want { - // create the user in the database - err := _database.CreateUser(user) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - } - - got, err := _database.GetUserList() - - if test.failure { - if err == nil { - t.Errorf("GetUserList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserList is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_GetUserLiteList(t *testing.T) { - // setup types - _userOne := testUser() - _userOne.SetID(1) - _userOne.SetName("foo") - - _userTwo := testUser() - _userTwo.SetID(2) - _userTwo.SetName("bar") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want []*library.User - }{ - { - failure: false, - want: []*library.User{_userTwo, _userOne}, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - for _, user := range test.want { - // set the required fields for the user - user.SetToken("baz") - user.SetHash("foob") - - // create the user in the database - err := _database.CreateUser(user) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - // clear the required fields for the user - // so we get back the expected data - user.SetToken("") - user.SetHash("") - } - - got, err := _database.GetUserLiteList(1, 10) - - if test.failure { - if err == nil { - t.Errorf("GetUserLiteList should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUserLiteList returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUserLiteList is %v, want %v", got, test.want) - } - } -} diff --git a/database/sqlite/user_test.go b/database/sqlite/user_test.go deleted file mode 100644 index 21f6d9543..000000000 --- a/database/sqlite/user_test.go +++ /dev/null @@ -1,248 +0,0 @@ -// Copyright (c) 2022 Target Brands, Inc. All rights reserved. -// -// Use of this source code is governed by the LICENSE file in this repository. - -package sqlite - -import ( - "reflect" - "testing" - - "github.com/go-vela/types/library" -) - -func TestSqlite_Client_GetUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - want *library.User - }{ - { - failure: false, - want: _user, - }, - { - failure: true, - want: nil, - }, - } - - // run tests - for _, test := range tests { - if test.want != nil { - // create the user in the database - err := _database.CreateUser(test.want) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - } - - got, err := _database.GetUser(1) - - // cleanup the users table - _ = _database.Sqlite.Exec("DELETE FROM users;") - - if test.failure { - if err == nil { - t.Errorf("GetUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("GetUser returned err: %v", err) - } - - if !reflect.DeepEqual(got, test.want) { - t.Errorf("GetUser is %v, want %v", got, test.want) - } - } -} - -func TestSqlite_Client_CreateUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - err := _database.CreateUser(_user) - - if test.failure { - if err == nil { - t.Errorf("CreateUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("CreateUser returned err: %v", err) - } - } -} - -func TestSqlite_Client_UpdateUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - // create the user in the database - err := _database.CreateUser(_user) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - err = _database.UpdateUser(_user) - - if test.failure { - if err == nil { - t.Errorf("UpdateUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("UpdateUser returned err: %v", err) - } - } -} - -func TestSqlite_Client_DeleteUser(t *testing.T) { - // setup types - _user := testUser() - _user.SetID(1) - _user.SetName("foo") - _user.SetToken("bar") - _user.SetHash("baz") - - // setup the test database client - _database, err := NewTest() - if err != nil { - t.Errorf("unable to create new sqlite test database: %v", err) - } - - defer func() { _sql, _ := _database.Sqlite.DB(); _sql.Close() }() - - // setup tests - tests := []struct { - failure bool - }{ - { - failure: false, - }, - } - - // run tests - for _, test := range tests { - // defer cleanup of the users table - defer _database.Sqlite.Exec("delete from users;") - - // create the user in the database - err := _database.CreateUser(_user) - if err != nil { - t.Errorf("unable to create test user: %v", err) - } - - err = _database.DeleteUser(1) - - if test.failure { - if err == nil { - t.Errorf("DeleteUser should have returned err") - } - - continue - } - - if err != nil { - t.Errorf("DeleteUser returned err: %v", err) - } - } -} - -// testUser is a test helper function to create a -// library User type with all fields set to their -// zero values. -func testUser() *library.User { - i64 := int64(0) - str := "" - b := false - - var arr []string - - return &library.User{ - ID: &i64, - Name: &str, - RefreshToken: &str, - Token: &str, - Hash: &str, - Favorites: &arr, - Active: &b, - Admin: &b, - } -} diff --git a/database/sqlite/user_count.go b/database/user/count.go similarity index 53% rename from database/sqlite/user_count.go rename to database/user/count.go index 72b70b003..074a5ef66 100644 --- a/database/sqlite/user_count.go +++ b/database/user/count.go @@ -2,25 +2,24 @@ // // Use of this source code is governed by the LICENSE file in this repository. -package sqlite +package user import ( - "github.com/go-vela/server/database/sqlite/dml" "github.com/go-vela/types/constants" ) -// GetUserCount gets a count of all users from the database. -func (c *client) GetUserCount() (int64, error) { - c.Logger.Trace("getting count of users from the database") +// CountUsers gets the count of all users from the database. +func (e *engine) CountUsers() (int64, error) { + e.logger.Tracef("getting count of all users from the database") // variable to store query results var u int64 // send query to the database and store result in variable - err := c.Sqlite. + err := e.client. Table(constants.TableUser). - Raw(dml.SelectUsersCount). - Pluck("count", &u).Error + Count(&u). + Error return u, err } diff --git a/database/user/count_test.go b/database/user/count_test.go new file mode 100644 index 000000000..be7cb6437 --- /dev/null +++ b/database/user/count_test.go @@ -0,0 +1,93 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_CountUsers(t *testing.T) { + // setup types + _userOne := testUser() + _userOne.SetID(1) + _userOne.SetName("foo") + _userOne.SetToken("bar") + _userOne.SetHash("baz") + + _userTwo := testUser() + _userTwo.SetID(2) + _userTwo.SetName("baz") + _userTwo.SetToken("bar") + _userTwo.SetHash("foo") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "users"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_userOne) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + err = _sqlite.CreateUser(_userTwo) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want int64 + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: 2, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: 2, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.CountUsers() + + if test.failure { + if err == nil { + t.Errorf("CountUsers for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CountUsers for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("CountUsers for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/create.go b/database/user/create.go new file mode 100644 index 000000000..51c960e79 --- /dev/null +++ b/database/user/create.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// nolint: dupl // ignore similar code in update.go +package user + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// CreateUser creates a new user in the database. +func (e *engine) CreateUser(u *library.User) error { + e.logger.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Tracef("creating user %s in the database", u.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary + user := database.UserFromLibrary(u) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate + err := user.Validate() + if err != nil { + return err + } + + // encrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt + err = user.Encrypt(e.config.EncryptionKey) + if err != nil { + return fmt.Errorf("unable to encrypt user %s: %w", u.GetName(), err) + } + + // send query to the database + return e.client. + Table(constants.TableUser). + Create(user). + Error +} diff --git a/database/user/create_test.go b/database/user/create_test.go new file mode 100644 index 000000000..12de80f96 --- /dev/null +++ b/database/user/create_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_CreateUser(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"id"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`INSERT INTO "users" +("name","refresh_token","token","hash","favorites","active","admin","id") +VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`). + WithArgs("foo", AnyArgument{}, AnyArgument{}, AnyArgument{}, nil, false, false, 1). + WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateUser(_user) + + if test.failure { + if err == nil { + t.Errorf("CreateUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateUser for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/delete.go b/database/user/delete.go new file mode 100644 index 000000000..95b77ff64 --- /dev/null +++ b/database/user/delete.go @@ -0,0 +1,30 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// DeleteUser deletes an existing user from the database. +func (e *engine) DeleteUser(u *library.User) error { + e.logger.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Tracef("deleting user %s from the database", u.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary + user := database.UserFromLibrary(u) + + // send query to the database + return e.client. + Table(constants.TableUser). + Delete(user). + Error +} diff --git a/database/user/delete_test.go b/database/user/delete_test.go new file mode 100644 index 000000000..0dec0a6b2 --- /dev/null +++ b/database/user/delete_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_DeleteUser(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`DELETE FROM "users" WHERE "users"."id" = $1`). + WithArgs(1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_user) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.DeleteUser(_user) + + if test.failure { + if err == nil { + t.Errorf("DeleteUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("DeleteUser for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/get.go b/database/user/get.go new file mode 100644 index 000000000..da8b1a0e7 --- /dev/null +++ b/database/user/get.go @@ -0,0 +1,48 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// GetUser gets a user by ID from the database. +func (e *engine) GetUser(id int64) (*library.User, error) { + e.logger.Tracef("getting user %d from the database", id) + + // variable to store query results + u := new(database.User) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableUser). + Where("id = ?", id). + Limit(1). + Scan(u). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt + err = u.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted users + e.logger.Errorf("unable to decrypt user %d: %v", u.ID.Int64, err) + } + + // return the decrypted user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary + return u.ToLibrary(), nil +} diff --git a/database/user/get_name.go b/database/user/get_name.go new file mode 100644 index 000000000..2244bfb86 --- /dev/null +++ b/database/user/get_name.go @@ -0,0 +1,51 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// GetUserForName gets a user by name from the database. +func (e *engine) GetUserForName(name string) (*library.User, error) { + e.logger.WithFields(logrus.Fields{ + "user": name, + }).Tracef("getting user %s from the database", name) + + // variable to store query results + u := new(database.User) + + // send query to the database and store result in variable + err := e.client. + Table(constants.TableUser). + Where("name = ?", name). + Limit(1). + Scan(u). + Error + if err != nil { + return nil, err + } + + // decrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt + err = u.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted users + e.logger.Errorf("unable to decrypt user %d: %v", u.ID.Int64, err) + } + + // return the decrypted user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary + return u.ToLibrary(), nil +} diff --git a/database/user/get_name_test.go b/database/user/get_name_test.go new file mode 100644 index 000000000..4b0a6446b --- /dev/null +++ b/database/user/get_name_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestUser_Engine_GetUserForName(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + _user.SetFavorites([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}). + AddRow(1, "foo", "", "bar", "baz", "{}", false, false) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "users" WHERE name = $1 LIMIT 1`).WithArgs("foo").WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_user) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.User + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _user, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _user, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetUserForName("foo") + + if test.failure { + if err == nil { + t.Errorf("GetUserForName for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetUserForName for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetUserForName for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/get_test.go b/database/user/get_test.go new file mode 100644 index 000000000..4593ce48f --- /dev/null +++ b/database/user/get_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestUser_Engine_GetUser(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + _user.SetFavorites([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows( + []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}). + AddRow(1, "foo", "", "bar", "baz", "{}", false, false) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "users" WHERE id = $1 LIMIT 1`).WithArgs(1).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_user) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want *library.User + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: _user, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: _user, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.GetUser(1) + + if test.failure { + if err == nil { + t.Errorf("GetUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("GetUser for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("GetUser for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/index.go b/database/user/index.go new file mode 100644 index 000000000..5445e963e --- /dev/null +++ b/database/user/index.go @@ -0,0 +1,24 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +const ( + // CreateUserRefreshIndex represents a query to create an + // index on the users table for the refresh_token column. + CreateUserRefreshIndex = ` +CREATE INDEX +IF NOT EXISTS +users_refresh +ON users (refresh_token); +` +) + +// CreateUserIndexes creates the indexes for the users table in the database. +func (e *engine) CreateUserIndexes() error { + e.logger.Tracef("creating indexes for users table in the database") + + // create the refresh_token column index for the users table + return e.client.Exec(CreateUserRefreshIndex).Error +} diff --git a/database/user/index_test.go b/database/user/index_test.go new file mode 100644 index 000000000..55728b77a --- /dev/null +++ b/database/user/index_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_CreateUserIndexes(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateUserIndexes() + + if test.failure { + if err == nil { + t.Errorf("CreateUserIndexes for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateUserIndexes for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/list.go b/database/user/list.go new file mode 100644 index 000000000..4bc730f27 --- /dev/null +++ b/database/user/list.go @@ -0,0 +1,67 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListUsers gets a list of all users from the database. +func (e *engine) ListUsers() ([]*library.User, error) { + e.logger.Trace("listing all users from the database") + + // variables to store query results and return value + count := int64(0) + u := new([]database.User) + users := []*library.User{} + + // count the results + count, err := e.CountUsers() + if err != nil { + return nil, err + } + + // short-circuit if there are no results + if count == 0 { + return users, nil + } + + // send query to the database and store result in variable + err = e.client. + Table(constants.TableUser). + Find(&u). + Error + if err != nil { + return nil, err + } + + // iterate through all query results + for _, user := range *u { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := user + + // decrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Decrypt + err = tmp.Decrypt(e.config.EncryptionKey) + if err != nil { + // TODO: remove backwards compatibility before 1.x.x release + // + // ensures that the change is backwards compatible + // by logging the error instead of returning it + // which allows us to fetch unencrypted users + e.logger.Errorf("unable to decrypt user %d: %v", tmp.ID.Int64, err) + } + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary + users = append(users, tmp.ToLibrary()) + } + + return users, nil +} diff --git a/database/user/list_lite.go b/database/user/list_lite.go new file mode 100644 index 000000000..50957e5f5 --- /dev/null +++ b/database/user/list_lite.go @@ -0,0 +1,61 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" +) + +// ListLiteUsers gets a lite (only: id, name) list of users from the database. +// +// nolint: lll // ignore long line length due to variable names +func (e *engine) ListLiteUsers(page, perPage int) ([]*library.User, int64, error) { + e.logger.Trace("listing lite users from the database") + + // variables to store query results and return values + count := int64(0) + u := new([]database.User) + users := []*library.User{} + + // count the results + count, err := e.CountUsers() + if err != nil { + return users, 0, err + } + + // short-circuit if there are no results + if count == 0 { + return users, 0, nil + } + + // calculate offset for pagination through results + offset := perPage * (page - 1) + + err = e.client. + Table(constants.TableUser). + Select("id", "name"). + Limit(perPage). + Offset(offset). + Find(&u). + Error + if err != nil { + return nil, count, err + } + + // iterate through all query results + for _, user := range *u { + // https://golang.org/doc/faq#closures_and_goroutines + tmp := user + + // convert query result to library type + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.ToLibrary + users = append(users, tmp.ToLibrary()) + } + + return users, count, nil +} diff --git a/database/user/list_lite_test.go b/database/user/list_lite_test.go new file mode 100644 index 000000000..4c44bc20b --- /dev/null +++ b/database/user/list_lite_test.go @@ -0,0 +1,116 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestUser_Engine_ListLiteUsers(t *testing.T) { + // setup types + _userOne := testUser() + _userOne.SetID(1) + _userOne.SetName("foo") + _userOne.SetToken("bar") + _userOne.SetHash("baz") + _userOne.SetFavorites([]string{}) + + _userTwo := testUser() + _userTwo.SetID(2) + _userTwo.SetName("baz") + _userTwo.SetToken("bar") + _userTwo.SetHash("foo") + _userTwo.SetFavorites([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(1) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "users"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "name"}). + AddRow(1, "foo"). + AddRow(2, "baz") + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT "id","name" FROM "users" LIMIT 10`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_userOne) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + err = _sqlite.CreateUser(_userTwo) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // empty fields not returned by query + _userOne.RefreshToken = new(string) + _userOne.Token = new(string) + _userOne.Hash = new(string) + _userOne.Favorites = new([]string) + + _userTwo.RefreshToken = new(string) + _userTwo.Token = new(string) + _userTwo.Hash = new(string) + _userTwo.Favorites = new([]string) + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.User + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.User{_userOne, _userTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.User{_userTwo, _userOne}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, _, err := test.database.ListLiteUsers(1, 10) + + if test.failure { + if err == nil { + t.Errorf("ListLiteUsers for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListLiteUsers for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListLiteUsers for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/list_test.go b/database/user/list_test.go new file mode 100644 index 000000000..61293d44c --- /dev/null +++ b/database/user/list_test.go @@ -0,0 +1,105 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" +) + +func TestUser_Engine_ListUsers(t *testing.T) { + // setup types + _userOne := testUser() + _userOne.SetID(1) + _userOne.SetName("foo") + _userOne.SetToken("bar") + _userOne.SetHash("baz") + _userOne.SetFavorites([]string{}) + + _userTwo := testUser() + _userTwo.SetID(2) + _userTwo.SetName("baz") + _userTwo.SetToken("bar") + _userTwo.SetHash("foo") + _userTwo.SetFavorites([]string{}) + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // create expected result in mock + _rows := sqlmock.NewRows([]string{"count"}).AddRow(2) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT count(*) FROM "users"`).WillReturnRows(_rows) + + // create expected result in mock + _rows = sqlmock.NewRows( + []string{"id", "name", "refresh_token", "token", "hash", "favorites", "active", "admin"}). + AddRow(1, "foo", "", "bar", "baz", "{}", false, false). + AddRow(2, "baz", "", "bar", "foo", "{}", false, false) + + // ensure the mock expects the query + _mock.ExpectQuery(`SELECT * FROM "users"`).WillReturnRows(_rows) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_userOne) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + err = _sqlite.CreateUser(_userTwo) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + want []*library.User + }{ + { + failure: false, + name: "postgres", + database: _postgres, + want: []*library.User{_userOne, _userTwo}, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + want: []*library.User{_userOne, _userTwo}, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := test.database.ListUsers() + + if test.failure { + if err == nil { + t.Errorf("ListUsers for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("ListUsers for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("ListUsers for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} diff --git a/database/user/opts.go b/database/user/opts.go new file mode 100644 index 000000000..58780c317 --- /dev/null +++ b/database/user/opts.go @@ -0,0 +1,54 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +// EngineOpt represents a configuration option to initialize the database engine for Users. +type EngineOpt func(*engine) error + +// WithClient sets the gorm.io/gorm client in the database engine for Users. +func WithClient(client *gorm.DB) EngineOpt { + return func(e *engine) error { + // set the gorm.io/gorm client in the user engine + e.client = client + + return nil + } +} + +// WithEncryptionKey sets the encryption key in the database engine for Users. +func WithEncryptionKey(key string) EngineOpt { + return func(e *engine) error { + // set the encryption key in the user engine + e.config.EncryptionKey = key + + return nil + } +} + +// WithLogger sets the github.com/sirupsen/logrus logger in the database engine for Users. +func WithLogger(logger *logrus.Entry) EngineOpt { + return func(e *engine) error { + // set the github.com/sirupsen/logrus logger in the user engine + e.logger = logger + + return nil + } +} + +// WithSkipCreation sets the skip creation logic in the database engine for Users. +func WithSkipCreation(skipCreation bool) EngineOpt { + return func(e *engine) error { + // set to skip creating tables and indexes in the user engine + e.config.SkipCreation = skipCreation + + return nil + } +} diff --git a/database/user/opts_test.go b/database/user/opts_test.go new file mode 100644 index 000000000..77fb9ae23 --- /dev/null +++ b/database/user/opts_test.go @@ -0,0 +1,210 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "reflect" + "testing" + + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +func TestUser_EngineOpt_WithClient(t *testing.T) { + // setup types + e := &engine{client: new(gorm.DB)} + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + want *gorm.DB + }{ + { + failure: false, + name: "client set to new database", + client: new(gorm.DB), + want: new(gorm.DB), + }, + { + failure: false, + name: "client set to nil", + client: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithClient(test.client)(e) + + if test.failure { + if err == nil { + t.Errorf("WithClient for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithClient returned err: %v", err) + } + + if !reflect.DeepEqual(e.client, test.want) { + t.Errorf("WithClient is %v, want %v", e.client, test.want) + } + }) + } +} + +func TestUser_EngineOpt_WithEncryptionKey(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + key string + want string + }{ + { + failure: false, + name: "encryption key set", + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + want: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + }, + { + failure: false, + name: "encryption key not set", + key: "", + want: "", + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithEncryptionKey(test.key)(e) + + if test.failure { + if err == nil { + t.Errorf("WithEncryptionKey for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithEncryptionKey returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.EncryptionKey, test.want) { + t.Errorf("WithEncryptionKey is %v, want %v", e.config.EncryptionKey, test.want) + } + }) + } +} + +func TestUser_EngineOpt_WithLogger(t *testing.T) { + // setup types + e := &engine{logger: new(logrus.Entry)} + + // setup tests + tests := []struct { + failure bool + name string + logger *logrus.Entry + want *logrus.Entry + }{ + { + failure: false, + name: "logger set to new entry", + logger: new(logrus.Entry), + want: new(logrus.Entry), + }, + { + failure: false, + name: "logger set to nil", + logger: nil, + want: nil, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithLogger(test.logger)(e) + + if test.failure { + if err == nil { + t.Errorf("WithLogger for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithLogger returned err: %v", err) + } + + if !reflect.DeepEqual(e.logger, test.want) { + t.Errorf("WithLogger is %v, want %v", e.logger, test.want) + } + }) + } +} + +func TestUser_EngineOpt_WithSkipCreation(t *testing.T) { + // setup types + e := &engine{config: new(config)} + + // setup tests + tests := []struct { + failure bool + name string + skipCreation bool + want bool + }{ + { + failure: false, + name: "skip creation set to true", + skipCreation: true, + want: true, + }, + { + failure: false, + name: "skip creation set to false", + skipCreation: false, + want: false, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := WithSkipCreation(test.skipCreation)(e) + + if test.failure { + if err == nil { + t.Errorf("WithSkipCreation for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("WithSkipCreation returned err: %v", err) + } + + if !reflect.DeepEqual(e.config.SkipCreation, test.want) { + t.Errorf("WithSkipCreation is %v, want %v", e.config.SkipCreation, test.want) + } + }) + } +} diff --git a/database/user/service.go b/database/user/service.go new file mode 100644 index 000000000..096cea933 --- /dev/null +++ b/database/user/service.go @@ -0,0 +1,45 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/library" +) + +// UserService represents the Vela interface for user +// functions with the supported Database backends. +// +// nolint: revive // ignore name stutter +type UserService interface { + // User Data Definition Language Functions + // + // https://en.wikipedia.org/wiki/Data_definition_language + + // CreateUserIndexes defines a function that creates the indexes for the users table. + CreateUserIndexes() error + // CreateUserTable defines a function that creates the users table. + CreateUserTable(string) error + + // User Data Manipulation Language Functions + // + // https://en.wikipedia.org/wiki/Data_manipulation_language + + // CountUsers defines a function that gets the count of all users. + CountUsers() (int64, error) + // CreateUser defines a function that creates a new user. + CreateUser(*library.User) error + // DeleteUser defines a function that deletes an existing user. + DeleteUser(*library.User) error + // GetUser defines a function that gets a user by ID. + GetUser(int64) (*library.User, error) + // GetUserForName defines a function that gets a user by name. + GetUserForName(string) (*library.User, error) + // ListUsers defines a function that gets a list of all users. + ListUsers() ([]*library.User, error) + // ListLiteUsers defines a function that gets a lite list of users. + ListLiteUsers(int, int) ([]*library.User, int64, error) + // UpdateUser defines a function that updates an existing user. + UpdateUser(*library.User) error +} diff --git a/database/user/table.go b/database/user/table.go new file mode 100644 index 000000000..456853770 --- /dev/null +++ b/database/user/table.go @@ -0,0 +1,62 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "github.com/go-vela/types/constants" +) + +const ( + // CreatePostgresTable represents a query to create the Postgres users table. + CreatePostgresTable = ` +CREATE TABLE +IF NOT EXISTS +users ( + id SERIAL PRIMARY KEY, + name VARCHAR(250), + refresh_token VARCHAR(500), + token VARCHAR(500), + hash VARCHAR(500), + favorites VARCHAR(5000), + active BOOLEAN, + admin BOOLEAN, + UNIQUE(name) +); +` + + // CreateSqliteTable represents a query to create the Sqlite users table. + CreateSqliteTable = ` +CREATE TABLE +IF NOT EXISTS +users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, + refresh_token TEXT, + token TEXT, + hash TEXT, + favorites TEXT, + active BOOLEAN, + admin BOOLEAN, + UNIQUE(name) +); +` +) + +// CreateUserTable creates the users table in the database. +func (e *engine) CreateUserTable(driver string) error { + e.logger.Tracef("creating users table in the database") + + // handle the driver provided to create the table + switch driver { + case constants.DriverPostgres: + // create the users table for Postgres + return e.client.Exec(CreatePostgresTable).Error + case constants.DriverSqlite: + fallthrough + default: + // create the users table for Sqlite + return e.client.Exec(CreateSqliteTable).Error + } +} diff --git a/database/user/table_test.go b/database/user/table_test.go new file mode 100644 index 000000000..95a2d4c00 --- /dev/null +++ b/database/user/table_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_CreateUserTable(t *testing.T) { + // setup types + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.database.CreateUserTable(test.name) + + if test.failure { + if err == nil { + t.Errorf("CreateUserTable for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("CreateUserTable for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/update.go b/database/user/update.go new file mode 100644 index 000000000..f41d80a30 --- /dev/null +++ b/database/user/update.go @@ -0,0 +1,49 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// nolint: dupl // ignore similar code in create.go +package user + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/go-vela/types/database" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" +) + +// UpdateUser updates an existing user in the database. +func (e *engine) UpdateUser(u *library.User) error { + e.logger.WithFields(logrus.Fields{ + "user": u.GetName(), + }).Tracef("updating user %s in the database", u.GetName()) + + // cast the library type to database type + // + // https://pkg.go.dev/github.com/go-vela/types/database#UserFromLibrary + user := database.UserFromLibrary(u) + + // validate the necessary fields are populated + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Validate + err := user.Validate() + if err != nil { + return err + } + + // encrypt the fields for the user + // + // https://pkg.go.dev/github.com/go-vela/types/database#User.Encrypt + err = user.Encrypt(e.config.EncryptionKey) + if err != nil { + return fmt.Errorf("unable to encrypt user %s: %w", u.GetName(), err) + } + + // send query to the database + return e.client. + Table(constants.TableUser). + Save(user). + Error +} diff --git a/database/user/update_test.go b/database/user/update_test.go new file mode 100644 index 000000000..4253ac435 --- /dev/null +++ b/database/user/update_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" +) + +func TestUser_Engine_UpdateUser(t *testing.T) { + // setup types + _user := testUser() + _user.SetID(1) + _user.SetName("foo") + _user.SetToken("bar") + _user.SetHash("baz") + + _postgres, _mock := testPostgres(t) + defer func() { _sql, _ := _postgres.client.DB(); _sql.Close() }() + + // ensure the mock expects the query + _mock.ExpectExec(`UPDATE "users" +SET "name"=$1,"refresh_token"=$2,"token"=$3,"hash"=$4,"favorites"=$5,"active"=$6,"admin"=$7 +WHERE "id" = $8`). + WithArgs("foo", AnyArgument{}, AnyArgument{}, AnyArgument{}, nil, false, false, 1). + WillReturnResult(sqlmock.NewResult(1, 1)) + + _sqlite := testSqlite(t) + defer func() { _sql, _ := _sqlite.client.DB(); _sql.Close() }() + + err := _sqlite.CreateUser(_user) + if err != nil { + t.Errorf("unable to create test user for sqlite: %v", err) + } + + // setup tests + tests := []struct { + failure bool + name string + database *engine + }{ + { + failure: false, + name: "postgres", + database: _postgres, + }, + { + failure: false, + name: "sqlite3", + database: _sqlite, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err = test.database.UpdateUser(_user) + + if test.failure { + if err == nil { + t.Errorf("UpdateUser for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("UpdateUser for %s returned err: %v", test.name, err) + } + }) + } +} diff --git a/database/user/user.go b/database/user/user.go new file mode 100644 index 000000000..e37b18644 --- /dev/null +++ b/database/user/user.go @@ -0,0 +1,82 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "fmt" + + "github.com/go-vela/types/constants" + "github.com/sirupsen/logrus" + + "gorm.io/gorm" +) + +type ( + // config represents the settings required to create the engine that implements the UserService interface. + config struct { + // specifies the encryption key to use for the User engine + EncryptionKey string + // specifies to skip creating tables and indexes for the User engine + SkipCreation bool + } + + // engine represents the user functionality that implements the UserService interface. + engine struct { + // engine configuration settings used in user functions + config *config + + // gorm.io/gorm database client used in user functions + // + // https://pkg.go.dev/gorm.io/gorm#DB + client *gorm.DB + + // sirupsen/logrus logger used in user functions + // + // https://pkg.go.dev/github.com/sirupsen/logrus#Entry + logger *logrus.Entry + } +) + +// New creates and returns a Vela service for integrating with users in the database. +// +// nolint: revive // ignore returning unexported engine +func New(opts ...EngineOpt) (*engine, error) { + // create new User engine + e := new(engine) + + // create new fields + e.client = new(gorm.DB) + e.config = new(config) + e.logger = new(logrus.Entry) + + // apply all provided configuration options + for _, opt := range opts { + err := opt(e) + if err != nil { + return nil, err + } + } + + // check if we should skip creating user database objects + if e.config.SkipCreation { + e.logger.Warning("skipping creation of users table and indexes in the database") + + return e, nil + } + + // create the users table + err := e.CreateUserTable(e.client.Config.Dialector.Name()) + if err != nil { + return nil, fmt.Errorf("unable to create %s table: %w", constants.TableUser, err) + } + + // create the indexes for the users table + err = e.CreateUserIndexes() + if err != nil { + return nil, fmt.Errorf("unable to create indexes for %s table: %w", constants.TableUser, err) + } + + return e, nil +} diff --git a/database/user/user_test.go b/database/user/user_test.go new file mode 100644 index 000000000..1586103a7 --- /dev/null +++ b/database/user/user_test.go @@ -0,0 +1,200 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package user + +import ( + "database/sql/driver" + "reflect" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/go-vela/types/library" + "github.com/sirupsen/logrus" + + "gorm.io/driver/postgres" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func TestUser_New(t *testing.T) { + // setup types + logger := logrus.NewEntry(logrus.StandardLogger()) + + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + defer _sql.Close() + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + _config := &gorm.Config{SkipDefaultTransaction: true} + + _postgres, err := gorm.Open(postgres.New(postgres.Config{Conn: _sql}), _config) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _sqlite, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), _config) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + defer func() { _sql, _ := _sqlite.DB(); _sql.Close() }() + + // setup tests + tests := []struct { + failure bool + name string + client *gorm.DB + key string + logger *logrus.Entry + skipCreation bool + want *engine + }{ + { + failure: false, + name: "postgres", + client: _postgres, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _postgres, + config: &config{EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", SkipCreation: false}, + logger: logger, + }, + }, + { + failure: false, + name: "sqlite3", + client: _sqlite, + key: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", + logger: logger, + skipCreation: false, + want: &engine{ + client: _sqlite, + config: &config{EncryptionKey: "A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW", SkipCreation: false}, + logger: logger, + }, + }, + } + + // run tests + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := New( + WithClient(test.client), + WithEncryptionKey(test.key), + WithLogger(test.logger), + WithSkipCreation(test.skipCreation), + ) + + if test.failure { + if err == nil { + t.Errorf("New for %s should have returned err", test.name) + } + + return + } + + if err != nil { + t.Errorf("New for %s returned err: %v", test.name, err) + } + + if !reflect.DeepEqual(got, test.want) { + t.Errorf("New for %s is %v, want %v", test.name, got, test.want) + } + }) + } +} + +// testPostgres is a helper function to create a Postgres engine for testing. +func testPostgres(t *testing.T) (*engine, sqlmock.Sqlmock) { + // create the new mock sql database + // + // https://pkg.go.dev/github.com/DATA-DOG/go-sqlmock#New + _sql, _mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)) + if err != nil { + t.Errorf("unable to create new SQL mock: %v", err) + } + + _mock.ExpectExec(CreatePostgresTable).WillReturnResult(sqlmock.NewResult(1, 1)) + _mock.ExpectExec(CreateUserRefreshIndex).WillReturnResult(sqlmock.NewResult(1, 1)) + + // create the new mock Postgres database client + // + // https://pkg.go.dev/gorm.io/gorm#Open + _postgres, err := gorm.Open( + postgres.New(postgres.Config{Conn: _sql}), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new postgres database: %v", err) + } + + _engine, err := New( + WithClient(_postgres), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new postgres user engine: %v", err) + } + + return _engine, _mock +} + +// testSqlite is a helper function to create a Sqlite engine for testing. +func testSqlite(t *testing.T) *engine { + _sqlite, err := gorm.Open( + sqlite.Open("file::memory:?cache=shared"), + &gorm.Config{SkipDefaultTransaction: true}, + ) + if err != nil { + t.Errorf("unable to create new sqlite database: %v", err) + } + + _engine, err := New( + WithClient(_sqlite), + WithEncryptionKey("A1B2C3D4E5G6H7I8J9K0LMNOPQRSTUVW"), + WithLogger(logrus.NewEntry(logrus.StandardLogger())), + WithSkipCreation(false), + ) + if err != nil { + t.Errorf("unable to create new sqlite user engine: %v", err) + } + + return _engine +} + +// testUser is a test helper function to create a library +// User type with all fields set to their zero values. +func testUser() *library.User { + return &library.User{ + ID: new(int64), + Name: new(string), + RefreshToken: new(string), + Token: new(string), + Hash: new(string), + Favorites: new([]string), + Active: new(bool), + Admin: new(bool), + } +} + +// This will be used with the github.com/DATA-DOG/go-sqlmock library to compare values +// that are otherwise not easily compared. These typically would be values generated +// before adding or updating them in the database. +// +// https://github.com/DATA-DOG/go-sqlmock#matching-arguments-like-timetime +type AnyArgument struct{} + +// Match satisfies sqlmock.Argument interface. +func (a AnyArgument) Match(v driver.Value) bool { + return true +} diff --git a/router/middleware/token/token.go b/router/middleware/token/token.go index cf40b882a..68c53aa99 100644 --- a/router/middleware/token/token.go +++ b/router/middleware/token/token.go @@ -113,7 +113,7 @@ func Parse(t string, db database.Service) (*library.User, error) { // lookup the user in the database logrus.WithField("user", name).Debugf("reading user %s", name) - u, err = db.GetUserName(name) + u, err = db.GetUserForName(name) return []byte(u.GetHash()), err })