Skip to content

Commit 3aa900c

Browse files
kroxylicious-authorization: A Filter authorizing metadata only (#2909)
* Adds a new filter which will eventually be able to apply equivalent authorization as a Kafka broker * The general pattern which will be applied with be to have an `Enforcement` class for each API, which knows how to handle that API (e.g. knows where the topic names are in the `RequestData`). * There is a `Passthrough` class which is use for APIs which do not require enforcement (because they contain no resource names) * The filter is designed to "fail closed", so any API key, or API version, which it doesn't know about will be rejected. This protects us from future evolution of the protocol result in accidantal information disclouse (e.g. imagine a new API version adding a topic name and an attack being able to infer the existence of topic from the response), but means we're on the hook to add/update `Enforcement` as the protocol evolved * It this commit we only actually apply topic authorization to `Metadata` requests. Obviously all the clients need metadata to do just about anything. * `Metadata` is the hardest API to apply authorization to (don't worry the other's won't be this hard). * That's because requests can have the side effect of creating topics, so care needs to be taken to not create a topic which the user is not authorized by the filter to create. This means that for some client requests we need to make several requests to the broker. * Also included are ITs which check for "Kafka equivalence" using the low level `KafkaClient` to make requests and comparing the responses with the same requests made to a reference (unproxied) Kafka cluster which is configured with equivalent authorization rules. * A later PR will add a higher level test using the Apache Kafka client APIs to ensure that logic of the filter is not interfering with the correct functioning of clients. Signed-off-by: Tom Bentley <tbentley@redhat.com> Signed-off-by: Robert Young <robertyoungnz@gmail.com> Co-authored-by: Robert Young <robertyoungnz@gmail.com>
1 parent 6e7731a commit 3aa900c

88 files changed

Lines changed: 8668 additions & 52 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

kroxylicious-app/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,16 @@
386386
<artifactId>kroxylicious-sasl-inspection</artifactId>
387387
<scope>runtime</scope>
388388
</dependency>
389+
<dependency>
390+
<groupId>io.kroxylicious</groupId>
391+
<artifactId>kroxylicious-authorization</artifactId>
392+
<scope>runtime</scope>
393+
</dependency>
394+
<dependency>
395+
<groupId>io.kroxylicious</groupId>
396+
<artifactId>kroxylicious-authorizer-acl</artifactId>
397+
<scope>runtime</scope>
398+
</dependency>
389399
</dependencies>
390400
</profile>
391401
</profiles>

kroxylicious-authorizer-providers/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
<name>Authorizer provider implementations</name>
2222
<artifactId>kroxylicious-authorizer-providers</artifactId>
23+
<description>Authorizer provider implementations</description>
2324
<packaging>pom</packaging>
2425

2526
<modules>

kroxylicious-bom/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@
177177
<version>${project.version}</version>
178178
</dependency>
179179

180+
<dependency>
181+
<groupId>io.kroxylicious</groupId>
182+
<artifactId>kroxylicious-authorization</artifactId>
183+
<version>${project.version}</version>
184+
</dependency>
185+
180186
<dependency>
181187
<groupId>io.kroxylicious</groupId>
182188
<artifactId>kroxylicious-kafka-message-tools</artifactId>

kroxylicious-filter-test-support/src/main/java/io/kroxylicious/test/RequestFactory.java

Lines changed: 72 additions & 42 deletions
Large diffs are not rendered by default.
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Copyright Kroxylicious Authors.
5+
6+
Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
7+
8+
-->
9+
10+
<project xmlns="http://maven.apache.org/POM/4.0.0"
11+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
12+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
13+
<modelVersion>4.0.0</modelVersion>
14+
<parent>
15+
<groupId>io.kroxylicious</groupId>
16+
<artifactId>kroxylicious-filter-parent</artifactId>
17+
<version>0.18.0-SNAPSHOT</version>
18+
<relativePath>../pom.xml</relativePath>
19+
</parent>
20+
21+
<artifactId>kroxylicious-authorization</artifactId>
22+
<packaging>jar</packaging>
23+
<name>Authorization Filter</name>
24+
<description>A filter providing access controls for Kafka entities such as Topics</description>
25+
26+
<dependencies>
27+
<!-- project dependencies - runtime and compile -->
28+
<dependency>
29+
<groupId>io.kroxylicious</groupId>
30+
<artifactId>kroxylicious-api</artifactId>
31+
</dependency>
32+
<dependency>
33+
<groupId>io.kroxylicious</groupId>
34+
<artifactId>kroxylicious-annotations</artifactId>
35+
</dependency>
36+
<dependency>
37+
<groupId>io.kroxylicious</groupId>
38+
<artifactId>kroxylicious-authorizer-api</artifactId>
39+
</dependency>
40+
<dependency>
41+
<groupId>org.apache.kafka</groupId>
42+
<artifactId>kafka-clients</artifactId>
43+
</dependency>
44+
<dependency>
45+
<groupId>com.fasterxml.jackson.core</groupId>
46+
<artifactId>jackson-annotations</artifactId>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.slf4j</groupId>
50+
<artifactId>slf4j-api</artifactId>
51+
</dependency>
52+
<dependency>
53+
<groupId>com.github.spotbugs</groupId>
54+
<artifactId>spotbugs-annotations</artifactId>
55+
</dependency>
56+
<dependency>
57+
<groupId>org.junit.jupiter</groupId>
58+
<artifactId>junit-jupiter-api</artifactId>
59+
<scope>test</scope>
60+
</dependency>
61+
<dependency>
62+
<groupId>com.google.guava</groupId>
63+
<artifactId>guava</artifactId>
64+
<scope>test</scope>
65+
</dependency>
66+
<dependency>
67+
<groupId>com.fasterxml.jackson.core</groupId>
68+
<artifactId>jackson-databind</artifactId>
69+
<scope>test</scope>
70+
</dependency>
71+
<dependency>
72+
<groupId>com.fasterxml.jackson.core</groupId>
73+
<artifactId>jackson-core</artifactId>
74+
<scope>test</scope>
75+
</dependency>
76+
<dependency>
77+
<groupId>com.fasterxml.jackson.dataformat</groupId>
78+
<artifactId>jackson-dataformat-yaml</artifactId>
79+
<scope>test</scope>
80+
</dependency>
81+
<dependency>
82+
<groupId>org.assertj</groupId>
83+
<artifactId>assertj-core</artifactId>
84+
</dependency>
85+
<dependency>
86+
<groupId>org.mockito</groupId>
87+
<artifactId>mockito-core</artifactId>
88+
<scope>test</scope>
89+
</dependency>
90+
<dependency>
91+
<groupId>org.junit.jupiter</groupId>
92+
<artifactId>junit-jupiter-params</artifactId>
93+
<scope>test</scope>
94+
</dependency>
95+
<dependency>
96+
<groupId>io.kroxylicious</groupId>
97+
<artifactId>kroxylicious-filter-test-support</artifactId>
98+
<scope>test</scope>
99+
</dependency>
100+
<dependency>
101+
<groupId>org.mockito</groupId>
102+
<artifactId>mockito-junit-jupiter</artifactId>
103+
<scope>test</scope>
104+
</dependency>
105+
106+
</dependencies>
107+
108+
</project>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
package io.kroxylicious.filter.authorization;
8+
9+
import java.util.Collection;
10+
import java.util.List;
11+
import java.util.concurrent.CompletionStage;
12+
import java.util.function.Function;
13+
14+
import org.apache.kafka.common.message.RequestHeaderData;
15+
import org.apache.kafka.common.message.ResponseHeaderData;
16+
import org.apache.kafka.common.protocol.ApiMessage;
17+
18+
import io.kroxylicious.authorizer.service.ResourceType;
19+
import io.kroxylicious.proxy.filter.FilterContext;
20+
import io.kroxylicious.proxy.filter.RequestFilterResult;
21+
import io.kroxylicious.proxy.filter.ResponseFilterResult;
22+
23+
/**
24+
* <p>Enforces authorization rules for a range of versions of a single API key.</p>
25+
*
26+
* <p> If the request contains references
27+
* to access-controlled entities (such as topics) then {@link AuthorizationFilter#authorization(FilterContext, List)}
28+
* and {@link io.kroxylicious.authorizer.service.AuthorizeResult#partition(Collection, ResourceType, Function)}
29+
* should be used to partition the entities into a subset which can be forwarded to the broker, and a subset
30+
* which need to be rejected with some kind of authorization errors. When both of these sets are
31+
* non-empty it will be necessary to also handle the response from the broker to add the
32+
* locally generated subset of entities with authorization errors to the responses for the entities which
33+
* were forwarded to the broker.</p>
34+
*
35+
* <p>{@link AuthorizationFilter#pushInflightState(RequestHeaderData, InflightState)} can be used to
36+
* attach some state to a request that's going to be forwarded, and
37+
* {@link AuthorizationFilter#popAndApplyInflightState(ResponseHeaderData, Object)} is provided to easily
38+
* perform the merging of the locally-generated and upstream responses.</p>
39+
*
40+
* @param <Q> The request type.
41+
* @param <S> The response type.
42+
*/
43+
abstract class ApiEnforcement<Q extends ApiMessage, S extends ApiMessage> {
44+
45+
/**
46+
* @return The inclusive minimum version of the range of versions supported
47+
*/
48+
abstract short minSupportedVersion();
49+
50+
/**
51+
* @return The inclusive maximum version of the range of versions supported
52+
*/
53+
abstract short maxSupportedVersion();
54+
55+
/**
56+
* <p>Performs the necessary authorization of the request.</p>
57+
* @param header The request header.
58+
* @param request The request body.
59+
* @param context The filter context.
60+
* @param authorizationFilter The authorization filter.
61+
* @return The filter result.
62+
*/
63+
abstract CompletionStage<RequestFilterResult> onRequest(RequestHeaderData header,
64+
Q request,
65+
FilterContext context,
66+
AuthorizationFilter authorizationFilter);
67+
68+
/**
69+
* <p>Performs the necessary authorization or handling of the response.<p>
70+
* @param header The request header.
71+
* @param response The response body.
72+
* @param context The filter context.
73+
* @param authorizationFilter The authorization filter.
74+
* @return The filter result.
75+
*/
76+
CompletionStage<ResponseFilterResult> onResponse(ResponseHeaderData header,
77+
S response,
78+
FilterContext context,
79+
AuthorizationFilter authorizationFilter) {
80+
return context.forwardResponse(header, authorizationFilter.popAndApplyInflightState(header, response));
81+
}
82+
83+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
package io.kroxylicious.filter.authorization;
8+
9+
import java.util.HashSet;
10+
import java.util.Set;
11+
import java.util.stream.Collectors;
12+
13+
import io.kroxylicious.authorizer.service.Authorizer;
14+
import io.kroxylicious.authorizer.service.AuthorizerService;
15+
import io.kroxylicious.proxy.filter.Filter;
16+
import io.kroxylicious.proxy.filter.FilterFactory;
17+
import io.kroxylicious.proxy.filter.FilterFactoryContext;
18+
import io.kroxylicious.proxy.plugin.Plugin;
19+
import io.kroxylicious.proxy.plugin.PluginConfigurationException;
20+
import io.kroxylicious.proxy.plugin.Plugins;
21+
22+
import edu.umd.cs.findbugs.annotations.NonNull;
23+
import edu.umd.cs.findbugs.annotations.Nullable;
24+
25+
/**
26+
* The FilterFactory (service) for the {@link AuthorizationFilter}.
27+
*/
28+
@Plugin(configType = AuthorizationConfig.class)
29+
public class Authorization implements FilterFactory<AuthorizationConfig, Authorizer> {
30+
31+
private @Nullable AuthorizerService<?> authorizerService = null;
32+
33+
@SuppressWarnings({ "unchecked", "rawtypes" })
34+
@Override
35+
public Authorizer initialize(FilterFactoryContext context,
36+
AuthorizationConfig authorizationConfig)
37+
throws PluginConfigurationException {
38+
var configuration = Plugins.requireConfig(this, authorizationConfig);
39+
this.authorizerService = context.pluginInstance(AuthorizerService.class, configuration.authorizer());
40+
((AuthorizerService) authorizerService).initialize(configuration.authorizerConfig());
41+
42+
Authorizer authorizer = authorizerService.build();
43+
authorizer.supportedResourceTypes().ifPresent(usedTypes -> {
44+
var unsupportedResourceTypes = new HashSet<>(usedTypes);
45+
unsupportedResourceTypes.removeAll(Set.of(TopicResource.class));
46+
if (!unsupportedResourceTypes.isEmpty()) {
47+
throw new PluginConfigurationException(("%s specifies access controls for resource types which cannot be enforced by this filter. "
48+
+ "The unsupported types are: %s.").formatted(
49+
configuration.authorizer(),
50+
unsupportedResourceTypes.stream().map(Class::getName).collect(Collectors.joining(", "))));
51+
}
52+
});
53+
return authorizer;
54+
}
55+
56+
@Override
57+
@SuppressWarnings("java:S2638") // Tightening UnknownNullness
58+
public Filter createFilter(FilterFactoryContext context, @NonNull Authorizer authorizer) {
59+
return new AuthorizationFilter(authorizer);
60+
}
61+
62+
@Override
63+
@SuppressWarnings("java:S2638") // Tightening UnknownNullness
64+
public void close(@NonNull Authorizer authorizer) {
65+
if (authorizerService != null) {
66+
authorizerService.close();
67+
}
68+
}
69+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright Kroxylicious Authors.
3+
*
4+
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
7+
package io.kroxylicious.filter.authorization;
8+
9+
import com.fasterxml.jackson.annotation.JsonProperty;
10+
11+
import io.kroxylicious.authorizer.service.AuthorizerService;
12+
import io.kroxylicious.proxy.plugin.PluginImplConfig;
13+
import io.kroxylicious.proxy.plugin.PluginImplName;
14+
15+
/**
16+
* The configuration for the {@link Authorization} service.
17+
* @param authorizer The class name of the {@link AuthorizerService} implementation to use.
18+
* @param authorizerConfig The configuration object for the given {@link AuthorizerService}.
19+
*/
20+
public record AuthorizationConfig(
21+
@JsonProperty(required = true) @PluginImplName(AuthorizerService.class) String authorizer,
22+
@PluginImplConfig(implNameProperty = "authorizer") Object authorizerConfig) {
23+
24+
}

0 commit comments

Comments
 (0)