diff --git a/api/v1/README.md b/api/v1/README.md index f11929bf..3370b185 100644 --- a/api/v1/README.md +++ b/api/v1/README.md @@ -213,7 +213,7 @@ The GET route for the Vulnerabilities resource displays the vulnerabilities data ###### Example Request ```json -GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities?page=0&limit=2 HTTP/1.1 +GET http://localhost:6060/v1/namespaces/debian%3A8/vulnerabilities?limit=2 HTTP/1.1 ``` ###### Example Response @@ -247,7 +247,8 @@ Server: clair } } } - ] + ], + "NextPage":"gAAAAABW1ABiOlm6KMDKYFE022bEy_IFJdm4ExxTNuJZMN0Eycn0Sut2tOH9bDB4EWGy5s6xwATUHiG-6JXXaU5U32sBs6_DmA==" } ``` diff --git a/api/v1/models.go b/api/v1/models.go index 456024e5..72885283 100644 --- a/api/v1/models.go +++ b/api/v1/models.go @@ -19,6 +19,7 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "time" "github.com/coreos/clair/database" @@ -276,6 +277,7 @@ type NamespaceEnvelope struct { type VulnerabilityEnvelope struct { Vulnerability *Vulnerability `json:"Vulnerability,omitempty"` Vulnerabilities *[]Vulnerability `json:"Vulnerabilities,omitempty"` + NextPage *string `json:"NextPage,omitempty"` Error *Error `json:"Error,omitempty"` } @@ -317,3 +319,24 @@ func pageNumberToToken(page database.VulnerabilityNotificationPageNumber, key st return string(tokenBytes) } + +func tokenToNumber(token, key string) (int, error) { + k, _ := fernet.DecodeKey(key) + msg := fernet.VerifyAndDecrypt([]byte(token), time.Hour, []*fernet.Key{k}) + if msg == nil { + return -1, errors.New("invalid or expired pagination token") + } + + page, err := strconv.Atoi(string(msg)) + return page, err +} + +func numberToToken(page int, key string) string { + k, _ := fernet.DecodeKey(key) + tokenBytes, err := fernet.EncryptAndSign([]byte(strconv.Itoa(page)), k) + if err != nil { + log.Fatal("failed to encrypt number") + } + + return string(tokenBytes) +} diff --git a/api/v1/routes.go b/api/v1/routes.go index f7357f2a..0dc48572 100644 --- a/api/v1/routes.go +++ b/api/v1/routes.go @@ -202,21 +202,17 @@ func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Par return getVulnerabilitiesRoute, http.StatusBadRequest } + page := 0 pageStrs, pageExists := query["page"] - if !pageExists { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"must provide page query parameter"}}) - return getVulnerabilitiesRoute, http.StatusBadRequest - } - page, err := strconv.Atoi(pageStrs[0]) - if err != nil { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid page format: " + err.Error()}}) - return getVulnerabilitiesRoute, http.StatusBadRequest - } else if page < 0 { - writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"page value should not be less than zero"}}) - return getVulnerabilitiesRoute, http.StatusBadRequest + if pageExists { + page, err = tokenToNumber(pageStrs[0], ctx.Config.PaginationKey) + if err != nil { + writeResponse(w, r, http.StatusBadRequest, VulnerabilityEnvelope{Error: &Error{"invalid page format: " + err.Error()}}) + return getNotificationRoute, http.StatusBadRequest + } } - dbVulns, err := ctx.Store.ListVulnerabilities(p.ByName("namespaceName"), limit, page) + dbVulns, nextPage, err := ctx.Store.ListVulnerabilities(p.ByName("namespaceName"), limit, page) if err != nil { writeResponse(w, r, http.StatusInternalServerError, VulnerabilityEnvelope{Error: &Error{err.Error()}}) return getVulnerabilitiesRoute, http.StatusInternalServerError @@ -228,7 +224,12 @@ func getVulnerabilities(w http.ResponseWriter, r *http.Request, p httprouter.Par vulns = append(vulns, vuln) } - writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerabilities: &vulns}) + var nextPageStr string + if nextPage != -1 { + nextPageStr = numberToToken(nextPage, ctx.Config.PaginationKey) + } + + writeResponse(w, r, http.StatusOK, VulnerabilityEnvelope{Vulnerabilities: &vulns, NextPage: &nextPageStr}) return getVulnerabilitiesRoute, http.StatusOK } diff --git a/database/database.go b/database/database.go index cec45f0f..0c84f99e 100644 --- a/database/database.go +++ b/database/database.go @@ -62,7 +62,9 @@ type Datastore interface { // # Vulnerability // ListVulnerabilities returns the list of vulnerabilies of a certain Namespace. // The Limit and page parameters are used to paginate the return list. - ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, error) + // The first given page should be 0. The function will then return the next available page. + // If there is no more page, -1 has to be returned. + ListVulnerabilities(namespaceName string, limit int, page int) ([]Vulnerability, int, error) // InsertVulnerabilities stores the given Vulnerabilities in the database, updating them if // necessary. A vulnerability is uniquely identified by its Namespace and its Name. diff --git a/database/pgsql/queries.go b/database/pgsql/queries.go index a79f4a86..0b67ecf5 100644 --- a/database/pgsql/queries.go +++ b/database/pgsql/queries.go @@ -145,8 +145,9 @@ const ( searchVulnerabilityByNamespaceAndName = ` WHERE n.name = $1 AND v.name = $2 AND v.deleted_at IS NULL` searchVulnerabilityByID = ` WHERE v.id = $1` searchVulnerabilityByNamespace = ` WHERE n.name = $1 AND v.deleted_at IS NULL - ORDER BY v.name - LIMIT $2 offset $3` + AND v.id >= $2 + ORDER BY v.id + LIMIT $3` searchVulnerabilityFixedIn = ` SELECT vfif.version, f.id, f.Name diff --git a/database/pgsql/vulnerability.go b/database/pgsql/vulnerability.go index ade3a6f6..035f1413 100644 --- a/database/pgsql/vulnerability.go +++ b/database/pgsql/vulnerability.go @@ -28,22 +28,20 @@ import ( "github.com/guregu/null/zero" ) -func (pgSQL *pgSQL) ListVulnerabilities(namespaceName string, limit int, page int) ([]database.Vulnerability, error) { - return listVulnerabilities(pgSQL, namespaceName, limit, page) -} - -func listVulnerabilities(queryer Queryer, namespaceName string, limit int, page int) ([]database.Vulnerability, error) { +func (pgSQL *pgSQL) ListVulnerabilities(namespaceName string, limit int, startID int) ([]database.Vulnerability, int, error) { defer observeQueryTime("listVulnerabilities", "all", time.Now()) // Query. query := searchVulnerabilityBase + searchVulnerabilityByNamespace - rows, err := queryer.Query(query, namespaceName, limit, page*limit) + rows, err := pgSQL.Query(query, namespaceName, startID, limit+1) if err != nil { - return nil, handleError("searchVulnerabilityByNamespace", err) + return nil, -1, handleError("searchVulnerabilityByNamespace", err) } defer rows.Close() var vulns []database.Vulnerability + nextID := -1 + size := 0 // Scan query. for rows.Next() { var vulnerability database.Vulnerability @@ -59,17 +57,22 @@ func listVulnerabilities(queryer Queryer, namespaceName string, limit int, page &vulnerability.Metadata, ) if err != nil { - return nil, handleError("searchVulnerabilityByNamespace.Scan()", err) + return nil, -1, handleError("searchVulnerabilityByNamespace.Scan()", err) + } + size++ + if size > limit { + nextID = vulnerability.ID } else { vulns = append(vulns, vulnerability) } } if err := rows.Err(); err != nil { - return nil, handleError("searchVulnerabilityByNamespace.Rows()", err) + return nil, -1, handleError("searchVulnerabilityByNamespace.Rows()", err) } - return vulns, nil + fmt.Println(nextID) + return vulns, nextID, nil } func (pgSQL *pgSQL) FindVulnerability(namespaceName, name string) (database.Vulnerability, error) {