diff --git a/TestClient.go b/TestClient.go index 0cbd03e..1a37a81 100644 --- a/TestClient.go +++ b/TestClient.go @@ -5,6 +5,7 @@ import ( "time" "github.com/BeyondTrust/go-client-library-passwordsafe/api/authentication" + "github.com/BeyondTrust/go-client-library-passwordsafe/api/entities" logging "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging" managed_accounts "github.com/BeyondTrust/go-client-library-passwordsafe/api/managed_account" "github.com/BeyondTrust/go-client-library-passwordsafe/api/secrets" @@ -112,6 +113,51 @@ func main() { // WARNING: Do not log secrets in production code, the following log statement logs test secrets for testing purposes: zapLogger.Warn(fmt.Sprintf("%v", gotManagedAccount)) + account := entities.AccountDetails{ + AccountName: "ManagedAccountTest", + Password: "Passw0rd101!*", + DomainName: "exampleDomain", + UserPrincipalName: "user@example.com", + SAMAccountName: "samAccount", + DistinguishedName: "CN=example,CN=Users,DC=domain,DC=com", + PrivateKey: "privateKey", + Passphrase: "passphrase", + PasswordFallbackFlag: true, + LoginAccountFlag: false, + Description: "Sample account for testing", + ApiEnabled: true, + ReleaseNotificationEmail: "notify@example.com", + ChangeServicesFlag: false, + RestartServicesFlag: false, + ChangeTasksFlag: true, + MaxReleaseDuration: 300000, + ISAReleaseDuration: 180, + MaxConcurrentRequests: 5, + AutoManagementFlag: false, + DSSAutoManagementFlag: false, + CheckPasswordFlag: true, + ResetPasswordOnMismatchFlag: false, + ChangePasswordAfterAnyReleaseFlag: true, + ChangeFrequencyDays: 1, + ChangeTime: "", + NextChangeDate: "2023-12-01", + UseOwnCredentials: true, + ChangeWindowsAutoLogonFlag: true, + ChangeComPlusFlag: false, + ObjectID: "uniqueObjectID", + } + + // creating a managed account in system_integration_test system. + createResponse, err := manageAccountObj.ManageAccountCreateFlow("system_integration_test", account) + + if err != nil { + zapLogger.Error(fmt.Sprintf(" %v", err)) + return + } + + // WARNING: created managed account name. + zapLogger.Warn(fmt.Sprintf("Created Managed Account: %v", createResponse.AccountName)) + // signing out _ = authenticate.SignOut() diff --git a/api/entities/entities.go b/api/entities/entities.go index d0d10c9..dd3c88a 100644 --- a/api/entities/entities.go +++ b/api/entities/entities.go @@ -31,3 +31,54 @@ type GetTokenResponse struct { TokenType string `json:"token_type"` Scope string `json:"scope"` } + +type ManagedSystemResponse struct { + ManagedSystemID int + SystemName string +} + +type CreateManagedAccountsResponse struct { + ManagedAccountID int + ManagedSystemID int + AccountName string +} + +type AccountDetails struct { + AccountName string `validate:"required,max=245"` + Password string `validate:"required_if=AutoManagementFlag false"` + DomainName string `validate:"max=50"` + UserPrincipalName string `validate:"omitempty,max=500"` + SAMAccountName string `validate:"omitempty,max=20"` + DistinguishedName string `validate:"omitempty,max=1000"` + PrivateKey string `validate:"omitempty"` + Passphrase string `validate:"omitempty,required_if=PrivateKey Encrypted"` + PasswordFallbackFlag bool `validate:"omitempty"` + LoginAccountFlag bool `validate:"omitempty"` + Description string `validate:"omitempty,max=1024"` + PasswordRuleID int `validate:"omitempty,gte=0"` + ApiEnabled bool `validate:"omitempty"` + ReleaseNotificationEmail string `validate:"omitempty,email,max=255"` + ChangeServicesFlag bool `validate:"omitempty"` + RestartServicesFlag bool `validate:"omitempty"` + ChangeTasksFlag bool `validate:"omitempty"` + ReleaseDuration int `validate:"omitempty,min=1,max=525600,ltefield=MaxReleaseDuration"` + MaxReleaseDuration int `validate:"omitempty,min=1,max=525600"` + ISAReleaseDuration int `validate:"omitempty,min=1,max=525600"` + MaxConcurrentRequests int `validate:"omitempty,min=0,max=999"` + AutoManagementFlag bool `validate:"omitempty"` + DSSAutoManagementFlag bool `validate:"omitempty"` + CheckPasswordFlag bool `validate:"omitempty"` + ChangePasswordAfterAnyReleaseFlag bool `validate:"omitempty"` + ResetPasswordOnMismatchFlag bool `validate:"omitempty"` + ChangeFrequencyType string `validate:"omitempty,oneof=first last xdays"` + ChangeFrequencyDays int `validate:"omitempty,min=1,max=999"` + ChangeTime string `validate:"omitempty,datetime=15:04"` + NextChangeDate string `validate:"omitempty,datetime=2006-01-02"` + UseOwnCredentials bool `validate:"omitempty"` + WorkgroupID *int `validate:"omitempty"` + ChangeWindowsAutoLogonFlag bool `validate:"omitempty"` + ChangeComPlusFlag bool `validate:"omitempty"` + ChangeDComFlag bool `validate:"omitempty"` + ChangeSComFlag bool `validate:"omitempty"` + ObjectID string `validate:"omitempty,max=36"` +} diff --git a/api/managed_account/managed_account.go b/api/managed_account/managed_account.go index bd18fd2..8a13815 100644 --- a/api/managed_account/managed_account.go +++ b/api/managed_account/managed_account.go @@ -15,7 +15,6 @@ import ( "github.com/BeyondTrust/go-client-library-passwordsafe/api/entities" "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging" "github.com/BeyondTrust/go-client-library-passwordsafe/api/utils" - backoff "github.com/cenkalti/backoff/v4" ) @@ -250,3 +249,146 @@ func (managedAccounObj *ManagedAccountstObj) ManagedAccountRequestCheckIn(reques return "", nil } + +// ManageAccountCreateFlow is responsible for creating a managed accounts in Password Safe. +func (managedAccounObj *ManagedAccountstObj) ManageAccountCreateFlow(systemNameTarget string, accountDetails entities.AccountDetails) (entities.CreateManagedAccountsResponse, error) { + + var managedSystem *entities.ManagedSystemResponse + var createResponse entities.CreateManagedAccountsResponse + + accountDetails, err := utils.ValidateCreateManagedAccountInput(accountDetails) + + if err != nil { + return createResponse, err + } + + ManagedAccountSytemUrl := managedAccounObj.authenticationObj.ApiUrl.JoinPath("ManagedSystems").String() + managedSystemGetSystemsResponse, err := managedAccounObj.ManagedSystemGetSystems(ManagedAccountSytemUrl) + + if err != nil { + return createResponse, err + } + + for _, v := range managedSystemGetSystemsResponse { + if v.SystemName == systemNameTarget { + managedSystem = &v + break + } + } + + if managedSystem == nil { + return createResponse, fmt.Errorf("managed system %v was not found in managed system list", systemNameTarget) + } + + ManagedAccountCreateManagedAccountUrl := managedAccounObj.authenticationObj.ApiUrl.JoinPath("ManagedSystems", fmt.Sprintf("%d", managedSystem.ManagedSystemID), "ManagedAccounts").String() + createResponse, err = managedAccounObj.ManagedAccountCreateManagedAccount(accountDetails, ManagedAccountCreateManagedAccountUrl) + + if err != nil { + return createResponse, err + } + + return createResponse, nil + +} + +// ManagedAccountCreateManagedAccount calls Secret Safe API Requests enpoint to create managed accounts. +func (managedAccounObj *ManagedAccountstObj) ManagedAccountCreateManagedAccount(accountDetails entities.AccountDetails, url string) (entities.CreateManagedAccountsResponse, error) { + messageLog := fmt.Sprintf("%v %v", "POST", url) + managedAccounObj.log.Debug(messageLog) + + accountDetailsJson, err := json.Marshal(accountDetails) + if err != nil { + return entities.CreateManagedAccountsResponse{}, err + } + + accountDetailsJsonString := string(accountDetailsJson) + + managedAccounObj.log.Debug(accountDetailsJsonString) + + b := bytes.NewBufferString(accountDetailsJsonString) + + 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, "ManagedAccountCreateManagedAccount", "", "") + return technicalError + }, managedAccounObj.authenticationObj.ExponentialBackOff) + + var CreateManagedAccountsResponse entities.CreateManagedAccountsResponse + + if technicalError != nil { + return entities.CreateManagedAccountsResponse{}, technicalError + } + + if businessError != nil { + return entities.CreateManagedAccountsResponse{}, businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return entities.CreateManagedAccountsResponse{}, err + } + + err = json.Unmarshal([]byte(bodyBytes), &CreateManagedAccountsResponse) + + if err != nil { + managedAccounObj.log.Error(err.Error()) + return entities.CreateManagedAccountsResponse{}, err + } + + return CreateManagedAccountsResponse, nil + +} + +// ManagedAccountGetSystem is responsible for retrieving managed systems list +func (managedAccounObj *ManagedAccountstObj) ManagedSystemGetSystems(url string) ([]entities.ManagedSystemResponse, 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{}, "ManagedSystemGetSystems", "", "") + if technicalError != nil { + return technicalError + } + return nil + + }, managedAccounObj.authenticationObj.ExponentialBackOff) + + var managedSystemObject []entities.ManagedSystemResponse + + if technicalError != nil { + return managedSystemObject, technicalError + } + + if businessError != nil { + return managedSystemObject, businessError + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + + if err != nil { + return managedSystemObject, err + } + + err = json.Unmarshal(bodyBytes, &managedSystemObject) + if err != nil { + managedAccounObj.log.Error(err.Error()) + return managedSystemObject, err + } + + if len(managedSystemObject) == 0 { + return managedSystemObject, fmt.Errorf("empty System Account List") + } + + return managedSystemObject, nil + +} diff --git a/api/managed_account/managed_account_test.go b/api/managed_account/managed_account_test.go index 4d16897..8ed3629 100644 --- a/api/managed_account/managed_account_test.go +++ b/api/managed_account/managed_account_test.go @@ -4,6 +4,7 @@ package managed_accounts import ( + "fmt" "net/http" "net/http/httptest" "net/url" @@ -32,6 +33,12 @@ type ManagedAccountTestConfigStringResponse struct { response string } +type CreateManagedAccountsResponse struct { + name string + server *httptest.Server + response *entities.CreateManagedAccountsResponse +} + func TestManagedAccountGet(t *testing.T) { logger, _ := zap.NewDevelopment() @@ -1051,3 +1058,296 @@ func TestManageAccountFlowGetAccountBadResponse(t *testing.T) { t.Errorf("Test case Failed") } } + +func TestManagedAccountCreateManagedAccount(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.MaxElapsedTime = time.Second + + var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := CreateManagedAccountsResponse{ + name: "TestManagedAccountCreateManagedAccount", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/ManagedSystems/5/ManagedAccounts": + _, err := w.Write([]byte(`{"ManagedSystemID":5, "ManagedAccountID":10, "AccountName": "Managed Account Name"}`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: &entities.CreateManagedAccountsResponse{ + ManagedAccountID: 10, + ManagedSystemID: 5, + AccountName: "Managed Account Name", + }, + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + + accountAccountDetailsObj := entities.AccountDetails{ + AccountName: "Managed_account_name", + Password: "MyPassword1707*!", + Description: "Sample account for testing", + MaxReleaseDuration: 300000, + ReleaseDuration: 300000, + ISAReleaseDuration: 180, + ChangeFrequencyDays: 1, + } + + ManagedAccountCreateManagedAccountUrl := authenticate.ApiUrl.JoinPath("ManagedSystems", fmt.Sprintf("%d", 5), "ManagedAccounts").String() + + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + response, err := managedAccountObj.ManagedAccountCreateManagedAccount(accountAccountDetailsObj, ManagedAccountCreateManagedAccountUrl) + + 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 TestManagedAccountCreateManagedAccountExistingOne(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.MaxElapsedTime = time.Second + + var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := ManagedAccountTestConfigStringResponse{ + name: "TestManagedAccountCreateManagedAccountExistingOne", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/ManagedSystems/5/ManagedAccounts": + w.WriteHeader(http.StatusBadRequest) + _, err := w.Write([]byte(`Managed System/Account already exists: 1/ManagedAccount10`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "error - status code: 400 - Managed System/Account already exists: 1/ManagedAccount10", + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + + ManagedAccountCreateManagedAccountUrl := authenticate.ApiUrl.JoinPath("ManagedSystems", fmt.Sprintf("%d", 5), "ManagedAccounts").String() + + accountAccountDetailsObj := entities.AccountDetails{ + AccountName: "Managed_account_name", + Password: "MyPassword1707*!", + Description: "Sample account for testing", + } + + _, err := managedAccountObj.ManagedAccountCreateManagedAccount(accountAccountDetailsObj, ManagedAccountCreateManagedAccountUrl) + + if err.Error() != testConfig.response { + t.Errorf("Test case Failed %v} %v", err.Error(), testConfig.response) + } + +} + +func TestManagedAccountCreateManagedAccountFlow(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.MaxElapsedTime = time.Second + + var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := CreateManagedAccountsResponse{ + name: "TestManagedAccountCreateManagedAccountFlow", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/ManagedSystems/5/ManagedAccounts": + _, err := w.Write([]byte(`{"ManagedSystemID":5, "ManagedAccountID":10, "AccountName": "Managed_account_name"}`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/ManagedSystems": + _, err := w.Write([]byte(`[{"ManagedSystemID":5, "SystemName":"system01", "EntityTypeID": 4}]`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: &entities.CreateManagedAccountsResponse{ + ManagedAccountID: 10, + ManagedSystemID: 5, + AccountName: "Managed_account_name", + }, + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + accountAccountDetailsObj := entities.AccountDetails{ + AccountName: "Managed_account_name", + Password: "MyPassword1707*!", + Description: "Sample account for testing", + MaxReleaseDuration: 300000, + ReleaseDuration: 300000, + ISAReleaseDuration: 180, + ChangeFrequencyDays: 1, + } + response, err := managedAccountObj.ManageAccountCreateFlow("system01", accountAccountDetailsObj) + + 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 TestManagedAccountCreateManagedAccountFlowSystemNotFound(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.MaxElapsedTime = time.Second + + var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := ManagedAccountTestConfigStringResponse{ + name: "TestManagedAccountCreateManagedAccountFlowSystemNotFound", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + + case "/ManagedSystems/5/ManagedAccounts": + _, err := w.Write([]byte(`{"ManagedSystemID":5, "ManagedAccountID":10, "AccountName": "Managed Account Name"}`)) + if err != nil { + t.Error("Test case Failed") + } + + case "/ManagedSystems": + _, err := w.Write([]byte(`[{"ManagedSystemID":5, "SystemName":"system01", "EntityTypeID": 4}]`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "managed system system02 was not found in managed system list", + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + + accountAccountDetailsObj := entities.AccountDetails{ + AccountName: "Managed_account_name", + Password: "MyPassword1707*!", + Description: "Sample account for testing", + MaxReleaseDuration: 300000, + ReleaseDuration: 300000, + ISAReleaseDuration: 180, + ChangeFrequencyDays: 1, + } + _, err := managedAccountObj.ManageAccountCreateFlow("system02", accountAccountDetailsObj) + + if err.Error() != testConfig.response { + t.Errorf("Test case Failed %v, %v", err.Error(), testConfig.response) + } + +} + +func TestManagedAccountCreateManagedAccountFlowEmptySystemList(t *testing.T) { + logger, _ := zap.NewDevelopment() + + // create a zap logger wrapper + zapLogger := logging.NewZapLogger(logger) + + httpClientObj, _ := utils.GetHttpClient(5, false, "", "", zapLogger) + + backoffDefinition := backoff.NewExponentialBackOff() + backoffDefinition.MaxElapsedTime = time.Second + + var authenticate, _ = authentication.Authenticate(*httpClientObj, backoffDefinition, "https://fake.api.com:443/BeyondTrust/api/public/v3/", "fakeone_a654+9sdf7+8we4f", "fakeone_aasd156465sfdef", zapLogger, 300) + testConfig := ManagedAccountTestConfigStringResponse{ + name: "TestManagedAccountCreateManagedAccountFlowEmptySystemList", + server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Mocking Response according to the endpoint path + switch r.URL.Path { + case "/ManagedSystems": + _, err := w.Write([]byte(`[]`)) + if err != nil { + t.Error("Test case Failed") + } + + default: + http.NotFound(w, r) + } + })), + response: "empty System Account List", + } + + apiUrl, _ := url.Parse(testConfig.server.URL + "/") + authenticate.ApiUrl = *apiUrl + + managedAccountObj, _ := NewManagedAccountObj(*authenticate, zapLogger) + + accountAccountDetailsObj := entities.AccountDetails{ + AccountName: "Managed_account_name", + Password: "MyPassword1707*!", + Description: "Sample account for testing", + MaxReleaseDuration: 300000, + ReleaseDuration: 300000, + ISAReleaseDuration: 180, + ChangeFrequencyDays: 1, + } + + _, err := managedAccountObj.ManageAccountCreateFlow("system02", accountAccountDetailsObj) + + if err.Error() != testConfig.response { + t.Errorf("Test case Failed %v} %v", err.Error(), testConfig.response) + } + +} diff --git a/api/utils/validator.go b/api/utils/validator.go index d9bd89d..13def52 100644 --- a/api/utils/validator.go +++ b/api/utils/validator.go @@ -9,6 +9,7 @@ import ( "strings" "unicode/utf8" + "github.com/BeyondTrust/go-client-library-passwordsafe/api/entities" logging "github.com/BeyondTrust/go-client-library-passwordsafe/api/logging" validator "github.com/go-playground/validator/v10" @@ -218,3 +219,34 @@ func ValidateURL(apiUrl string) error { return nil } + +func ValidateCreateManagedAccountInput(accountDetails entities.AccountDetails) (entities.AccountDetails, error) { + validate := validator.New() + err := validate.Struct(accountDetails) + + if err != nil { + return accountDetails, err + } + + if accountDetails.ChangeFrequencyType == "" { + accountDetails.ChangeFrequencyType = "first" + } + + if accountDetails.ReleaseDuration == 0 { + accountDetails.ReleaseDuration = 120 + } + + if accountDetails.MaxReleaseDuration == 0 { + accountDetails.MaxReleaseDuration = 525600 + } + + if accountDetails.ISAReleaseDuration == 0 { + accountDetails.ISAReleaseDuration = 120 + } + + if accountDetails.ChangeTime == "" { + accountDetails.ChangeTime = "00:00" + } + + return accountDetails, nil +}