-
Notifications
You must be signed in to change notification settings - Fork 2.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add forward-auth plugin #6037
Conversation
apisix/plugins/forward-auth.lua
Outdated
end | ||
|
||
local params = { | ||
method = "GET", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the request method be mapped to the client request method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, use
GET
as a fixed request method you should remove it because the default request method isGET
Oh, I get it. Modify will be made later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed
apisix/plugins/forward-auth.lua
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
local res, error = httpc:request_uri(conf.discovery, params) | |
if not res then | |
err = "Accessing discovery URL (" .. conf.discovery .. ") failed: " .. error |
apisix/apisix/plugins/serverless/generic-upstream.lua
Lines 110 to 113 in 58fec8f
local res, err = httpc:request_uri(conf.function_uri, params) | |
if not res or err then | |
core.log.error("failed to process ", plugin_name, ", err: ", err) |
apisix/apisix/plugins/wolf-rbac.lua
Lines 145 to 147 in 58fec8f
local res, err = httpc:request_uri(uri, params) | |
if err then | |
core.log.error("FAIL REQUEST [ ",core.json.delay_encode( |
I hope to get your guidance.
cc @spacewander
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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, err
res, nil
, while res is a table
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Referring to the way it is usually used, I will check if err
is not nil
.
if err then
xxx
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be used
if res then
err
end
instead of
if err then
err
end
this 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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
apisix/core/request.lua
Outdated
@@ -278,6 +278,15 @@ function _M.get_path(ctx) | |||
end | |||
|
|||
|
|||
function _M.get_uri(ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed, I will remove get_path
in a later PR.
apisix/plugins/forward-auth.lua
Outdated
type = "array", | ||
default = {}, | ||
items = {type = "string"}, | ||
description = "client request header that will be sent to the authorization" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description = "client request header that will be sent to the authorization" | |
description = "client request header that will be sent to the authorization service" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
apisix/plugins/forward-auth.lua
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.. "the client when authorize failure" | |
.. "the client when authorizing failed" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
apisix/plugins/forward-auth.lua
Outdated
-- 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better not trust the client's X-Forwarded-XX
by default
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
local auth_headers = { | |
["X-Forwarded-Proto"] = core.request.get_scheme(ctx), | |
["X-Forwarded-Method"] = core.request.get_method(), | |
["X-Forwarded-Host"] = core.request.get_host(ctx), | |
["X-Forwarded-Uri"] = core.request.get_uri(ctx), | |
["X-Forwarded-For"] = core.request.get_remote_client_ip(ctx), | |
} | |
-- 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 | |
if not auth_headers[header] then | |
auth_headers[header] = core.request.header(ctx, header) | |
end | |
end | |
else |
apisix/plugins/forward-auth.lua
Outdated
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
t/plugin/forward-auth.t
Outdated
"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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a test that the client passes X-Forwarded-XX by itself
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's doc the behavior when this field is empty
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
| 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` | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
Co-authored-by: 罗泽轩 <spacewanderlzx@gmail.com>
t/plugin/forward-auth.t
Outdated
"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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be better to log down the "X-Forwarded-XX" headers and check it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Log check for X-Forwarded-XX
, I will add it later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
apisix/plugins/forward-auth.lua
Outdated
client_headers[header] = res.headers[header] | ||
end | ||
else | ||
client_headers = res.headers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, changed
| -- | -- | -- | -- | -- | -- | | ||
| 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). | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to update the doc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
updated
"serverless-pre-function": { | ||
"phase": "rewrite", | ||
"functions": [ | ||
"return function(conf, ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be better to log down the APISIX generated "X-Forwarded-XX" headers and check it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
--- error_code: 403 | |
--- response_body eval | |
qr/\"x-forwarded-proto\":\"http\"/ and qr/\"x-forwarded-method\":\"GET\"/ and | |
qr/\"x-forwarded-host\":\"localhost\"/ and qr/\"x-forwarded-uri\":\"\\\/hello\"/ and | |
qr/\"x-forwarded-for\":\"127.0.0.1\"/ |
| -- | -- | -- | -- | -- | -- | | ||
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's provide a list of the X-Forwarded-XXX
headers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the Data Definition
section added
What this PR does / why we need it:
Add the
forward-auth
plugin.Fix #6007
Fix #5475
Pre-submission checklist: