From 21daf45f8280f3e2ff5f75e1e9c3045dbb478ce7 Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Fri, 13 Dec 2024 11:37:32 +0700 Subject: [PATCH] evaluate accepted content type --- connector/connector_test.go | 6 +- connector/internal/request_builder.go | 2 +- connector/internal/request_builder_test.go | 60 +++++++++++++++++++ connector/internal/types.go | 1 - connector/internal/utils.go | 18 +++++- connector/internal/utils_test.go | 13 ++++ connector/query.go | 8 +++ .../openapi/testdata/petstore2/expected.json | 32 ++++++++++ .../openapi/testdata/petstore2/schema.json | 17 ++++++ .../openapi/testdata/petstore2/swagger.json | 24 ++++++++ 10 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 connector/internal/utils_test.go diff --git a/connector/connector_test.go b/connector/connector_test.go index e169be2..727ace8 100644 --- a/connector/connector_test.go +++ b/connector/connector_test.go @@ -143,7 +143,7 @@ func TestHTTPConnector_authentication(t *testing.T) { assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{ Details: schema.ExplainResponseDetails{ "url": server.URL + "/pet", - "headers": `{"Api_key":["ran*******(14)"],"Content-Type":["application/json"]}`, + "headers": `{"Accept":["application/json"],"Api_key":["ran*******(14)"],"Content-Type":["application/json"]}`, }, }) }) @@ -201,7 +201,7 @@ func TestHTTPConnector_authentication(t *testing.T) { assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{ Details: schema.ExplainResponseDetails{ "url": server.URL + "/pet", - "headers": `{"Api_key":["ran*******(14)"],"Content-Type":["application/json"]}`, + "headers": `{"Accept":["application/json"],"Api_key":["ran*******(14)"],"Content-Type":["application/json"]}`, "body": `{"name":"pet"}`, }, }) @@ -253,7 +253,7 @@ func TestHTTPConnector_authentication(t *testing.T) { assertHTTPResponse(t, res, http.StatusOK, schema.ExplainResponse{ Details: schema.ExplainResponseDetails{ "url": server.URL + "/pet/findByStatus?status=available", - "headers": `{"Authorization":["Bearer ran*******(19)"],"Content-Type":["application/json"],"X-Custom-Header":["This is a test"]}`, + "headers": `{"Accept":["application/json"],"Authorization":["Bearer ran*******(19)"],"Content-Type":["application/json"],"X-Custom-Header":["This is a test"]}`, }, }) }) diff --git a/connector/internal/request_builder.go b/connector/internal/request_builder.go index f708e7f..d3cf4c7 100644 --- a/connector/internal/request_builder.go +++ b/connector/internal/request_builder.go @@ -57,7 +57,7 @@ func (c *RequestBuilder) Build() (*RetryableRequest, error) { } if rawRequest.Response.ContentType != "" && request.Headers.Get(acceptHeader) == "" { - request.Headers.Set(acceptHeader, rawRequest.Response.ContentType) + request.Headers.Set(acceptHeader, evalAcceptContentType(rawRequest.Response.ContentType)) } if rawRequest.RuntimeSettings != nil { diff --git a/connector/internal/request_builder_test.go b/connector/internal/request_builder_test.go index e298ede..4a24129 100644 --- a/connector/internal/request_builder_test.go +++ b/connector/internal/request_builder_test.go @@ -77,6 +77,57 @@ func TestEvalURLAndHeaderParameters(t *testing.T) { } } +func TestEvalURLAndHeaderParametersOAS2(t *testing.T) { + testCases := []struct { + name string + rawArguments string + expectedURL string + errorMsg string + headers map[string]string + }{ + { + name: "get_subject", + rawArguments: `{ + "identifier": "thesauri/material/AAT.11914" + }`, + expectedURL: "/id/thesauri/material/AAT.11914", + }, + } + + ndcSchema := createMockSchemaOAS2(t) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var info *rest.OperationInfo + for key, f := range ndcSchema.Functions { + if key == tc.name { + info = &f + break + } + } + var arguments map[string]any + assert.NilError(t, json.Unmarshal([]byte(tc.rawArguments), &arguments)) + + builder := RequestBuilder{ + Schema: ndcSchema, + Operation: info, + Arguments: arguments, + } + result, headers, err := builder.evalURLAndHeaderParameters() + if tc.errorMsg != "" { + assert.ErrorContains(t, err, tc.errorMsg) + } else { + assert.NilError(t, err) + decodedValue, err := url.QueryUnescape(result.String()) + assert.NilError(t, err) + assert.Equal(t, tc.expectedURL, decodedValue) + for k, v := range tc.headers { + assert.Equal(t, v, headers.Get(k)) + } + } + }) + } +} + func createMockSchema(t *testing.T) *rest.NDCHttpSchema { var ndcSchema rest.NDCHttpSchema rawSchemaBytes, err := os.ReadFile("../../ndc-http-schema/openapi/testdata/petstore3/expected.json") @@ -85,3 +136,12 @@ func createMockSchema(t *testing.T) *rest.NDCHttpSchema { return &ndcSchema } + +func createMockSchemaOAS2(t *testing.T) *rest.NDCHttpSchema { + var ndcSchema rest.NDCHttpSchema + rawSchemaBytes, err := os.ReadFile("../../ndc-http-schema/openapi/testdata/petstore2/expected.json") + assert.NilError(t, err) + assert.NilError(t, json.Unmarshal(rawSchemaBytes, &ndcSchema)) + + return &ndcSchema +} diff --git a/connector/internal/types.go b/connector/internal/types.go index b079388..3347fce 100644 --- a/connector/internal/types.go +++ b/connector/internal/types.go @@ -31,7 +31,6 @@ type HTTPOptions struct { Servers []string `json:"serverIds" yaml:"serverIds"` Parallel bool `json:"parallel" yaml:"parallel"` - Explain bool `json:"-" yaml:"-"` Distributed bool `json:"-" yaml:"-"` Concurrency uint `json:"-" yaml:"-"` } diff --git a/connector/internal/utils.go b/connector/internal/utils.go index 992045a..9b25818 100644 --- a/connector/internal/utils.go +++ b/connector/internal/utils.go @@ -10,13 +10,29 @@ import ( "go.opentelemetry.io/otel/trace" ) +// IsSensitiveHeader checks if the header name is sensitive. +func IsSensitiveHeader(name string) bool { + return sensitiveHeaderRegex.MatchString(strings.ToLower(name)) +} + +func evalAcceptContentType(contentType string) string { + switch { + case strings.HasPrefix(contentType, "image/"): + return "image/*" + case strings.HasPrefix(contentType, "video/"): + return "video/*" + default: + return contentType + } +} + func setHeaderAttributes(span trace.Span, prefix string, httpHeaders http.Header) { for key, headers := range httpHeaders { if len(headers) == 0 { continue } values := headers - if sensitiveHeaderRegex.MatchString(strings.ToLower(key)) { + if IsSensitiveHeader(key) { values = make([]string, len(headers)) for i, header := range headers { values[i] = utils.MaskString(header) diff --git a/connector/internal/utils_test.go b/connector/internal/utils_test.go new file mode 100644 index 0000000..acbec64 --- /dev/null +++ b/connector/internal/utils_test.go @@ -0,0 +1,13 @@ +package internal + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestEvalAcceptContentType(t *testing.T) { + assert.Equal(t, "image/*", evalAcceptContentType("image/jpeg")) + assert.Equal(t, "video/*", evalAcceptContentType("video/mp4")) + assert.Equal(t, "application/json", evalAcceptContentType("application/json")) +} diff --git a/connector/query.go b/connector/query.go index 10cd6c0..a21a5c3 100644 --- a/connector/query.go +++ b/connector/query.go @@ -8,6 +8,7 @@ import ( "github.com/hasura/ndc-http/connector/internal" "github.com/hasura/ndc-http/ndc-http-schema/configuration" + restUtils "github.com/hasura/ndc-http/ndc-http-schema/utils" "github.com/hasura/ndc-sdk-go/schema" "github.com/hasura/ndc-sdk-go/utils" "go.opentelemetry.io/otel/codes" @@ -175,6 +176,13 @@ func (c *HTTPConnector) serializeExplainResponse(ctx context.Context, requests * } defer cancel() + // mask sensitive forwarded headers if exists + for key := range req.Header { + if internal.IsSensitiveHeader(key) { + req.Header.Set(key, restUtils.MaskString(req.Header.Get(key))) + } + } + c.upstreams.InjectMockRequestSettings(req, requests.Schema.Name, httpRequest.RawRequest.Security) explainResp.Details["url"] = req.URL.String() diff --git a/ndc-http-schema/openapi/testdata/petstore2/expected.json b/ndc-http-schema/openapi/testdata/petstore2/expected.json index 6d8291e..d6bd6c3 100644 --- a/ndc-http-schema/openapi/testdata/petstore2/expected.json +++ b/ndc-http-schema/openapi/testdata/petstore2/expected.json @@ -564,6 +564,38 @@ "type": "named" } }, + "get_subject": { + "request": { + "url": "/id/{identifier}", + "method": "get", + "response": { + "contentType": "application/json" + } + }, + "arguments": { + "identifier": { + "description": "The identifier path of the `subject` you're looking for", + "type": { + "name": "String", + "type": "named" + }, + "http": { + "name": "identifier", + "in": "path", + "schema": { + "type": [ + "string" + ] + } + } + } + }, + "description": "Explore details about a given subject node", + "result_type": { + "name": "JSON", + "type": "named" + } + }, "listOAuth2Clients": { "request": { "url": "/clients", diff --git a/ndc-http-schema/openapi/testdata/petstore2/schema.json b/ndc-http-schema/openapi/testdata/petstore2/schema.json index a660857..68418bc 100644 --- a/ndc-http-schema/openapi/testdata/petstore2/schema.json +++ b/ndc-http-schema/openapi/testdata/petstore2/schema.json @@ -260,6 +260,23 @@ "type": "named" } }, + { + "arguments": { + "identifier": { + "description": "The identifier path of the `subject` you're looking for", + "type": { + "name": "String", + "type": "named" + } + } + }, + "description": "Explore details about a given subject node", + "name": "get_subject", + "result_type": { + "name": "JSON", + "type": "named" + } + }, { "arguments": { "client_name": { diff --git a/ndc-http-schema/openapi/testdata/petstore2/swagger.json b/ndc-http-schema/openapi/testdata/petstore2/swagger.json index 4ae38ad..45027eb 100644 --- a/ndc-http-schema/openapi/testdata/petstore2/swagger.json +++ b/ndc-http-schema/openapi/testdata/petstore2/swagger.json @@ -904,6 +904,30 @@ } ] } + }, + "/id/{identifier}": { + "get": { + "operationId": "get subject", + "parameters": [ + { + "description": "The identifier path of the `subject` you're looking for\n", + "in": "path", + "name": "identifier", + "required": true, + "type": "string" + } + ], + "produces": ["text/html", "application/ld+json", "application/json"], + "responses": { + "200": { + "description": "`subject` found\n" + }, + "404": { + "description": "`subject` not found\n" + } + }, + "summary": "Explore details about a given subject node" + } } }, "securityDefinitions": {