diff --git a/TestClient.go b/TestClient.go index ba5117a..327bc2b 100644 --- a/TestClient.go +++ b/TestClient.go @@ -10,6 +10,7 @@ import ( managed_accounts "github.com/BeyondTrust/go-client-library-passwordsafe/api/managed_account" "github.com/BeyondTrust/go-client-library-passwordsafe/api/secrets" "github.com/BeyondTrust/go-client-library-passwordsafe/api/utils" + "github.com/google/uuid" //"os" @@ -75,7 +76,7 @@ func main() { authenticate, _ := authentication.Authenticate(*httpClientObj, backoffDefinition, apiUrl, clientId, clientSecret, zapLogger, retryMaxElapsedTimeMinutes) // authenticating - _, err := authenticate.GetPasswordSafeAuthentication() + userObject, err := authenticate.GetPasswordSafeAuthentication() if err != nil { return } @@ -157,6 +158,107 @@ func main() { // WARNING: Do not log secrets in production code, the following log statement logs test secrets for testing purposes: zapLogger.Warn(fmt.Sprintf("Created Managed Account: %v", createResponse.AccountName)) + objCredential := entities.SecretCredentialDetails{ + Title: "CREDENTIAL_" + uuid.New().String(), + Description: "My Credential Secret Description", + Username: "my_user", + Password: "MyPass2#$!", + OwnerType: "User", + Notes: "My note", + Owners: []entities.OwnerDetails{ + { + OwnerId: userObject.UserId, + Owner: userObject.UserName, + Email: userObject.EmailAddress, + }, + }, + Urls: []entities.UrlDetails{ + { + Id: uuid.New(), + CredentialId: uuid.New(), + Url: "https://www.test.com/", + }, + }, + } + + // creating a credential secret in folder1. + createdSecret, err := secretObj.CreateSecretFlow("folder1", objCredential) + + 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 Credential secret: %v", createdSecret.Title)) + + objText := entities.SecretTextDetails{ + Title: "TEXT_" + uuid.New().String(), + Description: "My Text Secret Description", + Text: "my_p4ssword!*2024", + OwnerType: "User", + OwnerId: userObject.UserId, + FolderId: uuid.New(), + Owners: []entities.OwnerDetails{ + { + OwnerId: userObject.UserId, + Owner: userObject.UserName, + Email: userObject.EmailAddress, + }, + }, + Urls: []entities.UrlDetails{ + { + Id: uuid.New(), + CredentialId: uuid.New(), + Url: "https://www.test.com/", + }, + }, + } + + // creating a text secret in folder1. + createdSecret, err = secretObj.CreateSecretFlow("folder1", objText) + + 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 Text secret: %v", createdSecret.Title)) + + objFile := entities.SecretFileDetails{ + Title: "FILE_" + uuid.New().String(), + Description: "My File Secret Description", + OwnerType: "User", + OwnerId: userObject.UserId, + Owners: []entities.OwnerDetails{ + { + OwnerId: userObject.UserId, + Owner: userObject.UserName, + Email: userObject.EmailAddress, + }, + }, + Notes: "Notes 1", + FileName: "my_secret.txt", + FileContent: "my_p4ssword!*2024", + Urls: []entities.UrlDetails{ + { + Id: uuid.New(), + CredentialId: uuid.New(), + Url: "https://www.test.com/", + }, + }, + } + + // creating a file secret in folder1. + createdSecret, err = secretObj.CreateSecretFlow("folder1", objFile) + + 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 File secret: %v", createdSecret.Title)) + // signing out _ = authenticate.SignOut() diff --git a/api/authentication/authentication.go b/api/authentication/authentication.go index 9fb7ac8..0bf0dcd 100644 --- a/api/authentication/authentication.go +++ b/api/authentication/authentication.go @@ -99,7 +99,7 @@ func (authenticationObj *AuthenticationObj) GetToken(endpointUrl string, clientI buffer.WriteString(params.Encode()) technicalError = backoff.Retry(func() error { - body, _, technicalError, businessError = authenticationObj.HttpClient.CallSecretSafeAPI(endpointUrl, "POST", buffer, "GetToken", "", "") + body, _, technicalError, businessError = authenticationObj.HttpClient.CallSecretSafeAPI(endpointUrl, "POST", buffer, "GetToken", "", "", "application/json") return technicalError }, authenticationObj.ExponentialBackOff) @@ -144,7 +144,7 @@ func (authenticationObj *AuthenticationObj) SignAppin(endpointUrl string, access var scode int err := backoff.Retry(func() error { - body, scode, technicalError, businessError = authenticationObj.HttpClient.CallSecretSafeAPI(endpointUrl, "POST", bytes.Buffer{}, "SignAppin", accessToken, apiKey) + body, scode, technicalError, businessError = authenticationObj.HttpClient.CallSecretSafeAPI(endpointUrl, "POST", bytes.Buffer{}, "SignAppin", accessToken, apiKey, "application/json") if scode == 0 { return nil } @@ -189,7 +189,7 @@ func (authenticationObj *AuthenticationObj) SignOut() error { var body io.ReadCloser technicalError = backoff.Retry(func() error { - body, _, technicalError, businessError = authenticationObj.HttpClient.CallSecretSafeAPI(authenticationObj.ApiUrl.JoinPath("Auth/Signout").String(), "POST", bytes.Buffer{}, "SignOut", "", "") + body, _, technicalError, businessError = authenticationObj.HttpClient.CallSecretSafeAPI(authenticationObj.ApiUrl.JoinPath("Auth/Signout").String(), "POST", bytes.Buffer{}, "SignOut", "", "", "application/json") return technicalError }, authenticationObj.ExponentialBackOff) diff --git a/api/entities/entities.go b/api/entities/entities.go index dd3c88a..0e0fdf0 100644 --- a/api/entities/entities.go +++ b/api/entities/entities.go @@ -2,6 +2,10 @@ // Package entities implements DTO's used by Beyondtrust Secret Safe API. package entities +import ( + "github.com/google/uuid" +) + // SignApinResponse responsbile for API sign in information. type SignApinResponse struct { UserId int `json:"UserId"` @@ -82,3 +86,65 @@ type AccountDetails struct { ChangeSComFlag bool `validate:"omitempty"` ObjectID string `validate:"omitempty,max=36"` } + +type FolderResponse struct { + Id string + Name string + Description string +} + +type CreateSecretResponse struct { + Id string + Title string + Description string + FolderId string +} + +type SecretCredentialDetails struct { + Title string `json:",omitempty" validate:"required"` + Description string `json:",omitempty" validate:"omitempty,max=256"` + Username string `json:",omitempty" validate:"required"` + Password string `json:",omitempty" validate:"max=256,required_without=PasswordRuleID"` + OwnerId int `json:",omitempty" validate:"required_if=OwnerType Group"` + OwnerType string `json:",omitempty" validate:"required,oneof=User Group"` + Owners []OwnerDetails `json:",omitempty" validate:"required_if=OwnerType User"` + Notes string `json:",omitempty" validate:"omitempty,max=4000"` + Urls []UrlDetails `json:",omitempty" validate:"omitempty"` + PasswordRuleID int `json:",omitempty" validate:"omitempty"` +} + +type SecretTextDetails struct { + Title string `json:",omitempty" validate:"required,max=256"` + Description string `json:",omitempty" validate:"omitempty,max=256"` + Text string `json:",omitempty" validate:"required,max=4096"` + OwnerId int `json:",omitempty" validate:"required_if=OwnerType Group"` + OwnerType string `json:",omitempty" validate:"required,oneof=User Group"` + Owners []OwnerDetails `json:",omitempty" validate:"required_if=OwnerType User"` + Notes string `json:",omitempty" validate:"omitempty,max=4000"` + FolderId uuid.UUID `json:",omitempty" validate:"omitempty"` + Urls []UrlDetails `json:",omitempty" validate:"omitempty"` +} + +type SecretFileDetails struct { + Title string `json:",omitempty" validate:"required,max=256"` + Description string `json:",omitempty" validate:"omitempty,max=256"` + OwnerId int `json:",omitempty" validate:"required_if=OwnerType Group"` + OwnerType string `json:",omitempty" validate:"required,oneof=User Group"` + Owners []OwnerDetails `json:",omitempty" validate:"required_if=OwnerType User"` + Notes string `json:",omitempty" validate:"omitempty,max=4000"` + FileName string `json:",omitempty" validate:"required,max=256"` + FileContent string `json:",omitempty" validate:"required,max=256"` + Urls []UrlDetails `json:",omitempty" validate:"omitempty"` +} + +type OwnerDetails struct { + OwnerId int `json:",omitempty" validate:"required,min=1,max=2147483647"` + Owner string `json:",omitempty" validate:"omitempty"` + Email string `json:",omitempty" validate:"omitempty"` +} + +type UrlDetails struct { + Id uuid.UUID `json:",omitempty" validate:"omitempty,uuid"` + CredentialId uuid.UUID `json:",omitempty" validate:"omitempty,uuid"` + Url string `json:",omitempty" validate:"required,max=2048,url"` +} diff --git a/api/managed_account/managed_account.go b/api/managed_account/managed_account.go index f80465f..9c88487 100644 --- a/api/managed_account/managed_account.go +++ b/api/managed_account/managed_account.go @@ -116,7 +116,7 @@ func (managedAccountObj *ManagedAccountstObj) ManagedAccountGet(systemName strin var businessError error technicalError = backoff.Retry(func() error { - body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "ManagedAccountGet", "", "") + body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "ManagedAccountGet", "", "", "application/json") if technicalError != nil { return technicalError } @@ -163,7 +163,7 @@ func (managedAccountObj *ManagedAccountstObj) ManagedAccountCreateRequest(system var businessError error technicalError = backoff.Retry(func() error { - body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "POST", *b, "ManagedAccountCreateRequest", "", "") + body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "POST", *b, "ManagedAccountCreateRequest", "", "", "application/json") return technicalError }, managedAccountObj.authenticationObj.ExponentialBackOff) @@ -199,7 +199,7 @@ func (managedAccountObj *ManagedAccountstObj) CredentialByRequestId(requestId st var businessError error technicalError = backoff.Retry(func() error { - body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "CredentialByRequestId", "", "") + body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "CredentialByRequestId", "", "", "application/json") return technicalError }, managedAccountObj.authenticationObj.ExponentialBackOff) @@ -235,7 +235,7 @@ func (managedAccountObj *ManagedAccountstObj) ManagedAccountRequestCheckIn(reque var businessError error technicalError = backoff.Retry(func() error { - _, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "PUT", *b, "ManagedAccountRequestCheckIn", "", "") + _, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "PUT", *b, "ManagedAccountRequestCheckIn", "", "", "application/json") return technicalError }, managedAccountObj.authenticationObj.ExponentialBackOff) @@ -310,7 +310,7 @@ func (managedAccountObj *ManagedAccountstObj) ManagedAccountCreateManagedAccount var businessError error technicalError = backoff.Retry(func() error { - body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "POST", *b, "ManagedAccountCreateManagedAccount", "", "") + body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "POST", *b, "ManagedAccountCreateManagedAccount", "", "", "application/json") return technicalError }, managedAccountObj.authenticationObj.ExponentialBackOff) @@ -352,7 +352,7 @@ func (managedAccountObj *ManagedAccountstObj) ManagedSystemGetSystems(url string var businessError error technicalError = backoff.Retry(func() error { - body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "ManagedSystemGetSystems", "", "") + body, _, technicalError, businessError = managedAccountObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "ManagedSystemGetSystems", "", "", "application/json") if technicalError != nil { return technicalError } diff --git a/api/secrets/secrets.go b/api/secrets/secrets.go index cce39dc..04099dd 100644 --- a/api/secrets/secrets.go +++ b/api/secrets/secrets.go @@ -118,7 +118,7 @@ func (secretObj *SecretObj) SecretGetSecretByPath(secretPath string, secretTitle secretObj.log.Debug(messageLog) technicalError = backoff.Retry(func() error { - body, scode, technicalError, businessError = secretObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "SecretGetSecretByPath", "", "") + body, scode, technicalError, businessError = secretObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "SecretGetSecretByPath", "", "", "application/json") return technicalError }, secretObj.authenticationObj.ExponentialBackOff) @@ -166,7 +166,7 @@ func (secretObj *SecretObj) SecretGetFileSecret(secretId string, endpointPath st url := secretObj.authenticationObj.ApiUrl.JoinPath(endpointPath, secretId, "/file/download").String() technicalError = backoff.Retry(func() error { - body, _, technicalError, businessError = secretObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "SecretGetFileSecret", "", "") + body, _, technicalError, businessError = secretObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "SecretGetFileSecret", "", "", "application/json") return technicalError }, secretObj.authenticationObj.ExponentialBackOff) @@ -188,3 +188,185 @@ func (secretObj *SecretObj) SecretGetFileSecret(secretId string, endpointPath st return responseString, nil } + +// CreateSecretCredentialFlow is responsible for creating secrets in Password Safe. +func (secretObj *SecretObj) CreateSecretFlow(folderTarget string, secretDetails interface{}) (entities.CreateSecretResponse, error) { + + var folder *entities.FolderResponse + var createResponse entities.CreateSecretResponse + + secretDetails, err := utils.ValidateCreateSecretInput(secretDetails) + + if err != nil { + return createResponse, err + } + + folders, err := secretObj.SecretGetFolders("secrets-safe/folders/") + + if err != nil { + return createResponse, err + } + + for _, v := range folders { + if v.Name == strings.TrimSpace(folderTarget) { + folder = &v + break + } + } + + if folder == nil { + return createResponse, fmt.Errorf("folder %v was not found in folder list", folderTarget) + } + + if err != nil { + return entities.CreateSecretResponse{}, err + } + + createResponse, err = secretObj.SecretCreateSecret(folder.Id, secretDetails) + + if err != nil { + return createResponse, err + } + + return createResponse, nil +} + +// SecretCreateSecret calls Secret Safe API Requests enpoint to create secrets in Password Safe. +func (secretObj *SecretObj) SecretCreateSecret(folderId string, secretDetails interface{}) (entities.CreateSecretResponse, error) { + + secretCredentialDetailsJson, err := json.Marshal(secretDetails) + + if err != nil { + return entities.CreateSecretResponse{}, err + } + + payload := string(secretCredentialDetailsJson) + + var CreateSecretResponse entities.CreateSecretResponse + + b := bytes.NewBufferString(payload) + + // path depends on the type of secret (credential, text, file). + var path string + switch secretDetails.(type) { + case entities.SecretCredentialDetails: + path = "secrets" + case entities.SecretTextDetails: + path = "secrets/text" + case entities.SecretFileDetails: + path = "secrets/file" + } + + SecretCreateSecretUrl := secretObj.authenticationObj.ApiUrl.JoinPath("secrets-safe/folders", folderId, path).String() + messageLog := fmt.Sprintf("%v %v", "POST", SecretCreateSecretUrl) + secretObj.log.Debug(messageLog) + + var fileSecret entities.SecretFileDetails + var ok bool + + // file secrets have a special behavior, they need to be created using multipart request. + if path == "secrets/file" { + if fileSecret, ok = secretDetails.(entities.SecretFileDetails); ok { + body, err := secretObj.authenticationObj.HttpClient.CreateMultiPartRequest(SecretCreateSecretUrl, fileSecret.FileName, []byte(payload), fileSecret.FileContent) + if err != nil { + return entities.CreateSecretResponse{}, err + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return entities.CreateSecretResponse{}, err + } + + err = json.Unmarshal([]byte(bodyBytes), &CreateSecretResponse) + + if err != nil { + return entities.CreateSecretResponse{}, err + } + } + return CreateSecretResponse, nil + } + + var body io.ReadCloser + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + body, _, technicalError, businessError = secretObj.authenticationObj.HttpClient.CallSecretSafeAPI(SecretCreateSecretUrl, "POST", *b, "SecretCreateSecret", "", "", "application/json") + return technicalError + }, secretObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return entities.CreateSecretResponse{}, technicalError + } + + if businessError != nil { + return entities.CreateSecretResponse{}, businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return entities.CreateSecretResponse{}, err + } + + err = json.Unmarshal([]byte(bodyBytes), &CreateSecretResponse) + + if err != nil { + secretObj.log.Error(err.Error()) + return entities.CreateSecretResponse{}, err + } + + return CreateSecretResponse, nil + +} + +// SecretGetFolders call secrets-safe/folders/ enpoint +// and returns folder list +func (secretObj *SecretObj) SecretGetFolders(endpointPath string) ([]entities.FolderResponse, error) { + messageLog := fmt.Sprintf("%v %v", "GET", endpointPath) + secretObj.log.Debug(messageLog + endpointPath) + + var body io.ReadCloser + var technicalError error + var businessError error + + url := secretObj.authenticationObj.ApiUrl.JoinPath(endpointPath).String() + + var foldersObj []entities.FolderResponse + + technicalError = backoff.Retry(func() error { + body, _, technicalError, businessError = secretObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "SecretGetFolders", "", "", "application/json") + return technicalError + }, secretObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return foldersObj, technicalError + } + + if businessError != nil { + return foldersObj, businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return foldersObj, err + } + + err = json.Unmarshal(bodyBytes, &foldersObj) + if err != nil { + secretObj.log.Error(err.Error()) + return foldersObj, err + } + + if len(foldersObj) == 0 { + return foldersObj, fmt.Errorf("empty Folder List") + } + + return foldersObj, nil + +} diff --git a/api/secrets/secrets_test.go b/api/secrets/secrets_test.go index f59625f..7450109 100644 --- a/api/secrets/secrets_test.go +++ b/api/secrets/secrets_test.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" "go.uber.org/zap" @@ -653,3 +654,472 @@ func TestSecretFlowBadBody(t *testing.T) { t.Errorf("Test case Failed") } } + +func TestSecretCreateTextSecretFlow(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: "TestSecretCreateSecretFlowFolderNotFound", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/secrets-safe/folders/": + _, 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") + } + + case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets/text": + _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title", "Description": "Title Description"}`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + secretObj, _ := NewSecretObj(*authenticate, zapLogger, 4000) + + secretTextDetails := entities.SecretTextDetails{ + Title: "Secret Title", + Description: "Title Description", + Text: "PasswordTest", + OwnerType: "User", + OwnerId: 1, + FolderId: uuid.New(), + Owners: []entities.OwnerDetails{ + { + OwnerId: 1, + Owner: "administrator", + Email: "test@beyondtrust.com", + }, + }, + } + + response, err := secretObj.CreateSecretFlow("folder1", secretTextDetails) + + if response.Title != "Secret Title" { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if response.Description != "Title Description" { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } + +} + +func TestSecretCreateCredentialSecretFlow(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: "TestSecretCreateSecretFlowFolderNotFound", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/secrets-safe/folders/": + _, 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") + } + + case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets": + _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title", "Description": "Title Description"}`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + secretObj, _ := NewSecretObj(*authenticate, zapLogger, 4000) + + secretTextDetails := entities.SecretCredentialDetails{ + Title: "Secret Title", + Description: "Title Description", + Username: "TestUserName", + Password: "PasswordTest", + OwnerType: "User", + OwnerId: 1, + Owners: []entities.OwnerDetails{ + { + OwnerId: 1, + Owner: "administrator", + Email: "test@beyondtrust.com", + }, + }, + } + + response, err := secretObj.CreateSecretFlow("folder1", secretTextDetails) + + if response.Title != "Secret Title" { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if response.Description != "Title Description" { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } + +} + +func TestSecretCreateFileSecretFlow(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: "TestSecretCreateSecretFlowFolderNotFound", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/secrets-safe/folders/": + _, 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") + } + + case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets/file": + _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "File Secret Title", "Description": "Title Description", "FileName": "textfile.txt"}`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + secretObj, _ := NewSecretObj(*authenticate, zapLogger, 4000) + + secretTextDetails := entities.SecretFileDetails{ + Title: "Secret Title", + Description: "File Title Description", + FileName: "textfile.txt", + FileContent: "Secret Content", + OwnerType: "User", + OwnerId: 1, + Owners: []entities.OwnerDetails{ + { + OwnerId: 1, + Owner: "administrator", + Email: "test@beyondtrust.com", + }, + }, + } + + response, err := secretObj.CreateSecretFlow("folder1", secretTextDetails) + + if response.Title != "File Secret Title" { + t.Errorf("Test case Failed %v, %v", response, "File Secret Title") + } + + if response.Description != "Title Description" { + t.Errorf("Test case Failed %v, %v", response, "Title Description") + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } + +} + +func TestSecretCreateFileSecretFlowError(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: "TestSecretCreateSecretFlowFolderNotFound", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/secrets-safe/folders/": + _, 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") + } + + case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets/file": + w.WriteHeader(http.StatusConflict) + _, err := w.Write([]byte(`{"error":"Title name already exists"}`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "error - status code: 409 - {\"error\":\"Title name already exists\"}", + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + secretObj, _ := NewSecretObj(*authenticate, zapLogger, 4000) + + secretTextDetails := entities.SecretFileDetails{ + Title: "Secret Title", + Description: "File Title Description", + FileName: "textfile.txt", + FileContent: "Secret Content", + OwnerType: "User", + OwnerId: 1, + Owners: []entities.OwnerDetails{ + { + OwnerId: 1, + Owner: "administrator", + Email: "test@beyondtrust.com", + }, + }, + } + + _, err := secretObj.CreateSecretFlow("folder1", secretTextDetails) + + if err.Error() != testConfig.response { + t.Errorf("Test case Failed %v, %v", err.Error(), testConfig.response) + } + + if err == nil { + t.Errorf("Test case Failed: %v", err) + } + +} + +func TestSecretCreateBadInput(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: "TestSecretCreateSecretFlowFolderNotFound", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/secrets-safe/folders/": + _, 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") + } + + case "/secrets-safe/folders/cb871861-8b40-4556-820c-1ca6d522adfa/secrets": + _, err := w.Write([]byte(`{"Id": "01ca9cf3-0751-4a90-4856-08dcf22d7472","Title": "Secret Title", "Description": "Title Description"}`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "The field 'Title' is required.", + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + secretObj, _ := NewSecretObj(*authenticate, zapLogger, 4000) + + secretTextDetails := entities.SecretCredentialDetails{ + Title: "", + Description: "Title Description", + Username: "TestUserName", + Password: "PasswordTest", + OwnerType: "User", + OwnerId: 1, + Owners: []entities.OwnerDetails{ + { + OwnerId: 1, + Owner: "administrator", + Email: "test@beyondtrust.com", + }, + }, + } + + _, err := secretObj.CreateSecretFlow("folder1", secretTextDetails) + + if err.Error() != testConfig.response { + t.Errorf("Test case Failed %v, %v", err.Error(), testConfig.response) + } + +} + +func TestSecretCreateSecretFlowFolderNotFound(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: "TestSecretCreateSecretFlowFolderNotFound", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/secrets-safe/folders/": + _, 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") + } + + default: + http.NotFound(w, r) + } + })), + response: "folder folder_name was not found in folder list", + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + secretObj, _ := NewSecretObj(*authenticate, zapLogger, 4000) + + secretTextDetails := entities.SecretTextDetails{ + Title: "Secret Title", + Description: "Title Description", + Text: "PasswordTest", + OwnerType: "User", + OwnerId: 1, + FolderId: uuid.New(), + Owners: []entities.OwnerDetails{ + { + OwnerId: 1, + Owner: "administrator", + Email: "test@beyondtrust.com", + }, + }, + } + + _, err := secretObj.CreateSecretFlow("folder_name", secretTextDetails) + + if err.Error() != testConfig.response { + t.Errorf("Test case Failed %v, %v", err.Error(), testConfig.response) + } + +} + +func TestSecretCreateSecretFlowEmptyFolderList(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: "TestSecretCreateSecretFlowFolderNotFound", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/secrets-safe/folders/": + _, err := w.Write([]byte(`[]`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "empty Folder List", + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + secretObj, _ := NewSecretObj(*authenticate, zapLogger, 4000) + + secretTextDetails := entities.SecretTextDetails{ + Title: "Secret Title", + Description: "Title Description", + Text: "PasswordTest", + OwnerType: "User", + OwnerId: 1, + FolderId: uuid.New(), + Owners: []entities.OwnerDetails{ + { + OwnerId: 1, + Owner: "administrator", + Email: "test@beyondtrust.com", + }, + }, + } + + _, err := secretObj.CreateSecretFlow("folder_name", secretTextDetails) + + if err.Error() != testConfig.response { + t.Errorf("Test case Failed %v, %v", err.Error(), testConfig.response) + } + +} diff --git a/api/utils/httpclient.go b/api/utils/httpclient.go index 4a67755..d41b7e0 100644 --- a/api/utils/httpclient.go +++ b/api/utils/httpclient.go @@ -9,10 +9,12 @@ import ( "errors" "fmt" "io" + "mime/multipart" "net/http" "net/http/cookiejar" "os" "path/filepath" + "strings" "time" logging "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging" @@ -114,8 +116,8 @@ func GetPFXContent(clientCertificatePath string, clientCertificateName string, c } // CallSecretSafeAPI prepares http call -func (client *HttpClientObj) CallSecretSafeAPI(url string, httpMethod string, body bytes.Buffer, method string, accesToken string, apiKey string) (io.ReadCloser, int, error, error) { - response, scode, technicalError, businessError := client.HttpRequest(url, httpMethod, body, accesToken, apiKey) +func (client *HttpClientObj) CallSecretSafeAPI(url string, httpMethod string, body bytes.Buffer, method string, accesToken string, apiKey string, contentType string) (io.ReadCloser, int, error, error) { + response, scode, technicalError, businessError := client.HttpRequest(url, httpMethod, body, accesToken, apiKey, contentType) if technicalError != nil { messageLog := fmt.Sprintf("Error in %v %v \n", method, technicalError) client.log.Error(messageLog) @@ -129,13 +131,13 @@ func (client *HttpClientObj) CallSecretSafeAPI(url string, httpMethod string, bo } // HttpRequest makes http request to the server. -func (client *HttpClientObj) HttpRequest(url string, method string, body bytes.Buffer, accesToken string, apiKey string) (closer io.ReadCloser, scode int, technicalError error, businessError error) { +func (client *HttpClientObj) HttpRequest(url string, method string, body bytes.Buffer, accesToken string, apiKey string, contentType string) (closer io.ReadCloser, scode int, technicalError error, businessError error) { req, err := http.NewRequest(method, url, &body) if err != nil { return nil, 0, err, nil } - req.Header = http.Header{"Content-Type": []string{"application/json"}} + req.Header = http.Header{"Content-Type": []string{contentType}} if accesToken != "" { req.Header.Set("Authorization", "Bearer "+accesToken) @@ -171,3 +173,42 @@ func (client *HttpClientObj) HttpRequest(url string, method string, body bytes.B return resp.Body, resp.StatusCode, nil, nil } + +// CreateMultipartRequest creates and sends multipart request. +func (client *HttpClientObj) CreateMultiPartRequest(url, fileName string, metadata []byte, fileContent string) (io.ReadCloser, error) { + + var requestBody bytes.Buffer + + multipartWriter := multipart.NewWriter(&requestBody) + + err := multipartWriter.WriteField("secretmetadata", string(metadata)) + if err != nil { + return nil, err + } + + fileWriter, err := multipartWriter.CreateFormFile("file", fileName) + if err != nil { + return nil, err + } + + fileReader := strings.NewReader(fileContent) + + _, err = io.Copy(fileWriter, fileReader) + if err != nil { + return nil, err + } + + multipartWriter.Close() + + body, _, technicalError, businessError := client.CallSecretSafeAPI(url, "POST", requestBody, "CreateMultiPartRequest", "", "", multipartWriter.FormDataContentType()) + + if technicalError != nil { + return body, technicalError + } + + if businessError != nil { + return body, businessError + } + + return body, nil +} diff --git a/api/utils/validator.go b/api/utils/validator.go index f08ba32..c0c5783 100644 --- a/api/utils/validator.go +++ b/api/utils/validator.go @@ -16,7 +16,7 @@ import ( ) type ValidationParams struct { - ApiKey string + ApiKey string ClientID string ClientSecret string ApiUrl *string @@ -73,7 +73,7 @@ func ValidateInputs(params ValidationParams) error { validate = validator.New(validator.WithRequiredStructEnabled()) userInput := &UserInputValidaton{ - ApiKey: params.ApiKey, + ApiKey: params.ApiKey, ClientId: params.ClientID, ClientSecret: params.ClientSecret, ApiUrl: *params.ApiUrl, @@ -223,12 +223,15 @@ func ValidateURL(apiUrl string) error { return nil } +// ValidateCreateManagedAccountInput responsible for validating Managed Account input func ValidateCreateManagedAccountInput(accountDetails entities.AccountDetails) (entities.AccountDetails, error) { validate := validator.New() err := validate.Struct(accountDetails) if err != nil { - return accountDetails, err + for _, err := range err.(validator.ValidationErrors) { + return accountDetails, errors.New(formatErrorMessage(err)) + } } if accountDetails.ChangeFrequencyType == "" { @@ -253,3 +256,31 @@ func ValidateCreateManagedAccountInput(accountDetails entities.AccountDetails) ( return accountDetails, nil } + +// ValidateCreateSecretInput responsible for validating secret input. +func ValidateCreateSecretInput(secretDetails interface{}) (interface{}, error) { + validate := validator.New() + err := validate.Struct(secretDetails) + if err != nil { + for _, err := range err.(validator.ValidationErrors) { + return secretDetails, errors.New(formatErrorMessage(err)) + } + } + return secretDetails, nil +} + +// formatErrorMessage responsible for formating errors text. +func formatErrorMessage(err validator.FieldError) string { + switch err.Tag() { + case "required": + return fmt.Sprintf("The field '%s' is required.", err.Field()) + case "required_if": + return fmt.Sprintf("Field '%s' is mandatory when %s", err.Field(), err.Param()) + case "oneof": + return fmt.Sprintf("The field '%s' must be one of the following values: %s.", err.Field(), err.Param()) + case "required_without": + return fmt.Sprintf("The field '%s' is required when the field '%s' is not provided.", err.Field(), err.Param()) + default: + return fmt.Sprintf("Error en el campo '%s': %s.", err.Field(), err.Tag()) + } +} diff --git a/go.mod b/go.mod index cfe53fd..4166117 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.22.0 // indirect diff --git a/go.sum b/go.sum index ec8bdb8..ac7a390 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtP github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=