diff --git a/TestClient.go b/TestClient.go index 327bc2b..a733829 100644 --- a/TestClient.go +++ b/TestClient.go @@ -259,6 +259,22 @@ func main() { // WARNING: Do not log secrets in production code, the following log statement logs test secrets for testing purposes: zapLogger.Debug(fmt.Sprintf("Created File secret: %v", createdSecret.Title)) + folderDetails := entities.FolderDetails{ + Name: "FOLDER_" + uuid.New().String(), + Description: "My Folder Secret Description", + } + + // creating a folder secret in folder1 folder. + createdFolder, err := secretObj.CreateFolderFlow("folder1", folderDetails) + + if err != nil { + zapLogger.Error(err.Error()) + return + } + + // WARNING: Do not log secrets in production code, the following log statement logs test secrets for testing purposes: + zapLogger.Debug(fmt.Sprintf("Created Folder: %v", createdFolder.Name)) + // signing out _ = authenticate.SignOut() diff --git a/api/authentication/authentication_test.go b/api/authentication/authentication_test.go index d3e8487..0cd2e42 100644 --- a/api/authentication/authentication_test.go +++ b/api/authentication/authentication_test.go @@ -118,7 +118,7 @@ func TestSignAppinWithApiKey(t *testing.T) { var authenticate, _ = AuthenticateUsingApiKey(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", zapLogger, 300, "fake_api_key_") testConfig := UserTestConfig{ - name: "TestSignAppin", + name: "TestSignAppinWithApiKey", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`)) if err != nil { diff --git a/api/entities/entities.go b/api/entities/entities.go index 0e0fdf0..6e8ac66 100644 --- a/api/entities/entities.go +++ b/api/entities/entities.go @@ -148,3 +148,18 @@ type UrlDetails struct { CredentialId uuid.UUID `json:",omitempty" validate:"omitempty,uuid"` Url string `json:",omitempty" validate:"required,max=2048,url"` } + +type CreateFolderResponse struct { + Id uuid.UUID `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ParentId uuid.UUID `json:"parentId"` + UserGroupId int `json:"userGroupId"` +} + +type FolderDetails struct { + Name string `json:",omitempty" validate:"required"` + Description string `json:",omitempty" validate:"omitempty,max=256"` + ParentId uuid.UUID `json:",omitempty" validate:"required"` + UserGroupId int `json:",omitempty" validate:"omitempty"` +} diff --git a/api/managed_account/managed_account_test.go b/api/managed_account/managed_account_test.go index 8ed3629..6cfdf1d 100644 --- a/api/managed_account/managed_account_test.go +++ b/api/managed_account/managed_account_test.go @@ -283,7 +283,7 @@ func TestManageAccountFlowNotFound(t *testing.T) { var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) testConfig := ManagedAccountTestConfigStringResponse{ - name: "TestManageAccountFlowFailedManagedAccounts", + name: "TestManageAccountFlowNotFound", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Mocking Response according to the endpoint path switch r.URL.Path { diff --git a/api/secrets/secrets.go b/api/secrets/secrets.go index 04099dd..45634a6 100644 --- a/api/secrets/secrets.go +++ b/api/secrets/secrets.go @@ -15,6 +15,7 @@ import ( "github.com/BeyondTrust/go-client-library-passwordsafe/api/entities" "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging" "github.com/BeyondTrust/go-client-library-passwordsafe/api/utils" + "github.com/google/uuid" backoff "github.com/cenkalti/backoff/v4" ) @@ -370,3 +371,101 @@ func (secretObj *SecretObj) SecretGetFolders(endpointPath string) ([]entities.Fo return foldersObj, nil } + +// CreateFolderFlow is responsible for creating folder in Password Safe. +func (secretObj *SecretObj) CreateFolderFlow(folderTarget string, folderDetails entities.FolderDetails) (entities.CreateFolderResponse, error) { + + var folder *entities.FolderResponse + var createFolderesponse entities.CreateFolderResponse + + folders, err := secretObj.SecretGetFolders("secrets-safe/folders/") + + if err != nil { + return createFolderesponse, err + } + + for _, v := range folders { + if v.Name == strings.TrimSpace(folderTarget) { + folder = &v + break + } + } + + if folder == nil { + return createFolderesponse, fmt.Errorf("folder %v was not found in folder list", folderTarget) + } + + folderId, _ := uuid.Parse(folder.Id) + folderDetails.ParentId = folderId + + folderDetails, err = utils.ValidateCreateFolderInput(folderDetails) + + if err != nil { + return createFolderesponse, err + } + + if err != nil { + return entities.CreateFolderResponse{}, err + } + + createFolderesponse, err = secretObj.SecretCreateFolder(folderDetails) + + if err != nil { + return createFolderesponse, err + } + + return createFolderesponse, nil +} + +// SecretCreateFolder calls Secret Safe API Requests enpoint to create folders in Password Safe. +func (secretObj *SecretObj) SecretCreateFolder(folderDetails entities.FolderDetails) (entities.CreateFolderResponse, error) { + + folderCredentialDetailsJson, err := json.Marshal(folderDetails) + + if err != nil { + return entities.CreateFolderResponse{}, err + } + + payload := string(folderCredentialDetailsJson) + b := bytes.NewBufferString(payload) + + var createSecretResponse entities.CreateFolderResponse + + SecretCreateSecreUrl := secretObj.authenticationObj.ApiUrl.JoinPath("secrets-safe/folders/").String() + messageLog := fmt.Sprintf("%v %v", "POST", SecretCreateSecreUrl) + secretObj.log.Debug(messageLog) + + var body io.ReadCloser + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + body, _, technicalError, businessError = secretObj.authenticationObj.HttpClient.CallSecretSafeAPI(SecretCreateSecreUrl, "POST", *b, "SecretCreateSecret", "", "", "application/json") + return technicalError + }, secretObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return entities.CreateFolderResponse{}, technicalError + } + + if businessError != nil { + return entities.CreateFolderResponse{}, businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return entities.CreateFolderResponse{}, err + } + + err = json.Unmarshal([]byte(bodyBytes), &createSecretResponse) + + if err != nil { + secretObj.log.Error(err.Error()) + return entities.CreateFolderResponse{}, err + } + + return createSecretResponse, nil + +} diff --git a/api/secrets/secrets_test.go b/api/secrets/secrets_test.go index 7450109..bbbe3d9 100644 --- a/api/secrets/secrets_test.go +++ b/api/secrets/secrets_test.go @@ -187,7 +187,7 @@ func TestSecretFlow_SecretNotFound(t *testing.T) { var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) testConfig := SecretTestConfigStringResponse{ - name: "TestSecretFlow", + name: "TestSecretFlow_SecretNotFound", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Mocking Response according to the endpoint path switch r.URL.Path { @@ -668,7 +668,7 @@ func TestSecretCreateTextSecretFlow(t *testing.T) { var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) testConfig := SecretTestConfigStringResponse{ - name: "TestSecretCreateSecretFlowFolderNotFound", + name: "TestSecretCreateTextSecretFlow", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Mocking Response according to the endpoint path switch r.URL.Path { @@ -740,7 +740,7 @@ func TestSecretCreateCredentialSecretFlow(t *testing.T) { var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) testConfig := SecretTestConfigStringResponse{ - name: "TestSecretCreateSecretFlowFolderNotFound", + name: "TestSecretCreateCredentialSecretFlow", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Mocking Response according to the endpoint path switch r.URL.Path { @@ -812,7 +812,7 @@ func TestSecretCreateFileSecretFlow(t *testing.T) { var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) testConfig := SecretTestConfigStringResponse{ - name: "TestSecretCreateSecretFlowFolderNotFound", + name: "TestSecretCreateFileSecretFlow", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Mocking Response according to the endpoint path switch r.URL.Path { @@ -884,7 +884,7 @@ func TestSecretCreateFileSecretFlowError(t *testing.T) { var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) testConfig := SecretTestConfigStringResponse{ - name: "TestSecretCreateSecretFlowFolderNotFound", + name: "TestSecretCreateFileSecretFlowError", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Mocking Response according to the endpoint path switch r.URL.Path { @@ -954,7 +954,7 @@ func TestSecretCreateBadInput(t *testing.T) { var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) testConfig := SecretTestConfigStringResponse{ - name: "TestSecretCreateSecretFlowFolderNotFound", + name: "TestSecretCreateBadInput", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Mocking Response according to the endpoint path switch r.URL.Path { @@ -1078,7 +1078,7 @@ func TestSecretCreateSecretFlowEmptyFolderList(t *testing.T) { var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) testConfig := SecretTestConfigStringResponse{ - name: "TestSecretCreateSecretFlowFolderNotFound", + name: "TestSecretCreateSecretFlowEmptyFolderList", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Mocking Response according to the endpoint path switch r.URL.Path { @@ -1123,3 +1123,115 @@ func TestSecretCreateSecretFlowEmptyFolderList(t *testing.T) { } } + +func TestSecretFolderFlow(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.MaxElapsedTime = time.Second + + var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := SecretTestConfigStringResponse{ + name: "TestSecretFolderFlow", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Mocking Response according to the endpoint path + if r.URL.Path == "/secrets-safe/folders/" && r.Method == "GET" { + _, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}, {"Id": "a4af73dc-4e89-41ec-eb9a-08dcf22d3aba","Name": "folder2"}]`)) + if err != nil { + t.Error("Test case Failed") + } + } + if r.URL.Path == "/secrets-safe/folders/" && r.Method == "POST" { + _, err := w.Write([]byte(`{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "Folder Title", "Description": "Folder Description"}`)) + if err != nil { + t.Error("Test case Failed") + } + } + })), + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + secretObj, _ := NewSecretObj(*authenticate, zapLogger, 4000) + + folderDetails := entities.FolderDetails{ + Name: "FOLDER_" + uuid.New().String(), + Description: "My Folder Description", + } + + response, err := secretObj.CreateFolderFlow("folder1", folderDetails) + + if response.Name != "Folder Title" { + t.Errorf("Test case Failed %v, %v", response.Name, "Folder Title") + } + + if response.Description != "Folder Description" { + t.Errorf("Test case Failed %v, %v", response.Description, "Folder Description") + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestSecretFolderFlowBadParentFolder(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.MaxElapsedTime = time.Second + + var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := SecretTestConfigStringResponse{ + name: "TestSecretFolderFlowBadParentFolder", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // Mocking Response according to the endpoint path + if r.URL.Path == "/secrets-safe/folders/" && r.Method == "GET" { + _, err := w.Write([]byte(`[{"Id": "cb871861-8b40-4556-820c-1ca6d522adfa","Name": "folder1"}, {"Id": "a4af73dc-4e89-41ec-eb9a-08dcf22d3aba","Name": "folder2"}]`)) + if err != nil { + t.Error("Test case Failed") + } + } else if r.URL.Path == "/secrets-safe/folders/" && r.Method == "POST" { + w.WriteHeader(http.StatusBadRequest) + _, err := w.Write([]byte(`{"error": "InvalidFolderName"}`)) + if err != nil { + t.Error("Test case Failed") + } + } else { + http.NotFound(w, r) + } + + })), + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + secretObj, _ := NewSecretObj(*authenticate, zapLogger, 4000) + + folderDetails := entities.FolderDetails{ + Name: "FOLDER_" + uuid.New().String(), + Description: "My Folder Description", + } + + _, err := secretObj.CreateFolderFlow("folder1", folderDetails) + + expetedErrorMessage := `error - status code: 400 - {"error": "InvalidFolderName"}` + + if err.Error() != expetedErrorMessage { + t.Errorf("Test case Failed %v, %v", err.Error(), expetedErrorMessage) + } + if err == nil { + t.Errorf("Test case Failed: %v", err) + } +} diff --git a/api/utils/validator.go b/api/utils/validator.go index c0c5783..cb79e5b 100644 --- a/api/utils/validator.go +++ b/api/utils/validator.go @@ -284,3 +284,15 @@ func formatErrorMessage(err validator.FieldError) string { return fmt.Sprintf("Error en el campo '%s': %s.", err.Field(), err.Tag()) } } + +// ValidateCreateFolderInput responsible for validating folder input. +func ValidateCreateFolderInput(folderDetails entities.FolderDetails) (entities.FolderDetails, error) { + validate := validator.New() + err := validate.Struct(folderDetails) + if err != nil { + for _, err := range err.(validator.ValidationErrors) { + return folderDetails, errors.New(formatErrorMessage(err)) + } + } + return folderDetails, nil +}