Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
package javasabr.mqtt.acl.groovy.dsl.loader

import javasabr.mqtt.acl.engine.builder.RuleContainerBuilder
import javasabr.mqtt.acl.engine.exception.AclConfigurationException
import javasabr.mqtt.acl.engine.model.rule.AclRule
import javasabr.mqtt.acl.groovy.dsl.builder.AclRulesBuilder
import javasabr.mqtt.model.acl.Operation
import javasabr.rlib.collections.array.Array

import java.nio.file.Files
import java.nio.file.Path
import java.nio.charset.StandardCharsets

class AclRulesLoader {

static Map<Operation, Array<AclRule>> load(String aclConfigPath) {
return load(Path.of(aclConfigPath))
}

static Map<Operation, Array<AclRule>> load(Path aclConfigPath) {
if (Files.notExists(aclConfigPath)) {
throw new AclConfigurationException("Config file:[%s] doesn't exist".formatted(aclConfigPath))
}


static Map<Operation, Array<AclRule>> load(InputStream aclConfigInputStream) {
AclRulesBuilder aclRulesBuilder = new AclRulesBuilder()

def binding = new Binding()
Expand All @@ -32,8 +22,8 @@ class AclRulesLoader {
}

def groovyShell = new GroovyShell(binding)
groovyShell.evaluate(aclConfigPath.toFile())
groovyShell.evaluate(new InputStreamReader(aclConfigInputStream, StandardCharsets.UTF_8))

return RuleContainerBuilder.groupRulesByOperation(aclRulesBuilder.build())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import javasabr.mqtt.service.acl.TestRulesGenerator
import javasabr.mqtt.test.support.UnitSpecification
import javasabr.rlib.collections.array.Array

import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.CompletionException

import static javasabr.mqtt.acl.engine.model.Action.ALLOW
Expand All @@ -36,31 +38,22 @@ class AclRulesLoaderTest extends UnitSpecification {

def "should load test Groovy DSL config"() {
given:
def ruleFile = TestRulesGenerator.generate(100)
def aclConfigFile = TestRulesGenerator.generate(100)
def aclConfigInputStream = new FileInputStream(aclConfigFile)
when:
def load = AclRulesLoader.load(ruleFile.toString())
def load = AclRulesLoader.load(aclConfigInputStream)
then:
load.get(SUBSCRIBE).size() == 50
load.get(PUBLISH).size() == 50
ruleFile.delete()
}

def "should throw exception if config not exists"(String configPath, String errorMessage) {
when:
AclRulesLoader.load(configPath)
then:
def exception = thrown(AclConfigurationException)
exception.message == errorMessage
where:
configPath | errorMessage
"not/existed/path" | 'Config file:[not/existed/path] doesn\'t exist'
aclConfigFile.delete()
}

def "should work fine with only publish rules"() {
given:
def onlyPublishRulesAclPath = getAbsolutePath("acl/config/acl-publish-only.gacl")
def aclConfigInputStream = Files.newInputStream(Path.of(onlyPublishRulesAclPath))
when:
def ruleMap = AclRulesLoader.load(onlyPublishRulesAclPath)
def ruleMap = AclRulesLoader.load(aclConfigInputStream)
then:
noExceptionThrown()
!ruleMap.get(PUBLISH).isEmpty()
Expand All @@ -70,8 +63,9 @@ class AclRulesLoaderTest extends UnitSpecification {
def "should throw exception if config is invalid"(String invalidAclFileName, String errorMessage, Class<? extends Exception> exceptionClass) {
given:
def invalidAclPath = getAbsolutePath("acl/config/invalid/${invalidAclFileName}")
def aclConfigInputStream = Files.newInputStream(Path.of(invalidAclPath))
when:
AclRulesLoader.load(invalidAclPath)
AclRulesLoader.load(aclConfigInputStream)
then:
def exception = thrown CompletionException
exceptionClass.isInstance exception.cause
Expand All @@ -96,9 +90,11 @@ class AclRulesLoaderTest extends UnitSpecification {

@SuppressWarnings('GroovyAccessibility')
def "should parse Groovy DSL config"() {
when:
given:
def absolutePath = getAbsolutePath("acl/config/acl.gacl")
def rules = AclRulesLoader.load(absolutePath)
def aclConfigInputStream = Files.newInputStream(Path.of(absolutePath))
when:
def rules = AclRulesLoader.load(aclConfigInputStream)
then:
verifyAll(rules.get(PUBLISH)) {
size() == 3
Expand Down Expand Up @@ -211,8 +207,8 @@ class AclRulesLoaderTest extends UnitSpecification {
with(matcher as StartsWithMatcher) { prefix() == "device_" }
}
with(topicCondition().matchers) {
with(get(0) as DynamicTopicMatcher<TopicName>) {
originalTopic.rawTopic() == "/devices/{clientId}/notify"
with(get(0) as DynamicTopicMatcher<TopicName>) {
originalTopic.rawTopic() == "/devices/{clientId}/notify"
resolvers.length == 4
resolvers[0].class == NoOpsTopicSegmentResolver
resolvers[1].class == NoOpsTopicSegmentResolver
Expand All @@ -224,4 +220,16 @@ class AclRulesLoaderTest extends UnitSpecification {
}
}
}

def "should decode Groovy DSL config using UTF-8 charset"() {
given:
def utf8AclPath = getAbsolutePath("acl/config/utf8-encoding.gacl")
def aclConfigInputStream = Files.newInputStream(Path.of(utf8AclPath))
when:
def rules = AclRulesLoader.load(aclConfigInputStream)
then:
def publishRule = rules.get(PUBLISH).get(0) as AbstractAclRule
def topicMatcher = publishRule.topicCondition().matchers.get(0) as TopicNameMatcher
topicMatcher.expected.rawTopic() == "/tëméric"
}
}
10 changes: 10 additions & 0 deletions acl-groovy-dsl/src/test/resources/acl/config/utf8-encoding.gacl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package acl.config

allowPublish {
users {
anyUser()
}
topics {
eq("/tëméric")
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package javasabr.mqtt.acl.mug.dsl.loader;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import javasabr.mqtt.acl.engine.builder.RuleContainerBuilder;
import javasabr.mqtt.acl.engine.exception.AclConfigurationException;
Expand All @@ -18,15 +18,12 @@ public class AclRulesLoader {

private final GaclParser parser;

public Map<Operation, Array<AclRule>> load(Path aclConfigPath) {
if (Files.notExists(aclConfigPath)) {
throw new AclConfigurationException("Config file:[%s] doesn't exist".formatted(aclConfigPath));
}
public Map<Operation, Array<AclRule>> load(InputStream aclConfigInputStream) {
String content;
try {
content = Files.readString(aclConfigPath);
} catch (IOException e) {
throw new AclConfigurationException("Failed to read ACL file:[%s]".formatted(aclConfigPath), e);
content = new InputStreamReader(aclConfigInputStream, StandardCharsets.UTF_8).readAllAsString();
} catch (Exception e) {
throw new AclConfigurationException("Failed to read ACL input stream", e);
}
Array<AclRule> rules = new ArrayBuilder<>(AclRule.class)
.add(parser.parse(content))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package javasabr.mqtt.acl.mug.dsl.loader;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package javasabr.mqtt.acl.mug.dsl.parser;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import javasabr.mqtt.acl.engine.exception.AclConfigurationException
import javasabr.mqtt.acl.mug.dsl.parser.GaclParser
import spock.lang.Specification

import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.charset.StandardCharsets

class AclRulesLoaderTest extends Specification {

Expand All @@ -20,7 +18,7 @@ class AclRulesLoaderTest extends Specification {

def "should throw exception when file does not exist"() {
given:
Path nonExistent = Paths.get("non-existent.gacl")
InputStream nonExistent = AclRulesLoaderTest.class.getResourceAsStream("non-existent.gacl")

when:
loader.load(nonExistent)
Expand All @@ -29,38 +27,41 @@ class AclRulesLoaderTest extends Specification {
thrown(AclConfigurationException)
}

def "should throw exception when file read fails"() {
def "should load rules successfully"() {
given:
Path unreadableFile = Files.createTempFile("unreadable", ".gacl")
// Make the file unreadable to trigger IOException during Files.readString
unreadableFile.toFile().setReadable(false)
def aclConfigMock = "mock content"
def aclConfigInputStream = new ByteArrayInputStream(aclConfigMock.getBytes())

when:
loader.load(unreadableFile)
def rules = loader.load(aclConfigInputStream)

then:
thrown(AclConfigurationException)

cleanup:
unreadableFile.toFile().setReadable(true)
Files.deleteIfExists(unreadableFile)
1 * parser.parse("mock content") >> []
rules != null
}

def "should load rules successfully"() {
def "should decode ACL config using UTF-8 charset"() {
given:
Path tempFile = Files.createTempFile("test", ".gacl")
Files.writeString(tempFile, "mock content")

and:
parser.parse("mock content") >> []

def nonAsciiContent = "user sënsor = pässwörd"
def aclConfigInputStream = new ByteArrayInputStream(nonAsciiContent.getBytes(StandardCharsets.UTF_8))
when:
def rules = loader.load(tempFile)

loader.load(aclConfigInputStream)
then:
rules != null
1 * parser.parse(nonAsciiContent) >> []
}

cleanup:
Files.deleteIfExists(tempFile)
def "should throw AclConfigurationException when input stream fails during read"() {
given:
def failingStream = new InputStream() {
@Override
int read() throws IOException {
throw new IOException("simulated read failure")
}
}
when:
loader.load(failingStream)
then:
def exception = thrown(AclConfigurationException)
exception.cause instanceof IOException
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package javasabr.mqtt.acl.service.impl;

import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Function;
import javasabr.mqtt.acl.engine.AclEngine;
import javasabr.mqtt.acl.engine.exception.AclConfigurationException;
import javasabr.mqtt.acl.engine.model.rule.AclRule;
import javasabr.mqtt.acl.service.AclEngineBasedAuthorizationService;
import javasabr.mqtt.base.util.ClassPathResourceResolver;
import javasabr.mqtt.model.acl.Operation;
import javasabr.rlib.collections.array.Array;
import lombok.AccessLevel;
Expand All @@ -21,18 +21,16 @@
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class UriLoaderAuthorizationService extends AclEngineBasedAuthorizationService {

Function<Path, Map<Operation, Array<AclRule>>> rulesLoader;
Function<InputStream, Map<Operation, Array<AclRule>>> rulesLoader;

public void loadFrom(URI resource) {
Path localFile = Path.of(resource);
if (Files.notExists(localFile)) {
throw new AclConfigurationException("ACL configuration:[%s] doesn't exist".formatted(resource));
} else if (Files.isDirectory(localFile)) {
throw new AclConfigurationException("ACL configuration:[%s] is directory".formatted(resource));
try (InputStream aclInputStream = ClassPathResourceResolver.newInputStream(resource)) {
Map<Operation, Array<AclRule>> aclRulesMap = rulesLoader.apply(aclInputStream);
switchTo(new AclEngine(aclRulesMap));
log.info(resource, aclRulesMap, UriLoaderAuthorizationService::buildServiceDescription);
} catch (Exception e) {
throw new AclConfigurationException("Unable to load ACL configuration:[%s]".formatted(resource), e);
}
Map<Operation, Array<AclRule>> loadedAclRulesMap = rulesLoader.apply(localFile);
switchTo(new AclEngine(loadedAclRulesMap));
log.info(resource, loadedAclRulesMap, UriLoaderAuthorizationService::buildServiceDescription);
}

private static String buildServiceDescription(URI resource, Map<Operation, Array<AclRule>> aclRulesMap) {
Expand Down
5 changes: 2 additions & 3 deletions application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ dependencies {

testImplementation projects.testSupport
testImplementation testFixtures(projects.network)
testImplementation projects.credentialsSourceFile
testImplementation projects.authenticationProviderBasic
runtimeOnly projects.credentialsSourceFile
runtimeOnly projects.authenticationProviderBasic
}

tasks.withType(GroovyCompile).configureEach {
Expand All @@ -28,7 +28,6 @@ tasks.withType(GroovyCompile).configureEach {
}

bootRun {
mainClass = "javasabr.mqtt.broker.application.MqttBrokerApplication"
jvmArgs += "--enable-preview"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ import org.springframework.core.env.MapPropertySource

class TestSslPropertiesInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

TestSslContexts sslContexts = TestSslContexts.getInstance()
static final TestSslContexts TEST_SSL_CONTEXT = TestSslContexts.getInstance()

@Override
void initialize(ConfigurableApplicationContext applicationContext) {
def props = [
"mqtt.external.tls.keystore-path": sslContexts.serverKeystorePath.toString(),
"mqtt.external.tls.keystore-password": sslContexts.password,
"mqtt.external.tls.truststore-path": sslContexts.truststore.toString(),
"mqtt.external.tls.truststore-password": sslContexts.password
applicationContext.environment.propertySources.addFirst(new MapPropertySource("tlsProps", getProps()))
}

static Map<String, Object> getProps() {
return [
"mqtt.external.tls.keystore-path": TEST_SSL_CONTEXT.serverKeystorePath.toString(),
"mqtt.external.tls.keystore-password": TEST_SSL_CONTEXT.password,
"mqtt.external.tls.truststore-path": TEST_SSL_CONTEXT.truststore.toString(),
"mqtt.external.tls.truststore-password": TEST_SSL_CONTEXT.password
]
applicationContext.environment.propertySources.addFirst(new MapPropertySource("tlsProps", props))
}
}
Loading
Loading