From 0bc8bc54b1f4355d8ab186dfaf78985635d5b998 Mon Sep 17 00:00:00 2001 From: Bob Callaway <bobcallaway@users.noreply.github.com> Date: Tue, 19 Jan 2021 09:58:04 -0500 Subject: [PATCH] add redis-based index & query endpoint (#112) Signed-off-by: Bob Callaway <bcallawa@redhat.com> Co-authored-by: Dan Lorenc <dlorenc@google.com> --- Dockerfile | 4 +- cmd/cli/app/pflags.go | 44 +++- cmd/cli/app/pflags_test.go | 144 +++++++++++ cmd/cli/app/search.go | 178 ++++++++++++++ cmd/server/app/root.go | 4 + config/rekor.yaml | 2 + docker-compose.debug.yml | 3 + docker-compose.yml | 12 + go.mod | 3 +- go.sum | 8 + openapi.yaml | 47 +++- pkg/api/api.go | 11 +- pkg/api/entries.go | 13 + pkg/api/error.go | 13 + pkg/api/index.go | 98 ++++++++ .../client/entries/entries_client.go | 10 +- pkg/generated/client/index/index_client.go | 86 +++++++ .../client/index/search_index_parameters.go | 152 ++++++++++++ .../client/index/search_index_responses.go | 171 +++++++++++++ pkg/generated/client/rekor_client.go | 7 +- pkg/generated/client/tlog/tlog_client.go | 6 +- pkg/generated/models/search_index.go | 226 ++++++++++++++++++ .../restapi/configure_rekor_server.go | 8 + pkg/generated/restapi/doc.go | 1 - pkg/generated/restapi/embedded_spec.go | 178 +++++++++++++- .../restapi/operations/index/search_index.go | 75 ++++++ .../index/search_index_parameters.go | 94 ++++++++ .../index/search_index_responses.go | 180 ++++++++++++++ .../index/search_index_urlbuilder.go | 101 ++++++++ .../restapi/operations/rekor_server_api.go | 13 + pkg/generated/restapi/server.go | 1 - pkg/types/rekord/rekord_test.go | 4 + pkg/types/rekord/v0.0.1/entry.go | 82 +++---- pkg/types/types.go | 1 + pkg/util/fetch.go | 67 ++++++ tests/e2e_test.go | 19 +- 36 files changed, 1988 insertions(+), 78 deletions(-) create mode 100644 cmd/cli/app/search.go create mode 100644 pkg/api/index.go create mode 100644 pkg/generated/client/index/index_client.go create mode 100644 pkg/generated/client/index/search_index_parameters.go create mode 100644 pkg/generated/client/index/search_index_responses.go create mode 100644 pkg/generated/models/search_index.go create mode 100644 pkg/generated/restapi/operations/index/search_index.go create mode 100644 pkg/generated/restapi/operations/index/search_index_parameters.go create mode 100644 pkg/generated/restapi/operations/index/search_index_responses.go create mode 100644 pkg/generated/restapi/operations/index/search_index_urlbuilder.go create mode 100644 pkg/util/fetch.go diff --git a/Dockerfile b/Dockerfile index 9a2fa1b..848a7e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN go build ./cmd/server RUN CGO_ENABLED=0 go build -gcflags "all=-N -l" -o server_debug ./cmd/server # Multi-Stage production build -FROM registry.access.redhat.com/ubi8/ubi-minimal as deploy +FROM golang:1.15 as deploy # Retrieve the binary from the previous stage COPY --from=builder /opt/app-root/src/server /usr/local/bin/rekor-server @@ -23,7 +23,7 @@ CMD ["rekor-server", "serve"] # debug compile options & debugger FROM deploy as debug +RUN go get github.com/go-delve/delve/cmd/dlv # overwrite server and include debugger COPY --from=builder /opt/app-root/src/server_debug /usr/local/bin/rekor-server -COPY --from=builder /usr/bin/dlv /usr/local/bin/dlv diff --git a/cmd/cli/app/pflags.go b/cmd/cli/app/pflags.go index b2a2a4e..4d7ced9 100644 --- a/cmd/cli/app/pflags.go +++ b/cmd/cli/app/pflags.go @@ -36,9 +36,37 @@ import ( "github.com/spf13/viper" ) +func addSearchPFlags(cmd *cobra.Command) error { + cmd.Flags().Var(&pkiFormatFlag{value: "pgp"}, "pki-format", "format of the signature and/or public key") + + cmd.Flags().Var(&fileOrURLFlag{}, "public-key", "path or URL to public key file") + + cmd.Flags().Var(&fileOrURLFlag{}, "artifact", "path or URL to artifact file") + + cmd.Flags().Var(&shaFlag{}, "sha", "the sha of the artifact") + return nil +} + +func validateSearchPFlags() error { + artifactStr := viper.GetString("artifact") + + publicKey := viper.GetString("public-key") + sha := viper.GetString("sha") + + if artifactStr == "" && publicKey == "" && sha == "" { + return errors.New("either 'sha' or 'artifact' or 'public-key' must be specified") + } + if publicKey != "" { + if viper.GetString("pki-format") == "" { + return errors.New("pki-format must be specified if searching by public-key") + } + } + return nil +} + func addArtifactPFlags(cmd *cobra.Command) error { cmd.Flags().Var(&fileOrURLFlag{}, "signature", "path or URL to detached signature file") - cmd.Flags().Var(&sigFormatFlag{value: "pgp"}, "signature-format", "format of the signature") + cmd.Flags().Var(&pkiFormatFlag{value: "pgp"}, "pki-format", "format of the signature and/or public key") cmd.Flags().Var(&fileOrURLFlag{}, "public-key", "path or URL to public key file") @@ -161,8 +189,8 @@ func CreateRekordFromPFlags() (models.ProposedEntry, error) { } re.RekordObj.Signature = &models.RekordV001SchemaSignature{} - sigFormat := viper.GetString("signature-format") - switch sigFormat { + pkiFormat := viper.GetString("pki-format") + switch pkiFormat { case "pgp": re.RekordObj.Signature.Format = models.RekordV001SchemaSignatureFormatPgp case "minisign": @@ -240,19 +268,19 @@ func (f *fileOrURLFlag) Type() string { return "fileOrURLFlag" } -type sigFormatFlag struct { +type pkiFormatFlag struct { value string } -func (f *sigFormatFlag) Type() string { - return "sigFormat" +func (f *pkiFormatFlag) Type() string { + return "pkiFormat" } -func (f *sigFormatFlag) String() string { +func (f *pkiFormatFlag) String() string { return f.value } -func (f *sigFormatFlag) Set(s string) error { +func (f *pkiFormatFlag) Set(s string) error { set := map[string]struct{}{ "pgp": {}, "minisign": {}, diff --git a/cmd/cli/app/pflags_test.go b/cmd/cli/app/pflags_test.go index f72f695..d18be1e 100644 --- a/cmd/cli/app/pflags_test.go +++ b/cmd/cli/app/pflags_test.go @@ -393,3 +393,147 @@ func TestValidateRekorServerURL(t *testing.T) { } } } + +func TestSearchPFlags(t *testing.T) { + type test struct { + caseDesc string + artifact string + publicKey string + sha string + pkiFormat string + expectParseSuccess bool + expectValidateSuccess bool + } + + testServer := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + file := []byte{} + var err error + + switch r.URL.Path { + case "/artifact": + file, err = ioutil.ReadFile("../../../tests/test_file.txt") + case "/publicKey": + file, err = ioutil.ReadFile("../../../tests/test_public_key.key") + case "/not_found": + err = errors.New("file not found") + } + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write(file) + })) + defer testServer.Close() + + tests := []test{ + { + caseDesc: "valid local artifact", + artifact: "../../../tests/test_file.txt", + expectParseSuccess: true, + expectValidateSuccess: true, + }, + { + caseDesc: "valid remote artifact", + artifact: testServer.URL + "/artifact", + expectParseSuccess: true, + expectValidateSuccess: true, + }, + { + caseDesc: "nonexistant local artifact", + artifact: "../../../tests/not_a_file", + expectParseSuccess: false, + expectValidateSuccess: false, + }, + { + caseDesc: "nonexistant remote artifact", + artifact: testServer.URL + "/not_found", + expectParseSuccess: true, + expectValidateSuccess: true, + }, + { + caseDesc: "valid local public key", + publicKey: "../../../tests/test_public_key.key", + expectParseSuccess: true, + expectValidateSuccess: true, + }, + { + caseDesc: "valid local minisign public key", + publicKey: "../../../pkg/pki/minisign/testdata/minisign.pub", + pkiFormat: "minisign", + expectParseSuccess: true, + expectValidateSuccess: true, + }, + { + caseDesc: "valid remote public key", + publicKey: testServer.URL + "/publicKey", + expectParseSuccess: true, + expectValidateSuccess: true, + }, + { + caseDesc: "nonexistant local public key", + publicKey: "../../../tests/not_a_file", + expectParseSuccess: false, + expectValidateSuccess: false, + }, + { + caseDesc: "nonexistant remote public key", + publicKey: testServer.URL + "/not_found", + expectParseSuccess: true, + expectValidateSuccess: true, + }, + { + caseDesc: "valid SHA", + sha: "45c7b11fcbf07dec1694adecd8c5b85770a12a6c8dfdcf2580a2db0c47c31779", + expectParseSuccess: true, + expectValidateSuccess: true, + }, + { + caseDesc: "invalid SHA", + sha: "45c7b11fcbf", + expectParseSuccess: false, + expectValidateSuccess: false, + }, + { + caseDesc: "no flags when either artifact, sha, or public key are needed", + expectParseSuccess: true, + expectValidateSuccess: false, + }, + } + + for _, tc := range tests { + var blankCmd = &cobra.Command{} + if err := addSearchPFlags(blankCmd); err != nil { + t.Fatalf("unexpected error adding flags in '%v': %v", tc.caseDesc, err) + } + + args := []string{} + + if tc.artifact != "" { + args = append(args, "--artifact", tc.artifact) + } + if tc.publicKey != "" { + args = append(args, "--public-key", tc.publicKey) + } + if tc.pkiFormat != "" { + args = append(args, "--pki-format", tc.pkiFormat) + } + if tc.sha != "" { + args = append(args, "--sha", tc.sha) + } + + if err := blankCmd.ParseFlags(args); (err == nil) != tc.expectParseSuccess { + t.Errorf("unexpected result parsing '%v': %v", tc.caseDesc, err) + continue + } + + if err := viper.BindPFlags(blankCmd.Flags()); err != nil { + t.Fatalf("unexpected result initializing viper in '%v': %v", tc.caseDesc, err) + } + if err := validateSearchPFlags(); (err == nil) != tc.expectValidateSuccess { + t.Errorf("unexpected result validating '%v': %v", tc.caseDesc, err) + continue + } + } +} diff --git a/cmd/cli/app/search.go b/cmd/cli/app/search.go new file mode 100644 index 0000000..860dd18 --- /dev/null +++ b/cmd/cli/app/search.go @@ -0,0 +1,178 @@ +/* +Copyright © 2021 Bob Callaway <bcallawa@redhat.com> + +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 app + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/go-openapi/swag" + + "github.com/go-openapi/strfmt" + + "github.com/projectrekor/rekor/cmd/cli/app/format" + "github.com/projectrekor/rekor/pkg/generated/client/index" + "github.com/projectrekor/rekor/pkg/generated/models" + "github.com/projectrekor/rekor/pkg/log" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type searchCmdOutput struct { + uuids []string +} + +func (s *searchCmdOutput) String() string { + str := "No matching entries were found\n" + for i, uuid := range s.uuids { + if i == 0 { + str = "Found matching entries (listed by UUID):\n" + } + str += fmt.Sprintf("%v\n", uuid) + } + return str +} + +// searchCmd represents the get command +var searchCmd = &cobra.Command{ + Use: "search", + Short: "Rekor search command", + Long: `Searches the Rekor index to find entries by artifact or public key`, + PreRun: func(cmd *cobra.Command, args []string) { + // these are bound here so that they are not overwritten by other commands + if err := viper.BindPFlags(cmd.Flags()); err != nil { + log.Logger.Fatal("Error initializing cmd line args: ", err) + } + if err := validateSearchPFlags(); err != nil { + log.Logger.Error(err) + _ = cmd.Help() + os.Exit(1) + } + }, + Run: format.WrapCmd(func(args []string) (interface{}, error) { + log := log.Logger + rekorClient, err := GetRekorClient(viper.GetString("rekor_server")) + if err != nil { + return nil, err + } + + params := index.NewSearchIndexParams() + params.Query = &models.SearchIndex{} + + artifactStr := viper.GetString("artifact") + sha := viper.GetString("sha") + if sha != "" { + params.Query.Hash = sha + } else if artifactStr != "" { + artifact := fileOrURLFlag{} + if err := artifact.Set(artifactStr); err != nil { + return nil, err + } + + hasher := sha256.New() + var tee io.Reader + if artifact.IsURL { + /* #nosec G107 */ + resp, err := http.Get(artifact.String()) + if err != nil { + return nil, fmt.Errorf("error fetching '%v': %w", artifact.String(), err) + } + defer resp.Body.Close() + tee = io.TeeReader(resp.Body, hasher) + } else { + file, err := os.Open(filepath.Clean(artifact.String())) + if err != nil { + return nil, fmt.Errorf("error opening file '%v': %w", artifact.String(), err) + } + defer func() { + if err := file.Close(); err != nil { + log.Error(err) + } + }() + + tee = io.TeeReader(file, hasher) + } + if _, err := ioutil.ReadAll(tee); err != nil { + return nil, fmt.Errorf("error processing '%v': %w", artifact.String(), err) + } + + hashVal := strings.ToLower(hex.EncodeToString(hasher.Sum(nil))) + params.Query.Hash = hashVal + } + + publicKeyStr := viper.GetString("public-key") + if publicKeyStr != "" { + params.Query.PublicKey = &models.SearchIndexPublicKey{} + pkiFormat := viper.GetString("pki-format") + switch pkiFormat { + case "pgp": + params.Query.PublicKey.Format = swag.String(models.SearchIndexPublicKeyFormatPgp) + case "minisign": + params.Query.PublicKey.Format = swag.String(models.SearchIndexPublicKeyFormatMinisign) + case "x509": + params.Query.PublicKey.Format = swag.String(models.SearchIndexPublicKeyFormatX509) + default: + return nil, fmt.Errorf("unknown pki-format %v", pkiFormat) + } + publicKey := fileOrURLFlag{} + if err := publicKey.Set(publicKeyStr); err != nil { + return nil, err + } + if publicKey.IsURL { + params.Query.PublicKey.URL = strfmt.URI(publicKey.String()) + } else { + keyBytes, err := ioutil.ReadFile(filepath.Clean(publicKey.String())) + if err != nil { + return nil, fmt.Errorf("error reading public key file: %w", err) + } + params.Query.PublicKey.Content = strfmt.Base64(keyBytes) + } + } + + resp, err := rekorClient.Index.SearchIndex(params) + if err != nil { + switch t := err.(type) { + case *index.SearchIndexDefault: + if t.Code() == http.StatusNotImplemented { + return nil, fmt.Errorf("search index not enabled on %v", viper.GetString("rekor_server")) + } + return nil, err + default: + return nil, err + } + } + + return &searchCmdOutput{ + uuids: resp.GetPayload(), + }, nil + }), +} + +func init() { + if err := addSearchPFlags(searchCmd); err != nil { + log.Logger.Fatal("Error parsing cmd line args:", err) + } + + rootCmd.AddCommand(searchCmd) +} diff --git a/cmd/server/app/root.go b/cmd/server/app/root.go index 84033ee..dccf9a8 100644 --- a/cmd/server/app/root.go +++ b/cmd/server/app/root.go @@ -63,6 +63,10 @@ func init() { rootCmd.PersistentFlags().String("rekor_server.address", "127.0.0.1", "Address to bind to") rootCmd.PersistentFlags().Uint16("rekor_server.port", 3000, "Port to bind to") + rootCmd.PersistentFlags().Bool("enable_retrieve_api", true, "enables Redis-based index API endpoint") + rootCmd.PersistentFlags().String("redis_server.address", "127.0.0.1", "Redis server address") + rootCmd.PersistentFlags().Uint16("redis_server.port", 6379, "Redis server port") + if err := viper.BindPFlags(rootCmd.PersistentFlags()); err != nil { log.Logger.Fatal(err) } diff --git a/config/rekor.yaml b/config/rekor.yaml index 4c91a83..c340245 100644 --- a/config/rekor.yaml +++ b/config/rekor.yaml @@ -24,6 +24,8 @@ spec: "--trillian_log_server.address=trillian-server", "--trillian_log_server.port=8091", "--rekor_server.address=0.0.0.0", + "--redis_server.address=10.234.175.59", + "--redis_server.port=6379" ] --- apiVersion: v1 diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml index 3f74dbe..575a5d0 100644 --- a/docker-compose.debug.yml +++ b/docker-compose.debug.yml @@ -16,6 +16,8 @@ services: "serve", "--trillian_log_server.address=trillian-log-server", "--trillian_log_server.port=8091", + "--redis_server.address=redis-server", + "--redis_server.port=6379", "--rekor_server.address=0.0.0.0", ] restart: always # keep the server running @@ -24,4 +26,5 @@ services: - "2345:2345" depends_on: - mysql + - redis-server - trillian-log-server diff --git a/docker-compose.yml b/docker-compose.yml index f63957a..2b4c315 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,15 @@ services: - MYSQL_USER=test - MYSQL_PASSWORD=zaphod restart: always # keep the MySQL server running + redis-server: + image: docker.io/redis:5.0.10 + command: [ + "--bind", + "0.0.0.0", + ] + ports: + - "6379:6379" + restart: always # keep the redis server running trillian-log-server: image: gcr.io/trillian-opensource-ci/log_server command: [ @@ -47,6 +56,8 @@ services: "serve", "--trillian_log_server.address=trillian-log-server", "--trillian_log_server.port=8091", + "--redis_server.address=redis-server", + "--redis_server.port=6379", "--rekor_server.address=0.0.0.0", ] restart: always # keep the server running @@ -54,4 +65,5 @@ services: - "3000:3000" depends_on: - mysql + - redis-server - trillian-log-server diff --git a/go.mod b/go.mod index 0cb0091..dff9352 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/go-chi/chi v4.1.2+incompatible github.com/go-openapi/errors v0.19.9 github.com/go-openapi/loads v0.20.0 - github.com/go-openapi/runtime v0.19.24 + github.com/go-openapi/runtime v0.19.26 github.com/go-openapi/spec v0.20.1 github.com/go-openapi/strfmt v0.20.0 github.com/go-openapi/swag v0.19.13 @@ -25,6 +25,7 @@ require ( github.com/jedisct1/go-minisign v0.0.0-20210106175330-e54e81d562c7 github.com/kr/pretty v0.2.1 // indirect github.com/magiconair/properties v1.8.4 // indirect + github.com/mediocregopher/radix/v4 v4.0.0-beta.1 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/mapstructure v1.4.1 github.com/pelletier/go-toml v1.8.1 // indirect diff --git a/go.sum b/go.sum index a70a1ff..f62ae2e 100644 --- a/go.sum +++ b/go.sum @@ -227,6 +227,8 @@ github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiS github.com/go-openapi/runtime v0.19.20/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= github.com/go-openapi/runtime v0.19.24 h1:TqagMVlRAOTwllE/7hNKx6rQ10O6T8ZzeJdMjSTKaD4= github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk= +github.com/go-openapi/runtime v0.19.26 h1:K/6PoVNj5WJXUnMk+VEbELeXjtBkCS1UxTDa04tdXE0= +github.com/go-openapi/runtime v0.19.26/go.mod h1:BvrQtn6iVb2QmiVXRsFAm6ZCAZBpbVKFfN6QWCp582M= github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= @@ -562,6 +564,8 @@ github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mediocregopher/radix/v4 v4.0.0-beta.1 h1:RDgQ4wCQ6f+pUsX20CIzjNJnI5P9KDw4j1sjOVHxo7Y= +github.com/mediocregopher/radix/v4 v4.0.0-beta.1/go.mod h1:Z74pilm773ghbGV4EEoPvi6XWgkAfr0VCNkfa8gI1PU= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -618,6 +622,8 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -752,6 +758,8 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tilinna/clock v1.0.2 h1:6BO2tyAC9JbPExKH/z9zl44FLu1lImh3nDNKA0kgrkI= +github.com/tilinna/clock v1.0.2/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao= github.com/timakin/bodyclose v0.0.0-20190721030226-87058b9bfcec/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= diff --git a/openapi.yaml b/openapi.yaml index a91ffd2..0b2c941 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -7,7 +7,6 @@ info: host: api.rekor.dev schemes: - http - - https consumes: - application/json @@ -17,6 +16,31 @@ produces: - application/yaml paths: + /api/v1/index/retrieve: + post: + summary: Searches index by entry metadata + operationId: searchIndex + tags: + - index + parameters: + - in: body + name: query + required: true + schema: + $ref: '#/definitions/SearchIndex' + responses: + 200: + description: Returns zero or more entry UUIDs from the transparency log based on search query + schema: + type: array + items: + type: string + description: Entry UUID in transparency log + pattern: '^[0-9a-fA-F]{64}$' + 400: + $ref: '#/responses/BadContent' + default: + $ref: '#/responses/InternalServerError' /api/v1/log: get: summary: Get information about the current state of the transparency log @@ -249,6 +273,27 @@ definitions: required: - "body" + SearchIndex: + type: object + properties: + publicKey: + type: object + properties: + format: + type: string + enum: ['pgp','x509','minisign'] + content: + type: string + format: byte + url: + type: string + format: uri + required: + - "format" + hash: + type: string + pattern: '^[0-9a-fA-F]{64}$' + SearchLogQuery: type: object properties: diff --git a/pkg/api/api.go b/pkg/api/api.go index 7e70f51..a8b407b 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -23,6 +23,7 @@ import ( "github.com/google/trillian" "github.com/google/trillian/crypto/keyspb" + radix "github.com/mediocregopher/radix/v4" "github.com/projectrekor/rekor/pkg/log" "github.com/spf13/viper" "google.golang.org/grpc" @@ -82,15 +83,23 @@ func NewAPI() (*API, error) { } var ( - api *API + api *API + redisClient radix.Client ) func ConfigureAPI() { + cfg := radix.PoolConfig{} var err error api, err = NewAPI() if err != nil { log.Logger.Panic(err) } + if viper.GetBool("enable_retrieve_api") { + redisClient, err = cfg.New(context.Background(), "tcp", fmt.Sprintf("%v:%v", viper.GetString("redis_server.address"), viper.GetUint64("redis_server.port"))) + if err != nil { + log.Logger.Panic(err) + } + } } func NewTrillianClient(ctx context.Context) TrillianClient { diff --git a/pkg/api/entries.go b/pkg/api/entries.go index 02cce07..526b023 100644 --- a/pkg/api/entries.go +++ b/pkg/api/entries.go @@ -16,6 +16,7 @@ limitations under the License. package api import ( + "context" "crypto" "crypto/x509" "encoding/hex" @@ -24,6 +25,7 @@ import ( "net/http" "github.com/google/trillian" + "github.com/spf13/viper" "golang.org/x/sync/errgroup" "github.com/go-openapi/swag" @@ -31,6 +33,7 @@ import ( "google.golang.org/genproto/googleapis/rpc/code" "google.golang.org/grpc/codes" + "github.com/projectrekor/rekor/pkg/log" "github.com/projectrekor/rekor/pkg/types" "github.com/projectrekor/rekor/pkg/generated/models" @@ -114,6 +117,16 @@ func CreateLogEntryHandler(params entries.CreateLogEntryParams) middleware.Respo }, } + if viper.GetBool("enable_retrieve_api") { + go func() { + for _, key := range entry.IndexKeys() { + if err := addToIndex(context.Background(), key, uuid); err != nil { + log.RequestIDLogger(params.HTTPRequest).Error(err) + } + } + }() + } + location := strfmt.URI(fmt.Sprintf("%v/%v", httpReq.URL, uuid)) return entries.NewCreateLogEntryCreated().WithPayload(logEntry).WithLocation(location).WithETag(uuid) } diff --git a/pkg/api/error.go b/pkg/api/error.go index aee5c03..de92c06 100644 --- a/pkg/api/error.go +++ b/pkg/api/error.go @@ -24,6 +24,7 @@ import ( "github.com/mitchellh/mapstructure" "github.com/projectrekor/rekor/pkg/generated/models" "github.com/projectrekor/rekor/pkg/generated/restapi/operations/entries" + "github.com/projectrekor/rekor/pkg/generated/restapi/operations/index" "github.com/projectrekor/rekor/pkg/generated/restapi/operations/tlog" "github.com/projectrekor/rekor/pkg/log" ) @@ -35,6 +36,10 @@ const ( entryAlreadyExists = "An equivalent entry already exists in the transparency log" firstSizeLessThanLastSize = "firstSize(%v) must be less than lastSize(%v)" malformedUUID = "UUID must be a 64-character hexadecimal string" + malformedHash = "Hash must be a 64-character hexadecimal string created from SHA256 algorithm" + malformedPublicKey = "Public key provided could not be parsed" + failedToGenerateCanonicalKey = "Error generating canonicalized public key" + redisUnexpectedResult = "Unexpected result from searching index" ) func errorMsg(message string, code int) *models.Error { @@ -118,6 +123,14 @@ func handleRekorAPIError(params interface{}, code int, err error, message string case tlog.GetPublicKeyParams: logMsg(params.HTTPRequest) return tlog.NewGetPublicKeyDefault(code).WithPayload(errorMsg(message, code)) + case index.SearchIndexParams: + logMsg(params.HTTPRequest) + switch code { + case http.StatusBadRequest: + return index.NewSearchIndexBadRequest() + default: + return index.NewSearchIndexDefault(code).WithPayload(errorMsg(message, code)) + } default: log.Logger.Errorf("unable to find method for type %T; error: %v", params, err) return middleware.Error(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) diff --git a/pkg/api/index.go b/pkg/api/index.go new file mode 100644 index 0000000..5ebe8a5 --- /dev/null +++ b/pkg/api/index.go @@ -0,0 +1,98 @@ +/* +Copyright © 2021 Bob Callaway <bcallawa@redhat.com> + +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 ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "net/http" + "strings" + + "github.com/projectrekor/rekor/pkg/pki" + + radix "github.com/mediocregopher/radix/v4" + + "github.com/asaskevich/govalidator" + "github.com/go-openapi/runtime/middleware" + "github.com/go-openapi/swag" + "github.com/projectrekor/rekor/pkg/generated/models" + "github.com/projectrekor/rekor/pkg/generated/restapi/operations/index" + "github.com/projectrekor/rekor/pkg/util" +) + +func SearchIndexHandler(params index.SearchIndexParams) middleware.Responder { + httpReqCtx := params.HTTPRequest.Context() + + var result []string + if params.Query.Hash != "" { + //validate this is only a valid sha256 hash + if !govalidator.IsSHA256(params.Query.Hash) { + return handleRekorAPIError(params, http.StatusBadRequest, errors.New("invalid hash value specified"), malformedHash) + } + var resultUUIDs []string + if err := redisClient.Do(httpReqCtx, radix.Cmd(&resultUUIDs, "LRANGE", strings.ToLower(params.Query.Hash), "0", "-1")); err != nil { + return handleRekorAPIError(params, http.StatusInternalServerError, err, redisUnexpectedResult) + } + result = append(result, resultUUIDs...) + } + if params.Query.PublicKey != nil { + af := pki.NewArtifactFactory(swag.StringValue(params.Query.PublicKey.Format)) + keyReader, err := util.FileOrURLReadCloser(httpReqCtx, params.Query.PublicKey.URL.String(), params.Query.PublicKey.Content, true) + if err != nil { + return handleRekorAPIError(params, http.StatusBadRequest, err, malformedPublicKey) + } + defer keyReader.Close() + + key, err := af.NewPublicKey(keyReader) + if err != nil { + return handleRekorAPIError(params, http.StatusBadRequest, err, malformedPublicKey) + } + canonicalKey, err := key.CanonicalValue() + if err != nil { + return handleRekorAPIError(params, http.StatusInternalServerError, err, failedToGenerateCanonicalKey) + } + + hasher := sha256.New() + if _, err := hasher.Write(canonicalKey); err != nil { + return handleRekorAPIError(params, http.StatusInternalServerError, err, failedToGenerateCanonicalKey) + } + keyHash := hasher.Sum(nil) + var resultUUIDs []string + if err := redisClient.Do(httpReqCtx, radix.Cmd(&resultUUIDs, "LRANGE", strings.ToLower(hex.EncodeToString(keyHash)), "0", "-1")); err != nil { + return handleRekorAPIError(params, http.StatusInternalServerError, err, redisUnexpectedResult) + } + result = append(result, resultUUIDs...) + } + + return index.NewSearchIndexOK().WithPayload(result) +} + +func SearchIndexNotImplementedHandler(params index.SearchIndexParams) middleware.Responder { + err := models.Error{ + Code: http.StatusNotImplemented, + Message: "Search Index API not enabled in this Rekor instance", + } + + return index.NewSearchIndexDefault(http.StatusNotImplemented).WithPayload(&err) + +} + +func addToIndex(ctx context.Context, key, value string) error { + return redisClient.Do(ctx, radix.Cmd(nil, "LPUSH", key, value)) +} diff --git a/pkg/generated/client/entries/entries_client.go b/pkg/generated/client/entries/entries_client.go index 995df90..aa5d14b 100644 --- a/pkg/generated/client/entries/entries_client.go +++ b/pkg/generated/client/entries/entries_client.go @@ -73,7 +73,7 @@ func (a *Client) CreateLogEntry(params *CreateLogEntryParams) (*CreateLogEntryCr PathPattern: "/api/v1/log/entries", ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"}, ConsumesMediaTypes: []string{"application/json", "application/yaml"}, - Schemes: []string{"http", "https"}, + Schemes: []string{"http"}, Params: params, Reader: &CreateLogEntryReader{formats: a.formats}, Context: params.Context, @@ -106,7 +106,7 @@ func (a *Client) GetLogEntryByIndex(params *GetLogEntryByIndexParams) (*GetLogEn PathPattern: "/api/v1/log/entries", ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"}, ConsumesMediaTypes: []string{"application/json", "application/yaml"}, - Schemes: []string{"http", "https"}, + Schemes: []string{"http"}, Params: params, Reader: &GetLogEntryByIndexReader{formats: a.formats}, Context: params.Context, @@ -139,7 +139,7 @@ func (a *Client) GetLogEntryByUUID(params *GetLogEntryByUUIDParams) (*GetLogEntr PathPattern: "/api/v1/log/entries/{entryUUID}", ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"}, ConsumesMediaTypes: []string{"application/json", "application/yaml"}, - Schemes: []string{"http", "https"}, + Schemes: []string{"http"}, Params: params, Reader: &GetLogEntryByUUIDReader{formats: a.formats}, Context: params.Context, @@ -174,7 +174,7 @@ func (a *Client) GetLogEntryProof(params *GetLogEntryProofParams) (*GetLogEntryP PathPattern: "/api/v1/log/entries/{entryUUID}/proof", ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"}, ConsumesMediaTypes: []string{"application/json", "application/yaml"}, - Schemes: []string{"http", "https"}, + Schemes: []string{"http"}, Params: params, Reader: &GetLogEntryProofReader{formats: a.formats}, Context: params.Context, @@ -207,7 +207,7 @@ func (a *Client) SearchLogQuery(params *SearchLogQueryParams) (*SearchLogQueryOK PathPattern: "/api/v1/log/entries/retrieve", ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"}, ConsumesMediaTypes: []string{"application/json", "application/yaml"}, - Schemes: []string{"http", "https"}, + Schemes: []string{"http"}, Params: params, Reader: &SearchLogQueryReader{formats: a.formats}, Context: params.Context, diff --git a/pkg/generated/client/index/index_client.go b/pkg/generated/client/index/index_client.go new file mode 100644 index 0000000..f096243 --- /dev/null +++ b/pkg/generated/client/index/index_client.go @@ -0,0 +1,86 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// /* +// Copyright The Rekor Authors. +// +// 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 index + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" +) + +// New creates a new index API client. +func New(transport runtime.ClientTransport, formats strfmt.Registry) ClientService { + return &Client{transport: transport, formats: formats} +} + +/* +Client for index API +*/ +type Client struct { + transport runtime.ClientTransport + formats strfmt.Registry +} + +// ClientService is the interface for Client methods +type ClientService interface { + SearchIndex(params *SearchIndexParams) (*SearchIndexOK, error) + + SetTransport(transport runtime.ClientTransport) +} + +/* + SearchIndex searches index by entry metadata +*/ +func (a *Client) SearchIndex(params *SearchIndexParams) (*SearchIndexOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewSearchIndexParams() + } + + result, err := a.transport.Submit(&runtime.ClientOperation{ + ID: "searchIndex", + Method: "POST", + PathPattern: "/api/v1/index/retrieve", + ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"}, + ConsumesMediaTypes: []string{"application/json", "application/yaml"}, + Schemes: []string{"http"}, + Params: params, + Reader: &SearchIndexReader{formats: a.formats}, + Context: params.Context, + Client: params.HTTPClient, + }) + if err != nil { + return nil, err + } + success, ok := result.(*SearchIndexOK) + if ok { + return success, nil + } + // unexpected success response + unexpectedSuccess := result.(*SearchIndexDefault) + return nil, runtime.NewAPIError("unexpected success response: content available as default response in error", unexpectedSuccess, unexpectedSuccess.Code()) +} + +// SetTransport changes the transport on the client +func (a *Client) SetTransport(transport runtime.ClientTransport) { + a.transport = transport +} diff --git a/pkg/generated/client/index/search_index_parameters.go b/pkg/generated/client/index/search_index_parameters.go new file mode 100644 index 0000000..ca2e075 --- /dev/null +++ b/pkg/generated/client/index/search_index_parameters.go @@ -0,0 +1,152 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// /* +// Copyright The Rekor Authors. +// +// 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 index + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + "github.com/go-openapi/strfmt" + + "github.com/projectrekor/rekor/pkg/generated/models" +) + +// NewSearchIndexParams creates a new SearchIndexParams object +// with the default values initialized. +func NewSearchIndexParams() *SearchIndexParams { + var () + return &SearchIndexParams{ + + timeout: cr.DefaultTimeout, + } +} + +// NewSearchIndexParamsWithTimeout creates a new SearchIndexParams object +// with the default values initialized, and the ability to set a timeout on a request +func NewSearchIndexParamsWithTimeout(timeout time.Duration) *SearchIndexParams { + var () + return &SearchIndexParams{ + + timeout: timeout, + } +} + +// NewSearchIndexParamsWithContext creates a new SearchIndexParams object +// with the default values initialized, and the ability to set a context for a request +func NewSearchIndexParamsWithContext(ctx context.Context) *SearchIndexParams { + var () + return &SearchIndexParams{ + + Context: ctx, + } +} + +// NewSearchIndexParamsWithHTTPClient creates a new SearchIndexParams object +// with the default values initialized, and the ability to set a custom HTTPClient for a request +func NewSearchIndexParamsWithHTTPClient(client *http.Client) *SearchIndexParams { + var () + return &SearchIndexParams{ + HTTPClient: client, + } +} + +/*SearchIndexParams contains all the parameters to send to the API endpoint +for the search index operation typically these are written to a http.Request +*/ +type SearchIndexParams struct { + + /*Query*/ + Query *models.SearchIndex + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithTimeout adds the timeout to the search index params +func (o *SearchIndexParams) WithTimeout(timeout time.Duration) *SearchIndexParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the search index params +func (o *SearchIndexParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the search index params +func (o *SearchIndexParams) WithContext(ctx context.Context) *SearchIndexParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the search index params +func (o *SearchIndexParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the search index params +func (o *SearchIndexParams) WithHTTPClient(client *http.Client) *SearchIndexParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the search index params +func (o *SearchIndexParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithQuery adds the query to the search index params +func (o *SearchIndexParams) WithQuery(query *models.SearchIndex) *SearchIndexParams { + o.SetQuery(query) + return o +} + +// SetQuery adds the query to the search index params +func (o *SearchIndexParams) SetQuery(query *models.SearchIndex) { + o.Query = query +} + +// WriteToRequest writes these params to a swagger request +func (o *SearchIndexParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + if o.Query != nil { + if err := r.SetBodyParam(o.Query); err != nil { + return err + } + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/generated/client/index/search_index_responses.go b/pkg/generated/client/index/search_index_responses.go new file mode 100644 index 0000000..dc6eed2 --- /dev/null +++ b/pkg/generated/client/index/search_index_responses.go @@ -0,0 +1,171 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// /* +// Copyright The Rekor Authors. +// +// 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 index + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + + "github.com/projectrekor/rekor/pkg/generated/models" +) + +// SearchIndexReader is a Reader for the SearchIndex structure. +type SearchIndexReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *SearchIndexReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + case 200: + result := NewSearchIndexOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + case 400: + result := NewSearchIndexBadRequest() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return nil, result + default: + result := NewSearchIndexDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewSearchIndexOK creates a SearchIndexOK with default headers values +func NewSearchIndexOK() *SearchIndexOK { + return &SearchIndexOK{} +} + +/*SearchIndexOK handles this case with default header values. + +Returns zero or more entry UUIDs from the transparency log based on search query +*/ +type SearchIndexOK struct { + Payload []string +} + +func (o *SearchIndexOK) Error() string { + return fmt.Sprintf("[POST /api/v1/index/retrieve][%d] searchIndexOK %+v", 200, o.Payload) +} + +func (o *SearchIndexOK) GetPayload() []string { + return o.Payload +} + +func (o *SearchIndexOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + // response payload + if err := consumer.Consume(response.Body(), &o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewSearchIndexBadRequest creates a SearchIndexBadRequest with default headers values +func NewSearchIndexBadRequest() *SearchIndexBadRequest { + return &SearchIndexBadRequest{} +} + +/*SearchIndexBadRequest handles this case with default header values. + +The content supplied to the server was invalid +*/ +type SearchIndexBadRequest struct { + Payload *models.Error +} + +func (o *SearchIndexBadRequest) Error() string { + return fmt.Sprintf("[POST /api/v1/index/retrieve][%d] searchIndexBadRequest %+v", 400, o.Payload) +} + +func (o *SearchIndexBadRequest) GetPayload() *models.Error { + return o.Payload +} + +func (o *SearchIndexBadRequest) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.Error) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewSearchIndexDefault creates a SearchIndexDefault with default headers values +func NewSearchIndexDefault(code int) *SearchIndexDefault { + return &SearchIndexDefault{ + _statusCode: code, + } +} + +/*SearchIndexDefault handles this case with default header values. + +There was an internal error in the server while processing the request +*/ +type SearchIndexDefault struct { + _statusCode int + + Payload *models.Error +} + +// Code gets the status code for the search index default response +func (o *SearchIndexDefault) Code() int { + return o._statusCode +} + +func (o *SearchIndexDefault) Error() string { + return fmt.Sprintf("[POST /api/v1/index/retrieve][%d] searchIndex default %+v", o._statusCode, o.Payload) +} + +func (o *SearchIndexDefault) GetPayload() *models.Error { + return o.Payload +} + +func (o *SearchIndexDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.Error) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/pkg/generated/client/rekor_client.go b/pkg/generated/client/rekor_client.go index 458dc49..721331a 100644 --- a/pkg/generated/client/rekor_client.go +++ b/pkg/generated/client/rekor_client.go @@ -28,6 +28,7 @@ import ( "github.com/go-openapi/strfmt" "github.com/projectrekor/rekor/pkg/generated/client/entries" + "github.com/projectrekor/rekor/pkg/generated/client/index" "github.com/projectrekor/rekor/pkg/generated/client/tlog" ) @@ -44,7 +45,7 @@ const ( ) // DefaultSchemes are the default schemes found in Meta (info) section of spec file -var DefaultSchemes = []string{"http", "https"} +var DefaultSchemes = []string{"http"} // NewHTTPClient creates a new rekor HTTP client. func NewHTTPClient(formats strfmt.Registry) *Rekor { @@ -74,6 +75,7 @@ func New(transport runtime.ClientTransport, formats strfmt.Registry) *Rekor { cli := new(Rekor) cli.Transport = transport cli.Entries = entries.New(transport, formats) + cli.Index = index.New(transport, formats) cli.Tlog = tlog.New(transport, formats) return cli } @@ -121,6 +123,8 @@ func (cfg *TransportConfig) WithSchemes(schemes []string) *TransportConfig { type Rekor struct { Entries entries.ClientService + Index index.ClientService + Tlog tlog.ClientService Transport runtime.ClientTransport @@ -130,5 +134,6 @@ type Rekor struct { func (c *Rekor) SetTransport(transport runtime.ClientTransport) { c.Transport = transport c.Entries.SetTransport(transport) + c.Index.SetTransport(transport) c.Tlog.SetTransport(transport) } diff --git a/pkg/generated/client/tlog/tlog_client.go b/pkg/generated/client/tlog/tlog_client.go index f758d72..e9dc37a 100644 --- a/pkg/generated/client/tlog/tlog_client.go +++ b/pkg/generated/client/tlog/tlog_client.go @@ -68,7 +68,7 @@ func (a *Client) GetLogInfo(params *GetLogInfoParams) (*GetLogInfoOK, error) { PathPattern: "/api/v1/log", ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"}, ConsumesMediaTypes: []string{"application/json", "application/yaml"}, - Schemes: []string{"http", "https"}, + Schemes: []string{"http"}, Params: params, Reader: &GetLogInfoReader{formats: a.formats}, Context: params.Context, @@ -103,7 +103,7 @@ func (a *Client) GetLogProof(params *GetLogProofParams) (*GetLogProofOK, error) PathPattern: "/api/v1/log/proof", ProducesMediaTypes: []string{"application/json;q=1", "application/yaml"}, ConsumesMediaTypes: []string{"application/json", "application/yaml"}, - Schemes: []string{"http", "https"}, + Schemes: []string{"http"}, Params: params, Reader: &GetLogProofReader{formats: a.formats}, Context: params.Context, @@ -138,7 +138,7 @@ func (a *Client) GetPublicKey(params *GetPublicKeyParams) (*GetPublicKeyOK, erro PathPattern: "/api/v1/log/publicKey", ProducesMediaTypes: []string{"application/x-pem-file"}, ConsumesMediaTypes: []string{"application/json", "application/yaml"}, - Schemes: []string{"http", "https"}, + Schemes: []string{"http"}, Params: params, Reader: &GetPublicKeyReader{formats: a.formats}, Context: params.Context, diff --git a/pkg/generated/models/search_index.go b/pkg/generated/models/search_index.go new file mode 100644 index 0000000..f0e2519 --- /dev/null +++ b/pkg/generated/models/search_index.go @@ -0,0 +1,226 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// /* +// Copyright The Rekor Authors. +// +// 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 models + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" + + "github.com/go-openapi/errors" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/go-openapi/validate" +) + +// SearchIndex search index +// +// swagger:model SearchIndex +type SearchIndex struct { + + // hash + // Pattern: ^[0-9a-fA-F]{64}$ + Hash string `json:"hash,omitempty"` + + // public key + PublicKey *SearchIndexPublicKey `json:"publicKey,omitempty"` +} + +// Validate validates this search index +func (m *SearchIndex) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateHash(formats); err != nil { + res = append(res, err) + } + + if err := m.validatePublicKey(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +func (m *SearchIndex) validateHash(formats strfmt.Registry) error { + + if swag.IsZero(m.Hash) { // not required + return nil + } + + if err := validate.Pattern("hash", "body", string(m.Hash), `^[0-9a-fA-F]{64}$`); err != nil { + return err + } + + return nil +} + +func (m *SearchIndex) validatePublicKey(formats strfmt.Registry) error { + + if swag.IsZero(m.PublicKey) { // not required + return nil + } + + if m.PublicKey != nil { + if err := m.PublicKey.Validate(formats); err != nil { + if ve, ok := err.(*errors.Validation); ok { + return ve.ValidateName("publicKey") + } + return err + } + } + + return nil +} + +// MarshalBinary interface implementation +func (m *SearchIndex) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *SearchIndex) UnmarshalBinary(b []byte) error { + var res SearchIndex + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} + +// SearchIndexPublicKey search index public key +// +// swagger:model SearchIndexPublicKey +type SearchIndexPublicKey struct { + + // content + // Format: byte + Content strfmt.Base64 `json:"content,omitempty"` + + // format + // Required: true + // Enum: [pgp x509 minisign] + Format *string `json:"format"` + + // url + // Format: uri + URL strfmt.URI `json:"url,omitempty"` +} + +// Validate validates this search index public key +func (m *SearchIndexPublicKey) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateFormat(formats); err != nil { + res = append(res, err) + } + + if err := m.validateURL(formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +var searchIndexPublicKeyTypeFormatPropEnum []interface{} + +func init() { + var res []string + if err := json.Unmarshal([]byte(`["pgp","x509","minisign"]`), &res); err != nil { + panic(err) + } + for _, v := range res { + searchIndexPublicKeyTypeFormatPropEnum = append(searchIndexPublicKeyTypeFormatPropEnum, v) + } +} + +const ( + + // SearchIndexPublicKeyFormatPgp captures enum value "pgp" + SearchIndexPublicKeyFormatPgp string = "pgp" + + // SearchIndexPublicKeyFormatX509 captures enum value "x509" + SearchIndexPublicKeyFormatX509 string = "x509" + + // SearchIndexPublicKeyFormatMinisign captures enum value "minisign" + SearchIndexPublicKeyFormatMinisign string = "minisign" +) + +// prop value enum +func (m *SearchIndexPublicKey) validateFormatEnum(path, location string, value string) error { + if err := validate.EnumCase(path, location, value, searchIndexPublicKeyTypeFormatPropEnum, true); err != nil { + return err + } + return nil +} + +func (m *SearchIndexPublicKey) validateFormat(formats strfmt.Registry) error { + + if err := validate.Required("publicKey"+"."+"format", "body", m.Format); err != nil { + return err + } + + // value enum + if err := m.validateFormatEnum("publicKey"+"."+"format", "body", *m.Format); err != nil { + return err + } + + return nil +} + +func (m *SearchIndexPublicKey) validateURL(formats strfmt.Registry) error { + + if swag.IsZero(m.URL) { // not required + return nil + } + + if err := validate.FormatOf("publicKey"+"."+"url", "body", "uri", m.URL.String(), formats); err != nil { + return err + } + + return nil +} + +// MarshalBinary interface implementation +func (m *SearchIndexPublicKey) MarshalBinary() ([]byte, error) { + if m == nil { + return nil, nil + } + return swag.WriteJSON(m) +} + +// UnmarshalBinary interface implementation +func (m *SearchIndexPublicKey) UnmarshalBinary(b []byte) error { + var res SearchIndexPublicKey + if err := swag.ReadJSON(b, &res); err != nil { + return err + } + *m = res + return nil +} diff --git a/pkg/generated/restapi/configure_rekor_server.go b/pkg/generated/restapi/configure_rekor_server.go index 9ea5647..84fde0e 100644 --- a/pkg/generated/restapi/configure_rekor_server.go +++ b/pkg/generated/restapi/configure_rekor_server.go @@ -26,10 +26,12 @@ import ( "github.com/go-openapi/runtime" "github.com/mitchellh/mapstructure" "github.com/rs/cors" + "github.com/spf13/viper" pkgapi "github.com/projectrekor/rekor/pkg/api" "github.com/projectrekor/rekor/pkg/generated/restapi/operations" "github.com/projectrekor/rekor/pkg/generated/restapi/operations/entries" + "github.com/projectrekor/rekor/pkg/generated/restapi/operations/index" "github.com/projectrekor/rekor/pkg/generated/restapi/operations/tlog" "github.com/projectrekor/rekor/pkg/log" "github.com/projectrekor/rekor/pkg/util" @@ -76,6 +78,12 @@ func configureAPI(api *operations.RekorServerAPI) http.Handler { api.TlogGetLogProofHandler = tlog.GetLogProofHandlerFunc(pkgapi.GetLogProofHandler) api.TlogGetPublicKeyHandler = tlog.GetPublicKeyHandlerFunc(pkgapi.GetPublicKeyHandler) + if viper.GetBool("enable_retrieve_api") { + api.IndexSearchIndexHandler = index.SearchIndexHandlerFunc(pkgapi.SearchIndexHandler) + } else { + api.IndexSearchIndexHandler = index.SearchIndexHandlerFunc(pkgapi.SearchIndexNotImplementedHandler) + } + api.PreServerShutdown = func() {} api.ServerShutdown = func() {} diff --git a/pkg/generated/restapi/doc.go b/pkg/generated/restapi/doc.go index 93a1827..99ed0fd 100644 --- a/pkg/generated/restapi/doc.go +++ b/pkg/generated/restapi/doc.go @@ -21,7 +21,6 @@ // Rekor is a cryptographically secure, immutable transparency log for signed software releases. // Schemes: // http -// https // Host: api.rekor.dev // BasePath: / // Version: 0.0.1 diff --git a/pkg/generated/restapi/embedded_spec.go b/pkg/generated/restapi/embedded_spec.go index 8b669b6..432180c 100644 --- a/pkg/generated/restapi/embedded_spec.go +++ b/pkg/generated/restapi/embedded_spec.go @@ -44,8 +44,7 @@ func init() { "application/yaml" ], "schemes": [ - "http", - "https" + "http" ], "swagger": "2.0", "info": { @@ -55,6 +54,44 @@ func init() { }, "host": "api.rekor.dev", "paths": { + "/api/v1/index/retrieve": { + "post": { + "tags": [ + "index" + ], + "summary": "Searches index by entry metadata", + "operationId": "searchIndex", + "parameters": [ + { + "name": "query", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchIndex" + } + } + ], + "responses": { + "200": { + "description": "Returns zero or more entry UUIDs from the transparency log based on search query", + "schema": { + "type": "array", + "items": { + "description": "Entry UUID in transparency log", + "type": "string", + "pattern": "^[0-9a-fA-F]{64}$" + } + } + }, + "400": { + "$ref": "#/responses/BadContent" + }, + "default": { + "$ref": "#/responses/InternalServerError" + } + } + } + }, "/api/v1/log": { "get": { "description": "Returns the current root hash and size of the merkle tree used to store the log entries.", @@ -468,6 +505,39 @@ func init() { }, "discriminator": "kind" }, + "SearchIndex": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "pattern": "^[0-9a-fA-F]{64}$" + }, + "publicKey": { + "type": "object", + "required": [ + "format" + ], + "properties": { + "content": { + "type": "string", + "format": "byte" + }, + "format": { + "type": "string", + "enum": [ + "pgp", + "x509", + "minisign" + ] + }, + "url": { + "type": "string", + "format": "uri" + } + } + } + } + }, "SearchLogQuery": { "type": "object", "properties": { @@ -556,8 +626,7 @@ func init() { "application/yaml" ], "schemes": [ - "http", - "https" + "http" ], "swagger": "2.0", "info": { @@ -567,6 +636,50 @@ func init() { }, "host": "api.rekor.dev", "paths": { + "/api/v1/index/retrieve": { + "post": { + "tags": [ + "index" + ], + "summary": "Searches index by entry metadata", + "operationId": "searchIndex", + "parameters": [ + { + "name": "query", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/SearchIndex" + } + } + ], + "responses": { + "200": { + "description": "Returns zero or more entry UUIDs from the transparency log based on search query", + "schema": { + "type": "array", + "items": { + "description": "Entry UUID in transparency log", + "type": "string", + "pattern": "^[0-9a-fA-F]{64}$" + } + } + }, + "400": { + "description": "The content supplied to the server was invalid", + "schema": { + "$ref": "#/definitions/Error" + } + }, + "default": { + "description": "There was an internal error in the server while processing the request", + "schema": { + "$ref": "#/definitions/Error" + } + } + } + } + }, "/api/v1/log": { "get": { "description": "Returns the current root hash and size of the merkle tree used to store the log entries.", @@ -1216,6 +1329,63 @@ func init() { } } }, + "SearchIndex": { + "type": "object", + "properties": { + "hash": { + "type": "string", + "pattern": "^[0-9a-fA-F]{64}$" + }, + "publicKey": { + "type": "object", + "required": [ + "format" + ], + "properties": { + "content": { + "type": "string", + "format": "byte" + }, + "format": { + "type": "string", + "enum": [ + "pgp", + "x509", + "minisign" + ] + }, + "url": { + "type": "string", + "format": "uri" + } + } + } + } + }, + "SearchIndexPublicKey": { + "type": "object", + "required": [ + "format" + ], + "properties": { + "content": { + "type": "string", + "format": "byte" + }, + "format": { + "type": "string", + "enum": [ + "pgp", + "x509", + "minisign" + ] + }, + "url": { + "type": "string", + "format": "uri" + } + } + }, "SearchLogQuery": { "type": "object", "properties": { diff --git a/pkg/generated/restapi/operations/index/search_index.go b/pkg/generated/restapi/operations/index/search_index.go new file mode 100644 index 0000000..8a2d7d4 --- /dev/null +++ b/pkg/generated/restapi/operations/index/search_index.go @@ -0,0 +1,75 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// /* +// Copyright The Rekor Authors. +// +// 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 index + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime/middleware" +) + +// SearchIndexHandlerFunc turns a function with the right signature into a search index handler +type SearchIndexHandlerFunc func(SearchIndexParams) middleware.Responder + +// Handle executing the request and returning a response +func (fn SearchIndexHandlerFunc) Handle(params SearchIndexParams) middleware.Responder { + return fn(params) +} + +// SearchIndexHandler interface for that can handle valid search index params +type SearchIndexHandler interface { + Handle(SearchIndexParams) middleware.Responder +} + +// NewSearchIndex creates a new http.Handler for the search index operation +func NewSearchIndex(ctx *middleware.Context, handler SearchIndexHandler) *SearchIndex { + return &SearchIndex{Context: ctx, Handler: handler} +} + +/*SearchIndex swagger:route POST /api/v1/index/retrieve index searchIndex + +Searches index by entry metadata + +*/ +type SearchIndex struct { + Context *middleware.Context + Handler SearchIndexHandler +} + +func (o *SearchIndex) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewSearchIndexParams() + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params) // actually handle the request + + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/pkg/generated/restapi/operations/index/search_index_parameters.go b/pkg/generated/restapi/operations/index/search_index_parameters.go new file mode 100644 index 0000000..a186fde --- /dev/null +++ b/pkg/generated/restapi/operations/index/search_index_parameters.go @@ -0,0 +1,94 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// /* +// Copyright The Rekor Authors. +// +// 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 index + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "io" + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + "github.com/go-openapi/runtime/middleware" + + "github.com/projectrekor/rekor/pkg/generated/models" +) + +// NewSearchIndexParams creates a new SearchIndexParams object +// no default values defined in spec. +func NewSearchIndexParams() SearchIndexParams { + + return SearchIndexParams{} +} + +// SearchIndexParams contains all the bound params for the search index operation +// typically these are obtained from a http.Request +// +// swagger:parameters searchIndex +type SearchIndexParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + In: body + */ + Query *models.SearchIndex +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewSearchIndexParams() beforehand. +func (o *SearchIndexParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + if runtime.HasBody(r) { + defer r.Body.Close() + var body models.SearchIndex + if err := route.Consumer.Consume(r.Body, &body); err != nil { + if err == io.EOF { + res = append(res, errors.Required("query", "body", "")) + } else { + res = append(res, errors.NewParseError("query", "body", "", err)) + } + } else { + // validate body object + if err := body.Validate(route.Formats); err != nil { + res = append(res, err) + } + + if len(res) == 0 { + o.Query = &body + } + } + } else { + res = append(res, errors.Required("query", "body", "")) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/generated/restapi/operations/index/search_index_responses.go b/pkg/generated/restapi/operations/index/search_index_responses.go new file mode 100644 index 0000000..f9224f7 --- /dev/null +++ b/pkg/generated/restapi/operations/index/search_index_responses.go @@ -0,0 +1,180 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// /* +// Copyright The Rekor Authors. +// +// 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 index + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + "github.com/projectrekor/rekor/pkg/generated/models" +) + +// SearchIndexOKCode is the HTTP code returned for type SearchIndexOK +const SearchIndexOKCode int = 200 + +/*SearchIndexOK Returns zero or more entry UUIDs from the transparency log based on search query + +swagger:response searchIndexOK +*/ +type SearchIndexOK struct { + + /* + In: Body + */ + Payload []string `json:"body,omitempty"` +} + +// NewSearchIndexOK creates SearchIndexOK with default headers values +func NewSearchIndexOK() *SearchIndexOK { + + return &SearchIndexOK{} +} + +// WithPayload adds the payload to the search index o k response +func (o *SearchIndexOK) WithPayload(payload []string) *SearchIndexOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the search index o k response +func (o *SearchIndexOK) SetPayload(payload []string) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SearchIndexOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + payload := o.Payload + if payload == nil { + // return empty array + payload = make([]string, 0, 50) + } + + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } +} + +// SearchIndexBadRequestCode is the HTTP code returned for type SearchIndexBadRequest +const SearchIndexBadRequestCode int = 400 + +/*SearchIndexBadRequest The content supplied to the server was invalid + +swagger:response searchIndexBadRequest +*/ +type SearchIndexBadRequest struct { + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewSearchIndexBadRequest creates SearchIndexBadRequest with default headers values +func NewSearchIndexBadRequest() *SearchIndexBadRequest { + + return &SearchIndexBadRequest{} +} + +// WithPayload adds the payload to the search index bad request response +func (o *SearchIndexBadRequest) WithPayload(payload *models.Error) *SearchIndexBadRequest { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the search index bad request response +func (o *SearchIndexBadRequest) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SearchIndexBadRequest) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(400) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +/*SearchIndexDefault There was an internal error in the server while processing the request + +swagger:response searchIndexDefault +*/ +type SearchIndexDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewSearchIndexDefault creates SearchIndexDefault with default headers values +func NewSearchIndexDefault(code int) *SearchIndexDefault { + if code <= 0 { + code = 500 + } + + return &SearchIndexDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the search index default response +func (o *SearchIndexDefault) WithStatusCode(code int) *SearchIndexDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the search index default response +func (o *SearchIndexDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the search index default response +func (o *SearchIndexDefault) WithPayload(payload *models.Error) *SearchIndexDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the search index default response +func (o *SearchIndexDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *SearchIndexDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/pkg/generated/restapi/operations/index/search_index_urlbuilder.go b/pkg/generated/restapi/operations/index/search_index_urlbuilder.go new file mode 100644 index 0000000..419e816 --- /dev/null +++ b/pkg/generated/restapi/operations/index/search_index_urlbuilder.go @@ -0,0 +1,101 @@ +// Code generated by go-swagger; DO NOT EDIT. + +// /* +// Copyright The Rekor Authors. +// +// 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 index + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" +) + +// SearchIndexURL generates an URL for the search index operation +type SearchIndexURL struct { + _basePath string +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SearchIndexURL) WithBasePath(bp string) *SearchIndexURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *SearchIndexURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *SearchIndexURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/api/v1/index/retrieve" + + _basePath := o._basePath + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *SearchIndexURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *SearchIndexURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *SearchIndexURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on SearchIndexURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on SearchIndexURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *SearchIndexURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/generated/restapi/operations/rekor_server_api.go b/pkg/generated/restapi/operations/rekor_server_api.go index a25bc27..8cfc86e 100644 --- a/pkg/generated/restapi/operations/rekor_server_api.go +++ b/pkg/generated/restapi/operations/rekor_server_api.go @@ -39,6 +39,7 @@ import ( "github.com/go-openapi/swag" "github.com/projectrekor/rekor/pkg/generated/restapi/operations/entries" + "github.com/projectrekor/rekor/pkg/generated/restapi/operations/index" "github.com/projectrekor/rekor/pkg/generated/restapi/operations/tlog" ) @@ -90,6 +91,9 @@ func NewRekorServerAPI(spec *loads.Document) *RekorServerAPI { TlogGetPublicKeyHandler: tlog.GetPublicKeyHandlerFunc(func(params tlog.GetPublicKeyParams) middleware.Responder { return middleware.NotImplemented("operation tlog.GetPublicKey has not yet been implemented") }), + IndexSearchIndexHandler: index.SearchIndexHandlerFunc(func(params index.SearchIndexParams) middleware.Responder { + return middleware.NotImplemented("operation index.SearchIndex has not yet been implemented") + }), EntriesSearchLogQueryHandler: entries.SearchLogQueryHandlerFunc(func(params entries.SearchLogQueryParams) middleware.Responder { return middleware.NotImplemented("operation entries.SearchLogQuery has not yet been implemented") }), @@ -150,6 +154,8 @@ type RekorServerAPI struct { TlogGetLogProofHandler tlog.GetLogProofHandler // TlogGetPublicKeyHandler sets the operation handler for the get public key operation TlogGetPublicKeyHandler tlog.GetPublicKeyHandler + // IndexSearchIndexHandler sets the operation handler for the search index operation + IndexSearchIndexHandler index.SearchIndexHandler // EntriesSearchLogQueryHandler sets the operation handler for the search log query operation EntriesSearchLogQueryHandler entries.SearchLogQueryHandler // ServeError is called when an error is received, there is a default handler @@ -258,6 +264,9 @@ func (o *RekorServerAPI) Validate() error { if o.TlogGetPublicKeyHandler == nil { unregistered = append(unregistered, "tlog.GetPublicKeyHandler") } + if o.IndexSearchIndexHandler == nil { + unregistered = append(unregistered, "index.SearchIndexHandler") + } if o.EntriesSearchLogQueryHandler == nil { unregistered = append(unregistered, "entries.SearchLogQueryHandler") } @@ -386,6 +395,10 @@ func (o *RekorServerAPI) initHandlerCache() { if o.handlers["POST"] == nil { o.handlers["POST"] = make(map[string]http.Handler) } + o.handlers["POST"]["/api/v1/index/retrieve"] = index.NewSearchIndex(o.context, o.IndexSearchIndexHandler) + if o.handlers["POST"] == nil { + o.handlers["POST"] = make(map[string]http.Handler) + } o.handlers["POST"]["/api/v1/log/entries/retrieve"] = entries.NewSearchLogQuery(o.context, o.EntriesSearchLogQueryHandler) } diff --git a/pkg/generated/restapi/server.go b/pkg/generated/restapi/server.go index cdfa026..c95a38e 100644 --- a/pkg/generated/restapi/server.go +++ b/pkg/generated/restapi/server.go @@ -56,7 +56,6 @@ var defaultSchemes []string func init() { defaultSchemes = []string{ schemeHTTP, - schemeHTTPS, } } diff --git a/pkg/types/rekord/rekord_test.go b/pkg/types/rekord/rekord_test.go index 729b300..42ff5f5 100644 --- a/pkg/types/rekord/rekord_test.go +++ b/pkg/types/rekord/rekord_test.go @@ -38,6 +38,10 @@ func (u UnmarshalTester) APIVersion() string { return "2.0.1" } +func (u UnmarshalTester) IndexKeys() []string { + return []string{} +} + func (u UnmarshalTester) Canonicalize(ctx context.Context) ([]byte, error) { return nil, nil } diff --git a/pkg/types/rekord/v0.0.1/entry.go b/pkg/types/rekord/v0.0.1/entry.go index 88997ab..3eddc6f 100644 --- a/pkg/types/rekord/v0.0.1/entry.go +++ b/pkg/types/rekord/v0.0.1/entry.go @@ -16,9 +16,6 @@ limitations under the License. package rekord import ( - "bufio" - "bytes" - "compress/gzip" "context" "crypto/sha256" "encoding/base64" @@ -27,12 +24,12 @@ import ( "errors" "fmt" "io" - "io/ioutil" - "net/http" "reflect" + "strings" "github.com/projectrekor/rekor/pkg/log" "github.com/projectrekor/rekor/pkg/types" + "github.com/projectrekor/rekor/pkg/util" "github.com/asaskevich/govalidator" @@ -84,6 +81,35 @@ func Base64StringtoByteArray() mapstructure.DecodeHookFunc { } } +func (v V001Entry) IndexKeys() []string { + var result []string + + if v.HasExternalEntities() { + if err := v.FetchExternalEntities(context.Background()); err != nil { + log.Logger.Error(err) + return result + } + } + + key, err := v.keyObj.CanonicalValue() + if err != nil { + log.Logger.Error(err) + } else { + hasher := sha256.New() + if _, err := hasher.Write(key); err != nil { + log.Logger.Error(err) + } else { + result = append(result, strings.ToLower(hex.EncodeToString(hasher.Sum(nil)))) + } + } + + if v.RekordObj.Data.Hash != nil { + result = append(result, strings.ToLower(swag.StringValue(v.RekordObj.Data.Hash.Value))) + } + + return result +} + func (v *V001Entry) Unmarshal(pe models.ProposedEntry) error { rekord, ok := pe.(*models.Rekord) if !ok { @@ -129,46 +155,6 @@ func (v V001Entry) HasExternalEntities() bool { return false } -// fileOrURLReadCloser Note: caller is responsible for closing ReadCloser returned from method! -func fileOrURLReadCloser(ctx context.Context, url string, content []byte, checkGZIP bool) (io.ReadCloser, error) { - var dataReader io.ReadCloser - if url != "" { - //TODO: set timeout here, SSL settings? - client := &http.Client{} - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) - if err != nil { - return nil, err - } - resp, err := client.Do(req) - if err != nil { - return nil, err - } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return nil, fmt.Errorf("error received while fetching artifact: %v", resp.Status) - } - - if checkGZIP { - // read first 512 bytes to determine if content is gzip compressed - bufReader := bufio.NewReaderSize(resp.Body, 512) - ctBuf, err := bufReader.Peek(512) - if err != nil && err != bufio.ErrBufferFull && err != io.EOF { - return nil, err - } - - if http.DetectContentType(ctBuf) == "application/x-gzip" { - dataReader, _ = gzip.NewReader(io.MultiReader(bufReader, resp.Body)) - } else { - dataReader = ioutil.NopCloser(io.MultiReader(bufReader, resp.Body)) - } - } else { - dataReader = resp.Body - } - } else { - dataReader = ioutil.NopCloser(bytes.NewReader(content)) - } - return dataReader, nil -} - func (v *V001Entry) FetchExternalEntities(ctx context.Context) error { if v.fetchedExternalEntities { return nil @@ -209,7 +195,7 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error { defer hashW.Close() defer sigW.Close() - dataReadCloser, err := fileOrURLReadCloser(ctx, v.RekordObj.Data.URL.String(), v.RekordObj.Data.Content, true) + dataReadCloser, err := util.FileOrURLReadCloser(ctx, v.RekordObj.Data.URL.String(), v.RekordObj.Data.Content, true) if err != nil { return closePipesOnError(err) } @@ -250,7 +236,7 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error { g.Go(func() error { defer close(sigResult) - sigReadCloser, err := fileOrURLReadCloser(ctx, v.RekordObj.Signature.URL.String(), + sigReadCloser, err := util.FileOrURLReadCloser(ctx, v.RekordObj.Signature.URL.String(), v.RekordObj.Signature.Content, false) if err != nil { return closePipesOnError(err) @@ -275,7 +261,7 @@ func (v *V001Entry) FetchExternalEntities(ctx context.Context) error { g.Go(func() error { defer close(keyResult) - keyReadCloser, err := fileOrURLReadCloser(ctx, v.RekordObj.Signature.PublicKey.URL.String(), + keyReadCloser, err := util.FileOrURLReadCloser(ctx, v.RekordObj.Signature.PublicKey.URL.String(), v.RekordObj.Signature.PublicKey.Content, false) if err != nil { return closePipesOnError(err) diff --git a/pkg/types/types.go b/pkg/types/types.go index 465eaf5..0cbf39e 100644 --- a/pkg/types/types.go +++ b/pkg/types/types.go @@ -31,6 +31,7 @@ type TypeImpl interface { type EntryImpl interface { APIVersion() string + IndexKeys() []string Canonicalize(ctx context.Context) ([]byte, error) FetchExternalEntities(ctx context.Context) error HasExternalEntities() bool diff --git a/pkg/util/fetch.go b/pkg/util/fetch.go new file mode 100644 index 0000000..5b9a00f --- /dev/null +++ b/pkg/util/fetch.go @@ -0,0 +1,67 @@ +/* +Copyright © 2021 Bob Callaway <bcallawa@redhat.com> + +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 util + +import ( + "bufio" + "bytes" + "compress/gzip" + "context" + "fmt" + "io" + "io/ioutil" + "net/http" +) + +// FileOrURLReadCloser Note: caller is responsible for closing ReadCloser returned from method! +func FileOrURLReadCloser(ctx context.Context, url string, content []byte, checkGZIP bool) (io.ReadCloser, error) { + var dataReader io.ReadCloser + if url != "" { + //TODO: set timeout here, SSL settings? + client := &http.Client{} + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, err + } + resp, err := client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return nil, fmt.Errorf("error received while fetching artifact: %v", resp.Status) + } + + if checkGZIP { + // read first 512 bytes to determine if content is gzip compressed + bufReader := bufio.NewReaderSize(resp.Body, 512) + ctBuf, err := bufReader.Peek(512) + if err != nil && err != bufio.ErrBufferFull && err != io.EOF { + return nil, err + } + + if http.DetectContentType(ctBuf) == "application/x-gzip" { + dataReader, _ = gzip.NewReader(io.MultiReader(bufReader, resp.Body)) + } else { + dataReader = ioutil.NopCloser(io.MultiReader(bufReader, resp.Body)) + } + } else { + dataReader = resp.Body + } + } else { + dataReader = ioutil.NopCloser(bytes.NewReader(content)) + } + return dataReader, nil +} diff --git a/tests/e2e_test.go b/tests/e2e_test.go index a77befc..de2c330 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -105,6 +105,13 @@ func TestGet(t *testing.T) { t.Error(err) } // TODO: check the actual data in here. + + // check index via the file and public key to ensure that the index has updated correctly + out = runCli(t, "search", "--artifact", artifactPath) + outputContains(t, out, uuid) + + out = runCli(t, "search", "--public-key", pubPath) + outputContains(t, out, uuid) } func TestMinisign(t *testing.T) { @@ -126,13 +133,21 @@ func TestMinisign(t *testing.T) { // Now upload to the log! out = runCli(t, "upload", "--artifact", artifactPath, "--signature", sigPath, - "--public-key", pubPath, "--signature-format", "minisign") + "--public-key", pubPath, "--pki-format", "minisign") outputContains(t, out, "Created entry at") + // Output looks like "Created entry at $URL/UUID", so grab the UUID: + url := strings.Split(strings.TrimSpace(out), " ")[3] + splitUrl := strings.Split(url, "/") + uuid := splitUrl[len(splitUrl)-1] + // Wait and check it. time.Sleep(3 * time.Second) out = runCli(t, "verify", "--artifact", artifactPath, "--signature", sigPath, - "--public-key", pubPath, "--signature-format", "minisign") + "--public-key", pubPath, "--pki-format", "minisign") outputContains(t, out, "Inclusion Proof") + + out = runCli(t, "search", "--public-key", pubPath, "--pki-format", "minisign") + outputContains(t, out, uuid) } -- GitLab