Skip to content

Commit

Permalink
[THREESCALE-2236] Add methods option on Keycloak policy.
Browse files Browse the repository at this point in the history
This commits add the `methods` option on the Keycloak policy. That
allow users to define a new policy from any jwt claim, resource and
method.

To be backwards compatible 'ANY' method is in place, and if methods are
not defined this global method will be used and all will work as normal.

Example policy:

```
"policy_chain": [
  {
    "name": "apicast.policy.keycloak_role_check",
    "configuration": {
      "scopes": [
        {
          "realm_roles": [ { "name": "director" } ],
          "resource": "/confidential",
          "methods": ["POST"]
        }
      ],
      "type": "blacklist"
    }
  },
  { "name": "apicast.policy.apicast" }
]
```

Signed-off-by: Eloy Coto <eloy.coto@gmail.com>
  • Loading branch information
eloycoto committed May 15, 2019
1 parent f64716c commit ef24d2d
Show file tree
Hide file tree
Showing 6 changed files with 297 additions and 13 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- "Upstream Connection" policy. It allows to configure several options for the connections to the upstream [PR #1025](https://github.com/3scale/APIcast/pull/1025), [THREESCALE-2166](https://issues.jboss.org/browse/THREESCALE-2166)
- Enable APICAST_EXTENDED_METRICS environment variable to provide additional details [PR #1024](https://github.com/3scale/APIcast/pull/1024) [THREESCALE-2150](https://issues.jboss.org/browse/THREESCALE-2150)
- Add the option to obtain client_id from any JWT claim [THREESCALE-2264](https://issues.jboss.org/browse/THREESCALE-2264) [PR #1034](https://github.com/3scale/APIcast/pull/1034)

- Added `APICAST_PATH_ROUTING_ONLY` variable that allows to perform path-based routing without falling back to the default host-based routing [PR #1035](https://github.com/3scale/APIcast/pull/1035), [THREESCALE-1150](https://issues.jboss.org/browse/THREESCALE-1150)
- Added the option to manage access based on method on Keycloak Policy. [THREESCALE-2236](https://issues.jboss.org/browse/THREESCALE-2236) [PR #1039](https://github.com/3scale/APIcast/pull/1039)

### Fixed

Expand Down
14 changes: 14 additions & 0 deletions gateway/src/apicast/policy/keycloak_role_check/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,17 @@
]
}
```

- When you want to allow those who have the realm role `role1` to access `/resource1` and only methods GET and POST.

```json
{
"scopes": [
{
"realm_roles": [ { "name": "role1" } ],
"resource": "/resource1",
"methods": ["GET", "POST"]
}
]
}
```
20 changes: 20 additions & 0 deletions gateway/src/apicast/policy/keycloak_role_check/apicast-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,26 @@
"resource_type": {
"description": "How to evaluate 'resource'",
"$ref": "#/definitions/value_type"
},
"methods": {
"description": "Allowed methods",
"type": "array",
"default": ["ANY"],
"items": {
"type": "string",
"enum": [
"ANY",
"GET",
"HEAD",
"POST",
"PUT",
"DELETE",
"PATCH",
"OPTIONS",
"TRACE",
"CONNECT"
]
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ local default_type = 'plain'

local new = _M.new

local any_method = 'ANY'

local function create_template(value, value_type)
return TemplateString.new(value, value_type or default_type)
end
Expand Down Expand Up @@ -87,7 +89,12 @@ local function build_templates(scopes)
scope.resource_template_string = create_template(
scope.resource, scope.resource_type)

if not scope.methods then
scope.methods = { any_method }
end

end

end

function _M.new(config)
Expand Down Expand Up @@ -154,29 +161,38 @@ end

local function scope_check(scopes, context)
local uri = ngx.var.uri
local request_method = ngx.req.get_method()

if not context.jwt then
return false
end

for _, scope in ipairs(scopes) do
for _, method in ipairs(scope.methods) do
-- make a matched method just in case that `ANY` method is defined and
-- the mapping rules does not match, the matched_method need to be
-- cleared in the next interaction to trigger with the correct method.
local matched_method = request_method
if method == any_method then
matched_method = any_method
end

local resource = scope.resource_template_string:render(context)
local resource = scope.resource_template_string:render(context)

local mapping_rule = MappingRule.from_proxy_rule({
http_method = 'ANY',
pattern = resource,
querystring_parameters = {},
-- the name of the metric is irrelevant
metric_system_name = 'hits'
})
local mapping_rule = MappingRule.from_proxy_rule({
http_method = method,
pattern = resource,
querystring_parameters = {},
-- the name of the metric is irrelevant
metric_system_name = 'hits'
})

if mapping_rule:matches('ANY', uri) then
if match_realm_roles(scope, context) and match_client_roles(scope, context) then
return true
if mapping_rule:matches(matched_method, uri) then
if match_realm_roles(scope, context) and match_client_roles(scope, context) then
return true
end
end
end

end

return false
Expand Down
77 changes: 77 additions & 0 deletions spec/policy/keycloak_role_check/keycloak_role_check_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ describe('Keycloak Role check policy', function()
ngx.header = {}
stub(ngx, 'print')

stub(ngx.req, 'get_method', function() return 'GET' end)
stub(ngx_variable, 'available_context', function(context) return context end)
-- avoid stubbing all the ngx.var.* and ngx.req.* in the available context
stub(ngx_variable, 'available_context', function(context) return context end)
end)
Expand Down Expand Up @@ -572,6 +574,81 @@ describe('Keycloak Role check policy', function()
assert.not_same(ngx.status, 403)
end)
end)

describe("validate method roles", function()

before_each(function()
ngx.var = {
uri = '/foo'
}
end)

local context = {
jwt = {
realm_access = {
roles = { "known_role" }
},
resource_access = {
known_client = {
roles = { "role_of_known_client" }
}
}
},
service = {
auth_failed_status = 403,
error_auth_failed = "auth failed"
}
}

it("when jwt role matches and one method also match", function()
local role_check_policy = KeycloakRoleCheckPolicy.new({
scopes = {{
client_roles = { { name = "role_of_known_client", client = "known_client" } },
resource = "/foo",
methods = {"GET", "POST", "PUT"}
}}})
role_check_policy:access(context)
assert.not_same(ngx.status, 403)
end)

it("when jwt role matches and one method also match with blacklist mode", function()
local role_check_policy = KeycloakRoleCheckPolicy.new({
scopes = {{
client_roles = { { name = "role_of_known_client", client = "known_client" } },
resource = "/foo",
methods = {"GET", "POST", "PUT"}
}},
type = "blacklist"})

role_check_policy:access(context)
assert.same(ngx.status, 403)
end)

it("when jwt role matches and no methods matches", function()
local role_check_policy = KeycloakRoleCheckPolicy.new({
scopes = {{
client_roles = { { name = "role_of_known_client", client = "known_client" } },
resource = "/foo",
methods = {"POST", "PUT"}
}}})
role_check_policy:access(context)
assert.same(ngx.status, 403)
end)

it("when jwt role matches and no methods matches with blacklist mode", function()
local role_check_policy = KeycloakRoleCheckPolicy.new({
scopes = {{
client_roles = { { name = "role_of_known_client", client = "known_client" } },
resource = "/foo",
methods = {"POST", "PUT"}
}},
type = "blacklist"})
role_check_policy:access(context)
assert.not_same(ngx.status, 403)
end)

end)

end)
end)
end)
157 changes: 157 additions & 0 deletions t/apicast-policy-keycloak-role-check.t
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,160 @@ yay, api backend
--- no_error_log
[error]
oauth failed with
=== TEST6: Role check with allow methods
The client which has the appropriate role accesses the resource with only one method allowed
--- backend
location /transactions/oauth_authrep.xml {
content_by_lua_block {
ngx.exit(200)
}
}
--- configuration
{
"oidc": [
{
"issuer": "https://example.com/auth/realms/apicast",
"config": { "id_token_signing_alg_values_supported": [ "RS256" ] },
"keys": { "somekid": { "pem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALClz96cDQ965ENYMfZzG+Acu25lpx2K\nNpAALBQ+catCA59us7+uLY5rjQR6SOgZpCz5PJiKNAdRPDJMXSmXqM0CAwEAAQ==\n-----END PUBLIC KEY-----" } }
}
],
"services": [
{
"id": 42,
"backend_version": "oauth",
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"authentication_method": "oidc",
"oidc_issuer_endpoint": "https://example.com/auth/realms/apicast",
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 }
],
"policy_chain": [
{
"name": "apicast.policy.keycloak_role_check",
"configuration": {
"scopes": [
{
"realm_roles": [ { "name": "director" } ],
"resource": "/confidential",
"methods": ["GET"]
}
]
}
},
{ "name": "apicast.policy.apicast" }
]
}
}
]
}
--- upstream
location /confidential {
content_by_lua_block {
ngx.say('yay, api backend');
}
}
--- pipelined_requests eval
["GET /confidential", "POST /confidential"]
--- more_headers eval
[::authorization_bearer_jwt('audience', {
realm_access => {
roles => [ 'director' ]
}
}, 'somekid'),
::authorization_bearer_jwt('audience', {
realm_access => {
roles => [ 'director' ]
}
}, 'somekid')]
--- response_body eval
["yay, api backend\n", "Authentication failed"]
--- error_code eval
[200, 403]
--- no_error_log
[error]
oauth failed with
=== TEST7: Role check with allow methods and blacklist mode
Check an allowed role with the blacklisted mode with methods
--- backend
location /transactions/oauth_authrep.xml {
content_by_lua_block {
ngx.exit(200)
}
}
--- configuration
{
"oidc": [
{
"issuer": "https://example.com/auth/realms/apicast",
"config": { "id_token_signing_alg_values_supported": [ "RS256" ] },
"keys": { "somekid": { "pem": "-----BEGIN PUBLIC KEY-----\nMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALClz96cDQ965ENYMfZzG+Acu25lpx2K\nNpAALBQ+catCA59us7+uLY5rjQR6SOgZpCz5PJiKNAdRPDJMXSmXqM0CAwEAAQ==\n-----END PUBLIC KEY-----" } }
}
],
"services": [
{
"id": 42,
"backend_version": "oauth",
"backend_authentication_type": "service_token",
"backend_authentication_value": "token-value",
"proxy": {
"authentication_method": "oidc",
"oidc_issuer_endpoint": "https://example.com/auth/realms/apicast",
"api_backend": "http://test:$TEST_NGINX_SERVER_PORT/",
"proxy_rules": [
{ "pattern": "/", "http_method": "GET", "metric_system_name": "hits", "delta": 1 }
],
"policy_chain": [
{
"name": "apicast.policy.keycloak_role_check",
"configuration": {
"scopes": [
{
"realm_roles": [ { "name": "director" } ],
"resource": "/confidential",
"methods": ["POST"]
}
],
"type": "blacklist"
}
},
{ "name": "apicast.policy.apicast" }
]
}
}
]
}
--- upstream
location /confidential {
content_by_lua_block {
ngx.say('yay, api backend');
}
}
--- pipelined_requests eval
["GET /confidential", "POST /confidential"]
--- more_headers eval
[::authorization_bearer_jwt('audience', {
realm_access => {
roles => [ 'director' ]
}
}, 'somekid'),
::authorization_bearer_jwt('audience', {
realm_access => {
roles => [ 'director' ]
}
}, 'somekid')]
--- response_body eval
["yay, api backend\n", "Authentication failed"]
--- error_code eval
[200, 403]
--- no_error_log
[error]
oauth failed with

0 comments on commit ef24d2d

Please sign in to comment.