From 1b3281acd2a87e7a49a6d82e536f75dc160e3f3a 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/authetication.go | 236 ++++++++++++++++++++++++++++ api/entities/entities.go | 29 ++++ api/secrets/secrets.go | 168 ++++++++++++++++++++ api/utils/utils.go | 13 ++ go.mod | 8 + go.sum | 4 + main.go | 44 ++++++ 7 files changed, 502 insertions(+) create mode 100644 api/authentication/authetication.go create mode 100644 api/entities/entities.go create mode 100644 api/secrets/secrets.go create mode 100644 api/utils/utils.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/api/authentication/authetication.go b/api/authentication/authetication.go new file mode 100644 index 0000000..e26f677 --- /dev/null +++ b/api/authentication/authetication.go @@ -0,0 +1,236 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package client implements functions to call Beyondtrust Secret Safe API. +package authentication + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "go-client-library-passwordsafe/api/entities" + "go-client-library-passwordsafe/api/utils" + "io" + "io/ioutil" + "log" + "net/http" + "net/http/cookiejar" + "net/url" + "time" + + "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(endpointUrl string, clientId string, clientSecret string, certificate string, certificate_key string, verifyCa bool, logger log.Logger, clientTimeOut int) (*AuthenticationObj, error) { + var cert tls.Certificate + + if certificate != "" && certificate_key != "" { + certi, err := tls.X509KeyPair([]byte(certificate), []byte(certificate_key)) + + if err != nil { + utils.Logging("ERROR", err.Error(), logger) + 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, + }, + } + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.InitialInterval = 1 * time.Second + backoffDefinition.MaxElapsedTime = 15 * time.Second + backoffDefinition.RandomizationFactor = 0.5 + + var jar, _ = cookiejar.New(nil) + + // Client + var client = &http.Client{ + Transport: tr, + Jar: jar, + Timeout: time.Second * time.Duration(clientTimeOut), + } + + authenticationObj := &AuthenticationObj{ + ApiUrl: endpointUrl, + httpClient: client, + clientId: clientId, + clientSecret: clientSecret, + ExponentialBackOff: backoffDefinition, + autenticationLogger: logger, + } + authenticationObj.signApinResponse, _ = authenticationObj.GetPasswordSafeAuthentication() + + 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, "CredentialByRequestId", "") + return technicalError + }, c.ExponentialBackOff) + + if technicalError != nil { + return "", technicalError + } + + if businessError != nil { + return "", businessError + } + + bodyBytes, err := ioutil.ReadAll(body) + + if err != nil { + return "", err + } + + responseString := string(bodyBytes) + + var data entities.GetTokenResponse + + err = json.Unmarshal([]byte(responseString), &data) + if err != nil { + 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 := ioutil.ReadAll(body) + if err != nil { + return entities.SignApinResponse{}, err + } + + json.Unmarshal(bodyBytes, &userObject) + return userObject, 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/secrets/secrets.go b/api/secrets/secrets.go new file mode 100644 index 0000000..ed5838b --- /dev/null +++ b/api/secrets/secrets.go @@ -0,0 +1,168 @@ +// 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" + "io/ioutil" + "log" + "net/url" + "strings" + + "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) { + secretsToRetrieve := strings.Split(secretsList, ",") + return secretObj.GetSecretFlow(secretsToRetrieve, separator) +} + +// GetSecret returns secret value for a specific path and title. +func (secretObj *SecretObj) GetSecret(secret string, separator string) (map[string]string, error) { + secretsList := []string{} + secretsList = append(secretsList, secret) + + 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 := ioutil.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 := ioutil.ReadAll(body) + if err != nil { + return "", err + } + + responseString := string(responseData) + return responseString, 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/go.mod b/go.mod new file mode 100644 index 0000000..84defa1 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module go-client-library-passwordsafe + +go 1.20 + +require ( + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + golang.org/x/crypto v0.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4e30528 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +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= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= diff --git a/main.go b/main.go new file mode 100644 index 0000000..75450f7 --- /dev/null +++ b/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "go-client-library-passwordsafe/api/authentication" + "go-client-library-passwordsafe/api/secrets" + "go-client-library-passwordsafe/api/utils" + "log" + "os" +) + +var logger = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime) + +// main funtion +func main() { + + logFile, _ := os.Create("ProviderLogs.log") + logger.SetOutput(logFile) + + apiUrl := "" + clientId := "" + clientSecret := "" + separator := "/" + certificate := "" + certificate_key := "" + verifyCa := true + clientTimeOutinSeconds := 5 + + // instantiating authenticate obj + authenticate, _ := authentication.Authenticate(apiUrl, clientId, clientSecret, certificate, certificate_key, verifyCa, *logger, clientTimeOutinSeconds) + + // instantiating secret obj + secretObj, _ := secrets.NewSecretObj(*authenticate, *logger) + + // getting several secrets + secretList := "oauthgrp/text2,oauthgrp/text1" + gotSecrets, _ := secretObj.GetSecrets(secretList, separator) + utils.Logging("DEBUG", fmt.Sprintf("%v", gotSecrets), *logger) + + // getting a single secrett + secret := "oauthgrp/text2" + gotSecret, _ := secretObj.GetSecret(secret, separator) + utils.Logging("DEBUG", fmt.Sprintf("%v", gotSecret), *logger) +}