From 6853d2e6b9e413c836e47a437d21401882931a76 Mon Sep 17 00:00:00 2001 From: "EPAM\\Felipe_Hernandez" Date: Wed, 7 Feb 2024 19:16:59 -0500 Subject: [PATCH 1/2] feat: initial commit --- api/authentication/authentication_test.go | 184 ++++++++++++ api/authentication/authetication.go | 182 ++++++++++++ api/entities/entities.go | 29 ++ api/logging/logging.go | 93 ++++++ api/managed_account/managed_account.go | 274 ++++++++++++++++++ api/managed_account/managed_account_test.go | 301 ++++++++++++++++++++ api/secrets/secrets.go | 167 +++++++++++ api/secrets/secrets_test.go | 218 ++++++++++++++ api/utils/httpclient.go | 116 ++++++++ api/utils/validator.go | 102 +++++++ go.mod | 19 ++ go.sum | 28 ++ main.go | 99 +++++++ 13 files changed, 1812 insertions(+) create mode 100644 api/authentication/authentication_test.go create mode 100644 api/authentication/authetication.go create mode 100644 api/entities/entities.go create mode 100644 api/logging/logging.go create mode 100644 api/managed_account/managed_account.go create mode 100644 api/managed_account/managed_account_test.go create mode 100644 api/secrets/secrets.go create mode 100644 api/secrets/secrets_test.go create mode 100644 api/utils/httpclient.go create mode 100644 api/utils/validator.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/api/authentication/authentication_test.go b/api/authentication/authentication_test.go new file mode 100644 index 0000000..377e496 --- /dev/null +++ b/api/authentication/authentication_test.go @@ -0,0 +1,184 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package authentication implements functions to call Beyondtrust Secret Safe API. +// Unit tests for authentication package. +package authentication + +import ( + "go-client-library-passwordsafe/api/entities" + "go-client-library-passwordsafe/api/logging" + "go-client-library-passwordsafe/api/utils" + "reflect" + + "net/http" + "net/http/httptest" + "testing" + + "go.uber.org/zap" +) + +type UserTestConfig struct { + name string + server *httptest.Server + response *entities.SignApinResponse +} + +type GetTokenConfig struct { + name string + server *httptest.Server + response string +} + +type GetPasswordSafeAuthenticationConfig struct { + name string + server *httptest.Server + response *entities.SignApinResponse +} + +func TestSignOut(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := UserTestConfig{ + name: "TestSignOut", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(``)) + if err != nil { + t.Error("Test case Failed") + } + + })), + response: nil, + } + + err := authenticate.SignOut(testConfig.server.URL) + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestSignAppin(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := UserTestConfig{ + name: "TestSignAppin", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`)) + if err != nil { + t.Error("Test case Failed") + } + })), + response: &entities.SignApinResponse{ + UserId: 1, + EmailAddress: "Felipe", + }, + } + + response, err := authenticate.SignAppin(testConfig.server.URL+"/"+"TestSignAppin", "") + + if !reflect.DeepEqual(response, *testConfig.response) { + t.Errorf("Test case Failed %v, %v", response, *testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestGetToken(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := GetTokenConfig{ + name: "TestGetToken", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response accorging to the endpoint path + switch r.URL.Path { + + case "/Auth/connect/token": + _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer", "scope": "publicapi"}`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "fake_token", + } + + response, err := authenticate.GetToken(testConfig.server.URL+"/"+"Auth/connect/token", "", "") + + if response != testConfig.response { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestGetPasswordSafeAuthentication(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := GetPasswordSafeAuthenticationConfig{ + name: "TestGetPasswordSafeAuthentication", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/Auth/connect/token": + _, err := w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer", "scope": "publicapi"}`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/Auth/SignAppIn": + _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`)) + + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: &entities.SignApinResponse{ + UserId: 1, + EmailAddress: "Felipe", + }, + } + authenticate.ApiUrl = testConfig.server.URL + "/" + response, err := authenticate.GetPasswordSafeAuthentication() + + if !reflect.DeepEqual(response, *testConfig.response) { + t.Errorf("Test case Failed %v, %v", response, *testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} diff --git a/api/authentication/authetication.go b/api/authentication/authetication.go new file mode 100644 index 0000000..56affb4 --- /dev/null +++ b/api/authentication/authetication.go @@ -0,0 +1,182 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package client implements functions to call Beyondtrust Secret Safe API. +package authentication + +import ( + "bytes" + "encoding/json" + "fmt" + "go-client-library-passwordsafe/api/entities" + "go-client-library-passwordsafe/api/logging" + "go-client-library-passwordsafe/api/utils" + "io" + + "net/url" + "time" + + backoff "github.com/cenkalti/backoff/v4" +) + +type AuthenticationObj struct { + ApiUrl string + clientId string + clientSecret string + HttpClient utils.HttpClientObj + ExponentialBackOff *backoff.ExponentialBackOff + log logging.Logger +} + +// Authenticate in PS API +func Authenticate(httpClient utils.HttpClientObj, endpointUrl string, clientId string, clientSecret string, logger logging.Logger, retryMaxElapsedTimeSeconds int) (*AuthenticationObj, error) { + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.InitialInterval = 1 * time.Second + backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeSeconds) * time.Second + backoffDefinition.RandomizationFactor = 0.5 + + // Client + var client = httpClient + + authenticationObj := &AuthenticationObj{ + ApiUrl: endpointUrl, + HttpClient: client, + clientId: clientId, + clientSecret: clientSecret, + ExponentialBackOff: backoffDefinition, + log: logger, + } + + return authenticationObj, nil +} + +// GetPasswordSafeAuthentication call get token and sign app endpoint +func (authenticationObj *AuthenticationObj) GetPasswordSafeAuthentication() (entities.SignApinResponse, error) { + accessToken, err := authenticationObj.GetToken(fmt.Sprintf("%v%v", authenticationObj.ApiUrl, "Auth/connect/token"), authenticationObj.clientId, authenticationObj.clientSecret) + if err != nil { + return entities.SignApinResponse{}, err + } + signApinResponse, err := authenticationObj.SignAppin(fmt.Sprintf("%v%v", authenticationObj.ApiUrl, "Auth/SignAppIn"), accessToken) + if err != nil { + return entities.SignApinResponse{}, err + } + return signApinResponse, nil +} + +// GetToken get token from PS API +func (authenticationObj *AuthenticationObj) GetToken(endpointUrl string, clientId string, clientSecret string) (string, error) { + + params := url.Values{} + params.Add("client_id", clientId) + params.Add("client_secret", clientSecret) + params.Add("grant_type", "client_credentials") + + var body io.ReadCloser + var technicalError error + var businessError error + + var buffer bytes.Buffer + buffer.WriteString(params.Encode()) + + technicalError = backoff.Retry(func() error { + body, technicalError, businessError, _ = authenticationObj.HttpClient.CallSecretSafeAPI(endpointUrl, "POST", buffer, "GetToken", "") + return technicalError + }, authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return "", err + } + + responseString := string(bodyBytes) + + var data entities.GetTokenResponse + + err = json.Unmarshal([]byte(responseString), &data) + if err != nil { + authenticationObj.log.Error(err.Error()) + return "", err + } + + return data.AccessToken, nil + +} + +// SignAppin Signs app in PS API +func (authenticationObj *AuthenticationObj) SignAppin(endpointUrl string, accessToken string) (entities.SignApinResponse, error) { + + var userObject entities.SignApinResponse + var body io.ReadCloser + var technicalError error + var businessError error + var scode int + + err := backoff.Retry(func() error { + body, technicalError, businessError, scode = authenticationObj.HttpClient.CallSecretSafeAPI(endpointUrl, "POST", bytes.Buffer{}, "SignAppin", accessToken) + if scode == 0 { + return nil + } + return technicalError + }, authenticationObj.ExponentialBackOff) + + if err != nil { + return entities.SignApinResponse{}, err + } + + if scode == 0 { + return entities.SignApinResponse{}, technicalError + } + + if businessError != nil { + return entities.SignApinResponse{}, businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + if err != nil { + return entities.SignApinResponse{}, err + } + + err = json.Unmarshal(bodyBytes, &userObject) + + if err != nil { + authenticationObj.log.Error(err.Error()) + return entities.SignApinResponse{}, err + } + + return userObject, nil +} + +// SignOut signs out Secret Safe API. +// Warn: should only be called one time for all data sources. +func (authenticationObj *AuthenticationObj) SignOut(url string) error { + authenticationObj.log.Debug(url) + + var technicalError error + var businessError error + var body io.ReadCloser + + technicalError = backoff.Retry(func() error { + body, technicalError, businessError, _ = authenticationObj.HttpClient.CallSecretSafeAPI(url, "POST", bytes.Buffer{}, "SignOut", "") + return technicalError + }, authenticationObj.ExponentialBackOff) + + defer body.Close() + if businessError != nil { + authenticationObj.log.Error(businessError.Error()) + return businessError + } + + defer authenticationObj.HttpClient.HttpClient.CloseIdleConnections() + + return nil +} diff --git a/api/entities/entities.go b/api/entities/entities.go new file mode 100644 index 0000000..7677dda --- /dev/null +++ b/api/entities/entities.go @@ -0,0 +1,29 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package entities implements DTO's used by Beyondtrust Secret Safe API. +package entities + +type SignApinResponse struct { + UserId int `json:"UserId"` + EmailAddress string `json:"EmailAddress"` + UserName string `json:"UserName"` + Name string `json:"Name"` +} + +type ManagedAccount struct { + SystemId int + AccountId int +} + +type Secret struct { + Id string + Title string + Password string + SecretType string +} + +type GetTokenResponse struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` +} diff --git a/api/logging/logging.go b/api/logging/logging.go new file mode 100644 index 0000000..ee8b7fd --- /dev/null +++ b/api/logging/logging.go @@ -0,0 +1,93 @@ +package logging + +import ( + "fmt" + "log" + + "github.com/go-logr/logr" + "go.uber.org/zap" +) + +// Logger is an interface that defines the logging methods +type Logger interface { + Info(msg string) + Error(msg string) + Debug(msg string) +} + +// ZapLogger is a struct that implements the Logger interface using zap +type ZapLogger struct { + logger *zap.Logger +} + +// Info logs a message at info level +func (z *ZapLogger) Info(msg string) { + z.logger.Info(msg) +} + +// Error logs a message at error level +func (z *ZapLogger) Error(msg string) { + z.logger.Error(msg) +} + +// Error logs a message at error level +func (z *ZapLogger) Debug(msg string) { + z.logger.Debug(msg) +} + +// logr.logger +type LogrLogger struct { + logger *logr.Logger +} + +// Info logs a message at info level +func (r *LogrLogger) Info(msg string) { + r.logger.Info(msg) +} + +// Error logs a message at error level +func (r *LogrLogger) Error(msg string) { + r.logger.Error(fmt.Errorf("an error"), msg) +} + +func (r *LogrLogger) Debug(msg string) { + r.logger.Info(msg) +} + +// log.logger +type LogLogger struct { + logger *log.Logger +} + +// Info logs a message at info level +func (l *LogLogger) Info(msg string) { + prefix := fmt.Sprintf("%v :", "Info") + l.logger.SetPrefix(prefix) + l.logger.Println(msg) +} + +// Error logs a message at error level +func (l *LogLogger) Error(msg string) { + prefix := fmt.Sprintf("%v :", "Error") + l.logger.SetPrefix(prefix) + l.logger.Println(msg) +} + +func (l *LogLogger) Debug(msg string) { + prefix := fmt.Sprintf("%v :", "Debug") + l.logger.SetPrefix(prefix) + l.logger.Println(msg) +} + +// NewZapLogger creates a new ZapLogger with the given zap.Logger +func NewZapLogger(logger *zap.Logger) *ZapLogger { + return &ZapLogger{logger: logger} +} + +func NewLogrLogger(logger *logr.Logger) *LogrLogger { + return &LogrLogger{logger: logger} +} + +func NewLogLogger(logger *log.Logger) *LogLogger { + return &LogLogger{logger: logger} +} diff --git a/api/managed_account/managed_account.go b/api/managed_account/managed_account.go new file mode 100644 index 0000000..ef19663 --- /dev/null +++ b/api/managed_account/managed_account.go @@ -0,0 +1,274 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package managed_accounts implements Get managed account logic + +package managed_accounts + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "go-client-library-passwordsafe/api/authentication" + "go-client-library-passwordsafe/api/entities" + "go-client-library-passwordsafe/api/logging" + "io" + "strconv" + "strings" + + backoff "github.com/cenkalti/backoff/v4" +) + +type ManagedAccountstObj struct { + log logging.Logger + authenticationObj authentication.AuthenticationObj +} + +// NewManagedAccountObj creates managed account obj +func NewManagedAccountObj(authentication authentication.AuthenticationObj, logger logging.Logger) (*ManagedAccountstObj, error) { + managedAccounObj := &ManagedAccountstObj{ + log: logger, + authenticationObj: authentication, + } + return managedAccounObj, nil +} + +// GetSecrets returns secret value for a System Name and Account Name list. +func (managedAccounObj *ManagedAccountstObj) GetSecrets(secretsList []string, separator string) (map[string]string, error) { + if separator == "" { + separator = "" + } + return managedAccounObj.ManageAccountFlow(secretsList, separator, make(map[string]string)) +} + +// GetSecret returns secret value for a specific System Name and Account Name. +func (managedAccounObj *ManagedAccountstObj) GetSecret(secretsList []string, separator string) (map[string]string, error) { + return managedAccounObj.ManageAccountFlow(secretsList, separator, make(map[string]string)) +} + +// ManageAccountFlow returns value for a specific System Name and Account Name. +func (managedAccounObj *ManagedAccountstObj) ManageAccountFlow(secretsToRetrieve []string, separator string, paths map[string]string) (map[string]string, error) { + + secretDictionary := make(map[string]string) + + for _, secretToRetrieve := range secretsToRetrieve { + + secretData := strings.Split(secretToRetrieve, separator) + + systemName := secretData[0] + accountName := secretData[1] + + systemName = strings.TrimSpace(systemName) + accountName = strings.TrimSpace(accountName) + + if len(paths) == 0 { + paths["SignAppinPath"] = "Auth/SignAppin" + paths["SignAppOutPath"] = "Auth/Signout" + paths["ManagedAccountGetPath"] = fmt.Sprintf("ManagedAccounts?systemName=%v&accountName=%v", systemName, accountName) + paths["ManagedAccountCreateRequestPath"] = "Requests" + paths["CredentialByRequestIdPath"] = "Credentials/%v" + paths["ManagedAccountRequestCheckInPath"] = "Requests/%v/checkin" + } + + var err error + + if systemName == "" { + err = errors.New("Please use a valid system_name value") + managedAccounObj.log.Error(err.Error()) + return nil, err + } + + if accountName == "" { + err = errors.New("Please use a valid system_name value") + managedAccounObj.log.Error(err.Error()) + return nil, err + } + + ManagedAccountGetUrl := managedAccounObj.RequestPath(paths["ManagedAccountGetPath"]) + managedAccount, err := managedAccounObj.ManagedAccountGet(systemName, accountName, ManagedAccountGetUrl) + if err != nil { + managedAccounObj.log.Error(err.Error()) + return nil, err + } + + ManagedAccountCreateRequestUrl := managedAccounObj.RequestPath(paths["ManagedAccountCreateRequestPath"]) + requestId, err := managedAccounObj.ManagedAccountCreateRequest(managedAccount.SystemId, managedAccount.AccountId, ManagedAccountCreateRequestUrl) + if err != nil { + managedAccounObj.log.Error(err.Error()) + return nil, err + } + + CredentialByRequestIdUrl := managedAccounObj.RequestPath(fmt.Sprintf(paths["CredentialByRequestIdPath"], requestId)) + secret, err := managedAccounObj.CredentialByRequestId(requestId, CredentialByRequestIdUrl) + if err != nil { + managedAccounObj.log.Error(err.Error()) + return nil, err + } + + ManagedAccountRequestCheckInPath := fmt.Sprintf(paths["ManagedAccountRequestCheckInPath"], requestId) + ManagedAccountRequestCheckInUrl := managedAccounObj.RequestPath(ManagedAccountRequestCheckInPath) + _, err = managedAccounObj.ManagedAccountRequestCheckIn(requestId, ManagedAccountRequestCheckInUrl) + + if err != nil { + managedAccounObj.log.Error(err.Error()) + return nil, err + } + + secretValue, _ := strconv.Unquote(secret) + secretDictionary[secretToRetrieve] = secretValue + + } + return secretDictionary, nil +} + +func (managedAccounObj *ManagedAccountstObj) ManagedAccountGet(systemName string, accountName string, url string) (entities.ManagedAccount, error) { + messageLog := fmt.Sprintf("%v %v", "GET", url) + managedAccounObj.log.Debug(messageLog) + + var body io.ReadCloser + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + body, technicalError, businessError, _ = managedAccounObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "ManagedAccountGet", "") + if technicalError != nil { + return technicalError + } + return nil + + }, managedAccounObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return entities.ManagedAccount{}, technicalError + } + + if businessError != nil { + return entities.ManagedAccount{}, businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return entities.ManagedAccount{}, err + } + + var managedAccountObject entities.ManagedAccount + err = json.Unmarshal(bodyBytes, &managedAccountObject) + if err != nil { + managedAccounObj.log.Error(err.Error()) + return entities.ManagedAccount{}, err + } + + return managedAccountObject, nil + +} + +// ManagedAccountCreateRequest calls Secret Safe API Requests enpoint and returns a request Id as string. +func (managedAccounObj *ManagedAccountstObj) ManagedAccountCreateRequest(systemName int, accountName int, url string) (string, error) { + messageLog := fmt.Sprintf("%v %v", "POST", url) + managedAccounObj.log.Debug(messageLog) + + data := fmt.Sprintf(`{"SystemID":%v, "AccountID":%v, "DurationMinutes":5, "Reason":"Tesr", "ConflictOption": "reuse"}`, systemName, accountName) + b := bytes.NewBufferString(data) + + var body io.ReadCloser + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + body, technicalError, businessError, _ = managedAccounObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "POST", *b, "ManagedAccountCreateRequest", "") + return technicalError + }, managedAccounObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return "", err + } + + responseString := string(bodyBytes) + + return responseString, nil + +} + +// CredentialByRequestId calls Secret Safe API Credentials/ +// enpoint and returns secret value by request Id. +func (managedAccounObj *ManagedAccountstObj) CredentialByRequestId(requestId string, url string) (string, error) { + messageLog := fmt.Sprintf("%v %v", "GET", url) + managedAccounObj.log.Debug(strings.Replace(messageLog, requestId, "****", -1)) + + var body io.ReadCloser + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + body, technicalError, businessError, _ = managedAccounObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "CredentialByRequestId", "") + return technicalError + }, managedAccounObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + if err != nil { + managedAccounObj.log.Error(err.Error()) + return "", err + } + + if err != nil { + return "", err + } + + responseString := string(bodyBytes) + + return responseString, nil + +} + +// ManagedAccountRequestCheckIn calls Secret Safe API "Requests//checkin enpoint. +func (managedAccounObj *ManagedAccountstObj) ManagedAccountRequestCheckIn(requestId string, url string) (string, error) { + messageLog := fmt.Sprintf("%v %v", "PUT", url) + managedAccounObj.log.Debug(strings.Replace(messageLog, requestId, "****", -1)) + + data := "{}" + b := bytes.NewBufferString(data) + + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + _, technicalError, businessError, _ = managedAccounObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "PUT", *b, "ManagedAccountRequestCheckIn", "") + return technicalError + }, managedAccounObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + return "", nil +} + +// requestPath Build endpint path. +func (managedAccounObj *ManagedAccountstObj) RequestPath(path string) string { + return fmt.Sprintf("%v/%v", managedAccounObj.authenticationObj.ApiUrl, path) +} diff --git a/api/managed_account/managed_account_test.go b/api/managed_account/managed_account_test.go new file mode 100644 index 0000000..bceca17 --- /dev/null +++ b/api/managed_account/managed_account_test.go @@ -0,0 +1,301 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package managed_accounts implements functions to retrieve managed accounts +// Unit tests for managed_accounts package. +package managed_accounts + +import ( + "fmt" + "go-client-library-passwordsafe/api/authentication" + "go-client-library-passwordsafe/api/entities" + "go-client-library-passwordsafe/api/logging" + "go-client-library-passwordsafe/api/utils" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "go.uber.org/zap" +) + +type ManagedAccountTestConfig struct { + name string + server *httptest.Server + response *entities.ManagedAccount +} + +type ManagedAccountTestConfigStringResponse struct { + name string + server *httptest.Server + response string +} + +func TestManagedAccountGet(t *testing.T) { + + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + + testConfig := ManagedAccountTestConfig{ + name: "TestManagedAccountGet", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response + _, err := w.Write([]byte(`{"SystemId": 1,"AccountId": 10}`)) + if err != nil { + t.Error("Test case Failed") + } + + })), + response: &entities.ManagedAccount{ + SystemId: 1, + AccountId: 10, + }, + } + authenticate.ApiUrl = testConfig.server.URL + "/" + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + response, err := managedAccountObj.ManagedAccountGet("fake_system_name", "fake_account_name", testConfig.server.URL) + + if response != *testConfig.response { + t.Errorf("Test case Failed %v, %v", response, *testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestManagedAccountCreateRequest(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := ManagedAccountTestConfigStringResponse{ + name: "TestManagedAccountCreateRequest", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response + _, err := w.Write([]byte(`124`)) + if err != nil { + t.Error("Test case Failed") + } + })), + response: "124", + } + + authenticate.ApiUrl = testConfig.server.URL + "/" + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + response, err := managedAccountObj.ManagedAccountCreateRequest(1, 10, testConfig.server.URL) + + if response != testConfig.response { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestCredentialByRequestId(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := ManagedAccountTestConfigStringResponse{ + name: "TestCredentialByRequestId", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response + _, err := w.Write([]byte(`fake_credential`)) + if err != nil { + t.Error("Test case Failed") + } + })), + response: "fake_credential", + } + + authenticate.ApiUrl = testConfig.server.URL + "/" + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + response, err := managedAccountObj.CredentialByRequestId("124", testConfig.server.URL) + + if response != testConfig.response { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestManagedAccountRequestCheckIn(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := ManagedAccountTestConfigStringResponse{ + name: "TestManagedAccountRequestCheckIn", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response + _, err := w.Write([]byte(``)) + if err != nil { + t.Error("Test case Failed") + } + })), + response: "", + } + + authenticate.ApiUrl = testConfig.server.URL + "/" + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + response, err := managedAccountObj.ManagedAccountRequestCheckIn("124", testConfig.server.URL) + + if response != testConfig.response { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestManageAccountFlow(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := ManagedAccountTestConfigStringResponse{ + name: "TestManageAccountFlow", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response accorging to the endpoint path + switch r.URL.Path { + + case "/Auth/SignAppin": + _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/Auth/Signout": + _, err := w.Write([]byte(``)) + if err != nil { + t.Error("Test case Failed") + } + + case "/ManagedAccounts": + _, err := w.Write([]byte(`{"SystemId":1,"AccountId":10}`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/Requests": + _, err := w.Write([]byte(`124`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/Credentials/124": + _, err := w.Write([]byte(`"fake_credential"`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/Requests/124/checkin": + _, err := w.Write([]byte(``)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "fake_credential", + } + + authenticate.ApiUrl = testConfig.server.URL + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + + secretDictionary := make(map[string]string) + managedAccounList := strings.Split("oauthgrp_nocert/Test1,oauthgrp_nocert/client_id", ",") + + response, err := managedAccountObj.ManageAccountFlow(managedAccounList, "/", secretDictionary) + + if response["oauthgrp_nocert/Test1"] != testConfig.response { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestManageAccountFlowNotFound(t *testing.T) { + logger, _ := zap.NewDevelopment() + defer logger.Sync() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := ManagedAccountTestConfigStringResponse{ + name: "TestManageAccountFlowFailedManagedAccounts", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response accorging to the endpoint path + switch r.URL.Path { + + case "/Auth/SignAppin": + w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`)) + + case "/Auth/Signout": + w.Write([]byte(``)) + + case fmt.Sprintf("/ManagedAccounts"): + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`"Managed Account not found"`)) + + case "/Requests": + w.Write([]byte(`124`)) + + case "/Credentials/124": + w.Write([]byte(`"fake_credential"`)) + + case "/Requests/124/checkin": + w.Write([]byte(``)) + + default: + http.NotFound(w, r) + } + })), + response: `got a non 200 status code: 404 - "Managed Account not found"`, + } + + authenticate.ApiUrl = testConfig.server.URL + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + + secretDictionary := make(map[string]string) + managedAccounList := strings.Split("oauthgrp_nocert/Test1,oauthgrp_nocert/client_id", ",") + + _, err := managedAccountObj.ManageAccountFlow(managedAccounList, "/", secretDictionary) + + if err.Error() != testConfig.response { + t.Errorf("Test case Failed %v, %v", err.Error(), testConfig.response) + } +} diff --git a/api/secrets/secrets.go b/api/secrets/secrets.go new file mode 100644 index 0000000..3e72c71 --- /dev/null +++ b/api/secrets/secrets.go @@ -0,0 +1,167 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package secrets implements Get secret logic +package secrets + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "go-client-library-passwordsafe/api/authentication" + "go-client-library-passwordsafe/api/entities" + "go-client-library-passwordsafe/api/logging" + "io" + "net/url" + "strings" + + backoff "github.com/cenkalti/backoff/v4" +) + +type SecretObj struct { + log logging.Logger + authenticationObj authentication.AuthenticationObj +} + +// NewSecretObj creates secret obj +func NewSecretObj(authentication authentication.AuthenticationObj, logger logging.Logger) (*SecretObj, error) { + secretObj := &SecretObj{ + log: logger, + authenticationObj: authentication, + } + return secretObj, nil +} + +// GetSecrets returns secret value for a path and title list. +func (secretObj *SecretObj) GetSecrets(secretsList []string, separator string) (map[string]string, error) { + if separator == "" { + separator = "" + } + return secretObj.GetSecretFlow(secretsList, separator) +} + +// GetSecret returns secret value for a specific path and title. +func (secretObj *SecretObj) GetSecret(secretsList []string, separator string) (map[string]string, error) { + return secretObj.GetSecretFlow(secretsList, separator) +} + +// GetSecretFlow returns secret value for a specific path and title list +func (secretObj *SecretObj) GetSecretFlow(secretsToRetrieve []string, separator string) (map[string]string, error) { + + secretDictionary := make(map[string]string) + + for _, secretToRetrieve := range secretsToRetrieve { + secretData := strings.Split(secretToRetrieve, separator) + + secretPath := secretData[0] + secretTitle := secretData[1] + + secret, err := secretObj.SecretGetSecretByPath(secretPath, secretTitle, separator, "secrets-safe/secrets") + + if err != nil { + return nil, err + } + + // When secret type is FILE, it calls SecretGetFileSecret method. + if strings.ToUpper(secret.SecretType) == "FILE" { + fileSecretContent, err := secretObj.SecretGetFileSecret(secret.Id, "secrets-safe/secrets/") + if err != nil { + secretObj.log.Error(err.Error()) + return nil, err + } + + secretDictionary[secretToRetrieve] = fileSecretContent + } + secretDictionary[secretToRetrieve] = secret.Password + + } + + return secretDictionary, nil +} + +// SecretGetSecretByPath returns secret object for a specific path, title. +func (secretObj *SecretObj) SecretGetSecretByPath(secretPath string, secretTitle string, separator string, endpointPath string) (entities.Secret, error) { + messageLog := fmt.Sprintf("%v %v", "GET", endpointPath) + secretObj.log.Debug(messageLog) + + var body io.ReadCloser + var technicalError error + var businessError error + var scode int + + params := url.Values{} + params.Add("path", secretPath) + params.Add("title", secretTitle) + + url := fmt.Sprintf("%s%s?%s", secretObj.authenticationObj.ApiUrl, endpointPath, params.Encode()) + + technicalError = backoff.Retry(func() error { + body, technicalError, businessError, scode = secretObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "SecretGetSecretByPath", "") + return technicalError + }, secretObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return entities.Secret{}, technicalError + } + + if businessError != nil { + return entities.Secret{}, businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return entities.Secret{}, err + } + + var SecretObjectList []entities.Secret + err = json.Unmarshal([]byte(bodyBytes), &SecretObjectList) + if err != nil { + err = errors.New(err.Error() + ", Ensure Password Safe version is 23.1 or greater.") + return entities.Secret{}, err + } + + if len(SecretObjectList) == 0 { + scode = 404 + err = fmt.Errorf("Error %v: StatusCode: %v ", "SecretGetSecretByPath, Secret was not found", scode) + return entities.Secret{}, err + } + + return SecretObjectList[0], nil +} + +// SecretGetFileSecret call secrets-safe/secrets//file/download enpoint +// and returns file secret value. +func (secretObj *SecretObj) SecretGetFileSecret(secretId string, endpointPath string) (string, error) { + messageLog := fmt.Sprintf("%v %v", "GET", endpointPath) + secretObj.log.Debug(messageLog) + + var body io.ReadCloser + var technicalError error + var businessError error + + url := fmt.Sprintf("%s%s%s%s", secretObj.authenticationObj.ApiUrl, endpointPath, secretId, "/file/download") + + technicalError = backoff.Retry(func() error { + body, technicalError, businessError, _ = secretObj.authenticationObj.HttpClient.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "SecretGetFileSecret", "") + return technicalError + }, secretObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + defer body.Close() + responseData, err := io.ReadAll(body) + if err != nil { + return "", err + } + + responseString := string(responseData) + return responseString, nil + +} diff --git a/api/secrets/secrets_test.go b/api/secrets/secrets_test.go new file mode 100644 index 0000000..33c8cbb --- /dev/null +++ b/api/secrets/secrets_test.go @@ -0,0 +1,218 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package secrets implements functions to retrieve secrets +// Unit tests for secrets package. +package secrets + +import ( + "go-client-library-passwordsafe/api/authentication" + "go-client-library-passwordsafe/api/entities" + "go-client-library-passwordsafe/api/logging" + "go-client-library-passwordsafe/api/utils" + "strings" + + "net/http" + "net/http/httptest" + "testing" + + "go.uber.org/zap" +) + +type SecretTestConfig struct { + name string + server *httptest.Server + response *entities.Secret +} + +type SecretTestConfigStringResponse struct { + name string + server *httptest.Server + response string +} + +func TestSecretGetSecretByPath(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := SecretTestConfig{ + name: "TestSecretGetSecretByPath", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response + _, err := w.Write([]byte(`[{"Password": "credential_in_sub_3_password","Id": "9152f5b6-07d6-4955-175a-08db047219ce","Title": "credential_in_sub_3"}]`)) + if err != nil { + t.Error("Test case Failed") + } + })), + response: &entities.Secret{ + Id: "9152f5b6-07d6-4955-175a-08db047219ce", + Title: "credential_in_sub_3", + Password: "credential_in_sub_3_password", + }, + } + + authenticate.ApiUrl = testConfig.server.URL + "/" + secretObj, _ := NewSecretObj(*authenticate, zapLogger) + + response, err := secretObj.SecretGetSecretByPath("path1/path2", "fake_title", "/", "secrets-safe/secrets") + + if response != *testConfig.response { + t.Errorf("Test case Failed %v, %v", response, *testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestSecretGetFileSecret(t *testing.T) { + logger, _ := zap.NewDevelopment() + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := SecretTestConfig{ + name: "TestSecretGetFileSecret", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, err := w.Write([]byte(`fake_password`)) + + if err != nil { + t.Error("Test case Failed") + } + })), + } + + authenticate.ApiUrl = testConfig.server.URL + "/" + secretObj, _ := NewSecretObj(*authenticate, zapLogger) + response, err := secretObj.SecretGetFileSecret("1", testConfig.server.URL) + + if response != "fake_password" { + t.Errorf("Test case Failed %v, %v", response, *testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestSecretFlow(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := SecretTestConfigStringResponse{ + name: "TestSecretFlow", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response accorging to the endpoint path + switch r.URL.Path { + + case "/Auth/SignAppin": + _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/Auth/Signout": + _, err := w.Write([]byte(``)) + if err != nil { + t.Error("Test case Failed") + } + + case "/secrets-safe/secrets": + _, err := w.Write([]byte(`[{"SecretType": "FILE", "Password": "credential_in_sub_3_password","Id": "9152f5b6-07d6-4955-175a-08db047219ce","Title": "credential_in_sub_3"}]`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/secrets-safe/secrets/9152f5b6-07d6-4955-175a-08db047219ce/file/download": + _, err := w.Write([]byte(`fake_password`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "credential_in_sub_3_password", + } + + authenticate.ApiUrl = testConfig.server.URL + "/" + secretObj, _ := NewSecretObj(*authenticate, zapLogger) + + secretList := strings.Split("oauthgrp_nocert/Test1,oauthgrp_nocert/client_id", ",") + response, err := secretObj.GetSecretFlow(secretList, "/") + + if response["oauthgrp_nocert/Test1"] != testConfig.response { + t.Errorf("Test case Failed %v, %v", response, testConfig.response) + } + + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestSecretFlow_SecretNotFound(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + var authenticate, _ = authentication.Authenticate(*httpClientObj, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := SecretTestConfigStringResponse{ + name: "TestSecretFlow", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response accorging to the endpoint path + switch r.URL.Path { + + case "/Auth/SignAppin": + _, err := w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/Auth/Signout": + _, err := w.Write([]byte(``)) + if err != nil { + t.Error("Test case Failed") + } + + case "/secrets-safe/secrets": + _, err := w.Write([]byte(`[]`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "Error SecretGetSecretByPath, Secret was not found: StatusCode: 404 ", + } + + authenticate.ApiUrl = testConfig.server.URL + "/" + secretObj, _ := NewSecretObj(*authenticate, zapLogger) + + secretList := strings.Split("oauthgrp_nocert/Test1,oauthgrp_nocert/client_id", ",") + _, err := secretObj.GetSecretFlow(secretList, "/") + + if err == nil { + t.Errorf("Test case Failed: %v", err) + } + + 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 new file mode 100644 index 0000000..58e3eb8 --- /dev/null +++ b/api/utils/httpclient.go @@ -0,0 +1,116 @@ +package utils + +import ( + "bytes" + "crypto/tls" + "fmt" + logging "go-client-library-passwordsafe/api/logging" + "io" + "net/http" + "net/http/cookiejar" + "time" +) + +type HttpClientObj struct { + HttpClient *http.Client + log logging.Logger +} + +func GetHttpClient(clientTimeOut int, verifyCa bool, certificate string, certificate_key string, logger logging.Logger) (*HttpClientObj, error) { + var cert tls.Certificate + + if certificate != "" && certificate_key != "" { + certi, err := tls.X509KeyPair([]byte(certificate), []byte(certificate_key)) + + if err != nil { + return nil, err + } + + cert = certi + } + + // TSL Config + var tr = &http.Transport{ + TLSClientConfig: &tls.Config{ + Renegotiation: tls.RenegotiateOnceAsClient, + InsecureSkipVerify: !verifyCa, + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS12, + }, + } + + var jar, _ = cookiejar.New(nil) + + // Client + var client = &http.Client{ + Transport: tr, + Jar: jar, + Timeout: time.Second * time.Duration(clientTimeOut), + } + + httpClientObj := &HttpClientObj{ + HttpClient: client, + log: logger, + } + + return httpClientObj, nil +} + +// CallSecretSafeAPI prepares http call +func (client *HttpClientObj) CallSecretSafeAPI(url string, httpMethod string, body bytes.Buffer, method string, accesToken string) (io.ReadCloser, error, error, int) { + response, technicalError, businessError, scode := client.HttpRequest(url, httpMethod, body, accesToken) + if technicalError != nil { + messageLog := fmt.Sprintf("Error in %v %v \n", method, technicalError) + client.log.Error(messageLog) + } + + if businessError != nil { + messageLog := fmt.Sprintf("Error in %v: %v \n", method, businessError) + client.log.Debug(messageLog) + } + return response, technicalError, businessError, scode +} + +// HttpRequest makes http request to he server +func (client *HttpClientObj) HttpRequest(url string, method string, body bytes.Buffer, accesToken string) (closer io.ReadCloser, technicalError error, businessError error, scode int) { + + req, err := http.NewRequest(method, url, &body) + if err != nil { + return nil, err, nil, 0 + } + req.Header = http.Header{ + "Content-Type": {"application/json"}, + } + + if accesToken != "" { + req.Header.Set("Authorization", "Bearer "+accesToken) + } + + resp, err := client.HttpClient.Do(req) + if err != nil { + client.log.Error(fmt.Sprintf("%v %v", "Error Making request: ", err.Error())) + return nil, err, nil, 0 + } + + fmt.Println(resp) + if resp.StatusCode >= http.StatusInternalServerError || resp.StatusCode == http.StatusRequestTimeout { + err = fmt.Errorf("error %v: StatusCode: %v, %v, %v", method, scode, err, body) + client.log.Error(err.Error()) + return nil, err, nil, resp.StatusCode + } + + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + respBody := new(bytes.Buffer) + _, err = respBody.ReadFrom(resp.Body) + if err != nil { + client.log.Error(err.Error()) + return nil, err, nil, 0 + } + + err = fmt.Errorf("got a non 200 status code: %v - %v", resp.StatusCode, respBody) + return nil, nil, err, resp.StatusCode + } + + return resp.Body, nil, nil, resp.StatusCode +} diff --git a/api/utils/validator.go b/api/utils/validator.go new file mode 100644 index 0000000..130ebc2 --- /dev/null +++ b/api/utils/validator.go @@ -0,0 +1,102 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package utils implements inputs validations +package utils + +import ( + "errors" + logging "go-client-library-passwordsafe/api/logging" + "strings" + "unicode/utf8" + + validator "github.com/go-playground/validator/v10" +) + +type UserInputValidaton struct { + ClientId string `validate:"required,min=36,max=36"` + ClientSecret string `validate:"required,min=36,max=64"` + ApiUrl string `validate:"required,http_url"` + ClientTimeOutinSeconds int `validate:"gte=1,lte=301"` + Separator string `validate:"required,min=1,max=1"` + VerifyCa bool `validate:"required"` +} + +var validate *validator.Validate + +// ValidateInputs validate inputs +func ValidateInputs(clientId string, clientSecret string, apiUrl string, clientTimeOutinSeconds int, separator *string, verifyCa bool, logger logging.Logger, certificate string, certificate_key string) error { + + validate = validator.New(validator.WithRequiredStructEnabled()) + + userInput := &UserInputValidaton{ + ClientId: clientId, + ClientSecret: clientSecret, + ApiUrl: apiUrl, + ClientTimeOutinSeconds: clientTimeOutinSeconds, + Separator: *separator, + VerifyCa: verifyCa, + } + + if strings.TrimSpace(*separator) == "" { + *separator = "/" + } + + err := validate.Struct(userInput) + if err != nil { + logger.Error(err.Error()) + return err + } + + message := "" + + if certificate != "" && certificate_key != "" { + + certificateLengthInBits := utf8.RuneCountInString(certificate) * 8 + + if certificateLengthInBits > 32768 { + message = "Invalid length for certificate" + logger.Error(message) + return errors.New(message) + } + + certificateKeyLengthInBits := utf8.RuneCountInString(certificate_key) * 8 + + if certificateKeyLengthInBits > 32768 { + message = "Invalid length for certificate key" + logger.Error(message) + return errors.New(message) + } + + if !strings.HasPrefix(certificate, "-----BEGIN CERTIFICATE-----") || !strings.HasSuffix(certificate, "-----END CERTIFICATE-----") { + message = "Invalid certificate content" + logger.Error(message) + return errors.New(message) + } + + if !strings.HasPrefix(certificate_key, "-----BEGIN PRIVATE KEY-----") || !strings.HasSuffix(certificate_key, "-----END PRIVATE KEY-----") { + message = "Invalid certificate key content" + logger.Error(message) + return errors.New(message) + } + + } + + if !strings.Contains(apiUrl, "/BeyondTrust/api/public/v3/") { + message = "Invalid API URL, it should contains /BeyondTrust/api/public/v3/ as path" + logger.Error(message) + return errors.New(message) + } + + logger.Debug("Validation passed!") + //Logging("DEBUG", "Validation passed!", *logger) + return nil +} + +// ValidatePaths validate path +func ValidatePath(path string) error { + message := "" + if len(path) > 303 { + message = "Invalid Path Lenght" + return errors.New(message) + } + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1863417 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module go-client-library-passwordsafe + +go 1.20 + +require ( + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.18.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..eeee033 --- /dev/null +++ b/go.sum @@ -0,0 +1,28 @@ +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= +github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +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= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/main.go b/main.go new file mode 100644 index 0000000..69fb4eb --- /dev/null +++ b/main.go @@ -0,0 +1,99 @@ +package main + +import ( + "fmt" + "go-client-library-passwordsafe/api/authentication" + logging "go-client-library-passwordsafe/api/logging" + managed_accounts "go-client-library-passwordsafe/api/managed_account" + "go-client-library-passwordsafe/api/secrets" + "go-client-library-passwordsafe/api/utils" + "strings" + + "go.uber.org/zap" +) + +//var logger = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime) + +// main funtion +func main() { + + //logFile, _ := os.Create("ProviderLogs.log") + //logger.SetOutput(logFile) + + // create a zap logger + //logger, _ := zap.NewProduction() + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + apiUrl := "https://jury2310.ps-dev.beyondtrustcloud.com:443/BeyondTrust/api/public/v3/" + clientId := "" + clientSecret := "" + separator := "/" + certificate := "" + certificate_key := "" + clientTimeOutinSeconds := 5 + verifyCa := true + maxElapsedTime := 15 + + // validate inputs + errors_in_inputs := utils.ValidateInputs(clientId, clientSecret, apiUrl, clientTimeOutinSeconds, &separator, verifyCa, zapLogger, certificate, certificate_key) + + if errors_in_inputs != nil { + return + } + + // creating a http client + httpClientObj, _ := utils.GetHttpClient(clientTimeOutinSeconds, verifyCa, certificate, certificate_key, zapLogger) + + // instantiating authenticate obj, injecting httpClient object + authenticate, _ := authentication.Authenticate(*httpClientObj, apiUrl, clientId, clientSecret, zapLogger, maxElapsedTime) + + // authenticating in PS API + _, err := authenticate.GetPasswordSafeAuthentication() + if err != nil { + return + } + + // instantiating secret obj + secretObj, _ := secrets.NewSecretObj(*authenticate, zapLogger) + + paths := "oauthgrp/text1,oauthgrp/text2" + errors_in_path := utils.ValidatePath(paths) + if errors_in_path != nil { + return + } + + // getting secrets + secretList := strings.Split(paths, ",") + gotSecrets, _ := secretObj.GetSecrets(secretList, separator) + zapLogger.Info(fmt.Sprintf("%v", gotSecrets)) + + // getting single secret + secretList = strings.Split("oauthgrp/text1", ",") + gotSecret, _ := secretObj.GetSecret(secretList, separator) + zapLogger.Info(fmt.Sprintf("%v", gotSecret)) + + // instantiating managed account obj + manageAccountObj, _ := managed_accounts.NewManagedAccountObj(*authenticate, zapLogger) + + paths = "system01/managed_account01,system02/managed_account01" + errors_in_path = utils.ValidatePath(paths) + if errors_in_path != nil { + return + } + + managedAccountList := strings.Split(paths, ",") + gotManagedAccounts, _ := manageAccountObj.GetSecrets(managedAccountList, separator) + zapLogger.Info(fmt.Sprintf("%v", gotManagedAccounts)) + + // getting single managed account + managedAccountList = []string{} + gotManagedAccount, _ := manageAccountObj.GetSecret(append(managedAccountList, "system01/managed_account01"), separator) + zapLogger.Info(fmt.Sprintf("%v", gotManagedAccount)) + + // signing out + _ = authenticate.SignOut(fmt.Sprintf("%v%v", authenticate.ApiUrl, "Auth/Signout")) + +} From 332dd550695f9b234944ee3298bc9d503829a3d5 Mon Sep 17 00:00:00 2001 From: "EPAM\\Felipe_Hernandez" Date: Fri, 23 Feb 2024 14:28:44 -0500 Subject: [PATCH 2/2] feat: solve PR comments --- main.go => TestClient.go | 30 +++++++----- api/authentication/authentication_test.go | 2 +- api/authentication/authetication.go | 21 ++++---- api/logging/logging.go | 2 + api/managed_account/managed_account.go | 34 +++++-------- api/managed_account/managed_account_test.go | 6 +-- api/secrets/secrets.go | 9 ++-- api/secrets/secrets_test.go | 8 +-- api/utils/httpclient.go | 8 +-- api/utils/validator.go | 54 +++++++++++++++++---- 10 files changed, 101 insertions(+), 73 deletions(-) rename main.go => TestClient.go (70%) diff --git a/main.go b/TestClient.go similarity index 70% rename from main.go rename to TestClient.go index 69fb4eb..08301a5 100644 --- a/main.go +++ b/TestClient.go @@ -12,8 +12,6 @@ import ( "go.uber.org/zap" ) -//var logger = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime) - // main funtion func main() { @@ -27,30 +25,30 @@ func main() { // create a zap logger wrapper zapLogger := logging.NewZapLogger(logger) - apiUrl := "https://jury2310.ps-dev.beyondtrustcloud.com:443/BeyondTrust/api/public/v3/" + apiUrl := "https://example.com:443/BeyondTrust/api/public/v3/" clientId := "" clientSecret := "" separator := "/" certificate := "" certificate_key := "" - clientTimeOutinSeconds := 5 + clientTimeOutInSeconds := 5 verifyCa := true maxElapsedTime := 15 // validate inputs - errors_in_inputs := utils.ValidateInputs(clientId, clientSecret, apiUrl, clientTimeOutinSeconds, &separator, verifyCa, zapLogger, certificate, certificate_key) + errors_in_inputs := utils.ValidateInputs(clientId, clientSecret, apiUrl, clientTimeOutInSeconds, &separator, verifyCa, zapLogger, certificate, certificate_key) if errors_in_inputs != nil { return } // creating a http client - httpClientObj, _ := utils.GetHttpClient(clientTimeOutinSeconds, verifyCa, certificate, certificate_key, zapLogger) + httpClientObj, _ := utils.GetHttpClient(clientTimeOutInSeconds, verifyCa, certificate, certificate_key, zapLogger) // instantiating authenticate obj, injecting httpClient object authenticate, _ := authentication.Authenticate(*httpClientObj, apiUrl, clientId, clientSecret, zapLogger, maxElapsedTime) - // authenticating in PS API + // authenticating _, err := authenticate.GetPasswordSafeAuthentication() if err != nil { return @@ -59,7 +57,7 @@ func main() { // instantiating secret obj secretObj, _ := secrets.NewSecretObj(*authenticate, zapLogger) - paths := "oauthgrp/text1,oauthgrp/text2" + paths := "fake/text1,fake/text2" errors_in_path := utils.ValidatePath(paths) if errors_in_path != nil { return @@ -68,17 +66,20 @@ func main() { // getting secrets secretList := strings.Split(paths, ",") gotSecrets, _ := secretObj.GetSecrets(secretList, separator) + + // WARNING: Do not log secrets in production code, the following log statement logs test secrets for testing purposes: zapLogger.Info(fmt.Sprintf("%v", gotSecrets)) // getting single secret - secretList = strings.Split("oauthgrp/text1", ",") - gotSecret, _ := secretObj.GetSecret(secretList, separator) + gotSecret, _ := secretObj.GetSecret("fake/text1", separator) + + // WARNING: Do not log secrets in production code, the following log statement logs test secrets for testing purposes: zapLogger.Info(fmt.Sprintf("%v", gotSecret)) // instantiating managed account obj manageAccountObj, _ := managed_accounts.NewManagedAccountObj(*authenticate, zapLogger) - paths = "system01/managed_account01,system02/managed_account01" + paths = "fake/account01,fake/account02" errors_in_path = utils.ValidatePath(paths) if errors_in_path != nil { return @@ -86,11 +87,14 @@ func main() { managedAccountList := strings.Split(paths, ",") gotManagedAccounts, _ := manageAccountObj.GetSecrets(managedAccountList, separator) + + // WARNING: Do not log secrets in production code, the following log statement logs test secrets for testing purposes: zapLogger.Info(fmt.Sprintf("%v", gotManagedAccounts)) // getting single managed account - managedAccountList = []string{} - gotManagedAccount, _ := manageAccountObj.GetSecret(append(managedAccountList, "system01/managed_account01"), separator) + gotManagedAccount, _ := manageAccountObj.GetSecret("fake/account01", separator) + + // WARNING: Do not log secrets in production code, the following log statement logs test secrets for testing purposes: zapLogger.Info(fmt.Sprintf("%v", gotManagedAccount)) // signing out diff --git a/api/authentication/authentication_test.go b/api/authentication/authentication_test.go index 377e496..8d75d7a 100644 --- a/api/authentication/authentication_test.go +++ b/api/authentication/authentication_test.go @@ -107,7 +107,7 @@ func TestGetToken(t *testing.T) { testConfig := GetTokenConfig{ name: "TestGetToken", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Mocking Response accorging to the endpoint path + // Mocking Response according to the endpoint path switch r.URL.Path { case "/Auth/connect/token": diff --git a/api/authentication/authetication.go b/api/authentication/authetication.go index 56affb4..818ea1d 100644 --- a/api/authentication/authetication.go +++ b/api/authentication/authetication.go @@ -26,7 +26,7 @@ type AuthenticationObj struct { log logging.Logger } -// Authenticate in PS API +// Authenticate is responsible for Auth configuration. func Authenticate(httpClient utils.HttpClientObj, endpointUrl string, clientId string, clientSecret string, logger logging.Logger, retryMaxElapsedTimeSeconds int) (*AuthenticationObj, error) { backoffDefinition := backoff.NewExponentialBackOff() @@ -34,12 +34,9 @@ func Authenticate(httpClient utils.HttpClientObj, endpointUrl string, clientId s backoffDefinition.MaxElapsedTime = time.Duration(retryMaxElapsedTimeSeconds) * time.Second backoffDefinition.RandomizationFactor = 0.5 - // Client - var client = httpClient - authenticationObj := &AuthenticationObj{ ApiUrl: endpointUrl, - HttpClient: client, + HttpClient: httpClient, clientId: clientId, clientSecret: clientSecret, ExponentialBackOff: backoffDefinition, @@ -49,7 +46,7 @@ func Authenticate(httpClient utils.HttpClientObj, endpointUrl string, clientId s return authenticationObj, nil } -// GetPasswordSafeAuthentication call get token and sign app endpoint +// GetPasswordSafeAuthentication is responsible for getting a token and signing in. func (authenticationObj *AuthenticationObj) GetPasswordSafeAuthentication() (entities.SignApinResponse, error) { accessToken, err := authenticationObj.GetToken(fmt.Sprintf("%v%v", authenticationObj.ApiUrl, "Auth/connect/token"), authenticationObj.clientId, authenticationObj.clientSecret) if err != nil { @@ -62,7 +59,7 @@ func (authenticationObj *AuthenticationObj) GetPasswordSafeAuthentication() (ent return signApinResponse, nil } -// GetToken get token from PS API +// GetToken is responsible for getting a token from the PS API. func (authenticationObj *AuthenticationObj) GetToken(endpointUrl string, clientId string, clientSecret string) (string, error) { params := url.Values{} @@ -107,11 +104,13 @@ func (authenticationObj *AuthenticationObj) GetToken(endpointUrl string, clientI return "", err } + authenticationObj.log.Debug("Successfully retrieved token") + return data.AccessToken, nil } -// SignAppin Signs app in PS API +// SignAppin is responsible for creating a PS API session. func (authenticationObj *AuthenticationObj) SignAppin(endpointUrl string, accessToken string) (entities.SignApinResponse, error) { var userObject entities.SignApinResponse @@ -152,11 +151,11 @@ func (authenticationObj *AuthenticationObj) SignAppin(endpointUrl string, access authenticationObj.log.Error(err.Error()) return entities.SignApinResponse{}, err } - + authenticationObj.log.Debug("Successfully Signed App In") return userObject, nil } -// SignOut signs out Secret Safe API. +// SignOut is responsible for closing the PS API session and cleaning up idle connections. // Warn: should only be called one time for all data sources. func (authenticationObj *AuthenticationObj) SignOut(url string) error { authenticationObj.log.Debug(url) @@ -177,6 +176,6 @@ func (authenticationObj *AuthenticationObj) SignOut(url string) error { } defer authenticationObj.HttpClient.HttpClient.CloseIdleConnections() - + authenticationObj.log.Debug("Successfully Signed out.") return nil } diff --git a/api/logging/logging.go b/api/logging/logging.go index ee8b7fd..41b1052 100644 --- a/api/logging/logging.go +++ b/api/logging/logging.go @@ -73,6 +73,7 @@ func (l *LogLogger) Error(msg string) { l.logger.Println(msg) } +// Debug logs a message at debug level func (l *LogLogger) Debug(msg string) { prefix := fmt.Sprintf("%v :", "Debug") l.logger.SetPrefix(prefix) @@ -84,6 +85,7 @@ func NewZapLogger(logger *zap.Logger) *ZapLogger { return &ZapLogger{logger: logger} } +// NewLogrLogger creates a new logrLogger with the given logr.logger func NewLogrLogger(logger *logr.Logger) *LogrLogger { return &LogrLogger{logger: logger} } diff --git a/api/managed_account/managed_account.go b/api/managed_account/managed_account.go index ef19663..fc31299 100644 --- a/api/managed_account/managed_account.go +++ b/api/managed_account/managed_account.go @@ -6,11 +6,11 @@ package managed_accounts import ( "bytes" "encoding/json" - "errors" "fmt" "go-client-library-passwordsafe/api/authentication" "go-client-library-passwordsafe/api/entities" "go-client-library-passwordsafe/api/logging" + "go-client-library-passwordsafe/api/utils" "io" "strconv" "strings" @@ -32,34 +32,33 @@ func NewManagedAccountObj(authentication authentication.AuthenticationObj, logge return managedAccounObj, nil } -// GetSecrets returns secret value for a System Name and Account Name list. -func (managedAccounObj *ManagedAccountstObj) GetSecrets(secretsList []string, separator string) (map[string]string, error) { +// GetSecrets is responsible for getting a list of managed account secret values based on the list of systems and account names. +func (managedAccounObj *ManagedAccountstObj) GetSecrets(secretPaths []string, separator string) (map[string]string, error) { if separator == "" { separator = "" } - return managedAccounObj.ManageAccountFlow(secretsList, separator, make(map[string]string)) + return managedAccounObj.ManageAccountFlow(secretPaths, separator, make(map[string]string)) } // GetSecret returns secret value for a specific System Name and Account Name. -func (managedAccounObj *ManagedAccountstObj) GetSecret(secretsList []string, separator string) (map[string]string, error) { - return managedAccounObj.ManageAccountFlow(secretsList, separator, make(map[string]string)) +func (managedAccounObj *ManagedAccountstObj) GetSecret(secretPath string, separator string) (map[string]string, error) { + managedAccountList := []string{} + return managedAccounObj.ManageAccountFlow(append(managedAccountList, secretPath), separator, make(map[string]string)) } -// ManageAccountFlow returns value for a specific System Name and Account Name. +// ManageAccountFlow is responsible for creating a dictionary of managed account system/name and secret key-value pairs. func (managedAccounObj *ManagedAccountstObj) ManageAccountFlow(secretsToRetrieve []string, separator string, paths map[string]string) (map[string]string, error) { secretDictionary := make(map[string]string) - for _, secretToRetrieve := range secretsToRetrieve { + secretsToRetrieve, _ = utils.ValidatePaths(secretsToRetrieve, separator, managedAccounObj.log) + for _, secretToRetrieve := range secretsToRetrieve { secretData := strings.Split(secretToRetrieve, separator) systemName := secretData[0] accountName := secretData[1] - systemName = strings.TrimSpace(systemName) - accountName = strings.TrimSpace(accountName) - if len(paths) == 0 { paths["SignAppinPath"] = "Auth/SignAppin" paths["SignAppOutPath"] = "Auth/Signout" @@ -71,18 +70,6 @@ func (managedAccounObj *ManagedAccountstObj) ManageAccountFlow(secretsToRetrieve var err error - if systemName == "" { - err = errors.New("Please use a valid system_name value") - managedAccounObj.log.Error(err.Error()) - return nil, err - } - - if accountName == "" { - err = errors.New("Please use a valid system_name value") - managedAccounObj.log.Error(err.Error()) - return nil, err - } - ManagedAccountGetUrl := managedAccounObj.RequestPath(paths["ManagedAccountGetPath"]) managedAccount, err := managedAccounObj.ManagedAccountGet(systemName, accountName, ManagedAccountGetUrl) if err != nil { @@ -120,6 +107,7 @@ func (managedAccounObj *ManagedAccountstObj) ManageAccountFlow(secretsToRetrieve return secretDictionary, nil } +// ManagedAccountGet is responsible for retrieving a managed account secret based on the system and name. func (managedAccounObj *ManagedAccountstObj) ManagedAccountGet(systemName string, accountName string, url string) (entities.ManagedAccount, error) { messageLog := fmt.Sprintf("%v %v", "GET", url) managedAccounObj.log.Debug(messageLog) diff --git a/api/managed_account/managed_account_test.go b/api/managed_account/managed_account_test.go index bceca17..9b3aeae 100644 --- a/api/managed_account/managed_account_test.go +++ b/api/managed_account/managed_account_test.go @@ -182,7 +182,7 @@ func TestManageAccountFlow(t *testing.T) { testConfig := ManagedAccountTestConfigStringResponse{ name: "TestManageAccountFlow", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Mocking Response accorging to the endpoint path + // Mocking Response according to the endpoint path switch r.URL.Path { case "/Auth/SignAppin": @@ -258,7 +258,7 @@ func TestManageAccountFlowNotFound(t *testing.T) { testConfig := ManagedAccountTestConfigStringResponse{ name: "TestManageAccountFlowFailedManagedAccounts", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Mocking Response accorging to the endpoint path + // Mocking Response according to the endpoint path switch r.URL.Path { case "/Auth/SignAppin": @@ -284,7 +284,7 @@ func TestManageAccountFlowNotFound(t *testing.T) { http.NotFound(w, r) } })), - response: `got a non 200 status code: 404 - "Managed Account not found"`, + response: `error - status code: 404 - "Managed Account not found"`, } authenticate.ApiUrl = testConfig.server.URL diff --git a/api/secrets/secrets.go b/api/secrets/secrets.go index 3e72c71..fc3d18f 100644 --- a/api/secrets/secrets.go +++ b/api/secrets/secrets.go @@ -1,5 +1,5 @@ // Copyright 2024 BeyondTrust. All rights reserved. -// Package secrets implements Get secret logic +// Package secrets implements Get secret logic for Secrets Safe (cred, text, file) package secrets import ( @@ -40,11 +40,12 @@ func (secretObj *SecretObj) GetSecrets(secretsList []string, separator string) ( } // GetSecret returns secret value for a specific path and title. -func (secretObj *SecretObj) GetSecret(secretsList []string, separator string) (map[string]string, error) { - return secretObj.GetSecretFlow(secretsList, separator) +func (secretObj *SecretObj) GetSecret(secretPath string, separator string) (map[string]string, error) { + secretList := []string{} + return secretObj.GetSecretFlow(append(secretList, secretPath), separator) } -// GetSecretFlow returns secret value for a specific path and title list +// GetSecretFlow is responsible for creating a dictionary of secrets safe secret paths and secret key-value pairs. func (secretObj *SecretObj) GetSecretFlow(secretsToRetrieve []string, separator string) (map[string]string, error) { secretDictionary := make(map[string]string) diff --git a/api/secrets/secrets_test.go b/api/secrets/secrets_test.go index 33c8cbb..b31b378 100644 --- a/api/secrets/secrets_test.go +++ b/api/secrets/secrets_test.go @@ -112,7 +112,7 @@ func TestSecretFlow(t *testing.T) { testConfig := SecretTestConfigStringResponse{ name: "TestSecretFlow", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Mocking Response accorging to the endpoint path + // Mocking Response according to the endpoint path switch r.URL.Path { case "/Auth/SignAppin": @@ -149,8 +149,8 @@ func TestSecretFlow(t *testing.T) { authenticate.ApiUrl = testConfig.server.URL + "/" secretObj, _ := NewSecretObj(*authenticate, zapLogger) - secretList := strings.Split("oauthgrp_nocert/Test1,oauthgrp_nocert/client_id", ",") - response, err := secretObj.GetSecretFlow(secretList, "/") + secretsPaths := strings.Split("oauthgrp_nocert/Test1,oauthgrp_nocert/client_id", ",") + response, err := secretObj.GetSecretFlow(secretsPaths, "/") if response["oauthgrp_nocert/Test1"] != testConfig.response { t.Errorf("Test case Failed %v, %v", response, testConfig.response) @@ -173,7 +173,7 @@ func TestSecretFlow_SecretNotFound(t *testing.T) { testConfig := SecretTestConfigStringResponse{ name: "TestSecretFlow", server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Mocking Response accorging to the endpoint path + // Mocking Response according to the endpoint path switch r.URL.Path { case "/Auth/SignAppin": diff --git a/api/utils/httpclient.go b/api/utils/httpclient.go index 58e3eb8..25fa045 100644 --- a/api/utils/httpclient.go +++ b/api/utils/httpclient.go @@ -16,6 +16,7 @@ type HttpClientObj struct { log logging.Logger } +// GetHttpClient is responsible for configuring an HTTP client and transport for API calls. func GetHttpClient(clientTimeOut int, verifyCa bool, certificate string, certificate_key string, logger logging.Logger) (*HttpClientObj, error) { var cert tls.Certificate @@ -72,7 +73,7 @@ func (client *HttpClientObj) CallSecretSafeAPI(url string, httpMethod string, bo return response, technicalError, businessError, scode } -// HttpRequest makes http request to he server +// HttpRequest makes http request to the server. func (client *HttpClientObj) HttpRequest(url string, method string, body bytes.Buffer, accesToken string) (closer io.ReadCloser, technicalError error, businessError error, scode int) { req, err := http.NewRequest(method, url, &body) @@ -93,14 +94,13 @@ func (client *HttpClientObj) HttpRequest(url string, method string, body bytes.B return nil, err, nil, 0 } - fmt.Println(resp) if resp.StatusCode >= http.StatusInternalServerError || resp.StatusCode == http.StatusRequestTimeout { err = fmt.Errorf("error %v: StatusCode: %v, %v, %v", method, scode, err, body) client.log.Error(err.Error()) return nil, err, nil, resp.StatusCode } - if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + if resp.StatusCode > http.StatusBadRequest { respBody := new(bytes.Buffer) _, err = respBody.ReadFrom(resp.Body) if err != nil { @@ -108,7 +108,7 @@ func (client *HttpClientObj) HttpRequest(url string, method string, body bytes.B return nil, err, nil, 0 } - err = fmt.Errorf("got a non 200 status code: %v - %v", resp.StatusCode, respBody) + err = fmt.Errorf("error - status code: %v - %v", resp.StatusCode, respBody) return nil, nil, err, resp.StatusCode } diff --git a/api/utils/validator.go b/api/utils/validator.go index 130ebc2..81a783d 100644 --- a/api/utils/validator.go +++ b/api/utils/validator.go @@ -4,6 +4,7 @@ package utils import ( "errors" + "fmt" logging "go-client-library-passwordsafe/api/logging" "strings" "unicode/utf8" @@ -22,7 +23,7 @@ type UserInputValidaton struct { var validate *validator.Validate -// ValidateInputs validate inputs +// ValidateInputs is responsible for validating end-user inputs. func ValidateInputs(clientId string, clientSecret string, apiUrl string, clientTimeOutinSeconds int, separator *string, verifyCa bool, logger logging.Logger, certificate string, certificate_key string) error { validate = validator.New(validator.WithRequiredStructEnabled()) @@ -53,7 +54,7 @@ func ValidateInputs(clientId string, clientSecret string, apiUrl string, clientT certificateLengthInBits := utf8.RuneCountInString(certificate) * 8 if certificateLengthInBits > 32768 { - message = "Invalid length for certificate" + message = "Invalid length for certificate, the maximum size is 32768 bits" logger.Error(message) return errors.New(message) } @@ -61,42 +62,75 @@ func ValidateInputs(clientId string, clientSecret string, apiUrl string, clientT certificateKeyLengthInBits := utf8.RuneCountInString(certificate_key) * 8 if certificateKeyLengthInBits > 32768 { - message = "Invalid length for certificate key" + message = "Invalid length for certificate key, the maximum size is 32768 bits" logger.Error(message) return errors.New(message) } if !strings.HasPrefix(certificate, "-----BEGIN CERTIFICATE-----") || !strings.HasSuffix(certificate, "-----END CERTIFICATE-----") { - message = "Invalid certificate content" + message = "Invalid certificate content, must contain BEGIN and END CERTIFICATE" logger.Error(message) return errors.New(message) } if !strings.HasPrefix(certificate_key, "-----BEGIN PRIVATE KEY-----") || !strings.HasSuffix(certificate_key, "-----END PRIVATE KEY-----") { - message = "Invalid certificate key content" + message = "Invalid certificate key content, must contain BEGIN and END PRIVATE KEY" logger.Error(message) return errors.New(message) } } - if !strings.Contains(apiUrl, "/BeyondTrust/api/public/v3/") { - message = "Invalid API URL, it should contains /BeyondTrust/api/public/v3/ as path" + if !strings.Contains(apiUrl, "/BeyondTrust/api/public/v") { + message = "Invalid API URL, it must contains /BeyondTrust/api/public/v as part of the route" logger.Error(message) return errors.New(message) } logger.Debug("Validation passed!") - //Logging("DEBUG", "Validation passed!", *logger) return nil } -// ValidatePaths validate path +// ValidatePaths is responsible for validating secret paths func ValidatePath(path string) error { message := "" if len(path) > 303 { - message = "Invalid Path Lenght" + message = fmt.Sprintf("Invalid Path Length, valid paths have a maximum size of %v", 303) return errors.New(message) } return nil } + +// ValidatePaths validate managed accounts paths +func ValidatePaths(secretPaths []string, separator string, logger logging.Logger) ([]string, error) { + + newSecretPaths := []string{} + + for _, secretToRetrieve := range secretPaths { + + if strings.TrimSpace(secretToRetrieve) == "" { + logger.Debug("Please use a valid path") + continue + } + + secretData := strings.Split(secretToRetrieve, separator) + + systemName := secretData[0] + accountName := secretData[1] + + systemName = strings.TrimSpace(systemName) + accountName = strings.TrimSpace(accountName) + + if systemName == "" { + logger.Debug("Please use a valid system name value") + } else if accountName == "" { + logger.Debug("Please use a valid account name value") + } else { + secretPath := fmt.Sprintf("%s%s%s", systemName, separator, accountName) + newSecretPaths = append(newSecretPaths, secretPath) + } + } + + return newSecretPaths, nil + +}