Skip to content

Commit 9ed4a93

Browse files
committed
- Added XACML Request template file (request.xacml.json.ftl) to
assembled package (tar.gz) - CombinedXacmlAclAuthorizer class: - better javadoc - better logging - XACML Request template loaded from file location (instead of string in Java property)
1 parent 0faa874 commit 9ed4a93

File tree

5 files changed

+85
-50
lines changed

5 files changed

+85
-50
lines changed

README.md

+2-10
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ To enable the authorizer on Kafka, set the server's property:
2727

2828
To enable XACML evaluation, set the extra following authorizer properties:
2929
* **`org.ow2.authzforce.kafka.pep.xacml.pdp.url`**: XACML PDP resource's URL, as defined by [REST Profile of XACML 3.0](http://docs.oasis-open.org/xacml/xacml-rest/v1.0/xacml-rest-v1.0.html), §2.2.2, e.g. `https://serverhostname/services/pdp` for a [AuthzForce RESTful PDP](https://github.com/authzforce/restful-pdp) instance, or `https://serverhostname/authzforce-ce/domains/XXX/pdp` for a domain `XXX` on a [AuthzForce Server](https://github.com/authzforce/server) instance.
30-
* **`org.ow2.authzforce.kafka.pep.xacml.req.tmpl`:** [Freemarker](https://freemarker.apache.org/) template of XACML Request formatted according to [JSON Profile of XACML 3.0](http://docs.oasis-open.org/xacml/xacml-json-http/v1.0/xacml-json-http-v1.0.html), in which you can use [Freemarker expressions](https://freemarker.apache.org/docs/dgui_template_exp.html), enclosed between `${` and `}`, and have access to the following [top-level variables](https://freemarker.apache.org/docs/dgui_template_exp.html#dgui_template_exp_var_toplevel) from Kafka's authorization context:
30+
* **`org.ow2.authzforce.kafka.pep.xacml.req.tmpl.location`:** location of a file that contains a [Freemarker](https://freemarker.apache.org/) template of XACML Request formatted according to [JSON Profile of XACML 3.0](http://docs.oasis-open.org/xacml/xacml-json-http/v1.0/xacml-json-http-v1.0.html), in which you can use [Freemarker expressions](https://freemarker.apache.org/docs/dgui_template_exp.html), enclosed between `${` and `}`, and have access to the following [top-level variables](https://freemarker.apache.org/docs/dgui_template_exp.html#dgui_template_exp_var_toplevel) from Kafka's authorization context:
3131

3232
| Variable name | Variable type | Description |
3333
| --- | --- | --- |
@@ -37,15 +37,7 @@ To enable XACML evaluation, set the extra following authorizer properties:
3737
|`resourceType`|[org.apache.kafka.common.resource.ResourceType](https://kafka.apache.org/11/javadoc/org/apache/kafka/common/resource/ResourceType.html)|resource type|
3838
|`resourceName`|`String`|resource name|
3939

40-
For example:
41-
42-
```json
43-
org.ow2.authzforce.kafka.pep.xacml.req.tmpl={"Request"\:{"Category"\:[{"CategoryId"\:"urn\:oasis\:names\:tc\:xacml\:1.0\:subject-category\:access-subject","Attribute"\:[{"AttributeId"\:"urn\:oasis\:names\:tc\:xacml\:1.0\:subject\:subject-id","DataType"\:"http\://www.w3.org/2001/XMLSchema#string","Value"\:"${principal.name}"},{"AttributeId"\:"urn\:oasis\:names\:tc\:xacml\:1.0\:subject\:authn-locality\:dns-name","DataType"\:"urn\:oasis\:names\:tc\:xacml\:2.0\:data-type\:dnsName","Value"\:"${clientHost.hostName}"},{"AttributeId"\:"urn\:oasis\:names\:tc\:xacml\:3.0\:subject\:authn-locality\:ip-address","DataType"\:"urn\:oasis\:names\:tc\:xacml\:2.0\:data-type\:ipAddress","Value"\:"${clientHost.hostAddress}"}]},{"CategoryId"\:"urn\:oasis\:names\:tc\:xacml\:3.0\:attribute-category\:action","Attribute"\:[{"AttributeId"\:"urn\:oasis\:names\:tc\:xacml\:1.0\:action\:action-id","DataType"\:"http\://www.w3.org/2001/XMLSchema#string","Value"\:"${operation}",}]},{"CategoryId"\:"urn\:oasis\:names\:tc\:xacml\:3.0\:attribute-category\:resource","Attribute"\:[{"AttributeId"\:"urn\:thalesgroup\:xacml\:resource\:resource-type","DataType"\:"http\://www.w3.org/2001/XMLSchema#string","Value"\:"${resourceType}"},{"AttributeId"\:"urn\:oasis\:names\:tc\:xacml\:1.0\:resource\:resource-id","DataType"\:"http\://www.w3.org/2001/XMLSchema#string","Value"\:"${resourceName}"}]},{"CategoryId"\:"urn\:oasis\:names\:tc\:xacml\:3.0\:attribute-category\:environment","Attribute"\:[{"AttributeId"\:"urn\:thalesgroup\:xacml\:environment\:deployment-environment","DataType"\:"http\://www.w3.org/2001/XMLSchema#string","Value"\:"DEV"}]}]}}
44-
```
45-
46-
This example is derived from the [template in the source](src/test/resources/request.xacml.json.ftl), i.e. adapted for the Java Properties format, and should be applicable to most cases.
47-
48-
As shown in this example, the property value must be formatted according to [Java Properties API](https://docs.oracle.com/javase/8/docs/api/index.html?java/util/Properties.html). In particular, you must **either compact your JSON template on one line; or on multiple lines but only if you terminate each line with a backslash as mentioned on [Java Properties#load(Reader) API](https://docs.oracle.com/javase/8/docs/api/java/util/Properties.html#load-java.io.Reader-). You must also escape all ':' with backslash**, because ':' is a special character (like '=') in Java properties file format.
40+
For an example of XACML Request template, see the file `request.xacml.json.ftl` in the [source](src/test/resources/request.xacml.json.ftl) or in the same folder as this README if part of a release package (tar.gz). This example should be sufficient for most cases.
4941

5042
## Starting Kafka
5143
Make sure Zookeeper is started first:

assembly.xml

+8-8
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<dependencySet>
1111
<useProjectArtifact>true</useProjectArtifact>
1212
<excludes>
13-
<!-- Already provided with Kafka -->
13+
<!-- Already provided with Kafka -->
1414
<exclude>org.slf4j:slf4j-api</exclude>
1515
</excludes>
1616
<outputDirectory>lib</outputDirectory>
@@ -26,12 +26,12 @@
2626
<include>NOTICE*</include>
2727
</includes>
2828
</fileSet>
29-
<!-- <fileSet> -->
30-
<!-- <directory>etc</directory> -->
31-
<!-- <outputDirectory></outputDirectory> -->
32-
<!-- <includes> -->
33-
<!-- <include>startup.*</include> -->
34-
<!-- </includes> -->
35-
<!-- </fileSet> -->
29+
<fileSet>
30+
<directory>${project.basedir}/src/test/resources</directory>
31+
<outputDirectory>/</outputDirectory>
32+
<includes>
33+
<include>request.xacml.json.ftl</include>
34+
</includes>
35+
</fileSet>
3636
</fileSets>
3737
</assembly>

pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<version>7.2.0</version>
1010
</parent>
1111
<artifactId>authzforce-ce-kafka-extensions</artifactId>
12-
<version>0.1.0-SNAPSHOT</version>
12+
<version>0.2.0</version>
1313
<name>AuthzForce CE - Extensions for Apache Kafka</name>
1414
<repositories>
1515
<repository>
@@ -205,4 +205,4 @@
205205
</plugins>
206206
</build>
207207
<description>Includes Authorizer (KIP-11) implementation supporting both Kafka ACL and XACML policy evaluation for Attribute-Based Access Control</description>
208-
</project>
208+
</project>

src/main/java/org/ow2/authzforce/kafka/pep/CombinedXacmlAclAuthorizer.java

+70-21
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
*/
1818
package org.ow2.authzforce.kafka.pep;
1919

20+
import java.io.FileNotFoundException;
2021
import java.io.IOException;
2122
import java.io.StringReader;
2223
import java.io.StringWriter;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
2326
import java.util.Collections;
2427
import java.util.Map;
25-
26-
import javax.ws.rs.core.MediaType;
28+
import java.util.stream.Collectors;
2729

2830
import org.apache.cxf.ext.logging.LoggingFeature;
2931
import org.apache.cxf.feature.Feature;
@@ -34,6 +36,7 @@
3436
import org.ow2.authzforce.xacml.json.model.Xacml3JsonUtils;
3537
import org.slf4j.Logger;
3638
import org.slf4j.LoggerFactory;
39+
import org.springframework.util.ResourceUtils;
3740

3841
import com.google.common.collect.ImmutableMap;
3942

@@ -42,23 +45,46 @@
4245
import freemarker.template.Template;
4346
import freemarker.template.TemplateExceptionHandler;
4447
import kafka.network.RequestChannel.Session;
48+
import kafka.security.auth.Authorizer;
4549
import kafka.security.auth.Operation;
4650
import kafka.security.auth.Resource;
4751
import kafka.security.auth.SimpleAclAuthorizer;
4852

53+
/**
54+
* Combined ACL and XACML-based {@link Authorizer} for Apache Kafka. Gets authorization decisions from a XACML PDP's REST API - as defined by OASIS standard 'REST Profile of XACML 3.0' - iff Kafka ACL
55+
* (evaluated by {@link SimpleAclAuthorizer}) returns Deny. To enable XACML authorization, you need to set two extra configuration properties:
56+
* <ul>
57+
* <li>{@value #XACML_PDP_URL_CFG_PROPERTY_NAME}: XACML PDP resource's URL, as defined by <a href="http://docs.oasis-open.org/xacml/xacml-rest/v1.0/xacml-rest-v1.0.html">REST Profile of XACML 3.0</a>,
58+
* §2.2.2, e.g. {@code https://serverhostname/services/pdp}</li>
59+
* <li>{@value #XACML_REQUEST_TEMPLATE_LOCATION_CFG_PROPERTY_NAME}: location of a file that contains a <a href="https://freemarker.apache.org/">Freemarker</a> template of XACML Request formatted
60+
* according to <a href="http://docs.oasis-open.org/xacml/xacml-json-http/v1.0/xacml-json-http-v1.0.html">JSON Profile of XACML 3.0</a>, in which you can use
61+
* <a href="https://freemarker.apache.org/docs/dgui_template_exp.html">Freemarker expressions</a>, enclosed between <code>${</code> and <code>}</code>, and have access to the following
62+
* <a href="https://freemarker.apache.org/docs/dgui_template_exp.html#dgui_template_exp_var_toplevel">top-level variables</a> from Kafka's authorization context:
63+
* <ul>
64+
* <li><code>clientHost</code> ({@link java.net.InetAddress}): client/user host name or IP address</li>
65+
* <li><code>principal</code> ({@link org.apache.kafka.common.security.auth.KafkaPrincipal}): user principal</li>
66+
* <li><code>operation</code> ({@link org.apache.kafka.common.acl.AclOperation}): operation</li>
67+
* <li><code>resourceType</code> ({@link org.apache.kafka.common.resource.ResourceType}): resource type</li>
68+
* <li><code>resourceName</code> ({@link String}): resource name</li>
69+
* </ul>
70+
* </li>
71+
* </ul>
72+
*/
4973
public class CombinedXacmlAclAuthorizer extends SimpleAclAuthorizer
5074
{
5175
private static final Logger LOGGER = LoggerFactory.getLogger(CombinedXacmlAclAuthorizer.class);
5276

77+
private static final String XACML_JSON_MEDIA_TYPE = "application/xacml+json";
78+
5379
/**
5480
* Name of Kafka configuration property specifying the RESTful XACML PDP resource's URL (e.g. https://services.example.com/pdp), as defined by REST Profile of XACML, §2.2.2
5581
*/
56-
public static final String XACML_PDP_URL = "org.ow2.authzforce.kafka.pep.xacml.pdp.url";
82+
public static final String XACML_PDP_URL_CFG_PROPERTY_NAME = "org.ow2.authzforce.kafka.pep.xacml.pdp.url";
5783

5884
/**
59-
* Name of Kafka configuration property specifying the XACML Request template
85+
* Name of Kafka configuration property specifying the location to XACML Request template file. The location must be a URL resolvable by {@link ResourceUtils}.
6086
*/
61-
public static final String XACML_REQUEST_TEMPLATE_CFG_PROPERTY_NAME = "org.ow2.authzforce.kafka.pep.xacml.req.tmpl";
87+
public static final String XACML_REQUEST_TEMPLATE_LOCATION_CFG_PROPERTY_NAME = "org.ow2.authzforce.kafka.pep.xacml.req.tmpl.location";
6288

6389
private static final int MAX_JSON_STRING_LENGTH = 1000;
6490

@@ -80,39 +106,62 @@ public void configure(Map<String, ?> authorizerProperties)
80106
{
81107
super.configure(authorizerProperties);
82108

83-
final Object xacmlPdpUrlObj = authorizerProperties.get(XACML_PDP_URL);
109+
final Object xacmlPdpUrlObj = authorizerProperties.get(XACML_PDP_URL_CFG_PROPERTY_NAME);
84110
if (xacmlPdpUrlObj == null)
85111
{
86-
LOGGER.info("Configuration property '{}' undefined -> XACML evaluation disabled, KAFKA ACL enabled only.", XACML_PDP_URL);
112+
LOGGER.info("Configuration property '{}' undefined -> XACML evaluation disabled, KAFKA ACL enabled only.", XACML_PDP_URL_CFG_PROPERTY_NAME);
87113
return;
88114
}
89115

90116
if (!(xacmlPdpUrlObj instanceof String))
91117
{
92-
throw new IllegalArgumentException(this + ": authorizer configuration property '" + XACML_PDP_URL + "' is not a String");
118+
throw new IllegalArgumentException(this + ": authorizer configuration property '" + XACML_PDP_URL_CFG_PROPERTY_NAME + "' is not a String");
93119
}
94120

95121
final String xacmlPdpUrlStr = (String) xacmlPdpUrlObj;
96-
LOGGER.debug("XACML PDP URL set from authorizer configuration property '{}': {}", XACML_PDP_URL, xacmlPdpUrlStr);
122+
LOGGER.debug("XACML PDP URL set from authorizer configuration property '{}': {}", XACML_PDP_URL_CFG_PROPERTY_NAME, xacmlPdpUrlStr);
97123

98124
pdpClient = WebClient
99125
.create(xacmlPdpUrlStr, Collections.singletonList(new JsonRiJaxrsProvider(/* extra parameters */)),
100126
LOGGER.isDebugEnabled() ? Collections.singletonList(new LoggingFeature()) : Collections.<Feature>emptyList(), null /* clientConfClasspathLocation */)
101-
.type(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON_TYPE);
127+
.type(XACML_JSON_MEDIA_TYPE).accept(XACML_JSON_MEDIA_TYPE);
102128

103-
final Object xacmlReqTmplObj = authorizerProperties.get(XACML_REQUEST_TEMPLATE_CFG_PROPERTY_NAME);
129+
final Object xacmlReqTmplObj = authorizerProperties.get(XACML_REQUEST_TEMPLATE_LOCATION_CFG_PROPERTY_NAME);
104130
if (!(xacmlReqTmplObj instanceof String))
105131
{
106-
throw new IllegalArgumentException(this + ": authorizer configuration property '" + XACML_REQUEST_TEMPLATE_CFG_PROPERTY_NAME + "' is missing or not a String");
132+
throw new IllegalArgumentException(this + ": authorizer configuration property '" + XACML_REQUEST_TEMPLATE_LOCATION_CFG_PROPERTY_NAME + "' is missing or not a String");
107133
}
108134

109-
final String xacmlReqTmplStr = (String) xacmlReqTmplObj;
110-
LOGGER.debug("Loading XACML Request template from authorizer configuration property '{}': {}", XACML_REQUEST_TEMPLATE_CFG_PROPERTY_NAME, xacmlReqTmplStr);
135+
final String xacmlReqTmplFileLocation = (String) xacmlReqTmplObj;
136+
LOGGER.debug("Loading XACML Request template from authorizer configuration property '{}': {}", XACML_REQUEST_TEMPLATE_LOCATION_CFG_PROPERTY_NAME, xacmlReqTmplFileLocation);
137+
138+
final Path xacmlReqTmplFile;
139+
try
140+
{
141+
xacmlReqTmplFile = ResourceUtils.getFile(xacmlReqTmplFileLocation).toPath();
142+
}
143+
catch (final FileNotFoundException e)
144+
{
145+
throw new IllegalArgumentException(
146+
"XACML JSON Request template file not found at location ('" + XACML_REQUEST_TEMPLATE_LOCATION_CFG_PROPERTY_NAME + "'=) '" + xacmlReqTmplFileLocation + "'", e);
147+
}
148+
149+
final String xacmlReqTmplStr;
150+
try
151+
{
152+
xacmlReqTmplStr = Files.lines(xacmlReqTmplFile).collect(Collectors.joining());
153+
}
154+
catch (final IOException e)
155+
{
156+
throw new RuntimeException(
157+
"Error opening XACML JSON Request template file at location ('" + XACML_REQUEST_TEMPLATE_LOCATION_CFG_PROPERTY_NAME + "'=) '" + xacmlReqTmplFileLocation + "'", e);
158+
}
111159

112160
final JSONObject jsonRequest = new LimitsCheckingJSONObject(new StringReader(xacmlReqTmplStr), MAX_JSON_STRING_LENGTH, MAX_JSON_CHILDREN_COUNT, MAX_JSON_DEPTH);
113161
if (!jsonRequest.has("Request"))
114162
{
115-
throw new IllegalArgumentException("Invalid XACML JSON Request file specified by '" + XACML_REQUEST_TEMPLATE_CFG_PROPERTY_NAME + "'. Root key is not 'Request' as expected.");
163+
throw new IllegalArgumentException("Invalid XACML JSON Request template file at location ('" + XACML_REQUEST_TEMPLATE_LOCATION_CFG_PROPERTY_NAME + "'=) '" + xacmlReqTmplFile
164+
+ "': root key is not 'Request' as expected.");
116165
}
117166

118167
Xacml3JsonUtils.REQUEST_SCHEMA.validate(jsonRequest);
@@ -162,17 +211,16 @@ public boolean authorize(Session session, Operation operation, Resource resource
162211
final boolean simpleAclAuthorized = super.authorize(session, operation, resource);
163212

164213
/*
165-
* TODO: define combining algorithm for combining simple ACLs with XACML eval. For now, we do deny unless permit, which is the easiest to implement because it takes into account the
166-
* isSuperUser() and isEmptyAclAndAuthorized()
214+
* We do deny-unless-permit combining between ACL and XACML evaluation, which is the easiest to implement because it takes into account the isSuperUser() and isEmptyAclAndAuthorized().
167215
*/
168216
if (simpleAclAuthorized || this.pdpClient == null)
169217
{
170218
return simpleAclAuthorized;
171219
}
172220
/*
173-
* Denied by ACL and pdpClient != null. Is it denied by PDP?
221+
* Denied by ACL and pdpClient != null. Is it denied by XACML PDP?
174222
*/
175-
LOGGER.debug("Authorization denied by SimpleAclAuthorizer. Trying XACML evaluation...");
223+
LOGGER.debug("Authorization denied by SimpleAclAuthorizer. Trying evaluation by XACML PDP...");
176224
final Map<String, Object> root = ImmutableMap.of("clientHost", session.clientAddress(), "principal", session.principal(), "operation", operation.toJava(), "resourceType",
177225
resource.resourceType().toJava(), "resourceName", resource.name());
178226
final StringWriter out = new StringWriter();
@@ -187,12 +235,13 @@ public boolean authorize(Session session, Operation operation, Resource resource
187235
}
188236

189237
final String xacmlReq = out.toString();
190-
LOGGER.debug("Calling PDP with client = {}, XACML request: {}", this.pdpClient, xacmlReq);
191238
final JSONObject jsonRequest = new JSONObject(xacmlReq);
192239
final JSONObject jsonResponse = pdpClient.post(jsonRequest, JSONObject.class);
193240
Xacml3JsonUtils.RESPONSE_SCHEMA.validate(jsonResponse);
194241
final String decision = jsonResponse.getJSONArray("Response").getJSONObject(0).getString("Decision");
195-
return decision.equals("Permit");
242+
final boolean isAuthorized = decision.equals("Permit");
243+
LOGGER.debug("isAuthorized (true iff Permit) = {}", isAuthorized);
244+
return isAuthorized;
196245
}
197246

198247
}

src/test/java/org/ow2/authzforce/kafka/pep/test/CombinedXacmlAclAutorizerTest.java

+3-9
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,11 @@
1818

1919
package org.ow2.authzforce.kafka.pep.test;
2020

21-
import java.io.File;
2221
import java.io.IOException;
2322
import java.net.InetAddress;
2423
import java.net.UnknownHostException;
25-
import java.nio.file.Files;
2624
import java.util.Map;
2725
import java.util.Set;
28-
import java.util.stream.Collectors;
2926

3027
import org.apache.kafka.common.acl.AclOperation;
3128
import org.apache.kafka.common.resource.ResourceType;
@@ -41,7 +38,6 @@
4138
import org.springframework.boot.test.context.SpringBootTest;
4239
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
4340
import org.springframework.test.context.junit4.SpringRunner;
44-
import org.springframework.util.ResourceUtils;
4541

4642
import com.google.common.collect.ImmutableMap;
4743
import com.google.common.collect.ImmutableSet;
@@ -103,11 +99,9 @@ public CombinedXacmlAclAutorizerTest() throws UnknownHostException
10399
@Before
104100
public void setUp() throws IOException
105101
{
106-
final File xacmlReqTmplFile = ResourceUtils.getFile(XACML_REQ_TMPL_LOCATION);
107-
108-
final String xacmlReqTmplStr = Files.lines(xacmlReqTmplFile.toPath()).collect(Collectors.joining());
109-
authorizer.configure(ImmutableMap.of(KafkaConfig.ZkConnectProp(), SHARED_ZOOKEEPER_TEST_RESOURCE.getZookeeperConnectString(), CombinedXacmlAclAuthorizer.XACML_PDP_URL,
110-
"http://localhost:" + port + "/services/pdp", CombinedXacmlAclAuthorizer.XACML_REQUEST_TEMPLATE_CFG_PROPERTY_NAME, xacmlReqTmplStr));
102+
authorizer.configure(ImmutableMap.of(KafkaConfig.ZkConnectProp(), SHARED_ZOOKEEPER_TEST_RESOURCE.getZookeeperConnectString(), CombinedXacmlAclAuthorizer.XACML_PDP_URL_CFG_PROPERTY_NAME,
103+
"http://localhost:" + port + "/services/pdp" /* "http://localhost:8080/authzforce-ce/domains/A0bdIbmGEeWhFwcKrC9gSQ/pdp" */,
104+
CombinedXacmlAclAuthorizer.XACML_REQUEST_TEMPLATE_LOCATION_CFG_PROPERTY_NAME, XACML_REQ_TMPL_LOCATION));
111105
}
112106

113107
private void testAuthorization(String username, boolean expectedAuthorized)

0 commit comments

Comments
 (0)