diff --git a/.gitignore b/.gitignore index e2aa8a6..e89e845 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ .vscode .idea test_config.json + +.DS_Store +bh_example.go diff --git a/go.mod b/go.mod index eb1117f..319448b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,10 @@ module github.com/DelineaXPM/tss-sdk-go/v2 -go 1.13 +go 1.22 + +require github.com/tidwall/gjson v1.17.1 + +require ( + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect +) diff --git a/go.sum b/go.sum index e69de29..a491a1d 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U= +github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= diff --git a/main.go b/main.go index 1a965f1..318256a 100644 --- a/main.go +++ b/main.go @@ -30,4 +30,4 @@ func main() { if pw, ok := s.Field("password"); ok { fmt.Print("the password is", pw) } -} +} \ No newline at end of file diff --git a/server/secret.go b/server/secret.go index 01287ff..1071f41 100644 --- a/server/secret.go +++ b/server/secret.go @@ -50,7 +50,7 @@ type SshKeyArgs struct { func (s Server) Secret(id int) (*Secret, error) { secret := new(Secret) - if data, err := s.accessResource("GET", resource, strconv.Itoa(id), nil); err == nil { + if data, err := s.accessResource("GET", resource, strconv.Itoa(id), "v2/", nil); err == nil { if err = json.Unmarshal(data, secret); err != nil { log.Printf("[ERROR] error parsing response from /%s/%d: %q", resource, id, data) return nil, err @@ -65,7 +65,7 @@ func (s Server) Secret(id int) (*Secret, error) { if element.IsFile && element.FileAttachmentID != 0 && element.Filename != "" { path := fmt.Sprintf("%d/fields/%s", id, element.Slug) - if data, err := s.accessResource("GET", resource, path, nil); err == nil { + if data, err := s.accessResource("GET", resource, path, "v1/", nil); err == nil { secret.Fields[index].ItemValue = string(data) } else { return nil, err @@ -79,7 +79,7 @@ func (s Server) Secret(id int) (*Secret, error) { // Secret gets the secret with id from the Secret Server of the given tenant func (s Server) Secrets(searchText, field string) ([]Secret, error) { searchResult := new(SearchResult) - if data, err := s.searchResources(resource, searchText, field); err == nil { + if data, err := s.searchResources(resource, searchText, field, "v1/"); err == nil { if err = json.Unmarshal(data, searchResult); err != nil { log.Printf("[ERROR] error parsing response from /%s/%s: %q", resource, searchText, data) return nil, err @@ -135,8 +135,9 @@ func (s Server) writeSecret(secret Secret, method string, path string) (*Secret, // This SDK does support secret templates that accept both kinds // of file fields. fileFields := make([]SecretField, 0) - generalFields := make([]SecretField, 0) if secret.SshKeyArgs == nil || !secret.SshKeyArgs.GenerateSshKeys { + //nolint: ineffassign + var generalFields = make([]SecretField, 0) fileFields, generalFields, err = secret.separateFileFields(template) if err != nil { return nil, err @@ -162,7 +163,7 @@ func (s Server) writeSecret(secret Secret, method string, path string) (*Secret, secret.Fields = make([]SecretField, 0) } - if data, err := s.accessResource(method, resource, path, secret); err == nil { + if data, err := s.accessResource(method, resource, path, "v1/", secret); err == nil { if err = json.Unmarshal(data, writtenSecret); err != nil { log.Printf("[ERROR] error parsing response from /%s: %q", resource, data) return nil, err @@ -179,7 +180,7 @@ func (s Server) writeSecret(secret Secret, method string, path string) (*Secret, } func (s Server) DeleteSecret(id int) error { - _, err := s.accessResource("DELETE", resource, strconv.Itoa(id), nil) + _, err := s.accessResource("DELETE", resource, strconv.Itoa(id), "v1/", nil) return err } @@ -231,7 +232,7 @@ func (s Server) updateFiles(secretId int, fileFields []SecretField) error { if element.ItemValue == "" { path = fmt.Sprintf("%d/general", secretId) input = secretPatch{Data: fieldMods{SecretFields: []fieldMod{{Slug: element.Slug, Dirty: true, Value: nil}}}} - if _, err := s.accessResource("PATCH", resource, path, input); err != nil { + if _, err := s.accessResource("PATCH", resource, path, "v2/", input); err != nil { return err } } else { diff --git a/server/secret_template.go b/server/secret_template.go index e37ad3a..8066a47 100644 --- a/server/secret_template.go +++ b/server/secret_template.go @@ -28,7 +28,7 @@ type SecretTemplateField struct { func (s Server) SecretTemplate(id int) (*SecretTemplate, error) { secretTemplate := new(SecretTemplate) - if data, err := s.accessResource("GET", templateResource, strconv.Itoa(id), nil); err == nil { + if data, err := s.accessResource("GET", templateResource, strconv.Itoa(id), "v1/", nil); err == nil { if err = json.Unmarshal(data, secretTemplate); err != nil { log.Printf("[ERROR] error parsing response from /%s/%d: %q", templateResource, id, data) return nil, err @@ -52,7 +52,7 @@ func (s Server) GeneratePassword(slug string, template *SecretTemplate) (string, } path := fmt.Sprintf("generate-password/%d", fieldId) - if data, err := s.accessResource("POST", templateResource, path, nil); err == nil { + if data, err := s.accessResource("POST", templateResource, path, "v1/", nil); err == nil { passwordWithQuotes := string(data) return passwordWithQuotes[1 : len(passwordWithQuotes)-1], nil } else { diff --git a/server/secret_template_test.go b/server/secret_template_test.go index ef7738f..131db67 100644 --- a/server/secret_template_test.go +++ b/server/secret_template_test.go @@ -25,7 +25,7 @@ func TestSecretTemplate(t *testing.T) { return } - if template == nil { + if template == nil || template.Fields == nil { t.Error("secret data is nil") } diff --git a/server/secret_test.go b/server/secret_test.go index f6185e6..47ea406 100644 --- a/server/secret_test.go +++ b/server/secret_test.go @@ -39,20 +39,21 @@ func GetSecret(t *testing.T, tss *Server) { s, err := tss.Secret(id) if err != nil { - t.Error("calling server.Secret:", err) + t.Errorf("calling server.Secret: err = %+v, config = %+v, secretID = %d", err, tss, id) return } - if s == nil { - t.Error("secret data is nil") - } - - if _, ok := s.Field("password"); !ok { - t.Error("no password field") - } + if s != nil { + if _, ok := s.Field("password"); !ok { + t.Error("no password field") + } - if _, ok := s.Field("nonexistent"); ok { - t.Error("s.Field says nonexistent field exists") + if _, ok := s.Field("nonexistent"); ok { + t.Error("s.Field says nonexistent field exists") + } + } else { + t.Error("secret data is nil") + return } } @@ -90,6 +91,8 @@ func SecretCRUD(t *testing.T, tss *Server) { } fieldId := -1 + fieldIdUserName := -1 + fieldIdMachineName := -1 if siteId < 0 || folderId < 0 || templateId < 0 { return } @@ -105,6 +108,12 @@ func SecretCRUD(t *testing.T, tss *Server) { fieldId = field.SecretTemplateFieldID break } + if field.DisplayName == "Machine" { + fieldIdMachineName = field.SecretTemplateFieldID + } + if field.DisplayName == "Username" { + fieldIdUserName = field.SecretTemplateFieldID + } } if fieldId < 0 { t.Errorf("Unable to find a password field on the secret template with the given id '%d'", templateId) @@ -113,15 +122,21 @@ func SecretCRUD(t *testing.T, tss *Server) { t.Logf("Using field ID '%d' for the password field on the template with ID '%d'", fieldId, templateId) // Test creation of a new secret - refSecret := new(Secret) password := testPassword - refSecret.Name = "Test Secret" + refSecret := new(Secret) + refSecret.Name = "Secret Server Unit Test" refSecret.SiteID = siteId refSecret.FolderID = folderId refSecret.SecretTemplateID = templateId - refSecret.Fields = make([]SecretField, 1) - refSecret.Fields[0].FieldID = fieldId + refSecret.AutoChangeEnabled = false + refSecret.Fields = make([]SecretField, 3) + refSecret.Fields[0].FieldID = fieldId // password refSecret.Fields[0].ItemValue = password + refSecret.Fields[1].FieldID = fieldIdMachineName // machine + refSecret.Fields[1].ItemValue = "SS Test" + refSecret.Fields[2].FieldID = fieldIdUserName // username + refSecret.Fields[2].ItemValue = "ss_test_username" + sc, err := tss.CreateSecret(*refSecret) if err != nil { t.Error("calling server.CreateSecret:", err) @@ -211,14 +226,14 @@ func SecretCRUD(t *testing.T, tss *Server) { // Test the deletion of the new secret err = tss.DeleteSecret(sc.ID) if err != nil { - t.Error("calling server.DeleteSecret:", err) + t.Error("error calling server.DeleteSecret:", err) return } // Test read of the deleted secret fails s, err := tss.Secret(sc.ID) - if s != nil && s.Active { - t.Errorf("deleted secret with id '%d' returned from read", sc.ID) + if s != nil && s.Active || err != nil { + t.Errorf("deleted secret with id '%d' returned from read, err = %+v", sc.ID, err) } } @@ -599,8 +614,8 @@ func SecretCRUDForSSHTemplate(t *testing.T, tss *Server) { // Test read of the deleted secret fails s, err := tss.Secret(sc.ID) - if s != nil && s.Active { - t.Errorf("deleted secret with id '%d' returned from read", sc.ID) + if s != nil && s.Active || err != nil { + t.Errorf("deleted secret with id '%d' returned from read. err = %+v", sc.ID, err) } } @@ -687,7 +702,7 @@ func initServer() (*Server, error) { if cj, err := ioutil.ReadFile("../test_config.json"); err == nil { config = new(Configuration) - json.Unmarshal(cj, &config) + _ = json.Unmarshal(cj, &config) } else { config = &Configuration{ Credentials: UserCredential{ @@ -708,7 +723,10 @@ func initPlatformServer() (*Server, error) { if cj, err := ioutil.ReadFile("../test_config.json"); err == nil { config = new(Configuration) - json.Unmarshal(cj, &config) + err := json.Unmarshal(cj, &config) + if err != nil { + return nil, err + } } else { config = &Configuration{ Credentials: UserCredential{ diff --git a/server/server.go b/server/server.go index fc21aca..c8b1fa1 100644 --- a/server/server.go +++ b/server/server.go @@ -17,7 +17,7 @@ import ( const ( cloudBaseURLTemplate string = "https://%s.secretservercloud.%s/" - defaultAPIPathURI string = "/api/v1" + defaultAPIPathURI string = "/api/" defaultTokenPathURI string = "/oauth2/token" defaultTLD string = "com" ) @@ -30,9 +30,9 @@ type UserCredential struct { // Configuration settings for the API type Configuration struct { - Credentials UserCredential - ServerURL, TLD, Tenant, apiPathURI, tokenPathURI string - TLSClientConfig *tls.Config + Credentials UserCredential + ServerURL, TLD, Tenant, apiPathURI, apiVersion, tokenPathURI string + TLSClientConfig *tls.Config } // Server provides access to secrets stored in Delinea Secret Server @@ -78,9 +78,10 @@ func (s Server) urlFor(resource, path string) string { strings.Trim(baseURL, "/"), strings.Trim(s.tokenPathURI, "/")) default: - return fmt.Sprintf("%s/%s/%s/%s", + return fmt.Sprintf("%s/%s/%s/%s/%s", strings.Trim(baseURL, "/"), strings.Trim(s.apiPathURI, "/"), + strings.Trim(s.apiVersion, "/"), strings.Trim(resource, "/"), strings.Trim(path, "/")) } @@ -96,9 +97,10 @@ func (s Server) urlForSearch(resource, searchText, fieldName string) string { } switch { case resource == "secrets": - url := fmt.Sprintf("%s/%s/%s?paging.filter.searchText=%s&paging.filter.searchField=%s&paging.filter.doNotCalculateTotal=true&paging.take=30&&paging.skip=0", + url := fmt.Sprintf("%s/%s/%s/%s?paging.filter.searchText=%s&paging.filter.searchField=%s&paging.filter.doNotCalculateTotal=true&paging.take=30&&paging.skip=0", strings.Trim(baseURL, "/"), strings.Trim(s.apiPathURI, "/"), + strings.Trim(s.apiVersion, "/"), strings.Trim(resource, "/"), searchText, fieldName) @@ -113,7 +115,7 @@ func (s Server) urlForSearch(resource, searchText, fieldName string) string { // accessResource uses the accessToken to access the API resource. // It assumes an appropriate combination of method, resource, path and input. -func (s Server) accessResource(method, resource, path string, input interface{}) ([]byte, error) { +func (s Server) accessResource(method, resource, path, version string, input interface{}) ([]byte, error) { switch resource { case "secrets": case "secret-templates": @@ -136,14 +138,13 @@ func (s Server) accessResource(method, resource, path string, input interface{}) } accessToken, err := s.getAccessToken() - if err != nil { log.Print("[ERROR] error getting accessToken:", err) return nil, err } + s.apiVersion = version req, err := http.NewRequest(method, s.urlFor(resource, path), body) - if err != nil { log.Printf("[ERROR] creating req: %s /%s/%s: %s", method, resource, path, err) return nil, err @@ -166,7 +167,7 @@ func (s Server) accessResource(method, resource, path string, input interface{}) // searchResources uses the accessToken to search for API resources. // It assumes an appropriate combination of resource, search text. // field is optional -func (s Server) searchResources(resource, searchText, field string) ([]byte, error) { +func (s Server) searchResources(resource, searchText, field, apiVersion string) ([]byte, error) { switch resource { case "secrets": default: @@ -176,9 +177,6 @@ func (s Server) searchResources(resource, searchText, field string) ([]byte, err return nil, fmt.Errorf(message) } - method := "GET" - body := bytes.NewBuffer([]byte{}) - accessToken, err := s.getAccessToken() if err != nil { @@ -186,6 +184,9 @@ func (s Server) searchResources(resource, searchText, field string) ([]byte, err return nil, err } + method := "GET" + body := bytes.NewBuffer([]byte{}) + s.apiVersion = apiVersion req, err := http.NewRequest(method, s.urlForSearch(resource, searchText, field), body) if err != nil { @@ -239,6 +240,7 @@ func (s Server) uploadFile(secretId int, fileField SecretField) error { return err } + s.apiVersion = "v1/" // Make the request req, err := http.NewRequest("PUT", s.urlFor(resource, path), body) if err != nil {