From 3e372cb6613f72098ede39d42f421b09374bb132 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 21 Nov 2024 18:18:09 -0300 Subject: [PATCH 001/100] feat: add SecureAccessKeyName VO --- src/domain/valueObject/secureAccessKeyName.go | 32 +++++++++++++++++ .../valueObject/secureAccessKeyName_test.go | 36 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/domain/valueObject/secureAccessKeyName.go create mode 100644 src/domain/valueObject/secureAccessKeyName_test.go diff --git a/src/domain/valueObject/secureAccessKeyName.go b/src/domain/valueObject/secureAccessKeyName.go new file mode 100644 index 000000000..e9e439d0c --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyName.go @@ -0,0 +1,32 @@ +package valueObject + +import ( + "errors" + "regexp" + + voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" +) + +const secureAccessKeyNameRegex string = `^[\w@\-_]{6,32}$` + +type SecureAccessKeyName string + +func NewSecureAccessKeyName( + value interface{}, +) (keyName SecureAccessKeyName, err error) { + stringValue, err := voHelper.InterfaceToString(value) + if err != nil { + return keyName, errors.New("SecureAccessKeyNameMustBeString") + } + + re := regexp.MustCompile(secureAccessKeyNameRegex) + if !re.MatchString(stringValue) { + return keyName, errors.New("InvalidSecureAccessKeyName") + } + + return SecureAccessKeyName(stringValue), nil +} + +func (vo SecureAccessKeyName) String() string { + return string(vo) +} diff --git a/src/domain/valueObject/secureAccessKeyName_test.go b/src/domain/valueObject/secureAccessKeyName_test.go new file mode 100644 index 000000000..3ee59a7a6 --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyName_test.go @@ -0,0 +1,36 @@ +package valueObject + +import ( + "testing" +) + +func TestSecureAccessKeyName(t *testing.T) { + t.Run("ValidSecureAccessKeyName", func(t *testing.T) { + rawValidSecureAccessKeyName := []interface{}{ + "myMachine@pop-os", "thats-my-only-pc", "tryingWithThisTypeOfName", + } + + for _, rawKeyName := range rawValidSecureAccessKeyName { + _, err := NewSecureAccessKeyName(rawKeyName) + if err != nil { + t.Errorf( + "Expected no error for '%v', got '%s'", rawKeyName, err.Error(), + ) + } + } + }) + + t.Run("InvalidSecureAccessKeyName", func(t *testing.T) { + rawInvalidSecureAccessKeyName := []interface{}{ + "", "that's not allowed, u know?", "maybe-with-#", + "thisIsAnEnormousNameToTestVoLength", + } + + for _, rawKeyName := range rawInvalidSecureAccessKeyName { + _, err := NewSecureAccessKeyName(rawKeyName) + if err == nil { + t.Errorf("Expected error for '%v', got nil", rawKeyName) + } + } + }) +} From 01484e97e388fb8227dba4c6a505575ad5ee8e54 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 21 Nov 2024 18:18:26 -0300 Subject: [PATCH 002/100] feat: add SecureAccessKeyContent VO --- .../valueObject/secureAccessKeyContent.go | 49 +++++++++++++ .../secureAccessKeyContent_test.go | 71 +++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 src/domain/valueObject/secureAccessKeyContent.go create mode 100644 src/domain/valueObject/secureAccessKeyContent_test.go diff --git a/src/domain/valueObject/secureAccessKeyContent.go b/src/domain/valueObject/secureAccessKeyContent.go new file mode 100644 index 000000000..e3c147333 --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyContent.go @@ -0,0 +1,49 @@ +package valueObject + +import ( + "errors" + "regexp" + "strings" + + voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" +) + +const secureAccessKeyContentRegex string = `^(?:ssh-rsa) (?:[\w\/\+\=]+)(?: [\w@\-_]{6,32})?$` + +type SecureAccessKeyContent string + +func NewSecureAccessKeyContent( + value interface{}, +) (keyContent SecureAccessKeyContent, err error) { + stringValue, err := voHelper.InterfaceToString(value) + if err != nil { + return keyContent, errors.New("SecureAccessKeyContentMustBeString") + } + + re := regexp.MustCompile(secureAccessKeyContentRegex) + if !re.MatchString(stringValue) { + return keyContent, errors.New("InvalidSecureAccessKeyContent") + } + + return SecureAccessKeyContent(stringValue), nil +} + +func (vo SecureAccessKeyContent) String() string { + return string(vo) +} + +func (vo SecureAccessKeyContent) ReadWithoutKeyName() string { + keyContentParts := strings.Split(string(vo), " ") + return keyContentParts[0] + " " + keyContentParts[1] +} + +func (vo SecureAccessKeyContent) ReadOnlyKeyName() ( + keyName SecureAccessKeyName, err error, +) { + keyContentParts := strings.Split(string(vo), " ") + if len(keyContentParts) == 2 { + return keyName, errors.New("SecureAccessKeyNameNotFound") + } + + return NewSecureAccessKeyName(keyContentParts[2]) +} diff --git a/src/domain/valueObject/secureAccessKeyContent_test.go b/src/domain/valueObject/secureAccessKeyContent_test.go new file mode 100644 index 000000000..693f62f3d --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyContent_test.go @@ -0,0 +1,71 @@ +package valueObject + +import "testing" + +func TestSecureAccessKeyContent(t *testing.T) { + t.Run("ValidSecureAccessKeyContent", func(t *testing.T) { + rawValidSecureAccessKeyContent := []interface{}{ + "ssh-rsa c2VjdXJlIGFjY2Vzcy/BrZXkgY29udGV/udCB0ZXN0+U= myMachine@pop-os", + "ssh-rsa c2VjdXJlIGFjY2Vzcy/BrZXkgY29udGV/udCB0ZXN0+U=", + } + + for _, rawKeyContent := range rawValidSecureAccessKeyContent { + _, err := NewSecureAccessKeyContent(rawKeyContent) + if err != nil { + t.Errorf( + "Expected no error for '%v', got '%s'", rawKeyContent, err.Error(), + ) + } + } + }) + + t.Run("InvalidSecureAccessKeyContent", func(t *testing.T) { + rawInvalidSecureAccessKeyContent := []interface{}{ + 12345, 1.25, true, "", "ssh-rsa", "ssh-rsa myMachine@pop-os", + "c2VjdXJlIGFjY2Vzcy/BrZXkgY29udGV/udCB0ZXN0+U= myMachine@pop-os", + } + + for _, rawKeyContent := range rawInvalidSecureAccessKeyContent { + _, err := NewSecureAccessKeyContent(rawKeyContent) + if err == nil { + t.Errorf("Expected error for '%v', got nil", rawKeyContent) + } + } + }) + + t.Run("ReadOnlyKeyName", func(t *testing.T) { + rawValidSecureAccessKeyContentWithName := "ssh-rsa c2VjdXJlIGFjY2Vzcy/BrZXkgY29udGV/udCB0ZXN0+U= myMachine@pop-os" + keyContentWithName, err := NewSecureAccessKeyContent(rawValidSecureAccessKeyContentWithName) + if err != nil { + t.Fatalf( + "Expected no error for '%v', got '%s'", + rawValidSecureAccessKeyContentWithName, err.Error(), + ) + } + + _, err = keyContentWithName.ReadOnlyKeyName() + if err != nil { + t.Errorf( + "Expected no error for '%v', got '%s'", + rawValidSecureAccessKeyContentWithName, err.Error(), + ) + } + + rawValidSecureAccessKeyContentWithoutName := "ssh-rsa c2VjdXJlIGFjY2Vzcy/BrZXkgY29udGV/udCB0ZXN0+U=" + keyContentWithoutName, err := NewSecureAccessKeyContent(rawValidSecureAccessKeyContentWithoutName) + if err != nil { + t.Fatalf( + "Expected no error for '%v', got '%s'", + rawValidSecureAccessKeyContentWithName, err.Error(), + ) + } + + _, err = keyContentWithoutName.ReadOnlyKeyName() + if err == nil { + t.Errorf( + "Expected error for '%v', got nil", + rawValidSecureAccessKeyContentWithName, + ) + } + }) +} From a7c7705ab9e668e1e1903943a5c1e0a210d5714c Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 21 Nov 2024 18:18:38 -0300 Subject: [PATCH 003/100] feat: add SecureAccessKey entity --- src/domain/entity/secureAccessKey.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/domain/entity/secureAccessKey.go diff --git a/src/domain/entity/secureAccessKey.go b/src/domain/entity/secureAccessKey.go new file mode 100644 index 000000000..bbfb15e89 --- /dev/null +++ b/src/domain/entity/secureAccessKey.go @@ -0,0 +1,18 @@ +package entity + +import "github.com/goinfinite/os/src/domain/valueObject" + +type SecureAccessKey struct { + Name valueObject.SecureAccessKeyName `json:"secureAccessKeyName"` + Content valueObject.SecureAccessKeyContent `json:"secureAccessKeyContent"` +} + +func NewSecureAccessKey( + name valueObject.SecureAccessKeyName, + content valueObject.SecureAccessKeyContent, +) SecureAccessKey { + return SecureAccessKey{ + Name: name, + Content: content, + } +} From ba295c96860477d5ad7212c5ad827c363106753d Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 21 Nov 2024 18:33:40 -0300 Subject: [PATCH 004/100] feat: add HashId to SecureAccessKey entity --- src/domain/entity/secureAccessKey.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/domain/entity/secureAccessKey.go b/src/domain/entity/secureAccessKey.go index bbfb15e89..ea182f0f4 100644 --- a/src/domain/entity/secureAccessKey.go +++ b/src/domain/entity/secureAccessKey.go @@ -3,15 +3,18 @@ package entity import "github.com/goinfinite/os/src/domain/valueObject" type SecureAccessKey struct { - Name valueObject.SecureAccessKeyName `json:"secureAccessKeyName"` - Content valueObject.SecureAccessKeyContent `json:"secureAccessKeyContent"` + HashId valueObject.Hash `json:"hashId"` + Name valueObject.SecureAccessKeyName `json:"name"` + Content valueObject.SecureAccessKeyContent `json:"-"` } func NewSecureAccessKey( + hashId valueObject.Hash, name valueObject.SecureAccessKeyName, content valueObject.SecureAccessKeyContent, ) SecureAccessKey { return SecureAccessKey{ + HashId: hashId, Name: name, Content: content, } From 870eea922390843fafd6cadad49a7fac0c66517e Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 21 Nov 2024 19:09:36 -0300 Subject: [PATCH 005/100] feat: add ACCOUNT_SECURE_ACCESS_KEY_SECRET to auto generate env vars --- .env.example | 1 + src/presentation/cli/middleware/checkEnvs.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.env.example b/.env.example index 22ded2211..ec3c7f8dc 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ DUMMY_USER_PASS=example123 ## Must be 32 bytes-key-before-base64-encoding if generated manually: #JWT_SECRET= #ACCOUNT_API_KEY_SECRET= +#ACCOUNT_SECURE_ACCESS_KEY_SECRET= # Optional TRUSTED_IPS= diff --git a/src/presentation/cli/middleware/checkEnvs.go b/src/presentation/cli/middleware/checkEnvs.go index 01d5ca424..592a84d50 100644 --- a/src/presentation/cli/middleware/checkEnvs.go +++ b/src/presentation/cli/middleware/checkEnvs.go @@ -13,12 +13,14 @@ import ( var requiredEnvVars = []string{ "ACCOUNT_API_KEY_SECRET", + "ACCOUNT_SECURE_ACCESS_KEY_SECRET", "JWT_SECRET", "PRIMARY_VHOST", } var envVarsToGenerateIfEmpty = []string{ "ACCOUNT_API_KEY_SECRET", + "ACCOUNT_SECURE_ACCESS_KEY_SECRET", "JWT_SECRET", } From 441b3e1aa81ce1ddecd8214b8912e33efd6aa2a7 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 21 Nov 2024 19:25:52 -0300 Subject: [PATCH 006/100] feat: add SecureAccessKeyHashId VO --- .../valueObject/secureAccessKeyHashId.go | 32 +++++++++++++++++ .../valueObject/secureAccessKeyHashId_test.go | 36 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 src/domain/valueObject/secureAccessKeyHashId.go create mode 100644 src/domain/valueObject/secureAccessKeyHashId_test.go diff --git a/src/domain/valueObject/secureAccessKeyHashId.go b/src/domain/valueObject/secureAccessKeyHashId.go new file mode 100644 index 000000000..5356cc25b --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyHashId.go @@ -0,0 +1,32 @@ +package valueObject + +import ( + "errors" + "regexp" + + voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" +) + +const secureAccessKeyHashIdRegex string = `^[\w\/\+\=]{24,}$` + +type SecureAccessKeyHashId string + +func NewSecureAccessKeyHashId( + value interface{}, +) (keyHashId SecureAccessKeyHashId, err error) { + stringValue, err := voHelper.InterfaceToString(value) + if err != nil { + return keyHashId, errors.New("SecureAccessKeyHashIdMustBeString") + } + + re := regexp.MustCompile(secureAccessKeyHashIdRegex) + if !re.MatchString(stringValue) { + return keyHashId, errors.New("InvalidSecureAccessKeyHashId") + } + + return SecureAccessKeyHashId(stringValue[:24]), nil +} + +func (vo SecureAccessKeyHashId) String() string { + return string(vo) +} diff --git a/src/domain/valueObject/secureAccessKeyHashId_test.go b/src/domain/valueObject/secureAccessKeyHashId_test.go new file mode 100644 index 000000000..1fee838f9 --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyHashId_test.go @@ -0,0 +1,36 @@ +package valueObject + +import ( + "testing" +) + +func TestSecureAccessKeyHashId(t *testing.T) { + t.Run("ValidSecureAccessKeyHashId", func(t *testing.T) { + rawValidSecureAccessKeyHashId := []interface{}{ + "q09qgpsmyqQg3QolSYgQSUYp", "rEJqbCBzJWsSKysWYkHmpsVa", + "kuLH8x2t96AIrPwcBr0kW2fH", "mW5f4ZQzUgxi2kGzZT+aC5Tf", + } + + for _, rawKeyHashId := range rawValidSecureAccessKeyHashId { + _, err := NewSecureAccessKeyHashId(rawKeyHashId) + if err != nil { + t.Errorf( + "Expected no error for '%v', got '%s'", rawKeyHashId, err.Error(), + ) + } + } + }) + + t.Run("InvalidSecureAccessKeyHashId", func(t *testing.T) { + rawInvalidSecureAccessKeyHashId := []interface{}{ + "", 1.50, true, 1000, + } + + for _, rawKeyHashId := range rawInvalidSecureAccessKeyHashId { + _, err := NewSecureAccessKeyHashId(rawKeyHashId) + if err == nil { + t.Errorf("Expected error for '%v', got nil", rawKeyHashId) + } + } + }) +} From 0bd9cf29c16533dce8e2b8ee185d0bfe62cade9c Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 21 Nov 2024 19:26:10 -0300 Subject: [PATCH 007/100] feat: implements SecureAccessKeyHashId VO as HashId type into SecureAccessKey entity --- src/domain/entity/secureAccessKey.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/entity/secureAccessKey.go b/src/domain/entity/secureAccessKey.go index ea182f0f4..154558f87 100644 --- a/src/domain/entity/secureAccessKey.go +++ b/src/domain/entity/secureAccessKey.go @@ -3,13 +3,13 @@ package entity import "github.com/goinfinite/os/src/domain/valueObject" type SecureAccessKey struct { - HashId valueObject.Hash `json:"hashId"` + HashId valueObject.SecureAccessKeyHashId `json:"hashId"` Name valueObject.SecureAccessKeyName `json:"name"` Content valueObject.SecureAccessKeyContent `json:"-"` } func NewSecureAccessKey( - hashId valueObject.Hash, + hashId valueObject.SecureAccessKeyHashId, name valueObject.SecureAccessKeyName, content valueObject.SecureAccessKeyContent, ) SecureAccessKey { From 48fba90b5b4ce8e1d06b1f6e0ba1e9b383e3d8ca Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Fri, 22 Nov 2024 11:02:28 -0300 Subject: [PATCH 008/100] feat: add ReadSecureAccessKeys to AccountQueryRepo infra --- src/domain/repository/accountQueryRepo.go | 3 + src/infra/account/accountQueryRepo.go | 99 +++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/domain/repository/accountQueryRepo.go b/src/domain/repository/accountQueryRepo.go index 9a748b0ad..d4c6fae16 100644 --- a/src/domain/repository/accountQueryRepo.go +++ b/src/domain/repository/accountQueryRepo.go @@ -9,4 +9,7 @@ type AccountQueryRepo interface { Read() ([]entity.Account, error) ReadByUsername(username valueObject.Username) (entity.Account, error) ReadById(accountId valueObject.AccountId) (entity.Account, error) + ReadSecureAccessKeys( + accountId valueObject.AccountId, + ) ([]entity.SecureAccessKey, error) } diff --git a/src/infra/account/accountQueryRepo.go b/src/infra/account/accountQueryRepo.go index 5f97b99b2..098654e28 100644 --- a/src/infra/account/accountQueryRepo.go +++ b/src/infra/account/accountQueryRepo.go @@ -3,9 +3,12 @@ package accountInfra import ( "errors" "log/slog" + "os" + "strings" "github.com/goinfinite/os/src/domain/entity" "github.com/goinfinite/os/src/domain/valueObject" + infraHelper "github.com/goinfinite/os/src/infra/helper" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" dbModel "github.com/goinfinite/os/src/infra/internalDatabase/model" ) @@ -79,3 +82,99 @@ func (repo *AccountQueryRepo) ReadByUsername( return accountModel.ToEntity() } + +func (repo *AccountQueryRepo) secureAccessKeyFactory( + rawSecureAccessKeyContent string, + secureAccessKeySecret string, +) (secureAccessKey entity.SecureAccessKey, err error) { + keyContent, err := valueObject.NewSecureAccessKeyContent( + rawSecureAccessKeyContent, + ) + if err != nil { + return secureAccessKey, err + } + + keyName, err := keyContent.ReadOnlyKeyName() + if err != nil { + return secureAccessKey, err + } + + rawKeyHashId, err := infraHelper.EncryptStr( + secureAccessKeySecret, keyContent.String(), + ) + if err != nil { + return secureAccessKey, err + } + + keyHashId, err := valueObject.NewSecureAccessKeyHashId(rawKeyHashId) + if err != nil { + return secureAccessKey, err + } + + return entity.NewSecureAccessKey(keyHashId, keyName, keyContent), nil +} + +func (repo *AccountQueryRepo) ReadSecureAccessKeys( + accountId valueObject.AccountId, +) ([]entity.SecureAccessKey, error) { + secureAccessKeys := []entity.SecureAccessKey{} + + account, err := repo.ReadById(accountId) + if err != nil { + return secureAccessKeys, errors.New("AccountNotFound") + } + accountUsernameStr := account.Username.String() + + secureAccessKeysDirPath := "/home/" + accountUsernameStr + "/.ssh" + err = infraHelper.MakeDir(secureAccessKeysDirPath) + if err != nil { + return secureAccessKeys, errors.New( + "CreateSecureAccessKeysDirectoryError: " + err.Error(), + ) + } + + secureAccessKeysFilePath := secureAccessKeysDirPath + "/authorized_keys" + if !infraHelper.FileExists(secureAccessKeysFilePath) { + _, err = os.Create(secureAccessKeysFilePath) + if err != nil { + return secureAccessKeys, errors.New( + "CreateSecureAccessKeysFileError: " + err.Error(), + ) + } + + _, err = infraHelper.RunCmd( + "chown", "-R", accountUsernameStr, secureAccessKeysFilePath, + ) + if err != nil { + return secureAccessKeys, errors.New( + "ChownSecureAccessKeysFileError: " + err.Error(), + ) + } + } + + secureAccessKeysFileContent, err := infraHelper.GetFileContent( + secureAccessKeysFilePath, + ) + if err != nil { + return secureAccessKeys, errors.New( + "ReadSecureAccessKeysFileContentError: " + err.Error(), + ) + } + + secretKey := os.Getenv("ACCOUNT_SECURE_ACCESS_KEY_SECRET") + + secureAccessKeysFileContentParts := strings.Split(secureAccessKeysFileContent, "\n") + for index, rawSecureAccessKeyContent := range secureAccessKeysFileContentParts { + secureAccessKey, err := repo.secureAccessKeyFactory( + rawSecureAccessKeyContent, secretKey, + ) + if err != nil { + slog.Debug(err.Error(), slog.Int("index", index)) + continue + } + + secureAccessKeys = append(secureAccessKeys, secureAccessKey) + } + + return secureAccessKeys, nil +} From 36ee4ed462cdbaf29a51071c1d3e54178ac4d62e Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Fri, 22 Nov 2024 16:29:33 -0300 Subject: [PATCH 009/100] refact: using ReadRequestBody API helper instead to create requestBody by my own --- src/presentation/api/controller/account.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/presentation/api/controller/account.go b/src/presentation/api/controller/account.go index 8c655bafd..49bcf4b6b 100644 --- a/src/presentation/api/controller/account.go +++ b/src/presentation/api/controller/account.go @@ -96,8 +96,9 @@ func (controller *AccountController) Update(c echo.Context) error { // @Success 200 {object} object{} "AccountDeleted" // @Router /v1/account/{accountId}/ [delete] func (controller *AccountController) Delete(c echo.Context) error { - requestBody := map[string]interface{}{ - "accountId": c.Param("accountId"), + requestBody, err := apiHelper.ReadRequestBody(c) + if err != nil { + return err } return apiHelper.ServiceResponseWrapper( From d6484e90f48756271e2a2b1a9f81daa7052a6634 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Fri, 22 Nov 2024 17:26:52 -0300 Subject: [PATCH 010/100] fix: ignoring EOF by checking if content is an empty string --- src/infra/account/accountQueryRepo.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/infra/account/accountQueryRepo.go b/src/infra/account/accountQueryRepo.go index 098654e28..3f0415028 100644 --- a/src/infra/account/accountQueryRepo.go +++ b/src/infra/account/accountQueryRepo.go @@ -165,6 +165,10 @@ func (repo *AccountQueryRepo) ReadSecureAccessKeys( secureAccessKeysFileContentParts := strings.Split(secureAccessKeysFileContent, "\n") for index, rawSecureAccessKeyContent := range secureAccessKeysFileContentParts { + if rawSecureAccessKeyContent == "" { + continue + } + secureAccessKey, err := repo.secureAccessKeyFactory( rawSecureAccessKeyContent, secretKey, ) From b10442d4e3fc74f634c201ec091c3798bf75d32c Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Fri, 22 Nov 2024 17:27:35 -0300 Subject: [PATCH 011/100] feat: add ReadAccountSecureAccessKeys use case --- .../useCase/readAccountSecureAccessKeys.go | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/domain/useCase/readAccountSecureAccessKeys.go diff --git a/src/domain/useCase/readAccountSecureAccessKeys.go b/src/domain/useCase/readAccountSecureAccessKeys.go new file mode 100644 index 000000000..7589057f0 --- /dev/null +++ b/src/domain/useCase/readAccountSecureAccessKeys.go @@ -0,0 +1,23 @@ +package useCase + +import ( + "errors" + "log/slog" + + "github.com/goinfinite/os/src/domain/entity" + "github.com/goinfinite/os/src/domain/repository" + "github.com/goinfinite/os/src/domain/valueObject" +) + +func ReadAccountSecureAccessKeys( + accountQueryRepo repository.AccountQueryRepo, + accountId valueObject.AccountId, +) (secureAccessKeys []entity.SecureAccessKey, err error) { + secureAccessKeys, err = accountQueryRepo.ReadSecureAccessKeys(accountId) + if err != nil { + slog.Error("ReadAccountSecureAccessKeysInfraError", slog.Any("error", err)) + return secureAccessKeys, errors.New("ReadAccountSecureAccessKeysInfraError") + } + + return secureAccessKeys, nil +} From e25551e695c67418d8e95eca71e58a63d05b7181 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Fri, 22 Nov 2024 17:28:18 -0300 Subject: [PATCH 012/100] feat: implements new get route to API and CLI to read account secure access keys --- src/presentation/api/controller/account.go | 20 +++++++++++ src/presentation/api/router.go | 1 + src/presentation/cli/controller/account.go | 22 ++++++++++++ src/presentation/cli/router.go | 1 + src/presentation/service/account.go | 42 ++++++++++++++++++++++ 5 files changed, 86 insertions(+) diff --git a/src/presentation/api/controller/account.go b/src/presentation/api/controller/account.go index 49bcf4b6b..8ac9ac610 100644 --- a/src/presentation/api/controller/account.go +++ b/src/presentation/api/controller/account.go @@ -105,3 +105,23 @@ func (controller *AccountController) Delete(c echo.Context) error { c, controller.accountService.Delete(requestBody), ) } + +// ReadSecureAccessKeys godoc +// @Summary ReadSecureAccessKeys +// @Description List accounts secure access keys. +// @Tags account +// @Accept json +// @Produce json +// @Security Bearer +// @Success 200 {array} entity.SecureAccessKey +// @Router /v1/account/secure-access-key [get] +func (controller *AccountController) ReadSecureAccessKey(c echo.Context) error { + requestBody, err := apiHelper.ReadRequestBody(c) + if err != nil { + return err + } + + return apiHelper.ServiceResponseWrapper( + c, controller.accountService.ReadSecureAccessKey(requestBody), + ) +} diff --git a/src/presentation/api/router.go b/src/presentation/api/router.go index 6d989d5ba..028dd9b01 100644 --- a/src/presentation/api/router.go +++ b/src/presentation/api/router.go @@ -56,6 +56,7 @@ func (router Router) accountRoutes() { accountGroup.POST("/", accountController.Create) accountGroup.PUT("/", accountController.Update) accountGroup.DELETE("/:accountId/", accountController.Delete) + accountGroup.GET("/secure-access-key/", accountController.ReadSecureAccessKey) } func (router Router) cronRoutes() { diff --git a/src/presentation/cli/controller/account.go b/src/presentation/cli/controller/account.go index e3737dd64..72868af3d 100644 --- a/src/presentation/cli/controller/account.go +++ b/src/presentation/cli/controller/account.go @@ -116,3 +116,25 @@ func (controller *AccountController) Delete() *cobra.Command { cmd.MarkFlagRequired("account-id") return cmd } + +func (controller *AccountController) ReadSecureAccessKeys() *cobra.Command { + var accountIdStr string + + cmd := &cobra.Command{ + Use: "get", + Short: "GetSecureAccessKeys", + Run: func(cmd *cobra.Command, args []string) { + requestBody := map[string]interface{}{ + "accountId": accountIdStr, + } + + cliHelper.ServiceResponseWrapper( + controller.accountService.ReadSecureAccessKey(requestBody), + ) + }, + } + + cmd.Flags().StringVarP(&accountIdStr, "account-id", "i", "", "AccountId") + cmd.MarkFlagRequired("account-id") + return cmd +} diff --git a/src/presentation/cli/router.go b/src/presentation/cli/router.go index f933c2456..f8f9e7b8d 100644 --- a/src/presentation/cli/router.go +++ b/src/presentation/cli/router.go @@ -51,6 +51,7 @@ func (router Router) accountRoutes() { accountCmd.AddCommand(accountController.Create()) accountCmd.AddCommand(accountController.Update()) accountCmd.AddCommand(accountController.Delete()) + accountCmd.AddCommand(accountController.ReadSecureAccessKeys()) } func (router Router) authenticationRoutes() { diff --git a/src/presentation/service/account.go b/src/presentation/service/account.go index e1e3766ed..e7ddca2f3 100644 --- a/src/presentation/service/account.go +++ b/src/presentation/service/account.go @@ -9,6 +9,7 @@ import ( activityRecordInfra "github.com/goinfinite/os/src/infra/activityRecord" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" serviceHelper "github.com/goinfinite/os/src/presentation/service/helper" + sharedHelper "github.com/goinfinite/os/src/presentation/shared/helper" ) var LocalOperatorAccountId, _ = valueObject.NewAccountId(0) @@ -19,6 +20,7 @@ type AccountService struct { accountQueryRepo *accountInfra.AccountQueryRepo accountCmdRepo *accountInfra.AccountCmdRepo activityRecordCmdRepo *activityRecordInfra.ActivityRecordCmdRepo + availabilityInspector *sharedHelper.ServiceAvailabilityInspector } func NewAccountService( @@ -30,6 +32,9 @@ func NewAccountService( accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), accountCmdRepo: accountInfra.NewAccountCmdRepo(persistentDbSvc), activityRecordCmdRepo: activityRecordInfra.NewActivityRecordCmdRepo(trailDbSvc), + availabilityInspector: sharedHelper.NewServiceAvailabilityInspector( + persistentDbSvc, + ), } } @@ -207,3 +212,40 @@ func (service *AccountService) Delete(input map[string]interface{}) ServiceOutpu return NewServiceOutput(Success, "AccountDeleted") } + +func (service *AccountService) ReadSecureAccessKey( + input map[string]interface{}, +) ServiceOutput { + serviceName, _ := valueObject.NewServiceName("openssh") + if !service.availabilityInspector.IsAvailable(serviceName) { + return NewServiceOutput(InfraError, sharedHelper.ServiceUnavailableError) + } + + if input["id"] != nil { + input["accountId"] = input["id"] + } + + if input["accountId"] == nil { + input["accountId"] = input["operatorAccountId"] + } + + requiredParams := []string{"accountId"} + err := serviceHelper.RequiredParamsInspector(input, requiredParams) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + + accountId, err := valueObject.NewAccountId(input["accountId"]) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + + secureAccessKeys, err := useCase.ReadAccountSecureAccessKeys( + service.accountQueryRepo, accountId, + ) + if err != nil { + return NewServiceOutput(InfraError, err.Error()) + } + + return NewServiceOutput(Success, secureAccessKeys) +} From 4aa99e9cffdb180a56fa5c55c576da2cf0cea179 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 11:55:17 -0300 Subject: [PATCH 013/100] refact: change HashId to Uuid and add encoded content to entity to return encoded content instead default content --- src/domain/entity/secureAccessKey.go | 17 +++++---- .../valueObject/secureAccessKeyHashId.go | 32 ---------------- .../valueObject/secureAccessKeyHashId_test.go | 36 ------------------ src/domain/valueObject/secureAccessKeyUuid.go | 32 ++++++++++++++++ .../valueObject/secureAccessKeyUuid_test.go | 37 +++++++++++++++++++ src/infra/account/accountQueryRepo.go | 17 ++++++--- 6 files changed, 91 insertions(+), 80 deletions(-) delete mode 100644 src/domain/valueObject/secureAccessKeyHashId.go delete mode 100644 src/domain/valueObject/secureAccessKeyHashId_test.go create mode 100644 src/domain/valueObject/secureAccessKeyUuid.go create mode 100644 src/domain/valueObject/secureAccessKeyUuid_test.go diff --git a/src/domain/entity/secureAccessKey.go b/src/domain/entity/secureAccessKey.go index 154558f87..adff0c7d6 100644 --- a/src/domain/entity/secureAccessKey.go +++ b/src/domain/entity/secureAccessKey.go @@ -3,19 +3,22 @@ package entity import "github.com/goinfinite/os/src/domain/valueObject" type SecureAccessKey struct { - HashId valueObject.SecureAccessKeyHashId `json:"hashId"` - Name valueObject.SecureAccessKeyName `json:"name"` - Content valueObject.SecureAccessKeyContent `json:"-"` + Id valueObject.SecureAccessKeyUuid `json:"id"` + Name valueObject.SecureAccessKeyName `json:"name"` + Content valueObject.SecureAccessKeyContent `json:"-"` + EncodedContent valueObject.EncodedContent `json:"encodedContent"` } func NewSecureAccessKey( - hashId valueObject.SecureAccessKeyHashId, + id valueObject.SecureAccessKeyUuid, name valueObject.SecureAccessKeyName, content valueObject.SecureAccessKeyContent, + encodedContent valueObject.EncodedContent, ) SecureAccessKey { return SecureAccessKey{ - HashId: hashId, - Name: name, - Content: content, + Id: id, + Name: name, + Content: content, + EncodedContent: encodedContent, } } diff --git a/src/domain/valueObject/secureAccessKeyHashId.go b/src/domain/valueObject/secureAccessKeyHashId.go deleted file mode 100644 index 5356cc25b..000000000 --- a/src/domain/valueObject/secureAccessKeyHashId.go +++ /dev/null @@ -1,32 +0,0 @@ -package valueObject - -import ( - "errors" - "regexp" - - voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" -) - -const secureAccessKeyHashIdRegex string = `^[\w\/\+\=]{24,}$` - -type SecureAccessKeyHashId string - -func NewSecureAccessKeyHashId( - value interface{}, -) (keyHashId SecureAccessKeyHashId, err error) { - stringValue, err := voHelper.InterfaceToString(value) - if err != nil { - return keyHashId, errors.New("SecureAccessKeyHashIdMustBeString") - } - - re := regexp.MustCompile(secureAccessKeyHashIdRegex) - if !re.MatchString(stringValue) { - return keyHashId, errors.New("InvalidSecureAccessKeyHashId") - } - - return SecureAccessKeyHashId(stringValue[:24]), nil -} - -func (vo SecureAccessKeyHashId) String() string { - return string(vo) -} diff --git a/src/domain/valueObject/secureAccessKeyHashId_test.go b/src/domain/valueObject/secureAccessKeyHashId_test.go deleted file mode 100644 index 1fee838f9..000000000 --- a/src/domain/valueObject/secureAccessKeyHashId_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package valueObject - -import ( - "testing" -) - -func TestSecureAccessKeyHashId(t *testing.T) { - t.Run("ValidSecureAccessKeyHashId", func(t *testing.T) { - rawValidSecureAccessKeyHashId := []interface{}{ - "q09qgpsmyqQg3QolSYgQSUYp", "rEJqbCBzJWsSKysWYkHmpsVa", - "kuLH8x2t96AIrPwcBr0kW2fH", "mW5f4ZQzUgxi2kGzZT+aC5Tf", - } - - for _, rawKeyHashId := range rawValidSecureAccessKeyHashId { - _, err := NewSecureAccessKeyHashId(rawKeyHashId) - if err != nil { - t.Errorf( - "Expected no error for '%v', got '%s'", rawKeyHashId, err.Error(), - ) - } - } - }) - - t.Run("InvalidSecureAccessKeyHashId", func(t *testing.T) { - rawInvalidSecureAccessKeyHashId := []interface{}{ - "", 1.50, true, 1000, - } - - for _, rawKeyHashId := range rawInvalidSecureAccessKeyHashId { - _, err := NewSecureAccessKeyHashId(rawKeyHashId) - if err == nil { - t.Errorf("Expected error for '%v', got nil", rawKeyHashId) - } - } - }) -} diff --git a/src/domain/valueObject/secureAccessKeyUuid.go b/src/domain/valueObject/secureAccessKeyUuid.go new file mode 100644 index 000000000..db4608102 --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyUuid.go @@ -0,0 +1,32 @@ +package valueObject + +import ( + "errors" + "regexp" + + voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" +) + +const secureAccessKeyUuidRegex string = `^\w{10,16}$` + +type SecureAccessKeyUuid string + +func NewSecureAccessKeyUuid( + value interface{}, +) (keyUuid SecureAccessKeyUuid, err error) { + stringValue, err := voHelper.InterfaceToString(value) + if err != nil { + return keyUuid, errors.New("SecureAccessKeyUuidMustBeString") + } + + re := regexp.MustCompile(secureAccessKeyUuidRegex) + if !re.MatchString(stringValue) { + return keyUuid, errors.New("InvalidSecureAccessKeyUuid") + } + + return SecureAccessKeyUuid(stringValue), nil +} + +func (vo SecureAccessKeyUuid) String() string { + return string(vo) +} diff --git a/src/domain/valueObject/secureAccessKeyUuid_test.go b/src/domain/valueObject/secureAccessKeyUuid_test.go new file mode 100644 index 000000000..14694dff7 --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyUuid_test.go @@ -0,0 +1,37 @@ +package valueObject + +import ( + "testing" +) + +func TestSecureAccessKeyUuid(t *testing.T) { + t.Run("ValidSecureAccessKeyUuid", func(t *testing.T) { + rawValidSecureAccessKeyUuid := []interface{}{ + "abc123def4", "1234567890ab", "abcdef123456", "9876543210ab", + "1234abcd5678", + } + + for _, rawKeyUuid := range rawValidSecureAccessKeyUuid { + _, err := NewSecureAccessKeyUuid(rawKeyUuid) + if err != nil { + t.Errorf( + "Expected no error for '%v', got '%s'", rawKeyUuid, err.Error(), + ) + } + } + }) + + t.Run("InvalidSecureAccessKeyUuid", func(t *testing.T) { + rawInvalidSecureAccessKeyUuid := []interface{}{ + 1234, true, 1.40, "abc123", "tooLongSecureAccessKeyUuid", "12345678!@#", + "short12", + } + + for _, rawKeyUuid := range rawInvalidSecureAccessKeyUuid { + _, err := NewSecureAccessKeyUuid(rawKeyUuid) + if err == nil { + t.Errorf("Expected error for '%v', got nil", rawKeyUuid) + } + } + }) +} diff --git a/src/infra/account/accountQueryRepo.go b/src/infra/account/accountQueryRepo.go index 3f0415028..e37a47611 100644 --- a/src/infra/account/accountQueryRepo.go +++ b/src/infra/account/accountQueryRepo.go @@ -11,6 +11,7 @@ import ( infraHelper "github.com/goinfinite/os/src/infra/helper" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" dbModel "github.com/goinfinite/os/src/infra/internalDatabase/model" + "github.com/google/uuid" ) type AccountQueryRepo struct { @@ -87,6 +88,13 @@ func (repo *AccountQueryRepo) secureAccessKeyFactory( rawSecureAccessKeyContent string, secureAccessKeySecret string, ) (secureAccessKey entity.SecureAccessKey, err error) { + rawKeyUuid := uuid.New().String()[:16] + rawKeyUuidNoHyphens := strings.Replace(rawKeyUuid, "-", "", -1) + keyUuid, err := valueObject.NewSecureAccessKeyUuid(rawKeyUuidNoHyphens) + if err != nil { + return secureAccessKey, err + } + keyContent, err := valueObject.NewSecureAccessKeyContent( rawSecureAccessKeyContent, ) @@ -99,19 +107,18 @@ func (repo *AccountQueryRepo) secureAccessKeyFactory( return secureAccessKey, err } - rawKeyHashId, err := infraHelper.EncryptStr( - secureAccessKeySecret, keyContent.String(), + rawKeyHashContent, err := infraHelper.EncryptStr( + secureAccessKeySecret, keyContent.ReadWithoutKeyName(), ) if err != nil { return secureAccessKey, err } - - keyHashId, err := valueObject.NewSecureAccessKeyHashId(rawKeyHashId) + keyHashContent, err := valueObject.NewEncodedContent(rawKeyHashContent) if err != nil { return secureAccessKey, err } - return entity.NewSecureAccessKey(keyHashId, keyName, keyContent), nil + return entity.NewSecureAccessKey(keyUuid, keyName, keyContent, keyHashContent), nil } func (repo *AccountQueryRepo) ReadSecureAccessKeys( From bc003757a8029014b74a94384fdbfd94014b8182 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 12:54:15 -0300 Subject: [PATCH 014/100] feat: add CreateSecureAccessKey DTO --- src/domain/dto/createSecureAccessKey.go | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/domain/dto/createSecureAccessKey.go diff --git a/src/domain/dto/createSecureAccessKey.go b/src/domain/dto/createSecureAccessKey.go new file mode 100644 index 000000000..b34009738 --- /dev/null +++ b/src/domain/dto/createSecureAccessKey.go @@ -0,0 +1,26 @@ +package dto + +import "github.com/goinfinite/os/src/domain/valueObject" + +type CreateSecureAccessKey struct { + Name valueObject.SecureAccessKeyName `json:"name"` + Content valueObject.SecureAccessKeyContent `json:"content"` + AccountId valueObject.AccountId `json:"accountId"` + OperatorAccountId valueObject.AccountId `json:"-"` + OperatorIpAddress valueObject.IpAddress `json:"-"` +} + +func NewCreateSecureAccessKey( + name valueObject.SecureAccessKeyName, + content valueObject.SecureAccessKeyContent, + accountId, operatorAccountId valueObject.AccountId, + operatorIpAddress valueObject.IpAddress, +) CreateSecureAccessKey { + return CreateSecureAccessKey{ + Name: name, + Content: content, + AccountId: accountId, + OperatorAccountId: operatorAccountId, + OperatorIpAddress: operatorIpAddress, + } +} From 02837aae666f732ad7c357041d5a9d5e8aa888e3 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 13:10:34 -0300 Subject: [PATCH 015/100] refact: change error name when key content has no name --- src/domain/valueObject/secureAccessKeyContent.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/valueObject/secureAccessKeyContent.go b/src/domain/valueObject/secureAccessKeyContent.go index e3c147333..7dc622051 100644 --- a/src/domain/valueObject/secureAccessKeyContent.go +++ b/src/domain/valueObject/secureAccessKeyContent.go @@ -42,7 +42,7 @@ func (vo SecureAccessKeyContent) ReadOnlyKeyName() ( ) { keyContentParts := strings.Split(string(vo), " ") if len(keyContentParts) == 2 { - return keyName, errors.New("SecureAccessKeyNameNotFound") + return keyName, errors.New("SecureAccessKeyContentHasNoName") } return NewSecureAccessKeyName(keyContentParts[2]) From f96f175f0642c510a2ce4e3db215b94ec263dfdc Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 13:39:14 -0300 Subject: [PATCH 016/100] refact: change keyId to uint16 instead uuid --- src/domain/entity/secureAccessKey.go | 4 +- src/domain/valueObject/secureAccessKeyId.go | 27 ++++++++++++++ .../valueObject/secureAccessKeyId_test.go | 37 +++++++++++++++++++ src/domain/valueObject/secureAccessKeyUuid.go | 32 ---------------- .../valueObject/secureAccessKeyUuid_test.go | 37 ------------------- src/infra/account/accountQueryRepo.go | 11 +++--- 6 files changed, 71 insertions(+), 77 deletions(-) create mode 100644 src/domain/valueObject/secureAccessKeyId.go create mode 100644 src/domain/valueObject/secureAccessKeyId_test.go delete mode 100644 src/domain/valueObject/secureAccessKeyUuid.go delete mode 100644 src/domain/valueObject/secureAccessKeyUuid_test.go diff --git a/src/domain/entity/secureAccessKey.go b/src/domain/entity/secureAccessKey.go index adff0c7d6..b2bae9c1c 100644 --- a/src/domain/entity/secureAccessKey.go +++ b/src/domain/entity/secureAccessKey.go @@ -3,14 +3,14 @@ package entity import "github.com/goinfinite/os/src/domain/valueObject" type SecureAccessKey struct { - Id valueObject.SecureAccessKeyUuid `json:"id"` + Id valueObject.SecureAccessKeyId `json:"id"` Name valueObject.SecureAccessKeyName `json:"name"` Content valueObject.SecureAccessKeyContent `json:"-"` EncodedContent valueObject.EncodedContent `json:"encodedContent"` } func NewSecureAccessKey( - id valueObject.SecureAccessKeyUuid, + id valueObject.SecureAccessKeyId, name valueObject.SecureAccessKeyName, content valueObject.SecureAccessKeyContent, encodedContent valueObject.EncodedContent, diff --git a/src/domain/valueObject/secureAccessKeyId.go b/src/domain/valueObject/secureAccessKeyId.go new file mode 100644 index 000000000..714a6d32e --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyId.go @@ -0,0 +1,27 @@ +package valueObject + +import ( + "errors" + "strconv" + + voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" +) + +type SecureAccessKeyId uint16 + +func NewSecureAccessKeyId(value interface{}) (keyId SecureAccessKeyId, err error) { + uintValue, err := voHelper.InterfaceToUint(value) + if err != nil { + return keyId, errors.New("SecureAccessKeyIdMustBeUint") + } + + return SecureAccessKeyId(uintValue), nil +} + +func (vo SecureAccessKeyId) Uint16() uint16 { + return uint16(vo) +} + +func (vo SecureAccessKeyId) String() string { + return strconv.FormatUint(uint64(vo), 10) +} diff --git a/src/domain/valueObject/secureAccessKeyId_test.go b/src/domain/valueObject/secureAccessKeyId_test.go new file mode 100644 index 000000000..fd3aa917b --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyId_test.go @@ -0,0 +1,37 @@ +package valueObject + +import ( + "testing" +) + +func TestSecureAccessKeyId(t *testing.T) { + t.Run("ValidSecureAccessKeyId", func(t *testing.T) { + rawValidSecureAccessKeyId := []interface{}{ + "0", int(0), int8(0), int16(0), int32(0), int64(0), uint(0), uint8(0), + uint16(0), uint32(0), uint64(0), float32(0), float64(0), + } + + for _, rawKeyId := range rawValidSecureAccessKeyId { + _, err := NewSecureAccessKeyId(rawKeyId) + if err != nil { + t.Errorf( + "Expected no error for '%v', got '%s'", rawKeyId, err.Error(), + ) + } + } + }) + + t.Run("InvalidSecureAccessKeyId", func(t *testing.T) { + rawInvalidSecureAccessKeyId := []interface{}{ + "-1", int(-1), int8(-1), int16(-1), int32(-1), int64(-1), float32(-1), + float64(-1), + } + + for _, rawKeyId := range rawInvalidSecureAccessKeyId { + _, err := NewSecureAccessKeyId(rawKeyId) + if err == nil { + t.Errorf("Expected error for '%v', got nil", rawKeyId) + } + } + }) +} diff --git a/src/domain/valueObject/secureAccessKeyUuid.go b/src/domain/valueObject/secureAccessKeyUuid.go deleted file mode 100644 index db4608102..000000000 --- a/src/domain/valueObject/secureAccessKeyUuid.go +++ /dev/null @@ -1,32 +0,0 @@ -package valueObject - -import ( - "errors" - "regexp" - - voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" -) - -const secureAccessKeyUuidRegex string = `^\w{10,16}$` - -type SecureAccessKeyUuid string - -func NewSecureAccessKeyUuid( - value interface{}, -) (keyUuid SecureAccessKeyUuid, err error) { - stringValue, err := voHelper.InterfaceToString(value) - if err != nil { - return keyUuid, errors.New("SecureAccessKeyUuidMustBeString") - } - - re := regexp.MustCompile(secureAccessKeyUuidRegex) - if !re.MatchString(stringValue) { - return keyUuid, errors.New("InvalidSecureAccessKeyUuid") - } - - return SecureAccessKeyUuid(stringValue), nil -} - -func (vo SecureAccessKeyUuid) String() string { - return string(vo) -} diff --git a/src/domain/valueObject/secureAccessKeyUuid_test.go b/src/domain/valueObject/secureAccessKeyUuid_test.go deleted file mode 100644 index 14694dff7..000000000 --- a/src/domain/valueObject/secureAccessKeyUuid_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package valueObject - -import ( - "testing" -) - -func TestSecureAccessKeyUuid(t *testing.T) { - t.Run("ValidSecureAccessKeyUuid", func(t *testing.T) { - rawValidSecureAccessKeyUuid := []interface{}{ - "abc123def4", "1234567890ab", "abcdef123456", "9876543210ab", - "1234abcd5678", - } - - for _, rawKeyUuid := range rawValidSecureAccessKeyUuid { - _, err := NewSecureAccessKeyUuid(rawKeyUuid) - if err != nil { - t.Errorf( - "Expected no error for '%v', got '%s'", rawKeyUuid, err.Error(), - ) - } - } - }) - - t.Run("InvalidSecureAccessKeyUuid", func(t *testing.T) { - rawInvalidSecureAccessKeyUuid := []interface{}{ - 1234, true, 1.40, "abc123", "tooLongSecureAccessKeyUuid", "12345678!@#", - "short12", - } - - for _, rawKeyUuid := range rawInvalidSecureAccessKeyUuid { - _, err := NewSecureAccessKeyUuid(rawKeyUuid) - if err == nil { - t.Errorf("Expected error for '%v', got nil", rawKeyUuid) - } - } - }) -} diff --git a/src/infra/account/accountQueryRepo.go b/src/infra/account/accountQueryRepo.go index e37a47611..2100e6726 100644 --- a/src/infra/account/accountQueryRepo.go +++ b/src/infra/account/accountQueryRepo.go @@ -11,7 +11,6 @@ import ( infraHelper "github.com/goinfinite/os/src/infra/helper" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" dbModel "github.com/goinfinite/os/src/infra/internalDatabase/model" - "github.com/google/uuid" ) type AccountQueryRepo struct { @@ -86,11 +85,10 @@ func (repo *AccountQueryRepo) ReadByUsername( func (repo *AccountQueryRepo) secureAccessKeyFactory( rawSecureAccessKeyContent string, + rawKeyId int, secureAccessKeySecret string, ) (secureAccessKey entity.SecureAccessKey, err error) { - rawKeyUuid := uuid.New().String()[:16] - rawKeyUuidNoHyphens := strings.Replace(rawKeyUuid, "-", "", -1) - keyUuid, err := valueObject.NewSecureAccessKeyUuid(rawKeyUuidNoHyphens) + keyId, err := valueObject.NewSecureAccessKeyId(rawKeyId) if err != nil { return secureAccessKey, err } @@ -118,7 +116,7 @@ func (repo *AccountQueryRepo) secureAccessKeyFactory( return secureAccessKey, err } - return entity.NewSecureAccessKey(keyUuid, keyName, keyContent, keyHashContent), nil + return entity.NewSecureAccessKey(keyId, keyName, keyContent, keyHashContent), nil } func (repo *AccountQueryRepo) ReadSecureAccessKeys( @@ -176,8 +174,9 @@ func (repo *AccountQueryRepo) ReadSecureAccessKeys( continue } + rawKeyId := index + 1 secureAccessKey, err := repo.secureAccessKeyFactory( - rawSecureAccessKeyContent, secretKey, + rawSecureAccessKeyContent, rawKeyId, secretKey, ) if err != nil { slog.Debug(err.Error(), slog.Int("index", index)) From 150a377d6d2dd71272de63bbb3659bc1b0df5fe4 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 15:23:33 -0300 Subject: [PATCH 017/100] feat: add CreateSecureAccessKey flow and implements new ensure method to accountQueryRepo --- src/domain/repository/accountCmdRepo.go | 11 +- src/domain/useCase/createSecureAccessKey.go | 29 +++++ src/infra/account/accountCmdRepo.go | 121 +++++++++++++++++++- src/infra/account/accountQueryRepo.go | 30 +---- 4 files changed, 158 insertions(+), 33 deletions(-) create mode 100644 src/domain/useCase/createSecureAccessKey.go diff --git a/src/domain/repository/accountCmdRepo.go b/src/domain/repository/accountCmdRepo.go index f7d1b74f9..7f073050c 100644 --- a/src/domain/repository/accountCmdRepo.go +++ b/src/domain/repository/accountCmdRepo.go @@ -6,8 +6,11 @@ import ( ) type AccountCmdRepo interface { - Create(createAccount dto.CreateAccount) (valueObject.AccountId, error) - Delete(accountId valueObject.AccountId) error - UpdatePassword(accountId valueObject.AccountId, password valueObject.Password) error - UpdateApiKey(accountId valueObject.AccountId) (valueObject.AccessTokenStr, error) + Create(dto.CreateAccount) (valueObject.AccountId, error) + Delete(valueObject.AccountId) error + UpdatePassword(valueObject.AccountId, valueObject.Password) error + UpdateApiKey(valueObject.AccountId) (valueObject.AccessTokenStr, error) + CreateSecureAccessKey( + dto.CreateSecureAccessKey, + ) (valueObject.SecureAccessKeyId, error) } diff --git a/src/domain/useCase/createSecureAccessKey.go b/src/domain/useCase/createSecureAccessKey.go new file mode 100644 index 000000000..b24c4c2a2 --- /dev/null +++ b/src/domain/useCase/createSecureAccessKey.go @@ -0,0 +1,29 @@ +package useCase + +import ( + "errors" + "log/slog" + + "github.com/goinfinite/os/src/domain/dto" + "github.com/goinfinite/os/src/domain/repository" +) + +func CreateSecureAccessKey( + accountQueryRepo repository.AccountQueryRepo, + accountCmdRepo repository.AccountCmdRepo, + activityRecordCmdRepo repository.ActivityRecordCmdRepo, + createDto dto.CreateSecureAccessKey, +) error { + _, err := accountQueryRepo.ReadById(createDto.AccountId) + if err != nil { + return errors.New("AccountNotFound") + } + + _, err = accountCmdRepo.CreateSecureAccessKey(createDto) + if err != nil { + slog.Error("CreateSecureAccessKeyError", slog.Any("error", err)) + return errors.New("CreateSecureAccessKeyInfraError") + } + + return nil +} diff --git a/src/infra/account/accountCmdRepo.go b/src/infra/account/accountCmdRepo.go index 157cc7e93..8833ad665 100644 --- a/src/infra/account/accountCmdRepo.go +++ b/src/infra/account/accountCmdRepo.go @@ -2,6 +2,7 @@ package accountInfra import ( "errors" + "log/slog" "os" "os/user" "time" @@ -17,14 +18,16 @@ import ( ) type AccountCmdRepo struct { - persistentDbSvc *internalDbInfra.PersistentDatabaseService + persistentDbSvc *internalDbInfra.PersistentDatabaseService + accountQueryRepo *AccountQueryRepo } func NewAccountCmdRepo( persistentDbSvc *internalDbInfra.PersistentDatabaseService, ) *AccountCmdRepo { return &AccountCmdRepo{ - persistentDbSvc: persistentDbSvc, + persistentDbSvc: persistentDbSvc, + accountQueryRepo: NewAccountQueryRepo(persistentDbSvc), } } @@ -86,8 +89,7 @@ func (repo *AccountCmdRepo) Create( func (repo *AccountCmdRepo) readUsernameById( accountId valueObject.AccountId, ) (username valueObject.Username, err error) { - accountQuery := NewAccountQueryRepo(repo.persistentDbSvc) - accountEntity, err := accountQuery.ReadById(accountId) + accountEntity, err := repo.accountQueryRepo.ReadById(accountId) if err != nil { return username, err } @@ -179,3 +181,114 @@ func (repo *AccountCmdRepo) UpdateApiKey( return apiKey, nil } + +func (repo *AccountCmdRepo) ensureSecureAccessKeysDirAndFileExistence( + accountUsername valueObject.Username, +) error { + accountUsernameStr := accountUsername.String() + + secureAccessKeysDirPath := "/home/" + accountUsernameStr + "/.ssh" + if !infraHelper.FileExists(secureAccessKeysDirPath) { + err := infraHelper.MakeDir(secureAccessKeysDirPath) + if err != nil { + return errors.New("CreateSecureAccessKeysDirectoryError: " + err.Error()) + } + } + + secureAccessKeysFilePath := secureAccessKeysDirPath + "/authorized_keys" + if infraHelper.FileExists(secureAccessKeysFilePath) { + return nil + } + + _, err := os.Create(secureAccessKeysFilePath) + if err != nil { + return errors.New("CreateSecureAccessKeysFileError: " + err.Error()) + } + + _, err = infraHelper.RunCmd( + "chown", "-R", accountUsernameStr, secureAccessKeysFilePath, + ) + if err != nil { + return errors.New("ChownSecureAccessKeysFileError: " + err.Error()) + } + + return nil +} + +func (repo *AccountCmdRepo) isSecureAccessKeyValid( + keyContent valueObject.SecureAccessKeyContent, +) bool { + keyName, err := keyContent.ReadOnlyKeyName() + if err != nil { + slog.Error(err.Error()) + return false + } + keyNameStr := keyName.String() + + keyTempFilePath := "/tmp/" + keyNameStr + "_secureAccessKey" + shouldOverwrite := true + err = infraHelper.UpdateFile( + keyTempFilePath, keyContent.String(), shouldOverwrite, + ) + if err != nil { + slog.Error( + "CreateSecureAccessKeyTempFileError", slog.String("keyName", keyNameStr), + slog.Any("err", err), + ) + return false + } + + _, err = infraHelper.RunCmdWithSubShell("ssh-keygen -l -f " + keyTempFilePath) + if err != nil { + slog.Error( + "ValidateSecureAccessKeyError", slog.String("keyName", keyNameStr), + slog.Any("err", err), + ) + return false + } + + err = os.Remove(keyTempFilePath) + if err != nil { + slog.Error( + "DeleteSecureAccessKeyTempFileError", slog.String("keyName", keyNameStr), + slog.Any("err", err), + ) + } + + return true +} + +func (repo *AccountCmdRepo) CreateSecureAccessKey( + createDto dto.CreateSecureAccessKey, +) (keyId valueObject.SecureAccessKeyId, err error) { + account, err := repo.accountQueryRepo.ReadById(createDto.AccountId) + if err != nil { + return keyId, errors.New("AccountNotFound") + } + + err = repo.ensureSecureAccessKeysDirAndFileExistence(account.Username) + if err != nil { + return keyId, err + } + + keyContentStr := createDto.Content.ReadWithoutKeyName() + " " + + createDto.Name.String() + keyContent, err := valueObject.NewSecureAccessKeyContent(keyContentStr) + if err != nil { + return keyId, errors.New("InvalidSecureAccessKey") + } + + if !repo.isSecureAccessKeyValid(keyContent) { + return keyId, errors.New("InvalidSecureAccessKey") + } + + _, err = infraHelper.RunCmdWithSubShell( + "echo \"" + keyContentStr + "\" >> /home/" + account.Username.String() + + "/.ssh/authorized_keys", + ) + if err != nil { + return keyId, errors.New("FailToAddNewSecureAccessKeyToFile: " + err.Error()) + } + + return keyId, nil +} diff --git a/src/infra/account/accountQueryRepo.go b/src/infra/account/accountQueryRepo.go index 2100e6726..735f681c8 100644 --- a/src/infra/account/accountQueryRepo.go +++ b/src/infra/account/accountQueryRepo.go @@ -128,35 +128,15 @@ func (repo *AccountQueryRepo) ReadSecureAccessKeys( if err != nil { return secureAccessKeys, errors.New("AccountNotFound") } - accountUsernameStr := account.Username.String() - secureAccessKeysDirPath := "/home/" + accountUsernameStr + "/.ssh" - err = infraHelper.MakeDir(secureAccessKeysDirPath) + accountCmdRepo := NewAccountCmdRepo(repo.persistentDbSvc) + err = accountCmdRepo.ensureSecureAccessKeysDirAndFileExistence(account.Username) if err != nil { - return secureAccessKeys, errors.New( - "CreateSecureAccessKeysDirectoryError: " + err.Error(), - ) - } - - secureAccessKeysFilePath := secureAccessKeysDirPath + "/authorized_keys" - if !infraHelper.FileExists(secureAccessKeysFilePath) { - _, err = os.Create(secureAccessKeysFilePath) - if err != nil { - return secureAccessKeys, errors.New( - "CreateSecureAccessKeysFileError: " + err.Error(), - ) - } - - _, err = infraHelper.RunCmd( - "chown", "-R", accountUsernameStr, secureAccessKeysFilePath, - ) - if err != nil { - return secureAccessKeys, errors.New( - "ChownSecureAccessKeysFileError: " + err.Error(), - ) - } + return secureAccessKeys, err } + secureAccessKeysFilePath := "/home/" + account.Username.String() + "/.ssh" + + "/authorized_keys" secureAccessKeysFileContent, err := infraHelper.GetFileContent( secureAccessKeysFilePath, ) From 9b1e5ec4f0f1350db93f73a528fd6a505972527a Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 15:45:38 -0300 Subject: [PATCH 018/100] feat: implements ReadSecureAccessKeyByName and use in AccountCmdRepo --- src/infra/account/accountCmdRepo.go | 9 ++++++++- src/infra/account/accountQueryRepo.go | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/infra/account/accountCmdRepo.go b/src/infra/account/accountCmdRepo.go index 8833ad665..ecfd2091d 100644 --- a/src/infra/account/accountCmdRepo.go +++ b/src/infra/account/accountCmdRepo.go @@ -290,5 +290,12 @@ func (repo *AccountCmdRepo) CreateSecureAccessKey( return keyId, errors.New("FailToAddNewSecureAccessKeyToFile: " + err.Error()) } - return keyId, nil + key, err := repo.accountQueryRepo.ReadSecureAccessKeyByName( + createDto.AccountId, createDto.Name, + ) + if err != nil { + return keyId, errors.New("SecureAccessKeyNotCreated") + } + + return key.Id, nil } diff --git a/src/infra/account/accountQueryRepo.go b/src/infra/account/accountQueryRepo.go index 735f681c8..a3bb7633a 100644 --- a/src/infra/account/accountQueryRepo.go +++ b/src/infra/account/accountQueryRepo.go @@ -168,3 +168,23 @@ func (repo *AccountQueryRepo) ReadSecureAccessKeys( return secureAccessKeys, nil } + +func (repo *AccountQueryRepo) ReadSecureAccessKeyByName( + accountId valueObject.AccountId, + secureAccessKeyName valueObject.SecureAccessKeyName, +) (secureAccessKey entity.SecureAccessKey, err error) { + secureAccessKeys, err := repo.ReadSecureAccessKeys(accountId) + if err != nil { + return secureAccessKey, err + } + + for _, key := range secureAccessKeys { + if key.Name.String() != secureAccessKeyName.String() { + continue + } + + return key, nil + } + + return secureAccessKey, errors.New("SecureAccessKeyNotFound") +} From 19fdee2f2b1fd7fcad1d30e08f21ea55eaf1b422 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 15:45:59 -0300 Subject: [PATCH 019/100] feat: implements activity record to CreateSecureAccessKey UC --- src/domain/useCase/createActivityRecord.go | 18 ++++++++++++++++++ src/domain/useCase/createSecureAccessKey.go | 5 ++++- .../valueObject/systemResourceIdentifier.go | 12 +++++++++++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/domain/useCase/createActivityRecord.go b/src/domain/useCase/createActivityRecord.go index fb6a9b0d7..4c6a648f9 100644 --- a/src/domain/useCase/createActivityRecord.go +++ b/src/domain/useCase/createActivityRecord.go @@ -108,3 +108,21 @@ func (uc *CreateSecurityActivityRecord) DeleteAccount( uc.createActivityRecord(createRecordDto) } + +func (uc *CreateSecurityActivityRecord) CreateSecureAccessKey( + createDto dto.CreateSecureAccessKey, + keyId valueObject.SecureAccessKeyId, +) { + recordCode, _ := valueObject.NewActivityRecordCode("SecureAccessKeyCreated") + createRecordDto := dto.CreateActivityRecord{ + RecordLevel: uc.recordLevel, + RecordCode: recordCode, + AffectedResources: []valueObject.SystemResourceIdentifier{ + valueObject.NewSecureAccessKeySri(createDto.AccountId, keyId), + }, + OperatorAccountId: &createDto.OperatorAccountId, + OperatorIpAddress: &createDto.OperatorIpAddress, + } + + uc.createActivityRecord(createRecordDto) +} diff --git a/src/domain/useCase/createSecureAccessKey.go b/src/domain/useCase/createSecureAccessKey.go index b24c4c2a2..5c2f533dc 100644 --- a/src/domain/useCase/createSecureAccessKey.go +++ b/src/domain/useCase/createSecureAccessKey.go @@ -19,11 +19,14 @@ func CreateSecureAccessKey( return errors.New("AccountNotFound") } - _, err = accountCmdRepo.CreateSecureAccessKey(createDto) + keyId, err := accountCmdRepo.CreateSecureAccessKey(createDto) if err != nil { slog.Error("CreateSecureAccessKeyError", slog.Any("error", err)) return errors.New("CreateSecureAccessKeyInfraError") } + NewCreateSecurityActivityRecord(activityRecordCmdRepo). + CreateSecureAccessKey(createDto, keyId) + return nil } diff --git a/src/domain/valueObject/systemResourceIdentifier.go b/src/domain/valueObject/systemResourceIdentifier.go index 872d96354..ecdf23962 100644 --- a/src/domain/valueObject/systemResourceIdentifier.go +++ b/src/domain/valueObject/systemResourceIdentifier.go @@ -8,7 +8,7 @@ import ( voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" ) -const systemResourceIdentifierRegex string = `^sri://(?P[\d]{1,64}):(?P[\w\_\-]{2,64})\/(?P[\w\_\.\-]{2,256}|\*)$` +const systemResourceIdentifierRegex string = `^sri://(?P[\d]{1,64}):(?P[\w\_\-]{2,64})\/(?P[\w\_\.\-/]{1,256}|\*)$` type SystemResourceIdentifier string @@ -48,6 +48,16 @@ func NewAccountSri(accountId AccountId) SystemResourceIdentifier { ) } +func NewSecureAccessKeySri( + accountId AccountId, + SecureAccessKeyId SecureAccessKeyId, +) SystemResourceIdentifier { + return NewSystemResourceIdentifierIgnoreError( + "sri://" + accountId.String() + ":secureAccessKey/" + + SecureAccessKeyId.String(), + ) +} + func (vo SystemResourceIdentifier) String() string { return string(vo) } From 3116bca3fadac7d4313e0ef7f3934db80a2c47d5 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 16:05:36 -0300 Subject: [PATCH 020/100] feat: implements secure access key creation to API and CLI --- src/presentation/api/controller/account.go | 25 ++++++++- src/presentation/api/router.go | 1 + src/presentation/cli/controller/account.go | 34 +++++++++++- src/presentation/cli/router.go | 1 + src/presentation/service/account.go | 62 ++++++++++++++++++++++ 5 files changed, 120 insertions(+), 3 deletions(-) diff --git a/src/presentation/api/controller/account.go b/src/presentation/api/controller/account.go index 8ac9ac610..0edbd4a26 100644 --- a/src/presentation/api/controller/account.go +++ b/src/presentation/api/controller/account.go @@ -35,7 +35,7 @@ func (controller *AccountController) Read(c echo.Context) error { } // CreateAccount godoc -// @Summary CreateNewAccount +// @Summary CreateAccount // @Description Create a new account. // @Tags account // @Accept json @@ -114,7 +114,7 @@ func (controller *AccountController) Delete(c echo.Context) error { // @Produce json // @Security Bearer // @Success 200 {array} entity.SecureAccessKey -// @Router /v1/account/secure-access-key [get] +// @Router /v1/account/secure-access-key/ [get] func (controller *AccountController) ReadSecureAccessKey(c echo.Context) error { requestBody, err := apiHelper.ReadRequestBody(c) if err != nil { @@ -125,3 +125,24 @@ func (controller *AccountController) ReadSecureAccessKey(c echo.Context) error { c, controller.accountService.ReadSecureAccessKey(requestBody), ) } + +// CreateSecureAccessKey godoc +// @Summary CreateSecureAccessKey +// @Description Create a new secure access key. +// @Tags account +// @Accept json +// @Produce json +// @Security Bearer +// @Param createSecureAccessKey body dto.CreateSecureAccessKey true "All props are required." +// @Success 201 {object} object{} "SecureAccessKeyCreated" +// @Router /v1/account/secure-access-key/ [post] +func (controller *AccountController) CreateSecureAccessKey(c echo.Context) error { + requestBody, err := apiHelper.ReadRequestBody(c) + if err != nil { + return err + } + + return apiHelper.ServiceResponseWrapper( + c, controller.accountService.CreateSecureAccessKey(requestBody), + ) +} diff --git a/src/presentation/api/router.go b/src/presentation/api/router.go index 028dd9b01..948396202 100644 --- a/src/presentation/api/router.go +++ b/src/presentation/api/router.go @@ -57,6 +57,7 @@ func (router Router) accountRoutes() { accountGroup.PUT("/", accountController.Update) accountGroup.DELETE("/:accountId/", accountController.Delete) accountGroup.GET("/secure-access-key/", accountController.ReadSecureAccessKey) + accountGroup.POST("/secure-access-key/", accountController.CreateSecureAccessKey) } func (router Router) cronRoutes() { diff --git a/src/presentation/cli/controller/account.go b/src/presentation/cli/controller/account.go index 72868af3d..2ab0ac704 100644 --- a/src/presentation/cli/controller/account.go +++ b/src/presentation/cli/controller/account.go @@ -121,7 +121,7 @@ func (controller *AccountController) ReadSecureAccessKeys() *cobra.Command { var accountIdStr string cmd := &cobra.Command{ - Use: "get", + Use: "get-keys", Short: "GetSecureAccessKeys", Run: func(cmd *cobra.Command, args []string) { requestBody := map[string]interface{}{ @@ -138,3 +138,35 @@ func (controller *AccountController) ReadSecureAccessKeys() *cobra.Command { cmd.MarkFlagRequired("account-id") return cmd } + +func (controller *AccountController) CreateSecureAccessKey() *cobra.Command { + var accountIdStr, keyNameStr, keyContentStr string + + cmd := &cobra.Command{ + Use: "create-key", + Short: "CreateSecureAccessKey", + Run: func(cmd *cobra.Command, args []string) { + requestBody := map[string]interface{}{ + "accountId": accountIdStr, + "content": keyContentStr, + } + + if keyNameStr != "" { + requestBody["name"] = keyNameStr + } + + cliHelper.ServiceResponseWrapper( + controller.accountService.CreateSecureAccessKey(requestBody), + ) + }, + } + + cmd.Flags().StringVarP(&accountIdStr, "account-id", "i", "", "AccountId") + cmd.MarkFlagRequired("account-id") + cmd.Flags().StringVarP(&keyNameStr, "key-name", "n", "", "SecureAccessKeyName") + cmd.Flags().StringVarP( + &keyContentStr, "key-content", "c", "", "SecureAccessKeyContent", + ) + cmd.MarkFlagRequired("key-content") + return cmd +} diff --git a/src/presentation/cli/router.go b/src/presentation/cli/router.go index f8f9e7b8d..1d4770bf0 100644 --- a/src/presentation/cli/router.go +++ b/src/presentation/cli/router.go @@ -52,6 +52,7 @@ func (router Router) accountRoutes() { accountCmd.AddCommand(accountController.Update()) accountCmd.AddCommand(accountController.Delete()) accountCmd.AddCommand(accountController.ReadSecureAccessKeys()) + accountCmd.AddCommand(accountController.CreateSecureAccessKey()) } func (router Router) authenticationRoutes() { diff --git a/src/presentation/service/account.go b/src/presentation/service/account.go index e7ddca2f3..bf3cacfc8 100644 --- a/src/presentation/service/account.go +++ b/src/presentation/service/account.go @@ -249,3 +249,65 @@ func (service *AccountService) ReadSecureAccessKey( return NewServiceOutput(Success, secureAccessKeys) } + +func (service *AccountService) CreateSecureAccessKey( + input map[string]interface{}, +) ServiceOutput { + if input["id"] != nil { + input["accountId"] = input["id"] + } + + requiredParams := []string{"accountId", "content"} + err := serviceHelper.RequiredParamsInspector(input, requiredParams) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + + keyContent, err := valueObject.NewSecureAccessKeyContent(input["content"]) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + + keyName, err := valueObject.NewSecureAccessKeyName(input["name"]) + if err != nil { + keyName, err = keyContent.ReadOnlyKeyName() + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + } + + accountId, err := valueObject.NewAccountId(input["accountId"]) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + + operatorAccountId := LocalOperatorAccountId + if input["operatorAccountId"] != nil { + operatorAccountId, err = valueObject.NewAccountId(input["operatorAccountId"]) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + } + + operatorIpAddress := LocalOperatorIpAddress + if input["operatorIpAddress"] != nil { + operatorIpAddress, err = valueObject.NewIpAddress(input["operatorIpAddress"]) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + } + + createDto := dto.NewCreateSecureAccessKey( + keyName, keyContent, accountId, operatorAccountId, operatorIpAddress, + ) + + err = useCase.CreateSecureAccessKey( + service.accountQueryRepo, service.accountCmdRepo, + service.activityRecordCmdRepo, createDto, + ) + if err != nil { + return NewServiceOutput(InfraError, err.Error()) + } + + return NewServiceOutput(Created, "SecureAccessKeyCreated") +} From 9ca1c62a331dcee2fca8957105a269ea921ade09 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 17:08:24 -0300 Subject: [PATCH 021/100] feat: implements new secure access keys methods from AccountQueryRepo to infra tests --- src/infra/account/accountQueryRepo_test.go | 66 ++++++++++++++++++---- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/src/infra/account/accountQueryRepo_test.go b/src/infra/account/accountQueryRepo_test.go index 2272ae03f..b00f4e782 100644 --- a/src/infra/account/accountQueryRepo_test.go +++ b/src/infra/account/accountQueryRepo_test.go @@ -5,6 +5,7 @@ import ( "testing" testHelpers "github.com/goinfinite/os/src/devUtils" + "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/valueObject" ) @@ -14,37 +15,82 @@ func TestAccountQueryRepo(t *testing.T) { persistentDbSvc := testHelpers.GetPersistentDbSvc() accountQueryRepo := NewAccountQueryRepo(persistentDbSvc) - t.Run("GetValidAccounts", func(t *testing.T) { + accountId, _ := valueObject.NewAccountId(os.Getenv("DUMMY_USER_ID")) + + t.Run("ReadValidAccounts", func(t *testing.T) { _, err := accountQueryRepo.Read() if err != nil { - t.Error("UnexpectedError") + t.Errorf("Expecting no error, but got %s", err.Error()) } }) - t.Run("GetValidAccountByUsername", func(t *testing.T) { + t.Run("ReadValidAccountByUsername", func(t *testing.T) { username, _ := valueObject.NewUsername(os.Getenv("DUMMY_USER_NAME")) _, err := accountQueryRepo.ReadByUsername(username) if err != nil { - t.Error("UnexpectedError") + t.Errorf( + "Expecting no error for %s, but got %s", username.String(), err.Error(), + ) } }) - t.Run("GetValidAccountById", func(t *testing.T) { - accountId, _ := valueObject.NewAccountId(os.Getenv("DUMMY_USER_ID")) - + t.Run("ReadValidAccountById", func(t *testing.T) { _, err := accountQueryRepo.ReadById(accountId) if err != nil { - t.Error("UnexpectedError") + t.Errorf( + "Expecting no error for %d, but got %s", accountId.Uint64(), + err.Error(), + ) } }) - t.Run("GetInvalidAccount", func(t *testing.T) { + t.Run("ReadInvalidAccount", func(t *testing.T) { username, _ := valueObject.NewUsername("invalid") _, err := accountQueryRepo.ReadByUsername(username) if err == nil { - t.Error("ExpectingError") + t.Errorf("Expecting error for %s, but got nil", username.String()) + } + }) + + t.Skip("SkipSecureAccessKeysTests") + + accountCmdRepo := NewAccountCmdRepo(persistentDbSvc) + + keyName, _ := valueObject.NewSecureAccessKeyName("dummySecureAccessKey") + keyContent, _ := valueObject.NewSecureAccessKeyContent( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+GDqLA2sGauzU5hUxBbBmm6FfeZpUbiX6IlQO9KqeqAsum+Efhvj+qpatM5PzMMwtlcFwDS5Y4RcX9uxE8IGsYiALRfnLAX5p73zrcrXamMJSx25rXAu/VJdmekxHbDgsBPyk6/4dfu+3uW7ka7HHhPytPIqW2qBuPkalJinc7qKEuXdkCyX8+8a+0uN8XodLipLJwU8A1VPvI9thYxITyHWZnXRnin0r/unHgLrg9bBILXZf0JRslelYdCvuCGnRKZfokh153shMZ63S+iV/Tohg2bOVxyz3HIQ983ga24uTFQhLpITMe9JEfq3pp2wcCE5hNFlNKyeDG8kwB+8V", + ) + createDto := dto.NewCreateSecureAccessKey( + keyName, keyContent, accountId, accountId, valueObject.NewLocalhostIpAddress(), + ) + + _, err := accountCmdRepo.CreateSecureAccessKey(createDto) + if err != nil { + t.Fatalf("Fail to create dummy SecureAccessKey to test") + } + + t.Run("ReadSecureAccessKeys", func(t *testing.T) { + keys, err := accountQueryRepo.ReadSecureAccessKeys(accountId) + if err != nil { + t.Fatalf( + "Expecting no error for %d, but got %s", accountId.Uint64(), err.Error(), + ) + } + + if len(keys) == 0 { + t.Error("Expecting a keys list, but got an empty one") + } + }) + + t.Run("ReadSecureAccessKeyByName", func(t *testing.T) { + _, err := accountQueryRepo.ReadSecureAccessKeyByName(accountId, keyName) + if err != nil { + t.Fatalf( + "Expecting no error for %s (%d), but got %s", keyName.String(), + accountId.Uint64(), err.Error(), + ) } }) } From 64c9dd5786799520b849f17a154c20946ccd75f3 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 17:11:46 -0300 Subject: [PATCH 022/100] chore: add .3 to Go version --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c6611515b..937cfc3fa 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/goinfinite/os -go 1.23.0 +go 1.23.3 require ( github.com/alecthomas/chroma v0.10.0 From b277ccf29c57006394ba945eab79b9b02ba78001 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 17:55:41 -0300 Subject: [PATCH 023/100] feat: implements new secure access keys creation method from AccountCmdRepo to infra tests --- src/infra/account/accountCmdRepo_test.go | 37 +++++++++++++++++++++--- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/infra/account/accountCmdRepo_test.go b/src/infra/account/accountCmdRepo_test.go index fe6d0a002..4fd608744 100644 --- a/src/infra/account/accountCmdRepo_test.go +++ b/src/infra/account/accountCmdRepo_test.go @@ -53,7 +53,9 @@ func TestAccountCmdRepo(t *testing.T) { t.Run("AddValidAccount", func(t *testing.T) { err := addDummyUser() if err != nil { - t.Errorf("UnexpectedError: %v", err) + t.Errorf( + "Expected no error for %d, but got %s", accountId.Uint64(), err.Error(), + ) } }) @@ -75,7 +77,9 @@ func TestAccountCmdRepo(t *testing.T) { t.Run("DeleteValidAccount", func(t *testing.T) { err := deleteDummyUser() if err != nil { - t.Errorf("UnexpectedError: %v", err) + t.Errorf( + "Expected no error for %d, but got %s", accountId.Uint64(), err.Error(), + ) } }) @@ -86,7 +90,10 @@ func TestAccountCmdRepo(t *testing.T) { err := accountCmdRepo.UpdatePassword(accountId, newPassword) if err != nil { - t.Errorf("UnexpectedError: %v", err) + t.Errorf( + "Expected no error for %s, but got %s", newPassword.String(), + err.Error(), + ) } }) @@ -95,7 +102,29 @@ func TestAccountCmdRepo(t *testing.T) { _, err := accountCmdRepo.UpdateApiKey(accountId) if err != nil { - t.Errorf("UnexpectedError: %v", err) + t.Errorf( + "Expected no error for %d, but got %s", accountId.Uint64(), err.Error(), + ) + } + }) + + t.Skip("SkipSecureAccessKeysTests") + + t.Run("CreateSecureAccessKey", func(t *testing.T) { + keyName, _ := valueObject.NewSecureAccessKeyName("dummySecureAccessKey") + keyContent, _ := valueObject.NewSecureAccessKeyContent( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+GDqLA2sGauzU5hUxBbBmm6FfeZpUbiX6IlQO9KqeqAsum+Efhvj+qpatM5PzMMwtlcFwDS5Y4RcX9uxE8IGsYiALRfnLAX5p73zrcrXamMJSx25rXAu/VJdmekxHbDgsBPyk6/4dfu+3uW7ka7HHhPytPIqW2qBuPkalJinc7qKEuXdkCyX8+8a+0uN8XodLipLJwU8A1VPvI9thYxITyHWZnXRnin0r/unHgLrg9bBILXZf0JRslelYdCvuCGnRKZfokh153shMZ63S+iV/Tohg2bOVxyz3HIQ983ga24uTFQhLpITMe9JEfq3pp2wcCE5hNFlNKyeDG8kwB+8V", + ) + createDto := dto.NewCreateSecureAccessKey( + keyName, keyContent, accountId, accountId, + valueObject.NewLocalhostIpAddress(), + ) + + _, err := accountCmdRepo.CreateSecureAccessKey(createDto) + if err != nil { + t.Fatalf( + "Expected no error for %s, but got %s", keyName.String(), err.Error(), + ) } }) } From c678edcbd0e95c60e32d8f4900688f13dd20f197 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 19:53:35 -0300 Subject: [PATCH 024/100] feat: add DeleteSecureAccessKey DTO --- src/domain/dto/deleteSecureAccessKey.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/domain/dto/deleteSecureAccessKey.go diff --git a/src/domain/dto/deleteSecureAccessKey.go b/src/domain/dto/deleteSecureAccessKey.go new file mode 100644 index 000000000..8bc198ad1 --- /dev/null +++ b/src/domain/dto/deleteSecureAccessKey.go @@ -0,0 +1,23 @@ +package dto + +import "github.com/goinfinite/os/src/domain/valueObject" + +type DeleteSecureAccessKey struct { + Id valueObject.SecureAccessKeyId `json:"id"` + AccountId valueObject.AccountId `json:"accountId"` + OperatorAccountId valueObject.AccountId `json:"-"` + OperatorIpAddress valueObject.IpAddress `json:"-"` +} + +func NewDeleteSecureAccessKey( + id valueObject.SecureAccessKeyId, + accountId, operatorAccountId valueObject.AccountId, + operatorIpAddress valueObject.IpAddress, +) DeleteSecureAccessKey { + return DeleteSecureAccessKey{ + Id: id, + AccountId: accountId, + OperatorAccountId: operatorAccountId, + OperatorIpAddress: operatorIpAddress, + } +} From 7545e8336e8e1fdb5cbdcd41072aa41f07803d03 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 19:55:26 -0300 Subject: [PATCH 025/100] feat: add ReadSecureAccessKeyById to AccountQueryRepo --- src/infra/account/accountQueryRepo.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/infra/account/accountQueryRepo.go b/src/infra/account/accountQueryRepo.go index a3bb7633a..3ecdc4175 100644 --- a/src/infra/account/accountQueryRepo.go +++ b/src/infra/account/accountQueryRepo.go @@ -169,6 +169,26 @@ func (repo *AccountQueryRepo) ReadSecureAccessKeys( return secureAccessKeys, nil } +func (repo *AccountQueryRepo) ReadSecureAccessKeyById( + accountId valueObject.AccountId, + secureAccessKeyId valueObject.SecureAccessKeyId, +) (secureAccessKey entity.SecureAccessKey, err error) { + secureAccessKeys, err := repo.ReadSecureAccessKeys(accountId) + if err != nil { + return secureAccessKey, err + } + + for _, key := range secureAccessKeys { + if key.Id.Uint16() != secureAccessKeyId.Uint16() { + continue + } + + return key, nil + } + + return secureAccessKey, errors.New("SecureAccessKeyNotFound") +} + func (repo *AccountQueryRepo) ReadSecureAccessKeyByName( accountId valueObject.AccountId, secureAccessKeyName valueObject.SecureAccessKeyName, From 487c62c3b12588ff0251a6c03b0c615d99137ea5 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 20:03:19 -0300 Subject: [PATCH 026/100] feat: implements DeleteSecureAccessKey to CLI and API --- src/domain/dto/deleteSecureAccessKey.go | 2 +- src/domain/repository/accountCmdRepo.go | 1 + src/domain/useCase/createActivityRecord.go | 17 +++++++ src/domain/useCase/deleteSecureAccessKey.go | 32 ++++++++++++ src/infra/account/accountCmdRepo.go | 28 ++++++++++- src/presentation/api/controller/account.go | 28 ++++++++++- src/presentation/api/router.go | 11 +++- src/presentation/cli/controller/account.go | 56 ++++++++++++++++----- src/presentation/cli/router.go | 1 + src/presentation/service/account.go | 54 ++++++++++++++++++-- 10 files changed, 207 insertions(+), 23 deletions(-) create mode 100644 src/domain/useCase/deleteSecureAccessKey.go diff --git a/src/domain/dto/deleteSecureAccessKey.go b/src/domain/dto/deleteSecureAccessKey.go index 8bc198ad1..068dddd76 100644 --- a/src/domain/dto/deleteSecureAccessKey.go +++ b/src/domain/dto/deleteSecureAccessKey.go @@ -4,7 +4,7 @@ import "github.com/goinfinite/os/src/domain/valueObject" type DeleteSecureAccessKey struct { Id valueObject.SecureAccessKeyId `json:"id"` - AccountId valueObject.AccountId `json:"accountId"` + AccountId valueObject.AccountId `json:"-"` OperatorAccountId valueObject.AccountId `json:"-"` OperatorIpAddress valueObject.IpAddress `json:"-"` } diff --git a/src/domain/repository/accountCmdRepo.go b/src/domain/repository/accountCmdRepo.go index 7f073050c..ee0fba127 100644 --- a/src/domain/repository/accountCmdRepo.go +++ b/src/domain/repository/accountCmdRepo.go @@ -13,4 +13,5 @@ type AccountCmdRepo interface { CreateSecureAccessKey( dto.CreateSecureAccessKey, ) (valueObject.SecureAccessKeyId, error) + DeleteSecureAccessKey(dto.DeleteSecureAccessKey) error } diff --git a/src/domain/useCase/createActivityRecord.go b/src/domain/useCase/createActivityRecord.go index 4c6a648f9..5c1d91e7d 100644 --- a/src/domain/useCase/createActivityRecord.go +++ b/src/domain/useCase/createActivityRecord.go @@ -126,3 +126,20 @@ func (uc *CreateSecurityActivityRecord) CreateSecureAccessKey( uc.createActivityRecord(createRecordDto) } + +func (uc *CreateSecurityActivityRecord) DeleteSecureAccessKey( + deleteDto dto.DeleteSecureAccessKey, +) { + recordCode, _ := valueObject.NewActivityRecordCode("SecureAccessKeyDeleted") + createRecordDto := dto.CreateActivityRecord{ + RecordLevel: uc.recordLevel, + RecordCode: recordCode, + AffectedResources: []valueObject.SystemResourceIdentifier{ + valueObject.NewSecureAccessKeySri(deleteDto.AccountId, deleteDto.Id), + }, + OperatorAccountId: &deleteDto.OperatorAccountId, + OperatorIpAddress: &deleteDto.OperatorIpAddress, + } + + uc.createActivityRecord(createRecordDto) +} diff --git a/src/domain/useCase/deleteSecureAccessKey.go b/src/domain/useCase/deleteSecureAccessKey.go new file mode 100644 index 000000000..6aad79868 --- /dev/null +++ b/src/domain/useCase/deleteSecureAccessKey.go @@ -0,0 +1,32 @@ +package useCase + +import ( + "errors" + "log/slog" + + "github.com/goinfinite/os/src/domain/dto" + "github.com/goinfinite/os/src/domain/repository" +) + +func DeleteSecureAccessKey( + accountQueryRepo repository.AccountQueryRepo, + accountCmdRepo repository.AccountCmdRepo, + activityRecordCmdRepo repository.ActivityRecordCmdRepo, + deleteDto dto.DeleteSecureAccessKey, +) error { + _, err := accountQueryRepo.ReadById(deleteDto.AccountId) + if err != nil { + return errors.New("AccountNotFound") + } + + err = accountCmdRepo.DeleteSecureAccessKey(deleteDto) + if err != nil { + slog.Error("DeleteSecureAccessKeyError", slog.Any("error", err)) + return errors.New("DeleteSecureAccessKeyInfraError") + } + + NewCreateSecurityActivityRecord(activityRecordCmdRepo). + DeleteSecureAccessKey(deleteDto) + + return nil +} diff --git a/src/infra/account/accountCmdRepo.go b/src/infra/account/accountCmdRepo.go index ecfd2091d..0e645e277 100644 --- a/src/infra/account/accountCmdRepo.go +++ b/src/infra/account/accountCmdRepo.go @@ -294,8 +294,34 @@ func (repo *AccountCmdRepo) CreateSecureAccessKey( createDto.AccountId, createDto.Name, ) if err != nil { - return keyId, errors.New("SecureAccessKeyNotCreated") + return keyId, errors.New("SecureAccessKeyWasNotCreated") } return key.Id, nil } + +func (repo *AccountCmdRepo) DeleteSecureAccessKey( + deleteDto dto.DeleteSecureAccessKey, +) error { + account, err := repo.accountQueryRepo.ReadById(deleteDto.AccountId) + if err != nil { + return errors.New("AccountNotFound") + } + + keyToDelete, err := repo.accountQueryRepo.ReadSecureAccessKeyById( + deleteDto.AccountId, deleteDto.Id, + ) + if err != nil { + return err + } + + _, err = infraHelper.RunCmdWithSubShell( + "sed -i '\\|" + keyToDelete.Content.String() + "|d' " + + "/home/" + account.Username.String() + "/.ssh/authorized_keys", + ) + if err != nil { + return errors.New("FailToDeleteSecureAccessKeyFromFile: " + err.Error()) + } + + return nil +} diff --git a/src/presentation/api/controller/account.go b/src/presentation/api/controller/account.go index 0edbd4a26..fc34d1bc3 100644 --- a/src/presentation/api/controller/account.go +++ b/src/presentation/api/controller/account.go @@ -113,8 +113,9 @@ func (controller *AccountController) Delete(c echo.Context) error { // @Accept json // @Produce json // @Security Bearer +// @Param accountId path string true "AccountId that keys belongs to." // @Success 200 {array} entity.SecureAccessKey -// @Router /v1/account/secure-access-key/ [get] +// @Router /v1/account/{accountId}/secure-access-key/ [get] func (controller *AccountController) ReadSecureAccessKey(c echo.Context) error { requestBody, err := apiHelper.ReadRequestBody(c) if err != nil { @@ -133,9 +134,10 @@ func (controller *AccountController) ReadSecureAccessKey(c echo.Context) error { // @Accept json // @Produce json // @Security Bearer +// @Param accountId path string true "AccountId to create secure access key." // @Param createSecureAccessKey body dto.CreateSecureAccessKey true "All props are required." // @Success 201 {object} object{} "SecureAccessKeyCreated" -// @Router /v1/account/secure-access-key/ [post] +// @Router /v1/account/{accountId}/secure-access-key/ [post] func (controller *AccountController) CreateSecureAccessKey(c echo.Context) error { requestBody, err := apiHelper.ReadRequestBody(c) if err != nil { @@ -146,3 +148,25 @@ func (controller *AccountController) CreateSecureAccessKey(c echo.Context) error c, controller.accountService.CreateSecureAccessKey(requestBody), ) } + +// DeleteSecureAccessKey godoc +// @Summary DeleteSecureAccessKey +// @Description Delete a secure access key. +// @Tags account +// @Accept json +// @Produce json +// @Security Bearer +// @Param accountId path string true "AccountId that keys belongs to." +// @Param secureAccessKeyId path string true "SecureAccessKeyId to delete." +// @Success 200 {object} object{} "SecureAccessKeyDeleted" +// @Router /v1/account/{accountId}/secure-access-key/{secureAccessKeyId}/ [delete] +func (controller *AccountController) DeleteSecureAccessKey(c echo.Context) error { + requestBody, err := apiHelper.ReadRequestBody(c) + if err != nil { + return err + } + + return apiHelper.ServiceResponseWrapper( + c, controller.accountService.DeleteSecureAccessKey(requestBody), + ) +} diff --git a/src/presentation/api/router.go b/src/presentation/api/router.go index 948396202..83e622bd2 100644 --- a/src/presentation/api/router.go +++ b/src/presentation/api/router.go @@ -56,8 +56,15 @@ func (router Router) accountRoutes() { accountGroup.POST("/", accountController.Create) accountGroup.PUT("/", accountController.Update) accountGroup.DELETE("/:accountId/", accountController.Delete) - accountGroup.GET("/secure-access-key/", accountController.ReadSecureAccessKey) - accountGroup.POST("/secure-access-key/", accountController.CreateSecureAccessKey) + accountGroup.GET( + "/:accountId/secure-access-key/", accountController.ReadSecureAccessKey, + ) + accountGroup.POST( + "/:accountId/secure-access-key/", accountController.CreateSecureAccessKey, + ) + accountGroup.DELETE( + "/:accountId/secure-access-key/:secureAccessKeyId/", accountController.DeleteSecureAccessKey, + ) } func (router Router) cronRoutes() { diff --git a/src/presentation/cli/controller/account.go b/src/presentation/cli/controller/account.go index 2ab0ac704..766158a2e 100644 --- a/src/presentation/cli/controller/account.go +++ b/src/presentation/cli/controller/account.go @@ -58,7 +58,8 @@ func (controller *AccountController) Create() *cobra.Command { } func (controller *AccountController) Update() *cobra.Command { - var accountIdStr, usernameStr, passwordStr, shouldUpdateApiKeyStr string + var accountIdUint64 uint64 + var usernameStr, passwordStr, shouldUpdateApiKeyStr string cmd := &cobra.Command{ Use: "update", @@ -68,8 +69,8 @@ func (controller *AccountController) Update() *cobra.Command { "shouldUpdateApiKey": shouldUpdateApiKeyStr, } - if accountIdStr != "" { - requestBody["accountId"] = accountIdStr + if accountIdUint64 != 0 { + requestBody["accountId"] = accountIdUint64 } if usernameStr != "" { @@ -86,7 +87,7 @@ func (controller *AccountController) Update() *cobra.Command { }, } - cmd.Flags().StringVarP(&accountIdStr, "account-id", "i", "", "AccountId") + cmd.Flags().Uint64VarP(&accountIdUint64, "account-id", "i", 0, "AccountId") cmd.Flags().StringVarP(&usernameStr, "username", "u", "", "Username") cmd.Flags().StringVarP(&passwordStr, "password", "p", "", "Password") cmd.Flags().StringVarP( @@ -96,14 +97,14 @@ func (controller *AccountController) Update() *cobra.Command { } func (controller *AccountController) Delete() *cobra.Command { - var accountIdStr string + var accountIdUint64 uint64 cmd := &cobra.Command{ Use: "delete", Short: "DeleteAccount", Run: func(cmd *cobra.Command, args []string) { requestBody := map[string]interface{}{ - "accountId": accountIdStr, + "accountId": accountIdUint64, } cliHelper.ServiceResponseWrapper( @@ -112,20 +113,20 @@ func (controller *AccountController) Delete() *cobra.Command { }, } - cmd.Flags().StringVarP(&accountIdStr, "account-id", "i", "", "AccountId") + cmd.Flags().Uint64VarP(&accountIdUint64, "account-id", "i", 0, "AccountId") cmd.MarkFlagRequired("account-id") return cmd } func (controller *AccountController) ReadSecureAccessKeys() *cobra.Command { - var accountIdStr string + var accountIdUint64 uint64 cmd := &cobra.Command{ Use: "get-keys", Short: "GetSecureAccessKeys", Run: func(cmd *cobra.Command, args []string) { requestBody := map[string]interface{}{ - "accountId": accountIdStr, + "accountId": accountIdUint64, } cliHelper.ServiceResponseWrapper( @@ -134,20 +135,21 @@ func (controller *AccountController) ReadSecureAccessKeys() *cobra.Command { }, } - cmd.Flags().StringVarP(&accountIdStr, "account-id", "i", "", "AccountId") + cmd.Flags().Uint64VarP(&accountIdUint64, "account-id", "u", 0, "AccountId") cmd.MarkFlagRequired("account-id") return cmd } func (controller *AccountController) CreateSecureAccessKey() *cobra.Command { - var accountIdStr, keyNameStr, keyContentStr string + var accountIdUint64 uint64 + var keyNameStr, keyContentStr string cmd := &cobra.Command{ Use: "create-key", Short: "CreateSecureAccessKey", Run: func(cmd *cobra.Command, args []string) { requestBody := map[string]interface{}{ - "accountId": accountIdStr, + "accountId": accountIdUint64, "content": keyContentStr, } @@ -161,7 +163,7 @@ func (controller *AccountController) CreateSecureAccessKey() *cobra.Command { }, } - cmd.Flags().StringVarP(&accountIdStr, "account-id", "i", "", "AccountId") + cmd.Flags().Uint64VarP(&accountIdUint64, "account-id", "u", 0, "AccountId") cmd.MarkFlagRequired("account-id") cmd.Flags().StringVarP(&keyNameStr, "key-name", "n", "", "SecureAccessKeyName") cmd.Flags().StringVarP( @@ -170,3 +172,31 @@ func (controller *AccountController) CreateSecureAccessKey() *cobra.Command { cmd.MarkFlagRequired("key-content") return cmd } + +func (controller *AccountController) DeleteSecureAccessKey() *cobra.Command { + var accountIdUint64 uint64 + var keyIdUint16 uint16 + + cmd := &cobra.Command{ + Use: "delete-key", + Short: "DeleteSecureAccessKey", + Run: func(cmd *cobra.Command, args []string) { + requestBody := map[string]interface{}{ + "accountId": accountIdUint64, + "id": keyIdUint16, + } + + cliHelper.ServiceResponseWrapper( + controller.accountService.DeleteSecureAccessKey(requestBody), + ) + }, + } + + cmd.Flags().Uint64VarP( + &accountIdUint64, "account-id", "u", 0, "AccountId", + ) + cmd.MarkFlagRequired("account-id") + cmd.Flags().Uint16VarP(&keyIdUint16, "key-id", "i", 0, "SecureAccessKeyId") + cmd.MarkFlagRequired("key-id") + return cmd +} diff --git a/src/presentation/cli/router.go b/src/presentation/cli/router.go index 1d4770bf0..3cec1a5a4 100644 --- a/src/presentation/cli/router.go +++ b/src/presentation/cli/router.go @@ -53,6 +53,7 @@ func (router Router) accountRoutes() { accountCmd.AddCommand(accountController.Delete()) accountCmd.AddCommand(accountController.ReadSecureAccessKeys()) accountCmd.AddCommand(accountController.CreateSecureAccessKey()) + accountCmd.AddCommand(accountController.DeleteSecureAccessKey()) } func (router Router) authenticationRoutes() { diff --git a/src/presentation/service/account.go b/src/presentation/service/account.go index bf3cacfc8..2e00584b2 100644 --- a/src/presentation/service/account.go +++ b/src/presentation/service/account.go @@ -253,10 +253,6 @@ func (service *AccountService) ReadSecureAccessKey( func (service *AccountService) CreateSecureAccessKey( input map[string]interface{}, ) ServiceOutput { - if input["id"] != nil { - input["accountId"] = input["id"] - } - requiredParams := []string{"accountId", "content"} err := serviceHelper.RequiredParamsInspector(input, requiredParams) if err != nil { @@ -311,3 +307,53 @@ func (service *AccountService) CreateSecureAccessKey( return NewServiceOutput(Created, "SecureAccessKeyCreated") } + +func (service *AccountService) DeleteSecureAccessKey( + input map[string]interface{}, +) ServiceOutput { + requiredParams := []string{"accountId", "secureAccessKeyId"} + err := serviceHelper.RequiredParamsInspector(input, requiredParams) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + + keyId, err := valueObject.NewSecureAccessKeyId(input["secureAccessKeyId"]) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + + accountId, err := valueObject.NewAccountId(input["accountId"]) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + + operatorAccountId := LocalOperatorAccountId + if input["operatorAccountId"] != nil { + operatorAccountId, err = valueObject.NewAccountId(input["operatorAccountId"]) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + } + + operatorIpAddress := LocalOperatorIpAddress + if input["operatorIpAddress"] != nil { + operatorIpAddress, err = valueObject.NewIpAddress(input["operatorIpAddress"]) + if err != nil { + return NewServiceOutput(UserError, err.Error()) + } + } + + deleteDto := dto.NewDeleteSecureAccessKey( + keyId, accountId, operatorAccountId, operatorIpAddress, + ) + + err = useCase.DeleteSecureAccessKey( + service.accountQueryRepo, service.accountCmdRepo, + service.activityRecordCmdRepo, deleteDto, + ) + if err != nil { + return NewServiceOutput(InfraError, err.Error()) + } + + return NewServiceOutput(Created, "SecureAccessKeyDeleted") +} From 9e14dbf42d4e5f7e9a1d839904390a27a7cf25fa Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 20:03:44 -0300 Subject: [PATCH 027/100] refact: remove accountId from CreateSecureAccessKey DTO JSON annotation --- src/domain/dto/createSecureAccessKey.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/dto/createSecureAccessKey.go b/src/domain/dto/createSecureAccessKey.go index b34009738..85c126253 100644 --- a/src/domain/dto/createSecureAccessKey.go +++ b/src/domain/dto/createSecureAccessKey.go @@ -5,7 +5,7 @@ import "github.com/goinfinite/os/src/domain/valueObject" type CreateSecureAccessKey struct { Name valueObject.SecureAccessKeyName `json:"name"` Content valueObject.SecureAccessKeyContent `json:"content"` - AccountId valueObject.AccountId `json:"accountId"` + AccountId valueObject.AccountId `json:"-"` OperatorAccountId valueObject.AccountId `json:"-"` OperatorIpAddress valueObject.IpAddress `json:"-"` } From d6674bf3a9049d2cbe95807fa9394677db0a11e9 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 20:03:54 -0300 Subject: [PATCH 028/100] chore: update swagger --- src/presentation/api/docs/docs.go | 155 ++++++++++++++++++++++++- src/presentation/api/docs/swagger.json | 155 ++++++++++++++++++++++++- src/presentation/api/docs/swagger.yaml | 99 +++++++++++++++- 3 files changed, 406 insertions(+), 3 deletions(-) diff --git a/src/presentation/api/docs/docs.go b/src/presentation/api/docs/docs.go index 230a8917c..cdad2e58b 100644 --- a/src/presentation/api/docs/docs.go +++ b/src/presentation/api/docs/docs.go @@ -107,7 +107,7 @@ const docTemplate = `{ "tags": [ "account" ], - "summary": "CreateNewAccount", + "summary": "CreateAccount", "parameters": [ { "description": "All props are required.", @@ -166,6 +166,134 @@ const docTemplate = `{ } } }, + "/v1/account/{accountId}/secure-access-key/": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "List accounts secure access keys.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "account" + ], + "summary": "ReadSecureAccessKeys", + "parameters": [ + { + "type": "string", + "description": "AccountId that keys belongs to.", + "name": "accountId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.SecureAccessKey" + } + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new secure access key.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "account" + ], + "summary": "CreateSecureAccessKey", + "parameters": [ + { + "type": "string", + "description": "AccountId to create secure access key.", + "name": "accountId", + "in": "path", + "required": true + }, + { + "description": "All props are required.", + "name": "createSecureAccessKey", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateSecureAccessKey" + } + } + ], + "responses": { + "201": { + "description": "SecureAccessKeyCreated", + "schema": { + "type": "object" + } + } + } + } + }, + "/v1/account/{accountId}/secure-access-key/{secureAccessKeyId}/": { + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete a secure access key.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "account" + ], + "summary": "DeleteSecureAccessKey", + "parameters": [ + { + "type": "string", + "description": "AccountId that keys belongs to.", + "name": "accountId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "SecureAccessKeyId to delete.", + "name": "secureAccessKeyId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "SecureAccessKeyDeleted", + "schema": { + "type": "object" + } + } + } + } + }, "/v1/auth/login/": { "post": { "description": "Create a new session token with the provided credentials.", @@ -2369,6 +2497,17 @@ const docTemplate = `{ } } }, + "dto.CreateSecureAccessKey": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "dto.CreateSessionToken": { "type": "object", "properties": { @@ -3394,6 +3533,20 @@ const docTemplate = `{ } } }, + "entity.SecureAccessKey": { + "type": "object", + "properties": { + "encodedContent": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, "entity.SslCertificate": { "type": "object", "properties": { diff --git a/src/presentation/api/docs/swagger.json b/src/presentation/api/docs/swagger.json index 6ab041eb8..be4c2078d 100644 --- a/src/presentation/api/docs/swagger.json +++ b/src/presentation/api/docs/swagger.json @@ -101,7 +101,7 @@ "tags": [ "account" ], - "summary": "CreateNewAccount", + "summary": "CreateAccount", "parameters": [ { "description": "All props are required.", @@ -160,6 +160,134 @@ } } }, + "/v1/account/{accountId}/secure-access-key/": { + "get": { + "security": [ + { + "Bearer": [] + } + ], + "description": "List accounts secure access keys.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "account" + ], + "summary": "ReadSecureAccessKeys", + "parameters": [ + { + "type": "string", + "description": "AccountId that keys belongs to.", + "name": "accountId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.SecureAccessKey" + } + } + } + } + }, + "post": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Create a new secure access key.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "account" + ], + "summary": "CreateSecureAccessKey", + "parameters": [ + { + "type": "string", + "description": "AccountId to create secure access key.", + "name": "accountId", + "in": "path", + "required": true + }, + { + "description": "All props are required.", + "name": "createSecureAccessKey", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateSecureAccessKey" + } + } + ], + "responses": { + "201": { + "description": "SecureAccessKeyCreated", + "schema": { + "type": "object" + } + } + } + } + }, + "/v1/account/{accountId}/secure-access-key/{secureAccessKeyId}/": { + "delete": { + "security": [ + { + "Bearer": [] + } + ], + "description": "Delete a secure access key.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "account" + ], + "summary": "DeleteSecureAccessKey", + "parameters": [ + { + "type": "string", + "description": "AccountId that keys belongs to.", + "name": "accountId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "SecureAccessKeyId to delete.", + "name": "secureAccessKeyId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "SecureAccessKeyDeleted", + "schema": { + "type": "object" + } + } + } + } + }, "/v1/auth/login/": { "post": { "description": "Create a new session token with the provided credentials.", @@ -2363,6 +2491,17 @@ } } }, + "dto.CreateSecureAccessKey": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, "dto.CreateSessionToken": { "type": "object", "properties": { @@ -3388,6 +3527,20 @@ } } }, + "entity.SecureAccessKey": { + "type": "object", + "properties": { + "encodedContent": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, "entity.SslCertificate": { "type": "object", "properties": { diff --git a/src/presentation/api/docs/swagger.yaml b/src/presentation/api/docs/swagger.yaml index e0d026ef1..d98faf403 100644 --- a/src/presentation/api/docs/swagger.yaml +++ b/src/presentation/api/docs/swagger.yaml @@ -153,6 +153,13 @@ definitions: targetValue: type: string type: object + dto.CreateSecureAccessKey: + properties: + content: + type: string + name: + type: string + type: object dto.CreateSessionToken: properties: password: @@ -824,6 +831,15 @@ definitions: updatedAt: type: integer type: object + entity.SecureAccessKey: + properties: + encodedContent: + type: string + id: + type: integer + name: + type: string + type: object entity.SslCertificate: properties: altNames: @@ -1043,7 +1059,7 @@ paths: type: object security: - Bearer: [] - summary: CreateNewAccount + summary: CreateAccount tags: - account put: @@ -1092,6 +1108,87 @@ paths: summary: DeleteAccount tags: - account + /v1/account/{accountId}/secure-access-key/: + get: + consumes: + - application/json + description: List accounts secure access keys. + parameters: + - description: AccountId that keys belongs to. + in: path + name: accountId + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/entity.SecureAccessKey' + type: array + security: + - Bearer: [] + summary: ReadSecureAccessKeys + tags: + - account + post: + consumes: + - application/json + description: Create a new secure access key. + parameters: + - description: AccountId to create secure access key. + in: path + name: accountId + required: true + type: string + - description: All props are required. + in: body + name: createSecureAccessKey + required: true + schema: + $ref: '#/definitions/dto.CreateSecureAccessKey' + produces: + - application/json + responses: + "201": + description: SecureAccessKeyCreated + schema: + type: object + security: + - Bearer: [] + summary: CreateSecureAccessKey + tags: + - account + /v1/account/{accountId}/secure-access-key/{secureAccessKeyId}/: + delete: + consumes: + - application/json + description: Delete a secure access key. + parameters: + - description: AccountId that keys belongs to. + in: path + name: accountId + required: true + type: string + - description: SecureAccessKeyId to delete. + in: path + name: secureAccessKeyId + required: true + type: string + produces: + - application/json + responses: + "200": + description: SecureAccessKeyDeleted + schema: + type: object + security: + - Bearer: [] + summary: DeleteSecureAccessKey + tags: + - account /v1/auth/login/: post: consumes: From e82f88b928d97c5fc3d96daa2a00ad1dac549db5 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 20:07:49 -0300 Subject: [PATCH 029/100] chore: update CreateSecureAccessKey swagger notation --- src/presentation/api/controller/account.go | 2 +- src/presentation/api/docs/docs.go | 2 +- src/presentation/api/docs/swagger.json | 2 +- src/presentation/api/docs/swagger.yaml | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/presentation/api/controller/account.go b/src/presentation/api/controller/account.go index fc34d1bc3..798eafb8b 100644 --- a/src/presentation/api/controller/account.go +++ b/src/presentation/api/controller/account.go @@ -135,7 +135,7 @@ func (controller *AccountController) ReadSecureAccessKey(c echo.Context) error { // @Produce json // @Security Bearer // @Param accountId path string true "AccountId to create secure access key." -// @Param createSecureAccessKey body dto.CreateSecureAccessKey true "All props are required." +// @Param createSecureAccessKey body dto.CreateSecureAccessKey true "Only 'content' is required.
'name' will only become required if there is no name in 'content'. If the 'name' is provided, it will overwrite the name in the 'content'." // @Success 201 {object} object{} "SecureAccessKeyCreated" // @Router /v1/account/{accountId}/secure-access-key/ [post] func (controller *AccountController) CreateSecureAccessKey(c echo.Context) error { diff --git a/src/presentation/api/docs/docs.go b/src/presentation/api/docs/docs.go index cdad2e58b..d628cbe11 100644 --- a/src/presentation/api/docs/docs.go +++ b/src/presentation/api/docs/docs.go @@ -231,7 +231,7 @@ const docTemplate = `{ "required": true }, { - "description": "All props are required.", + "description": "Only 'content' is required.\u003cbr /\u003e'name' will only become required if there is no name in 'content'. If the 'name' is provided, it will overwrite the name in the 'content'.", "name": "createSecureAccessKey", "in": "body", "required": true, diff --git a/src/presentation/api/docs/swagger.json b/src/presentation/api/docs/swagger.json index be4c2078d..5b73cf86e 100644 --- a/src/presentation/api/docs/swagger.json +++ b/src/presentation/api/docs/swagger.json @@ -225,7 +225,7 @@ "required": true }, { - "description": "All props are required.", + "description": "Only 'content' is required.\u003cbr /\u003e'name' will only become required if there is no name in 'content'. If the 'name' is provided, it will overwrite the name in the 'content'.", "name": "createSecureAccessKey", "in": "body", "required": true, diff --git a/src/presentation/api/docs/swagger.yaml b/src/presentation/api/docs/swagger.yaml index d98faf403..e284f82a9 100644 --- a/src/presentation/api/docs/swagger.yaml +++ b/src/presentation/api/docs/swagger.yaml @@ -1143,7 +1143,9 @@ paths: name: accountId required: true type: string - - description: All props are required. + - description: Only 'content' is required.
'name' will only become required + if there is no name in 'content'. If the 'name' is provided, it will overwrite + the name in the 'content'. in: body name: createSecureAccessKey required: true From b4e79fdcdcd9ed20e1a931737dbb5115f9969d12 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Mon, 25 Nov 2024 21:32:41 -0300 Subject: [PATCH 030/100] refact: using try catch to return GET responses instead to use callback hell --- src/presentation/ui/assets/additional.js | 43 ++++++++++++++---------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/presentation/ui/assets/additional.js b/src/presentation/ui/assets/additional.js index 0fd53ca19..aecff5ebb 100644 --- a/src/presentation/ui/assets/additional.js +++ b/src/presentation/ui/assets/additional.js @@ -5,24 +5,31 @@ document.addEventListener("alpine:init", () => { const loadingOverlayElement = document.getElementById("loading-overlay"); loadingOverlayElement.classList.add("htmx-request"); - await fetch(url, { - method: method, - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }) - .then((response) => { - loadingOverlayElement.classList.remove("htmx-request"); - return response.json(); - }) - .then((parsedResponse) => { - Alpine.store("toast").displayToast(parsedResponse.body, "success"); - }) - .catch((parsedResponse) => { - Alpine.store("toast").displayToast(parsedResponse.body, "danger"); - }); + try { + const response = await fetch(url, { + method: method, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + const parsedResponse = await response.json() + + loadingOverlayElement.classList.remove("htmx-request"); + + if (!response.ok) { + throw new Error(parsedResponse.body); + } + + if (method.toUpperCase() !== "GET") { + Alpine.store("toast").displayToast(parsedResponse.body, "success"); + } + + return parsedResponse.body; + } catch (error) { + Alpine.store("toast").displayToast(error.message, "danger"); + } } function createRandomPassword() { From 3d7acced38185d0fd742df78b20cdb102df35ea9 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Tue, 26 Nov 2024 10:59:53 -0300 Subject: [PATCH 031/100] feat: add empty structure to accounts page to show all secure access keys without any implementation yet --- src/presentation/ui/page/accounts.templ | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/presentation/ui/page/accounts.templ b/src/presentation/ui/page/accounts.templ index 8c82a0181..2038c815a 100644 --- a/src/presentation/ui/page/accounts.templ +++ b/src/presentation/ui/page/accounts.templ @@ -18,6 +18,7 @@ script AccountsIndexLocalState() { username: '', password: '', apiKey: '', + secureAccessKeys: [], } }, init() { @@ -75,6 +76,25 @@ script AccountsIndexLocalState() { }, ); }, + isSecureAccessKeysModalOpen: false, + async readSecureAccessKeys() { + const responseData = await Infinite.JsonAjax( + 'GET', '/api/v1/account/' + this.account.id + '/secure-access-key/' + ); + this.account.secureAccessKeys = responseData; + }, + async openSecureAccessKeysModal(id, username) { + this.resetPrimaryStates(); + + this.account.id = id; + this.account.username = username; + await this.readSecureAccessKeys(); + + this.isSecureAccessKeysModalOpen = true; + }, + closeSecureAccessKeysModal() { + this.isSecureAccessKeysModalOpen = false; + }, isDeleteAccountModalOpen: false, openDeleteAccountModal(id, username) { this.resetPrimaryStates(); @@ -127,6 +147,7 @@ templ AccountsIndex(accounts []entity.Account) { @CreateAccountModal() @UpdatePasswordModal() @UpdateApiKeyModal() + @SecureAccessKeysModal() @componentStructural.DeleteModal( "isDeleteAccountModalOpen", "closeDeleteAccountModal()", "deleteAccount()", "account.username", "account.id", @@ -167,6 +188,11 @@ templ AccountsTable(accounts []entity.Account) { "openUpdateApiKeyModal("+account.Id.String()+", '"+account.Username.String()+"')", "update api key", "blue-500", ) + @componentStructural.CircularIconButtonWithTooltip( + "ph-fingerprint", "blue-900", "blue-700", + "openSecureAccessKeysModal("+account.Id.String()+")", + "secure access keys", "blue-500", + ) @componentStructural.CircularIconButtonWithTooltip( "ph-trash", "red-800", "red-600", "openDeleteAccountModal("+account.Id.String()+", '"+account.Username.String()+"')", @@ -255,3 +281,20 @@ templ UpdateApiKeyModal() { @UpdateApiKeyContent() } } + +templ SecureAccessKeysList() { + @componentStructural.Alert( + componentStructural.AlertTypeInfo, + "The management of these keys will only be possible if the OpenSSH service is installed. Otherwise, you won't be able to add or remove keys, as they are used for SSH access.", + ) +
+} + +templ SecureAccessKeysModal() { + @componentStructural.Modal( + "Secure Access Keys", "isSecureAccessKeysModalOpen", "closeSecureAccessKeysModal()", + "account.username", + ) { + @SecureAccessKeysList() + } +} From a6e69a71a07b633b2a3d4d3edfb9c06c31673d58 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Wed, 27 Nov 2024 14:13:54 -0300 Subject: [PATCH 032/100] feat: implements ssh dev build to install openssh and export port 22 to public port 2222 --- dev-build.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dev-build.sh b/dev-build.sh index 437dce7f5..00f31b826 100644 --- a/dev-build.sh +++ b/dev-build.sh @@ -10,6 +10,9 @@ http) ols) ports+=(-p 7080:7080) ;; +ssh) + ports+=(-p 2222:22) + ;; no-cache) podman image prune -a podman rmi localhost/os -f @@ -35,6 +38,11 @@ podman exec os /bin/bash -c 'rm -f os && ln -s bin/os os && supervisorctl restar echo "=> Creating a development account..." podman exec os /bin/bash -c 'os account create -u dev -p 123456' +if [ $1 == "ssh" ]; then + echo "=> Installing OpenSSH..." + podman exec os /bin/bash -c 'os services create-installable -n openssh' +fi + echo echo "<<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>" echo From 7ff8797b9841d091da087bb03cf970b47bba520e Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Wed, 27 Nov 2024 14:14:13 -0300 Subject: [PATCH 033/100] feat imlements openssh service availability validation to create and delete methods --- src/presentation/service/account.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/presentation/service/account.go b/src/presentation/service/account.go index 2e00584b2..8e60d05a5 100644 --- a/src/presentation/service/account.go +++ b/src/presentation/service/account.go @@ -253,6 +253,11 @@ func (service *AccountService) ReadSecureAccessKey( func (service *AccountService) CreateSecureAccessKey( input map[string]interface{}, ) ServiceOutput { + serviceName, _ := valueObject.NewServiceName("openssh") + if !service.availabilityInspector.IsAvailable(serviceName) { + return NewServiceOutput(InfraError, sharedHelper.ServiceUnavailableError) + } + requiredParams := []string{"accountId", "content"} err := serviceHelper.RequiredParamsInspector(input, requiredParams) if err != nil { @@ -311,6 +316,11 @@ func (service *AccountService) CreateSecureAccessKey( func (service *AccountService) DeleteSecureAccessKey( input map[string]interface{}, ) ServiceOutput { + serviceName, _ := valueObject.NewServiceName("openssh") + if !service.availabilityInspector.IsAvailable(serviceName) { + return NewServiceOutput(InfraError, sharedHelper.ServiceUnavailableError) + } + requiredParams := []string{"accountId", "secureAccessKeyId"} err := serviceHelper.RequiredParamsInspector(input, requiredParams) if err != nil { From 4c6cc828545953e77914fc2b9a89f94a5b719071 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Wed, 27 Nov 2024 19:24:36 -0300 Subject: [PATCH 034/100] refact: fit all group inside the circular button component --- .../ui/component/structural/circularIconButton.templ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/ui/component/structural/circularIconButton.templ b/src/presentation/ui/component/structural/circularIconButton.templ index ff6ca2785..829fc35a6 100644 --- a/src/presentation/ui/component/structural/circularIconButton.templ +++ b/src/presentation/ui/component/structural/circularIconButton.templ @@ -4,7 +4,7 @@ templ CircularIconButtonWithTooltip( icon, defaultColor, hoverColor, onClick, tooltipText, tooltipColor string, ) { -
+
} @@ -212,7 +250,7 @@ templ CreateAccountForm() { hx-indicator="#loading-overlay" hx-swap="none" > -
+
@componentForm.InputField("text", "username", "Username", "account.username", false) @componentForm.PasswordInput("password", "Password", "account.password")
@@ -287,7 +325,28 @@ templ SecureAccessKeysList() { componentStructural.AlertTypeInfo, "The management of these keys will only be possible if the OpenSSH service is installed. Otherwise, you won't be able to add or remove keys, as they are used for SSH access.", ) -
+
+ +
} templ SecureAccessKeysModal() { From ba1cebcaebfad6ffe5d98740d0778421eaad4a74 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Wed, 27 Nov 2024 20:59:32 -0300 Subject: [PATCH 036/100] chore: add "templ" as ignored cSpell word --- .vscode/settings.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3aede7ef5..350f34564 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -74,6 +74,7 @@ "svcs", "Svcs", "swaggo", + "templ", "Totalram", "userdel", "usermod", From 208b0810f404a29f9b54a603f42e2f80605e9bfb Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Wed, 27 Nov 2024 21:04:27 -0300 Subject: [PATCH 037/100] refact: add margin right to create account button --- src/presentation/ui/page/accounts.templ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/ui/page/accounts.templ b/src/presentation/ui/page/accounts.templ index f51feca2b..1b11aa392 100644 --- a/src/presentation/ui/page/accounts.templ +++ b/src/presentation/ui/page/accounts.templ @@ -200,7 +200,7 @@ templ AccountsTable(accounts []entity.Account) { Username(s) UserId(s)/GroupId(s) -
+
@componentForm.SubmitButton( "open-create-account-form-button", "Create account", "ph-user-plus", "openCreateAccountModal()", false, From 19a24be9b13e3d7478059475a00409198c6998ff Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Wed, 27 Nov 2024 21:15:54 -0300 Subject: [PATCH 038/100] refact: remove readUsernameById --- src/infra/account/accountCmdRepo.go | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/infra/account/accountCmdRepo.go b/src/infra/account/accountCmdRepo.go index 0e645e277..4e21c99cf 100644 --- a/src/infra/account/accountCmdRepo.go +++ b/src/infra/account/accountCmdRepo.go @@ -86,19 +86,8 @@ func (repo *AccountCmdRepo) Create( return accountId, nil } -func (repo *AccountCmdRepo) readUsernameById( - accountId valueObject.AccountId, -) (username valueObject.Username, err error) { - accountEntity, err := repo.accountQueryRepo.ReadById(accountId) - if err != nil { - return username, err - } - - return accountEntity.Username, nil -} - func (repo *AccountCmdRepo) Delete(accountId valueObject.AccountId) error { - username, err := repo.readUsernameById(accountId) + account, err := repo.accountQueryRepo.ReadById(accountId) if err != nil { return err } @@ -110,7 +99,7 @@ func (repo *AccountCmdRepo) Delete(accountId valueObject.AccountId) error { _, _ = infraHelper.RunCmd("pkill", "-9", "-U", accountIdStr) } - _, err = infraHelper.RunCmd("userdel", "-r", username.String()) + _, err = infraHelper.RunCmd("userdel", "-r", account.Username.String()) if err != nil { return err } @@ -135,12 +124,12 @@ func (repo *AccountCmdRepo) UpdatePassword( return errors.New("PasswordHashError: " + err.Error()) } - username, err := repo.readUsernameById(accountId) + account, err := repo.accountQueryRepo.ReadById(accountId) if err != nil { return err } - _, err = infraHelper.RunCmd("usermod", "-p", string(passHash), username.String()) + _, err = infraHelper.RunCmd("usermod", "-p", string(passHash), account.Username.String()) if err != nil { return errors.New("UserModFailed: " + err.Error()) } From 6a01bab30cfd38dc0700be12b57cc002fed6f80b Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Wed, 27 Nov 2024 21:46:13 -0300 Subject: [PATCH 039/100] feat: implements allowAccountSecureRemoteConnection to accountCmdRepo --- src/infra/account/accountCmdRepo.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/infra/account/accountCmdRepo.go b/src/infra/account/accountCmdRepo.go index 4e21c99cf..cb5204990 100644 --- a/src/infra/account/accountCmdRepo.go +++ b/src/infra/account/accountCmdRepo.go @@ -247,6 +247,27 @@ func (repo *AccountCmdRepo) isSecureAccessKeyValid( return true } +func (repo *AccountCmdRepo) allowAccountSecureRemoteConnection( + accountId valueObject.AccountId, +) error { + accountUsername, err := infraHelper.RunCmdWithSubShell( + "awk -F: '$3 == " + accountId.String() + + " && $7 != \"/bin/bash\" {print $1}' /etc/passwd", + ) + if err != nil { + return errors.New("ReadUnixUsernameFromFileError: " + err.Error()) + } + + _, err = infraHelper.RunCmdWithSubShell( + "chsh -s /bin/bash " + accountUsername, + ) + if err != nil { + return errors.New("ChangeDefaultBashError: " + err.Error()) + } + + return nil +} + func (repo *AccountCmdRepo) CreateSecureAccessKey( createDto dto.CreateSecureAccessKey, ) (keyId valueObject.SecureAccessKeyId, err error) { @@ -286,6 +307,11 @@ func (repo *AccountCmdRepo) CreateSecureAccessKey( return keyId, errors.New("SecureAccessKeyWasNotCreated") } + err = repo.allowAccountSecureRemoteConnection(createDto.AccountId) + if err != nil { + return keyId, err + } + return key.Id, nil } From b59ff5d88451751a16b1c41273a15269cc6b40e1 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 12:51:40 -0300 Subject: [PATCH 040/100] feat: implements infoTooltipContent to TextArea component and turn params into DTO --- .../ui/component/form/textArea.templ | 40 ++++++++++++------- src/presentation/ui/page/mappings.templ | 6 ++- src/presentation/ui/page/ssls.templ | 12 +++++- 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/presentation/ui/component/form/textArea.templ b/src/presentation/ui/component/form/textArea.templ index 6c5340d2c..6d07787d0 100644 --- a/src/presentation/ui/component/form/textArea.templ +++ b/src/presentation/ui/component/form/textArea.templ @@ -1,28 +1,40 @@ package componentForm -templ TextArea( - id, label, bindValuePath string, - denseMode bool, -) { +type TextAreaDto struct { + Id string + Label string + BindModelValuePath string + InfoTooltipContent string +} + +templ TextArea(inputDto TextAreaDto) {
- if label != "" { + if inputDto.InfoTooltipContent != "" { +
+ + +
+ } + if inputDto.Label != "" { }
diff --git a/src/presentation/ui/page/mappings.templ b/src/presentation/ui/page/mappings.templ index 6a6d4c961..d01020b25 100644 --- a/src/presentation/ui/page/mappings.templ +++ b/src/presentation/ui/page/mappings.templ @@ -321,7 +321,11 @@ templ CreateMappingForm(vhostsHostnames []string) { diff --git a/src/presentation/ui/page/ssls.templ b/src/presentation/ui/page/ssls.templ index d6a208882..9280686e8 100644 --- a/src/presentation/ui/page/ssls.templ +++ b/src/presentation/ui/page/ssls.templ @@ -212,8 +212,16 @@ templ ImportSslCertificateForm(vhostsHostnames []string) { )
- @componentForm.TextArea("certificate", "Certificate", "sslPair.certificate", false) - @componentForm.TextArea("key", "Private Key", "sslPair.key", false) + @componentForm.TextArea(componentForm.TextAreaDto{ + Id: "certificate", + Label: "Certificate", + BindModelValuePath: "sslPair.certificate", + }) + @componentForm.TextArea(componentForm.TextAreaDto{ + Id: "key", + Label: "Private Key", + BindModelValuePath: "sslPair.key", + })
@componentForm.FileUploadTextInputFileContentReader( From d82481ebac13c7bc4c72d89cdbe544ef66e8f57d Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 14:24:55 -0300 Subject: [PATCH 041/100] refact: split API router group to have accountGroup and secureAccessKeyGroup --- src/presentation/api/router.go | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/presentation/api/router.go b/src/presentation/api/router.go index 5b287b7ca..0dc35562b 100644 --- a/src/presentation/api/router.go +++ b/src/presentation/api/router.go @@ -57,15 +57,11 @@ func (router Router) accountRoutes() { accountGroup.POST("/", accountController.Create) accountGroup.PUT("/", accountController.Update) accountGroup.DELETE("/:accountId/", accountController.Delete) - accountGroup.GET( - "/:accountId/secure-access-key/", accountController.ReadSecureAccessKey, - ) - accountGroup.POST( - "/:accountId/secure-access-key/", accountController.CreateSecureAccessKey, - ) - accountGroup.DELETE( - "/:accountId/secure-access-key/:secureAccessKeyId/", accountController.DeleteSecureAccessKey, - ) + + secureAccessKeyGroup := accountGroup.Group("/:accountId/secure-access-key") + secureAccessKeyGroup.GET("/", accountController.ReadSecureAccessKey) + secureAccessKeyGroup.POST("/", accountController.CreateSecureAccessKey) + secureAccessKeyGroup.DELETE("/:secureAccessKeyId/", accountController.DeleteSecureAccessKey) } func (router Router) cronRoutes() { From abc40ac265b45508c690019b8961d723dc2dcb62 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 14:32:06 -0300 Subject: [PATCH 042/100] refact: remove account secure access key security var from env vars --- .env.example | 1 - src/presentation/cli/middleware/checkEnvs.go | 2 -- 2 files changed, 3 deletions(-) diff --git a/.env.example b/.env.example index ec3c7f8dc..22ded2211 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,6 @@ DUMMY_USER_PASS=example123 ## Must be 32 bytes-key-before-base64-encoding if generated manually: #JWT_SECRET= #ACCOUNT_API_KEY_SECRET= -#ACCOUNT_SECURE_ACCESS_KEY_SECRET= # Optional TRUSTED_IPS= diff --git a/src/presentation/cli/middleware/checkEnvs.go b/src/presentation/cli/middleware/checkEnvs.go index 300c0e765..b8e5b0248 100644 --- a/src/presentation/cli/middleware/checkEnvs.go +++ b/src/presentation/cli/middleware/checkEnvs.go @@ -14,14 +14,12 @@ import ( var requiredEnvVars = []string{ "ACCOUNT_API_KEY_SECRET", - "ACCOUNT_SECURE_ACCESS_KEY_SECRET", "JWT_SECRET", "PRIMARY_VHOST", } var envVarsToGenerateIfEmpty = []string{ "ACCOUNT_API_KEY_SECRET", - "ACCOUNT_SECURE_ACCESS_KEY_SECRET", "JWT_SECRET", } From 5234cc65a163f6040c89df73c33f22bab079f0e9 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 16:03:16 -0300 Subject: [PATCH 043/100] refact: move all secure access keys methods and implementation to you own infra (incomplete) --- src/domain/entity/secureAccessKey.go | 23 ++- src/domain/repository/accountCmdRepo.go | 4 - src/domain/repository/accountQueryRepo.go | 7 +- .../repository/secureAccessKeyCmdRepo.go | 11 + .../repository/secureAccessKeyQueryRepo.go | 10 + src/domain/useCase/createSecureAccessKey.go | 4 +- src/domain/useCase/deleteSecureAccessKey.go | 4 +- .../useCase/readAccountSecureAccessKeys.go | 23 --- src/domain/useCase/readSecureAccessKeys.go | 29 +++ src/infra/account/accountCmdRepo.go | 171 ---------------- src/infra/account/accountCmdRepo_test.go | 20 -- src/infra/account/accountQueryRepo.go | 129 ------------ src/infra/account/accountQueryRepo_test.go | 41 ---- .../secureAccessKey/secureAccessKeyCmdRepo.go | 190 ++++++++++++++++++ .../secureAccessKeyCmdRepo_test.go | 68 +++++++ .../secureAccessKeyQueryRepo.go | 147 ++++++++++++++ .../secureAccessKeyQueryRepo_test.go | 75 +++++++ src/infra/internalDatabase/model/account.go | 13 +- .../internalDatabase/model/secureAccessKey.go | 62 ++++++ .../persistentDatabaseService.go | 1 + src/presentation/service/account.go | 33 +-- 21 files changed, 641 insertions(+), 424 deletions(-) create mode 100644 src/domain/repository/secureAccessKeyCmdRepo.go create mode 100644 src/domain/repository/secureAccessKeyQueryRepo.go delete mode 100644 src/domain/useCase/readAccountSecureAccessKeys.go create mode 100644 src/domain/useCase/readSecureAccessKeys.go create mode 100644 src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go create mode 100644 src/infra/account/secureAccessKey/secureAccessKeyCmdRepo_test.go create mode 100644 src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go create mode 100644 src/infra/account/secureAccessKey/secureAccessKeyQueryRepo_test.go create mode 100644 src/infra/internalDatabase/model/secureAccessKey.go diff --git a/src/domain/entity/secureAccessKey.go b/src/domain/entity/secureAccessKey.go index b2bae9c1c..5e854beb6 100644 --- a/src/domain/entity/secureAccessKey.go +++ b/src/domain/entity/secureAccessKey.go @@ -3,22 +3,27 @@ package entity import "github.com/goinfinite/os/src/domain/valueObject" type SecureAccessKey struct { - Id valueObject.SecureAccessKeyId `json:"id"` - Name valueObject.SecureAccessKeyName `json:"name"` - Content valueObject.SecureAccessKeyContent `json:"-"` - EncodedContent valueObject.EncodedContent `json:"encodedContent"` + Id valueObject.SecureAccessKeyId `json:"id"` + AccountId valueObject.AccountId `json:"accountId"` + Name valueObject.SecureAccessKeyName `json:"name"` + Content valueObject.SecureAccessKeyContent `json:"content"` + CreatedAt valueObject.UnixTime `json:"createdAt"` + UpdatedAt valueObject.UnixTime `json:"updatedAt"` } func NewSecureAccessKey( id valueObject.SecureAccessKeyId, + accountId valueObject.AccountId, name valueObject.SecureAccessKeyName, content valueObject.SecureAccessKeyContent, - encodedContent valueObject.EncodedContent, + createdAt, updatedAt valueObject.UnixTime, ) SecureAccessKey { return SecureAccessKey{ - Id: id, - Name: name, - Content: content, - EncodedContent: encodedContent, + Id: id, + AccountId: accountId, + Name: name, + Content: content, + CreatedAt: createdAt, + UpdatedAt: updatedAt, } } diff --git a/src/domain/repository/accountCmdRepo.go b/src/domain/repository/accountCmdRepo.go index ee0fba127..b1e7ea389 100644 --- a/src/domain/repository/accountCmdRepo.go +++ b/src/domain/repository/accountCmdRepo.go @@ -10,8 +10,4 @@ type AccountCmdRepo interface { Delete(valueObject.AccountId) error UpdatePassword(valueObject.AccountId, valueObject.Password) error UpdateApiKey(valueObject.AccountId) (valueObject.AccessTokenStr, error) - CreateSecureAccessKey( - dto.CreateSecureAccessKey, - ) (valueObject.SecureAccessKeyId, error) - DeleteSecureAccessKey(dto.DeleteSecureAccessKey) error } diff --git a/src/domain/repository/accountQueryRepo.go b/src/domain/repository/accountQueryRepo.go index d4c6fae16..baeec88f9 100644 --- a/src/domain/repository/accountQueryRepo.go +++ b/src/domain/repository/accountQueryRepo.go @@ -7,9 +7,6 @@ import ( type AccountQueryRepo interface { Read() ([]entity.Account, error) - ReadByUsername(username valueObject.Username) (entity.Account, error) - ReadById(accountId valueObject.AccountId) (entity.Account, error) - ReadSecureAccessKeys( - accountId valueObject.AccountId, - ) ([]entity.SecureAccessKey, error) + ReadByUsername(valueObject.Username) (entity.Account, error) + ReadById(valueObject.AccountId) (entity.Account, error) } diff --git a/src/domain/repository/secureAccessKeyCmdRepo.go b/src/domain/repository/secureAccessKeyCmdRepo.go new file mode 100644 index 000000000..b1a6208d7 --- /dev/null +++ b/src/domain/repository/secureAccessKeyCmdRepo.go @@ -0,0 +1,11 @@ +package repository + +import ( + "github.com/goinfinite/os/src/domain/dto" + "github.com/goinfinite/os/src/domain/valueObject" +) + +type SecureAccessKeyCmdRepo interface { + Create(dto.CreateSecureAccessKey) (valueObject.SecureAccessKeyId, error) + Delete(dto.DeleteSecureAccessKey) error +} diff --git a/src/domain/repository/secureAccessKeyQueryRepo.go b/src/domain/repository/secureAccessKeyQueryRepo.go new file mode 100644 index 000000000..02d065133 --- /dev/null +++ b/src/domain/repository/secureAccessKeyQueryRepo.go @@ -0,0 +1,10 @@ +package repository + +import ( + "github.com/goinfinite/os/src/domain/entity" + "github.com/goinfinite/os/src/domain/valueObject" +) + +type SecureAccessKeyQueryRepo interface { + Read(valueObject.AccountId) ([]entity.SecureAccessKey, error) +} diff --git a/src/domain/useCase/createSecureAccessKey.go b/src/domain/useCase/createSecureAccessKey.go index 5c2f533dc..d214a2d90 100644 --- a/src/domain/useCase/createSecureAccessKey.go +++ b/src/domain/useCase/createSecureAccessKey.go @@ -10,7 +10,7 @@ import ( func CreateSecureAccessKey( accountQueryRepo repository.AccountQueryRepo, - accountCmdRepo repository.AccountCmdRepo, + secureAccessKeyCmdRepo repository.SecureAccessKeyCmdRepo, activityRecordCmdRepo repository.ActivityRecordCmdRepo, createDto dto.CreateSecureAccessKey, ) error { @@ -19,7 +19,7 @@ func CreateSecureAccessKey( return errors.New("AccountNotFound") } - keyId, err := accountCmdRepo.CreateSecureAccessKey(createDto) + keyId, err := secureAccessKeyCmdRepo.Create(createDto) if err != nil { slog.Error("CreateSecureAccessKeyError", slog.Any("error", err)) return errors.New("CreateSecureAccessKeyInfraError") diff --git a/src/domain/useCase/deleteSecureAccessKey.go b/src/domain/useCase/deleteSecureAccessKey.go index 6aad79868..802f9d28c 100644 --- a/src/domain/useCase/deleteSecureAccessKey.go +++ b/src/domain/useCase/deleteSecureAccessKey.go @@ -10,7 +10,7 @@ import ( func DeleteSecureAccessKey( accountQueryRepo repository.AccountQueryRepo, - accountCmdRepo repository.AccountCmdRepo, + secureAccessKeyCmdRepo repository.SecureAccessKeyCmdRepo, activityRecordCmdRepo repository.ActivityRecordCmdRepo, deleteDto dto.DeleteSecureAccessKey, ) error { @@ -19,7 +19,7 @@ func DeleteSecureAccessKey( return errors.New("AccountNotFound") } - err = accountCmdRepo.DeleteSecureAccessKey(deleteDto) + err = secureAccessKeyCmdRepo.Delete(deleteDto) if err != nil { slog.Error("DeleteSecureAccessKeyError", slog.Any("error", err)) return errors.New("DeleteSecureAccessKeyInfraError") diff --git a/src/domain/useCase/readAccountSecureAccessKeys.go b/src/domain/useCase/readAccountSecureAccessKeys.go deleted file mode 100644 index 7589057f0..000000000 --- a/src/domain/useCase/readAccountSecureAccessKeys.go +++ /dev/null @@ -1,23 +0,0 @@ -package useCase - -import ( - "errors" - "log/slog" - - "github.com/goinfinite/os/src/domain/entity" - "github.com/goinfinite/os/src/domain/repository" - "github.com/goinfinite/os/src/domain/valueObject" -) - -func ReadAccountSecureAccessKeys( - accountQueryRepo repository.AccountQueryRepo, - accountId valueObject.AccountId, -) (secureAccessKeys []entity.SecureAccessKey, err error) { - secureAccessKeys, err = accountQueryRepo.ReadSecureAccessKeys(accountId) - if err != nil { - slog.Error("ReadAccountSecureAccessKeysInfraError", slog.Any("error", err)) - return secureAccessKeys, errors.New("ReadAccountSecureAccessKeysInfraError") - } - - return secureAccessKeys, nil -} diff --git a/src/domain/useCase/readSecureAccessKeys.go b/src/domain/useCase/readSecureAccessKeys.go new file mode 100644 index 000000000..06d044949 --- /dev/null +++ b/src/domain/useCase/readSecureAccessKeys.go @@ -0,0 +1,29 @@ +package useCase + +import ( + "errors" + "log/slog" + + "github.com/goinfinite/os/src/domain/entity" + "github.com/goinfinite/os/src/domain/repository" + "github.com/goinfinite/os/src/domain/valueObject" +) + +func ReadSecureAccessKeys( + accountQueryRepo repository.AccountQueryRepo, + secureAccessKeyQueryRepo repository.SecureAccessKeyQueryRepo, + accountId valueObject.AccountId, +) (secureAccessKeys []entity.SecureAccessKey, err error) { + _, err = accountQueryRepo.ReadById(accountId) + if err != nil { + return secureAccessKeys, errors.New("AccountNotFound") + } + + secureAccessKeys, err = secureAccessKeyQueryRepo.Read(accountId) + if err != nil { + slog.Error("ReadSecureAccessKeysInfraError", slog.Any("error", err)) + return secureAccessKeys, errors.New("ReadSecureAccessKeysInfraError") + } + + return secureAccessKeys, nil +} diff --git a/src/infra/account/accountCmdRepo.go b/src/infra/account/accountCmdRepo.go index cb5204990..57336200c 100644 --- a/src/infra/account/accountCmdRepo.go +++ b/src/infra/account/accountCmdRepo.go @@ -2,7 +2,6 @@ package accountInfra import ( "errors" - "log/slog" "os" "os/user" "time" @@ -170,173 +169,3 @@ func (repo *AccountCmdRepo) UpdateApiKey( return apiKey, nil } - -func (repo *AccountCmdRepo) ensureSecureAccessKeysDirAndFileExistence( - accountUsername valueObject.Username, -) error { - accountUsernameStr := accountUsername.String() - - secureAccessKeysDirPath := "/home/" + accountUsernameStr + "/.ssh" - if !infraHelper.FileExists(secureAccessKeysDirPath) { - err := infraHelper.MakeDir(secureAccessKeysDirPath) - if err != nil { - return errors.New("CreateSecureAccessKeysDirectoryError: " + err.Error()) - } - } - - secureAccessKeysFilePath := secureAccessKeysDirPath + "/authorized_keys" - if infraHelper.FileExists(secureAccessKeysFilePath) { - return nil - } - - _, err := os.Create(secureAccessKeysFilePath) - if err != nil { - return errors.New("CreateSecureAccessKeysFileError: " + err.Error()) - } - - _, err = infraHelper.RunCmd( - "chown", "-R", accountUsernameStr, secureAccessKeysFilePath, - ) - if err != nil { - return errors.New("ChownSecureAccessKeysFileError: " + err.Error()) - } - - return nil -} - -func (repo *AccountCmdRepo) isSecureAccessKeyValid( - keyContent valueObject.SecureAccessKeyContent, -) bool { - keyName, err := keyContent.ReadOnlyKeyName() - if err != nil { - slog.Error(err.Error()) - return false - } - keyNameStr := keyName.String() - - keyTempFilePath := "/tmp/" + keyNameStr + "_secureAccessKey" - shouldOverwrite := true - err = infraHelper.UpdateFile( - keyTempFilePath, keyContent.String(), shouldOverwrite, - ) - if err != nil { - slog.Error( - "CreateSecureAccessKeyTempFileError", slog.String("keyName", keyNameStr), - slog.Any("err", err), - ) - return false - } - - _, err = infraHelper.RunCmdWithSubShell("ssh-keygen -l -f " + keyTempFilePath) - if err != nil { - slog.Error( - "ValidateSecureAccessKeyError", slog.String("keyName", keyNameStr), - slog.Any("err", err), - ) - return false - } - - err = os.Remove(keyTempFilePath) - if err != nil { - slog.Error( - "DeleteSecureAccessKeyTempFileError", slog.String("keyName", keyNameStr), - slog.Any("err", err), - ) - } - - return true -} - -func (repo *AccountCmdRepo) allowAccountSecureRemoteConnection( - accountId valueObject.AccountId, -) error { - accountUsername, err := infraHelper.RunCmdWithSubShell( - "awk -F: '$3 == " + accountId.String() + - " && $7 != \"/bin/bash\" {print $1}' /etc/passwd", - ) - if err != nil { - return errors.New("ReadUnixUsernameFromFileError: " + err.Error()) - } - - _, err = infraHelper.RunCmdWithSubShell( - "chsh -s /bin/bash " + accountUsername, - ) - if err != nil { - return errors.New("ChangeDefaultBashError: " + err.Error()) - } - - return nil -} - -func (repo *AccountCmdRepo) CreateSecureAccessKey( - createDto dto.CreateSecureAccessKey, -) (keyId valueObject.SecureAccessKeyId, err error) { - account, err := repo.accountQueryRepo.ReadById(createDto.AccountId) - if err != nil { - return keyId, errors.New("AccountNotFound") - } - - err = repo.ensureSecureAccessKeysDirAndFileExistence(account.Username) - if err != nil { - return keyId, err - } - - keyContentStr := createDto.Content.ReadWithoutKeyName() + " " + - createDto.Name.String() - keyContent, err := valueObject.NewSecureAccessKeyContent(keyContentStr) - if err != nil { - return keyId, errors.New("InvalidSecureAccessKey") - } - - if !repo.isSecureAccessKeyValid(keyContent) { - return keyId, errors.New("InvalidSecureAccessKey") - } - - _, err = infraHelper.RunCmdWithSubShell( - "echo \"" + keyContentStr + "\" >> /home/" + account.Username.String() + - "/.ssh/authorized_keys", - ) - if err != nil { - return keyId, errors.New("FailToAddNewSecureAccessKeyToFile: " + err.Error()) - } - - key, err := repo.accountQueryRepo.ReadSecureAccessKeyByName( - createDto.AccountId, createDto.Name, - ) - if err != nil { - return keyId, errors.New("SecureAccessKeyWasNotCreated") - } - - err = repo.allowAccountSecureRemoteConnection(createDto.AccountId) - if err != nil { - return keyId, err - } - - return key.Id, nil -} - -func (repo *AccountCmdRepo) DeleteSecureAccessKey( - deleteDto dto.DeleteSecureAccessKey, -) error { - account, err := repo.accountQueryRepo.ReadById(deleteDto.AccountId) - if err != nil { - return errors.New("AccountNotFound") - } - - keyToDelete, err := repo.accountQueryRepo.ReadSecureAccessKeyById( - deleteDto.AccountId, deleteDto.Id, - ) - if err != nil { - return err - } - - _, err = infraHelper.RunCmdWithSubShell( - "sed -i '\\|" + keyToDelete.Content.String() + "|d' " + - "/home/" + account.Username.String() + "/.ssh/authorized_keys", - ) - if err != nil { - return errors.New("FailToDeleteSecureAccessKeyFromFile: " + err.Error()) - } - - return nil -} diff --git a/src/infra/account/accountCmdRepo_test.go b/src/infra/account/accountCmdRepo_test.go index 4fd608744..33efae238 100644 --- a/src/infra/account/accountCmdRepo_test.go +++ b/src/infra/account/accountCmdRepo_test.go @@ -107,24 +107,4 @@ func TestAccountCmdRepo(t *testing.T) { ) } }) - - t.Skip("SkipSecureAccessKeysTests") - - t.Run("CreateSecureAccessKey", func(t *testing.T) { - keyName, _ := valueObject.NewSecureAccessKeyName("dummySecureAccessKey") - keyContent, _ := valueObject.NewSecureAccessKeyContent( - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+GDqLA2sGauzU5hUxBbBmm6FfeZpUbiX6IlQO9KqeqAsum+Efhvj+qpatM5PzMMwtlcFwDS5Y4RcX9uxE8IGsYiALRfnLAX5p73zrcrXamMJSx25rXAu/VJdmekxHbDgsBPyk6/4dfu+3uW7ka7HHhPytPIqW2qBuPkalJinc7qKEuXdkCyX8+8a+0uN8XodLipLJwU8A1VPvI9thYxITyHWZnXRnin0r/unHgLrg9bBILXZf0JRslelYdCvuCGnRKZfokh153shMZ63S+iV/Tohg2bOVxyz3HIQ983ga24uTFQhLpITMe9JEfq3pp2wcCE5hNFlNKyeDG8kwB+8V", - ) - createDto := dto.NewCreateSecureAccessKey( - keyName, keyContent, accountId, accountId, - valueObject.NewLocalhostIpAddress(), - ) - - _, err := accountCmdRepo.CreateSecureAccessKey(createDto) - if err != nil { - t.Fatalf( - "Expected no error for %s, but got %s", keyName.String(), err.Error(), - ) - } - }) } diff --git a/src/infra/account/accountQueryRepo.go b/src/infra/account/accountQueryRepo.go index 3ecdc4175..5f97b99b2 100644 --- a/src/infra/account/accountQueryRepo.go +++ b/src/infra/account/accountQueryRepo.go @@ -3,12 +3,9 @@ package accountInfra import ( "errors" "log/slog" - "os" - "strings" "github.com/goinfinite/os/src/domain/entity" "github.com/goinfinite/os/src/domain/valueObject" - infraHelper "github.com/goinfinite/os/src/infra/helper" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" dbModel "github.com/goinfinite/os/src/infra/internalDatabase/model" ) @@ -82,129 +79,3 @@ func (repo *AccountQueryRepo) ReadByUsername( return accountModel.ToEntity() } - -func (repo *AccountQueryRepo) secureAccessKeyFactory( - rawSecureAccessKeyContent string, - rawKeyId int, - secureAccessKeySecret string, -) (secureAccessKey entity.SecureAccessKey, err error) { - keyId, err := valueObject.NewSecureAccessKeyId(rawKeyId) - if err != nil { - return secureAccessKey, err - } - - keyContent, err := valueObject.NewSecureAccessKeyContent( - rawSecureAccessKeyContent, - ) - if err != nil { - return secureAccessKey, err - } - - keyName, err := keyContent.ReadOnlyKeyName() - if err != nil { - return secureAccessKey, err - } - - rawKeyHashContent, err := infraHelper.EncryptStr( - secureAccessKeySecret, keyContent.ReadWithoutKeyName(), - ) - if err != nil { - return secureAccessKey, err - } - keyHashContent, err := valueObject.NewEncodedContent(rawKeyHashContent) - if err != nil { - return secureAccessKey, err - } - - return entity.NewSecureAccessKey(keyId, keyName, keyContent, keyHashContent), nil -} - -func (repo *AccountQueryRepo) ReadSecureAccessKeys( - accountId valueObject.AccountId, -) ([]entity.SecureAccessKey, error) { - secureAccessKeys := []entity.SecureAccessKey{} - - account, err := repo.ReadById(accountId) - if err != nil { - return secureAccessKeys, errors.New("AccountNotFound") - } - - accountCmdRepo := NewAccountCmdRepo(repo.persistentDbSvc) - err = accountCmdRepo.ensureSecureAccessKeysDirAndFileExistence(account.Username) - if err != nil { - return secureAccessKeys, err - } - - secureAccessKeysFilePath := "/home/" + account.Username.String() + "/.ssh" + - "/authorized_keys" - secureAccessKeysFileContent, err := infraHelper.GetFileContent( - secureAccessKeysFilePath, - ) - if err != nil { - return secureAccessKeys, errors.New( - "ReadSecureAccessKeysFileContentError: " + err.Error(), - ) - } - - secretKey := os.Getenv("ACCOUNT_SECURE_ACCESS_KEY_SECRET") - - secureAccessKeysFileContentParts := strings.Split(secureAccessKeysFileContent, "\n") - for index, rawSecureAccessKeyContent := range secureAccessKeysFileContentParts { - if rawSecureAccessKeyContent == "" { - continue - } - - rawKeyId := index + 1 - secureAccessKey, err := repo.secureAccessKeyFactory( - rawSecureAccessKeyContent, rawKeyId, secretKey, - ) - if err != nil { - slog.Debug(err.Error(), slog.Int("index", index)) - continue - } - - secureAccessKeys = append(secureAccessKeys, secureAccessKey) - } - - return secureAccessKeys, nil -} - -func (repo *AccountQueryRepo) ReadSecureAccessKeyById( - accountId valueObject.AccountId, - secureAccessKeyId valueObject.SecureAccessKeyId, -) (secureAccessKey entity.SecureAccessKey, err error) { - secureAccessKeys, err := repo.ReadSecureAccessKeys(accountId) - if err != nil { - return secureAccessKey, err - } - - for _, key := range secureAccessKeys { - if key.Id.Uint16() != secureAccessKeyId.Uint16() { - continue - } - - return key, nil - } - - return secureAccessKey, errors.New("SecureAccessKeyNotFound") -} - -func (repo *AccountQueryRepo) ReadSecureAccessKeyByName( - accountId valueObject.AccountId, - secureAccessKeyName valueObject.SecureAccessKeyName, -) (secureAccessKey entity.SecureAccessKey, err error) { - secureAccessKeys, err := repo.ReadSecureAccessKeys(accountId) - if err != nil { - return secureAccessKey, err - } - - for _, key := range secureAccessKeys { - if key.Name.String() != secureAccessKeyName.String() { - continue - } - - return key, nil - } - - return secureAccessKey, errors.New("SecureAccessKeyNotFound") -} diff --git a/src/infra/account/accountQueryRepo_test.go b/src/infra/account/accountQueryRepo_test.go index b00f4e782..45b18b4bd 100644 --- a/src/infra/account/accountQueryRepo_test.go +++ b/src/infra/account/accountQueryRepo_test.go @@ -5,7 +5,6 @@ import ( "testing" testHelpers "github.com/goinfinite/os/src/devUtils" - "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/valueObject" ) @@ -53,44 +52,4 @@ func TestAccountQueryRepo(t *testing.T) { t.Errorf("Expecting error for %s, but got nil", username.String()) } }) - - t.Skip("SkipSecureAccessKeysTests") - - accountCmdRepo := NewAccountCmdRepo(persistentDbSvc) - - keyName, _ := valueObject.NewSecureAccessKeyName("dummySecureAccessKey") - keyContent, _ := valueObject.NewSecureAccessKeyContent( - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+GDqLA2sGauzU5hUxBbBmm6FfeZpUbiX6IlQO9KqeqAsum+Efhvj+qpatM5PzMMwtlcFwDS5Y4RcX9uxE8IGsYiALRfnLAX5p73zrcrXamMJSx25rXAu/VJdmekxHbDgsBPyk6/4dfu+3uW7ka7HHhPytPIqW2qBuPkalJinc7qKEuXdkCyX8+8a+0uN8XodLipLJwU8A1VPvI9thYxITyHWZnXRnin0r/unHgLrg9bBILXZf0JRslelYdCvuCGnRKZfokh153shMZ63S+iV/Tohg2bOVxyz3HIQ983ga24uTFQhLpITMe9JEfq3pp2wcCE5hNFlNKyeDG8kwB+8V", - ) - createDto := dto.NewCreateSecureAccessKey( - keyName, keyContent, accountId, accountId, valueObject.NewLocalhostIpAddress(), - ) - - _, err := accountCmdRepo.CreateSecureAccessKey(createDto) - if err != nil { - t.Fatalf("Fail to create dummy SecureAccessKey to test") - } - - t.Run("ReadSecureAccessKeys", func(t *testing.T) { - keys, err := accountQueryRepo.ReadSecureAccessKeys(accountId) - if err != nil { - t.Fatalf( - "Expecting no error for %d, but got %s", accountId.Uint64(), err.Error(), - ) - } - - if len(keys) == 0 { - t.Error("Expecting a keys list, but got an empty one") - } - }) - - t.Run("ReadSecureAccessKeyByName", func(t *testing.T) { - _, err := accountQueryRepo.ReadSecureAccessKeyByName(accountId, keyName) - if err != nil { - t.Fatalf( - "Expecting no error for %s (%d), but got %s", keyName.String(), - accountId.Uint64(), err.Error(), - ) - } - }) } diff --git a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go new file mode 100644 index 000000000..afa36e547 --- /dev/null +++ b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go @@ -0,0 +1,190 @@ +package secureAccessKeyInfra + +import ( + "errors" + "log/slog" + "os" + + "github.com/goinfinite/os/src/domain/dto" + "github.com/goinfinite/os/src/domain/valueObject" + accountInfra "github.com/goinfinite/os/src/infra/account" + infraHelper "github.com/goinfinite/os/src/infra/helper" + internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" +) + +type SecureAccessKeyCmdRepo struct { + persistentDbSvc *internalDbInfra.PersistentDatabaseService + accountQueryRepo *accountInfra.AccountQueryRepo +} + +func NewSecureAccessKeyCmdRepo( + persistentDbSvc *internalDbInfra.PersistentDatabaseService, +) *SecureAccessKeyCmdRepo { + return &SecureAccessKeyCmdRepo{ + persistentDbSvc: persistentDbSvc, + accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), + } +} + +func (repo *SecureAccessKeyCmdRepo) ensureSecureAccessKeysDirAndFileExistence( + accountUsername valueObject.Username, +) error { + accountUsernameStr := accountUsername.String() + + secureAccessKeysDirPath := "/home/" + accountUsernameStr + "/.ssh" + if !infraHelper.FileExists(secureAccessKeysDirPath) { + err := infraHelper.MakeDir(secureAccessKeysDirPath) + if err != nil { + return errors.New("CreateSecureAccessKeysDirectoryError: " + err.Error()) + } + } + + secureAccessKeysFilePath := secureAccessKeysDirPath + "/authorized_keys" + if infraHelper.FileExists(secureAccessKeysFilePath) { + return nil + } + + _, err := os.Create(secureAccessKeysFilePath) + if err != nil { + return errors.New("CreateSecureAccessKeysFileError: " + err.Error()) + } + + _, err = infraHelper.RunCmd( + "chown", "-R", accountUsernameStr, secureAccessKeysFilePath, + ) + if err != nil { + return errors.New("ChownSecureAccessKeysFileError: " + err.Error()) + } + + return nil +} + +func (repo *SecureAccessKeyCmdRepo) isSecureAccessKeyValid( + keyContent valueObject.SecureAccessKeyContent, +) bool { + keyName, err := keyContent.ReadOnlyKeyName() + if err != nil { + slog.Error(err.Error()) + return false + } + keyNameStr := keyName.String() + + keyTempFilePath := "/tmp/" + keyNameStr + "_secureAccessKey" + shouldOverwrite := true + err = infraHelper.UpdateFile( + keyTempFilePath, keyContent.String(), shouldOverwrite, + ) + if err != nil { + slog.Error( + "CreateSecureAccessKeyTempFileError", slog.String("keyName", keyNameStr), + slog.Any("err", err), + ) + return false + } + + _, err = infraHelper.RunCmdWithSubShell("ssh-keygen -l -f " + keyTempFilePath) + if err != nil { + slog.Error( + "ValidateSecureAccessKeyError", slog.String("keyName", keyNameStr), + slog.Any("err", err), + ) + return false + } + + err = os.Remove(keyTempFilePath) + if err != nil { + slog.Error( + "DeleteSecureAccessKeyTempFileError", slog.String("keyName", keyNameStr), + slog.Any("err", err), + ) + } + + return true +} + +func (repo *SecureAccessKeyCmdRepo) allowAccountSecureRemoteConnection( + accountId valueObject.AccountId, +) error { + accountUsername, err := infraHelper.RunCmdWithSubShell( + "awk -F: '$3 == " + accountId.String() + + " && $7 != \"/bin/bash\" {print $1}' /etc/passwd", + ) + if err != nil { + return errors.New("ReadUnixUsernameFromFileError: " + err.Error()) + } + + _, err = infraHelper.RunCmdWithSubShell( + "chsh -s /bin/bash " + accountUsername, + ) + if err != nil { + return errors.New("ChangeDefaultBashError: " + err.Error()) + } + + return nil +} + +func (repo *SecureAccessKeyCmdRepo) Create( + createDto dto.CreateSecureAccessKey, +) (keyId valueObject.SecureAccessKeyId, err error) { + account, err := repo.accountQueryRepo.ReadById(createDto.AccountId) + if err != nil { + return keyId, errors.New("AccountNotFound") + } + + err = repo.ensureSecureAccessKeysDirAndFileExistence(account.Username) + if err != nil { + return keyId, err + } + + keyContentStr := createDto.Content.ReadWithoutKeyName() + " " + + createDto.Name.String() + keyContent, err := valueObject.NewSecureAccessKeyContent(keyContentStr) + if err != nil { + return keyId, errors.New("InvalidSecureAccessKey") + } + + if !repo.isSecureAccessKeyValid(keyContent) { + return keyId, errors.New("InvalidSecureAccessKey") + } + + _, err = infraHelper.RunCmdWithSubShell( + "echo \"" + keyContentStr + "\" >> /home/" + account.Username.String() + + "/.ssh/authorized_keys", + ) + if err != nil { + return keyId, errors.New("FailToAddNewSecureAccessKeyToFile: " + err.Error()) + } + + err = repo.allowAccountSecureRemoteConnection(createDto.AccountId) + if err != nil { + return keyId, err + } + + return keyId, nil +} + +func (repo *SecureAccessKeyCmdRepo) Delete( + deleteDto dto.DeleteSecureAccessKey, +) error { + _, err := repo.accountQueryRepo.ReadById(deleteDto.AccountId) + if err != nil { + return errors.New("AccountNotFound") + } + + /*keyToDelete, err := repo.secureAccessKeyQueryRepo.ReadById( + deleteDto.AccountId, deleteDto.Id, + ) + if err != nil { + return err + } + + _, err = infraHelper.RunCmdWithSubShell( + "sed -i '\\|" + keyToDelete.Content.String() + "|d' " + + "/home/" + account.Username.String() + "/.ssh/authorized_keys", + ) + if err != nil { + return errors.New("FailToDeleteSecureAccessKeyFromFile: " + err.Error()) + }*/ + + return nil +} diff --git a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo_test.go b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo_test.go new file mode 100644 index 000000000..a5d8baa43 --- /dev/null +++ b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo_test.go @@ -0,0 +1,68 @@ +package secureAccessKeyInfra + +import ( + "testing" + + testHelpers "github.com/goinfinite/os/src/devUtils" + "github.com/goinfinite/os/src/domain/dto" + "github.com/goinfinite/os/src/domain/valueObject" + accountInfra "github.com/goinfinite/os/src/infra/account" +) + +func TestSecureAccessKeyCmdRepo(t *testing.T) { + testHelpers.LoadEnvVars() + + accountId, _ := valueObject.NewAccountId(2000) + accountUsername, _ := valueObject.NewUsername("accountToTestSecureAccessKey") + accountPassword, _ := valueObject.NewPassword("q1w2e3r4") + ipAddress := valueObject.NewLocalhostIpAddress() + + createDto := dto.NewCreateAccount( + accountUsername, accountPassword, accountId, ipAddress, + ) + + accountCmdRepo := accountInfra.NewAccountCmdRepo(testHelpers.GetPersistentDbSvc()) + _, err := accountCmdRepo.Create(createDto) + if err != nil { + t.Fatalf("FailToCreateTestAccount") + } + + t.Skip("SkipSecureAccessKeysTests") + + var keyId valueObject.SecureAccessKeyId + + keyName, _ := valueObject.NewSecureAccessKeyName("testSecureAccessKey") + secureAccessKeyCmdRepo := NewSecureAccessKeyCmdRepo(testHelpers.GetPersistentDbSvc()) + + t.Run("CreateSecureAccessKey", func(t *testing.T) { + keyContent, _ := valueObject.NewSecureAccessKeyContent( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+GDqLA2sGauzU5hUxBbBmm6FfeZpUbiX6IlQO9KqeqAsum+Efhvj+qpatM5PzMMwtlcFwDS5Y4RcX9uxE8IGsYiALRfnLAX5p73zrcrXamMJSx25rXAu/VJdmekxHbDgsBPyk6/4dfu+3uW7ka7HHhPytPIqW2qBuPkalJinc7qKEuXdkCyX8+8a+0uN8XodLipLJwU8A1VPvI9thYxITyHWZnXRnin0r/unHgLrg9bBILXZf0JRslelYdCvuCGnRKZfokh153shMZ63S+iV/Tohg2bOVxyz3HIQ983ga24uTFQhLpITMe9JEfq3pp2wcCE5hNFlNKyeDG8kwB+8V", + ) + createDto := dto.NewCreateSecureAccessKey( + keyName, keyContent, accountId, accountId, ipAddress, + ) + + keyId, err = secureAccessKeyCmdRepo.Create(createDto) + if err != nil { + t.Fatalf( + "Expected no error for %s, but got %s", keyName.String(), err.Error(), + ) + } + }) + + t.Run("DeleteSecureAccessKey", func(t *testing.T) { + deleteDto := dto.NewDeleteSecureAccessKey(keyId, accountId, accountId, ipAddress) + + err = secureAccessKeyCmdRepo.Delete(deleteDto) + if err != nil { + t.Fatalf( + "Expected no error for %s, but got %s", keyName.String(), err.Error(), + ) + } + }) + + err = accountCmdRepo.Delete(accountId) + if err != nil { + t.Fatalf("FailToDeleteTestAccount") + } +} diff --git a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go new file mode 100644 index 000000000..c546a151c --- /dev/null +++ b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go @@ -0,0 +1,147 @@ +package secureAccessKeyInfra + +import ( + "errors" + "log/slog" + "strings" + + "github.com/goinfinite/os/src/domain/entity" + "github.com/goinfinite/os/src/domain/valueObject" + accountInfra "github.com/goinfinite/os/src/infra/account" + infraHelper "github.com/goinfinite/os/src/infra/helper" + internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" +) + +type SecureAccessKeyQueryRepo struct { + persistentDbSvc *internalDbInfra.PersistentDatabaseService + secureAccessKeyCmdRepo *SecureAccessKeyCmdRepo + accountQueryRepo *accountInfra.AccountQueryRepo +} + +func NewSecureAccessKeyQueryRepo( + persistentDbSvc *internalDbInfra.PersistentDatabaseService, +) *SecureAccessKeyQueryRepo { + return &SecureAccessKeyQueryRepo{ + persistentDbSvc: persistentDbSvc, + secureAccessKeyCmdRepo: NewSecureAccessKeyCmdRepo(persistentDbSvc), + accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), + } +} + +func (repo *SecureAccessKeyQueryRepo) secureAccessKeyFactory( + rawKeyId int, + rawSecureAccessKeyContent string, + accountId valueObject.AccountId, +) (secureAccessKey entity.SecureAccessKey, err error) { + keyId, err := valueObject.NewSecureAccessKeyId(rawKeyId) + if err != nil { + return secureAccessKey, err + } + + keyContent, err := valueObject.NewSecureAccessKeyContent( + rawSecureAccessKeyContent, + ) + if err != nil { + return secureAccessKey, err + } + + keyName, err := keyContent.ReadOnlyKeyName() + if err != nil { + return secureAccessKey, err + } + + now := valueObject.NewUnixTimeNow() + + return entity.NewSecureAccessKey( + keyId, accountId, keyName, keyContent, now, now, + ), nil +} + +func (repo *SecureAccessKeyQueryRepo) Read( + accountId valueObject.AccountId, +) ([]entity.SecureAccessKey, error) { + secureAccessKeys := []entity.SecureAccessKey{} + + account, err := repo.accountQueryRepo.ReadById(accountId) + if err != nil { + return secureAccessKeys, errors.New("AccountNotFound") + } + + err = repo.secureAccessKeyCmdRepo.ensureSecureAccessKeysDirAndFileExistence( + account.Username, + ) + if err != nil { + return secureAccessKeys, err + } + + secureAccessKeysFilePath := "/home/" + account.Username.String() + "/.ssh" + + "/authorized_keys" + secureAccessKeysFileContent, err := infraHelper.GetFileContent( + secureAccessKeysFilePath, + ) + if err != nil { + return secureAccessKeys, errors.New( + "ReadSecureAccessKeysFileContentError: " + err.Error(), + ) + } + + secureAccessKeysFileContentParts := strings.Split(secureAccessKeysFileContent, "\n") + for index, rawSecureAccessKeyContent := range secureAccessKeysFileContentParts { + if rawSecureAccessKeyContent == "" { + continue + } + + rawKeyId := index + 1 + secureAccessKey, err := repo.secureAccessKeyFactory( + rawKeyId, rawSecureAccessKeyContent, accountId, + ) + if err != nil { + slog.Debug(err.Error(), slog.Int("index", index)) + continue + } + + secureAccessKeys = append(secureAccessKeys, secureAccessKey) + } + + return secureAccessKeys, nil +} + +func (repo *SecureAccessKeyQueryRepo) ReadById( + accountId valueObject.AccountId, + secureAccessKeyId valueObject.SecureAccessKeyId, +) (secureAccessKey entity.SecureAccessKey, err error) { + secureAccessKeys, err := repo.Read(accountId) + if err != nil { + return secureAccessKey, err + } + + for _, key := range secureAccessKeys { + if key.Id.Uint16() != secureAccessKeyId.Uint16() { + continue + } + + return key, nil + } + + return secureAccessKey, errors.New("SecureAccessKeyNotFound") +} + +func (repo *SecureAccessKeyQueryRepo) ReadByName( + accountId valueObject.AccountId, + secureAccessKeyName valueObject.SecureAccessKeyName, +) (secureAccessKey entity.SecureAccessKey, err error) { + secureAccessKeys, err := repo.Read(accountId) + if err != nil { + return secureAccessKey, err + } + + for _, key := range secureAccessKeys { + if key.Name.String() != secureAccessKeyName.String() { + continue + } + + return key, nil + } + + return secureAccessKey, errors.New("SecureAccessKeyNotFound") +} diff --git a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo_test.go b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo_test.go new file mode 100644 index 000000000..959ea3308 --- /dev/null +++ b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo_test.go @@ -0,0 +1,75 @@ +package secureAccessKeyInfra + +import ( + "testing" + + testHelpers "github.com/goinfinite/os/src/devUtils" + "github.com/goinfinite/os/src/domain/dto" + "github.com/goinfinite/os/src/domain/valueObject" + accountInfra "github.com/goinfinite/os/src/infra/account" +) + +func TestSecureAccessKeyQueryRepo(t *testing.T) { + testHelpers.LoadEnvVars() + + accountId, _ := valueObject.NewAccountId(2000) + accountUsername, _ := valueObject.NewUsername("accountToTestSecureAccessKey") + accountPassword, _ := valueObject.NewPassword("q1w2e3r4") + ipAddress := valueObject.NewLocalhostIpAddress() + + createAccountDto := dto.NewCreateAccount( + accountUsername, accountPassword, accountId, ipAddress, + ) + + accountCmdRepo := accountInfra.NewAccountCmdRepo(testHelpers.GetPersistentDbSvc()) + _, err := accountCmdRepo.Create(createAccountDto) + if err != nil { + t.Fatalf("FailToCreateTestAccount") + } + + t.Skip("SkipSecureAccessKeysTests") + + keyName, _ := valueObject.NewSecureAccessKeyName("testSecureAccessKey") + keyContent, _ := valueObject.NewSecureAccessKeyContent( + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+GDqLA2sGauzU5hUxBbBmm6FfeZpUbiX6IlQO9KqeqAsum+Efhvj+qpatM5PzMMwtlcFwDS5Y4RcX9uxE8IGsYiALRfnLAX5p73zrcrXamMJSx25rXAu/VJdmekxHbDgsBPyk6/4dfu+3uW7ka7HHhPytPIqW2qBuPkalJinc7qKEuXdkCyX8+8a+0uN8XodLipLJwU8A1VPvI9thYxITyHWZnXRnin0r/unHgLrg9bBILXZf0JRslelYdCvuCGnRKZfokh153shMZ63S+iV/Tohg2bOVxyz3HIQ983ga24uTFQhLpITMe9JEfq3pp2wcCE5hNFlNKyeDG8kwB+8V", + ) + createDto := dto.NewCreateSecureAccessKey( + keyName, keyContent, accountId, accountId, ipAddress, + ) + + secureAccessKeyCmdRepo := NewSecureAccessKeyCmdRepo(testHelpers.GetPersistentDbSvc()) + _, err = secureAccessKeyCmdRepo.Create(createDto) + if err != nil { + t.Fatalf("Fail to create dummy SecureAccessKey to test") + } + + secureAccessKeyQueryRepo := NewSecureAccessKeyQueryRepo(testHelpers.GetPersistentDbSvc()) + + t.Run("ReadSecureAccessKeys", func(t *testing.T) { + keys, err := secureAccessKeyQueryRepo.Read(accountId) + if err != nil { + t.Fatalf( + "Expecting no error for %d, but got %s", accountId.Uint64(), err.Error(), + ) + } + + if len(keys) == 0 { + t.Error("Expecting a keys list, but got an empty one") + } + }) + + t.Run("ReadSecureAccessKeyByName", func(t *testing.T) { + _, err := secureAccessKeyQueryRepo.ReadByName(accountId, keyName) + if err != nil { + t.Fatalf( + "Expecting no error for %s (%d), but got %s", keyName.String(), + accountId.Uint64(), err.Error(), + ) + } + }) + + err = accountCmdRepo.Delete(accountId) + if err != nil { + t.Fatalf("FailToDeleteTestAccount") + } +} diff --git a/src/infra/internalDatabase/model/account.go b/src/infra/internalDatabase/model/account.go index 7f030003f..f65041444 100644 --- a/src/infra/internalDatabase/model/account.go +++ b/src/infra/internalDatabase/model/account.go @@ -8,12 +8,13 @@ import ( ) type Account struct { - ID uint64 `gorm:"primarykey"` - GroupId uint64 `gorm:"not null"` - Username string `gorm:"not null"` - KeyHash *string - CreatedAt time.Time - UpdatedAt time.Time + ID uint64 `gorm:"primarykey"` + GroupId uint64 `gorm:"not null"` + Username string `gorm:"not null"` + KeyHash *string + SecureAccessKeys []SecureAccessKey + CreatedAt time.Time + UpdatedAt time.Time } func (Account) TableName() string { diff --git a/src/infra/internalDatabase/model/secureAccessKey.go b/src/infra/internalDatabase/model/secureAccessKey.go new file mode 100644 index 000000000..4a2cbc529 --- /dev/null +++ b/src/infra/internalDatabase/model/secureAccessKey.go @@ -0,0 +1,62 @@ +package dbModel + +import ( + "time" + + "github.com/goinfinite/os/src/domain/entity" + "github.com/goinfinite/os/src/domain/valueObject" +) + +type SecureAccessKey struct { + ID uint16 `gorm:"primarykey"` + AccountId uint64 `gorm:"not null"` + Name string `gorm:"primarykey"` + Content string `gorm:"primarykey"` + CreatedAt time.Time + UpdatedAt time.Time +} + +func (SecureAccessKey) TableName() string { + return "secure_access_key" +} + +func (SecureAccessKey) ToModel( + secureAccessKeyEntity entity.SecureAccessKey, +) (model SecureAccessKey, err error) { + return SecureAccessKey{ + ID: secureAccessKeyEntity.Id.Uint16(), + AccountId: secureAccessKeyEntity.AccountId.Uint64(), + Name: secureAccessKeyEntity.Name.String(), + Content: secureAccessKeyEntity.Content.ReadWithoutKeyName(), + }, nil +} + +func (model SecureAccessKey) ToEntity() ( + secureAccessKeyEntity entity.SecureAccessKey, err error, +) { + id, err := valueObject.NewSecureAccessKeyId(model.ID) + if err != nil { + return secureAccessKeyEntity, err + } + + accountId, err := valueObject.NewAccountId(model.AccountId) + if err != nil { + return secureAccessKeyEntity, err + } + + name, err := valueObject.NewSecureAccessKeyName(model.Name) + if err != nil { + return secureAccessKeyEntity, err + } + + content, err := valueObject.NewSecureAccessKeyContent(model.Content) + if err != nil { + return secureAccessKeyEntity, err + } + + return entity.NewSecureAccessKey( + id, accountId, name, content, + valueObject.NewUnixTimeWithGoTime(model.CreatedAt), + valueObject.NewUnixTimeWithGoTime(model.UpdatedAt), + ), nil +} diff --git a/src/infra/internalDatabase/persistentDatabaseService.go b/src/infra/internalDatabase/persistentDatabaseService.go index d3fb3370f..b8bfadcff 100644 --- a/src/infra/internalDatabase/persistentDatabaseService.go +++ b/src/infra/internalDatabase/persistentDatabaseService.go @@ -101,6 +101,7 @@ func (dbSvc *PersistentDatabaseService) dbMigrate() error { &dbModel.MarketplaceInstalledItem{}, &dbModel.ScheduledTask{}, &dbModel.ScheduledTaskTag{}, + &dbModel.SecureAccessKey{}, &dbModel.VirtualHost{}, ) if err != nil { diff --git a/src/presentation/service/account.go b/src/presentation/service/account.go index 8e60d05a5..2a00a4a23 100644 --- a/src/presentation/service/account.go +++ b/src/presentation/service/account.go @@ -6,6 +6,7 @@ import ( "github.com/goinfinite/os/src/domain/valueObject" voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" accountInfra "github.com/goinfinite/os/src/infra/account" + secureAccessKeyInfra "github.com/goinfinite/os/src/infra/account/secureAccessKey" activityRecordInfra "github.com/goinfinite/os/src/infra/activityRecord" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" serviceHelper "github.com/goinfinite/os/src/presentation/service/helper" @@ -16,11 +17,13 @@ var LocalOperatorAccountId, _ = valueObject.NewAccountId(0) var LocalOperatorIpAddress = valueObject.NewLocalhostIpAddress() type AccountService struct { - persistentDbSvc *internalDbInfra.PersistentDatabaseService - accountQueryRepo *accountInfra.AccountQueryRepo - accountCmdRepo *accountInfra.AccountCmdRepo - activityRecordCmdRepo *activityRecordInfra.ActivityRecordCmdRepo - availabilityInspector *sharedHelper.ServiceAvailabilityInspector + persistentDbSvc *internalDbInfra.PersistentDatabaseService + accountQueryRepo *accountInfra.AccountQueryRepo + accountCmdRepo *accountInfra.AccountCmdRepo + secureAccessKeyQueryRepo *secureAccessKeyInfra.SecureAccessKeyQueryRepo + secureAccessKeyCmdRepo *secureAccessKeyInfra.SecureAccessKeyCmdRepo + activityRecordCmdRepo *activityRecordInfra.ActivityRecordCmdRepo + availabilityInspector *sharedHelper.ServiceAvailabilityInspector } func NewAccountService( @@ -28,9 +31,15 @@ func NewAccountService( trailDbSvc *internalDbInfra.TrailDatabaseService, ) *AccountService { return &AccountService{ - persistentDbSvc: persistentDbSvc, - accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), - accountCmdRepo: accountInfra.NewAccountCmdRepo(persistentDbSvc), + persistentDbSvc: persistentDbSvc, + accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), + accountCmdRepo: accountInfra.NewAccountCmdRepo(persistentDbSvc), + secureAccessKeyQueryRepo: secureAccessKeyInfra.NewSecureAccessKeyQueryRepo( + persistentDbSvc, + ), + secureAccessKeyCmdRepo: secureAccessKeyInfra.NewSecureAccessKeyCmdRepo( + persistentDbSvc, + ), activityRecordCmdRepo: activityRecordInfra.NewActivityRecordCmdRepo(trailDbSvc), availabilityInspector: sharedHelper.NewServiceAvailabilityInspector( persistentDbSvc, @@ -240,8 +249,8 @@ func (service *AccountService) ReadSecureAccessKey( return NewServiceOutput(UserError, err.Error()) } - secureAccessKeys, err := useCase.ReadAccountSecureAccessKeys( - service.accountQueryRepo, accountId, + secureAccessKeys, err := useCase.ReadSecureAccessKeys( + service.accountQueryRepo, service.secureAccessKeyQueryRepo, accountId, ) if err != nil { return NewServiceOutput(InfraError, err.Error()) @@ -303,7 +312,7 @@ func (service *AccountService) CreateSecureAccessKey( ) err = useCase.CreateSecureAccessKey( - service.accountQueryRepo, service.accountCmdRepo, + service.accountQueryRepo, service.secureAccessKeyCmdRepo, service.activityRecordCmdRepo, createDto, ) if err != nil { @@ -358,7 +367,7 @@ func (service *AccountService) DeleteSecureAccessKey( ) err = useCase.DeleteSecureAccessKey( - service.accountQueryRepo, service.accountCmdRepo, + service.accountQueryRepo, service.secureAccessKeyCmdRepo, service.activityRecordCmdRepo, deleteDto, ) if err != nil { From aa333c53468876c54c0e31a9a96cf79e150df608 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 16:38:28 -0300 Subject: [PATCH 044/100] feat: implements new secure access key creation using database and file recreation like mapping creation --- .../secureAccessKey/secureAccessKeyCmdRepo.go | 76 ++++++++++++++++--- .../secureAccessKeyQueryRepo.go | 17 +---- .../internalDatabase/model/secureAccessKey.go | 25 +++--- 3 files changed, 85 insertions(+), 33 deletions(-) diff --git a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go index afa36e547..7486d9773 100644 --- a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go +++ b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go @@ -10,23 +10,26 @@ import ( accountInfra "github.com/goinfinite/os/src/infra/account" infraHelper "github.com/goinfinite/os/src/infra/helper" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" + dbModel "github.com/goinfinite/os/src/infra/internalDatabase/model" ) type SecureAccessKeyCmdRepo struct { - persistentDbSvc *internalDbInfra.PersistentDatabaseService - accountQueryRepo *accountInfra.AccountQueryRepo + persistentDbSvc *internalDbInfra.PersistentDatabaseService + secureAccessKeyQueryRepo *SecureAccessKeyQueryRepo + accountQueryRepo *accountInfra.AccountQueryRepo } func NewSecureAccessKeyCmdRepo( persistentDbSvc *internalDbInfra.PersistentDatabaseService, ) *SecureAccessKeyCmdRepo { return &SecureAccessKeyCmdRepo{ - persistentDbSvc: persistentDbSvc, - accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), + persistentDbSvc: persistentDbSvc, + secureAccessKeyQueryRepo: NewSecureAccessKeyQueryRepo(persistentDbSvc), + accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), } } -func (repo *SecureAccessKeyCmdRepo) ensureSecureAccessKeysDirAndFileExistence( +func (repo *SecureAccessKeyCmdRepo) createSecureAccessKeysFileIfNotExists( accountUsername valueObject.Username, ) error { accountUsernameStr := accountUsername.String() @@ -123,6 +126,42 @@ func (repo *SecureAccessKeyCmdRepo) allowAccountSecureRemoteConnection( return nil } +func (repo *SecureAccessKeyCmdRepo) recreateSecureAccessKeysFile( + accountId valueObject.AccountId, + accountUsername valueObject.Username, +) error { + keys, err := repo.secureAccessKeyQueryRepo.Read(accountId) + if err != nil { + return err + } + + keysFilePath := "/home/" + accountUsername.String() + "/.ssh/authorized_keys" + if infraHelper.FileExists(keysFilePath) { + err = os.Remove(keysFilePath) + if err != nil { + return errors.New("DeleteSecureAccessKeysFileError: " + err.Error()) + } + } + + err = repo.createSecureAccessKeysFileIfNotExists(accountUsername) + if err != nil { + return errors.New("CreateSecureAccessKeysFileError: " + err.Error()) + } + + keysFileContent := "" + for _, key := range keys { + keysFileContent += key.Content.String() + "\n" + } + + shouldOverwrite := true + err = infraHelper.UpdateFile(keysFilePath, keysFileContent, shouldOverwrite) + if err != nil { + return errors.New("UpdateSecureAccessKeysFileContentError: " + err.Error()) + } + + return nil +} + func (repo *SecureAccessKeyCmdRepo) Create( createDto dto.CreateSecureAccessKey, ) (keyId valueObject.SecureAccessKeyId, err error) { @@ -131,7 +170,7 @@ func (repo *SecureAccessKeyCmdRepo) Create( return keyId, errors.New("AccountNotFound") } - err = repo.ensureSecureAccessKeysDirAndFileExistence(account.Username) + err = repo.createSecureAccessKeysFileIfNotExists(account.Username) if err != nil { return keyId, err } @@ -155,23 +194,38 @@ func (repo *SecureAccessKeyCmdRepo) Create( return keyId, errors.New("FailToAddNewSecureAccessKeyToFile: " + err.Error()) } - err = repo.allowAccountSecureRemoteConnection(createDto.AccountId) + err = repo.allowAccountSecureRemoteConnection(account.Id) + if err != nil { + return keyId, err + } + + secureAccessKeyModel := dbModel.NewSecureAccessKey( + 0, account.Id.Uint64(), createDto.Name.String(), + createDto.Content.ReadWithoutKeyName(), + ) + + createResult := repo.persistentDbSvc.Handler.Create(&secureAccessKeyModel) + if createResult.Error != nil { + return keyId, createResult.Error + } + + keyId, err = valueObject.NewSecureAccessKeyId(secureAccessKeyModel.ID) if err != nil { return keyId, err } - return keyId, nil + return keyId, repo.recreateSecureAccessKeysFile(account.Id, account.Username) } func (repo *SecureAccessKeyCmdRepo) Delete( deleteDto dto.DeleteSecureAccessKey, ) error { - _, err := repo.accountQueryRepo.ReadById(deleteDto.AccountId) + account, err := repo.accountQueryRepo.ReadById(deleteDto.AccountId) if err != nil { return errors.New("AccountNotFound") } - /*keyToDelete, err := repo.secureAccessKeyQueryRepo.ReadById( + keyToDelete, err := repo.secureAccessKeyQueryRepo.ReadById( deleteDto.AccountId, deleteDto.Id, ) if err != nil { @@ -184,7 +238,7 @@ func (repo *SecureAccessKeyCmdRepo) Delete( ) if err != nil { return errors.New("FailToDeleteSecureAccessKeyFromFile: " + err.Error()) - }*/ + } return nil } diff --git a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go index c546a151c..25982f0af 100644 --- a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go +++ b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go @@ -13,18 +13,16 @@ import ( ) type SecureAccessKeyQueryRepo struct { - persistentDbSvc *internalDbInfra.PersistentDatabaseService - secureAccessKeyCmdRepo *SecureAccessKeyCmdRepo - accountQueryRepo *accountInfra.AccountQueryRepo + persistentDbSvc *internalDbInfra.PersistentDatabaseService + accountQueryRepo *accountInfra.AccountQueryRepo } func NewSecureAccessKeyQueryRepo( persistentDbSvc *internalDbInfra.PersistentDatabaseService, ) *SecureAccessKeyQueryRepo { return &SecureAccessKeyQueryRepo{ - persistentDbSvc: persistentDbSvc, - secureAccessKeyCmdRepo: NewSecureAccessKeyCmdRepo(persistentDbSvc), - accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), + persistentDbSvc: persistentDbSvc, + accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), } } @@ -67,13 +65,6 @@ func (repo *SecureAccessKeyQueryRepo) Read( return secureAccessKeys, errors.New("AccountNotFound") } - err = repo.secureAccessKeyCmdRepo.ensureSecureAccessKeysDirAndFileExistence( - account.Username, - ) - if err != nil { - return secureAccessKeys, err - } - secureAccessKeysFilePath := "/home/" + account.Username.String() + "/.ssh" + "/authorized_keys" secureAccessKeysFileContent, err := infraHelper.GetFileContent( diff --git a/src/infra/internalDatabase/model/secureAccessKey.go b/src/infra/internalDatabase/model/secureAccessKey.go index 4a2cbc529..a78d3088d 100644 --- a/src/infra/internalDatabase/model/secureAccessKey.go +++ b/src/infra/internalDatabase/model/secureAccessKey.go @@ -20,15 +20,22 @@ func (SecureAccessKey) TableName() string { return "secure_access_key" } -func (SecureAccessKey) ToModel( - secureAccessKeyEntity entity.SecureAccessKey, -) (model SecureAccessKey, err error) { - return SecureAccessKey{ - ID: secureAccessKeyEntity.Id.Uint16(), - AccountId: secureAccessKeyEntity.AccountId.Uint64(), - Name: secureAccessKeyEntity.Name.String(), - Content: secureAccessKeyEntity.Content.ReadWithoutKeyName(), - }, nil +func NewSecureAccessKey( + id uint16, + accountId uint64, + name, content string, +) SecureAccessKey { + model := SecureAccessKey{ + AccountId: accountId, + Name: name, + Content: content, + } + + if id != 0 { + model.ID = id + } + + return model } func (model SecureAccessKey) ToEntity() ( From fa8386ccacd14ad70e061abade612977c57aff0c Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 16:43:37 -0300 Subject: [PATCH 045/100] refact: remove account existence validation from secure access key use cases --- src/domain/useCase/createSecureAccessKey.go | 6 ------ src/domain/useCase/deleteSecureAccessKey.go | 8 +------- src/domain/useCase/readSecureAccessKeys.go | 6 ------ src/presentation/service/account.go | 8 +++----- 4 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/domain/useCase/createSecureAccessKey.go b/src/domain/useCase/createSecureAccessKey.go index d214a2d90..158dd0339 100644 --- a/src/domain/useCase/createSecureAccessKey.go +++ b/src/domain/useCase/createSecureAccessKey.go @@ -9,16 +9,10 @@ import ( ) func CreateSecureAccessKey( - accountQueryRepo repository.AccountQueryRepo, secureAccessKeyCmdRepo repository.SecureAccessKeyCmdRepo, activityRecordCmdRepo repository.ActivityRecordCmdRepo, createDto dto.CreateSecureAccessKey, ) error { - _, err := accountQueryRepo.ReadById(createDto.AccountId) - if err != nil { - return errors.New("AccountNotFound") - } - keyId, err := secureAccessKeyCmdRepo.Create(createDto) if err != nil { slog.Error("CreateSecureAccessKeyError", slog.Any("error", err)) diff --git a/src/domain/useCase/deleteSecureAccessKey.go b/src/domain/useCase/deleteSecureAccessKey.go index 802f9d28c..fa92a4e6d 100644 --- a/src/domain/useCase/deleteSecureAccessKey.go +++ b/src/domain/useCase/deleteSecureAccessKey.go @@ -9,17 +9,11 @@ import ( ) func DeleteSecureAccessKey( - accountQueryRepo repository.AccountQueryRepo, secureAccessKeyCmdRepo repository.SecureAccessKeyCmdRepo, activityRecordCmdRepo repository.ActivityRecordCmdRepo, deleteDto dto.DeleteSecureAccessKey, ) error { - _, err := accountQueryRepo.ReadById(deleteDto.AccountId) - if err != nil { - return errors.New("AccountNotFound") - } - - err = secureAccessKeyCmdRepo.Delete(deleteDto) + err := secureAccessKeyCmdRepo.Delete(deleteDto) if err != nil { slog.Error("DeleteSecureAccessKeyError", slog.Any("error", err)) return errors.New("DeleteSecureAccessKeyInfraError") diff --git a/src/domain/useCase/readSecureAccessKeys.go b/src/domain/useCase/readSecureAccessKeys.go index 06d044949..2d331450b 100644 --- a/src/domain/useCase/readSecureAccessKeys.go +++ b/src/domain/useCase/readSecureAccessKeys.go @@ -10,15 +10,9 @@ import ( ) func ReadSecureAccessKeys( - accountQueryRepo repository.AccountQueryRepo, secureAccessKeyQueryRepo repository.SecureAccessKeyQueryRepo, accountId valueObject.AccountId, ) (secureAccessKeys []entity.SecureAccessKey, err error) { - _, err = accountQueryRepo.ReadById(accountId) - if err != nil { - return secureAccessKeys, errors.New("AccountNotFound") - } - secureAccessKeys, err = secureAccessKeyQueryRepo.Read(accountId) if err != nil { slog.Error("ReadSecureAccessKeysInfraError", slog.Any("error", err)) diff --git a/src/presentation/service/account.go b/src/presentation/service/account.go index 2a00a4a23..e10a98f9c 100644 --- a/src/presentation/service/account.go +++ b/src/presentation/service/account.go @@ -250,7 +250,7 @@ func (service *AccountService) ReadSecureAccessKey( } secureAccessKeys, err := useCase.ReadSecureAccessKeys( - service.accountQueryRepo, service.secureAccessKeyQueryRepo, accountId, + service.secureAccessKeyQueryRepo, accountId, ) if err != nil { return NewServiceOutput(InfraError, err.Error()) @@ -312,8 +312,7 @@ func (service *AccountService) CreateSecureAccessKey( ) err = useCase.CreateSecureAccessKey( - service.accountQueryRepo, service.secureAccessKeyCmdRepo, - service.activityRecordCmdRepo, createDto, + service.secureAccessKeyCmdRepo, service.activityRecordCmdRepo, createDto, ) if err != nil { return NewServiceOutput(InfraError, err.Error()) @@ -367,8 +366,7 @@ func (service *AccountService) DeleteSecureAccessKey( ) err = useCase.DeleteSecureAccessKey( - service.accountQueryRepo, service.secureAccessKeyCmdRepo, - service.activityRecordCmdRepo, deleteDto, + service.secureAccessKeyCmdRepo, service.activityRecordCmdRepo, deleteDto, ) if err != nil { return NewServiceOutput(InfraError, err.Error()) From c8c5f2b36391a188a2bf703b15b32430b7873585 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 19:30:52 -0300 Subject: [PATCH 046/100] feat: add SecureAccessKeyFingerprint --- .../valueObject/secureAccessKeyFingerprint.go | 32 +++++++++++++++ .../secureAccessKeyFingerprint_test.go | 39 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/domain/valueObject/secureAccessKeyFingerprint.go create mode 100644 src/domain/valueObject/secureAccessKeyFingerprint_test.go diff --git a/src/domain/valueObject/secureAccessKeyFingerprint.go b/src/domain/valueObject/secureAccessKeyFingerprint.go new file mode 100644 index 000000000..e30d3cdc9 --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyFingerprint.go @@ -0,0 +1,32 @@ +package valueObject + +import ( + "errors" + "regexp" + + voHelper "github.com/goinfinite/os/src/domain/valueObject/helper" +) + +const secureAccessKeyFingerprintRegex string = `^SHA256:[\w\/\+\=]{43}$` + +type SecureAccessKeyFingerprint string + +func NewSecureAccessKeyFingerprint( + value interface{}, +) (keyFingerprint SecureAccessKeyFingerprint, err error) { + stringValue, err := voHelper.InterfaceToString(value) + if err != nil { + return keyFingerprint, errors.New("SecureAccessKeyFingerprintMustBeString") + } + + re := regexp.MustCompile(secureAccessKeyFingerprintRegex) + if !re.MatchString(stringValue) { + return keyFingerprint, errors.New("InvalidSecureAccessKeyFingerprint") + } + + return SecureAccessKeyFingerprint(stringValue), nil +} + +func (vo SecureAccessKeyFingerprint) String() string { + return string(vo) +} diff --git a/src/domain/valueObject/secureAccessKeyFingerprint_test.go b/src/domain/valueObject/secureAccessKeyFingerprint_test.go new file mode 100644 index 000000000..d53be5da7 --- /dev/null +++ b/src/domain/valueObject/secureAccessKeyFingerprint_test.go @@ -0,0 +1,39 @@ +package valueObject + +import ( + "testing" +) + +func TestSecureAccessKeyFingerprint(t *testing.T) { + t.Run("ValidSecureAccessKeyFingerprint", func(t *testing.T) { + rawValidSecureAccessKeyFingerprint := []interface{}{ + "SHA256:+DZVNCZhuX6xKglL9R3mUkvRJpMeL8ptNi8kaxAShg4", + "SHA256:4/A1a6zPZdue6c03mG9DBk7e0Mqt7167wK5ikSvxynw", + "SHA256:fTmGqpEJy0oCGGobdzvH9KeBvPrQRFTxn1zr/ss4Wow", + } + + for _, rawKeyFingerprint := range rawValidSecureAccessKeyFingerprint { + _, err := NewSecureAccessKeyFingerprint(rawKeyFingerprint) + if err != nil { + t.Errorf( + "Expected no error for '%v', got '%s'", rawKeyFingerprint, err.Error(), + ) + } + } + }) + + t.Run("InvalidSecureAccessKeyFingerprint", func(t *testing.T) { + rawInvalidSecureAccessKeyFingerprint := []interface{}{ + "", "SHA256", ":+DZVNCZhuX6xKglL9R3mUkvRJpMeL8ptNi8kaxAShg4", + "SHA256+DZVNCZhuX6xKglL9R3mUkvRJpMeL8ptNi8kaxAShg4", + "+DZVNCZhuX6xKglL9R3mUkvRJpMeL8ptNi8kaxAShg4", + } + + for _, rawKeyFingerprint := range rawInvalidSecureAccessKeyFingerprint { + _, err := NewSecureAccessKeyFingerprint(rawKeyFingerprint) + if err == nil { + t.Errorf("Expected error for '%v', got nil", rawKeyFingerprint) + } + } + }) +} From 32ea2befe4797c0a7ca1c49be5f1c66a9ce5ec3b Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 19:40:38 -0300 Subject: [PATCH 047/100] feat: implements pagination with new fingerprint prop --- src/domain/dto/readSecureAccessKeys.go | 18 ++ src/domain/entity/secureAccessKey.go | 27 +-- .../repository/secureAccessKeyQueryRepo.go | 5 +- src/domain/useCase/readSecureAccessKeys.go | 14 +- .../secureAccessKey/secureAccessKeyCmdRepo.go | 50 ++++-- .../secureAccessKeyQueryRepo.go | 154 +++++++++--------- .../secureAccessKeyQueryRepo_test.go | 15 +- .../internalDatabase/model/secureAccessKey.go | 29 ++-- src/presentation/api/controller/account.go | 11 +- src/presentation/service/account.go | 76 ++++++++- 10 files changed, 262 insertions(+), 137 deletions(-) create mode 100644 src/domain/dto/readSecureAccessKeys.go diff --git a/src/domain/dto/readSecureAccessKeys.go b/src/domain/dto/readSecureAccessKeys.go new file mode 100644 index 000000000..2e4749387 --- /dev/null +++ b/src/domain/dto/readSecureAccessKeys.go @@ -0,0 +1,18 @@ +package dto + +import ( + "github.com/goinfinite/os/src/domain/entity" + "github.com/goinfinite/os/src/domain/valueObject" +) + +type ReadSecureAccessKeysRequest struct { + Pagination Pagination `json:"pagination"` + AccountId valueObject.AccountId `json:"accountId,omitempty"` + SecureAccessKeyId *valueObject.SecureAccessKeyId `json:"id,omitempty"` + SecureAccessKeyName *valueObject.SecureAccessKeyName `json:"name,omitempty"` +} + +type ReadSecureAccessKeysResponse struct { + Pagination Pagination `json:"pagination"` + SecureAccessKeys []entity.SecureAccessKey `json:"secureAccessKeys"` +} diff --git a/src/domain/entity/secureAccessKey.go b/src/domain/entity/secureAccessKey.go index 5e854beb6..a3844ca5f 100644 --- a/src/domain/entity/secureAccessKey.go +++ b/src/domain/entity/secureAccessKey.go @@ -3,12 +3,13 @@ package entity import "github.com/goinfinite/os/src/domain/valueObject" type SecureAccessKey struct { - Id valueObject.SecureAccessKeyId `json:"id"` - AccountId valueObject.AccountId `json:"accountId"` - Name valueObject.SecureAccessKeyName `json:"name"` - Content valueObject.SecureAccessKeyContent `json:"content"` - CreatedAt valueObject.UnixTime `json:"createdAt"` - UpdatedAt valueObject.UnixTime `json:"updatedAt"` + Id valueObject.SecureAccessKeyId `json:"id"` + AccountId valueObject.AccountId `json:"accountId"` + Name valueObject.SecureAccessKeyName `json:"name"` + Content valueObject.SecureAccessKeyContent `json:"--"` + Fingerprint valueObject.SecureAccessKeyFingerprint `json:"fingerprint"` + CreatedAt valueObject.UnixTime `json:"createdAt"` + UpdatedAt valueObject.UnixTime `json:"updatedAt"` } func NewSecureAccessKey( @@ -16,14 +17,16 @@ func NewSecureAccessKey( accountId valueObject.AccountId, name valueObject.SecureAccessKeyName, content valueObject.SecureAccessKeyContent, + fingerprint valueObject.SecureAccessKeyFingerprint, createdAt, updatedAt valueObject.UnixTime, ) SecureAccessKey { return SecureAccessKey{ - Id: id, - AccountId: accountId, - Name: name, - Content: content, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + Id: id, + AccountId: accountId, + Name: name, + Content: content, + Fingerprint: fingerprint, + CreatedAt: createdAt, + UpdatedAt: updatedAt, } } diff --git a/src/domain/repository/secureAccessKeyQueryRepo.go b/src/domain/repository/secureAccessKeyQueryRepo.go index 02d065133..f495a799c 100644 --- a/src/domain/repository/secureAccessKeyQueryRepo.go +++ b/src/domain/repository/secureAccessKeyQueryRepo.go @@ -1,10 +1,9 @@ package repository import ( - "github.com/goinfinite/os/src/domain/entity" - "github.com/goinfinite/os/src/domain/valueObject" + "github.com/goinfinite/os/src/domain/dto" ) type SecureAccessKeyQueryRepo interface { - Read(valueObject.AccountId) ([]entity.SecureAccessKey, error) + Read(dto.ReadSecureAccessKeysRequest) (dto.ReadSecureAccessKeysResponse, error) } diff --git a/src/domain/useCase/readSecureAccessKeys.go b/src/domain/useCase/readSecureAccessKeys.go index 2d331450b..dfbcd405b 100644 --- a/src/domain/useCase/readSecureAccessKeys.go +++ b/src/domain/useCase/readSecureAccessKeys.go @@ -4,16 +4,20 @@ import ( "errors" "log/slog" - "github.com/goinfinite/os/src/domain/entity" + "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/repository" - "github.com/goinfinite/os/src/domain/valueObject" ) +var SecureAccessKeysDefaultPagination dto.Pagination = dto.Pagination{ + PageNumber: 0, + ItemsPerPage: 10, +} + func ReadSecureAccessKeys( secureAccessKeyQueryRepo repository.SecureAccessKeyQueryRepo, - accountId valueObject.AccountId, -) (secureAccessKeys []entity.SecureAccessKey, err error) { - secureAccessKeys, err = secureAccessKeyQueryRepo.Read(accountId) + requestDto dto.ReadSecureAccessKeysRequest, +) (secureAccessKeys dto.ReadSecureAccessKeysResponse, err error) { + secureAccessKeys, err = secureAccessKeyQueryRepo.Read(requestDto) if err != nil { slog.Error("ReadSecureAccessKeysInfraError", slog.Any("error", err)) return secureAccessKeys, errors.New("ReadSecureAccessKeysInfraError") diff --git a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go index 7486d9773..dd2b8cd8e 100644 --- a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go +++ b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go @@ -130,11 +130,21 @@ func (repo *SecureAccessKeyCmdRepo) recreateSecureAccessKeysFile( accountId valueObject.AccountId, accountUsername valueObject.Username, ) error { - keys, err := repo.secureAccessKeyQueryRepo.Read(accountId) + readRequestDto := dto.ReadSecureAccessKeysRequest{ + Pagination: dto.Pagination{ + ItemsPerPage: 1000, + }, + AccountId: accountId, + } + keys, err := repo.secureAccessKeyQueryRepo.Read(readRequestDto) if err != nil { return err } + if len(keys.SecureAccessKeys) == 0 { + return errors.New("NoSecureAccessKeyFound") + } + keysFilePath := "/home/" + accountUsername.String() + "/.ssh/authorized_keys" if infraHelper.FileExists(keysFilePath) { err = os.Remove(keysFilePath) @@ -149,8 +159,8 @@ func (repo *SecureAccessKeyCmdRepo) recreateSecureAccessKeysFile( } keysFileContent := "" - for _, key := range keys { - keysFileContent += key.Content.String() + "\n" + for _, key := range keys.SecureAccessKeys { + keysFileContent += key.Content.String() + " " + key.Name.String() + "\n" } shouldOverwrite := true @@ -186,6 +196,17 @@ func (repo *SecureAccessKeyCmdRepo) Create( return keyId, errors.New("InvalidSecureAccessKey") } + rawFingerprint, err := infraHelper.RunCmdWithSubShell( + "echo \"" + keyContentStr + "\" | ssh-keygen -lf /dev/stdin | awk '{print $2}'", + ) + if err != nil { + return keyId, errors.New("FailToReadSecureAccessKeyFingerprint: " + err.Error()) + } + fingerPrint, err := valueObject.NewSecureAccessKeyFingerprint(rawFingerprint) + if err != nil { + return keyId, err + } + _, err = infraHelper.RunCmdWithSubShell( "echo \"" + keyContentStr + "\" >> /home/" + account.Username.String() + "/.ssh/authorized_keys", @@ -201,7 +222,7 @@ func (repo *SecureAccessKeyCmdRepo) Create( secureAccessKeyModel := dbModel.NewSecureAccessKey( 0, account.Id.Uint64(), createDto.Name.String(), - createDto.Content.ReadWithoutKeyName(), + createDto.Content.ReadWithoutKeyName(), fingerPrint.String(), ) createResult := repo.persistentDbSvc.Handler.Create(&secureAccessKeyModel) @@ -225,20 +246,21 @@ func (repo *SecureAccessKeyCmdRepo) Delete( return errors.New("AccountNotFound") } - keyToDelete, err := repo.secureAccessKeyQueryRepo.ReadById( - deleteDto.AccountId, deleteDto.Id, - ) + readFirstRequestDto := dto.ReadSecureAccessKeysRequest{ + AccountId: deleteDto.AccountId, + SecureAccessKeyId: &deleteDto.Id, + } + _, err = repo.secureAccessKeyQueryRepo.ReadFirst(readFirstRequestDto) if err != nil { - return err + return errors.New("SecureAccessKeyNotFound") } - _, err = infraHelper.RunCmdWithSubShell( - "sed -i '\\|" + keyToDelete.Content.String() + "|d' " + - "/home/" + account.Username.String() + "/.ssh/authorized_keys", - ) + err = repo.persistentDbSvc.Handler.Delete( + dbModel.SecureAccessKey{}, deleteDto.Id.Uint16(), + ).Error if err != nil { - return errors.New("FailToDeleteSecureAccessKeyFromFile: " + err.Error()) + return err } - return nil + return repo.recreateSecureAccessKeysFile(account.Id, account.Username) } diff --git a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go index 25982f0af..146360cb0 100644 --- a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go +++ b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go @@ -3,13 +3,14 @@ package secureAccessKeyInfra import ( "errors" "log/slog" - "strings" + "math" + "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/entity" - "github.com/goinfinite/os/src/domain/valueObject" accountInfra "github.com/goinfinite/os/src/infra/account" - infraHelper "github.com/goinfinite/os/src/infra/helper" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" + dbModel "github.com/goinfinite/os/src/infra/internalDatabase/model" + "github.com/iancoleman/strcase" ) type SecureAccessKeyQueryRepo struct { @@ -26,113 +27,106 @@ func NewSecureAccessKeyQueryRepo( } } -func (repo *SecureAccessKeyQueryRepo) secureAccessKeyFactory( - rawKeyId int, - rawSecureAccessKeyContent string, - accountId valueObject.AccountId, -) (secureAccessKey entity.SecureAccessKey, err error) { - keyId, err := valueObject.NewSecureAccessKeyId(rawKeyId) - if err != nil { - return secureAccessKey, err +func (repo *SecureAccessKeyQueryRepo) Read( + requestDto dto.ReadSecureAccessKeysRequest, +) (responseDto dto.ReadSecureAccessKeysResponse, err error) { + model := dbModel.SecureAccessKey{ + AccountId: requestDto.AccountId.Uint64(), } - - keyContent, err := valueObject.NewSecureAccessKeyContent( - rawSecureAccessKeyContent, - ) - if err != nil { - return secureAccessKey, err + if requestDto.SecureAccessKeyId != nil { + model.ID = requestDto.SecureAccessKeyId.Uint16() } - - keyName, err := keyContent.ReadOnlyKeyName() - if err != nil { - return secureAccessKey, err + if requestDto.SecureAccessKeyName != nil { + model.Name = requestDto.SecureAccessKeyName.String() } - now := valueObject.NewUnixTimeNow() - - return entity.NewSecureAccessKey( - keyId, accountId, keyName, keyContent, now, now, - ), nil -} - -func (repo *SecureAccessKeyQueryRepo) Read( - accountId valueObject.AccountId, -) ([]entity.SecureAccessKey, error) { - secureAccessKeys := []entity.SecureAccessKey{} - - account, err := repo.accountQueryRepo.ReadById(accountId) - if err != nil { - return secureAccessKeys, errors.New("AccountNotFound") - } + dbQuery := repo.persistentDbSvc.Handler. + Model(&model). + Where(&model) - secureAccessKeysFilePath := "/home/" + account.Username.String() + "/.ssh" + - "/authorized_keys" - secureAccessKeysFileContent, err := infraHelper.GetFileContent( - secureAccessKeysFilePath, - ) + var itemsTotal int64 + err = dbQuery.Count(&itemsTotal).Error if err != nil { - return secureAccessKeys, errors.New( - "ReadSecureAccessKeysFileContentError: " + err.Error(), + return responseDto, errors.New( + "CountSecureAccessKeysTotalError: " + err.Error(), ) } - secureAccessKeysFileContentParts := strings.Split(secureAccessKeysFileContent, "\n") - for index, rawSecureAccessKeyContent := range secureAccessKeysFileContentParts { - if rawSecureAccessKeyContent == "" { - continue + dbQuery.Limit(int(requestDto.Pagination.ItemsPerPage)) + if requestDto.Pagination.LastSeenId == nil { + offset := int(requestDto.Pagination.PageNumber) * int(requestDto.Pagination.ItemsPerPage) + dbQuery = dbQuery.Offset(offset) + } else { + dbQuery = dbQuery.Where("id > ?", requestDto.Pagination.LastSeenId.String()) + } + if requestDto.Pagination.SortBy != nil { + orderStatement := requestDto.Pagination.SortBy.String() + orderStatement = strcase.ToSnake(orderStatement) + if orderStatement == "id" { + orderStatement = "ID" } - rawKeyId := index + 1 - secureAccessKey, err := repo.secureAccessKeyFactory( - rawKeyId, rawSecureAccessKeyContent, accountId, - ) - if err != nil { - slog.Debug(err.Error(), slog.Int("index", index)) - continue + if requestDto.Pagination.SortDirection != nil { + orderStatement += " " + requestDto.Pagination.SortDirection.String() } - secureAccessKeys = append(secureAccessKeys, secureAccessKey) + dbQuery = dbQuery.Order(orderStatement) } - return secureAccessKeys, nil -} - -func (repo *SecureAccessKeyQueryRepo) ReadById( - accountId valueObject.AccountId, - secureAccessKeyId valueObject.SecureAccessKeyId, -) (secureAccessKey entity.SecureAccessKey, err error) { - secureAccessKeys, err := repo.Read(accountId) + models := []dbModel.SecureAccessKey{} + err = dbQuery.Find(&models).Error if err != nil { - return secureAccessKey, err + return responseDto, errors.New("ReadSecureAccessKeysError: " + err.Error()) } - for _, key := range secureAccessKeys { - if key.Id.Uint16() != secureAccessKeyId.Uint16() { + entities := []entity.SecureAccessKey{} + for _, model := range models { + entity, err := model.ToEntity() + if err != nil { + slog.Debug( + "SecureAccessKeyModelToEntityError", + slog.Uint64("id", uint64(model.ID)), slog.Any("error", err), + ) continue } - return key, nil + entities = append(entities, entity) + } + + itemsTotalUint := uint64(itemsTotal) + pagesTotal := uint32( + math.Ceil(float64(itemsTotal) / float64(requestDto.Pagination.ItemsPerPage)), + ) + responsePagination := dto.Pagination{ + PageNumber: requestDto.Pagination.PageNumber, + ItemsPerPage: requestDto.Pagination.ItemsPerPage, + SortBy: requestDto.Pagination.SortBy, + SortDirection: requestDto.Pagination.SortDirection, + PagesTotal: &pagesTotal, + ItemsTotal: &itemsTotalUint, } - return secureAccessKey, errors.New("SecureAccessKeyNotFound") + return dto.ReadSecureAccessKeysResponse{ + Pagination: responsePagination, + SecureAccessKeys: entities, + }, nil } -func (repo *SecureAccessKeyQueryRepo) ReadByName( - accountId valueObject.AccountId, - secureAccessKeyName valueObject.SecureAccessKeyName, +func (repo *SecureAccessKeyQueryRepo) ReadFirst( + requestDto dto.ReadSecureAccessKeysRequest, ) (secureAccessKey entity.SecureAccessKey, err error) { - secureAccessKeys, err := repo.Read(accountId) + requestDto.Pagination = dto.Pagination{ + PageNumber: 0, + ItemsPerPage: 1, + } + responseDto, err := repo.Read(requestDto) if err != nil { return secureAccessKey, err } - for _, key := range secureAccessKeys { - if key.Name.String() != secureAccessKeyName.String() { - continue - } - - return key, nil + if len(responseDto.SecureAccessKeys) == 0 { + return secureAccessKey, errors.New("SecureAccessKeyNotFound") } - return secureAccessKey, errors.New("SecureAccessKeyNotFound") + return responseDto.SecureAccessKeys[0], nil } diff --git a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo_test.go b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo_test.go index 959ea3308..202c311a9 100644 --- a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo_test.go +++ b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo_test.go @@ -38,28 +38,33 @@ func TestSecureAccessKeyQueryRepo(t *testing.T) { ) secureAccessKeyCmdRepo := NewSecureAccessKeyCmdRepo(testHelpers.GetPersistentDbSvc()) - _, err = secureAccessKeyCmdRepo.Create(createDto) + keyId, err := secureAccessKeyCmdRepo.Create(createDto) if err != nil { t.Fatalf("Fail to create dummy SecureAccessKey to test") } secureAccessKeyQueryRepo := NewSecureAccessKeyQueryRepo(testHelpers.GetPersistentDbSvc()) + requestDto := dto.ReadSecureAccessKeysRequest{ + AccountId: accountId, + SecureAccessKeyId: &keyId, + } t.Run("ReadSecureAccessKeys", func(t *testing.T) { - keys, err := secureAccessKeyQueryRepo.Read(accountId) + responseDto, err := secureAccessKeyQueryRepo.Read(requestDto) if err != nil { t.Fatalf( - "Expecting no error for %d, but got %s", accountId.Uint64(), err.Error(), + "Expecting no error for %d, but got %s", accountId.Uint64(), + err.Error(), ) } - if len(keys) == 0 { + if len(responseDto.SecureAccessKeys) == 0 { t.Error("Expecting a keys list, but got an empty one") } }) t.Run("ReadSecureAccessKeyByName", func(t *testing.T) { - _, err := secureAccessKeyQueryRepo.ReadByName(accountId, keyName) + _, err := secureAccessKeyQueryRepo.ReadFirst(requestDto) if err != nil { t.Fatalf( "Expecting no error for %s (%d), but got %s", keyName.String(), diff --git a/src/infra/internalDatabase/model/secureAccessKey.go b/src/infra/internalDatabase/model/secureAccessKey.go index a78d3088d..a746d33e7 100644 --- a/src/infra/internalDatabase/model/secureAccessKey.go +++ b/src/infra/internalDatabase/model/secureAccessKey.go @@ -8,12 +8,13 @@ import ( ) type SecureAccessKey struct { - ID uint16 `gorm:"primarykey"` - AccountId uint64 `gorm:"not null"` - Name string `gorm:"primarykey"` - Content string `gorm:"primarykey"` - CreatedAt time.Time - UpdatedAt time.Time + ID uint16 `gorm:"primarykey"` + AccountId uint64 `gorm:"not null"` + Name string `gorm:"not null"` + Content string `gorm:"not null"` + Fingerprint string `gorm:"not null"` + CreatedAt time.Time + UpdatedAt time.Time } func (SecureAccessKey) TableName() string { @@ -23,12 +24,13 @@ func (SecureAccessKey) TableName() string { func NewSecureAccessKey( id uint16, accountId uint64, - name, content string, + name, content, fingerprint string, ) SecureAccessKey { model := SecureAccessKey{ - AccountId: accountId, - Name: name, - Content: content, + AccountId: accountId, + Name: name, + Content: content, + Fingerprint: fingerprint, } if id != 0 { @@ -61,8 +63,13 @@ func (model SecureAccessKey) ToEntity() ( return secureAccessKeyEntity, err } + fingerprint, err := valueObject.NewSecureAccessKeyFingerprint(model.Fingerprint) + if err != nil { + return secureAccessKeyEntity, err + } + return entity.NewSecureAccessKey( - id, accountId, name, content, + id, accountId, name, content, fingerprint, valueObject.NewUnixTimeWithGoTime(model.CreatedAt), valueObject.NewUnixTimeWithGoTime(model.UpdatedAt), ), nil diff --git a/src/presentation/api/controller/account.go b/src/presentation/api/controller/account.go index 798eafb8b..ee00d8916 100644 --- a/src/presentation/api/controller/account.go +++ b/src/presentation/api/controller/account.go @@ -113,8 +113,15 @@ func (controller *AccountController) Delete(c echo.Context) error { // @Accept json // @Produce json // @Security Bearer -// @Param accountId path string true "AccountId that keys belongs to." -// @Success 200 {array} entity.SecureAccessKey +// @Param accountId query uint true "AccountId that keys belongs to" +// @Param id query string false "Id" +// @Param name query string false "Name" +// @Param pageNumber query uint false "PageNumber (Pagination)" +// @Param itemsPerPage query uint false "ItemsPerPage (Pagination)" +// @Param sortBy query string false "SortBy (Pagination)" +// @Param sortDirection query string false "SortDirection (Pagination)" +// @Param lastSeenId query string false "LastSeenId (Pagination)" +// @Success 200 {object} dto.ReadSecureAccessKeysResponse // @Router /v1/account/{accountId}/secure-access-key/ [get] func (controller *AccountController) ReadSecureAccessKey(c echo.Context) error { requestBody, err := apiHelper.ReadRequestBody(c) diff --git a/src/presentation/service/account.go b/src/presentation/service/account.go index e10a98f9c..d805320e6 100644 --- a/src/presentation/service/account.go +++ b/src/presentation/service/account.go @@ -1,6 +1,8 @@ package service import ( + "errors" + "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/useCase" "github.com/goinfinite/os/src/domain/valueObject" @@ -234,10 +236,6 @@ func (service *AccountService) ReadSecureAccessKey( input["accountId"] = input["id"] } - if input["accountId"] == nil { - input["accountId"] = input["operatorAccountId"] - } - requiredParams := []string{"accountId"} err := serviceHelper.RequiredParamsInspector(input, requiredParams) if err != nil { @@ -249,8 +247,76 @@ func (service *AccountService) ReadSecureAccessKey( return NewServiceOutput(UserError, err.Error()) } + var idPtr *valueObject.SecureAccessKeyId + if input["id"] != nil { + id, err := valueObject.NewSecureAccessKeyId(input["id"]) + if err != nil { + return NewServiceOutput(UserError, err) + } + idPtr = &id + } + + var namePtr *valueObject.SecureAccessKeyName + if input["name"] != nil { + name, err := valueObject.NewSecureAccessKeyName(input["name"]) + if err != nil { + return NewServiceOutput(UserError, err) + } + namePtr = &name + } + + paginationDto := useCase.MarketplaceDefaultPagination + if input["pageNumber"] != nil { + pageNumber, err := voHelper.InterfaceToUint32(input["pageNumber"]) + if err != nil { + return NewServiceOutput(UserError, errors.New("InvalidPageNumber")) + } + paginationDto.PageNumber = pageNumber + } + + if input["itemsPerPage"] != nil { + itemsPerPage, err := voHelper.InterfaceToUint16(input["itemsPerPage"]) + if err != nil { + return NewServiceOutput(UserError, errors.New("InvalidItemsPerPage")) + } + paginationDto.ItemsPerPage = itemsPerPage + } + + if input["sortBy"] != nil { + sortBy, err := valueObject.NewPaginationSortBy(input["sortBy"]) + if err != nil { + return NewServiceOutput(UserError, err) + } + paginationDto.SortBy = &sortBy + } + + if input["sortDirection"] != nil { + sortDirection, err := valueObject.NewPaginationSortDirection( + input["sortDirection"], + ) + if err != nil { + return NewServiceOutput(UserError, err) + } + paginationDto.SortDirection = &sortDirection + } + + if input["lastSeenId"] != nil { + lastSeenId, err := valueObject.NewPaginationLastSeenId(input["lastSeenId"]) + if err != nil { + return NewServiceOutput(UserError, err) + } + paginationDto.LastSeenId = &lastSeenId + } + + readRequestDto := dto.ReadSecureAccessKeysRequest{ + Pagination: paginationDto, + AccountId: accountId, + SecureAccessKeyId: idPtr, + SecureAccessKeyName: namePtr, + } + secureAccessKeys, err := useCase.ReadSecureAccessKeys( - service.secureAccessKeyQueryRepo, accountId, + service.secureAccessKeyQueryRepo, readRequestDto, ) if err != nil { return NewServiceOutput(InfraError, err.Error()) From fd0653809f739b5b960eab7872c3c110a6b2af48 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 19:59:36 -0300 Subject: [PATCH 048/100] fix: remove useless accountQueryRepo instance from secureAccessKeyQueryRepo struct --- .../account/secureAccessKey/secureAccessKeyQueryRepo.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go index 146360cb0..6640f4a02 100644 --- a/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go +++ b/src/infra/account/secureAccessKey/secureAccessKeyQueryRepo.go @@ -7,23 +7,20 @@ import ( "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/entity" - accountInfra "github.com/goinfinite/os/src/infra/account" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" dbModel "github.com/goinfinite/os/src/infra/internalDatabase/model" "github.com/iancoleman/strcase" ) type SecureAccessKeyQueryRepo struct { - persistentDbSvc *internalDbInfra.PersistentDatabaseService - accountQueryRepo *accountInfra.AccountQueryRepo + persistentDbSvc *internalDbInfra.PersistentDatabaseService } func NewSecureAccessKeyQueryRepo( persistentDbSvc *internalDbInfra.PersistentDatabaseService, ) *SecureAccessKeyQueryRepo { return &SecureAccessKeyQueryRepo{ - persistentDbSvc: persistentDbSvc, - accountQueryRepo: accountInfra.NewAccountQueryRepo(persistentDbSvc), + persistentDbSvc: persistentDbSvc, } } From 7176b89737a1bffd6c3217f06123ae65359de6ee Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 21:29:12 -0300 Subject: [PATCH 049/100] refact: change all secureAccessKeys and accounts infra, UC and presentation --- src/domain/dto/deleteSecureAccessKey.go | 4 +- src/domain/dto/readAccounts.go | 18 ++ src/domain/entity/account.go | 27 ++- src/domain/entity/secureAccessKey.go | 2 +- src/domain/repository/accountQueryRepo.go | 3 +- .../repository/secureAccessKeyCmdRepo.go | 2 +- .../repository/secureAccessKeyQueryRepo.go | 2 + src/domain/useCase/createActivityRecord.go | 3 +- src/domain/useCase/deleteSecureAccessKey.go | 13 +- src/domain/useCase/readAccounts.go | 11 +- src/infra/account/accountCmdRepo.go | 3 +- src/infra/account/accountQueryRepo.go | 87 ++++++-- src/infra/account/accountQueryRepo_test.go | 6 +- .../secureAccessKey/secureAccessKeyCmdRepo.go | 49 ++--- .../secureAccessKeyCmdRepo_test.go | 4 +- src/infra/internalDatabase/model/account.go | 11 +- .../internalDatabase/model/secureAccessKey.go | 2 +- src/presentation/api/controller/account.go | 53 ++--- src/presentation/api/router.go | 3 +- src/presentation/cli/controller/account.go | 90 ++++++-- src/presentation/cli/router.go | 1 - src/presentation/service/account.go | 200 ++++++++---------- src/presentation/ui/presenter/accounts.go | 6 +- 23 files changed, 355 insertions(+), 245 deletions(-) create mode 100644 src/domain/dto/readAccounts.go diff --git a/src/domain/dto/deleteSecureAccessKey.go b/src/domain/dto/deleteSecureAccessKey.go index 068dddd76..eb753c31a 100644 --- a/src/domain/dto/deleteSecureAccessKey.go +++ b/src/domain/dto/deleteSecureAccessKey.go @@ -4,19 +4,17 @@ import "github.com/goinfinite/os/src/domain/valueObject" type DeleteSecureAccessKey struct { Id valueObject.SecureAccessKeyId `json:"id"` - AccountId valueObject.AccountId `json:"-"` OperatorAccountId valueObject.AccountId `json:"-"` OperatorIpAddress valueObject.IpAddress `json:"-"` } func NewDeleteSecureAccessKey( id valueObject.SecureAccessKeyId, - accountId, operatorAccountId valueObject.AccountId, + operatorAccountId valueObject.AccountId, operatorIpAddress valueObject.IpAddress, ) DeleteSecureAccessKey { return DeleteSecureAccessKey{ Id: id, - AccountId: accountId, OperatorAccountId: operatorAccountId, OperatorIpAddress: operatorIpAddress, } diff --git a/src/domain/dto/readAccounts.go b/src/domain/dto/readAccounts.go new file mode 100644 index 000000000..d2005dc94 --- /dev/null +++ b/src/domain/dto/readAccounts.go @@ -0,0 +1,18 @@ +package dto + +import ( + "github.com/goinfinite/os/src/domain/entity" + "github.com/goinfinite/os/src/domain/valueObject" +) + +type ReadAccountsRequest struct { + Pagination Pagination `json:"pagination"` + AccountId *valueObject.AccountId `json:"id,omitempty"` + AccountUsername *valueObject.Username `json:"username,omitempty"` + ShouldIncludeSecureAccessKeys *bool `json:"shouldIncludeSecureAccessKeys,omitempty"` +} + +type ReadAccountsResponse struct { + Pagination Pagination `json:"pagination"` + Accounts []entity.Account `json:"accounts"` +} diff --git a/src/domain/entity/account.go b/src/domain/entity/account.go index 2a6ff3bb7..a30d31ee7 100644 --- a/src/domain/entity/account.go +++ b/src/domain/entity/account.go @@ -1,26 +1,31 @@ package entity -import "github.com/goinfinite/os/src/domain/valueObject" +import ( + "github.com/goinfinite/os/src/domain/valueObject" +) type Account struct { - Id valueObject.AccountId `json:"id"` - GroupId valueObject.GroupId `json:"groupId"` - Username valueObject.Username `json:"username"` - CreatedAt valueObject.UnixTime `json:"createdAt"` - UpdatedAt valueObject.UnixTime `json:"updatedAt"` + Id valueObject.AccountId `json:"id"` + GroupId valueObject.GroupId `json:"groupId"` + Username valueObject.Username `json:"username"` + SecureAccessKeys []SecureAccessKey `json:"secureAccessKeys"` + CreatedAt valueObject.UnixTime `json:"createdAt"` + UpdatedAt valueObject.UnixTime `json:"updatedAt"` } func NewAccount( accountId valueObject.AccountId, groupId valueObject.GroupId, username valueObject.Username, + secureAccessKeys []SecureAccessKey, createdAt, updatedAt valueObject.UnixTime, ) Account { return Account{ - Id: accountId, - GroupId: groupId, - Username: username, - CreatedAt: createdAt, - UpdatedAt: updatedAt, + Id: accountId, + GroupId: groupId, + Username: username, + SecureAccessKeys: secureAccessKeys, + CreatedAt: createdAt, + UpdatedAt: updatedAt, } } diff --git a/src/domain/entity/secureAccessKey.go b/src/domain/entity/secureAccessKey.go index a3844ca5f..780688c1b 100644 --- a/src/domain/entity/secureAccessKey.go +++ b/src/domain/entity/secureAccessKey.go @@ -6,7 +6,7 @@ type SecureAccessKey struct { Id valueObject.SecureAccessKeyId `json:"id"` AccountId valueObject.AccountId `json:"accountId"` Name valueObject.SecureAccessKeyName `json:"name"` - Content valueObject.SecureAccessKeyContent `json:"--"` + Content valueObject.SecureAccessKeyContent `json:"-"` Fingerprint valueObject.SecureAccessKeyFingerprint `json:"fingerprint"` CreatedAt valueObject.UnixTime `json:"createdAt"` UpdatedAt valueObject.UnixTime `json:"updatedAt"` diff --git a/src/domain/repository/accountQueryRepo.go b/src/domain/repository/accountQueryRepo.go index baeec88f9..bc911b885 100644 --- a/src/domain/repository/accountQueryRepo.go +++ b/src/domain/repository/accountQueryRepo.go @@ -1,12 +1,13 @@ package repository import ( + "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/entity" "github.com/goinfinite/os/src/domain/valueObject" ) type AccountQueryRepo interface { - Read() ([]entity.Account, error) + Read(dto.ReadAccountsRequest) (dto.ReadAccountsResponse, error) ReadByUsername(valueObject.Username) (entity.Account, error) ReadById(valueObject.AccountId) (entity.Account, error) } diff --git a/src/domain/repository/secureAccessKeyCmdRepo.go b/src/domain/repository/secureAccessKeyCmdRepo.go index b1a6208d7..1622bd290 100644 --- a/src/domain/repository/secureAccessKeyCmdRepo.go +++ b/src/domain/repository/secureAccessKeyCmdRepo.go @@ -7,5 +7,5 @@ import ( type SecureAccessKeyCmdRepo interface { Create(dto.CreateSecureAccessKey) (valueObject.SecureAccessKeyId, error) - Delete(dto.DeleteSecureAccessKey) error + Delete(valueObject.SecureAccessKeyId) error } diff --git a/src/domain/repository/secureAccessKeyQueryRepo.go b/src/domain/repository/secureAccessKeyQueryRepo.go index f495a799c..da35d39ca 100644 --- a/src/domain/repository/secureAccessKeyQueryRepo.go +++ b/src/domain/repository/secureAccessKeyQueryRepo.go @@ -2,8 +2,10 @@ package repository import ( "github.com/goinfinite/os/src/domain/dto" + "github.com/goinfinite/os/src/domain/entity" ) type SecureAccessKeyQueryRepo interface { Read(dto.ReadSecureAccessKeysRequest) (dto.ReadSecureAccessKeysResponse, error) + ReadFirst(dto.ReadSecureAccessKeysRequest) (entity.SecureAccessKey, error) } diff --git a/src/domain/useCase/createActivityRecord.go b/src/domain/useCase/createActivityRecord.go index 64f62b591..546f1ec2f 100644 --- a/src/domain/useCase/createActivityRecord.go +++ b/src/domain/useCase/createActivityRecord.go @@ -132,13 +132,14 @@ func (uc *CreateSecurityActivityRecord) CreateSecureAccessKey( func (uc *CreateSecurityActivityRecord) DeleteSecureAccessKey( deleteDto dto.DeleteSecureAccessKey, + accountId valueObject.AccountId, ) { recordCode, _ := valueObject.NewActivityRecordCode("SecureAccessKeyDeleted") createRecordDto := dto.CreateActivityRecord{ RecordLevel: uc.recordLevel, RecordCode: recordCode, AffectedResources: []valueObject.SystemResourceIdentifier{ - valueObject.NewSecureAccessKeySri(deleteDto.AccountId, deleteDto.Id), + valueObject.NewSecureAccessKeySri(accountId, deleteDto.Id), }, OperatorAccountId: &deleteDto.OperatorAccountId, OperatorIpAddress: &deleteDto.OperatorIpAddress, diff --git a/src/domain/useCase/deleteSecureAccessKey.go b/src/domain/useCase/deleteSecureAccessKey.go index fa92a4e6d..aaeb304dd 100644 --- a/src/domain/useCase/deleteSecureAccessKey.go +++ b/src/domain/useCase/deleteSecureAccessKey.go @@ -9,18 +9,27 @@ import ( ) func DeleteSecureAccessKey( + secureAccessKeyQueryRepo repository.SecureAccessKeyQueryRepo, secureAccessKeyCmdRepo repository.SecureAccessKeyCmdRepo, activityRecordCmdRepo repository.ActivityRecordCmdRepo, deleteDto dto.DeleteSecureAccessKey, ) error { - err := secureAccessKeyCmdRepo.Delete(deleteDto) + readRequestDto := dto.ReadSecureAccessKeysRequest{ + SecureAccessKeyId: &deleteDto.Id, + } + keyToDelete, err := secureAccessKeyQueryRepo.ReadFirst(readRequestDto) + if err != nil { + return errors.New("SecureAccessKeyNotFound") + } + + err = secureAccessKeyCmdRepo.Delete(keyToDelete.Id) if err != nil { slog.Error("DeleteSecureAccessKeyError", slog.Any("error", err)) return errors.New("DeleteSecureAccessKeyInfraError") } NewCreateSecurityActivityRecord(activityRecordCmdRepo). - DeleteSecureAccessKey(deleteDto) + DeleteSecureAccessKey(deleteDto, keyToDelete.AccountId) return nil } diff --git a/src/domain/useCase/readAccounts.go b/src/domain/useCase/readAccounts.go index dcc0cd355..a4f0514d9 100644 --- a/src/domain/useCase/readAccounts.go +++ b/src/domain/useCase/readAccounts.go @@ -4,18 +4,19 @@ import ( "errors" "log/slog" - "github.com/goinfinite/os/src/domain/entity" + "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/repository" ) func ReadAccounts( accountQueryRepo repository.AccountQueryRepo, -) ([]entity.Account, error) { - accountsList, err := accountQueryRepo.Read() + requestDto dto.ReadAccountsRequest, +) (responseDto dto.ReadAccountsResponse, err error) { + responseDto, err = accountQueryRepo.Read(requestDto) if err != nil { slog.Error("ReadAccountsInfraError", slog.Any("error", err)) - return accountsList, errors.New("ReadAccountsInfraError") + return responseDto, errors.New("ReadAccountsInfraError") } - return accountsList, nil + return responseDto, nil } diff --git a/src/infra/account/accountCmdRepo.go b/src/infra/account/accountCmdRepo.go index 57336200c..1488a3bb7 100644 --- a/src/infra/account/accountCmdRepo.go +++ b/src/infra/account/accountCmdRepo.go @@ -69,7 +69,8 @@ func (repo *AccountCmdRepo) Create( nowUnixTime := valueObject.NewUnixTimeNow() accountEntity := entity.NewAccount( - accountId, groupId, createDto.Username, nowUnixTime, nowUnixTime, + accountId, groupId, createDto.Username, []entity.SecureAccessKey{}, + nowUnixTime, nowUnixTime, ) accountModel, err := dbModel.Account{}.ToModel(accountEntity) diff --git a/src/infra/account/accountQueryRepo.go b/src/infra/account/accountQueryRepo.go index 5f97b99b2..4c464b674 100644 --- a/src/infra/account/accountQueryRepo.go +++ b/src/infra/account/accountQueryRepo.go @@ -3,11 +3,14 @@ package accountInfra import ( "errors" "log/slog" + "math" + "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/entity" "github.com/goinfinite/os/src/domain/valueObject" internalDbInfra "github.com/goinfinite/os/src/infra/internalDatabase" dbModel "github.com/goinfinite/os/src/infra/internalDatabase/model" + "github.com/iancoleman/strcase" ) type AccountQueryRepo struct { @@ -22,32 +25,88 @@ func NewAccountQueryRepo( } } -func (repo *AccountQueryRepo) Read() ([]entity.Account, error) { - accountEntities := []entity.Account{} +func (repo *AccountQueryRepo) Read( + requestDto dto.ReadAccountsRequest, +) (responseDto dto.ReadAccountsResponse, err error) { + model := dbModel.Account{} + if requestDto.AccountId != nil { + model.ID = requestDto.AccountId.Uint64() + } + if requestDto.AccountUsername != nil { + model.Username = requestDto.AccountUsername.String() + } - var accountModels []dbModel.Account - err := repo.persistentDbSvc.Handler. - Model(&dbModel.Account{}). - Find(&accountModels).Error + dbQuery := repo.persistentDbSvc.Handler. + Model(&model). + Where(&model) + if requestDto.ShouldIncludeSecureAccessKeys != nil && *requestDto.ShouldIncludeSecureAccessKeys { + dbQuery = dbQuery.Preload("SecureAccessKeys") + } + + var itemsTotal int64 + err = dbQuery.Count(&itemsTotal).Error if err != nil { - return accountEntities, errors.New("QueryAccountsError: " + err.Error()) + return responseDto, errors.New("CountAccountsTotalError: " + err.Error()) } - for _, accountModel := range accountModels { - accountEntity, err := accountModel.ToEntity() + dbQuery.Limit(int(requestDto.Pagination.ItemsPerPage)) + if requestDto.Pagination.LastSeenId == nil { + offset := int(requestDto.Pagination.PageNumber) * int(requestDto.Pagination.ItemsPerPage) + dbQuery = dbQuery.Offset(offset) + } else { + dbQuery = dbQuery.Where("id > ?", requestDto.Pagination.LastSeenId.String()) + } + if requestDto.Pagination.SortBy != nil { + orderStatement := requestDto.Pagination.SortBy.String() + orderStatement = strcase.ToSnake(orderStatement) + if orderStatement == "id" { + orderStatement = "ID" + } + + if requestDto.Pagination.SortDirection != nil { + orderStatement += " " + requestDto.Pagination.SortDirection.String() + } + + dbQuery = dbQuery.Order(orderStatement) + } + + models := []dbModel.Account{} + err = dbQuery.Find(&models).Error + if err != nil { + return responseDto, errors.New("ReadAccountsError: " + err.Error()) + } + + entities := []entity.Account{} + for _, model := range models { + entity, err := model.ToEntity() if err != nil { slog.Debug( - "ModelToEntityError", - slog.Any("error", err.Error()), - slog.Uint64("accountId", accountModel.ID), + "AccountModelToEntityError", slog.Uint64("id", uint64(model.ID)), + slog.Any("error", err), ) continue } - accountEntities = append(accountEntities, accountEntity) + entities = append(entities, entity) + } + + itemsTotalUint := uint64(itemsTotal) + pagesTotal := uint32( + math.Ceil(float64(itemsTotal) / float64(requestDto.Pagination.ItemsPerPage)), + ) + responsePagination := dto.Pagination{ + PageNumber: requestDto.Pagination.PageNumber, + ItemsPerPage: requestDto.Pagination.ItemsPerPage, + SortBy: requestDto.Pagination.SortBy, + SortDirection: requestDto.Pagination.SortDirection, + PagesTotal: &pagesTotal, + ItemsTotal: &itemsTotalUint, } - return accountEntities, nil + return dto.ReadAccountsResponse{ + Pagination: responsePagination, + Accounts: entities, + }, nil } func (repo *AccountQueryRepo) ReadById( diff --git a/src/infra/account/accountQueryRepo_test.go b/src/infra/account/accountQueryRepo_test.go index 45b18b4bd..0eff2419c 100644 --- a/src/infra/account/accountQueryRepo_test.go +++ b/src/infra/account/accountQueryRepo_test.go @@ -5,6 +5,7 @@ import ( "testing" testHelpers "github.com/goinfinite/os/src/devUtils" + "github.com/goinfinite/os/src/domain/dto" "github.com/goinfinite/os/src/domain/valueObject" ) @@ -17,7 +18,10 @@ func TestAccountQueryRepo(t *testing.T) { accountId, _ := valueObject.NewAccountId(os.Getenv("DUMMY_USER_ID")) t.Run("ReadValidAccounts", func(t *testing.T) { - _, err := accountQueryRepo.Read() + requestDto := dto.ReadAccountsRequest{ + AccountId: &accountId, + } + _, err := accountQueryRepo.Read(requestDto) if err != nil { t.Errorf("Expecting no error, but got %s", err.Error()) } diff --git a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go index dd2b8cd8e..6ec460538 100644 --- a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go +++ b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo.go @@ -130,34 +130,30 @@ func (repo *SecureAccessKeyCmdRepo) recreateSecureAccessKeysFile( accountId valueObject.AccountId, accountUsername valueObject.Username, ) error { - readRequestDto := dto.ReadSecureAccessKeysRequest{ - Pagination: dto.Pagination{ - ItemsPerPage: 1000, - }, - AccountId: accountId, - } - keys, err := repo.secureAccessKeyQueryRepo.Read(readRequestDto) - if err != nil { - return err - } - - if len(keys.SecureAccessKeys) == 0 { - return errors.New("NoSecureAccessKeyFound") - } - keysFilePath := "/home/" + accountUsername.String() + "/.ssh/authorized_keys" if infraHelper.FileExists(keysFilePath) { - err = os.Remove(keysFilePath) + err := os.Remove(keysFilePath) if err != nil { return errors.New("DeleteSecureAccessKeysFileError: " + err.Error()) } } - err = repo.createSecureAccessKeysFileIfNotExists(accountUsername) + err := repo.createSecureAccessKeysFileIfNotExists(accountUsername) if err != nil { return errors.New("CreateSecureAccessKeysFileError: " + err.Error()) } + readRequestDto := dto.ReadSecureAccessKeysRequest{ + Pagination: dto.Pagination{ + ItemsPerPage: 1000, + }, + AccountId: accountId, + } + keys, err := repo.secureAccessKeyQueryRepo.Read(readRequestDto) + if err != nil { + return err + } + keysFileContent := "" for _, key := range keys.SecureAccessKeys { keysFileContent += key.Content.String() + " " + key.Name.String() + "\n" @@ -239,24 +235,23 @@ func (repo *SecureAccessKeyCmdRepo) Create( } func (repo *SecureAccessKeyCmdRepo) Delete( - deleteDto dto.DeleteSecureAccessKey, + secureAccessKeyId valueObject.SecureAccessKeyId, ) error { - account, err := repo.accountQueryRepo.ReadById(deleteDto.AccountId) - if err != nil { - return errors.New("AccountNotFound") - } - readFirstRequestDto := dto.ReadSecureAccessKeysRequest{ - AccountId: deleteDto.AccountId, - SecureAccessKeyId: &deleteDto.Id, + SecureAccessKeyId: &secureAccessKeyId, } - _, err = repo.secureAccessKeyQueryRepo.ReadFirst(readFirstRequestDto) + keyToDelete, err := repo.secureAccessKeyQueryRepo.ReadFirst(readFirstRequestDto) if err != nil { return errors.New("SecureAccessKeyNotFound") } + account, err := repo.accountQueryRepo.ReadById(keyToDelete.AccountId) + if err != nil { + return errors.New("AccountNotFound") + } + err = repo.persistentDbSvc.Handler.Delete( - dbModel.SecureAccessKey{}, deleteDto.Id.Uint16(), + dbModel.SecureAccessKey{}, secureAccessKeyId.Uint16(), ).Error if err != nil { return err diff --git a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo_test.go b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo_test.go index a5d8baa43..970c1f3bc 100644 --- a/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo_test.go +++ b/src/infra/account/secureAccessKey/secureAccessKeyCmdRepo_test.go @@ -51,9 +51,7 @@ func TestSecureAccessKeyCmdRepo(t *testing.T) { }) t.Run("DeleteSecureAccessKey", func(t *testing.T) { - deleteDto := dto.NewDeleteSecureAccessKey(keyId, accountId, accountId, ipAddress) - - err = secureAccessKeyCmdRepo.Delete(deleteDto) + err = secureAccessKeyCmdRepo.Delete(keyId) if err != nil { t.Fatalf( "Expected no error for %s, but got %s", keyName.String(), err.Error(), diff --git a/src/infra/internalDatabase/model/account.go b/src/infra/internalDatabase/model/account.go index f65041444..ee12f9fe4 100644 --- a/src/infra/internalDatabase/model/account.go +++ b/src/infra/internalDatabase/model/account.go @@ -46,8 +46,17 @@ func (model Account) ToEntity() (accountEntity entity.Account, err error) { return accountEntity, err } + secureAccessKeys := []entity.SecureAccessKey{} + for _, secureAccessKeyModel := range model.SecureAccessKeys { + secureAccessKeyEntity, err := secureAccessKeyModel.ToEntity() + if err != nil { + return accountEntity, err + } + secureAccessKeys = append(secureAccessKeys, secureAccessKeyEntity) + } + return entity.NewAccount( - accountId, groupId, username, + accountId, groupId, username, secureAccessKeys, valueObject.NewUnixTimeWithGoTime(model.CreatedAt), valueObject.NewUnixTimeWithGoTime(model.UpdatedAt), ), nil diff --git a/src/infra/internalDatabase/model/secureAccessKey.go b/src/infra/internalDatabase/model/secureAccessKey.go index a746d33e7..cb27c733e 100644 --- a/src/infra/internalDatabase/model/secureAccessKey.go +++ b/src/infra/internalDatabase/model/secureAccessKey.go @@ -18,7 +18,7 @@ type SecureAccessKey struct { } func (SecureAccessKey) TableName() string { - return "secure_access_key" + return "secure_access_keys" } func NewSecureAccessKey( diff --git a/src/presentation/api/controller/account.go b/src/presentation/api/controller/account.go index ee00d8916..012fdd05e 100644 --- a/src/presentation/api/controller/account.go +++ b/src/presentation/api/controller/account.go @@ -28,10 +28,25 @@ func NewAccountController( // @Accept json // @Produce json // @Security Bearer -// @Success 200 {array} entity.Account +// @Param id query string false "Id" +// @Param username query string false "Username" +// @Param shouldIncludeSecureAccessKeys query bool false "ShouldIncludeSecureAccessKeys (this prop only works if OpenSSH service is installed)" +// @Param pageNumber query uint false "PageNumber (Pagination)" +// @Param itemsPerPage query uint false "ItemsPerPage (Pagination)" +// @Param sortBy query string false "SortBy (Pagination)" +// @Param sortDirection query string false "SortDirection (Pagination)" +// @Param lastSeenId query string false "LastSeenId (Pagination)" +// @Success 200 {object} dto.ReadAccountsResponse // @Router /v1/account/ [get] func (controller *AccountController) Read(c echo.Context) error { - return apiHelper.ServiceResponseWrapper(c, controller.accountService.Read()) + requestBody, err := apiHelper.ReadRequestBody(c) + if err != nil { + return err + } + + return apiHelper.ServiceResponseWrapper( + c, controller.accountService.Read(requestBody), + ) } // CreateAccount godoc @@ -106,34 +121,6 @@ func (controller *AccountController) Delete(c echo.Context) error { ) } -// ReadSecureAccessKeys godoc -// @Summary ReadSecureAccessKeys -// @Description List accounts secure access keys. -// @Tags account -// @Accept json -// @Produce json -// @Security Bearer -// @Param accountId query uint true "AccountId that keys belongs to" -// @Param id query string false "Id" -// @Param name query string false "Name" -// @Param pageNumber query uint false "PageNumber (Pagination)" -// @Param itemsPerPage query uint false "ItemsPerPage (Pagination)" -// @Param sortBy query string false "SortBy (Pagination)" -// @Param sortDirection query string false "SortDirection (Pagination)" -// @Param lastSeenId query string false "LastSeenId (Pagination)" -// @Success 200 {object} dto.ReadSecureAccessKeysResponse -// @Router /v1/account/{accountId}/secure-access-key/ [get] -func (controller *AccountController) ReadSecureAccessKey(c echo.Context) error { - requestBody, err := apiHelper.ReadRequestBody(c) - if err != nil { - return err - } - - return apiHelper.ServiceResponseWrapper( - c, controller.accountService.ReadSecureAccessKey(requestBody), - ) -} - // CreateSecureAccessKey godoc // @Summary CreateSecureAccessKey // @Description Create a new secure access key. @@ -141,10 +128,9 @@ func (controller *AccountController) ReadSecureAccessKey(c echo.Context) error { // @Accept json // @Produce json // @Security Bearer -// @Param accountId path string true "AccountId to create secure access key." // @Param createSecureAccessKey body dto.CreateSecureAccessKey true "Only 'content' is required.
'name' will only become required if there is no name in 'content'. If the 'name' is provided, it will overwrite the name in the 'content'." // @Success 201 {object} object{} "SecureAccessKeyCreated" -// @Router /v1/account/{accountId}/secure-access-key/ [post] +// @Router /v1/account/secure-access-key/ [post] func (controller *AccountController) CreateSecureAccessKey(c echo.Context) error { requestBody, err := apiHelper.ReadRequestBody(c) if err != nil { @@ -163,10 +149,9 @@ func (controller *AccountController) CreateSecureAccessKey(c echo.Context) error // @Accept json // @Produce json // @Security Bearer -// @Param accountId path string true "AccountId that keys belongs to." // @Param secureAccessKeyId path string true "SecureAccessKeyId to delete." // @Success 200 {object} object{} "SecureAccessKeyDeleted" -// @Router /v1/account/{accountId}/secure-access-key/{secureAccessKeyId}/ [delete] +// @Router /v1/account/secure-access-key/{secureAccessKeyId}/ [delete] func (controller *AccountController) DeleteSecureAccessKey(c echo.Context) error { requestBody, err := apiHelper.ReadRequestBody(c) if err != nil { diff --git a/src/presentation/api/router.go b/src/presentation/api/router.go index 0dc35562b..5d6bb7251 100644 --- a/src/presentation/api/router.go +++ b/src/presentation/api/router.go @@ -58,8 +58,7 @@ func (router Router) accountRoutes() { accountGroup.PUT("/", accountController.Update) accountGroup.DELETE("/:accountId/", accountController.Delete) - secureAccessKeyGroup := accountGroup.Group("/:accountId/secure-access-key") - secureAccessKeyGroup.GET("/", accountController.ReadSecureAccessKey) + secureAccessKeyGroup := accountGroup.Group("/secure-access-key") secureAccessKeyGroup.POST("/", accountController.CreateSecureAccessKey) secureAccessKeyGroup.DELETE("/:secureAccessKeyId/", accountController.DeleteSecureAccessKey) } diff --git a/src/presentation/cli/controller/account.go b/src/presentation/cli/controller/account.go index 766158a2e..1453ee758 100644 --- a/src/presentation/cli/controller/account.go +++ b/src/presentation/cli/controller/account.go @@ -21,14 +21,80 @@ func NewAccountController( } func (controller *AccountController) Read() *cobra.Command { + var accountIdUint64 uint64 + var accountUsernameStr, shouldIncludeSecureAccessKeysStr string + var paginationPageNumberUint32 uint32 + var paginationItemsPerPageUint16 uint16 + var paginationSortByStr, paginationSortDirectionStr, paginationLastSeenIdStr string + cmd := &cobra.Command{ Use: "get", Short: "GetAccounts", Run: func(cmd *cobra.Command, args []string) { - cliHelper.ServiceResponseWrapper(controller.accountService.Read()) + requestBody := map[string]interface{}{ + "shouldIncludeSecureAccessKeys": shouldIncludeSecureAccessKeysStr, + } + + if accountIdUint64 != 0 { + requestBody["id"] = accountIdUint64 + } + + if accountUsernameStr != "" { + requestBody["username"] = accountUsernameStr + } + + if paginationPageNumberUint32 != 0 { + requestBody["pageNumber"] = paginationPageNumberUint32 + } + + if paginationItemsPerPageUint16 != 0 { + requestBody["itemsPerPage"] = paginationItemsPerPageUint16 + } + + if paginationSortByStr != "" { + requestBody["sortBy"] = paginationSortByStr + } + + if paginationSortDirectionStr != "" { + requestBody["sortDirection"] = paginationSortDirectionStr + } + + if paginationLastSeenIdStr != "" { + requestBody["lastSeenId"] = paginationLastSeenIdStr + } + + cliHelper.ServiceResponseWrapper( + controller.accountService.Read(requestBody), + ) }, } + cmd.Flags().Uint64VarP(&accountIdUint64, "account-id", "i", 0, "AccountId") + cmd.Flags().StringVarP( + &accountUsernameStr, "account-username", "n", "", "AccountUsername", + ) + cmd.Flags().StringVarP( + &shouldIncludeSecureAccessKeysStr, "should-include-secure-access-keys", + "s", "false", "ShouldIncludeSecureAccessKeys", + ) + cmd.Flags().Uint32VarP( + &paginationPageNumberUint32, "page-number", "p", 0, "PageNumber (Pagination)", + ) + cmd.Flags().Uint16VarP( + &paginationItemsPerPageUint16, "items-per-page", "m", 0, + "ItemsPerPage (Pagination)", + ) + cmd.Flags().StringVarP( + &paginationSortByStr, "sort-by", "y", "", "SortBy (Pagination)", + ) + cmd.Flags().StringVarP( + &paginationSortDirectionStr, "sort-direction", "r", "", + "SortDirection (Pagination)", + ) + cmd.Flags().StringVarP( + &paginationLastSeenIdStr, "last-seen-id", "l", "", "LastSeenId (Pagination)", + ) + return cmd } @@ -118,28 +184,6 @@ func (controller *AccountController) Delete() *cobra.Command { return cmd } -func (controller *AccountController) ReadSecureAccessKeys() *cobra.Command { - var accountIdUint64 uint64 - - cmd := &cobra.Command{ - Use: "get-keys", - Short: "GetSecureAccessKeys", - Run: func(cmd *cobra.Command, args []string) { - requestBody := map[string]interface{}{ - "accountId": accountIdUint64, - } - - cliHelper.ServiceResponseWrapper( - controller.accountService.ReadSecureAccessKey(requestBody), - ) - }, - } - - cmd.Flags().Uint64VarP(&accountIdUint64, "account-id", "u", 0, "AccountId") - cmd.MarkFlagRequired("account-id") - return cmd -} - func (controller *AccountController) CreateSecureAccessKey() *cobra.Command { var accountIdUint64 uint64 var keyNameStr, keyContentStr string diff --git a/src/presentation/cli/router.go b/src/presentation/cli/router.go index ebf48c9d4..8031c2621 100644 --- a/src/presentation/cli/router.go +++ b/src/presentation/cli/router.go @@ -52,7 +52,6 @@ func (router Router) accountRoutes() { accountCmd.AddCommand(accountController.Create()) accountCmd.AddCommand(accountController.Update()) accountCmd.AddCommand(accountController.Delete()) - accountCmd.AddCommand(accountController.ReadSecureAccessKeys()) accountCmd.AddCommand(accountController.CreateSecureAccessKey()) accountCmd.AddCommand(accountController.DeleteSecureAccessKey()) } diff --git a/src/presentation/service/account.go b/src/presentation/service/account.go index d805320e6..2a4a65837 100644 --- a/src/presentation/service/account.go +++ b/src/presentation/service/account.go @@ -49,8 +49,91 @@ func NewAccountService( } } -func (service *AccountService) Read() ServiceOutput { - accountsList, err := useCase.ReadAccounts(service.accountQueryRepo) +func (service *AccountService) Read(input map[string]interface{}) ServiceOutput { + if input["id"] != nil { + input["accountId"] = input["id"] + } + + var idPtr *valueObject.AccountId + if input["id"] != nil { + id, err := valueObject.NewAccountId(input["id"]) + if err != nil { + return NewServiceOutput(UserError, err) + } + idPtr = &id + } + + var usernamePtr *valueObject.Username + if input["name"] != nil { + username, err := valueObject.NewUsername(input["username"]) + if err != nil { + return NewServiceOutput(UserError, err) + } + usernamePtr = &username + } + + shouldIncludeSecureAccessKeys := false + if input["shouldIncludeSecureAccessKeys"] != nil { + var err error + shouldIncludeSecureAccessKeys, err = voHelper.InterfaceToBool( + input["shouldIncludeSecureAccessKeys"], + ) + if err != nil { + return NewServiceOutput(UserError, err) + } + } + + paginationDto := useCase.MarketplaceDefaultPagination + if input["pageNumber"] != nil { + pageNumber, err := voHelper.InterfaceToUint32(input["pageNumber"]) + if err != nil { + return NewServiceOutput(UserError, errors.New("InvalidPageNumber")) + } + paginationDto.PageNumber = pageNumber + } + + if input["itemsPerPage"] != nil { + itemsPerPage, err := voHelper.InterfaceToUint16(input["itemsPerPage"]) + if err != nil { + return NewServiceOutput(UserError, errors.New("InvalidItemsPerPage")) + } + paginationDto.ItemsPerPage = itemsPerPage + } + + if input["sortBy"] != nil { + sortBy, err := valueObject.NewPaginationSortBy(input["sortBy"]) + if err != nil { + return NewServiceOutput(UserError, err) + } + paginationDto.SortBy = &sortBy + } + + if input["sortDirection"] != nil { + sortDirection, err := valueObject.NewPaginationSortDirection( + input["sortDirection"], + ) + if err != nil { + return NewServiceOutput(UserError, err) + } + paginationDto.SortDirection = &sortDirection + } + + if input["lastSeenId"] != nil { + lastSeenId, err := valueObject.NewPaginationLastSeenId(input["lastSeenId"]) + if err != nil { + return NewServiceOutput(UserError, err) + } + paginationDto.LastSeenId = &lastSeenId + } + + readRequestDto := dto.ReadAccountsRequest{ + Pagination: paginationDto, + AccountId: idPtr, + AccountUsername: usernamePtr, + ShouldIncludeSecureAccessKeys: &shouldIncludeSecureAccessKeys, + } + + accountsList, err := useCase.ReadAccounts(service.accountQueryRepo, readRequestDto) if err != nil { return NewServiceOutput(InfraError, err.Error()) } @@ -224,107 +307,6 @@ func (service *AccountService) Delete(input map[string]interface{}) ServiceOutpu return NewServiceOutput(Success, "AccountDeleted") } -func (service *AccountService) ReadSecureAccessKey( - input map[string]interface{}, -) ServiceOutput { - serviceName, _ := valueObject.NewServiceName("openssh") - if !service.availabilityInspector.IsAvailable(serviceName) { - return NewServiceOutput(InfraError, sharedHelper.ServiceUnavailableError) - } - - if input["id"] != nil { - input["accountId"] = input["id"] - } - - requiredParams := []string{"accountId"} - err := serviceHelper.RequiredParamsInspector(input, requiredParams) - if err != nil { - return NewServiceOutput(UserError, err.Error()) - } - - accountId, err := valueObject.NewAccountId(input["accountId"]) - if err != nil { - return NewServiceOutput(UserError, err.Error()) - } - - var idPtr *valueObject.SecureAccessKeyId - if input["id"] != nil { - id, err := valueObject.NewSecureAccessKeyId(input["id"]) - if err != nil { - return NewServiceOutput(UserError, err) - } - idPtr = &id - } - - var namePtr *valueObject.SecureAccessKeyName - if input["name"] != nil { - name, err := valueObject.NewSecureAccessKeyName(input["name"]) - if err != nil { - return NewServiceOutput(UserError, err) - } - namePtr = &name - } - - paginationDto := useCase.MarketplaceDefaultPagination - if input["pageNumber"] != nil { - pageNumber, err := voHelper.InterfaceToUint32(input["pageNumber"]) - if err != nil { - return NewServiceOutput(UserError, errors.New("InvalidPageNumber")) - } - paginationDto.PageNumber = pageNumber - } - - if input["itemsPerPage"] != nil { - itemsPerPage, err := voHelper.InterfaceToUint16(input["itemsPerPage"]) - if err != nil { - return NewServiceOutput(UserError, errors.New("InvalidItemsPerPage")) - } - paginationDto.ItemsPerPage = itemsPerPage - } - - if input["sortBy"] != nil { - sortBy, err := valueObject.NewPaginationSortBy(input["sortBy"]) - if err != nil { - return NewServiceOutput(UserError, err) - } - paginationDto.SortBy = &sortBy - } - - if input["sortDirection"] != nil { - sortDirection, err := valueObject.NewPaginationSortDirection( - input["sortDirection"], - ) - if err != nil { - return NewServiceOutput(UserError, err) - } - paginationDto.SortDirection = &sortDirection - } - - if input["lastSeenId"] != nil { - lastSeenId, err := valueObject.NewPaginationLastSeenId(input["lastSeenId"]) - if err != nil { - return NewServiceOutput(UserError, err) - } - paginationDto.LastSeenId = &lastSeenId - } - - readRequestDto := dto.ReadSecureAccessKeysRequest{ - Pagination: paginationDto, - AccountId: accountId, - SecureAccessKeyId: idPtr, - SecureAccessKeyName: namePtr, - } - - secureAccessKeys, err := useCase.ReadSecureAccessKeys( - service.secureAccessKeyQueryRepo, readRequestDto, - ) - if err != nil { - return NewServiceOutput(InfraError, err.Error()) - } - - return NewServiceOutput(Success, secureAccessKeys) -} - func (service *AccountService) CreateSecureAccessKey( input map[string]interface{}, ) ServiceOutput { @@ -395,7 +377,7 @@ func (service *AccountService) DeleteSecureAccessKey( return NewServiceOutput(InfraError, sharedHelper.ServiceUnavailableError) } - requiredParams := []string{"accountId", "secureAccessKeyId"} + requiredParams := []string{"secureAccessKeyId"} err := serviceHelper.RequiredParamsInspector(input, requiredParams) if err != nil { return NewServiceOutput(UserError, err.Error()) @@ -406,11 +388,6 @@ func (service *AccountService) DeleteSecureAccessKey( return NewServiceOutput(UserError, err.Error()) } - accountId, err := valueObject.NewAccountId(input["accountId"]) - if err != nil { - return NewServiceOutput(UserError, err.Error()) - } - operatorAccountId := LocalOperatorAccountId if input["operatorAccountId"] != nil { operatorAccountId, err = valueObject.NewAccountId(input["operatorAccountId"]) @@ -428,11 +405,12 @@ func (service *AccountService) DeleteSecureAccessKey( } deleteDto := dto.NewDeleteSecureAccessKey( - keyId, accountId, operatorAccountId, operatorIpAddress, + keyId, operatorAccountId, operatorIpAddress, ) err = useCase.DeleteSecureAccessKey( - service.secureAccessKeyCmdRepo, service.activityRecordCmdRepo, deleteDto, + service.secureAccessKeyQueryRepo, service.secureAccessKeyCmdRepo, + service.activityRecordCmdRepo, deleteDto, ) if err != nil { return NewServiceOutput(InfraError, err.Error()) diff --git a/src/presentation/ui/presenter/accounts.go b/src/presentation/ui/presenter/accounts.go index 448de9e8a..b14bf627f 100644 --- a/src/presentation/ui/presenter/accounts.go +++ b/src/presentation/ui/presenter/accounts.go @@ -25,7 +25,11 @@ func NewAccountsPresenter( } func (presenter *AccountsPresenter) Handler(c echo.Context) error { - responseOutput := presenter.accountService.Read() + responseOutput := presenter.accountService.Read( + map[string]interface{}{ + "shouldIncludeSecureAccessKeys": true, + }, + ) if responseOutput.Status != service.Success { return nil } From d87b2cc7b059f5235cde6ef16b2c45d5c76b0dbb Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Thu, 28 Nov 2024 21:32:52 -0300 Subject: [PATCH 050/100] chore: update swagger --- src/presentation/api/docs/docs.go | 188 ++++++++++++++----------- src/presentation/api/docs/swagger.json | 188 ++++++++++++++----------- src/presentation/api/docs/swagger.yaml | 97 +++++++------ 3 files changed, 269 insertions(+), 204 deletions(-) diff --git a/src/presentation/api/docs/docs.go b/src/presentation/api/docs/docs.go index 5d5cdd8dd..b402b5a6b 100644 --- a/src/presentation/api/docs/docs.go +++ b/src/presentation/api/docs/docs.go @@ -42,14 +42,61 @@ const docTemplate = `{ "account" ], "summary": "ReadAccounts", + "parameters": [ + { + "type": "string", + "description": "Id", + "name": "id", + "in": "query" + }, + { + "type": "string", + "description": "Username", + "name": "username", + "in": "query" + }, + { + "type": "boolean", + "description": "ShouldIncludeSecureAccessKeys (this prop only works if OpenSSH service is installed)", + "name": "shouldIncludeSecureAccessKeys", + "in": "query" + }, + { + "type": "integer", + "description": "PageNumber (Pagination)", + "name": "pageNumber", + "in": "query" + }, + { + "type": "integer", + "description": "ItemsPerPage (Pagination)", + "name": "itemsPerPage", + "in": "query" + }, + { + "type": "string", + "description": "SortBy (Pagination)", + "name": "sortBy", + "in": "query" + }, + { + "type": "string", + "description": "SortDirection (Pagination)", + "name": "sortDirection", + "in": "query" + }, + { + "type": "string", + "description": "LastSeenId (Pagination)", + "name": "lastSeenId", + "in": "query" + } + ], "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/entity.Account" - } + "$ref": "#/definitions/dto.ReadAccountsResponse" } } } @@ -129,14 +176,14 @@ const docTemplate = `{ } } }, - "/v1/account/{accountId}/": { - "delete": { + "/v1/account/secure-access-key/": { + "post": { "security": [ { "Bearer": [] } ], - "description": "Delete an account.", + "description": "Create a new secure access key.", "consumes": [ "application/json" ], @@ -146,19 +193,21 @@ const docTemplate = `{ "tags": [ "account" ], - "summary": "DeleteAccount", + "summary": "CreateSecureAccessKey", "parameters": [ { - "type": "string", - "description": "AccountId to delete.", - "name": "accountId", - "in": "path", - "required": true + "description": "Only 'content' is required.\u003cbr /\u003e'name' will only become required if there is no name in 'content'. If the 'name' is provided, it will overwrite the name in the 'content'.", + "name": "createSecureAccessKey", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateSecureAccessKey" + } } ], "responses": { - "200": { - "description": "AccountDeleted", + "201": { + "description": "SecureAccessKeyCreated", "schema": { "type": "object" } @@ -166,14 +215,14 @@ const docTemplate = `{ } } }, - "/v1/account/{accountId}/secure-access-key/": { - "get": { + "/v1/account/secure-access-key/{secureAccessKeyId}/": { + "delete": { "security": [ { "Bearer": [] } ], - "description": "List accounts secure access keys.", + "description": "Delete a secure access key.", "consumes": [ "application/json" ], @@ -183,66 +232,19 @@ const docTemplate = `{ "tags": [ "account" ], - "summary": "ReadSecureAccessKeys", + "summary": "DeleteSecureAccessKey", "parameters": [ { "type": "string", - "description": "AccountId that keys belongs to.", - "name": "accountId", + "description": "SecureAccessKeyId to delete.", + "name": "secureAccessKeyId", "in": "path", "required": true } ], "responses": { "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/entity.SecureAccessKey" - } - } - } - } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Create a new secure access key.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "account" - ], - "summary": "CreateSecureAccessKey", - "parameters": [ - { - "type": "string", - "description": "AccountId to create secure access key.", - "name": "accountId", - "in": "path", - "required": true - }, - { - "description": "Only 'content' is required.\u003cbr /\u003e'name' will only become required if there is no name in 'content'. If the 'name' is provided, it will overwrite the name in the 'content'.", - "name": "createSecureAccessKey", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/dto.CreateSecureAccessKey" - } - } - ], - "responses": { - "201": { - "description": "SecureAccessKeyCreated", + "description": "SecureAccessKeyDeleted", "schema": { "type": "object" } @@ -250,14 +252,14 @@ const docTemplate = `{ } } }, - "/v1/account/{accountId}/secure-access-key/{secureAccessKeyId}/": { + "/v1/account/{accountId}/": { "delete": { "security": [ { "Bearer": [] } ], - "description": "Delete a secure access key.", + "description": "Delete an account.", "consumes": [ "application/json" ], @@ -267,26 +269,19 @@ const docTemplate = `{ "tags": [ "account" ], - "summary": "DeleteSecureAccessKey", + "summary": "DeleteAccount", "parameters": [ { "type": "string", - "description": "AccountId that keys belongs to.", + "description": "AccountId to delete.", "name": "accountId", "in": "path", "required": true - }, - { - "type": "string", - "description": "SecureAccessKeyId to delete.", - "name": "secureAccessKeyId", - "in": "path", - "required": true } ], "responses": { "200": { - "description": "SecureAccessKeyDeleted", + "description": "AccountDeleted", "schema": { "type": "object" } @@ -2739,6 +2734,20 @@ const docTemplate = `{ } } }, + "dto.ReadAccountsResponse": { + "type": "object", + "properties": { + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Account" + } + }, + "pagination": { + "$ref": "#/definitions/dto.Pagination" + } + } + }, "dto.ReadInstallableServicesItemsResponse": { "type": "object", "properties": { @@ -3041,6 +3050,12 @@ const docTemplate = `{ "id": { "type": "integer" }, + "secureAccessKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.SecureAccessKey" + } + }, "updatedAt": { "type": "integer" }, @@ -3536,7 +3551,13 @@ const docTemplate = `{ "entity.SecureAccessKey": { "type": "object", "properties": { - "encodedContent": { + "accountId": { + "type": "integer" + }, + "createdAt": { + "type": "integer" + }, + "fingerprint": { "type": "string" }, "id": { @@ -3544,6 +3565,9 @@ const docTemplate = `{ }, "name": { "type": "string" + }, + "updatedAt": { + "type": "integer" } } }, diff --git a/src/presentation/api/docs/swagger.json b/src/presentation/api/docs/swagger.json index 4de0bee84..b8da155e5 100644 --- a/src/presentation/api/docs/swagger.json +++ b/src/presentation/api/docs/swagger.json @@ -36,14 +36,61 @@ "account" ], "summary": "ReadAccounts", + "parameters": [ + { + "type": "string", + "description": "Id", + "name": "id", + "in": "query" + }, + { + "type": "string", + "description": "Username", + "name": "username", + "in": "query" + }, + { + "type": "boolean", + "description": "ShouldIncludeSecureAccessKeys (this prop only works if OpenSSH service is installed)", + "name": "shouldIncludeSecureAccessKeys", + "in": "query" + }, + { + "type": "integer", + "description": "PageNumber (Pagination)", + "name": "pageNumber", + "in": "query" + }, + { + "type": "integer", + "description": "ItemsPerPage (Pagination)", + "name": "itemsPerPage", + "in": "query" + }, + { + "type": "string", + "description": "SortBy (Pagination)", + "name": "sortBy", + "in": "query" + }, + { + "type": "string", + "description": "SortDirection (Pagination)", + "name": "sortDirection", + "in": "query" + }, + { + "type": "string", + "description": "LastSeenId (Pagination)", + "name": "lastSeenId", + "in": "query" + } + ], "responses": { "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/entity.Account" - } + "$ref": "#/definitions/dto.ReadAccountsResponse" } } } @@ -123,14 +170,14 @@ } } }, - "/v1/account/{accountId}/": { - "delete": { + "/v1/account/secure-access-key/": { + "post": { "security": [ { "Bearer": [] } ], - "description": "Delete an account.", + "description": "Create a new secure access key.", "consumes": [ "application/json" ], @@ -140,19 +187,21 @@ "tags": [ "account" ], - "summary": "DeleteAccount", + "summary": "CreateSecureAccessKey", "parameters": [ { - "type": "string", - "description": "AccountId to delete.", - "name": "accountId", - "in": "path", - "required": true + "description": "Only 'content' is required.\u003cbr /\u003e'name' will only become required if there is no name in 'content'. If the 'name' is provided, it will overwrite the name in the 'content'.", + "name": "createSecureAccessKey", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateSecureAccessKey" + } } ], "responses": { - "200": { - "description": "AccountDeleted", + "201": { + "description": "SecureAccessKeyCreated", "schema": { "type": "object" } @@ -160,14 +209,14 @@ } } }, - "/v1/account/{accountId}/secure-access-key/": { - "get": { + "/v1/account/secure-access-key/{secureAccessKeyId}/": { + "delete": { "security": [ { "Bearer": [] } ], - "description": "List accounts secure access keys.", + "description": "Delete a secure access key.", "consumes": [ "application/json" ], @@ -177,66 +226,19 @@ "tags": [ "account" ], - "summary": "ReadSecureAccessKeys", + "summary": "DeleteSecureAccessKey", "parameters": [ { "type": "string", - "description": "AccountId that keys belongs to.", - "name": "accountId", + "description": "SecureAccessKeyId to delete.", + "name": "secureAccessKeyId", "in": "path", "required": true } ], "responses": { "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/entity.SecureAccessKey" - } - } - } - } - }, - "post": { - "security": [ - { - "Bearer": [] - } - ], - "description": "Create a new secure access key.", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "account" - ], - "summary": "CreateSecureAccessKey", - "parameters": [ - { - "type": "string", - "description": "AccountId to create secure access key.", - "name": "accountId", - "in": "path", - "required": true - }, - { - "description": "Only 'content' is required.\u003cbr /\u003e'name' will only become required if there is no name in 'content'. If the 'name' is provided, it will overwrite the name in the 'content'.", - "name": "createSecureAccessKey", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/dto.CreateSecureAccessKey" - } - } - ], - "responses": { - "201": { - "description": "SecureAccessKeyCreated", + "description": "SecureAccessKeyDeleted", "schema": { "type": "object" } @@ -244,14 +246,14 @@ } } }, - "/v1/account/{accountId}/secure-access-key/{secureAccessKeyId}/": { + "/v1/account/{accountId}/": { "delete": { "security": [ { "Bearer": [] } ], - "description": "Delete a secure access key.", + "description": "Delete an account.", "consumes": [ "application/json" ], @@ -261,26 +263,19 @@ "tags": [ "account" ], - "summary": "DeleteSecureAccessKey", + "summary": "DeleteAccount", "parameters": [ { "type": "string", - "description": "AccountId that keys belongs to.", + "description": "AccountId to delete.", "name": "accountId", "in": "path", "required": true - }, - { - "type": "string", - "description": "SecureAccessKeyId to delete.", - "name": "secureAccessKeyId", - "in": "path", - "required": true } ], "responses": { "200": { - "description": "SecureAccessKeyDeleted", + "description": "AccountDeleted", "schema": { "type": "object" } @@ -2733,6 +2728,20 @@ } } }, + "dto.ReadAccountsResponse": { + "type": "object", + "properties": { + "accounts": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Account" + } + }, + "pagination": { + "$ref": "#/definitions/dto.Pagination" + } + } + }, "dto.ReadInstallableServicesItemsResponse": { "type": "object", "properties": { @@ -3035,6 +3044,12 @@ "id": { "type": "integer" }, + "secureAccessKeys": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.SecureAccessKey" + } + }, "updatedAt": { "type": "integer" }, @@ -3530,7 +3545,13 @@ "entity.SecureAccessKey": { "type": "object", "properties": { - "encodedContent": { + "accountId": { + "type": "integer" + }, + "createdAt": { + "type": "integer" + }, + "fingerprint": { "type": "string" }, "id": { @@ -3538,6 +3559,9 @@ }, "name": { "type": "string" + }, + "updatedAt": { + "type": "integer" } } }, diff --git a/src/presentation/api/docs/swagger.yaml b/src/presentation/api/docs/swagger.yaml index 7e0689068..0adcb8215 100644 --- a/src/presentation/api/docs/swagger.yaml +++ b/src/presentation/api/docs/swagger.yaml @@ -311,6 +311,15 @@ definitions: sortDirection: type: string type: object + dto.ReadAccountsResponse: + properties: + accounts: + items: + $ref: '#/definitions/entity.Account' + type: array + pagination: + $ref: '#/definitions/dto.Pagination' + type: object dto.ReadInstallableServicesItemsResponse: properties: installableServices: @@ -508,6 +517,10 @@ definitions: type: integer id: type: integer + secureAccessKeys: + items: + $ref: '#/definitions/entity.SecureAccessKey' + type: array updatedAt: type: integer username: @@ -833,12 +846,18 @@ definitions: type: object entity.SecureAccessKey: properties: - encodedContent: + accountId: + type: integer + createdAt: + type: integer + fingerprint: type: string id: type: integer name: type: string + updatedAt: + type: integer type: object entity.SslCertificate: properties: @@ -1025,15 +1044,47 @@ paths: consumes: - application/json description: List accounts. + parameters: + - description: Id + in: query + name: id + type: string + - description: Username + in: query + name: username + type: string + - description: ShouldIncludeSecureAccessKeys (this prop only works if OpenSSH + service is installed) + in: query + name: shouldIncludeSecureAccessKeys + type: boolean + - description: PageNumber (Pagination) + in: query + name: pageNumber + type: integer + - description: ItemsPerPage (Pagination) + in: query + name: itemsPerPage + type: integer + - description: SortBy (Pagination) + in: query + name: sortBy + type: string + - description: SortDirection (Pagination) + in: query + name: sortDirection + type: string + - description: LastSeenId (Pagination) + in: query + name: lastSeenId + type: string produces: - application/json responses: "200": description: OK schema: - items: - $ref: '#/definitions/entity.Account' - type: array + $ref: '#/definitions/dto.ReadAccountsResponse' security: - Bearer: [] summary: ReadAccounts @@ -1108,41 +1159,12 @@ paths: summary: DeleteAccount tags: - account - /v1/account/{accountId}/secure-access-key/: - get: - consumes: - - application/json - description: List accounts secure access keys. - parameters: - - description: AccountId that keys belongs to. - in: path - name: accountId - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/entity.SecureAccessKey' - type: array - security: - - Bearer: [] - summary: ReadSecureAccessKeys - tags: - - account + /v1/account/secure-access-key/: post: consumes: - application/json description: Create a new secure access key. parameters: - - description: AccountId to create secure access key. - in: path - name: accountId - required: true - type: string - description: Only 'content' is required.
'name' will only become required if there is no name in 'content'. If the 'name' is provided, it will overwrite the name in the 'content'. @@ -1163,17 +1185,12 @@ paths: summary: CreateSecureAccessKey tags: - account - /v1/account/{accountId}/secure-access-key/{secureAccessKeyId}/: + /v1/account/secure-access-key/{secureAccessKeyId}/: delete: consumes: - application/json description: Delete a secure access key. parameters: - - description: AccountId that keys belongs to. - in: path - name: accountId - required: true - type: string - description: SecureAccessKeyId to delete. in: path name: secureAccessKeyId From 4fd6c78f1c7ec23f14a9358078b1fdd78fe59bb6 Mon Sep 17 00:00:00 2001 From: Matheus Marques Polillo Date: Fri, 29 Nov 2024 01:42:16 -0300 Subject: [PATCH 051/100] feat: implements DTO to InputField component with InfoTooltipContent --- .../ui/component/form/inputField.templ | 47 ++++++++++++------- .../ui/component/form/textArea.templ | 4 +- src/presentation/ui/page/databases.templ | 14 +++++- src/presentation/ui/page/mappings.templ | 42 ++++++++++++++--- .../phpRuntimeHorizontalTabContent.templ | 16 ++++--- 5 files changed, 90 insertions(+), 33 deletions(-) diff --git a/src/presentation/ui/component/form/inputField.templ b/src/presentation/ui/component/form/inputField.templ index c68ab7823..b69419aac 100644 --- a/src/presentation/ui/component/form/inputField.templ +++ b/src/presentation/ui/component/form/inputField.templ @@ -1,33 +1,46 @@ package componentForm -templ InputField( - inputType, id, label, bindValuePath string, - denseMode bool, -) { +type InputFieldDto struct { + Type string + Id string + Label string + BindModelValuePath string + InfoTooltipContent string +} + +templ InputField(inputDto InputFieldDto) {
- if label != "" { + if inputDto.InfoTooltipContent != "" { +
+ + +
+ } + if inputDto.Label != "" { }
diff --git a/src/presentation/ui/component/form/textArea.templ b/src/presentation/ui/component/form/textArea.templ index 6d07787d0..fca761d3b 100644 --- a/src/presentation/ui/component/form/textArea.templ +++ b/src/presentation/ui/component/form/textArea.templ @@ -17,10 +17,10 @@ templ TextArea(inputDto TextAreaDto) { if inputDto.BindModelValuePath != "" { x-model={ inputDto.BindModelValuePath } } - class="bg-os-300 border-os-200 hover:border-os-100 autofill:bg-os-300 focus:border-os-50 h-30 peer relative w-full rounded-md border px-3 pt-1.5 text-sm text-neutral-100 placeholder-transparent outline-none transition-all" + class="bg-os-300 border-os-200 hover:border-os-100 autofill:bg-os-300 focus:border-os-50 h-30 peer relative w-full rounded-md border px-3 pr-7 pt-1.5 text-sm text-neutral-100 placeholder-transparent outline-none transition-all" > if inputDto.InfoTooltipContent != "" { -
+
@componentForm.DeactivatableSubmitButton("Create", "ph-check-fat", "closeCreateVhostModal", "shouldDisableCreateVhostSubmitButton", false) @@ -316,7 +341,12 @@ templ CreateMappingForm(vhostsHostnames []string) {