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(authz-keycloak): dynamic scope and resource mapping. #3308

Merged

Conversation

jenskeiner
Copy link
Contributor

@jenskeiner jenskeiner commented Jan 15, 2021

What this PR does / why we need it:

Add code to authz-keycloak plugin to optionally resolve requests URI to resource(s) and map HTTP method to scope; fixes #3274.

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

Open this PR to get some feedback on the changes. Still need to add tests to cover the new functionality. May still need to polish the JSON schema slightly as well. However, already tested changes in a real deployment to confirm this is actually working.

Also, strictly speaking, this plugin is not backwards compatible. E.g. we now require static permissions to be set when not using the new dynamic resource resolution. We also require client_id to be set because it will be used in any sensible configuration scenario. This mostly affects corner cases, so I don't think it represents a real problem.

In any case, will likely polish everything a bit more, but would appreciate any feedback already.

@sshniro You should definitely have a look.

@@ -371,16 +374,16 @@ http {
{% end %}
{% end %} {% -- if enable_ipv6 %}

{% if ssl.ssl_trusted_certificate ~= nil 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.

Moved this here since it was previously only used when APISIX itself was accepting HTTPS from the outside. But even if TLS is disabled, internally, plugins that send requests may still need to be able to use TLS and may have custom CA certs configured.

@@ -42,7 +43,6 @@ local schema = {
enum = {"urn:ietf:params:oauth:grant-type:uma-ticket"},
minLength = 1, maxLength = 100
},
audience = {type = "string", minLength = 1, maxLength = 100},
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is now called client_id which is more consistent with the opendid-connect plugin as well.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can keep the old field and add a deprecated check?

apisix/apisix/init.lua

Lines 463 to 470 in bbbdf58

if upstream.value.enable_websocket then
core.log.warn("DEPRECATE: enable websocket in upstream will be removed soon. ",
"Please enable it in route/service level.")
enable_websocket = true
end
if upstream.value.pass_host then
api_ctx.pass_host = upstream.value.pass_host

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@spacewander Possible, it's just a name. So we keep client_id as well and prefer it, if given?

Copy link
Member

Choose a reason for hiding this comment

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

Yes. Maybe we can add a description on them:

description = "client IP",

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, will change accordingly. Will also add/fix more test cases soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, done.

| policy_enforcement_mode | string | optional | "ENFORCING" | ["ENFORCING", "PERMISSIVE"] | |
| permissions | array[string] | optional | | | Static permission to request, an array of strings each representing a resources and optionally one or more scopes the client is seeking access. |
| lazy_load_paths | boolean | optional | false | | Dynamically resolve the request URI to resource(s) using the resource registration endpoint instead of using the static permission. |
| http_method_as_scope | boolean | optional | false | | Map HTTP request type to scope of same name and add to all permissions requested. |
Copy link
Member

Choose a reason for hiding this comment

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

Missing doc for cache_ttl_seconds?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Correct. Will add.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

@@ -171,13 +222,13 @@ local function authz_keycloak_discover(url, ssl_verify, keepalive, timeout,
keepalive = (keepalive ~= "no")
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 bug in the previous code: the keepalive and ssl_verify are boolean.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My bad, was copied over from openidc module. Fixed it and consolidated the HTTP client setup -- which is done in various places -- to apply options like timeout, keepalive, ssl_verifyand others consistently.

| token_endpoint | string | optional | | https://host.domain/auth/realms/foo/protocol/openid-connect/token | A OAuth2-compliant Token Endpoint that supports the `urn:ietf:params:oauth:grant-type:uma-ticket` grant type. Overrides value from discovery, if given. |
| resource_registration_endpoint | string | optional | | https://host.domain/auth/realms/foo/authz/protection/resource_set | A Keycloak Protection API-compliant resource registration endpoint. Overrides value from discovery, if given. |
| grant_type | string | optional | "urn:ietf:params:oauth:grant-type:uma-ticket" | ["urn:ietf:params:oauth:grant-type:uma-ticket"] | |
| client_id | string | required | | | The client identifier of the resource server to which the client is seeking access. <br>This parameter is mandatory when parameter permission is defined. |
Copy link
Member

Choose a reason for hiding this comment

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

This parameter is mandatory when parameter permission is defined.

Look like it should be when parameter lazy_load_paths is defined? Should require it in the schema and add a test for it.

Copy link
Contributor Author

@jenskeiner jenskeiner Jan 21, 2021

Choose a reason for hiding this comment

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

Ok, so I'm now again requiring one of client_id or audience, the latter for better backwards compatibility. Rationale: The plugin always uses the token endpoint to get a decision from Keycloak on the requested permissions. As per https://www.keycloak.org/docs/latest/authorization_services/#_service_obtaining_permissions the audience parameter (what is either in client_id or audience for the plugin) is required when the permission parameter is set (which the plugin always does).

If lazy_load_paths is true, the plugin also needs to obtain an access token for itself from Keycloak. To that end, it most likely also needs client_secret to be set, but that depends on how Keycloak is configured. Hence, this is still optional.

Also added test cases to check if one of client_id or audience is correctly required and schema verification fails if both are absent.

Copy link
Member

Choose a reason for hiding this comment

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

I see.

end

local session = authz_keycloak_cache_get("access_tokens", token_endpoint .. ":"
.. client_id)
Copy link
Member

Choose a reason for hiding this comment

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

The client_id could be nil if you don't force when lazy_load_paths is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

See above, we're now requiring client_id (or audience).

@@ -137,7 +224,7 @@ done
"authz-keycloak": {
"token_endpoint": "http://127.0.0.1:8090/auth/realms/University/protocol/openid-connect/token",
"permissions": ["course_resource#view"],
"audience": "course_management",
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can leave audience unchanged in one or two cases so that we know the old configuration is still compatible?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I can add some cases that use it.

@spacewander
Copy link
Member

BTW, you can split the test into two files, like the https://github.com/apache/apisix/blob/master/t/plugin/limit-conn2.t. Since it is too big now.

@jenskeiner
Copy link
Contributor Author

BTW, you can split the test into two files, like the https://github.com/apache/apisix/blob/master/t/plugin/limit-conn2.t. Since it is too big now.

done

@jenskeiner
Copy link
Contributor Author

@spacewander I think I've addressed all of the feedback so far. Thanks for reviewing. Let me know any further issues.

-- Return access_token expires_in value (in seconds).
local function authz_keycloak_access_token_expires_in(conf, expires_in)
return (expires_in or conf.access_token_expires_in or 300)
- 1 - (conf.access_token_expires_leeway or 0)
Copy link
Member

Choose a reason for hiding this comment

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

Will you add this configurations in this PR, or in the future one?

-- Return refresh_token expires_in value (in seconds).
local function authz_keycloak_refresh_token_expires_in(conf, expires_in)
return (expires_in or conf.refresh_token_expires_in or 3600)
- 1 - (conf.refresh_token_expires_leeway or 0)
Copy link
Member

Choose a reason for hiding this comment

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

Ditto

Copy link
Member

@spacewander spacewander left a comment

Choose a reason for hiding this comment

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

We can merge this PR first and solve the remain in the next one.

@jenskeiner
Copy link
Contributor Author

@spacewander Thanks, I will surely continue the work on this plugin and I can add the config options in a future PR.

@membphis
Copy link
Member

I have been busy recently, I will review this PR later.

Welcome everyone to review. many thx

@spacewander spacewander changed the title feat: authz-keycloak plugin - dynamic scope and resource mapping. feat(authz-keycloak): dynamic scope and resource mapping. Jan 27, 2021
@spacewander spacewander merged commit b5374bb into apache:master Jan 27, 2021
@jenskeiner jenskeiner deleted the dynamic-scope-and-resource-mapping branch January 29, 2021 08:57
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.

feat: auth-keycloak plugin - Dynamically resolve resources and scopes from incoming request.
5 participants