diff --git a/apisix/plugins/opa.lua b/apisix/plugins/opa.lua index f33c3d042b2a..cfe0b5810d47 100644 --- a/apisix/plugins/opa.lua +++ b/apisix/plugins/opa.lua @@ -18,6 +18,7 @@ local core = require("apisix.core") local http = require("resty.http") local helper = require("apisix.plugins.opa.helper") +local type = type local schema = { type = "object", @@ -89,13 +90,37 @@ function _M.access(conf, ctx) -- parse the results of the decision local data, err = core.json.decode(res.body) - if err then + if err or not data then core.log.error("invalid response body: ", res.body, " err: ", err) return 503 end if not data.result then - return 403 + core.log.error("invalid OPA decision format: ", res.body, + " err: `result` field does not exist") + return 503 + end + + local result = data.result + + if not result.allow then + if result.headers then + core.response.set_header(result.headers) + end + + local status_code = 403 + if result.status_code then + status_code = result.status_code + end + + local reason = nil + if result.reason then + reason = type(result.reason) == "table" + and core.json.encode(result.reason) + or result.reason + end + + return status_code, reason end end diff --git a/apisix/plugins/opa/helper.lua b/apisix/plugins/opa/helper.lua index 2a8cf94316b4..059ea0826203 100644 --- a/apisix/plugins/opa/helper.lua +++ b/apisix/plugins/opa/helper.lua @@ -34,13 +34,13 @@ end local function build_http_request(conf, ctx) return { - scheme = core.request.get_scheme(ctx), - method = core.request.get_method(ctx), - host = core.request.get_host(ctx), - port = core.request.get_port(ctx), - path = core.request.get_path(ctx), - header = core.request.headers(ctx), - query = core.request.get_uri_args(ctx), + scheme = core.request.get_scheme(ctx), + method = core.request.get_method(ctx), + host = core.request.get_host(ctx), + port = core.request.get_port(ctx), + path = core.request.get_path(ctx), + headers = core.request.headers(ctx), + query = core.request.get_uri_args(ctx), } end diff --git a/ci/linux-ci-init-service.sh b/ci/linux-ci-init-service.sh index 765c1155a111..6a7ffbbfb9b2 100755 --- a/ci/linux-ci-init-service.sh +++ b/ci/linux-ci-init-service.sh @@ -34,14 +34,3 @@ docker exec -i rmqnamesrv /home/rocketmq/rocketmq-4.6.0/bin/mqadmin updateTopic # prepare vault kv engine docker exec -i vault sh -c "VAULT_TOKEN='root' VAULT_ADDR='http://0.0.0.0:8200' vault secrets enable -path=kv -version=1 kv" - -# prepare OPA env -curl -XPUT 'http://localhost:8181/v1/policies/example' \ ---header 'Content-Type: text/plain' \ ---data-raw 'package example - -default allow = false - -allow { - input.request.header["test-header"] == "only-for-test" -}' diff --git a/ci/pod/docker-compose.yml b/ci/pod/docker-compose.yml index b632a59c7e7c..b5d8062c2ed2 100644 --- a/ci/pod/docker-compose.yml +++ b/ci/pod/docker-compose.yml @@ -402,7 +402,14 @@ services: restart: unless-stopped ports: - 8181:8181 - command: run -s + command: run -s /example.rego /data.json + volumes: + - type: bind + source: ./ci/pod/opa/example.rego + target: /example.rego + - type: bind + source: ./ci/pod/opa/data.json + target: /data.json networks: opa_net: diff --git a/ci/pod/opa/data.json b/ci/pod/opa/data.json new file mode 100644 index 000000000000..33565594f030 --- /dev/null +++ b/ci/pod/opa/data.json @@ -0,0 +1,25 @@ +{ + "users": { + "alice": { + "headers": { + "Location": "http://example.com/auth" + }, + "status_code": 302 + }, + "bob": { + "headers": { + "test": "abcd", + "abcd": "test" + } + }, + "carla": { + "reason": "Give you a string reason" + }, + "dylon": { + "reason": { + "code": 40001, + "desc": "Give you a object reason" + } + } + } +} diff --git a/ci/pod/opa/example.rego b/ci/pod/opa/example.rego new file mode 100644 index 000000000000..2eb912e08c52 --- /dev/null +++ b/ci/pod/opa/example.rego @@ -0,0 +1,45 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +package example + +import input.request +import data.users + +default allow = false + +allow { + request.headers["test-header"] == "only-for-test" + request.method == "GET" + startswith(request.path, "/hello") + request.query["test"] != "abcd" + request.query["user"] +} + +reason = users[request.query["user"]].reason { + not allow + request.query["user"] +} + +headers = users[request.query["user"]].headers { + not allow + request.query["user"] +} + +status_code = users[request.query["user"]].status_code { + not allow + request.query["user"] +} diff --git a/t/plugin/opa.t b/t/plugin/opa.t index 3592f68c6a50..064661aea2a6 100644 --- a/t/plugin/opa.t +++ b/t/plugin/opa.t @@ -71,7 +71,7 @@ property "host" validation failed: wrong type: expected string, got number "plugins": { "opa": { "host": "http://127.0.0.1:8181", - "policy": "example/allow" + "policy": "example" } }, "upstream": { @@ -80,7 +80,7 @@ property "host" validation failed: wrong type: expected string, got number }, "type": "roundrobin" }, - "uri": "/hello" + "uris": ["/hello", "/test"] }]] ) @@ -95,19 +95,91 @@ passed -=== TEST 3: hit route (with wrong header request) +=== TEST 3: hit route (with correct request) --- request -GET /hello +GET /hello?test=1234&user=none +--- more_headers +test-header: only-for-test +--- response_body +hello world + + + +=== TEST 4: hit route (with wrong header request) +--- request +GET /hello?test=1234&user=none --- more_headers test-header: not-for-test --- error_code: 403 -=== TEST 4: hit route (with correct request) +=== TEST 5: hit route (with wrong query request) --- request -GET /hello +GET /hello?test=abcd&user=none --- more_headers test-header: only-for-test ---- response_body -hello world +--- error_code: 403 + + + +=== TEST 6: hit route (with wrong method request) +--- request +POST /hello?test=1234&user=none +--- more_headers +test-header: only-for-test +--- error_code: 403 + + + +=== TEST 7: hit route (with wrong path request) +--- request +GET /test?test=1234&user=none +--- more_headers +test-header: only-for-test +--- error_code: 403 + + + +=== TEST 8: hit route (response status code and header) +--- request +GET /test?test=abcd&user=alice +--- more_headers +test-header: only-for-test +--- error_code: 302 +--- response_headers +Location: http://example.com/auth + + + +=== TEST 9: hit route (response multiple header reason) +--- request +GET /test?test=abcd&user=bob +--- more_headers +test-header: only-for-test +--- error_code: 403 +--- response_headers +test: abcd +abcd: test + + + +=== TEST 10: hit route (response string reason) +--- request +GET /test?test=abcd&user=carla +--- more_headers +test-header: only-for-test +--- error_code: 403 +--- response +Give you a string reason + + + +=== TEST 11: hit route (response json reason) +--- request +GET /test?test=abcd&user=dylon +--- more_headers +test-header: only-for-test +--- error_code: 403 +--- response +{"code":40001,"desc":"Give you a object reason"}