diff --git a/docs/api.md b/docs/api.md index afc579a..811dc8f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -16,24 +16,25 @@ This document contains the API documentation for both Events and Configuration A 1. [Crate Event Type](#create-event-type) 1. [Update Event Type](#update-event-type) 1. [Delete Event Type](#delete-event-type) - 1. [Get Event Types](#get-event-types) + 1. [List Event Types](#list-event-types) 1. [Get Event Type](#get-event-type) 1. [Functions](#functions) 1. [Register Function](#register-function) 1. [Update Function](#update-function) 1. [Delete Function](#delete-function) - 1. [Get Functions](#get-functions) + 1. [List Functions](#list-functions) 1. [Get Function](#get-function) 1. [Subscriptions](#subscriptions) 1. [Create Subscription](#create-subscription) 1. [Update Subscription](#update-subscription) 1. [Delete Subscription](#delete-subscription) - 1. [Get Subscriptions](#get-subscriptions) + 1. [List Subscriptions](#list-subscriptions) 1. [Get Subscription](#get-subscription) 1. [CORS](#cors-1) 1. [Create CORS Configuration](#create-cors-configuration) 1. [Update CORS Configuration](#update-cors-configuration) 1. [Delete CORS Configuration](#delete-cors-configuration) + 1. [List CORS Configurations](#list-cors-configurations) 1. [Get CORS Configuration](#get-cors-configuration) 1. [Prometheus Metrics](#prometheus-metrics) 1. [Status](#status) @@ -195,7 +196,7 @@ Status code: --- -#### Get Event Types +#### List Event Types **Endpoint** @@ -347,7 +348,7 @@ Status code: --- -#### Get Functions +#### List Functions **Endpoint** @@ -473,7 +474,7 @@ Status code: --- -#### Get Subscriptions +#### List Subscriptions **Endpoint** @@ -611,6 +612,32 @@ Status code: --- +#### List CORS Configurations + +**Endpoint** + +`GET /v1/spaces//cors` + +**Response** + +Status code: + +* `200 OK` on success + +JSON object: + +* `cors` - `array` of `object` - CORS configurations + * `space` - `string` - space name + * `corsId` - `string` - CORS configuration ID + * `method` - `string` - endpoint method + * `path` - `string` - endpoint path + * `allowedOrigins` - `array` of `string` - allowed origins + * `allowedMethods` - `array` of `string` - allowed methods + * `allowedHeaders` - `array` of `string` - allowed headers + * `allowCredentials` - `boolean` - allow credentials + +--- + #### Get CORS Configuration **Endpoint** diff --git a/docs/openapi/openapi-config-api.yaml b/docs/openapi/openapi-config-api.yaml index 59fcad4..0ed43a2 100644 --- a/docs/openapi/openapi-config-api.yaml +++ b/docs/openapi/openapi-config-api.yaml @@ -331,6 +331,22 @@ paths: /spaces/{spaceName}/cors: summary: "Operations about CORS configuration" + get: + summary: "List CORS configurations" + tags: + - "cors" + operationId: "ListCORS" + parameters: + - $ref: "#/components/parameters/Space" + responses: + 200: + description: "CORS configurations returned" + content: + application/json: + schema: + $ref: "#/components/schemas/CORSes" + 500: + $ref: '#/components/responses/Error' post: summary: "Create CORS configuration" tags: @@ -522,6 +538,13 @@ components: $ref: '#/components/schemas/AllowedHeaders' allowCredentials: $ref: '#/components/schemas/AllowCredentials' + CORSes: + type: object + properties: + cors: + type: array + items: + $ref: '#/components/schemas/CORS' AWSFirehose: type: object properties: diff --git a/httpapi/httpapi.go b/httpapi/httpapi.go index f691f1e..f0d211f 100644 --- a/httpapi/httpapi.go +++ b/httpapi/httpapi.go @@ -36,6 +36,11 @@ type SubscriptionsResponse struct { Subscriptions subscription.Subscriptions `json:"subscriptions"` } +// CORSResponse is a HTTPAPI JSON response containing cors configuration. +type CORSResponse struct { + CORSes cors.CORSes `json:"cors"` +} + // RegisterRoutes register HTTP API routes func (h HTTPAPI) RegisterRoutes(router *httprouter.Router) { router.GET("/v1/status", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {}) @@ -59,6 +64,7 @@ func (h HTTPAPI) RegisterRoutes(router *httprouter.Router) { router.PUT("/v1/spaces/:space/subscriptions/:id", h.updateSubscription) router.DELETE("/v1/spaces/:space/subscriptions/:id", h.deleteSubscription) + router.GET("/v1/spaces/:space/cors", h.listCORS) router.GET("/v1/spaces/:space/cors/*id", h.getCORS) router.POST("/v1/spaces/:space/cors", h.createCORS) router.PUT("/v1/spaces/:space/cors/*id", h.updateCORS) @@ -473,6 +479,22 @@ func (h HTTPAPI) deleteSubscription(w http.ResponseWriter, r *http.Request, para metricConfigRequests.WithLabelValues(space, "subscription", "delete").Inc() } +func (h HTTPAPI) listCORS(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + w.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(w) + + space := params.ByName("space") + configs, err := h.CORSes.GetCORSes(space) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + encoder.Encode(&Response{Errors: []Error{{Message: err.Error()}}}) + } else { + encoder.Encode(&CORSResponse{configs}) + } + + metricConfigRequests.WithLabelValues(space, "cors", "list").Inc() +} + func (h HTTPAPI) getCORS(w http.ResponseWriter, r *http.Request, params httprouter.Params) { w.Header().Set("Content-Type", "application/json") encoder := json.NewEncoder(w) diff --git a/httpapi/httpapi_test.go b/httpapi/httpapi_test.go index 004c569..ea63d21 100644 --- a/httpapi/httpapi_test.go +++ b/httpapi/httpapi_test.go @@ -683,6 +683,39 @@ func TestGetCORS(t *testing.T) { }) } +func TestGetCORSes(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, _, _, _, corses := setup(ctrl) + + t.Run("CORS configurations returned", func(t *testing.T) { + returnedList := cors.CORSes{{ + Space: "default", + ID: cors.ID("GET%2Fhello"), + }} + corses.EXPECT().GetCORSes("default").Return(returnedList, nil) + + resp := request(router, http.MethodGet, "/v1/spaces/default/cors", nil) + + configs := &httpapi.CORSResponse{} + json.Unmarshal(resp.Body.Bytes(), configs) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "default", configs.CORSes[0].Space) + assert.Equal(t, cors.ID("GET%2Fhello"), configs.CORSes[0].ID) + }) + + t.Run("internal error", func(t *testing.T) { + corses.EXPECT().GetCORSes(gomock.Any()).Return(nil, errors.New("processing failed")) + + resp := request(router, http.MethodGet, "/v1/spaces/default/cors", nil) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.Equal(t, "processing failed", httpresp.Errors[0].Message) + }) +} + func TestCreateCORS(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/libkv/cors.go b/libkv/cors.go index f0ab723..40e81f9 100644 --- a/libkv/cors.go +++ b/libkv/cors.go @@ -70,6 +70,29 @@ func (service Service) GetCORS(space string, id cors.ID) (*cors.CORS, error) { return &config, nil } +// GetCORSes returns an array of all CORS configuration in the space. +func (service Service) GetCORSes(space string) (cors.CORSes, error) { + configs := []*cors.CORS{} + + kvs, err := service.CORSStore.List(spacePath(space), &store.ReadOptions{Consistent: true}) + if err != nil && err.Error() != errKeyNotFound { + return nil, err + } + + for _, kv := range kvs { + config := &cors.CORS{} + dec := json.NewDecoder(bytes.NewReader(kv.Value)) + err = dec.Decode(config) + if err != nil { + return nil, err + } + + configs = append(configs, config) + } + + return cors.CORSes(configs), nil +} + // UpdateCORS updates CORS configuration. func (service Service) UpdateCORS(config *cors.CORS) (*cors.CORS, error) { if err := validateCORS(config); err != nil { diff --git a/libkv/cors_test.go b/libkv/cors_test.go index 5bb0c27..186605d 100644 --- a/libkv/cors_test.go +++ b/libkv/cors_test.go @@ -150,6 +150,46 @@ func TestGetCORS(t *testing.T) { }) } +func TestGetCORSes(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + testConfig := &cors.CORS{Space: "default", ID: "GET%2Fhello"} + testPayload := []byte(`{"space":"default","corsId":"GET%2Fhello"}}`) + + t.Run("configurations returned", func(t *testing.T) { + kvs := []*store.KVPair{&store.KVPair{Value: testPayload}} + db := mock.NewMockStore(ctrl) + db.EXPECT().List("default/", &store.ReadOptions{Consistent: true}).Return(kvs, nil) + service := &Service{CORSStore: db, Log: zap.NewNop()} + + list, err := service.GetCORSes("default") + + assert.Nil(t, err) + assert.Equal(t, cors.CORSes{testConfig}, list) + }) + + t.Run("KV List error", func(t *testing.T) { + db := mock.NewMockStore(ctrl) + db.EXPECT().List(gomock.Any(), gomock.Any()).Return([]*store.KVPair{}, errors.New("KV list err")) + service := &Service{CORSStore: db, Log: zap.NewNop()} + + _, err := service.GetCORSes("default") + + assert.EqualError(t, err, "KV list err") + }) + + t.Run("KV List directory not found", func(t *testing.T) { + db := mock.NewMockStore(ctrl) + db.EXPECT().List(gomock.Any(), gomock.Any()).Return([]*store.KVPair{}, errors.New("Key not found in store")) + service := &Service{CORSStore: db, Log: zap.NewNop()} + + list, _ := service.GetCORSes("default") + + assert.Equal(t, cors.CORSes{}, list) + }) +} + func TestUpdateCORS(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/mock/cors.go b/mock/cors.go index 5cd5e04..2ab4428 100644 --- a/mock/cors.go +++ b/mock/cors.go @@ -71,6 +71,19 @@ func (mr *MockCORSServiceMockRecorder) GetCORS(arg0, arg1 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCORS", reflect.TypeOf((*MockCORSService)(nil).GetCORS), arg0, arg1) } +// GetCORSes mocks base method +func (m *MockCORSService) GetCORSes(arg0 string) (cors.CORSes, error) { + ret := m.ctrl.Call(m, "GetCORSes", arg0) + ret0, _ := ret[0].(cors.CORSes) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetCORSes indicates an expected call of GetCORSes +func (mr *MockCORSServiceMockRecorder) GetCORSes(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCORSes", reflect.TypeOf((*MockCORSService)(nil).GetCORSes), arg0) +} + // UpdateCORS mocks base method func (m *MockCORSService) UpdateCORS(arg0 *cors.CORS) (*cors.CORS, error) { ret := m.ctrl.Call(m, "UpdateCORS", arg0) diff --git a/subscription/cors/cors.go b/subscription/cors/cors.go index 680e343..ff10567 100644 --- a/subscription/cors/cors.go +++ b/subscription/cors/cors.go @@ -20,6 +20,9 @@ type CORS struct { AllowCredentials bool `json:"allowCredentials"` } +// CORSes is an array of CORS configurations. +type CORSes []*CORS + // MarshalLogObject is a part of zapcore.ObjectMarshaler interface func (c CORS) MarshalLogObject(enc zapcore.ObjectEncoder) error { enc.AddString("space", string(c.Space)) diff --git a/subscription/cors/service.go b/subscription/cors/service.go index 4c60c7c..080ab1c 100644 --- a/subscription/cors/service.go +++ b/subscription/cors/service.go @@ -5,5 +5,6 @@ type Service interface { CreateCORS(c *CORS) (*CORS, error) UpdateCORS(c *CORS) (*CORS, error) GetCORS(space string, id ID) (*CORS, error) + GetCORSes(space string) (CORSes, error) DeleteCORS(space string, id ID) error }