diff --git a/v3/api/template.go b/v3/api/template.go index 73e7423..161f703 100644 --- a/v3/api/template.go +++ b/v3/api/template.go @@ -18,8 +18,15 @@ import ( "encoding/json" "errors" "fmt" + "log" + "strconv" ) +// getTemplatesMaxPages is the maximum number of pages GetTemplates will fetch +// before aborting with an error. It is an unexported package-level var (not a +// const) so that tests can lower it without iterating thousands of times. +var getTemplatesMaxPages = 10000 + // GetTemplate takes arguments for a template ID used to facilitate the retrieval // of certificate template context. The primary query required to get certificate context is the template ID. A pointer // to a GetTemplateResponse structure is returned, containing the template context. @@ -59,9 +66,12 @@ func (c *Client) GetTemplate(Id interface{}) (*GetTemplateResponse, error) { return jsonResp, err } -// GetTemplates asks Keyfactor for a complete list of known certificate templates. A list of -// GetTemplateResponse structures is returned, containing the template context. +// GetTemplates asks Keyfactor for a complete list of known certificate templates, +// paginating automatically so that instances with more than the server's default +// page size (50) return all templates. A list of GetTemplateResponse structures +// is returned, containing the template context. func (c *Client) GetTemplates() ([]GetTemplateResponse, error) { + log.Println("[INFO] Listing certificate templates.") // Set Keyfactor-specific headers headers := &apiHeaders{ @@ -71,25 +81,49 @@ func (c *Client) GetTemplates() ([]GetTemplateResponse, error) { }, } - keyfactorAPIStruct := &request{ - Method: "GET", - Endpoint: "Templates/", - Headers: headers, - Query: nil, - Payload: nil, + const pageSize = 100 + var all []GetTemplateResponse + var page int + for page = 1; page <= getTemplatesMaxPages; page++ { + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: "Templates/", + Headers: headers, + Query: &apiQuery{ + Query: []StringTuple{ + {"PageReturned", strconv.Itoa(page)}, + {"ReturnLimit", strconv.Itoa(pageSize)}, + }, + }, + Payload: nil, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + log.Printf("[ERROR] GetTemplates: request for page %d failed: %s", page, err) + return nil, err + } + + var pageResults []GetTemplateResponse + decodeErr := json.NewDecoder(resp.Body).Decode(&pageResults) + resp.Body.Close() + if decodeErr != nil { + log.Printf("[ERROR] GetTemplates: failed to decode page %d: %s", page, decodeErr) + return nil, decodeErr + } + + all = append(all, pageResults...) + if len(pageResults) == 0 || len(pageResults) < pageSize { + break + } } - resp, err := c.sendRequest(keyfactorAPIStruct) - if err != nil { - return nil, err + if page > getTemplatesMaxPages { + return nil, fmt.Errorf("GetTemplates: exceeded max pages (%d); server may be ignoring pagination", getTemplatesMaxPages) } - var jsonResp []GetTemplateResponse - err = json.NewDecoder(resp.Body).Decode(&jsonResp) - if err != nil { - return nil, err - } - return jsonResp, err + log.Printf("[INFO] Listed %d certificate templates across %d page(s).", len(all), page) + return all, nil } // UpdateTemplate takes arguments for a UpdateTemplateArg structure used to facilitate the modification diff --git a/v3/api/template_test.go b/v3/api/template_test.go new file mode 100644 index 0000000..d6bc71a --- /dev/null +++ b/v3/api/template_test.go @@ -0,0 +1,132 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" +) + +// TestGetTemplates_Pagination verifies that GetTemplates fetches all pages. +// Before the fix, only the first 50 results were returned; a template sorted +// beyond position 50 (e.g. position 169 out of 278) would never be found by +// name, causing "Error template name not found" in keyfactor_template_role_binding. +func TestGetTemplates_Pagination(t *testing.T) { + const totalTemplates = 278 + + allTemplates := make([]GetTemplateResponse, totalTemplates) + for i := range allTemplates { + allTemplates[i] = GetTemplateResponse{ + Id: i + 1, + CommonName: "Template-" + strconv.Itoa(i+1), + } + } + // Place the known-problematic late-sorted template at index 168 (position 169). + allTemplates[168] = GetTemplateResponse{Id: 243, CommonName: "zzz-late-sorted-template"} + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page, _ := strconv.Atoi(r.URL.Query().Get("PageReturned")) + limit, _ := strconv.Atoi(r.URL.Query().Get("ReturnLimit")) + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 50 + } + start := (page - 1) * limit + end := start + limit + if start >= len(allTemplates) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("[]")) + return + } + if end > len(allTemplates) { + end = len(allTemplates) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(allTemplates[start:end]) + })) + defer srv.Close() + + c := newTestClient(srv) + + templates, err := c.GetTemplates() + if err != nil { + t.Fatalf("GetTemplates() error: %v", err) + } + if len(templates) != totalTemplates { + t.Errorf("GetTemplates() returned %d templates, want %d (pagination broken)", len(templates), totalTemplates) + } + + // Verify the late-sorted target template is present. + found := false + for _, tmpl := range templates { + if tmpl.CommonName == "zzz-late-sorted-template" { + found = true + if tmpl.Id != 243 { + t.Errorf("target template ID = %d, want 243", tmpl.Id) + } + break + } + } + if !found { + t.Errorf("target template %q (position 169) not found — page 2+ results missing", "zzz-late-sorted-template") + } +} + +// TestGetTemplates_MaxPagesGuard verifies that GetTemplates aborts with an error +// when the server always returns a full page (simulating a server that ignores +// pagination and would otherwise cause an infinite loop / unbounded memory growth). +func TestGetTemplates_MaxPagesGuard(t *testing.T) { + // Build a fixed full page of pageSize (100) items. + fullPage := make([]GetTemplateResponse, 100) + for i := range fullPage { + fullPage[i] = GetTemplateResponse{Id: i + 1, CommonName: "Template-" + strconv.Itoa(i+1)} + } + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Always return a full page regardless of PageReturned — simulates a + // server that ignores paging parameters. + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(fullPage) + })) + defer srv.Close() + + // Lower the safety bound so the test terminates quickly. + orig := getTemplatesMaxPages + getTemplatesMaxPages = 3 + defer func() { getTemplatesMaxPages = orig }() + + c := newTestClient(srv) + + _, err := c.GetTemplates() + if err == nil { + t.Fatal("GetTemplates() expected an error when max pages exceeded, got nil") + } +} + +// TestGetTemplates_SinglePage verifies that a sub-pageSize result terminates +// the pagination loop in a single call. +func TestGetTemplates_SinglePage(t *testing.T) { + calls := 0 + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode([]GetTemplateResponse{{Id: 1, CommonName: "OnlyTemplate"}}) + })) + defer srv.Close() + + c := newTestClient(srv) + + templates, err := c.GetTemplates() + if err != nil { + t.Fatalf("GetTemplates() error: %v", err) + } + if len(templates) != 1 { + t.Errorf("got %d templates, want 1", len(templates)) + } + if calls != 1 { + t.Errorf("server called %d times, want 1", calls) + } +}