Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 51 additions & 17 deletions v3/api/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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{
Expand All @@ -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
Expand Down
132 changes: 132 additions & 0 deletions v3/api/template_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading