feat: add forward-auth plugin#6037
Conversation
| end | ||
|
|
||
| local params = { | ||
| method = "GET", |
There was a problem hiding this comment.
Should the request method be mapped to the client request method?
There was a problem hiding this comment.
I don't think it's needed unless we provide other special features for this, the implementation in other software is the same.
There was a problem hiding this comment.
In our or other forward-auth plugins, authentication-related data is sent via fixed requests, not POST data, so I think that's enough too.
ping @shuaijinchao
There was a problem hiding this comment.
OK, use GET as a fixed request method you should remove it because the default request method is GET
There was a problem hiding this comment.
OK, use
GETas a fixed request method you should remove it because the default request method isGET
Oh, I get it. Modify will be made later.
| if not res or err then | ||
| core.log.error("failed to process forward auth, err: ", err) | ||
| return 403 | ||
| end |
There was a problem hiding this comment.
| if not res or err then | |
| core.log.error("failed to process forward auth, err: ", err) | |
| return 403 | |
| end | |
| if not res then | |
| core.log.error("failed to process forward auth, err: ", err) | |
| return 403 | |
| end |
Would it be better to judge this way?
There was a problem hiding this comment.
I don't understand how to do it really right, I noticed that in other plugin implementations there are those that use not res alone, those that use err alone and those that use not res or err, which one should we take?
apisix/apisix/plugins/authz-keycloak.lua
Lines 251 to 254 in 58fec8f
apisix/apisix/plugins/serverless/generic-upstream.lua
Lines 110 to 113 in 58fec8f
apisix/apisix/plugins/wolf-rbac.lua
Lines 145 to 147 in 58fec8f
I hope to get your guidance.
cc @spacewander
There was a problem hiding this comment.
I read through each branch of https://github.com/api7/lua-resty-http/blob/b63a9bbf2a3361836b2322e3c689d6d7fd83ec55/lib/resty/http.lua#L900, look like there are only two cases:
nil, errres, nil, while res is a table
There was a problem hiding this comment.
Referring to the way it is usually used, I will check if err is not nil.
if err then
xxx
endThere was a problem hiding this comment.
Also, I think there needs to make a rules that developers should follow to handle error checking (at least on the http client), the mix of different usages is confusing.
There was a problem hiding this comment.
Currently, we have to enforce it with code review. Maybe we can invest in linter in the future.
There was a problem hiding this comment.
I think it should be used
if res then
err
endinstead of
if err then
err
endthis way of writing may be more suitable for languages such as Go
the common exception function return in OpenResty is return nil, err, the first value is enough to know whether to handle err, and you can refer to more code and test cases in lua-resty-core, such as: https://github.com/openresty/lua-resty-core/blob/e5217414669c100b334940b250d5340911a02dd0/t/ctx.t#L133-L143
| end | ||
|
|
||
|
|
||
| function _M.get_uri(ctx) |
There was a problem hiding this comment.
We should not add a common method that is only used by a place. Actually, I think we should remove the get_path too.
It is strange that get_path fetches $uri but get_uri fetches $request_uri. Look like it is a premature optimization that brings inconsistent names.
There was a problem hiding this comment.
So I should just use ctx.var.request_uri instead of wrapping it? Can I merge the functionality of get_path into get_uri and add a with_args parameter indicating whether or not I need to carry queries.
There was a problem hiding this comment.
Don't make thing complex!
The name of request_uri and uri already show the difference. There is no need to wrap them into a function which is rarely used. Premature optimization is the source of evil.
There was a problem hiding this comment.
We can remove the get_path in the next PR. Better to get rid of it before the release, so that people won't ask why we use get_path in one place but use ctx.var.uri in the most elsewhere.
There was a problem hiding this comment.
removed, I will remove get_path in a later PR.
| type = "array", | ||
| default = {}, | ||
| items = {type = "string"}, | ||
| description = "client request header that will be sent to the authorization" |
There was a problem hiding this comment.
| description = "client request header that will be sent to the authorization" | |
| description = "client request header that will be sent to the authorization service" |
| default = {}, | ||
| items = {type = "string"}, | ||
| description = "authorization response header that will be sent to" | ||
| .. "the client when authorize failure" |
There was a problem hiding this comment.
| .. "the client when authorize failure" | |
| .. "the client when authorizing failed" |
| -- append headers that need to be get from the client request header | ||
| if #conf.request_headers > 0 then | ||
| for _, header in ipairs(conf.request_headers) do | ||
| auth_headers[header] = core.request.header(ctx, header) |
There was a problem hiding this comment.
Better not trust the client's X-Forwarded-XX by default
There was a problem hiding this comment.
fixed, because auth_headers has been set in our concern headers, after the judgment, the request header from the client can not override them, that's not trust the client header. L89-L91
apisix/apisix/plugins/forward-auth.lua
Lines 78 to 93 in 8a853d3
| auth_headers[header] = core.request.header(ctx, header) | ||
| end | ||
| else | ||
| auth_headers = core.table.merge(core.request.headers(), auth_headers) |
There was a problem hiding this comment.
Because, merge the list of defined headers into the client request header, any key headers passed in by the client are overwritten, so I implement the untrusted client header.
| } | ||
| ``` | ||
|
|
||
| 3. **client_headers** Send `authorization` service response header to `client` when authorize failure |
There was a problem hiding this comment.
| 3. **client_headers** Send `authorization` service response header to `client` when authorize failure | |
| 3. **client_headers** Send `authorization` service response header to `client` when authorizing failed |
| "plugins": { | ||
| "serverless-pre-function": { | ||
| "phase": "rewrite", | ||
| "functions": ["return function (conf, ctx) local core = require(\"apisix.core\"); local authorization = core.request.header(ctx, \"Authorization\"); if authorization == \"123\" then core.response.exit(200); elseif authorization == \"321\" then core.response.set_header(\"X-User-ID\", \"i-am-an-user\"); core.response.exit(200); else core.response.set_header(\"Location\", \"http://example.com/auth\"); core.response.exit(403); end end"] |
There was a problem hiding this comment.
Let's log down the X-Forwarded-XX received by the service. And better to break down the very long code.
There was a problem hiding this comment.
I used another method to record these heads. I also added a simple echo function under /auth route.
| GET /hello | ||
| --- error_code: 403 | ||
| --- response_headers | ||
| Location: http://example.com/auth |
There was a problem hiding this comment.
Let's add a test that the client passes X-Forwarded-XX by itself
There was a problem hiding this comment.
added, I added a test case to demonstrate that these client-side incoming key headers are indeed dropped in APISIX.
| | -- | -- | -- | -- | -- | -- | | ||
| | host | string | required | | | Authorization service host (eg. https://localhost:9188) | | ||
| | ssl_verify | boolean | optional | true | | Whether to verify the certificate | | ||
| | request_headers | array[string] | optional | | | `client` request header that will be sent to the `authorization` service | |
There was a problem hiding this comment.
Let's doc the behavior when this field is empty
| | host | string | required | | | Authorization service host (eg. https://localhost:9188) | | ||
| | ssl_verify | boolean | optional | true | | Whether to verify the certificate | | ||
| | request_headers | array[string] | optional | | | `client` request header that will be sent to the `authorization` service | | ||
| | upstream_headers | array[string] | optional | | | `authorization` service response header that will be sent to the `upstream` | |
| "serverless-pre-function": { | ||
| "phase": "rewrite", | ||
| "functions": [ | ||
| "return function (conf, ctx) local core = require(\"apisix.core\"); if core.request.header(ctx, \"Authorization\") == \"111\" then core.response.exit(200); end end", |
There was a problem hiding this comment.
Let's indent the code. Like:
diff --git t/plugin/forward-auth.t t/plugin/forward-auth.t
index dca35b76..285e60fb 100644
--- t/plugin/forward-auth.t
+++ t/plugin/forward-auth.t
@@ -74,7 +74,12 @@ property "request_headers" validation failed: wrong type: expected array, got st
"serverless-pre-function": {
"phase": "rewrite",
"functions": [
- "return function (conf, ctx) local core = require(\"apisix.core\"); if core.request.header(ctx, \"Authorization\") == \"111\" then core.response.exit(200); end end",
+ "return function (conf, ctx)
+ local core = require(\"apisix.core\");
+ if core.request.header(ctx, \"Authorization\") == \"111\" then
+ core.response.exit(200)
+ end
+ end",
"return function (conf, ctx) local core = require(\"apisix.core\"); if core.request.header(ctx, \"Authorization\") == \"222\" then core.response.set_header(\"X-User-ID\", \"i-am-an-user\"); core.response.exit(200); end end",
"return function (conf, ctx) local core = require(\"apisix.core\"); if core.request.header(ctx, \"Authorization\") == \"333\" then core.response.set_header(\"Location\", \"http://example.com/auth\"); core.response.exit(403); end end",
"return function (conf, ctx) local core = require(\"apisix.core\"); if core.request.header(ctx, \"Authorization\") == \"444\" then core.response.exit(403, core.request.headers(ctx)); end end"
There was a problem hiding this comment.
Would be better to log down the "X-Forwarded-XX" headers and check it
There was a problem hiding this comment.
Log check for X-Forwarded-XX, I will add it later.
There was a problem hiding this comment.
Splitting the single line long code is done, which uses ]]..[[ truncates the data code, the code is so long that it cannot be parsed, so I modified it by referring to the explanation in this issue.
There was a problem hiding this comment.
Log check forX-Forwarded-XX, I will add it later.
In the session of adding more cases, I found out that we won't actually use it, and that the function to print the request header is already implemented using /echo.
| }, | ||
| request_headers = { | ||
| type = "array", | ||
| default = {}, |
There was a problem hiding this comment.
Look like we can't support forward no headers from the client?
If this field is empty, all headers are forwarded. What about don't provide a default value? (Use empty array if no headers are need)
There was a problem hiding this comment.
I think it's rightfully so, and that's an oversight on my part. I have reversed the meaning of request_headers and client_headers, if the user does not set this parameter, APISIX will send nothing.
| client_headers[header] = res.headers[header] | ||
| end | ||
| else | ||
| client_headers = res.headers |
There was a problem hiding this comment.
IMHO, would be better to use allowlist? If no client_headers, no headers will be returned to the client. Some headers are not expected to be overridden, like Server / Date.
| | -- | -- | -- | -- | -- | -- | | ||
| | host | string | required | | | Authorization service host (eg. https://localhost:9188) | | ||
| | ssl_verify | boolean | optional | true | | Whether to verify the certificate | | ||
| | request_headers | array[string] | optional | | | `client` request header that will be sent to the `authorization` service. When it is not set, all `client` request headers are sent to the `authorization` service, except for those provided by APISIX (X-Forwarded-XXX). | |
| "serverless-pre-function": { | ||
| "phase": "rewrite", | ||
| "functions": [ | ||
| "return function(conf, ctx) |
There was a problem hiding this comment.
Would be better to log down the APISIX generated "X-Forwarded-XX" headers and check it
There was a problem hiding this comment.
I've merged it with the checkcase for overriding user input requests, which will check all APISIX generated headers at once. TEST 6: hit route (check APISIX generated headers and ignore client headers), It is provided by our echo function.
apisix/t/plugin/forward-auth.t
Lines 221 to 225 in beea582
| | -- | -- | -- | -- | -- | -- | | ||
| | host | string | required | | | Authorization service host (eg. https://localhost:9188) | | ||
| | ssl_verify | boolean | optional | true | | Whether to verify the certificate | | ||
| | request_headers | array[string] | optional | | | `client` request header that will be sent to the `authorization` service. When it is not set, no `client` request headers are sent to the `authorization` service, except for those provided by APISIX (X-Forwarded-XXX). | |
There was a problem hiding this comment.
Let's provide a list of the X-Forwarded-XXX headers
There was a problem hiding this comment.
the Data Definition section added
What this PR does / why we need it:
Add the
forward-authplugin.Fix #6007
Fix #5475
Pre-submission checklist: