From c600323709efd92d1ce48b9175014a77e6a760a8 Mon Sep 17 00:00:00 2001 From: "EPAM\\Felipe_Hernandez" Date: Wed, 7 Feb 2024 19:16:59 -0500 Subject: [PATCH] feat: initial commit --- api/authentication/authentication_test.go | 142 ++++++++++++ api/authentication/authetication.go | 232 +++++++++++++++++++ api/entities/entities.go | 29 +++ api/managed_account/managed_account.go | 265 ++++++++++++++++++++++ api/secrets/secrets.go | 166 ++++++++++++++ api/utils/httpclient.go | 44 ++++ api/utils/utils.go | 13 ++ api/utils/validator.go | 57 +++++ go.mod | 16 ++ go.sum | 22 ++ main.go | 81 +++++++ 11 files changed, 1067 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/managed_account/managed_account.go create mode 100644 api/secrets/secrets.go create mode 100644 api/utils/httpclient.go create mode 100644 api/utils/utils.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..c5a830e --- /dev/null +++ b/api/authentication/authentication_test.go @@ -0,0 +1,142 @@ +// 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/utils" + "log" + "os" + "reflect" + + "net/http" + "net/http/httptest" + "testing" +) + +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 +} + +var logger = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime) +var httpClient, _ = utils.GetHttpClient(5, true, "", "") +var authenticate, _ = Authenticate(httpClient, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", *logger) + +func TestSignOut(t *testing.T) { + + testConfig := UserTestConfig{ + name: "TestSignOut", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(``)) + })), + response: nil, + } + + err := authenticate.SignOut(testConfig.server.URL) + if err != nil { + t.Errorf("Test case Failed: %v", err) + } +} + +func TestSignAppin(t *testing.T) { + + testConfig := UserTestConfig{ + name: "TestSignAppin", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`)) + })), + 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) { + + 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": + w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer", "scope": "publicapi"}`)) + + 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) { + + 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": + w.Write([]byte(`{"access_token": "fake_token", "expires_in": 600, "token_type": "Bearer", "scope": "publicapi"}`)) + + case "/Auth/SignAppIn": + w.Write([]byte(`{"UserId":1, "EmailAddress":"Felipe"}`)) + + 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..859047d --- /dev/null +++ b/api/authentication/authetication.go @@ -0,0 +1,232 @@ +// 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/utils" + "io" + + "log" + "net/http" + "net/url" + "time" + + backoff "github.com/cenkalti/backoff/v4" +) + +type AuthenticationObj struct { + ApiUrl string + clientId string + clientSecret string + httpClient *http.Client + ExponentialBackOff *backoff.ExponentialBackOff + signApinResponse entities.SignApinResponse + autenticationLogger log.Logger +} + +// Authenticate in PS API +func Authenticate(httpClient *http.Client, endpointUrl string, clientId string, clientSecret string, logger *log.Logger, maxElapsedTime int) (*AuthenticationObj, error) { + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.InitialInterval = 1 * time.Second + backoffDefinition.MaxElapsedTime = time.Duration(maxElapsedTime) * time.Second + backoffDefinition.RandomizationFactor = 0.5 + + // Client + var client = httpClient + + authenticationObj := &AuthenticationObj{ + ApiUrl: endpointUrl, + httpClient: client, + clientId: clientId, + clientSecret: clientSecret, + ExponentialBackOff: backoffDefinition, + autenticationLogger: *logger, + } + + return authenticationObj, nil +} + +// GetPasswordSafeAuthentication call get token and sign app endpoint +func (c *AuthenticationObj) GetPasswordSafeAuthentication() (entities.SignApinResponse, error) { + accessToken, err := c.GetToken(fmt.Sprintf("%v%v", c.ApiUrl, "Auth/connect/token"), c.clientId, c.clientSecret) + if err != nil { + return entities.SignApinResponse{}, err + } + signApinResponse, err := c.SignAppin(fmt.Sprintf("%v%v", c.ApiUrl, "Auth/SignAppIn"), accessToken) + if err != nil { + return entities.SignApinResponse{}, err + } + return signApinResponse, nil +} + +// GetToken get token from PS API +func (c *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, _ = c.CallSecretSafeAPI(endpointUrl, "POST", buffer, "GetToken", "") + return technicalError + }, c.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + 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 { + utils.Logging("ERROR", err.Error(), c.autenticationLogger) + return "", err + } + + return data.AccessToken, nil + +} + +// SignAppin Signs app in PS API +func (c *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 = c.CallSecretSafeAPI(endpointUrl, "POST", bytes.Buffer{}, "SignAppin", accessToken) + if scode == 0 { + return nil + } + return technicalError + }, c.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 { + utils.Logging("ERROR", err.Error(), c.autenticationLogger) + 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 (c *AuthenticationObj) SignOut(url string) error { + + utils.Logging("DEBUG", url, c.autenticationLogger) + + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + _, technicalError, businessError, _ = c.CallSecretSafeAPI(url, "POST", bytes.Buffer{}, "SignOut", "") + return technicalError + }, c.ExponentialBackOff) + + if businessError != nil { + utils.Logging("ERROR", businessError.Error(), c.autenticationLogger) + return businessError + } + + return nil +} + +// CallSecretSafeAPI prepares http call +func (c *AuthenticationObj) CallSecretSafeAPI(url string, httpMethod string, body bytes.Buffer, method string, accesToken string) (io.ReadCloser, error, error, int) { + response, technicalError, businessError, scode := c.HttpRequest(url, httpMethod, body, accesToken) + if technicalError != nil { + messageLog := fmt.Sprintf("Error in %v %v \n", method, technicalError) + utils.Logging("ERROR", messageLog, c.autenticationLogger) + } + + if businessError != nil { + messageLog := fmt.Sprintf("Error in %v: %v \n", method, businessError) + utils.Logging("ERROR", messageLog, c.autenticationLogger) + } + return response, technicalError, businessError, scode +} + +// HttpRequest makes http request to he server +func (c *AuthenticationObj) 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 := c.httpClient.Do(req) + if err != nil { + utils.Logging("ERROR", err.Error(), c.autenticationLogger) + return nil, err, nil, 0 + } + + if resp.StatusCode >= http.StatusInternalServerError || resp.StatusCode == http.StatusRequestTimeout { + err = fmt.Errorf("Error %v: StatusCode: %v, %v, %v", method, scode, err, body) + utils.Logging("ERROR", err.Error(), c.autenticationLogger) + return nil, err, nil, resp.StatusCode + } + + if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { + respBody := new(bytes.Buffer) + respBody.ReadFrom(resp.Body) + 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/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/managed_account/managed_account.go b/api/managed_account/managed_account.go new file mode 100644 index 0000000..d28d399 --- /dev/null +++ b/api/managed_account/managed_account.go @@ -0,0 +1,265 @@ +// 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/utils" + "io" + "log" + "strconv" + "strings" + + backoff "github.com/cenkalti/backoff/v4" +) + +type ManagedAccountstObj struct { + managedAccocuntsLogger log.Logger + authenticationObj authentication.AuthenticationObj +} + +// NewManagedAccountObj creates managed account obj +func NewManagedAccountObj(authentication authentication.AuthenticationObj, logger log.Logger) (*ManagedAccountstObj, error) { + managedAccounObj := &ManagedAccountstObj{ + managedAccocuntsLogger: 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") + utils.Logging("ERROR", err.Error(), managedAccounObj.managedAccocuntsLogger) + return nil, err + } + + if accountName == "" { + err = errors.New("Please use a valid system_name value") + utils.Logging("ERROR", err.Error(), managedAccounObj.managedAccocuntsLogger) + return nil, err + } + + ManagedAccountGetUrl := managedAccounObj.RequestPath(paths["ManagedAccountGetPath"]) + managedAccount, err := managedAccounObj.ManagedAccountGet(systemName, accountName, ManagedAccountGetUrl) + if err != nil { + utils.Logging("ERROR", err.Error(), managedAccounObj.managedAccocuntsLogger) + return nil, err + } + + ManagedAccountCreateRequestUrl := managedAccounObj.RequestPath(paths["ManagedAccountCreateRequestPath"]) + requestId, err := managedAccounObj.ManagedAccountCreateRequest(managedAccount.SystemId, managedAccount.AccountId, ManagedAccountCreateRequestUrl) + if err != nil { + utils.Logging("ERROR", err.Error(), managedAccounObj.managedAccocuntsLogger) + return nil, err + } + + CredentialByRequestIdUrl := managedAccounObj.RequestPath(fmt.Sprintf(paths["CredentialByRequestIdPath"], requestId)) + secret, err := managedAccounObj.CredentialByRequestId(requestId, CredentialByRequestIdUrl) + if err != nil { + utils.Logging("ERROR", err.Error(), managedAccounObj.managedAccocuntsLogger) + return nil, err + } + + ManagedAccountRequestCheckInPath := fmt.Sprintf(paths["ManagedAccountRequestCheckInPath"], requestId) + ManagedAccountRequestCheckInUrl := managedAccounObj.RequestPath(ManagedAccountRequestCheckInPath) + _, err = managedAccounObj.ManagedAccountRequestCheckIn(requestId, ManagedAccountRequestCheckInUrl) + + if err != nil { + utils.Logging("ERROR", err.Error(), managedAccounObj.managedAccocuntsLogger) + 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) + utils.Logging("DEBUG", messageLog, managedAccounObj.managedAccocuntsLogger) + + var body io.ReadCloser + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + body, technicalError, businessError, _ = managedAccounObj.authenticationObj.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 + } + + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return entities.ManagedAccount{}, err + } + + var managedAccountObject entities.ManagedAccount + json.Unmarshal(bodyBytes, &managedAccountObject) + + 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) + utils.Logging("DEBUG", messageLog, managedAccounObj.managedAccocuntsLogger) + + 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.CallSecretSafeAPI(url, "POST", *b, "ManagedAccountCreateRequest", "") + return technicalError + }, managedAccounObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + 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) + + utils.Logging("DEBUG", messageLog, managedAccounObj.managedAccocuntsLogger) + + var body io.ReadCloser + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + body, technicalError, businessError, _ = managedAccounObj.authenticationObj.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "CredentialByRequestId", "") + return technicalError + }, managedAccounObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + bodyBytes, err := io.ReadAll(body) + + 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) + utils.Logging("DEBUG", messageLog, managedAccounObj.managedAccocuntsLogger) + + data := "{}" + b := bytes.NewBufferString(data) + + var technicalError error + var businessError error + + technicalError = backoff.Retry(func() error { + _, technicalError, businessError, _ = managedAccounObj.authenticationObj.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/secrets/secrets.go b/api/secrets/secrets.go new file mode 100644 index 0000000..9aef7aa --- /dev/null +++ b/api/secrets/secrets.go @@ -0,0 +1,166 @@ +// 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/utils" + "io" + "log" + "net/url" + "strings" + + backoff "github.com/cenkalti/backoff/v4" +) + +type SecretObj struct { + secretLogger log.Logger + authenticationObj authentication.AuthenticationObj +} + +// NewSecretObj creates secret obj +func NewSecretObj(authentication authentication.AuthenticationObj, logger log.Logger) (*SecretObj, error) { + secretObj := &SecretObj{ + secretLogger: 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 { + utils.Logging("ERROR", err.Error(), secretObj.secretLogger) + 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) + utils.Logging("DEBUG", messageLog, secretObj.secretLogger) + + 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.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 + } + + 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) + utils.Logging("DEBUG", messageLog, secretObj.secretLogger) + + 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.CallSecretSafeAPI(url, "GET", bytes.Buffer{}, "SecretGetFileSecret", "") + return technicalError + }, secretObj.authenticationObj.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + responseData, err := io.ReadAll(body) + if err != nil { + return "", err + } + + responseString := string(responseData) + return responseString, nil + +} diff --git a/api/utils/httpclient.go b/api/utils/httpclient.go new file mode 100644 index 0000000..f43ed4e --- /dev/null +++ b/api/utils/httpclient.go @@ -0,0 +1,44 @@ +package utils + +import ( + "crypto/tls" + "net/http" + "net/http/cookiejar" + "time" +) + +func GetHttpClient(clientTimeOut int, verifyCa bool, certificate string, certificate_key string) (*http.Client, 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), + } + + return client, nil +} diff --git a/api/utils/utils.go b/api/utils/utils.go new file mode 100644 index 0000000..27c8e4d --- /dev/null +++ b/api/utils/utils.go @@ -0,0 +1,13 @@ +package utils + +import ( + "fmt" + "log" +) + +// log message in log file. +func Logging(prefix string, message string, logger log.Logger) { + prefix = fmt.Sprintf("%v :", prefix) + logger.SetPrefix(prefix) + logger.Println(message) +} diff --git a/api/utils/validator.go b/api/utils/validator.go new file mode 100644 index 0000000..7996fb7 --- /dev/null +++ b/api/utils/validator.go @@ -0,0 +1,57 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package utils implements inputs validations +package utils + +import ( + "errors" + "log" + + validator "github.com/go-playground/validator/v10" +) + +type UserInputValidaton struct { + ClientId string `validate:"required,min=3,max=100"` + ClientSecret string `validate:"required,min=3,max=100"` + ApiUrl string `validate:"required,http_url"` + ClientTimeOutinSeconds int `validate:"gte=3"` + 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 *log.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, + } + + err := validate.Struct(userInput) + if err != nil { + Logging("ERROR", err.Error(), *logger) + return err + } + + message := "" + + if certificate != "" && certificate_key != "" { + if len(certificate) < 100 || len(certificate) > 4000 { + message = "Invalid length for certificate" + return errors.New(message) + } else if len(certificate_key) < 100 || len(certificate_key) > 4000 { + message = "Invalid length for certificate key" + return errors.New(message) + } + } + + Logging("DEBUG", "Validation passed!", *logger) + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e4efd20 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +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-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 + 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..a683cfc --- /dev/null +++ b/go.sum @@ -0,0 +1,22 @@ +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-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= +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..ba31ea8 --- /dev/null +++ b/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "fmt" + "go-client-library-passwordsafe/api/authentication" + managed_accounts "go-client-library-passwordsafe/api/managed_account" + secrets "go-client-library-passwordsafe/api/secrets" + "go-client-library-passwordsafe/api/utils" + "log" + "os" + "strings" +) + +var logger = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime) + +// main funtion +func main() { + + //logFile, _ := os.Create("ProviderLogs.log") + //logger.SetOutput(logFile) + + 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, logger, certificate, certificate_key) + + if errors_in_inputs != nil { + utils.Logging("ERROR", errors_in_inputs.Error(), *logger) + return + } + + // creating a http client + httpClient, _ := utils.GetHttpClient(clientTimeOutinSeconds, verifyCa, certificate, certificate_key) + + // instantiating authenticate obj, injecting httpClient object + authenticate, _ := authentication.Authenticate(httpClient, apiUrl, clientId, clientSecret, logger, maxElapsedTime) + + // authenticating in PS API + _, err := authenticate.GetPasswordSafeAuthentication() + if err != nil { + return + } + + // instantiating secret obj + secretObj, _ := secrets.NewSecretObj(*authenticate, *logger) + + // getting secrets + secretList := strings.Split("oauthgrp/text2,oauthgrp/text1", ",") + gotSecrets, _ := secretObj.GetSecrets(secretList, separator) + utils.Logging("DEBUG", fmt.Sprintf("%v", gotSecrets), *logger) + + // getting secrets + secretList = strings.Split("oauthgrp/text2", ",") + gotSecret, _ := secretObj.GetSecret(secretList, separator) + utils.Logging("DEBUG", fmt.Sprintf("%v", gotSecret), *logger) + + // instantiating managed account obj + manageAccountObj, _ := managed_accounts.NewManagedAccountObj(*authenticate, *logger) + + // getting managed accounts + managedAccountList := strings.Split("system01/managed_account01,system02/managed_account01", ",") + gotManagedAccounts, _ := manageAccountObj.GetSecrets(managedAccountList, separator) + utils.Logging("DEBUG", fmt.Sprintf("%v", gotManagedAccounts), *logger) + + // getting single managed account + managedAccountList = []string{} + gotManagedAccount, _ := manageAccountObj.GetSecret(append(managedAccountList, "system01/managed_account01"), separator) + utils.Logging("DEBUG", fmt.Sprintf("%v", gotManagedAccount), *logger) + + // signing out + authenticate.SignOut(fmt.Sprintf("%v%v", authenticate.ApiUrl, "Auth/Signout")) + +}