-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: single character wildcard matching #437
base: main
Are you sure you want to change the base?
Changes from 4 commits
1d2958e
6b156de
9d0911d
48d07c7
9bab92b
c1f6d9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
--- | ||
services: | ||
aws.greengrass.Nucleus: | ||
configuration: | ||
runWithDefault: | ||
posixUser: nobody | ||
windowsUser: integ-tester | ||
logging: | ||
level: "DEBUG" | ||
aws.greengrass.clientdevices.Auth: | ||
configuration: | ||
enableSingleCharacterWildcardMatching: true | ||
deviceGroups: | ||
formatVersion: "2021-03-05" | ||
definitions: | ||
myThing: | ||
selectionRule: "thingName: myThing" | ||
policyName: "thingAccessPolicy" | ||
policies: | ||
thingAccessPolicy: | ||
subscribe: | ||
statementDescription: "mqtt subscribe" | ||
operations: | ||
- "mqtt:subscribe" | ||
resources: | ||
- "mqtt:topic:${iot:Connection.Thing.ThingName}/???/test/*" | ||
main: | ||
dependencies: | ||
- aws.greengrass.clientdevices.Auth |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommendation generated by Amazon CodeGuru Reviewer. Leave feedback on this recommendation by replying to the comment or by reacting to the comment using emoji. The cyclomatic complexity of this method is 21. By comparison, 99% of the methods in the CodeGuru reference dataset have a lower cyclomatic complexity. This indicates the method has a high number of decisions and it can make the logic difficult to understand and test. We recommend that you simplify this method or break it into multiple methods. For example, consider extracting the code block on lines 123-132 into a separate method. |
||
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; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the combination of single level
?
and multi level*
valid in the same level and should we test for it? Ex:mqtt:topic:myThing/???*
ormqtt:topic:myThing/*???
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Evaluation here is more regex-like than mqtt:
*
doesn't know anything about topic levels.There should be test cases for this in WildcardTrieTest already (not all are passing quite yet)