[go: nahoru, domu]

Skip to content
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 auth plugin for casdoor #6382

Merged
merged 13 commits into from
Mar 28, 2022
Merged

Conversation

ComradeProgrammer
Copy link
Contributor
@ComradeProgrammer ComradeProgrammer commented Feb 19, 2022

What this PR does / why we need it:

auth-casdoor is an authorization plugin based on Casdoor. Casdoor is a centralized authentication / Single-Sign-On (SSO) platform

Pre-submission checklist:

  • Did you explain what problem does this PR solve? Or what new features have been added?
  • Have you added corresponding test cases?
  • Have you modified the corresponding document?
  • Is this PR backward compatible? If it is not backward compatible, please discuss on the mailing list first

@ComradeProgrammer ComradeProgrammer marked this pull request as ready for review February 19, 2022 06:44
@ComradeProgrammer ComradeProgrammer changed the title feat: add authz plugin for casdoor feat: add authn plugin for casdoor Feb 19, 2022
@ComradeProgrammer ComradeProgrammer changed the title feat: add authn plugin for casdoor feat: add auth plugin for casdoor Feb 19, 2022
local schema = {
type = "object",
properties = {
casdoor_endpoint = { type = "string" },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the casdoor_ prefix

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ComradeProgrammer
Please request me to review it once you are ready. Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the casdoor_ prefix

all other casdoor_ prefixes are removed except casdoor_endpoint, because i think just using "endpoint" may be a little confusing.

local schema = {
type = "object",
properties = {
casdoor_endpoint = { type = "string" },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the casdoor_ prefix

local core = require("apisix.core")
local log = core.log
local ngx = ngx
local session = require("resty.session")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's sort the imports like

local plugin = require("apisix.plugin")

@@ -340,6 +340,7 @@ plugins: # plugin list (sorted by priority)
- request-validation # priority: 2800
- openid-connect # priority: 2599
- authz-casbin # priority: 2560
- auth-casdoor # priority: 2561
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sort according to the priority and fix the indent


local _M = {
version = 0.1,
priority = 12,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority mismatch


if session_obj_read.data.access_token then
-- session exists
log.warn("session exists")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto. Warn log should not be used in per-req level

local args = ngx.req.get_uri_args()
if not args.code or not args.state then
log.err("failed when accessing token. Invalid code or state ")
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return nil
return nil, err

and log the err outside would be better

end
local client = http.new()
local res, err = client:request_uri(
get_path(conf.casdoor_endpoint) .. "/api/login/oauth/access_token",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's check if the endpoint contains ? in the schema instead of split it at runtime

log.err("failed when accessing token. ", err)
return nil
end
local data = cjson.decode(res.body)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to check if decode succeed


```

By doing this all requests toward `/anything/*` will be intercepted and whether this user has logged in via Casdoor will be checked (by checking session). If not, the user will be redirected to login page of casdoor, thus implementing authentication without modifying server's code.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you provide a step-by-step example?
I still can't fully understand it after reading it twice...
It looks like the callback_url needs to contain part of the request host & path?

@ComradeProgrammer
Copy link
Contributor Author

I am extremely flattered, delighted and grateful for this extremely detailed reply and advice from you. Revisions will be made accordingly soon and tests will be added soon together. @spacewander

client_id = { type = "string" },
client_secret = { type = "string" },
callback_url = { type = "string" },
test = { type= "boolean", default = false }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test is not in Attributes of plugin docs

end

function _M.access(conf, ctx)
log.info("hit auth-casdoor access")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line looks like it's only for debugging?

local function fetch_access_token(conf)
local args = core.request.get_uri_args()
if not args.code or not args.state then
log.err()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean this function? Extract "code" and "state" parameters and use them to launch to casdoor api, in order to fetch "access_token" so that we can confirm whether the user have really logged in

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The err doesn't have parameters in it

end

local function fetch_access_token(conf)
local args = core.request.get_uri_args()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local args = core.request.get_uri_args()
local args = core.request.get_uri_args(ctx)

and should pass ctx to this function


local function fetch_access_token(conf)
local args = core.request.get_uri_args()
if not args.code or not args.state then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's better to determine if args is empty

end

function _M.check_schema(conf)
local ok,err=core.schema.check(schema, conf)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local ok,err=core.schema.check(schema, conf)
local ok, err = core.schema.check(schema, conf)

local ok,err=core.schema.check(schema, conf)
if ok then
--check whether contains extra ? or /
if string.find(conf.callback_url,"?",1) then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use ngx.re.find is better?

if ok then
--check whether contains extra ? or /
if string.find(conf.callback_url,"?",1) then
return false,"callback_url should not contain get parameters or end up with /"
else
return true,nil
end
else
return false,err
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ok then
--check whether contains extra ? or /
if string.find(conf.callback_url,"?",1) then
return false,"callback_url should not contain get parameters or end up with /"
else
return true,nil
end
else
return false,err
end
if not ok then
return false, err
end
--check whether contains extra ? or /
local has_extra = string.find(conf.callback_url,"?",1)
if has_extra then
return false, "callback_url should not contain get parameters or end up with /"
end
return true, nil

is better?

if access_token then
local original_url = session_obj_read.data.original_uri
if not original_url then
return 503,"no original_url found in session"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return 503,"no original_url found in session"
return 503, "no original_url found in session"

session_obj_write.data.access_token = access_token
session_obj_write:save()
ngx.redirect(original_url, 302)
return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a bit strange that here return does not reverse anything, but just exits the current function?

@ComradeProgrammer
Copy link
Contributor Author

@spacewander @tzssangglass Most of the revisions in your comments have been implemented, and tests are also added. I think this code is ready to be reviewed now, THX.

@tzssangglass
Copy link
Member

@spacewander @tzssangglass Most of the revisions in your comments have been implemented, and tests are also added. I think this code is ready to be reviewed now, THX.

hi @ComradeProgrammer, after the CI run is finished, please fix the failure of the CI.

@ComradeProgrammer
Copy link
Contributor Author

hi @ComradeProgrammer, after the CI run is finished, please fix the failure of the CI.

I have made changes according to the CI information, and I wonder whether I can have CI run another time , so that I can confirm whether I can pass CI now?

@tzssangglass
Copy link
Member

I have made changes according to the CI information, and I wonder whether I can have CI run another time , so that I can confirm whether I can pass CI now?

hi @ComradeProgrammer , there are still some code lint check failed.

local schema = {
type = "object",
properties = {
casdoor_endpoint = {type = "string"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
casdoor_endpoint = {type = "string"},
endpoint_addr = {type = "string"},

would be better?

}
}

local _M = {version = 0.1, priority = 2559, name = plugin_name, schema = schema}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please put each field per line, like other plugins

local function fetch_access_token(conf)
local args = core.request.get_uri_args()
if not args.code or not args.state then
log.err()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The err doesn't have parameters in it

return nil, "failed when accessing token. Invalid code or state"
end
local client = http.new()
local url = get_path(conf.casdoor_endpoint) ..
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we require the endpoint not to have ? so we don't need to use get_path?

local ok, err = core.schema.check(schema, conf)
if not ok then return false, err end
-- check whether contains extra ? or /
local has_extra = string.find(conf.callback_url, "?", 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do the check in the schema via "pattern":

pattern = [[^[a-zA-Z0-9-_.]+$]]


The first one is "callback_url". This is exactly the callback url in OAuth2. It should be emphasized that this callback url **must belong to the "uri" you specified for the route**, for example, in this example, http://localhost:9080/anything/callback obviously belong to "/anything/*" Only by this way can the visit toward callback_url can be intercepted and utilized by the plugin(so that the plugin can get the code and state in Oauth2). The logic of callback_url is implemented competely by plugin so that there is no need to modify the server implement this callback.

The second parameter "callback_url" is obviously the url of Casdoor. The third and fourth parameters are "client_id" and "client_secret", which you can acquire from Casdoor when you register an app.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The second parameter "callback_url" is obviously the url of Casdoor. The third and fourth parameters are "client_id" and "client_secret", which you can acquire from Casdoor when you register an app.
The second parameter "endpoint_addr" is obviously the url of Casdoor. The third and fourth parameters are "client_id" and "client_secret", which you can acquire from Casdoor when you register an app.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing this one?


#### How it works?

Suppose a new user who have never visited this route before is going to visit it (http://localhost:9080/anything/d?param1=foo&param2=bar), considering that "auth-casdoor" is enabled, this visit would be processed by "auth-casdoor" plugin first. After checking the session and confirming that this use hasn't been authentication, the visit will be intercepted. After remembering the original url user wants to visit, he will be redirected to the login page of Casdoor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Suppose a new user who have never visited this route before is going to visit it (http://localhost:9080/anything/d?param1=foo&param2=bar), considering that "auth-casdoor" is enabled, this visit would be processed by "auth-casdoor" plugin first. After checking the session and confirming that this use hasn't been authentication, the visit will be intercepted. After remembering the original url user wants to visit, he will be redirected to the login page of Casdoor.
Suppose a new user who has never visited this route before is going to visit it (http://localhost:9080/anything/d?param1=foo&param2=bar), considering that "auth-casdoor" is enabled, this visit would be processed by "auth-casdoor" plugin first. After checking the session and confirming that this user hasn't been authenticated, the visit will be intercepted. With the original url user wants to visit kept, he will be redirected to the login page of Casdoor.


After successfully logging in with username and password(or whatever method he use), Casdoor will redirect this user to the "callback_url" with GET parameter "code " and "state" specified. Because the "callback_url" is known by the plugin, so when the visit toward the "callback_url" is intercepted this time, the logic of "Authorization code Grant Flow" in Oauth2 will be triggered, which means this plugin will request the access token to confirm whether this user is really logged in. After this confirmation, this plugin will redirect this user to the original url user wants to visit, which was remembered by us previously. The logged in status will also be remembered by session

Next time this user want to visit url behind this route (for example, http://localhost:9080/anything/d), after discovering that this user have been authentication previously, this plugin won't redirect this user any more so that this user can visit whatever he wants under this route without being interfered
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Next time this user want to visit url behind this route (for example, http://localhost:9080/anything/d), after discovering that this user have been authentication previously, this plugin won't redirect this user any more so that this user can visit whatever he wants under this route without being interfered
Next time this user want to visit url behind this route (for example, http://localhost:9080/anything/d), after discovering that this user has been authenticated previously, this plugin won't redirect this user anymore so that this user can visit whatever he wants under this route without being interfered


Suppose a new user who have never visited this route before is going to visit it (http://localhost:9080/anything/d?param1=foo&param2=bar), considering that "auth-casdoor" is enabled, this visit would be processed by "auth-casdoor" plugin first. After checking the session and confirming that this use hasn't been authentication, the visit will be intercepted. After remembering the original url user wants to visit, he will be redirected to the login page of Casdoor.

After successfully logging in with username and password(or whatever method he use), Casdoor will redirect this user to the "callback_url" with GET parameter "code " and "state" specified. Because the "callback_url" is known by the plugin, so when the visit toward the "callback_url" is intercepted this time, the logic of "Authorization code Grant Flow" in Oauth2 will be triggered, which means this plugin will request the access token to confirm whether this user is really logged in. After this confirmation, this plugin will redirect this user to the original url user wants to visit, which was remembered by us previously. The logged in status will also be remembered by session
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
After successfully logging in with username and password(or whatever method he use), Casdoor will redirect this user to the "callback_url" with GET parameter "code " and "state" specified. Because the "callback_url" is known by the plugin, so when the visit toward the "callback_url" is intercepted this time, the logic of "Authorization code Grant Flow" in Oauth2 will be triggered, which means this plugin will request the access token to confirm whether this user is really logged in. After this confirmation, this plugin will redirect this user to the original url user wants to visit, which was remembered by us previously. The logged in status will also be remembered by session
After successfully logging in with username and password(or whatever method he uses), Casdoor will redirect this user to the "callback_url" with GET parameter "code " and "state" specified. Because the "callback_url" is known by the plugin, when the visit toward the "callback_url" is intercepted this time, the logic of "Authorization code Grant Flow" in Oauth2 will be triggered, which means this plugin will request the access token to confirm whether this user is really logged in. After this confirmation, this plugin will redirect this user to the original url user wants to visit, which was kept by us previously. The logged-in status will also be kept in the session.


local _M = {version = 0.1, priority = 2559, name = plugin_name, schema = schema}

local function get_path(uri)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use

local url_decoded = url.parse(conf.uri)

to get the path instead of writing own solution.

@ComradeProgrammer
Copy link
Contributor Author

@spacewander revisions have been made

function _M.access(conf, ctx)
-- log.info("hit auth-casdoor access")
local current_uri = ctx.var.uri
local session_obj_read = session.open()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check if session exist? ref: bungle/lua-resty-session#12 (comment)

Comment on lines 88 to 82
-- step 1: check whether hits the callback

if current_uri == conf.callback_url:match(".-//[^/]+(/.*)") then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- step 1: check whether hits the callback
if current_uri == conf.callback_url:match(".-//[^/]+(/.*)") then
-- step 1: check whether hits the callback
if current_uri == conf.callback_url:match(".-//[^/]+(/.*)") then


## Name

`auth-casdoor` is an authorization plugin based on [Casdoor](https://casdoor.org/). Casdoor is a centralized authentication / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC and SAML, integrated with Casbin RBAC and ABAC permission management
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`auth-casdoor` is an authorization plugin based on [Casdoor](https://casdoor.org/). Casdoor is a centralized authentication / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC and SAML, integrated with Casbin RBAC and ABAC permission management
`auth-casdoor` is an authorization plugin based on [Casdoor](https://casdoor.org/). Casdoor is a centralized authentication / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC and SAML, integrated with Casbin RBAC and ABAC permission management.

local cookie = res1.headers["Set-Cookie"]


local res2,err2=httpc:request_uri(callback_url, {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local res2,err2=httpc:request_uri(callback_url, {
local res2,err2 = httpc:request_uri(callback_url, {

Please pay more attention to similar formatting issues in this PR.

@ComradeProgrammer
Copy link
Contributor Author

@spacewander @tzssangglass what about this time? (revisions have been made)

@ComradeProgrammer
Copy link
Contributor Author
ComradeProgrammer commented Mar 2, 2022

@spacewander @tzssangglass what kind of "chaos-test" is this? Besides, comparing with my last previous commit, the only change was format in test file t/plugin/auth-casdoor.t, but I passed all the tests in the previous commit.

@ComradeProgrammer
Copy link
Contributor Author

what is apisix/utils/batch-processor.lua? This file seemed to lead to the failure of lint but it's irrelevant with me......

@tzssangglass
Copy link
Member

what is apisix/utils/batch-processor.lua? This file seemed to lead to the failure of lint but it's irrelevant with me......

Please merge the APISIX master branch into your PR branch.

@ComradeProgrammer
Copy link
Contributor Author

rebase made(force-pushed was caused by rebase)

end
local access_token, lifetime, err =
fetch_access_token(args.code, conf)
if err then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if err then
if not access_token then

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revised

core.log.error(err)
return 503
end
if access_token then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch can be saved

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revised

session_obj_write.data.state = state
session_obj_write:save()

local redirect_url=conf.endpoint_addr .. "/login/oauth/authorize?" .. ngx.encode_args({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local redirect_url=conf.endpoint_addr .. "/login/oauth/authorize?" .. ngx.encode_args({
local redirect_url = conf.endpoint_addr .. "/login/oauth/authorize?" .. ngx.encode_args({

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revised

@ComradeProgrammer
Copy link
Contributor Author

revisions have been made

Copy link
Member
@leslie-tsang leslie-tsang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@spacewander @ComradeProgrammer The documentation in the PR still needs to be optimized, let's merge this PR first and deal with the documentation in the next PR ?

@spacewander
Copy link
Member

OK
@leslie-tsang
Let's open an issue about what needs to be proved.

@ComradeProgrammer
Copy link
Contributor Author
  1. Thanks! Please merge this PR.
  2. I will work on the docs for this plugin later in another PR .

@spacewander spacewander merged commit c8fe8ab into apache:master Mar 28, 2022
@spacewander
Copy link
Member

@ComradeProgrammer
Let's add casdoor to the README

@lijing-21
Copy link
Contributor

Hi @ComradeProgrammer , thank you for your contribution!

Here is the Contributor T-shirt form[1], if you're interested, kindly take a look :)

[1] https://github.com/apache/apisix/blob/master/CONTRIBUTING.md#contributor-t-shirt

Liu-Junlin pushed a commit to Liu-Junlin/apisix that referenced this pull request May 20, 2022
spacewander pushed a commit that referenced this pull request Jun 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants