Refactor the websocket client and add fixes
The websocket client and hub interaction has been simplified a bit. The hub now acts only as a tee writer to the various clients that register. Clients must register and unregister explicitly. The hub is no longer passed in to the client. Websocket clients now watch for password changes or jwt token expiration times. Clients are disconnected if auth token expires or if the password is changed. Various aditional safety checks have been added. Signed-off-by: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
This commit is contained in:
parent
ca7f20b62d
commit
dd1740c189
17 changed files with 426 additions and 143 deletions
|
|
@ -284,7 +284,7 @@ func (s *GithubTestSuite) TestCreateCredentials() {
|
|||
func (s *GithubTestSuite) TestCreateCredentialsFailsOnDuplicateCredentials() {
|
||||
ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T())
|
||||
testUser := garmTesting.CreateGARMTestUser(ctx, "testuser", s.db, s.T())
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser)
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser, nil)
|
||||
|
||||
credParams := params.CreateGithubCredentialsParams{
|
||||
Name: testCredsName,
|
||||
|
|
@ -313,8 +313,8 @@ func (s *GithubTestSuite) TestNormalUsersCanOnlySeeTheirOwnCredentialsAdminCanSe
|
|||
ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T())
|
||||
testUser := garmTesting.CreateGARMTestUser(ctx, "testuser1", s.db, s.T())
|
||||
testUser2 := garmTesting.CreateGARMTestUser(ctx, "testuser2", s.db, s.T())
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser)
|
||||
testUser2Ctx := auth.PopulateContext(context.Background(), testUser2)
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser, nil)
|
||||
testUser2Ctx := auth.PopulateContext(context.Background(), testUser2, nil)
|
||||
|
||||
credParams := params.CreateGithubCredentialsParams{
|
||||
Name: testCredsName,
|
||||
|
|
@ -370,7 +370,7 @@ func (s *GithubTestSuite) TestGetGithubCredentialsFailsWhenCredentialsDontExist(
|
|||
func (s *GithubTestSuite) TestGetGithubCredentialsByNameReturnsOnlyCurrentUserCredentials() {
|
||||
ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T())
|
||||
testUser := garmTesting.CreateGARMTestUser(ctx, "test-user1", s.db, s.T())
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser)
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser, nil)
|
||||
|
||||
credParams := params.CreateGithubCredentialsParams{
|
||||
Name: testCredsName,
|
||||
|
|
@ -472,7 +472,7 @@ func (s *GithubTestSuite) TestDeleteGithubCredentials() {
|
|||
func (s *GithubTestSuite) TestDeleteGithubCredentialsByNonAdminUser() {
|
||||
ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T())
|
||||
testUser := garmTesting.CreateGARMTestUser(ctx, "test-user4", s.db, s.T())
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser)
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser, nil)
|
||||
|
||||
credParams := params.CreateGithubCredentialsParams{
|
||||
Name: testCredsName,
|
||||
|
|
@ -682,7 +682,7 @@ func (s *GithubTestSuite) TestUpdateCredentialsFailsForNonExistingCredentials()
|
|||
func (s *GithubTestSuite) TestUpdateCredentialsFailsIfCredentialsAreOwnedByNonAdminUser() {
|
||||
ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T())
|
||||
testUser := garmTesting.CreateGARMTestUser(ctx, "test-user5", s.db, s.T())
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser)
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser, nil)
|
||||
|
||||
credParams := params.CreateGithubCredentialsParams{
|
||||
Name: testCredsName,
|
||||
|
|
@ -711,7 +711,7 @@ func (s *GithubTestSuite) TestUpdateCredentialsFailsIfCredentialsAreOwnedByNonAd
|
|||
func (s *GithubTestSuite) TestAdminUserCanUpdateAnyGithubCredentials() {
|
||||
ctx := garmTesting.ImpersonateAdminContext(context.Background(), s.db, s.T())
|
||||
testUser := garmTesting.CreateGARMTestUser(ctx, "test-user5", s.db, s.T())
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser)
|
||||
testUserCtx := auth.PopulateContext(context.Background(), testUser, nil)
|
||||
|
||||
credParams := params.CreateGithubCredentialsParams{
|
||||
Name: testCredsName,
|
||||
|
|
|
|||
|
|
@ -195,12 +195,13 @@ type Instance struct {
|
|||
type User struct {
|
||||
Base
|
||||
|
||||
Username string `gorm:"uniqueIndex;varchar(64)"`
|
||||
FullName string `gorm:"type:varchar(254)"`
|
||||
Email string `gorm:"type:varchar(254);unique;index:idx_email"`
|
||||
Password string `gorm:"type:varchar(60)"`
|
||||
IsAdmin bool
|
||||
Enabled bool
|
||||
Username string `gorm:"uniqueIndex;varchar(64)"`
|
||||
FullName string `gorm:"type:varchar(254)"`
|
||||
Email string `gorm:"type:varchar(254);unique;index:idx_email"`
|
||||
Password string `gorm:"type:varchar(60)"`
|
||||
Generation uint
|
||||
IsAdmin bool
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type ControllerInfo struct {
|
||||
|
|
|
|||
|
|
@ -239,7 +239,7 @@ func (s *sqlDatabase) migrateCredentialsToDB() (err error) {
|
|||
// user. GARM is not yet multi-user, so it's safe to assume we only have this
|
||||
// one user.
|
||||
adminCtx := context.Background()
|
||||
adminCtx = auth.PopulateContext(adminCtx, adminUser)
|
||||
adminCtx = auth.PopulateContext(adminCtx, adminUser, nil)
|
||||
|
||||
slog.Info("migrating credentials to DB")
|
||||
slog.Info("creating github endpoints table")
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import (
|
|||
"github.com/cloudbase/garm/params"
|
||||
)
|
||||
|
||||
func (s *sqlDatabase) getUserByUsernameOrEmail(user string) (User, error) {
|
||||
func (s *sqlDatabase) getUserByUsernameOrEmail(tx *gorm.DB, user string) (User, error) {
|
||||
field := "username"
|
||||
if util.IsValidEmail(user) {
|
||||
field = "email"
|
||||
|
|
@ -34,7 +34,7 @@ func (s *sqlDatabase) getUserByUsernameOrEmail(user string) (User, error) {
|
|||
query := fmt.Sprintf("%s = ?", field)
|
||||
|
||||
var dbUser User
|
||||
q := s.conn.Model(&User{}).Where(query, user).First(&dbUser)
|
||||
q := tx.Model(&User{}).Where(query, user).First(&dbUser)
|
||||
if q.Error != nil {
|
||||
if errors.Is(q.Error, gorm.ErrRecordNotFound) {
|
||||
return User{}, runnerErrors.ErrNotFound
|
||||
|
|
@ -44,9 +44,9 @@ func (s *sqlDatabase) getUserByUsernameOrEmail(user string) (User, error) {
|
|||
return dbUser, nil
|
||||
}
|
||||
|
||||
func (s *sqlDatabase) getUserByID(userID string) (User, error) {
|
||||
func (s *sqlDatabase) getUserByID(tx *gorm.DB, userID string) (User, error) {
|
||||
var dbUser User
|
||||
q := s.conn.Model(&User{}).Where("id = ?", userID).First(&dbUser)
|
||||
q := tx.Model(&User{}).Where("id = ?", userID).First(&dbUser)
|
||||
if q.Error != nil {
|
||||
if errors.Is(q.Error, gorm.ErrRecordNotFound) {
|
||||
return User{}, runnerErrors.ErrNotFound
|
||||
|
|
@ -57,20 +57,9 @@ func (s *sqlDatabase) getUserByID(userID string) (User, error) {
|
|||
}
|
||||
|
||||
func (s *sqlDatabase) CreateUser(_ context.Context, user params.NewUserParams) (params.User, error) {
|
||||
if user.Username == "" || user.Email == "" {
|
||||
return params.User{}, runnerErrors.NewBadRequestError("missing username or email")
|
||||
if user.Username == "" || user.Email == "" || user.Password == "" {
|
||||
return params.User{}, runnerErrors.NewBadRequestError("missing username, password or email")
|
||||
}
|
||||
if _, err := s.getUserByUsernameOrEmail(user.Username); err == nil || !errors.Is(err, runnerErrors.ErrNotFound) {
|
||||
return params.User{}, runnerErrors.NewConflictError("username already exists")
|
||||
}
|
||||
if _, err := s.getUserByUsernameOrEmail(user.Email); err == nil || !errors.Is(err, runnerErrors.ErrNotFound) {
|
||||
return params.User{}, runnerErrors.NewConflictError("email already exists")
|
||||
}
|
||||
|
||||
if s.HasAdminUser(context.Background()) && user.IsAdmin {
|
||||
return params.User{}, runnerErrors.NewBadRequestError("admin user already exists")
|
||||
}
|
||||
|
||||
newUser := User{
|
||||
Username: user.Username,
|
||||
Password: user.Password,
|
||||
|
|
@ -79,22 +68,42 @@ func (s *sqlDatabase) CreateUser(_ context.Context, user params.NewUserParams) (
|
|||
Email: user.Email,
|
||||
IsAdmin: user.IsAdmin,
|
||||
}
|
||||
err := s.conn.Transaction(func(tx *gorm.DB) error {
|
||||
if _, err := s.getUserByUsernameOrEmail(tx, user.Username); err == nil || !errors.Is(err, runnerErrors.ErrNotFound) {
|
||||
return runnerErrors.NewConflictError("username already exists")
|
||||
}
|
||||
if _, err := s.getUserByUsernameOrEmail(tx, user.Email); err == nil || !errors.Is(err, runnerErrors.ErrNotFound) {
|
||||
return runnerErrors.NewConflictError("email already exists")
|
||||
}
|
||||
|
||||
q := s.conn.Save(&newUser)
|
||||
if q.Error != nil {
|
||||
return params.User{}, errors.Wrap(q.Error, "creating user")
|
||||
if s.hasAdmin(tx) && user.IsAdmin {
|
||||
return runnerErrors.NewBadRequestError("admin user already exists")
|
||||
}
|
||||
|
||||
q := tx.Save(&newUser)
|
||||
if q.Error != nil {
|
||||
return errors.Wrap(q.Error, "creating user")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return params.User{}, errors.Wrap(err, "creating user")
|
||||
}
|
||||
return s.sqlToParamsUser(newUser), nil
|
||||
}
|
||||
|
||||
func (s *sqlDatabase) HasAdminUser(_ context.Context) bool {
|
||||
func (s *sqlDatabase) hasAdmin(tx *gorm.DB) bool {
|
||||
var user User
|
||||
q := s.conn.Model(&User{}).Where("is_admin = ?", true).First(&user)
|
||||
q := tx.Model(&User{}).Where("is_admin = ?", true).First(&user)
|
||||
return q.Error == nil
|
||||
}
|
||||
|
||||
func (s *sqlDatabase) HasAdminUser(_ context.Context) bool {
|
||||
return s.hasAdmin(s.conn)
|
||||
}
|
||||
|
||||
func (s *sqlDatabase) GetUser(_ context.Context, user string) (params.User, error) {
|
||||
dbUser, err := s.getUserByUsernameOrEmail(user)
|
||||
dbUser, err := s.getUserByUsernameOrEmail(s.conn, user)
|
||||
if err != nil {
|
||||
return params.User{}, errors.Wrap(err, "fetching user")
|
||||
}
|
||||
|
|
@ -102,7 +111,7 @@ func (s *sqlDatabase) GetUser(_ context.Context, user string) (params.User, erro
|
|||
}
|
||||
|
||||
func (s *sqlDatabase) GetUserByID(_ context.Context, userID string) (params.User, error) {
|
||||
dbUser, err := s.getUserByID(userID)
|
||||
dbUser, err := s.getUserByID(s.conn, userID)
|
||||
if err != nil {
|
||||
return params.User{}, errors.Wrap(err, "fetching user")
|
||||
}
|
||||
|
|
@ -110,27 +119,35 @@ func (s *sqlDatabase) GetUserByID(_ context.Context, userID string) (params.User
|
|||
}
|
||||
|
||||
func (s *sqlDatabase) UpdateUser(_ context.Context, user string, param params.UpdateUserParams) (params.User, error) {
|
||||
dbUser, err := s.getUserByUsernameOrEmail(user)
|
||||
var err error
|
||||
var dbUser User
|
||||
err = s.conn.Transaction(func(tx *gorm.DB) error {
|
||||
dbUser, err = s.getUserByUsernameOrEmail(tx, user)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "fetching user")
|
||||
}
|
||||
|
||||
if param.FullName != "" {
|
||||
dbUser.FullName = param.FullName
|
||||
}
|
||||
|
||||
if param.Enabled != nil {
|
||||
dbUser.Enabled = *param.Enabled
|
||||
}
|
||||
|
||||
if param.Password != "" {
|
||||
dbUser.Password = param.Password
|
||||
dbUser.Generation++
|
||||
}
|
||||
|
||||
if q := tx.Save(&dbUser); q.Error != nil {
|
||||
return errors.Wrap(q.Error, "saving user")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return params.User{}, errors.Wrap(err, "fetching user")
|
||||
return params.User{}, errors.Wrap(err, "updating user")
|
||||
}
|
||||
|
||||
if param.FullName != "" {
|
||||
dbUser.FullName = param.FullName
|
||||
}
|
||||
|
||||
if param.Enabled != nil {
|
||||
dbUser.Enabled = *param.Enabled
|
||||
}
|
||||
|
||||
if param.Password != "" {
|
||||
dbUser.Password = param.Password
|
||||
}
|
||||
|
||||
if q := s.conn.Save(&dbUser); q.Error != nil {
|
||||
return params.User{}, errors.Wrap(q.Error, "saving user")
|
||||
}
|
||||
|
||||
return s.sqlToParamsUser(dbUser), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ func (s *UserTestSuite) TestCreateUserMissingUsernameEmail() {
|
|||
_, err := s.Store.CreateUser(context.Background(), s.Fixtures.NewUserParams)
|
||||
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(("missing username or email"), err.Error())
|
||||
s.Require().Equal(("missing username, password or email"), err.Error())
|
||||
}
|
||||
|
||||
func (s *UserTestSuite) TestCreateUserUsernameAlreadyExist() {
|
||||
|
|
@ -154,7 +154,7 @@ func (s *UserTestSuite) TestCreateUserUsernameAlreadyExist() {
|
|||
_, err := s.Store.CreateUser(context.Background(), s.Fixtures.NewUserParams)
|
||||
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(("username already exists"), err.Error())
|
||||
s.Require().Equal(("creating user: username already exists"), err.Error())
|
||||
}
|
||||
|
||||
func (s *UserTestSuite) TestCreateUserEmailAlreadyExist() {
|
||||
|
|
@ -163,10 +163,11 @@ func (s *UserTestSuite) TestCreateUserEmailAlreadyExist() {
|
|||
_, err := s.Store.CreateUser(context.Background(), s.Fixtures.NewUserParams)
|
||||
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal(("email already exists"), err.Error())
|
||||
s.Require().Equal(("creating user: email already exists"), err.Error())
|
||||
}
|
||||
|
||||
func (s *UserTestSuite) TestCreateUserDBCreateErr() {
|
||||
s.Fixtures.SQLMock.ExpectBegin()
|
||||
s.Fixtures.SQLMock.
|
||||
ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE username = ? AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT ?")).
|
||||
WithArgs(s.Fixtures.NewUserParams.Username, 1).
|
||||
|
|
@ -175,7 +176,6 @@ func (s *UserTestSuite) TestCreateUserDBCreateErr() {
|
|||
ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE email = ? AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT ?")).
|
||||
WithArgs(s.Fixtures.NewUserParams.Email, 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}))
|
||||
s.Fixtures.SQLMock.ExpectBegin()
|
||||
s.Fixtures.SQLMock.
|
||||
ExpectExec("INSERT INTO `users`").
|
||||
WillReturnError(fmt.Errorf("creating user mock error"))
|
||||
|
|
@ -183,9 +183,9 @@ func (s *UserTestSuite) TestCreateUserDBCreateErr() {
|
|||
|
||||
_, err := s.StoreSQLMocked.CreateUser(context.Background(), s.Fixtures.NewUserParams)
|
||||
|
||||
s.assertSQLMockExpectations()
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("creating user: creating user mock error", err.Error())
|
||||
s.Require().Equal("creating user: creating user: creating user mock error", err.Error())
|
||||
s.assertSQLMockExpectations()
|
||||
}
|
||||
|
||||
func (s *UserTestSuite) TestHasAdminUserNoAdmin() {
|
||||
|
|
@ -253,15 +253,15 @@ func (s *UserTestSuite) TestUpdateUserNotFound() {
|
|||
_, err := s.Store.UpdateUser(context.Background(), "dummy-user", s.Fixtures.UpdateUserParams)
|
||||
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("fetching user: not found", err.Error())
|
||||
s.Require().Equal("updating user: fetching user: not found", err.Error())
|
||||
}
|
||||
|
||||
func (s *UserTestSuite) TestUpdateUserDBSaveErr() {
|
||||
s.Fixtures.SQLMock.ExpectBegin()
|
||||
s.Fixtures.SQLMock.
|
||||
ExpectQuery(regexp.QuoteMeta("SELECT * FROM `users` WHERE username = ? AND `users`.`deleted_at` IS NULL ORDER BY `users`.`id` LIMIT ?")).
|
||||
WithArgs(s.Fixtures.Users[0].ID, 1).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(s.Fixtures.Users[0].ID))
|
||||
s.Fixtures.SQLMock.ExpectBegin()
|
||||
s.Fixtures.SQLMock.
|
||||
ExpectExec(("UPDATE `users` SET")).
|
||||
WillReturnError(fmt.Errorf("saving user mock error"))
|
||||
|
|
@ -271,7 +271,7 @@ func (s *UserTestSuite) TestUpdateUserDBSaveErr() {
|
|||
|
||||
s.assertSQLMockExpectations()
|
||||
s.Require().NotNil(err)
|
||||
s.Require().Equal("saving user: saving user mock error", err.Error())
|
||||
s.Require().Equal("updating user: saving user: saving user mock error", err.Error())
|
||||
}
|
||||
|
||||
func TestUserTestSuite(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -316,15 +316,16 @@ func (s *sqlDatabase) sqlToCommonRepository(repo Repository, detailed bool) (par
|
|||
|
||||
func (s *sqlDatabase) sqlToParamsUser(user User) params.User {
|
||||
return params.User{
|
||||
ID: user.ID.String(),
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
Email: user.Email,
|
||||
Username: user.Username,
|
||||
FullName: user.FullName,
|
||||
Password: user.Password,
|
||||
Enabled: user.Enabled,
|
||||
IsAdmin: user.IsAdmin,
|
||||
ID: user.ID.String(),
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
Email: user.Email,
|
||||
Username: user.Username,
|
||||
FullName: user.FullName,
|
||||
Password: user.Password,
|
||||
Enabled: user.Enabled,
|
||||
IsAdmin: user.IsAdmin,
|
||||
Generation: user.Generation,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue