Skip to content

Commit 1add68a

Browse files
authored
kroxylicious-authorizer: Add the Authorizer API (kroxylicious#2899)
* kroxylicious-authorizer: Add the Authorizer API * This is a new interface intended for use as a service in Filters which do authorization. * This API is intentionally quite abstract. * It does not prescribe any particular type of access control. e.g. it could be implemented using ACLs or RBAC. * The evaluation could be in-process, or perhaps externalised (think https://www.openpolicyagent.org/ or https://openfga.dev/). * Nor does it prescribe what things are having access controlled. * A future PR will add an implementation of this API in terms of access control lists described in file referenced from the filter configuration. However, this API is very intentionally not limited to that implementation. * A another future PR will add such a Filter which implements authorization over the familiar Kafka entities (topics, consumer groups, etc), however this API is very intentionally not limited to that use case. Signed-off-by: Tom Bentley <tbentley@redhat.com>
1 parent b79307a commit 1add68a

13 files changed

Lines changed: 463 additions & 0 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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-parent</artifactId>
17+
<version>0.18.0-SNAPSHOT</version>
18+
<relativePath>../pom.xml</relativePath>
19+
</parent>
20+
21+
<artifactId>kroxylicious-authorizer-api</artifactId>
22+
<name>Authorizer Plugin API</name>
23+
24+
<dependencies>
25+
<dependency>
26+
<groupId>io.kroxylicious</groupId>
27+
<artifactId>kroxylicious-annotations</artifactId>
28+
</dependency>
29+
<dependency>
30+
<groupId>com.github.spotbugs</groupId>
31+
<artifactId>spotbugs-annotations</artifactId>
32+
</dependency>
33+
<dependency>
34+
<groupId>io.kroxylicious</groupId>
35+
<artifactId>kroxylicious-api</artifactId>
36+
</dependency>
37+
<dependency>
38+
<groupId>org.junit.jupiter</groupId>
39+
<artifactId>junit-jupiter-api</artifactId>
40+
<scope>test</scope>
41+
</dependency>
42+
<dependency>
43+
<groupId>org.assertj</groupId>
44+
<artifactId>assertj-core</artifactId>
45+
</dependency>
46+
</dependencies>
47+
48+
</project>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.authorizer.service;
8+
9+
import java.util.Objects;
10+
11+
/**
12+
* An action encapsulates an operation on a given resource identified by a name.
13+
* @param operation The operation (and the resource type)
14+
* @param resourceName The resource name
15+
*/
16+
public record Action(
17+
ResourceType<?> operation,
18+
String resourceName) {
19+
20+
public Action {
21+
Objects.requireNonNull(operation);
22+
Objects.requireNonNull(resourceName);
23+
}
24+
25+
@SuppressWarnings({ "rawtypes", "unchecked", "java:S1452" })
26+
public Class<? extends ResourceType<?>> resourceTypeClass() {
27+
return (Class) operation.getClass();
28+
}
29+
30+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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.authorizer.service;
8+
9+
import java.util.Collection;
10+
import java.util.HashMap;
11+
import java.util.List;
12+
import java.util.Map;
13+
import java.util.Objects;
14+
import java.util.function.Function;
15+
import java.util.stream.Collectors;
16+
17+
import io.kroxylicious.proxy.authentication.Subject;
18+
19+
/**
20+
* Represents the outcome of a call to {@link Authorizer#authorize(io.kroxylicious.proxy.authentication.Subject, List)}
21+
* @param allowed The allowed actions.
22+
* @param denied The denied actions.
23+
*/
24+
public record AuthorizeResult(
25+
Subject subject,
26+
List<Action> allowed,
27+
List<Action> denied) {
28+
29+
public AuthorizeResult {
30+
Objects.requireNonNull(allowed);
31+
Objects.requireNonNull(denied);
32+
}
33+
34+
/**
35+
* Returns a list of the names of all the resources which the {@link #subject()}
36+
* is allowed to perform the given {@code operation} on.
37+
* @param operation The operation.
38+
* @return The names of the allowed resources.
39+
*/
40+
public List<String> allowed(ResourceType<?> operation) {
41+
return allowed().stream()
42+
.filter(a -> a.operation().equals(operation))
43+
.map(Action::resourceName)
44+
.toList();
45+
}
46+
47+
/**
48+
* Returns a list of the names of all the resources which the {@link #subject()}
49+
* is denied from performing the given {@code operation} on.
50+
* @param operation The operation.
51+
* @return The names of the denied resources.
52+
*/
53+
public List<String> denied(ResourceType<?> operation) {
54+
return denied().stream()
55+
.filter(a -> a.operation().equals(operation))
56+
.map(Action::resourceName)
57+
.toList();
58+
}
59+
60+
/**
61+
* Returns the decision about whether the given {@link #subject()} is allowed to perform the given
62+
* {@code operation} on the resource with the given {@code resourceName}.
63+
* @param operation The operation that would be performed on the resource.
64+
* @param resourceName The name of the resource that the operation would be performed on.
65+
* @return The decision.
66+
*/
67+
public Decision decision(ResourceType<?> operation, String resourceName) {
68+
return allowed().stream()
69+
.anyMatch(a -> a.operation().equals(operation)
70+
&& a.resourceName().equals(resourceName)) ? Decision.ALLOW : Decision.DENY;
71+
}
72+
73+
/**
74+
* Partitions the given {@code items}, whose names can be obtained via the given {@code toName} function, into two lists
75+
* based on whether the {@link #subject()} is allowed to perform the given {@code operation} on them.
76+
* @param items The items to partition
77+
* @param operation The operation
78+
* @param toName A function that returns the name of each item.
79+
* @return A pair of lists of the items which the subject is allowed to, or denied from, performing the operation on.
80+
* It is guaranteed that there is always an entry for both {@code ALLOW} and {@code DENY} in the returned map.
81+
* @param <T> The type of item.
82+
*/
83+
public <T> Map<Decision, List<T>> partition(Collection<T> items, ResourceType<?> operation, Function<T, String> toName) {
84+
HashMap<Decision, List<T>> byDecision = items.stream().collect(Collectors.groupingBy(
85+
item -> decision(operation, toName.apply(item)),
86+
HashMap::new,
87+
Collectors.toList()));
88+
byDecision.putIfAbsent(Decision.ALLOW, List.of());
89+
byDecision.putIfAbsent(Decision.DENY, List.of());
90+
return byDecision;
91+
}
92+
93+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.authorizer.service;
8+
9+
import java.util.List;
10+
import java.util.Optional;
11+
import java.util.Set;
12+
import java.util.concurrent.CompletionStage;
13+
14+
import io.kroxylicious.proxy.authentication.Subject;
15+
16+
/**
17+
* <p>Abstracts making an allow/deny decision about some {@link Subject} performing some {@link Action} on a resource.
18+
* In other words, this is an access control policy decision point.</p>
19+
*
20+
* <p>{@code Authorizer} is a flexible abstraction, while it assumes that resources have names, it
21+
* doesn't prescribe any specific kinds of resource or operations. Instead, resource kinds and the operations they support
22+
* are represented as subclasses of {@link ResourceType}.</p>
23+
*/
24+
public interface Authorizer {
25+
26+
/**
27+
* Determines whether the given {@code subject} is allowed to perform the given {@code actions}.
28+
* The implementation must ensure that the returned authorization partitions all the given {@code actions}
29+
* between {@link AuthorizeResult#allowed()} and {@link AuthorizeResult#denied()}.
30+
* @param subject The subject.
31+
* @param actions The actions.
32+
* @return The outcome. The returned stage should fail with an {@link AuthorizerException} if the authorizer was not able to
33+
* make a decision.
34+
*/
35+
CompletionStage<AuthorizeResult> authorize(Subject subject, List<Action> actions);
36+
37+
/**
38+
* <p>Returns the types of resource that this authorizer is able to make decisions about.
39+
* If this is not known to the implementation it should return empty.</p>
40+
*
41+
* <p>This is provided so that an access control policy enforcement point can confirm that it
42+
* is capable of providing access control to all the resource types in the access control policy
43+
* backing this authorizer.</p>
44+
*
45+
* @return the types of resource that this authorizer is able to make decisions about.
46+
*/
47+
Optional<Set<Class<? extends ResourceType<?>>>> supportedResourceTypes();
48+
49+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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.authorizer.service;
8+
9+
/**
10+
* An exception to be thrown if an {@link Authorizer} cannot be built, or is not able to make a decision.
11+
*/
12+
public class AuthorizerException extends RuntimeException {
13+
public AuthorizerException(String message) {
14+
super(message);
15+
}
16+
17+
public AuthorizerException(String message, Throwable cause) {
18+
super(message, cause);
19+
}
20+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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.authorizer.service;
8+
9+
import javax.annotation.concurrent.ThreadSafe;
10+
11+
import edu.umd.cs.findbugs.annotations.NonNull;
12+
import edu.umd.cs.findbugs.annotations.UnknownNullness;
13+
14+
/**
15+
* Service interface for {@link Authorizer Authorizers}.
16+
* @param <C> The config type
17+
*/
18+
@ThreadSafe
19+
public interface AuthorizerService<C> {
20+
21+
/**
22+
* Initialises the service. This method must be invoked exactly once
23+
* before {@link #build()} is called.
24+
*
25+
* @param config Service configuration
26+
* @throws AuthorizerException If the service could not be initialized
27+
*/
28+
void initialize(@UnknownNullness C config);
29+
30+
/**
31+
* Builds an Authorizer service.
32+
* {@link #initialize(C)} must have been called before this method is invoked.
33+
*
34+
* @return the authorizer
35+
* @throws IllegalStateException if the service has not been initialised or the service is closed.
36+
* @throws AuthorizerException If the authorizer could not be built
37+
*/
38+
@NonNull
39+
Authorizer build() throws IllegalStateException;
40+
41+
/**
42+
* Closes the service. Once the service is closed, the building of new instances or the
43+
* continued use of previously built instances is not allowed. The effect using an instance
44+
* after the service that created it is closed is undefined.
45+
* <br/>
46+
* Implementations of this method must be idempotent.
47+
* <br/>
48+
* Close implementations must tolerate the closing of service that has not been initialized or
49+
* one for which initialization did not fully complete without further exception.
50+
*/
51+
default void close() {
52+
}
53+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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.authorizer.service;
8+
9+
/**
10+
* An authorization decision about whether a particular subject is allowed to perform a
11+
* specific operation on a particular resource.
12+
*/
13+
public enum Decision {
14+
/**
15+
* The subject is allowed to perform the operation on the resource.
16+
*/
17+
ALLOW,
18+
19+
/**
20+
* The subject is not allowed to perform the operation on the resource.
21+
*/
22+
DENY;
23+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.authorizer.service;
8+
9+
import java.util.Set;
10+
11+
/**
12+
* A {@code ResourceType} is an {@code enum} of the possible operations on a resource of a particular type.
13+
* We use a one-enum-per-resource-type pattern so that the {@link Class} an implementation also
14+
* serves to identify the resource type.
15+
* For this reason, implementations of this interface should be named for the type of resource
16+
* (for example {@code Topic}, or {@code ConsumerGroup}) rather than the operations
17+
* enumerated (so not {@code TopicOperations} or {@code ConsumerGroupOperations}).
18+
* @param <S> The self type.
19+
*/
20+
public interface ResourceType<S extends Enum<S> & ResourceType<S>> {
21+
22+
default Set<S> implies() {
23+
// TODO This is actually really tricky to model in a way that works for different Authorizer implementations
24+
// Allowing operations to express implication makes in-process authorization evaluations easier
25+
// because we can just call the method (either before or after querying internal data structures).
26+
// But it means an operation is more than just its name.
27+
// Which makes like harder for Authz-as-a-Service because either:
28+
// 1. they need to model the implication, in their backend representation of the rules
29+
// 2. Or else their Java client needs to use the implication to expand the set of actions being queried
30+
// prior to calling the service.
31+
// Either choice ends up coupling the Authz-as-a-Service Authorizer to particular Operation implementations
32+
return Set.of();
33+
}
34+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
/**
8+
* Provides an abstract API for authorizing subjects to perform actions.
9+
*/
10+
@ReturnValuesAreNonnullByDefault
11+
@DefaultAnnotationForParameters(NonNull.class)
12+
@DefaultAnnotation(NonNull.class)
13+
package io.kroxylicious.authorizer.service;
14+
15+
import edu.umd.cs.findbugs.annotations.DefaultAnnotation;
16+
import edu.umd.cs.findbugs.annotations.DefaultAnnotationForParameters;
17+
import edu.umd.cs.findbugs.annotations.NonNull;
18+
import edu.umd.cs.findbugs.annotations.ReturnValuesAreNonnullByDefault;

0 commit comments

Comments
 (0)