Skip to content

Commit 9212c8d

Browse files
authored
Merge pull request #1039 from eloycoto/THREESCALE-2236
[THREESCALE-2236] Add methods option on Keycloak policy.
2 parents 590cc7a + 32d8ecf commit 9212c8d

File tree

8 files changed

+339
-21
lines changed

8 files changed

+339
-21
lines changed

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1212
- "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)
1313
- 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)
1414
- 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)
15-
1615
- 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)
16+
- 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)
1717

1818
### Fixed
1919

gateway/src/apicast/mapping_rule.lua

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ local re_match = ngx.re.match
1313
local insert = table.insert
1414
local re_gsub = ngx.re.gsub
1515

16-
local _M = {}
16+
local _M = {
17+
any_method = "ANY"
18+
}
1719

1820
local mt = { __index = _M }
1921

@@ -132,7 +134,7 @@ end
132134
-- @tparam table args Table with the args and values of an HTTP request.
133135
-- @treturn boolean Whether the mapping rule matches the given request.
134136
function _M:matches(method, uri, args)
135-
local match = self.method == method and
137+
local match = (self.method == self.any_method or self.method == method) and
136138
matches_uri(self.regexpified_pattern, uri) and
137139
self.querystring_params(args)
138140

gateway/src/apicast/policy/keycloak_role_check/README.md

