diff --git a/httptransport/gone.go b/httptransport/gone.go new file mode 100644 index 0000000000..7ee6f3e535 --- /dev/null +++ b/httptransport/gone.go @@ -0,0 +1,8 @@ +package httptransport + +import "net/http" + +var gone = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + const msg = `{"code":"gone","message":"endpoint removed"}` + http.Error(w, msg, http.StatusGone) +}) diff --git a/httptransport/keybyidhandler.go b/httptransport/keybyidhandler.go deleted file mode 100644 index 5c74c6a38f..0000000000 --- a/httptransport/keybyidhandler.go +++ /dev/null @@ -1,79 +0,0 @@ -package httptransport - -import ( - "errors" - "net/http" - "path" - - "github.com/google/uuid" - je "github.com/quay/claircore/pkg/jsonerr" - jose "gopkg.in/square/go-jose.v2" - - clairerror "github.com/quay/clair/v4/clair-error" - "github.com/quay/clair/v4/internal/codec" - "github.com/quay/clair/v4/notifier" -) - -// KeyByIDHandler returns a particular key queried by ID in JWK format. -func KeyByIDHandler(keystore notifier.KeyStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - resp := &je.Response{ - Code: "method-not-allowed", - Message: "endpoint only allows GET", - } - je.Error(w, resp, http.StatusMethodNotAllowed) - return - } - keyParam := path.Base(r.URL.Path) - if keyParam == "" { - resp := &je.Response{ - Code: "bad-request", - Message: "malformed path. must provide a key id", - } - je.Error(w, resp, http.StatusBadRequest) - return - } - keyID, err := uuid.Parse(keyParam) - if err != nil { - resp := &je.Response{ - Code: "bad-request", - Message: "malformed path. could not parse into uuid: " + err.Error(), - } - je.Error(w, resp, http.StatusBadRequest) - return - } - - ctx := r.Context() - k, err := keystore.KeyByID(ctx, keyID) - switch { - case errors.As(err, &clairerror.ErrKeyNotFound{}): - resp := &je.Response{ - Code: "not-found", - Message: "the key id " + keyID.String() + " does not exist", - } - je.Error(w, resp, http.StatusNotFound) - return - case err == nil: - // hop out - default: - resp := &je.Response{ - Code: "internal-server-error", - Message: err.Error(), - } - je.Error(w, resp, http.StatusInternalServerError) - return - - } - - jwk := jose.JSONWebKey{ - Key: k.Public, - KeyID: k.ID.String(), - Use: "sig", - } - defer writerError(w, &err)() - enc := codec.GetEncoder(w) - defer codec.PutEncoder(enc) - err = enc.Encode(&jwk) - } -} diff --git a/httptransport/keysbyidhandler_test.go b/httptransport/keysbyidhandler_test.go deleted file mode 100644 index 6922fce292..0000000000 --- a/httptransport/keysbyidhandler_test.go +++ /dev/null @@ -1,151 +0,0 @@ -package httptransport - -import ( - "context" - "crypto/rsa" - "encoding/json" - "net/http" - "net/http/httptest" - "path" - "testing" - "time" - - "github.com/google/uuid" - clairerror "github.com/quay/clair/v4/clair-error" - "github.com/quay/clair/v4/notifier" - jose "gopkg.in/square/go-jose.v2" -) - -func TestKeyByIDHandler(t *testing.T) { - t.Run("KeyByID", testKeyByID) - t.Run("Methods", testKeyByIDMethods) - t.Run("BadPathParam", testKeyByIDBadPathParam) - t.Run("KeyNotFound", testKeyByIDNotFound) -} - -func testKeyByIDNotFound(t *testing.T) { - t.Parallel() - id := uuid.New() - mock := ¬ifier.MockKeyStore{ - KeyByID_: func(ctx context.Context, ID uuid.UUID) (notifier.Key, error) { - return notifier.Key{}, clairerror.ErrKeyNotFound{id} - }, - } - - h := KeyByIDHandler(mock) - rr := httptest.NewRecorder() - path := path.Join("/", id.String()) - req, err := http.NewRequest(http.MethodGet, path, nil) - if err != nil { - t.Fatalf("failed to create request: %v", err) - } - h.ServeHTTP(rr, req) - - if rr.Code != http.StatusNotFound { - t.Fatalf("got: %v want: %v", rr.Code, http.StatusNotFound) - } -} - -func testKeyByIDBadPathParam(t *testing.T) { - h := KeyByIDHandler(¬ifier.MockKeyStore{}) - - rr := httptest.NewRecorder() - req, err := http.NewRequest(http.MethodGet, "/bad-uuid", nil) - if err != nil { - t.Fatalf("failed to create request: %v", err) - } - h.ServeHTTP(rr, req) - - if rr.Code != http.StatusBadRequest { - t.Errorf("want %v got %v", rr.Code, http.StatusBadRequest) - } - - rr = httptest.NewRecorder() - req, err = http.NewRequest(http.MethodGet, "/", nil) - if err != nil { - t.Fatalf("failed to create request: %v", err) - } - h.ServeHTTP(rr, req) - - if rr.Code != http.StatusBadRequest { - t.Errorf("want %v got %v", rr.Code, http.StatusBadRequest) - } -} - -func testKeyByID(t *testing.T) { - t.Parallel() - kp := (genKeyPair(t, 1))[0] - exp := time.Now().Add(10 * time.Minute) - key := notifier.Key{kp.ID, exp, kp.Public} - - mock := ¬ifier.MockKeyStore{ - KeyByID_: func(ctx context.Context, ID uuid.UUID) (notifier.Key, error) { - if ID != kp.ID { - t.Fatalf("got %v want %v", ID, kp.ID) - } - return key, nil - }, - } - - h := KeyByIDHandler(mock) - rr := httptest.NewRecorder() - path := path.Join("/", kp.ID.String()) - req, err := http.NewRequest(http.MethodGet, path, nil) - if err != nil { - t.Fatalf("failed to create request: %v", err) - } - h.ServeHTTP(rr, req) - - if rr.Code != http.StatusOK { - t.Fatalf("request failed: %v", rr.Code) - } - - var jwk jose.JSONWebKey - if err := json.NewDecoder(rr.Body).Decode(&jwk); err != nil { - t.Fatalf("failed to deserialize response: %v", err) - } - - pub, ok := jwk.Key.(*rsa.PublicKey) - if !ok { - t.Errorf("failed to type assert key %v", kp.ID) - } - if pub.N.Cmp(kp.Public.N) != 0 { - t.Errorf("got: %v want %v", pub.N, kp.Public.N) - } - if pub.E != kp.Public.E { - t.Errorf("got: %v want %v", pub.E, kp.Public.E) - } - -} - -// testKeyByIDMethods confirms the handler only responds -// to the desired methods. -func testKeyByIDMethods(t *testing.T) { - t.Parallel() - h := KeyByIDHandler(¬ifier.MockKeyStore{}) - srv := httptest.NewServer(h) - defer srv.Close() - c := srv.Client() - - for _, m := range []string{ - http.MethodConnect, - http.MethodHead, - http.MethodOptions, - http.MethodPatch, - http.MethodPost, - http.MethodPut, - http.MethodTrace, - } { - req, err := http.NewRequest(m, srv.URL, nil) - if err != nil { - t.Fatalf("failed to create request: %v", err) - } - resp, err := c.Do(req) - if err != nil { - t.Fatalf("failed to make request: %v", err) - } - if resp.StatusCode != http.StatusMethodNotAllowed { - t.Fatalf("method: %v got: %v want: %v", m, resp.Status, http.StatusMethodNotAllowed) - } - } -} diff --git a/httptransport/keyshandler.go b/httptransport/keyshandler.go deleted file mode 100644 index a1a414e7c8..0000000000 --- a/httptransport/keyshandler.go +++ /dev/null @@ -1,61 +0,0 @@ -package httptransport - -import ( - "net/http" - - je "github.com/quay/claircore/pkg/jsonerr" - jose "gopkg.in/square/go-jose.v2" - - "github.com/quay/clair/v4/internal/codec" - "github.com/quay/clair/v4/notifier" -) - -// KeysHandler returns all keys persisted in the keystore in JWK set format. -func KeysHandler(keystore notifier.KeyStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - resp := &je.Response{ - Code: "method-not-allowed", - Message: "endpoint only allows GET", - } - je.Error(w, resp, http.StatusMethodNotAllowed) - return - } - - ctx := r.Context() - keys, err := keystore.Keys(ctx) - if err != nil { - resp := &je.Response{ - Code: "internal-server-error", - Message: err.Error(), - } - je.Error(w, resp, http.StatusInternalServerError) - return - } - - set := jose.JSONWebKeySet{ - Keys: make([]jose.JSONWebKey, 0, len(keys)), - } - for _, k := range keys { - if err := ctx.Err(); err != nil { - resp := &je.Response{ - Code: "internal-server-error", - Message: "internal server errror", - } - je.Error(w, resp, http.StatusInternalServerError) - return - } - jwk := jose.JSONWebKey{ - Key: k.Public, - KeyID: k.ID.String(), - Use: "sig", - } - set.Keys = append(set.Keys, jwk) - } - - defer writerError(w, &err)() - enc := codec.GetEncoder(w) - defer codec.PutEncoder(enc) - err = enc.Encode(&set) - } -} diff --git a/httptransport/keyshandler_test.go b/httptransport/keyshandler_test.go deleted file mode 100644 index db459b4e86..0000000000 --- a/httptransport/keyshandler_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package httptransport - -import ( - "context" - "crypto/rand" - "crypto/rsa" - "crypto/x509" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/google/uuid" - "github.com/quay/clair/v4/notifier" - "github.com/quay/clair/v4/notifier/keymanager" - jose "gopkg.in/square/go-jose.v2" -) - -func TestKeysHandler(t *testing.T) { - t.Run("Keys", testKeys) - t.Run("Methods", testKeyByIDHandlerMethods) -} - -func testKeys(t *testing.T) { - t.Parallel() - kps := genKeyPair(t, 4) - exp := time.Now().Add(10 * time.Minute) - keys := []notifier.Key{} - for _, kp := range kps { - keys = append(keys, notifier.Key{ - kp.ID, exp, kp.Public, - }) - } - - mock := ¬ifier.MockKeyStore{ - Keys_: func(ctx context.Context) ([]notifier.Key, error) { - return keys, nil - }, - } - - h := KeysHandler(mock) - rr := httptest.NewRecorder() - req, err := http.NewRequest(http.MethodGet, "/", nil) - if err != nil { - t.Fatalf("failed to create request: %v", err) - } - h.ServeHTTP(rr, req) - - if rr.Code != http.StatusOK { - t.Fatalf("request failed: %v", rr.Code) - } - - var set jose.JSONWebKeySet - if err := json.NewDecoder(rr.Body).Decode(&set); err != nil { - t.Fatalf("failed to deserialize response: %v", err) - } - - for _, kp := range kps { - jwks := set.Key(kp.ID.String()) - if len(jwks) != 1 { - t.Errorf("got: %d want %d", len(jwks), 1) - } - pub, ok := jwks[0].Key.(*rsa.PublicKey) - if !ok { - t.Errorf("failed to type assert key %v", kp.ID) - } - if pub.N.Cmp(kp.Public.N) != 0 { - t.Errorf("got: %v want %v", pub.N, kp.Public.N) - } - if pub.E != kp.Public.E { - t.Errorf("got: %v want %v", pub.E, kp.Public.E) - } - } -} - -func genKeyPair(t *testing.T, n int) (kps []keymanager.KeyPair) { - reader := rand.Reader - bitSize := 512 // low bitsize for test speed - for i := 0; i < n; i++ { - key, err := rsa.GenerateKey(reader, bitSize) - if err != nil { - t.Fatalf("failed to generate test key pair: %v", err) - } - - pub, err := x509.MarshalPKIXPublicKey(&key.PublicKey) - if err != nil { - t.Fatalf("failed to marshal public key to PKIX") - } - id := uuid.New() - - kps = append(kps, keymanager.KeyPair{ - ID: id, - Private: key, - Public: &key.PublicKey, - Der: pub, - }) - } - return kps -} - -// testKeysHandlerMethods confirms the handler only responds -// to the desired methods. -func testKeyByIDHandlerMethods(t *testing.T) { - t.Parallel() - h := KeysHandler(¬ifier.MockKeyStore{}) - srv := httptest.NewServer(h) - defer srv.Close() - c := srv.Client() - - for _, m := range []string{ - http.MethodConnect, - http.MethodHead, - http.MethodOptions, - http.MethodPatch, - http.MethodPost, - http.MethodPut, - http.MethodTrace, - } { - req, err := http.NewRequest(m, srv.URL, nil) - if err != nil { - t.Fatalf("failed to create request: %v", err) - } - resp, err := c.Do(req) - if err != nil { - t.Fatalf("failed to make request: %v", err) - } - if resp.StatusCode != http.StatusMethodNotAllowed { - t.Fatalf("method: %v got: %v want: %v", m, resp.Status, http.StatusMethodNotAllowed) - } - } -} diff --git a/httptransport/server.go b/httptransport/server.go index 0c6cd3bda3..a5b05ed910 100644 --- a/httptransport/server.go +++ b/httptransport/server.go @@ -212,16 +212,11 @@ func (t *Server) configureNotifierMode(ctx context.Context) error { t.Handle(NotificationAPIPath, intromw.InstrumentedHandler(NotificationAPIPath, t.traceOpt, NotificationHandler(t.notifier))) - ks := t.notifier.KeyStore(ctx) - if ks == nil { - return clairerror.ErrNotInitialized{Msg: "NotifierMode requires the notifier to provide a non-nil key store"} - } - t.Handle(KeysAPIPath, - intromw.InstrumentedHandler(KeysAPIPath, t.traceOpt, KeysHandler(ks))) + intromw.InstrumentedHandler(KeysAPIPath, t.traceOpt, gone)) t.Handle(KeyByIDAPIPath, - intromw.InstrumentedHandler(KeyByIDAPIPath+"_KEY", t.traceOpt, KeyByIDHandler(ks))) + intromw.InstrumentedHandler(KeyByIDAPIPath+"_KEY", t.traceOpt, gone)) return nil }