diff --git a/README.md b/README.md index da51d05..127962f 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ The library supports retrieval of secrets from BeyondInsight/Password Safe versi - type: string - required: False - verifyCA: - - description: Indicates whether to verify the certificate authority on the Secrets Safe instance. Warning: false is insecure, instructs the Secrets Safe custom action not to verify the certificate authority. + - description: Indicates whether to verify the certificate authority on the Secrets Safe instance. Warning: false is insecure, instructs not to verify the certificate authority. - type: boolean - default: True - required: False diff --git a/TestClient.go b/TestClient.go index 98e8d66..84385e2 100644 --- a/TestClient.go +++ b/TestClient.go @@ -7,7 +7,6 @@ import ( 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" ) @@ -54,45 +53,35 @@ func main() { // instantiating secret obj secretObj, _ := secrets.NewSecretObj(*authenticate, zapLogger) - paths := "fake/text1,fake/text2" - errors_in_path := utils.ValidatePath(paths) - if errors_in_path != nil { - return - } + secretPaths := []string{"fake/Client", "fake/test_file_1"} - // getting secrets - secretPaths := strings.Split(paths, ",") gotSecrets, _ := secretObj.GetSecrets(secretPaths, 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)) + zapLogger.Warn(fmt.Sprintf("%v", gotSecrets)) // getting single secret - gotSecret, _ := secretObj.GetSecret("fake/text1", separator) + gotSecret, _ := secretObj.GetSecret("fake/Test1", separator) // WARNING: Do not log secrets in production code, the following log statement logs test secrets for testing purposes: - zapLogger.Info(fmt.Sprintf("Secret Test: %v", gotSecret)) + zapLogger.Warn(fmt.Sprintf("Secret Test: %v", gotSecret)) // instantiating managed account obj manageAccountObj, _ := managed_accounts.NewManagedAccountObj(*authenticate, zapLogger) - paths = "fake/account01,fake/account02" - errors_in_path = utils.ValidatePath(paths) - if errors_in_path != nil { - return - } + newSecretPaths := []string{"fake/account01", "fake/account01"} - managedAccountList := strings.Split(paths, ",") - gotManagedAccounts, _ := manageAccountObj.GetSecrets(managedAccountList, separator) + //managedAccountList := strings.Split(paths, ",") + gotManagedAccounts, _ := manageAccountObj.GetSecrets(newSecretPaths, 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)) + zapLogger.Warn(fmt.Sprintf("%v", gotManagedAccounts)) // getting single managed account - gotManagedAccount, _ := manageAccountObj.GetSecret("fake/account01", separator) + gotManagedAccount, _ := manageAccountObj.GetSecret("fake/account04", 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)) + zapLogger.Warn(fmt.Sprintf("%v", gotManagedAccount)) // signing out _ = authenticate.SignOut(fmt.Sprintf("%v%v", authenticate.ApiUrl, "Auth/Signout")) diff --git a/api/authentication/authetication.go b/api/authentication/authetication.go index 818ea1d..0454919 100644 --- a/api/authentication/authetication.go +++ b/api/authentication/authetication.go @@ -17,6 +17,7 @@ import ( backoff "github.com/cenkalti/backoff/v4" ) +// AuthenticationObj responsbile for authentication request data. type AuthenticationObj struct { ApiUrl string clientId string @@ -151,12 +152,13 @@ func (authenticationObj *AuthenticationObj) SignAppin(endpointUrl string, access authenticationObj.log.Error(err.Error()) return entities.SignApinResponse{}, err } - authenticationObj.log.Debug("Successfully Signed App In") + authenticationObj.log.Info("Successfully Signed App In") return userObject, nil } // 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. +// Warn: should only be called one time for all data sources. The session is closed server +// side automatically after 20 minutes of uninterupted inactivity. func (authenticationObj *AuthenticationObj) SignOut(url string) error { authenticationObj.log.Debug(url) @@ -176,6 +178,6 @@ func (authenticationObj *AuthenticationObj) SignOut(url string) error { } defer authenticationObj.HttpClient.HttpClient.CloseIdleConnections() - authenticationObj.log.Debug("Successfully Signed out.") + authenticationObj.log.Info("Successfully Signed out.") return nil } diff --git a/api/entities/entities.go b/api/entities/entities.go index 7677dda..d0d10c9 100644 --- a/api/entities/entities.go +++ b/api/entities/entities.go @@ -2,6 +2,7 @@ // Package entities implements DTO's used by Beyondtrust Secret Safe API. package entities +// SignApinResponse responsbile for API sign in information. type SignApinResponse struct { UserId int `json:"UserId"` EmailAddress string `json:"EmailAddress"` @@ -9,11 +10,13 @@ type SignApinResponse struct { Name string `json:"Name"` } +// ManagedAccount responsible for managed account response data. type ManagedAccount struct { SystemId int AccountId int } +// Secret responsible for secrets-safe response data. type Secret struct { Id string Title string @@ -21,6 +24,7 @@ type Secret struct { SecretType string } +// GetTokenResponse responsible for token response data. type GetTokenResponse struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` diff --git a/api/logging/logging.go b/api/logging/logging.go index 41b1052..d74f0b6 100644 --- a/api/logging/logging.go +++ b/api/logging/logging.go @@ -1,3 +1,5 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// Package logging abstraction. package logging import ( @@ -13,6 +15,7 @@ type Logger interface { Info(msg string) Error(msg string) Debug(msg string) + Warn(msg string) } // ZapLogger is a struct that implements the Logger interface using zap @@ -30,11 +33,16 @@ func (z *ZapLogger) Error(msg string) { z.logger.Error(msg) } -// Error logs a message at error level +// Debug logs a message at error level func (z *ZapLogger) Debug(msg string) { z.logger.Debug(msg) } +// Warn logs a message at error level +func (z *ZapLogger) Warn(msg string) { + z.logger.Warn(msg) +} + // logr.logger type LogrLogger struct { logger *logr.Logger @@ -54,6 +62,10 @@ func (r *LogrLogger) Debug(msg string) { r.logger.Info(msg) } +func (r *LogrLogger) Warn(msg string) { + r.logger.Info(msg) +} + // log.logger type LogLogger struct { logger *log.Logger @@ -80,6 +92,13 @@ func (l *LogLogger) Debug(msg string) { l.logger.Println(msg) } +// Warn logs a message at debug level +func (l *LogLogger) Warn(msg string) { + prefix := fmt.Sprintf("%v :", "Warn") + 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} @@ -90,6 +109,7 @@ func NewLogrLogger(logger *logr.Logger) *LogrLogger { return &LogrLogger{logger: logger} } +// NewLogLogger creates a new go log 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 index 6cceedd..892ffed 100644 --- a/api/managed_account/managed_account.go +++ b/api/managed_account/managed_account.go @@ -1,6 +1,5 @@ // Copyright 2024 BeyondTrust. All rights reserved. // Package managed_accounts implements Get managed account logic - package managed_accounts import ( @@ -18,6 +17,7 @@ import ( backoff "github.com/cenkalti/backoff/v4" ) +// ManagedAccountstObj responsible for session requests. type ManagedAccountstObj struct { log logging.Logger authenticationObj authentication.AuthenticationObj @@ -51,10 +51,10 @@ func (managedAccounObj *ManagedAccountstObj) GetSecret(secretPath string, separa // 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) { + secretsToRetrieve = utils.ValidatePaths(secretsToRetrieve, true, separator, managedAccounObj.log) + managedAccounObj.log.Info(fmt.Sprintf("Retrieving %v Secrets", len(secretsToRetrieve))) secretDictionary := make(map[string]string) - secretsToRetrieve, _ = utils.ValidatePaths(secretsToRetrieve, separator, managedAccounObj.log) - for _, secretToRetrieve := range secretsToRetrieve { secretData := strings.Split(secretToRetrieve, separator) diff --git a/api/secrets/secrets.go b/api/secrets/secrets.go index 8c4793b..e90b200 100644 --- a/api/secrets/secrets.go +++ b/api/secrets/secrets.go @@ -10,6 +10,7 @@ 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" "io" "net/url" "strings" @@ -17,6 +18,7 @@ import ( backoff "github.com/cenkalti/backoff/v4" ) +// SecretObj responsible for session requests. type SecretObj struct { log logging.Logger authenticationObj authentication.AuthenticationObj @@ -50,6 +52,8 @@ func (secretObj *SecretObj) GetSecret(secretPath string, separator string) (stri // 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) { + secretsToRetrieve = utils.ValidatePaths(secretsToRetrieve, false, separator, secretObj.log) + secretObj.log.Info(fmt.Sprintf("Retrieving %v Secrets", len(secretsToRetrieve))) secretDictionary := make(map[string]string) for _, secretToRetrieve := range secretsToRetrieve { diff --git a/api/utils/httpclient.go b/api/utils/httpclient.go index 25fa045..399c3c9 100644 --- a/api/utils/httpclient.go +++ b/api/utils/httpclient.go @@ -1,3 +1,5 @@ +// Copyright 2024 BeyondTrust. All rights reserved. +// utils responsible for utility functions. package utils import ( @@ -11,6 +13,7 @@ import ( "time" ) +// HttpClientObj responsible for http request instance. type HttpClientObj struct { HttpClient *http.Client log logging.Logger diff --git a/api/utils/validator.go b/api/utils/validator.go index c7443c2..38d4498 100644 --- a/api/utils/validator.go +++ b/api/utils/validator.go @@ -6,12 +6,14 @@ import ( "errors" "fmt" logging "go-client-library-passwordsafe/api/logging" + "net/url" "strings" "unicode/utf8" validator "github.com/go-playground/validator/v10" ) +// UserInputValidaton responsible for input paramerter validation. type UserInputValidaton struct { ClientId string `validate:"required,min=36,max=36"` ClientSecret string `validate:"required,min=36,max=64"` @@ -47,6 +49,10 @@ func ValidateInputs(clientId string, clientSecret string, apiUrl string, clientT VerifyCa: verifyCa, } + if !verifyCa { + logger.Warn("verifyCa=false is insecure, instructs not to verify the certificate authority.") + } + if strings.TrimSpace(*separator) == "" { *separator = "/" } @@ -64,7 +70,7 @@ func ValidateInputs(clientId string, clientSecret string, apiUrl string, clientT certificateLengthInBits := utf8.RuneCountInString(certificate) * 8 if certificateLengthInBits > 32768 { - message = "Invalid length for certificate, the maximum size is 32768 bits" + message = "invalid length for certificate, the maximum size is 32768 bits" logger.Error(message) return errors.New(message) } @@ -72,75 +78,110 @@ func ValidateInputs(clientId string, clientSecret string, apiUrl string, clientT certificateKeyLengthInBits := utf8.RuneCountInString(certificate_key) * 8 if certificateKeyLengthInBits > 32768 { - message = "Invalid length for certificate key, the maximum size is 32768 bits" + 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, must contain BEGIN and END CERTIFICATE" + 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, must contain BEGIN and END PRIVATE KEY" + 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/v") { - message = "Invalid API URL, it must contains /BeyondTrust/api/public/v as part of the route" - logger.Error(message) - return errors.New(message) + err = ValidateURL(apiUrl) + if err != nil { + logger.Error(err.Error()) + return err } - logger.Debug("Validation passed!") - return nil -} - -// ValidatePaths is responsible for validating secret paths -func ValidatePath(path string) error { - message := "" - if len(path) > 303 { - message = fmt.Sprintf("Invalid Path Length, valid paths have a maximum size of %v", 303) - return errors.New(message) - } + message = fmt.Sprintf("Library settings: ApiUrl=%v, ClientTimeOutinSeconds=%v, Separator=%v, VerifyCa=%v", userInput.ApiUrl, userInput.ClientTimeOutinSeconds, userInput.Separator, userInput.VerifyCa) + logger.Debug(message) return nil } -// ValidatePaths validate managed accounts paths -func ValidatePaths(secretPaths []string, separator string, logger logging.Logger) ([]string, error) { +// This method is responsbile for validating that the paths and names are valid. +func ValidatePaths(secretPaths []string, isManagedAccount bool, separator string, logger logging.Logger) []string { newSecretPaths := []string{} + var maxAccountNameLength = 246 + var maxSystemNameLength = 129 + var maxPathLength = 1792 + var maxTitleLength = 256 + var maxPath = 0 + var maxName = 0 + var invalidPathName = "" + var invalidName = "" + for _, secretToRetrieve := range secretPaths { if strings.TrimSpace(secretToRetrieve) == "" { - logger.Debug("Please use a valid path") + logger.Warn("Empty path encountered.") continue } secretData := strings.Split(secretToRetrieve, separator) - systemName := secretData[0] - accountName := secretData[1] + path := secretData[0] + name := secretData[1] - systemName = strings.TrimSpace(systemName) - accountName = strings.TrimSpace(accountName) + if isManagedAccount { + maxPath = maxSystemNameLength + maxName = maxAccountNameLength + invalidPathName = "system name" + invalidName = "account name" + } else { + maxPath = maxPathLength + maxName = maxTitleLength + invalidPathName = "path" + invalidName = "title" + } - if systemName == "" { - logger.Debug("Please use a valid system name value") - } else if accountName == "" { - logger.Debug("Please use a valid account name value") + path = strings.TrimSpace(path) + name = strings.TrimSpace(name) + + if len(path) > maxPath || path == "" { + message := fmt.Sprintf("Invalid %s length=%v, valid length between 1 and %v, this secret will be skipped.", invalidPathName, len(path), maxName) + logger.Warn(message) + } else if len(name) > maxName || name == "" { + message := fmt.Sprintf("%s=%s but found invalid %s length=%v, valid length between 1 and %v, this secret will be skipped.", invalidPathName, path, invalidName, len(name), maxName) + logger.Warn(message) } else { - secretPath := fmt.Sprintf("%s%s%s", systemName, separator, accountName) + secretPath := fmt.Sprintf("%s%s%s", path, separator, name) newSecretPaths = append(newSecretPaths, secretPath) } } - return newSecretPaths, nil + return newSecretPaths + +} + +// ValidateURL responsible for validating the Password Safe API URL. +func ValidateURL(apiUrl string) error { + val, err := url.Parse(apiUrl) + if err != nil { + return err + } + scheme := val.Scheme + if scheme == "http" { + message := "http is not support. Use https" + return errors.New(message) + } + + if !strings.Contains(apiUrl, "/BeyondTrust/api/public/v") { + message := "invalid API URL, it must contains /BeyondTrust/api/public/v as part of the route" + return errors.New(message) + } + + return nil }