Skip to content

Commit

Permalink
feat: add creating folders feature in terraform provider (#165)
Browse files Browse the repository at this point in the history
* feat: add creating folders feature in terraform provider

* fix: fix description max length validation
  • Loading branch information
btfhernandez authored Nov 29, 2024
1 parent fde139f commit 825aa4a
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 9 deletions.
16 changes: 16 additions & 0 deletions TestClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion api/authentication/authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
15 changes: 15 additions & 0 deletions api/entities/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
2 changes: 1 addition & 1 deletion api/managed_account/managed_account_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
99 changes: 99 additions & 0 deletions api/secrets/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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

}
126 changes: 119 additions & 7 deletions api/secrets/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}
12 changes: 12 additions & 0 deletions api/utils/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

0 comments on commit 825aa4a

Please sign in to comment.