Skip to content

Commit

Permalink
feat: support single character wildcard resource matching
Browse files Browse the repository at this point in the history
  • Loading branch information
jcosentino11 committed Sep 11, 2024
1 parent 9d0911d commit 48d07c7
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -274,39 +274,33 @@ public static Stream<Arguments> authzRequests() {
.resource("mqtt:topic:myThing/world")
.expectedResult(false)
.build(),
// mqtt wildcards eval not supported by default
// single character eval not supported by default
AuthZRequest.builder()
.thingName("myThing")
.operation("mqtt:subscribe")
.resource("mqtt:topic:myThing/test/test/*")
.resource("mqtt:topic:myThing/#/test/abc")
.expectedResult(false)
.build(),
AuthZRequest.builder()
.thingName("myThing")
.operation("mqtt:subscribe")
.resource("mqtt:topic:myThing/#/test/*")
.resource("mqtt:topic:myThing/#/test/???")
.expectedResult(true)
.build()
)),

Arguments.of("mqtt-wildcards-in-resource.yaml", Arrays.asList(
Arguments.of("single-character-wildcards-in-resource.yaml", Arrays.asList(
AuthZRequest.builder()
.thingName("myThing")
.operation("mqtt:publish")
.resource("mqtt:topic:*/myThing/*")
.expectedResult(true)
.build(),
AuthZRequest.builder()
.thingName("myThing")
.operation("mqtt:publish")
.resource("mqtt:topic:hello/myThing/world")
.operation("mqtt:subscribe")
.resource("mqtt:topic:myThing/abc/test/a/b")
.expectedResult(true)
.build(),
AuthZRequest.builder()
.thingName("myThing")
.operation("mqtt:subscribe")
.resource("mqtt:topic:myThing/test/test/test/test")
.expectedResult(true)
.resource("mqtt:topic:myThing/abcd/test/a/b")
.expectedResult(false)
.build()
)),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ services:
level: "DEBUG"
aws.greengrass.clientdevices.Auth:
configuration:
enableMqttWildcardEvaluation: true
enableSingleCharacterWildcardMatching: true
deviceGroups:
formatVersion: "2021-03-05"
definitions:
Expand All @@ -18,18 +18,12 @@ services:
policyName: "thingAccessPolicy"
policies:
thingAccessPolicy:
publish:
statementDescription: "mqtt publish"
operations:
- "mqtt:publish"
resources:
- "mqtt:topic:*/myThing/*"
subscribe:
statementDescription: "mqtt subscribe"
operations:
- "mqtt:subscribe"
resources:
- "mqtt:topic:${iot:Connection.Thing.ThingName}/+/test/#"
- "mqtt:topic:${iot:Connection.Thing.ThingName}/???/test/*"
main:
dependencies:
- aws.greengrass.clientdevices.Auth
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ services:
operations:
- "mqtt:subscribe"
resources:
- "mqtt:topic:myThing/#/test/*"
- "mqtt:topic:myThing/#/test/???"
main:
dependencies:
- aws.greengrass.clientdevices.Auth
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@

package com.aws.greengrass.clientdevices.auth;

import com.aws.greengrass.authorization.WildcardTrie;
import com.aws.greengrass.clientdevices.auth.configuration.CDAConfiguration;
import com.aws.greengrass.clientdevices.auth.configuration.GroupManager;
import com.aws.greengrass.clientdevices.auth.configuration.Permission;
import com.aws.greengrass.clientdevices.auth.exception.PolicyException;
import com.aws.greengrass.clientdevices.auth.session.Session;
import com.aws.greengrass.clientdevices.auth.util.WildcardTrie;
import com.aws.greengrass.logging.api.Logger;
import com.aws.greengrass.logging.impl.LogManager;
import com.aws.greengrass.util.Utils;
Expand Down Expand Up @@ -137,28 +137,14 @@ private boolean compareResource(Resource requestResource, String policyResource)
if (Objects.equals(requestResource.getResourceStr(), policyResource)) {
return true;
}

if (matchMqttWildcards()) {
String name = extractResourceName(policyResource);
WildcardTrie trie = new WildcardTrie();
trie.add(name);
return trie.matchesMQTT(requestResource.getResourceName());
} else {
WildcardTrie trie = new WildcardTrie();
trie.add(policyResource);
return trie.matchesStandard(requestResource.getResourceStr());
}
}

private String extractResourceName(String resource) {
// resource is considered valid at this point, so don't need to duplicate validation from parseResource
String typeAndName = resource.substring(resource.indexOf(':') + 1);
return typeAndName.substring(typeAndName.indexOf(':') + 1);
WildcardTrie trie = new WildcardTrie();
trie.add(policyResource);
return trie.matches(requestResource.getResourceStr(), matchSingleCharacterWildcard());
}

private boolean matchMqttWildcards() {
private boolean matchSingleCharacterWildcard() {
CDAConfiguration config = cdaConfiguration;
return config != null && config.isEnableMqttWildcardEvaluation();
return config != null && config.isMatchSingleCharacterWildcard();
}

private Operation parseOperation(String operationStr) throws PolicyException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
@Builder

Check notice

Code scanning / CodeQL

Use of default toString() Note

Default toString(): MetricsConfiguration inherits toString() from Object, and so is not suitable for printing.
Default toString(): SecurityConfiguration inherits toString() from Object, and so is not suitable for printing.
Default toString(): CAConfiguration inherits toString() from Object, and so is not suitable for printing.
Default toString(): RuntimeConfiguration inherits toString() from Object, and so is not suitable for printing.
Default toString(): DomainEvents inherits toString() from Object, and so is not suitable for printing.
public final class CDAConfiguration {

public static final String ENABLE_MQTT_WILDCARD_EVALUATION = "enableMqttWildcardEvaluation";
public static final String ENABLE_MQTT_WILDCARD_EVALUATION = "enableSingleCharacterWildcardMatching";

private final DomainEvents domainEvents;
private final RuntimeConfiguration runtime;
Expand All @@ -62,7 +62,7 @@ public final class CDAConfiguration {
private final SecurityConfiguration security;
private final MetricsConfiguration metricsConfiguration;
@Getter
private final boolean enableMqttWildcardEvaluation;
private final boolean matchSingleCharacterWildcard;

/**
* Creates the CDA (Client Device Auth) Service configuration. And allows it to be available in the context with the
Expand All @@ -84,7 +84,7 @@ public static CDAConfiguration from(CDAConfiguration existingConfig, Topics topi
.certificateAuthorityConfiguration(CAConfiguration.from(serviceConfiguration))
.security(SecurityConfiguration.from(serviceConfiguration))
.metricsConfiguration(MetricsConfiguration.from(serviceConfiguration))
.enableMqttWildcardEvaluation(
.matchSingleCharacterWildcard(
Coerce.toBoolean(serviceConfiguration.find(ENABLE_MQTT_WILDCARD_EVALUATION)))
.build();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

package com.aws.greengrass.clientdevices.auth.util;


import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

public class WildcardTrie {
private static final String GLOB_WILDCARD = "*";
private static final String SINGLE_CHAR_WILDCARD = "?";

private final Map<String, WildcardTrie> children = new DefaultHashMap<>(WildcardTrie::new);

private boolean isTerminal;
private boolean isGlobWildcard;
private boolean isSingleCharWildcard;

public void add(String subject) {
add(subject, true);
}

private WildcardTrie add(String subject, boolean isTerminal) {
if (subject == null || subject.isEmpty()) {
this.isTerminal |= isTerminal;
return this;
}
StringBuilder currPrefix = new StringBuilder(subject.length());
for (int i = 0; i < subject.length(); i++) {
char c = subject.charAt(i);
if (c == GLOB_WILDCARD.charAt(0)) {
return addGlobWildcard(subject, currPrefix.toString(), isTerminal);
}
if (c == SINGLE_CHAR_WILDCARD.charAt(0)) {
return addSingleCharWildcard(subject, currPrefix.toString(), isTerminal);
}
currPrefix.append(c);
}
WildcardTrie node = children.get(currPrefix.toString());
node.isTerminal |= isTerminal;
return node;
}

private WildcardTrie addGlobWildcard(String subject, String currPrefix, boolean isTerminal) {
WildcardTrie node = this;
node = node.add(currPrefix, false);
node = node.children.get(GLOB_WILDCARD);
node.isGlobWildcard = true;
// wildcard at end of subject is terminal
if (subject.length() - currPrefix.length() == 1) {
node.isTerminal = isTerminal;
return node;
}
return node.add(subject.substring(currPrefix.length() + 2), true);
}

private WildcardTrie addSingleCharWildcard(String subject, String currPrefix, boolean isTerminal) {
WildcardTrie node = this;
node = node.add(currPrefix, false);
node = node.children.get(SINGLE_CHAR_WILDCARD);
node.isSingleCharWildcard = true;
// wildcard at end of subject is terminal
if (subject.length() - currPrefix.length() == 1) {
node.isTerminal = isTerminal;
return node;
}
return node.add(subject.substring(currPrefix.length() + 1), true);
}

public boolean matches(String s) {
return matches(s, true);
}

public boolean matches(String s, boolean matchSingleCharWildcard) {
if (s == null) {
return children.isEmpty();
}

if ((isWildcard() && isTerminal) || (isTerminal && s.isEmpty())) {
return true;
}

boolean childMatchesWildcard = children
.values()
.stream()
.filter(WildcardTrie::isWildcard)
.filter(childNode -> matchSingleCharWildcard || !childNode.isSingleCharWildcard)
.anyMatch(childNode -> childNode.matches(s, matchSingleCharWildcard));
if (childMatchesWildcard) {
return true;
}

if (matchSingleCharWildcard) {
boolean childMatchesSingleCharWildcard = children
.values()
.stream()
.filter(childNode -> childNode.isSingleCharWildcard)
.anyMatch(childNode -> childNode.matches(s, matchSingleCharWildcard));
if (childMatchesSingleCharWildcard) {
return true;
}
}

boolean childMatchesRegularCharacters = children
.keySet()
.stream()
.filter(s::startsWith)
.anyMatch(childToken -> {
WildcardTrie childNode = children.get(childToken);
String rest = s.substring(childToken.length());
return childNode.matches(rest, matchSingleCharWildcard);
});
if (childMatchesRegularCharacters) {
return true;
}

if (isWildcard() && !isTerminal) {
return findMatchingChildSuffixesAfterWildcard(s, matchSingleCharWildcard)
.entrySet()
.stream()
.anyMatch((e) -> {
String suffix = e.getKey();
WildcardTrie childNode = e.getValue();
return childNode.matches(suffix, matchSingleCharWildcard);
});
}
return false;
}

private Map<String, WildcardTrie> findMatchingChildSuffixesAfterWildcard(String s, boolean matchSingleCharWildcard) {
Map<String, WildcardTrie> matchingSuffixes = new HashMap<>();
for (Map.Entry<String, WildcardTrie> e : children.entrySet()) {
String childToken = e.getKey();
WildcardTrie childNode = e.getValue();
int suffixIndex = s.indexOf(childToken);
if (matchSingleCharWildcard && suffixIndex > 1) {
continue;
}
while (suffixIndex >= 0) {
matchingSuffixes.put(s.substring(suffixIndex + childToken.length()), childNode);
suffixIndex = s.indexOf(childToken, suffixIndex + 1);
}
}
return matchingSuffixes;
}

private boolean isWildcard() {
return isGlobWildcard || isSingleCharWildcard;
}

@SuppressFBWarnings("EQ_DOESNT_OVERRIDE_EQUALS")
private static class DefaultHashMap<K, V> extends HashMap<K, V> {

Check failure

Code scanning / CodeQL

No clone method Error

No clone method, yet implements Cloneable.
private final transient Supplier<V> defaultVal;

public DefaultHashMap(Supplier<V> defaultVal) {
super();
this.defaultVal = defaultVal;
}

@Override
@SuppressWarnings("unchecked")
public V get(Object key) {
return super.computeIfAbsent((K) key, (k) -> defaultVal.get());
}

@Override
public boolean containsKey(Object key) {
return super.get(key) != null;
}
}
}
Loading

0 comments on commit 48d07c7

Please sign in to comment.