Skip to content

Commit

Permalink
feat: add acl host matching
Browse files Browse the repository at this point in the history
  • Loading branch information
biggusdonzus committed Nov 5, 2024
1 parent 49ccda9 commit 57bb239
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 34 deletions.
2 changes: 2 additions & 0 deletions src/main/java/io/aiven/kafka/auth/AivenAclAuthorizerV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ public final List<AuthorizationResult> authorize(final AuthorizableRequestContex
final String resourceToCheck =
LegacyResourceTypeNameFormatter.format(resourcePattern.resourceType())
+ ":" + resourcePattern.name();
final String host = requestContext.clientAddress().getHostAddress();
final boolean verdict = cacheReference.get().get(principal,
host,
LegacyOperationNameFormatter.format(operation),
resourceToCheck);
final var authResult = verdict ? AuthorizationResult.ALLOWED : AuthorizationResult.DENIED;
Expand Down
12 changes: 8 additions & 4 deletions src/main/java/io/aiven/kafka/auth/VerdictCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,22 @@ private VerdictCache(@Nonnull final List<AivenAcl> denyAclEntries, @Nonnull fina
this.allowAclEntries = allowAclEntries;
}

public boolean get(final KafkaPrincipal principal,
final String operation,
final String resource) {
public boolean get(
final KafkaPrincipal principal,
final String host,
final String operation,
final String resource
) {
final String principalType = principal.getPrincipalType();
final String cacheKey = resource
+ "|" + operation
+ "|" + host
+ "|" + principal.getName()
+ "|" + principalType;

return cache.computeIfAbsent(cacheKey, key -> {
final Predicate<AivenAcl> matcher = acl ->
acl.match(principalType, principal.getName(), operation, resource);
acl.match(principalType, principal.getName(), host, operation, resource);
if (denyAclEntries.stream().anyMatch(matcher)) {
return false;
} else {
Expand Down
14 changes: 13 additions & 1 deletion src/main/java/io/aiven/kafka/auth/json/AivenAcl.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public class AivenAcl {
@SerializedName("principal")
public final Pattern principalRe;

@SerializedName("host")
public final String hostMatcher;

@SerializedName("operation")
public final Pattern operationRe;

Expand All @@ -43,12 +46,14 @@ public class AivenAcl {

public AivenAcl(final String principalType,
final String principal,
final String host,
final String operation,
final String resource,
final String resourcePattern,
final AclPermissionType permissionType) {
this.principalType = principalType;
this.principalRe = Pattern.compile(principal);
this.hostMatcher = host;
this.operationRe = Pattern.compile(operation);
this.resourceRe = Objects.nonNull(resource) ? Pattern.compile(resource) : null;
this.resourceRePattern = resourcePattern;
Expand All @@ -68,12 +73,13 @@ public AclPermissionType getPermissionType() {
*/
public Boolean match(final String principalType,
final String principal,
final String host,
final String operation,
final String resource) {
if (this.principalType == null || this.principalType.equals(principalType)) {
final Matcher mp = this.principalRe.matcher(principal);
final Matcher mo = this.operationRe.matcher(operation);
if (mp.find() && mo.find()) {
if (mp.find() && mo.find() && this.hostMatch(host)) {
Matcher mr = null;
if (this.resourceRe != null) {
mr = this.resourceRe.matcher(resource);
Expand All @@ -90,6 +96,12 @@ public Boolean match(final String principalType,
return false;
}

private boolean hostMatch(String host) {
return this.hostMatcher == null
|| this.hostMatcher.equals("*")
|| this.hostMatcher.equals(host);
}

@Override
public boolean equals(final Object o) {
if (this == o) {
Expand Down
29 changes: 16 additions & 13 deletions src/test/java/io/aiven/kafka/auth/json/AivenAclTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,46 +28,49 @@ public void testAivenAclEntry() {
AivenAcl entry = new AivenAcl(
"User", // principal type
"^CN=p_(.*)_s$", // principal
"*", // host
"^(Describe|Read)$", // operation
"^Topic:p_(.*)_s", // resource,
null, // resource pattern
null
);

assertTrue(entry.match("User", "CN=p_pass_s", "Read", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=fail", "Read", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=p_pass_s", "Write", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=p_pass_s", "Read", "Topic:fail"));
assertFalse(entry.match("NonUser", "CN=p_pass_s", "Read", "Topic:p_pass_s"));
assertTrue(entry.match("User", "CN=p_pass_s", "*", "Read", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=fail", "*", "Read", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=p_pass_s", "*", "Write", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=p_pass_s", "*", "Read", "Topic:fail"));
assertFalse(entry.match("NonUser", "CN=p_pass_s", "*", "Read", "Topic:p_pass_s"));

// Test with principal undefined
entry = new AivenAcl(
null, // principal type
"^CN=p_(.*)_s$", // principal
"*", // host
"^(Describe|Read)$", // operation
"^Topic:p_(.*)_s", // resource
null, // resource pattern
null
);

assertTrue(entry.match("User", "CN=p_pass_s", "Read", "Topic:p_pass_s"));
assertTrue(entry.match("NonUser", "CN=p_pass_s", "Read", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=fail", "Read", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=p_pass_s", "Read", "Topic:fail"));
assertTrue(entry.match("User", "CN=p_pass_s", "*", "Read", "Topic:p_pass_s"));
assertTrue(entry.match("NonUser", "CN=p_pass_s", "*", "Read", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=fail", "*", "Read", "Topic:p_pass_s"));
assertFalse(entry.match("User", "CN=p_pass_s", "*", "Read", "Topic:fail"));

// Test resources defined by pattern
entry = new AivenAcl(
"User", // principal type
"^CN=p_(?<username>[a-z0-9]+)_s$", // principal
"*", // host
"^(Describe|Read)$", // operation
null, // resource
"^Topic:p_${username}_s\\$", // resource pattern
null
);

assertTrue(entry.match("User", "CN=p_user1_s", "Read", "Topic:p_user1_s"));
assertTrue(entry.match("User", "CN=p_user2_s", "Read", "Topic:p_user2_s"));
assertFalse(entry.match("User", "CN=p_user1_s", "Read", "Topic:p_user2_s"));
assertFalse(entry.match("User", "CN=p_user2_s", "Read", "Topic:p_user1_s"));
assertTrue(entry.match("User", "CN=p_user1_s", "*", "Read", "Topic:p_user1_s"));
assertTrue(entry.match("User", "CN=p_user2_s", "*", "Read", "Topic:p_user2_s"));
assertFalse(entry.match("User", "CN=p_user1_s", "*", "Read", "Topic:p_user2_s"));
assertFalse(entry.match("User", "CN=p_user2_s", "*", "Read", "Topic:p_user1_s"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,23 @@ public final void parseAcls() {
final var jsonReader = new AclJsonReader(path);
final var acls = jsonReader.read();
assertThat(acls).containsExactly(
new AivenAcl("User", "^pass-3$", "^Read$", "^Topic:denied$", null, AclPermissionType.DENY),
new AivenAcl("User", "^pass-0$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-1$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-2$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-3$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-4$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-5$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-6$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-7$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-8$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-9$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-10$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-11$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-12$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl(null, "^pass-notype$", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-3$", "*", "^Read$", "^Topic:denied$", null, AclPermissionType.DENY),
new AivenAcl("User", "^pass-0$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-1$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-2$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-3$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-4$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-5$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-6$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-7$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-8$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-9$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-10$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-11$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl("User", "^pass-12$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl(null, "^pass-notype$", "*", "^Read$", "^Topic:(.*)$", null, AclPermissionType.ALLOW),
new AivenAcl(
"User", "^pass-resource-pattern$", "^Read$", null, "^Topic:${projectid}-(.*)", AclPermissionType.ALLOW
"User", "^pass-resource-pattern$", "*", "^Read$", null, "^Topic:${projectid}-(.*)", AclPermissionType.ALLOW
)
);
}
Expand All @@ -65,6 +65,7 @@ public final void parseDenyAcl() {
final var allowAcl = new AivenAcl(
"User",
"^allow$",
"*",
"^Read$",
"^(.*)$",
null,
Expand All @@ -73,6 +74,7 @@ public final void parseDenyAcl() {
final var denyAcl = new AivenAcl(
"User",
"^deny$",
"*",
"^Read$",
"^(.*)$",
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public final void testConvertSimple() {
new AivenAcl(
"User",
"^(test\\-user)$",
"*",
"^(Alter|AlterConfigs|Delete|Read|Write)$",
"^Topic:(xxx)$",
null,
Expand Down Expand Up @@ -72,6 +73,7 @@ public final void testNullPermissionTypeIsAllow() {
new AivenAcl(
"User",
"^(test\\-user)$",
"*",
"^Read$",
"^Topic:(xxx)$",
null,
Expand All @@ -92,6 +94,7 @@ public final void testConvertPrefix() {
new AivenAcl(
"User",
"^(test\\-user)$",
"*",
"^Read$",
"^Topic:(topic\\.(.*))$",
null,
Expand All @@ -112,6 +115,7 @@ public final void testDeny() {
new AivenAcl(
"User",
"^(test\\-user)$",
"*",
"^Read$",
"^Topic:(topic\\.(.*))$",
null,
Expand All @@ -132,6 +136,7 @@ public final void testConvertMultiplePrefixes() {
new AivenAcl(
"User",
"^(test\\-user)$",
"*",
"^(Delete|Read|Write)$",
"^Topic:(topic\\.(.*)|prefix\\-(.*))$",
null,
Expand Down Expand Up @@ -172,6 +177,7 @@ public final void testSuperadmin() {
new AivenAcl(
"User",
"^(admin)$",
"*",
"^(.*)$",
"^(.*)$",
null,
Expand Down Expand Up @@ -205,6 +211,7 @@ public final void testAllUsers() {
new AivenAcl(
"User",
"^(.*)$",
"*",
"^Read$",
"^Topic:(xxx)$",
null,
Expand All @@ -226,6 +233,7 @@ public final void testNoUserPrincipalType() {
new AivenAcl(
"Group",
"^example$",
"*",
"^Read$",
"^Topic:(xxx)$",
null,
Expand Down

0 comments on commit 57bb239

Please sign in to comment.