From d054b80ce2f4cf8938a9db20091ba8005ff5ce18 Mon Sep 17 00:00:00 2001 From: Guilherme Cassolato Date: Mon, 4 Oct 2021 19:07:08 +0200 Subject: [PATCH] Add `parameters` and `contentType` options to `spec.metadata.http` `parameters` is a list of static and dynamic values (fetched from the authroization JSON) to be encoded in the body of HTTP request whenever using `POST` method. `contentType` drives the encoding of `parameters` in the body of the request. Accepted values are: `application/x-www-form-urlencoded` (default) and `application/json`. --- api/v1beta1/auth_config_types.go | 11 ++++ api/v1beta1/zz_generated.deepcopy.go | 7 +++ controllers/auth_config_controller.go | 13 ++++ examples/ext-http-metadata.yaml | 63 ++++++++++++++++--- .../crd/authorino.3scale.net_authconfigs.yaml | 31 +++++++++ pkg/config/metadata/generic_http.go | 44 ++++++++++++- pkg/config/metadata/generic_http_test.go | 7 ++- 7 files changed, 163 insertions(+), 13 deletions(-) diff --git a/api/v1beta1/auth_config_types.go b/api/v1beta1/auth_config_types.go index 92432bbd..9c5683da 100644 --- a/api/v1beta1/auth_config_types.go +++ b/api/v1beta1/auth_config_types.go @@ -195,6 +195,9 @@ type Metadata_UMA struct { // +kubebuilder:validation:Enum:=GET;POST type GenericHTTP_Method string +// +kubebuilder:validation:Enum:=application/x-www-form-urlencoded;application/json +type Metadata_GenericHTTP_ContentType string + // Generic HTTP interface to obtain authorization metadata from a HTTP service. type Metadata_GenericHTTP struct { // Endpoint of the HTTP service. @@ -207,6 +210,14 @@ type Metadata_GenericHTTP struct { // When the request method is POST, the authorization JSON is passed in the body of the request. Method GenericHTTP_Method `json:"method,omitempty"` + // Custom parameters to encode in the body of the HTTP request. + // Use it with method=POST; for GET requests, specify parameters using placeholders in the endpoint. + Parameters []JsonProperty `json:"bodyParameters,omitempty"` + + // Content-Type of the request body. + // +kubebuilder:default:=application/x-www-form-urlencoded + ContentType Metadata_GenericHTTP_ContentType `json:"contentType,omitempty"` + // Reference to a Secret key whose value will be passed by Authorino in the request. // The HTTP service can use the shared secret to authenticate the origin of the request. SharedSecret *SecretKeyReference `json:"sharedSecretRef,omitempty"` diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 6cb04eff..772e4ec8 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -523,6 +523,13 @@ func (in *Metadata) DeepCopy() *Metadata { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Metadata_GenericHTTP) DeepCopyInto(out *Metadata_GenericHTTP) { *out = *in + if in.Parameters != nil { + in, out := &in.Parameters, &out.Parameters + *out = make([]JsonProperty, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.SharedSecret != nil { in, out := &in.SharedSecret, &out.SharedSecret *out = new(SecretKeyReference) diff --git a/controllers/auth_config_controller.go b/controllers/auth_config_controller.go index 1fc14587..7795d5b6 100644 --- a/controllers/auth_config_controller.go +++ b/controllers/auth_config_controller.go @@ -219,9 +219,22 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf sharedSecret = string(secret.Data[sharedSecretRef.Key]) } + params := make([]common.JSONProperty, 0, len(genericHttp.Parameters)) + for _, param := range genericHttp.Parameters { + params = append(params, common.JSONProperty{ + Name: param.Name, + Value: common.JSONValue{ + Static: param.Value, + Pattern: param.ValueFrom.AuthJSON, + }, + }) + } + translatedMetadata.GenericHTTP = &authorinoMetadata.GenericHttp{ Endpoint: genericHttp.Endpoint, Method: string(genericHttp.Method), + Parameters: params, + ContentType: string(genericHttp.ContentType), SharedSecret: sharedSecret, AuthCredentials: auth_credentials.NewAuthCredential(creds.KeySelector, string(creds.In)), } diff --git a/examples/ext-http-metadata.yaml b/examples/ext-http-metadata.yaml index b0453d57..a4b963b7 100644 --- a/examples/ext-http-metadata.yaml +++ b/examples/ext-http-metadata.yaml @@ -15,23 +15,68 @@ spec: in: authorization_header keySelector: APIKEY metadata: - - name: echo-api + - name: echo-api-get http: - endpoint: http://talker-api.authorino.svc.cluster.local:3000/authz?requested_path={context.request.http.path} - method: POST + endpoint: http://talker-api.authorino.svc.cluster.local:3000/metadata?encoding=text/plain&original_path={context.request.http.path} + method: GET sharedSecretRef: name: talker-api-protection-secret key: echo-metadata-shared-auth credentials: in: authorization_header keySelector: Bearer - authorization: - - name: validate-echo-response + - name: echo-api-post-form + http: + endpoint: http://talker-api.authorino.svc.cluster.local:3000/metadata?encoding=form-data + method: POST + bodyParameters: + - name: original_path + valueFrom: + authJSON: context.request.http.path + - name: my_str + value: foo + - name: my_num + value: 123 + - name: my_bool + value: true + - name: my_arr + value: ["a", "b", "c"] + - name: my_obj + value: + a_prop: "a value" + - name: echo-api-post-json + http: + endpoint: http://talker-api.authorino.svc.cluster.local:3000/metadata?encoding=json + method: POST + contentType: application/json + bodyParameters: + - name: original_path + valueFrom: + authJSON: context.request.http.path + - name: my_str + value: foo + - name: my_num + value: 123 + - name: my_bool + value: true + - name: my_arr + value: ["a", "b", "c"] + - name: my_obj + value: + a_prop: "a value" + response: + - name: echo-api-metadata json: - rules: - - selector: auth.metadata.echo-api.path - operator: eq - value: /authz + properties: + - name: get-request + valueFrom: + authJSON: auth.metadata.echo-api-get + - name: post-request-form + valueFrom: + authJSON: auth.metadata.echo-api-post-form + - name: post-request-json + valueFrom: + authJSON: auth.metadata.echo-api-post-json --- apiVersion: v1 kind: Secret diff --git a/install/crd/authorino.3scale.net_authconfigs.yaml b/install/crd/authorino.3scale.net_authconfigs.yaml index 591653fa..79c8279c 100644 --- a/install/crd/authorino.3scale.net_authconfigs.yaml +++ b/install/crd/authorino.3scale.net_authconfigs.yaml @@ -537,6 +537,37 @@ spec: description: Generic HTTP interface to obtain authorization metadata from a HTTP service. properties: + bodyParameters: + description: Custom parameters to encode in the body of + the HTTP request. Use it with method=POST; for GET requests, + specify parameters using placeholders in the endpoint. + items: + properties: + name: + description: The name of the claim + type: string + value: + description: Static value of the claim + x-kubernetes-preserve-unknown-fields: true + valueFrom: + description: Dynamic value of the claim + properties: + authJSON: + description: Selector to fill the value of claim + from the authorization JSON + type: string + type: object + required: + - name + type: object + type: array + contentType: + default: application/x-www-form-urlencoded + description: Content-Type of the request body. + enum: + - application/x-www-form-urlencoded + - application/json + type: string credentials: description: Defines where client credentials will be passed in the request to the service. If omitted, it defaults diff --git a/pkg/config/metadata/generic_http.go b/pkg/config/metadata/generic_http.go index 3920a100..8f006ef1 100644 --- a/pkg/config/metadata/generic_http.go +++ b/pkg/config/metadata/generic_http.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" "github.com/kuadrant/authorino/pkg/common" "github.com/kuadrant/authorino/pkg/common/auth_credentials" @@ -15,6 +16,8 @@ import ( type GenericHttp struct { Endpoint string Method string + Parameters []common.JSONProperty + ContentType string SharedSecret string auth_credentials.AuthCredentials } @@ -28,13 +31,20 @@ func (h *GenericHttp) Call(pipeline common.AuthPipeline, ctx context.Context) (i endpoint := common.ReplaceJSONPlaceholders(h.Endpoint, string(authData)) var requestBody io.Reader + var contentType string method := h.Method switch method { case "GET": + contentType = "text/plain" requestBody = nil case "POST": - requestBody = bytes.NewBuffer(authData) + var err error + contentType = h.ContentType + requestBody, err = h.buildRequestBody(string(authData)) + if err != nil { + return nil, err + } default: return nil, fmt.Errorf("unsupported method") } @@ -44,6 +54,8 @@ func (h *GenericHttp) Call(pipeline common.AuthPipeline, ctx context.Context) (i return nil, err } + req.Header.Set("Content-Type", contentType) + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err @@ -59,3 +71,33 @@ func (h *GenericHttp) Call(pipeline common.AuthPipeline, ctx context.Context) (i return claims, nil } + +func (h *GenericHttp) buildRequestBody(authData string) (io.Reader, error) { + data := make(map[string]interface{}) + for _, param := range h.Parameters { + data[param.Name] = param.Value.ResolveFor(authData) + } + + switch h.ContentType { + case "application/x-www-form-urlencoded": + formData := url.Values{} + for key, value := range data { + if valueAsStr, err := common.StringifyJSON(value); err != nil { + return nil, fmt.Errorf("failed to encode http request") + } else { + formData.Set(key, valueAsStr) + } + } + return bytes.NewBufferString(formData.Encode()), nil + + case "application/json": + if dataJSON, err := json.Marshal(data); err != nil { + return nil, err + } else { + return bytes.NewBuffer(dataJSON), nil + } + + default: + return nil, fmt.Errorf("unsupported content-type") + } +} diff --git a/pkg/config/metadata/generic_http_test.go b/pkg/config/metadata/generic_http_test.go index 0d8864db..176c195e 100644 --- a/pkg/config/metadata/generic_http_test.go +++ b/pkg/config/metadata/generic_http_test.go @@ -3,10 +3,10 @@ package metadata import ( "bytes" "context" - "encoding/json" "net/http" "testing" + "github.com/kuadrant/authorino/pkg/common" . "github.com/kuadrant/authorino/pkg/common/auth_credentials/mocks" . "github.com/kuadrant/authorino/pkg/common/mocks" @@ -72,14 +72,15 @@ func TestGenericHttpCallWithPOST(t *testing.T) { pipelineMock.EXPECT().GetDataForAuthorization().Return(dataForAuthorization) sharedCredsMock := NewMockAuthCredentials(ctrl) - identityObjectMockJSON, _ := json.Marshal(dataForAuthorization) - requestBody := bytes.NewBuffer(identityObjectMockJSON) + requestBody := bytes.NewBuffer([]byte("user=mock")) httpRequestMock, _ := http.NewRequest("POST", endpoint, requestBody) sharedCredsMock.EXPECT().BuildRequestWithCredentials(ctx, endpoint, "POST", "secret", requestBody).Return(httpRequestMock, nil) metadata := &GenericHttp{ Endpoint: endpoint, Method: "POST", + Parameters: []common.JSONProperty{{Name: "user", Value: common.JSONValue{Pattern: "auth.identity.user"}}}, + ContentType: "application/x-www-form-urlencoded", SharedSecret: "secret", AuthCredentials: sharedCredsMock, }