From bbfc35681df40c635ba742fa9c413c9a5768f572 Mon Sep 17 00:00:00 2001 From: Gerard Snaauw Date: Thu, 13 Jun 2024 18:41:56 +0200 Subject: [PATCH] add separate endpoint for APISIX --- Dockerfile | 2 +- api/opa/api.go | 81 +++++++++++++++++++++++++++++++++++++++++++- api/opa/generated.go | 75 +++++++++++++++++++++++++++++++++++++++- api/pip/generated.go | 2 +- main.go | 2 +- makefile | 2 +- oas/opa.yaml | 29 +++++++++++++++- 7 files changed, 186 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 245d04b..d3532a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # golang alpine -FROM golang:1.22.3-alpine as builder +FROM golang:1.22.4-alpine as builder ARG TARGETARCH ARG TARGETOS diff --git a/api/opa/api.go b/api/opa/api.go index ed8dbdb..e2fa99b 100644 --- a/api/opa/api.go +++ b/api/opa/api.go @@ -2,9 +2,13 @@ package opa import ( "context" + "encoding/base64" + "encoding/json" + "errors" "fmt" - "github.com/nuts-foundation/nuts-pxp/policy" "strings" + + "github.com/nuts-foundation/nuts-pxp/policy" ) var _ StrictServerInterface = (*Wrapper)(nil) @@ -13,6 +17,81 @@ type Wrapper struct { DecisionMaker policy.DecisionMaker } +func (w Wrapper) EvaluateDocumentApisix(ctx context.Context, request EvaluateDocumentApisixRequestObject) (EvaluateDocumentApisixResponseObject, error) { + // APISIX combines the 'openid-connect' and 'opa' plugin results into the following body: + //{ + // "input": { + // "var": { + // "server_port": "9080", + // "remote_addr": "172.90.10.2", + // "timestamp": 1718289289, + // "remote_port": "54228", + // "server_addr": "172.90.10.12" + // }, + // "type": "http", + // "request": { + // "scheme": "http", + // "method": "POST", + // "host": "pep-right", + // "query": {}, + // "path": "/web/external/transfer/notify/21189b43-04d5-4f4f-86ed-e5ae21a87f84", + // "headers": { + // "X-Userinfo": "eyJvcmdhbml6YXRpb25fbmFtZSI6IkxlZnQiLCJzY29wZSI6ImVPdmVyZHJhY2h0LXJlY2VpdmVyIiwic3ViIjoiZGlkOndlYjpub2RlLnJpZ2h0LmxvY2FsOmlhbTpyaWdodCIsImV4cCI6MTcxODI5MDE4NiwiaWF0IjoxNzE4Mjg5Mjg2LCJpc3MiOiJkaWQ6d2ViOm5vZGUucmlnaHQubG9jYWw6aWFtOnJpZ2h0IiwiYWN0aXZlIjp0cnVlLCJjbGllbnRfaWQiOiJkaWQ6d2ViOm5vZGUubGVmdC5sb2NhbDppYW06bGVmdCIsIm9yZ2FuaXphdGlvbl9jaXR5IjoiR3JvZW5sbyJ9", + // "host": "pep-right:9080", + // "authorization": "Bearer TonUNXLwVn2UgJgVfpVDNa7WaXAlE2W-mS6CfqDzeP0", + // "content-length": "0", + // "user-agent": "go-resty/2.13.1 (https://github.com/go-resty/resty)", + // "X-Access-Token": "TonUNXLwVn2UgJgVfpVDNa7WaXAlE2W-mS6CfqDzeP0", + // "accept-encoding": "gzip", + // "content-type": "text/plain; charset=utf-8", + // "connection": "close" + // }, + // "port": 9080 + // } + // } + //} + + input, ok := (*request.Body)["input"].(map[string]interface{}) + if !ok { + return nil, errors.New("invalid request, missing 'input'") + } + httpRequest, ok := input["request"].(map[string]interface{}) + if !ok { + return nil, errors.New("invalid request, missing 'input.request'") + } + httpHeaders, ok := httpRequest["headers"].(map[string]interface{}) + if !ok { + return nil, errors.New("invalid request, missing 'input.request.headers'") + } + xUserinfoBase64, ok := httpHeaders["X-Userinfo"].(string) + if !ok { + return nil, errors.New("invalid request, missing 'input.request.headers.X-Userinfo is not a string'") + } + xUserinfoJSON, err := base64.URLEncoding.DecodeString(xUserinfoBase64) + if err != nil { + return nil, fmt.Errorf("invalid request, failed to base64 decode X-Userinfo: %w", err) + } + xUserinfo := map[string]interface{}{} + err = json.Unmarshal(xUserinfoJSON, &xUserinfo) + if err != nil { + return nil, fmt.Errorf("invalid request, failed to unmarshal X-Userinfo: %w", err) + } + + descision, err := w.DecisionMaker.Query(ctx, httpRequest, xUserinfo) + if err != nil { + return nil, err + } + + // Expected response by APISIX is of the form: + //{ + // "result": { + // "allow": true + // } + //} + result := map[string]interface{}{"allow": descision} + return EvaluateDocumentApisix200JSONResponse{Result: result}, nil +} + func (w Wrapper) EvaluateDocument(ctx context.Context, request EvaluateDocumentRequestObject) (EvaluateDocumentResponseObject, error) { // parse the requestLine and extract the method and path // the requestLine is formatted as an HTTP request line diff --git a/api/opa/generated.go b/api/opa/generated.go index 95d2a54..9c6f17c 100644 --- a/api/opa/generated.go +++ b/api/opa/generated.go @@ -1,6 +1,6 @@ // Package opa provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.1.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. package opa import ( @@ -14,6 +14,11 @@ import ( strictecho "github.com/oapi-codegen/runtime/strictmiddleware/echo" ) +// ApisixOutcome defines model for ApisixOutcome. +type ApisixOutcome struct { + Result map[string]interface{} `json:"result"` +} + // Outcome defines model for Outcome. type Outcome struct { // Allow The result of the OPA policy evaluation @@ -29,11 +34,20 @@ type EvaluateDocumentParams struct { XUserinfo map[string]interface{} `json:"X-Userinfo"` } +// EvaluateDocumentApisixJSONBody defines parameters for EvaluateDocumentApisix. +type EvaluateDocumentApisixJSONBody = map[string]interface{} + +// EvaluateDocumentApisixJSONRequestBody defines body for EvaluateDocumentApisix for application/json ContentType. +type EvaluateDocumentApisixJSONRequestBody = EvaluateDocumentApisixJSONBody + // ServerInterface represents all server handlers. type ServerInterface interface { // calls https://www.openpolicyagent.org/docs/latest/rest-api/#get-a-document-with-input internally // (POST /v1/data) EvaluateDocument(ctx echo.Context, params EvaluateDocumentParams) error + // calls https://www.openpolicyagent.org/docs/latest/rest-api/#get-a-document-with-input internally + // (POST /v1/data/apisix) + EvaluateDocumentApisix(ctx echo.Context) error } // ServerInterfaceWrapper converts echo contexts to parameters. @@ -89,6 +103,15 @@ func (w *ServerInterfaceWrapper) EvaluateDocument(ctx echo.Context) error { return err } +// EvaluateDocumentApisix converts echo context to params. +func (w *ServerInterfaceWrapper) EvaluateDocumentApisix(ctx echo.Context) error { + var err error + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.EvaluateDocumentApisix(ctx) + return err +} + // This is a simple interface which specifies echo.Route addition functions which // are present on both echo.Echo and echo.Group, since we want to allow using // either of them for path registration @@ -118,6 +141,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL } router.POST(baseURL+"/v1/data", wrapper.EvaluateDocument) + router.POST(baseURL+"/v1/data/apisix", wrapper.EvaluateDocumentApisix) } @@ -138,11 +162,31 @@ func (response EvaluateDocument200JSONResponse) VisitEvaluateDocumentResponse(w return json.NewEncoder(w).Encode(response) } +type EvaluateDocumentApisixRequestObject struct { + Body *EvaluateDocumentApisixJSONRequestBody +} + +type EvaluateDocumentApisixResponseObject interface { + VisitEvaluateDocumentApisixResponse(w http.ResponseWriter) error +} + +type EvaluateDocumentApisix200JSONResponse ApisixOutcome + +func (response EvaluateDocumentApisix200JSONResponse) VisitEvaluateDocumentApisixResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + // StrictServerInterface represents all server handlers. type StrictServerInterface interface { // calls https://www.openpolicyagent.org/docs/latest/rest-api/#get-a-document-with-input internally // (POST /v1/data) EvaluateDocument(ctx context.Context, request EvaluateDocumentRequestObject) (EvaluateDocumentResponseObject, error) + // calls https://www.openpolicyagent.org/docs/latest/rest-api/#get-a-document-with-input internally + // (POST /v1/data/apisix) + EvaluateDocumentApisix(ctx context.Context, request EvaluateDocumentApisixRequestObject) (EvaluateDocumentApisixResponseObject, error) } type StrictHandlerFunc = strictecho.StrictEchoHandlerFunc @@ -181,3 +225,32 @@ func (sh *strictHandler) EvaluateDocument(ctx echo.Context, params EvaluateDocum } return nil } + +// EvaluateDocumentApisix operation middleware +func (sh *strictHandler) EvaluateDocumentApisix(ctx echo.Context) error { + var request EvaluateDocumentApisixRequestObject + + var body EvaluateDocumentApisixJSONRequestBody + if err := ctx.Bind(&body); err != nil { + return err + } + request.Body = &body + + handler := func(ctx echo.Context, request interface{}) (interface{}, error) { + return sh.ssi.EvaluateDocumentApisix(ctx.Request().Context(), request.(EvaluateDocumentApisixRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "EvaluateDocumentApisix") + } + + response, err := handler(ctx, request) + + if err != nil { + return err + } else if validResponse, ok := response.(EvaluateDocumentApisixResponseObject); ok { + return validResponse.VisitEvaluateDocumentApisixResponse(ctx.Response()) + } else if response != nil { + return fmt.Errorf("unexpected response type: %T", response) + } + return nil +} diff --git a/api/pip/generated.go b/api/pip/generated.go index 0a83b3d..ad8ca73 100644 --- a/api/pip/generated.go +++ b/api/pip/generated.go @@ -1,6 +1,6 @@ // Package pip provides primitives to interact with the openapi HTTP API. // -// Code generated by github.com/deepmap/oapi-codegen/v2 version v2.1.0 DO NOT EDIT. +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. package pip import ( diff --git a/main.go b/main.go index b4b0737..055a298 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,6 @@ import ( "context" "errors" "fmt" - "github.com/nuts-foundation/nuts-pxp/policy" "net/http" "os" "os/signal" @@ -35,6 +34,7 @@ import ( "github.com/nuts-foundation/nuts-pxp/api/pip" "github.com/nuts-foundation/nuts-pxp/config" "github.com/nuts-foundation/nuts-pxp/db" + "github.com/nuts-foundation/nuts-pxp/policy" ) func main() { diff --git a/makefile b/makefile index 2df9967..13622c2 100644 --- a/makefile +++ b/makefile @@ -3,7 +3,7 @@ run-generators: api install-tools: - go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@v2.1.0 + go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.3.0 go install go.uber.org/mock/mockgen@v0.4.0 go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.2 diff --git a/oas/opa.yaml b/oas/opa.yaml index 0a68b69..0a70731 100644 --- a/oas/opa.yaml +++ b/oas/opa.yaml @@ -38,6 +38,26 @@ paths: application/json: schema: $ref: '#/components/schemas/Outcome' + /v1/data/apisix: + post: + operationId: evaluateDocumentApisix + summary: calls https://www.openpolicyagent.org/docs/latest/rest-api/#get-a-document-with-input internally + description: | + The given request and X-Userinfo headers are used to create the input document for the OPA policy. + tags: + - opa + requestBody: + content: + application/json: + schema: + type: object + responses: + '200': + description: Successful request. Returns the result of the OPA policy evaluation + content: + application/json: + schema: + $ref: '#/components/schemas/ApisixOutcome' components: schemas: Outcome: @@ -48,4 +68,11 @@ components: allow: type: boolean description: The result of the OPA policy evaluation - example: true \ No newline at end of file + example: true + ApisixOutcome: + type: object + required: + - result + properties: + result: + type: object \ No newline at end of file