+14
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,17 @@
104104
]
105105
}
106106
```
107+
108+
- When you want to allow those who have the realm role `role1` to access `/resource1` and only methods GET and POST.
109+
110+
```json
111+
{
112+
"scopes": [
113+
{
114+
"realm_roles": [ { "name": "role1" } ],
115+
"resource": "/resource1",
116+
"methods": ["GET", "POST"]
117+
}
118+
]
119+
}
120+
```

gateway/src/apicast/policy/keycloak_role_check/apicast-policy.json

+20
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@
7777
"resource_type": {
7878
"description": "How to evaluate 'resource'",
7979
"$ref": "#/definitions/value_type"
80+
},
81+
"methods": {
82+
"description": "Allowed methods",
83+
"type": "array",
84+
"default": ["ANY"],
85+
"items": {
86+
"type": "string",
87+
"enum": [
88+
"ANY",
89+
"GET",
90+
"HEAD",
91+
"POST",
92+
"PUT",
93+
"DELETE",
94+
"PATCH",
95+
"OPTIONS",
96+
"TRACE",
97+
"CONNECT"
98+
]
99+
}
80100
}
81101
}
82102
}

gateway/src/apicast/policy/keycloak_role_check/keycloak_role_check.lua

+28-13
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ local default_type = 'plain'
6060

6161
local new = _M.new
6262

63+
local any_method = MappingRule.any_method
64+
6365
local function create_template(value, value_type)
6466
return TemplateString.new(value, value_type or default_type)
6567
end
6668

67-
local function build_templates(scopes)
69+
local function build_scopes(scopes)
6870
for _, scope in ipairs(scopes) do
6971

7072
if scope.realm_roles then
@@ -86,16 +88,20 @@ local function build_templates(scopes)
8688

8789
scope.resource_template_string = create_template(
8890
scope.resource, scope.resource_type)
91+
if (not scope.methods) or (scope.methods and #scope.methods == 0 ) then
92+
scope.methods = { any_method }
93+
end
8994

9095
end
96+
9197
end
9298

9399
function _M.new(config)
94100
local self = new()
95101
self.type = config.type or "whitelist"
96102
self.scopes = config.scopes or {}
97103

98-
build_templates(self.scopes)
104+
build_scopes(self.scopes)
99105

100106
return self
101107
end
@@ -152,38 +158,47 @@ local function match_client_roles(scope, context)
152158
return true
153159
end
154160

155-
local function scope_check(scopes, context)
156-
local uri = ngx.var.uri
157-
158-
if not context.jwt then
159-
return false
160-
end
161-
162-
for _, scope in ipairs(scopes) do
161+
local function validate_scope_access(scope, context, uri, request_method)
162+
for _, method in ipairs(scope.methods) do
163163

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

166166
local mapping_rule = MappingRule.from_proxy_rule({
167-
http_method = 'ANY',
167+
http_method = method,
168168
pattern = resource,
169169
querystring_parameters = {},
170170
-- the name of the metric is irrelevant
171171
metric_system_name = 'hits'
172172
})
173173

174-
if mapping_rule:matches('ANY', uri) then
174+
if mapping_rule:matches(request_method, uri) then
175175
if match_realm_roles(scope, context) and match_client_roles(scope, context) then
176176
return true
177177
end
178178
end
179+
end
180+
return false
181+
end
182+
183+
local function scopes_check(scopes, context)
184+
local uri = ngx.var.uri
185+
local request_method = ngx.req.get_method()
186+
187+
if not context.jwt then
188+
return false
189+
end
179190

191+
for _, scope in ipairs(scopes) do
192+
if validate_scope_access(scope, context, uri, request_method) then
193+
return true
194+
end
180195
end
181196

182197
return false
183198
end
184199

185200
function _M:access(context)
186-
if scope_check(self.scopes, context) then
201+
if scopes_check(self.scopes, context) then
187202
if self.type == "blacklist" then
188203
return errors.authorization_failed(context.service)
189204
end

spec/mapping_rule_spec.lua

+20
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,26 @@ describe('mapping_rule', function()
120120
assert.is_true(mapping_rule:matches('GET', '/foo/a:b/bar'))
121121
assert.is_true(mapping_rule:matches('GET', "/foo/a%b/bar"))
122122
end)
123+
end)
124+
125+
describe('.any_method', function()
126+
127+
it("Allow connections when any method is defined", function()
123128

129+
local mapping_rule = MappingRule.from_proxy_rule({
130+
http_method = MappingRule.any_method,
131+
pattern = '/foo/',
132+
querystring_parameters = { },
133+
metric_system_name = 'hits',
134+
delta = 1
135+
})
136+
137+
assert.is_true(mapping_rule:matches('GET', '/foo/'))
138+
assert.is_true(mapping_rule:matches('POST', '/foo/'))
139+
assert.is_true(mapping_rule:matches('PUT', "/foo/"))
140+
assert.is_true(mapping_rule:matches('DELETE', "/foo/"))
141+
assert.is_true(mapping_rule:matches('PATCH', "/foo/"))
142+
end)
124143
end)
144+
125145
end)

spec/policy/keycloak_role_check/keycloak_role_check_spec.lua

+90
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe('Keycloak Role check policy', function()
77
ngx.header = {}
88
stub(ngx, 'print')
99

10+
stub(ngx.req, 'get_method', function() return 'GET' end)
1011
-- avoid stubbing all the ngx.var.* and ngx.req.* in the available context
1112
stub(ngx_variable, 'available_context', function(context) return context end)
1213
end)
@@ -572,6 +573,95 @@ describe('Keycloak Role check policy', function()
572573
assert.not_same(ngx.status, 403)
573574
end)
574575
end)
576+
577+
describe("validate method roles", function()
578+
579+
before_each(function()
580+
ngx.var = {
581+
uri = '/foo'
582+
}
583+
end)
584+
585+
local context = {
586+
jwt = {
587+
realm_access = {
588+
roles = { "known_role" }
589+
},
590+
resource_access = {
591+
known_client = {
592+
roles = { "role_of_known_client" }
593+
}
594+
}
595+
},
596+
service = {
597+
auth_failed_status = 403,
598+
error_auth_failed = "auth failed"
599+
}
600+
}
601+
602+
it("Validates that an empty array still make a default any method", function()
603+
local role_check_policy = KeycloakRoleCheckPolicy.new({
604+
scopes = {{
605+
client_roles = { { name = "role_of_known_client", client = "known_client" } },
606+
resource = "/foo",
607+
methods = {}
608+
}}})
609+
role_check_policy:access(context)
610+
assert.same(ngx.status, nil)
611+
end)
612+
613+
it("when jwt role matches and one method also match", function()
614+
local role_check_policy = KeycloakRoleCheckPolicy.new({
615+
scopes = {{
616+
client_roles = { { name = "role_of_known_client", client = "known_client" } },
617+
resource = "/foo",
618+
methods = {"GET", "POST", "PUT"}
619+
}},
620+
type = "whitelist"})
621+
622+
role_check_policy:access(context)
623+
assert.same(ngx.status, nil)
624+
end)
625+
626+
it("when jwt role matches and one method also match with blacklist mode", function()
627+
local role_check_policy = KeycloakRoleCheckPolicy.new({
628+
scopes = {{
629+
client_roles = { { name = "role_of_known_client", client = "known_client" } },
630+
resource = "/foo",
631+
methods = {"GET", "POST", "PUT"}
632+
}},
633+
type = "blacklist"})
634+
635+
role_check_policy:access(context)
636+
assert.same(ngx.status, 403)
637+
end)
638+
639+
it("when jwt role matches and no methods matches", function()
640+
local role_check_policy = KeycloakRoleCheckPolicy.new({
641+
scopes = {{
642+
client_roles = { { name = "role_of_known_client", client = "known_client" } },
643+
resource = "/foo",
644+
methods = {"POST", "PUT"}
645+
}},
646+
type = "whitelist"})
647+
role_check_policy:access(context)
648+
assert.same(ngx.status, 403)
649+
end)
650+
651+
it("when jwt role matches and no methods matches with blacklist mode", function()
652+
local role_check_policy = KeycloakRoleCheckPolicy.new({
653+
scopes = {{
654+
client_roles = { { name = "role_of_known_client", client = "known_client" } },
655+
resource = "/foo",
656+
methods = {"POST", "PUT"}
657+
}},
658+
type = "blacklist"})
659+
role_check_policy:access(context)
660+
assert.same(ngx.status, nil)
661+
end)
662+
663+
end)
664+
575665
end)
576666
end)
577667
end)

0 commit comments

Comments
 (0)