diff --git a/v3/api/application.go b/v3/api/application.go new file mode 100644 index 0000000..d8002ad --- /dev/null +++ b/v3/api/application.go @@ -0,0 +1,232 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "fmt" + "log" + "strconv" + "strings" +) + +// isLegacyContainerAPI returns true when the server is pre-v25 and uses +// CertificateStoreContainers instead of the v25+ Applications endpoint. +// Both endpoints accept and return the same JSON schedule format. +func (c *Client) isLegacyContainerAPI() bool { + v := c.AuthClient.GetCommandVersion() + major := commandVersionMajor(v) + return major > 0 && major < 25 +} + +// commandVersionMajor parses the major version number from a product version +// string such as "24.4.0.0" or "25.1.0.0". Returns 0 if unparseable. +func commandVersionMajor(version string) int { + if version == "" { + return 0 + } + parts := strings.SplitN(version, ".", 2) + major, err := strconv.Atoi(parts[0]) + if err != nil { + return 0 + } + return major +} + +// appEndpoint returns the base API endpoint for the connected Command version. +// Pre-v25 uses CertificateStoreContainers; v25+ uses Applications. +// Both endpoints share the same JSON request/response format. +func (c *Client) appEndpoint() string { + if c.isLegacyContainerAPI() { + return "CertificateStoreContainers" + } + return "Applications" +} + +// ListApplications returns all applications/containers. +func (c *Client) ListApplications() ([]ApplicationListItem, error) { + log.Println("[INFO] Listing applications.") + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + req := &request{ + Method: "GET", + Endpoint: c.appEndpoint(), + Headers: headers, + } + + resp, err := c.sendRequest(req) + if err != nil { + return nil, err + } + + var result []ApplicationListItem + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +// GetApplication returns the full details of an application/container by integer ID. +func (c *Client) GetApplication(id int) (*ApplicationResponse, error) { + log.Printf("[INFO] Fetching application with ID %d.", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + req := &request{ + Method: "GET", + Endpoint: fmt.Sprintf("%s/%d", c.appEndpoint(), id), + Headers: headers, + } + + resp, err := c.sendRequest(req) + if err != nil { + return nil, err + } + + var result ApplicationResponse + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return &result, nil +} + +// GetApplicationByName returns the application matching the given name by +// listing all applications and then fetching the matching one by ID. +// Returns an error if no application with that name exists. +func (c *Client) GetApplicationByName(name string) (*ApplicationResponse, error) { + log.Printf("[INFO] Fetching application with name %q.", name) + + apps, err := c.ListApplications() + if err != nil { + return nil, err + } + + for _, app := range apps { + if app.Name == name { + return c.GetApplication(app.Id) + } + } + return nil, fmt.Errorf("application %q not found", name) +} + +// CreateApplication creates a new application/container and returns the created resource. +func (c *Client) CreateApplication(createReq *ApplicationCreateRequest) (*ApplicationResponse, error) { + log.Println("[INFO] Creating application.") + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + req := &request{ + Method: "POST", + Endpoint: c.appEndpoint(), + Headers: headers, + Payload: createReq, + } + + resp, err := c.sendRequest(req) + if err != nil { + return nil, err + } + + var result ApplicationResponse + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return &result, nil +} + +// UpdateApplication performs a full replacement of an existing application/container. +// For v25+ the API uses PUT /Applications with the ID in the body. +// For pre-v25 the API uses PUT /CertificateStoreContainers/{id} with the ID in the path. +func (c *Client) UpdateApplication(id int, updateReq *ApplicationUpdateRequest) (*ApplicationResponse, error) { + log.Printf("[INFO] Updating application with ID %d.", id) + + updateReq.Id = id + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + // Both pre-v25 and v25+ use PUT /{endpoint} with the ID in the request body. + endpoint := c.appEndpoint() + + req := &request{ + Method: "PUT", + Endpoint: endpoint, + Headers: headers, + Payload: updateReq, + } + + resp, err := c.sendRequest(req) + if err != nil { + return nil, err + } + + var result ApplicationResponse + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return &result, nil +} + +// DeleteApplication deletes an application/container by its integer ID. +// The server returns 204 No Content on success. +func (c *Client) DeleteApplication(id int) error { + log.Printf("[INFO] Deleting application with ID %d.", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + req := &request{ + Method: "DELETE", + Endpoint: fmt.Sprintf("%s/%d", c.appEndpoint(), id), + Headers: headers, + } + + resp, err := c.sendRequest(req) + if err != nil { + return err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to delete application: HTTP %d", resp.StatusCode) + } + + return nil +} diff --git a/v3/api/application_models.go b/v3/api/application_models.go new file mode 100644 index 0000000..63e1bac --- /dev/null +++ b/v3/api/application_models.go @@ -0,0 +1,96 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +// ApplicationScheduleInterval defines an interval-based inventory schedule. +type ApplicationScheduleInterval struct { + Minutes int `json:"Minutes"` +} + +// ApplicationScheduleDaily defines a daily time-based inventory schedule. +// Also reused as the shape for ExactlyOnce. +type ApplicationScheduleDaily struct { + Time string `json:"Time"` // ISO 8601 datetime string (e.g. "2023-11-25T23:30:00Z") +} + +// ApplicationScheduleWeekly defines a weekly inventory schedule. +// Days are weekday names ("Sunday"…"Saturday"); Time is an ISO 8601 UTC datetime. +type ApplicationScheduleWeekly struct { + Days []string `json:"Days"` // e.g. ["Monday", "Wednesday"] + Time string `json:"Time"` // ISO 8601 datetime string +} + +// ApplicationScheduleMonthly defines a monthly inventory schedule. +// Day is the day-of-month (1–31); Time is an ISO 8601 UTC datetime. +type ApplicationScheduleMonthly struct { + Day int `json:"Day"` + Time string `json:"Time"` // ISO 8601 datetime string +} + +// ApplicationSchedule holds the schedule configuration for an application. +// Set exactly one field; omit all to disable the schedule (Off). +// +// - Immediate: run once immediately (server may convert to ExactlyOnce on next read) +// - Interval: run every N minutes +// - Daily: run at the same time each day +// - Weekly: run on specific weekdays at a given time +// - Monthly: run on a specific day of each month at a given time +// - ExactlyOnce: run exactly once at the specified time +type ApplicationSchedule struct { + Immediate *bool `json:"Immediate,omitempty"` + Interval *ApplicationScheduleInterval `json:"Interval,omitempty"` + Daily *ApplicationScheduleDaily `json:"Daily,omitempty"` + Weekly *ApplicationScheduleWeekly `json:"Weekly,omitempty"` + Monthly *ApplicationScheduleMonthly `json:"Monthly,omitempty"` + ExactlyOnce *ApplicationScheduleDaily `json:"ExactlyOnce,omitempty"` +} + +// ApplicationCertStore is a minimal certificate store reference within an application detail response. +type ApplicationCertStore struct { + Id string `json:"Id"` // Store GUID (UUID) +} + +// ApplicationListItem represents one entry returned by GET /Applications (list endpoint). +// The Schedule field is returned as a cron expression string by the list endpoint. +type ApplicationListItem struct { + Id int `json:"Id"` + Name string `json:"Name"` + Schedule string `json:"Schedule"` +} + +// ApplicationResponse is the full application detail returned by GET /Applications/{id}. +type ApplicationResponse struct { + Id int `json:"Id"` + Name string `json:"Name"` + OverwriteSchedules bool `json:"OverwriteSchedules"` + Schedule *ApplicationSchedule `json:"Schedule,omitempty"` + CertificateStores []ApplicationCertStore `json:"CertificateStores,omitempty"` +} + +// ApplicationCreateRequest is the request body for POST /Applications. +type ApplicationCreateRequest struct { + Name string `json:"Name"` + OverwriteSchedules bool `json:"OverwriteSchedules"` + Schedule *ApplicationSchedule `json:"Schedule,omitempty"` +} + +// ApplicationUpdateRequest is the request body for PUT /Applications/{id}. +// The Id field is set automatically by UpdateApplication. +type ApplicationUpdateRequest struct { + Id int `json:"Id"` + Name string `json:"Name"` + OverwriteSchedules bool `json:"OverwriteSchedules"` + Schedule *ApplicationSchedule `json:"Schedule,omitempty"` +} diff --git a/v3/api/certificate.go b/v3/api/certificate.go index b2ac95e..8e281c2 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -299,27 +299,58 @@ func (c *Client) DownloadCertificate( return nil, nil, &jsonResp.Content, p7bErr } - var leaf *x509.Certificate + leaf := findLeafCert(certs) if len(certs) > 1 { - //leaf is last cert in chain - leaf = certs[0] // First cert in chain is the leaf return leaf, certs, &jsonResp.Content, nil } + return leaf, nil, &jsonResp.Content, nil +} + +// findLeafCert returns the end-entity (leaf) certificate from a set of +// certificates. It identifies the leaf as the cert whose Subject is not used +// as an Issuer by any other cert in the set — i.e. nothing is signed by it. +// This is order-independent and handles both root-first and leaf-first P7Bs. +// +// When the set contains only one cert, or when the algorithm cannot determine +// a unique leaf (e.g. all certs are self-signed), certs[0] is returned as a +// safe fallback. +func findLeafCert(certs []*x509.Certificate) *x509.Certificate { + if len(certs) == 0 { + return nil + } + if len(certs) == 1 { + return certs[0] + } - return certs[0], nil, &jsonResp.Content, nil + // Build a set of all RawIssuer values (subjects that issued something). + issuers := make(map[string]bool, len(certs)) + for _, c := range certs { + issuers[string(c.RawIssuer)] = true + } + + // The leaf's Subject is not in the issuers set. + for _, c := range certs { + if !issuers[string(c.RawSubject)] { + return c + } + } + + // Fallback: cannot distinguish (e.g. single self-signed cert in multi-cert set). + return certs[0] } // EnrollCSR takes arguments for EnrollCSRFctArgs to enroll a passed Certificate Signing // Request with Keyfactor. An EnrollResponse containing a signed certificate is returned upon successful // enrollment. Required fields to complete a CSR enrollment are: // - CSR : string -// - Template : string +// - Template : string (or EnrollmentPatternId on Command v25+) // - CertificateAuthority : string func (c *Client) EnrollCSR(ea *EnrollCSRFctArgs) (*EnrollResponse, error) { log.Println("[INFO] Signing CSR with Keyfactor") - /* Ensure required inputs exist */ - if (ea.Template == "") || (ea.CertificateAuthority == "") { + /* Ensure required inputs exist. + On Command v25+ an EnrollmentPatternId can substitute for Template. */ + if (ea.Template == "" && ea.EnrollmentPatternId == 0) || (ea.CertificateAuthority == "") { return nil, errors.New("invalid or nonexistent values required for csr enrollment") } diff --git a/v3/api/client.go b/v3/api/client.go index 7c7c136..add6a21 100644 --- a/v3/api/client.go +++ b/v3/api/client.go @@ -124,6 +124,14 @@ type AuthConfig interface { Authenticate() error GetHttpClient() (*http.Client, error) GetServerConfig() *auth_providers.Server + GetCommandVersion() string +} + +// NewKeyfactorClientWithAuth creates a Client with a pre-built AuthConfig, bypassing +// the Authenticate() network call. Used in unit tests with VCR cassettes. +func NewKeyfactorClientWithAuth(auth AuthConfig, ctx *context.Context) *Client { + initLogger(ctx) + return &Client{AuthClient: auth} } // NewKeyfactorClient creates a new Keyfactor client instance. A configured Client is returned with methods used to diff --git a/v3/api/helpers.go b/v3/api/helpers.go index 8a2d460..c57f590 100644 --- a/v3/api/helpers.go +++ b/v3/api/helpers.go @@ -242,6 +242,13 @@ func EncodePrivateKey(key interface{}) (*pem.Block, error) { Type: "PRIVATE KEY", Bytes: k, }, nil + case *pkcs12.OpaquePrivateKey: + // Algorithm not supported by Go's x509 (e.g. Ed448, OID 1.3.101.113). + // The DER is already valid PKCS#8; wrap it directly. + return &pem.Block{ + Type: "PRIVATE KEY", + Bytes: k.DER, + }, nil default: return nil, fmt.Errorf("unsupported private key type: %T", key) } diff --git a/v3/api/pam_types_test.go b/v3/api/pam_types_test.go index da664aa..e5b1d8f 100644 --- a/v3/api/pam_types_test.go +++ b/v3/api/pam_types_test.go @@ -41,6 +41,10 @@ func (m *mockAuthConfig) Authenticate() error { return nil } +func (m *mockAuthConfig) GetCommandVersion() string { + return "25.1.0.0" +} + // newTestClient creates a test client with mock server func newTestClient(server *httptest.Server) *Client { return &Client{ @@ -62,18 +66,18 @@ var ( mockProviderTypeResponse = ProviderTypeResponse{ Id: mockProviderTypeId, - Name: &mockProviderTypeName, + Name: mockProviderTypeName, Parameters: &[]ProviderTypeParameterResponse{ { Id: 1, - Name: stringPtr("Username"), + Name: "Username", DisplayName: stringPtr("User Name"), DataType: PamParameterDataTypeString, InstanceLevel: false, }, { Id: 2, - Name: stringPtr("Password"), + Name: "Password", DisplayName: stringPtr("Password"), DataType: PamParameterDataTypeSecret, InstanceLevel: true, diff --git a/v3/api/store.go b/v3/api/store.go index 7ff31b1..9baf9f7 100644 --- a/v3/api/store.go +++ b/v3/api/store.go @@ -117,7 +117,7 @@ func (c *Client) UpdateStore(ua *UpdateStoreFctArgs) (*UpdateStoreResponse, erro } keyfactorAPIStruct := &request{ - Method: "Put", + Method: "PUT", Endpoint: "CertificateStores", Headers: headers, Payload: &ua, @@ -614,6 +614,47 @@ func unmarshalPropertiesString(properties string) map[string]interface{} { return make(map[string]interface{}) } +// ScheduleImmediateInventory triggers an immediate inventory job on the given certificate store. +// If the store already has any inventory schedule configured this is a no-op. +// When no schedule is present (common for newly-created stores that have never had inventory run) +// it sends a PUT with InventorySchedule.Immediate=true so the orchestrator runs inventory on its +// next check-in and updates Command's inventory record. +// +// Password and server-credential Properties (ServerUsername/Password/UseSsl) are not included in +// the PUT body because they are write-only and are not returned by the GET endpoint. With the +// Password field now tagged json:"Password,omitempty", nil is omitted from JSON entirely so +// Command does not receive a null and will preserve whatever password is already configured. +func (c *Client) ScheduleImmediateInventory(storeId string) error { + storeResp, err := c.GetCertificateStoreByID(storeId) + if err != nil { + return fmt.Errorf("ScheduleImmediateInventory: could not read store %s: %w", storeId, err) + } + + // No-op if any schedule is already configured. + sched := storeResp.InventorySchedule + if sched.Immediate != nil || sched.Interval != nil || sched.Daily != nil || sched.ExactlyOnce != nil { + return nil + } + + immediate := true + _, err = c.UpdateStore(&UpdateStoreFctArgs{ + Id: storeResp.Id, + ClientMachine: storeResp.ClientMachine, + StorePath: storeResp.StorePath, + CertStoreType: storeResp.CertStoreType, + AgentId: storeResp.AgentId, + // Password intentionally nil (omitted from JSON via omitempty) — preserves existing password. + // PropertiesString intentionally empty (omitted from JSON via omitempty) — preserves existing properties. + InventorySchedule: &InventorySchedule{ + Immediate: &immediate, + }, + }) + if err != nil { + return fmt.Errorf("ScheduleImmediateInventory: UpdateStore failed for store %s: %w", storeId, err) + } + return nil +} + func validateCreateStoreArgs(ca *CreateStoreFctArgs) error { if ca.ClientMachine == "" { return errors.New("client machine is required for creation of new certificate store") diff --git a/v3/api/store_container.go b/v3/api/store_container.go index 1a7dcdf..74ec154 100644 --- a/v3/api/store_container.go +++ b/v3/api/store_container.go @@ -15,6 +15,7 @@ package api import ( + "bytes" "encoding/json" "fmt" "log" @@ -128,3 +129,95 @@ func (c *Client) GetStoreContainer(id interface{}) (*CertStoreContainer, error) } return nil, fmt.Errorf("invalid API response from Keyfactor while getting cert store container %s", id) } + +// CreateStoreContainer creates a new certificate store container (pre-v25 legacy API). +func (c *Client) CreateStoreContainer(req *CertStoreContainer) (*CertStoreContainer, error) { + log.Println("[INFO] Creating certificate store container.") + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + payload, err := json.Marshal(req) + if err != nil { + return nil, err + } + + keyfactorAPIStruct := &request{ + Method: "POST", + Endpoint: "CertificateStoreContainers", + Headers: headers, + Payload: bytes.NewReader(payload), + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + jsonResp := &CertStoreContainer{} + if err = json.NewDecoder(resp.Body).Decode(jsonResp); err != nil { + return nil, err + } + return jsonResp, nil +} + +// UpdateStoreContainer updates an existing certificate store container (pre-v25 legacy API). +func (c *Client) UpdateStoreContainer(id int, req *CertStoreContainer) (*CertStoreContainer, error) { + log.Printf("[INFO] Updating certificate store container %d.\n", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + req.Id = &id + payload, err := json.Marshal(req) + if err != nil { + return nil, err + } + + keyfactorAPIStruct := &request{ + Method: "PUT", + Endpoint: fmt.Sprintf("CertificateStoreContainers/%d", id), + Headers: headers, + Payload: bytes.NewReader(payload), + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + jsonResp := &CertStoreContainer{} + if err = json.NewDecoder(resp.Body).Decode(jsonResp); err != nil { + return nil, err + } + return jsonResp, nil +} + +// DeleteStoreContainer deletes a certificate store container by ID (pre-v25 legacy API). +func (c *Client) DeleteStoreContainer(id int) error { + log.Printf("[INFO] Deleting certificate store container %d.\n", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "DELETE", + Endpoint: fmt.Sprintf("CertificateStoreContainers/%d", id), + Headers: headers, + } + + _, err := c.sendRequest(keyfactorAPIStruct) + return err +} diff --git a/v3/api/store_models.go b/v3/api/store_models.go index 6cbb38c..0486fea 100644 --- a/v3/api/store_models.go +++ b/v3/api/store_models.go @@ -34,7 +34,7 @@ type CreateStoreFctArgs struct { InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *UpdateStorePasswordConfig `json:"Password"` + Password *UpdateStorePasswordConfig `json:"Password,omitempty"` } // UpdateStoreFctArgs holds the function arguments used for calling the UpdateStore method. @@ -58,7 +58,7 @@ type UpdateStoreFctArgs struct { InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *UpdateStorePasswordConfig `json:"Password"` + Password *UpdateStorePasswordConfig `json:"Password,omitempty"` } type UpdateStorePasswordConfig struct { diff --git a/v3/api/store_type.go b/v3/api/store_type.go index 94dff94..15032c0 100644 --- a/v3/api/store_type.go +++ b/v3/api/store_type.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "log" + "strconv" ) // GetCertificateStoreType takes arguments for a certificate store type ID or name and if found will return the certificate store type @@ -101,9 +102,9 @@ func (c *Client) GetCertificateStoreTypeById(id int) (*CertificateStoreType, err return &jsonResp, nil } -// ListCertificateStoreTypes takes no arguments and returns a list of certificate store types from Keyfactor. +// ListCertificateStoreTypes returns all certificate store types from Keyfactor, paginating +// automatically using pq.pageReturned / pq.returnLimit until all results are fetched. func (c *Client) ListCertificateStoreTypes() (*[]CertificateStoreType, error) { - // Set Keyfactor-specific headers headers := &apiHeaders{ Headers: []StringTuple{ {"x-keyfactor-api-version", "1"}, @@ -111,25 +112,38 @@ func (c *Client) ListCertificateStoreTypes() (*[]CertificateStoreType, error) { }, } - endpoint := "CertificateStoreTypes" - keyfactorAPIStruct := &request{ - Method: "GET", - Endpoint: endpoint, - Headers: headers, - Payload: nil, - } - - resp, err := c.sendRequest(keyfactorAPIStruct) - if err != nil { - return nil, err - } - - var jsonResp []CertificateStoreType - err = json.NewDecoder(resp.Body).Decode(&jsonResp) - if err != nil { - return nil, err + const pageSize = 100 + var all []CertificateStoreType + for page := 1; ; page++ { + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: "CertificateStoreTypes", + Headers: headers, + Payload: nil, + Query: &apiQuery{ + Query: []StringTuple{ + {"PageReturned", strconv.Itoa(page)}, + {"ReturnLimit", strconv.Itoa(pageSize)}, + }, + }, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var pageResults []CertificateStoreType + err = json.NewDecoder(resp.Body).Decode(&pageResults) + if err != nil { + return nil, err + } + all = append(all, pageResults...) + if len(pageResults) < pageSize { + break + } } - return &jsonResp, nil + return &all, nil } // CreateStoreType takes arguments for CreateStoreFctArgs to facilitate the creation