diff --git a/acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/AllOfBuilder.groovy b/acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/AllOfBuilder.groovy index 1ea98459..a3987584 100644 --- a/acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/AllOfBuilder.groovy +++ b/acl-groovy-dsl/src/main/groovy/javasabr/mqtt/acl/groovy/dsl/builder/AllOfBuilder.groovy @@ -12,7 +12,7 @@ class AllOfBuilder extends ConditionBuilder { USER_NAME, CLIENT_ID, IP_ADDRESS } - private final Set alreadySetIdentities = new HashSet<>() + private final Set alreadySetIdentities = EnumSet.noneOf(Identity.class) @Override ConditionBuilder userName(ValueMatcher... userNames) { diff --git a/application/build.gradle b/application/build.gradle index 8c1305d0..f2c4b8b8 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -9,12 +9,15 @@ description = "Standard configuration of standalone version of MQTT Broker" dependencies { implementation projects.coreService + implementation projects.credentialsSourceFile + implementation projects.credentialsSourceDb implementation projects.aclService implementation projects.aclGroovyDsl implementation libs.rlib.logger.slf4j implementation libs.springboot.starter.core implementation libs.springboot.starter.log4j2 + testImplementation libs.r2dbc.h2 testImplementation projects.testSupport testImplementation testFixtures(projects.network) } diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/AuthenticationSpringConfig.java b/application/src/main/java/javasabr/mqtt/broker/application/config/AuthenticationSpringConfig.java new file mode 100644 index 00000000..caa8bcab --- /dev/null +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/AuthenticationSpringConfig.java @@ -0,0 +1,96 @@ +package javasabr.mqtt.broker.application.config; + +import static io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider.OPTIONS; +import static io.r2dbc.spi.ConnectionFactoryOptions.DATABASE; +import static io.r2dbc.spi.ConnectionFactoryOptions.DRIVER; +import static io.r2dbc.spi.ConnectionFactoryOptions.HOST; +import static io.r2dbc.spi.ConnectionFactoryOptions.PASSWORD; +import static io.r2dbc.spi.ConnectionFactoryOptions.PORT; +import static io.r2dbc.spi.ConnectionFactoryOptions.USER; + +import io.r2dbc.pool.ConnectionPool; +import io.r2dbc.pool.ConnectionPoolConfiguration; +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; +import java.util.List; +import java.util.Map; +import javasabr.mqtt.service.auth.AuthenticationService; +import javasabr.mqtt.service.auth.DefaultAuthenticationService; +import javasabr.mqtt.service.auth.PasswordBasedAuthenticationProvider; +import javasabr.mqtt.service.auth.provider.AuthenticationProvider; +import javasabr.mqtt.service.auth.source.CredentialSource; +import javasabr.mqtt.service.auth.source.DatabaseProperties; +import javasabr.mqtt.service.auth.source.FileCredentialsSource; +import javasabr.mqtt.service.auth.source.R2dbcCredentialsSource; +import javasabr.rlib.collections.dictionary.DictionaryFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.r2dbc.core.DatabaseClient; + +@Configuration(proxyBeanMethods = false) +public class AuthenticationSpringConfig { + + private static final String LOCK_TIMEOUT_OPTION = "lock_timeout"; + private static final String STATEMENT_TIMEOUT_OPTION = "statement_timeout"; + + @Bean + ConnectionFactory connectionFactory(DatabaseProperties config) { + Map timeoutOptions = Map.of( + LOCK_TIMEOUT_OPTION, config.lockTimeout(), + STATEMENT_TIMEOUT_OPTION, config.statementTimeout()); + ConnectionFactoryOptions connectionFactoryOptions = ConnectionFactoryOptions + .builder() + .option(DRIVER, config.driver()) + .option(HOST, config.host()) + .option(PORT, config.port()) + .option(USER, config.username()) + .option(PASSWORD, config.password()) + .option(DATABASE, config.name()) + .option(OPTIONS, timeoutOptions) + .build(); + ConnectionFactory connectionFactory = ConnectionFactories.get(connectionFactoryOptions); + ConnectionPoolConfiguration configuration = ConnectionPoolConfiguration + .builder(connectionFactory) + .maxIdleTime(config.maxIdleTime()) + .maxSize(config.maxPoolSize()) + .initialSize(config.initialPoolSize()) + .build(); + return new ConnectionPool(configuration); + } + + @Bean + DatabaseClient databaseClient(ConnectionFactory connectionFactory) { + return DatabaseClient.create(connectionFactory); + } + + @Bean + CredentialSource credentialSource(@Value("${credentials.source.file.name:credentials}") String fileName) { + return new FileCredentialsSource(fileName); + } + + @Bean + CredentialSource dbCredentialSource(DatabaseClient connectionFactory, DatabaseProperties databaseProperties) { + return new R2dbcCredentialsSource(connectionFactory, databaseProperties.credentialsQuery()); + } + + @Bean + AuthenticationProvider passwordBasedAuthenticationProvider(CredentialSource credentialSource) { + return new PasswordBasedAuthenticationProvider(credentialSource); + } + + @Bean + AuthenticationService authenticationService( + List authenticationProviders, + @Value("${authentication.allow.anonymous:false}") boolean allowAnonymousAuth, + @Value("${authentication.provider.default:basic}") String defaultProviderName) { + var providers = DictionaryFactory.mutableRefToRefDictionary(String.class, AuthenticationProvider.class); + authenticationProviders.forEach(value -> providers.put(value.getAuthMethodName(), value)); + AuthenticationProvider defaultProvider = providers.get(defaultProviderName); + if (defaultProvider == null) { + throw new IllegalArgumentException("[%s] authenticator provider not found".formatted(defaultProviderName)); + } + return new DefaultAuthenticationService(providers.toReadOnly(), defaultProvider, allowAnonymousAuth); + } +} diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/CredentialsSourceDatabaseProperties.java b/application/src/main/java/javasabr/mqtt/broker/application/config/CredentialsSourceDatabaseProperties.java new file mode 100644 index 00000000..6856579b --- /dev/null +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/CredentialsSourceDatabaseProperties.java @@ -0,0 +1,20 @@ +package javasabr.mqtt.broker.application.config; + +import java.time.Duration; +import javasabr.mqtt.service.auth.source.DatabaseProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "credentials.source.db") +public record CredentialsSourceDatabaseProperties( + String username, + String password, + String driver, + String host, + int port, + String name, + String credentialsQuery, + Duration maxIdleTime, + int initialPoolSize, + int maxPoolSize, + String lockTimeout, + String statementTimeout) implements DatabaseProperties {} diff --git a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java index fa8f992e..650456b8 100644 --- a/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java +++ b/application/src/main/java/javasabr/mqtt/broker/application/config/MqttBrokerSpringConfig.java @@ -14,15 +14,14 @@ import javasabr.mqtt.network.message.in.PublishMqttInMessage; import javasabr.mqtt.network.user.NetworkMqttUserFactory; import javasabr.mqtt.service.AuthorizationService; -import javasabr.mqtt.service.AuthenticationService; import javasabr.mqtt.service.ClientIdRegistry; import javasabr.mqtt.service.ConnectionService; -import javasabr.mqtt.service.CredentialSource; import javasabr.mqtt.service.MessageOutFactoryService; import javasabr.mqtt.service.PublishDeliveringService; import javasabr.mqtt.service.PublishReceivingService; import javasabr.mqtt.service.SubscriptionService; import javasabr.mqtt.service.TopicService; +import javasabr.mqtt.service.auth.AuthenticationService; import javasabr.mqtt.service.handler.client.ExternalNetworkMqttUserReleaseHandler; import javasabr.mqtt.service.impl.DefaultConnectionService; import javasabr.mqtt.service.impl.DefaultMessageOutFactoryService; @@ -32,10 +31,8 @@ import javasabr.mqtt.service.impl.DefaultTopicService; import javasabr.mqtt.service.impl.DisabledAuthorizationService; import javasabr.mqtt.service.impl.ExternalNetworkMqttUserFactory; -import javasabr.mqtt.service.impl.FileCredentialsSource; import javasabr.mqtt.service.impl.InMemoryClientIdRegistry; import javasabr.mqtt.service.impl.InMemorySubscriptionService; -import javasabr.mqtt.service.impl.SimpleAuthenticationService; import javasabr.mqtt.service.message.handler.MqttInMessageHandler; import javasabr.mqtt.service.message.handler.impl.ConnectInMqttInMessageHandler; import javasabr.mqtt.service.message.handler.impl.DisconnectMqttInMessageHandler; @@ -73,6 +70,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -82,7 +80,8 @@ import org.springframework.core.env.Environment; @Import({ - GroovyDslBasedAclServiceSpringConfig.class + GroovyDslBasedAclServiceSpringConfig.class, + AuthenticationSpringConfig.class }) @CustomLog @Configuration(proxyBeanMethods = false) @@ -91,6 +90,7 @@ @PropertySource(value = "file:./application.properties", ignoreResourceNotFound = true), @PropertySource(value = "${BROKER_CONFIG}", ignoreResourceNotFound = true) }) +@EnableConfigurationProperties(CredentialsSourceDatabaseProperties.class) public class MqttBrokerSpringConfig { @Bean @@ -108,19 +108,6 @@ MqttSessionService mqttSessionService( return new InMemoryMqttSessionService(cleanInterval); } - @Bean - CredentialSource credentialSource( - @Value("${credentials.source.file.name:credentials}") String fileName) { - return new FileCredentialsSource(fileName); - } - - @Bean - AuthenticationService authenticationService( - CredentialSource credentialSource, - @Value("${authentication.allow.anonymous:false}") boolean allowAnonymousAuth) { - return new SimpleAuthenticationService(credentialSource, allowAnonymousAuth); - } - @Bean @ConditionalOnProperty( name = "acl.engine.type", @@ -180,7 +167,7 @@ MqttInMessageHandler publishAckMqttInMessageHandler(MessageOutFactoryService mes MqttInMessageHandler publishCompleteMqttInMessageHandler(MessageOutFactoryService messageOutFactoryService) { return new PublishCompleteMqttInMessageHandler(messageOutFactoryService); } - + @Bean PublishPayloadMqttInMessageFieldValidator publishPayloadMqttInMessageFieldValidator( MessageOutFactoryService messageOutFactoryService) { @@ -192,7 +179,7 @@ PublishQosMqttInMessageFieldValidator publishQosMqttInMessageFieldValidator( MessageOutFactoryService messageOutFactoryService) { return new PublishQosMqttInMessageFieldValidator(messageOutFactoryService); } - + @Bean PublishRetainMqttInMessageFieldValidator publishRetainMqttInMessageFieldValidator( MessageOutFactoryService messageOutFactoryService) { @@ -204,13 +191,13 @@ PublishMessageExpiryIntervalMqttInMessageFieldValidator publishMessageExpiryInte MessageOutFactoryService messageOutFactoryService) { return new PublishMessageExpiryIntervalMqttInMessageFieldValidator(messageOutFactoryService); } - + @Bean PublishResponseTopicMqttInMessageFieldValidator publishResponseTopicMqttInMessageFieldValidator( MessageOutFactoryService messageOutFactoryService) { return new PublishResponseTopicMqttInMessageFieldValidator(messageOutFactoryService); } - + @Bean PublishTopicAliasMqttInMessageFieldValidator publishTopicAliasMqttInMessageFieldValidator( MessageOutFactoryService messageOutFactoryService) { @@ -227,7 +214,8 @@ MqttInMessageHandler publishMqttInMessageHandler( return new PublishMqttInMessageHandler( publishReceivingService, messageOutFactoryService, - topicService, authorizationService, + topicService, + authorizationService, fieldValidators); } diff --git a/application/src/test/groovy/javasabr/mqtt/broker/application/AuthenticationTest.groovy b/application/src/test/groovy/javasabr/mqtt/broker/application/AuthenticationTest.groovy new file mode 100644 index 00000000..1c679605 --- /dev/null +++ b/application/src/test/groovy/javasabr/mqtt/broker/application/AuthenticationTest.groovy @@ -0,0 +1,186 @@ +package javasabr.mqtt.broker.application + +import com.hivemq.client.mqtt.mqtt3.exceptions.Mqtt3ConnAckException +import com.hivemq.client.mqtt.mqtt3.message.auth.Mqtt3SimpleAuth +import com.hivemq.client.mqtt.mqtt3.message.connect.Mqtt3Connect +import com.hivemq.client.mqtt.mqtt5.exceptions.Mqtt5ConnAckException +import com.hivemq.client.mqtt.mqtt5.message.auth.Mqtt5SimpleAuth +import com.hivemq.client.mqtt.mqtt5.message.connect.Mqtt5Connect +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestPropertySource + +import java.nio.charset.StandardCharsets +import java.util.concurrent.CompletionException + +@TestPropertySource(properties = ["authentication.allow.anonymous=false"]) +class AuthenticationTest { + + static class FileCredentialsSourceTest extends IntegrationSpecification { + + def "should not be able to connect with wrong password using mqtt 3.1.1 client"() { + given: + def existingUsername = "user" + def wrongPassword = "password1" + def subscriber = buildExternalMqtt311Client() + def connectMessage = Mqtt3Connect.builder() + .simpleAuth(Mqtt3SimpleAuth.builder() + .username(existingUsername) + .password(wrongPassword.getBytes(StandardCharsets.UTF_8)) + .build()) + .build() + when: + subscriber.connect(connectMessage).join() + then: + def e = thrown(CompletionException.class) + with(e.cause as Mqtt3ConnAckException) { + message == "CONNECT failed as CONNACK contained an Error Code: BAD_USER_NAME_OR_PASSWORD." + } + } + + def "should be able to connect with correct password using mqtt 3.1.1 client"() { + given: + def existingUsername = "user" + def correctPassword = "password" + and: + def subscriber = buildExternalMqtt311Client() + def connectMessage = Mqtt3Connect.builder() + .simpleAuth(Mqtt3SimpleAuth.builder() + .username(existingUsername) + .password(correctPassword.getBytes(StandardCharsets.UTF_8)) + .build()) + .build() + when: + subscriber.connect(connectMessage).join() + then: + noExceptionThrown() + cleanup: + subscriber.disconnect().join() + } + + def "should not be able to connect with wrong password using mqtt 5 client"() { + given: + def existingUsername = "user" + def wrongPassword = "password1" + and: + def subscriber = buildExternalMqtt5Client() + def connectMessage = Mqtt5Connect.builder() + .simpleAuth(Mqtt5SimpleAuth.builder() + .username(existingUsername) + .password(wrongPassword.getBytes(StandardCharsets.UTF_8)) + .build()) + .build() + when: + subscriber.connect(connectMessage).join() + then: + def e = thrown(CompletionException.class) + with(e.cause as Mqtt5ConnAckException) { + message == "CONNECT failed as CONNACK contained an Error Code: BAD_USER_NAME_OR_PASSWORD." + } + } + + def "should be able to connect with correct password using mqtt 5 client"() { + given: + def existingUsername = "user" + def correctPassword = "password" + and: + def subscriber = buildExternalMqtt5Client() + def connectMessage = Mqtt5Connect.builder() + .simpleAuth(Mqtt5SimpleAuth.builder() + .username(existingUsername) + .password(correctPassword.getBytes(StandardCharsets.UTF_8)) + .build()) + .build() + when: + subscriber.connect(connectMessage).join() + then: + noExceptionThrown() + cleanup: + subscriber.disconnect().join() + } + } + + @ContextConfiguration(classes = CredentialsSourceTestConfig) + static class R2dbcCredentialsSourceTest extends IntegrationSpecification { + + def "should not be able to connect with wrong password using mqtt 3.1.1 client"() { + given: + def existingUsername = "user" + def wrongPassword = "password1" + def subscriber = buildExternalMqtt311Client() + def connectMessage = Mqtt3Connect.builder() + .simpleAuth(Mqtt3SimpleAuth.builder() + .username(existingUsername) + .password(wrongPassword.getBytes(StandardCharsets.UTF_8)) + .build()) + .build() + when: + subscriber.connect(connectMessage).join() + then: + def e = thrown(CompletionException.class) + with(e.cause as Mqtt3ConnAckException) { + message == "CONNECT failed as CONNACK contained an Error Code: BAD_USER_NAME_OR_PASSWORD." + } + } + + def "should be able to connect with correct password using mqtt 3.1.1 client"() { + given: + def existingUsername = "user" + byte[] correctPassword = new byte[]{0x01, 0xBC, 0x2A} + and: + def subscriber = buildExternalMqtt311Client() + def connectMessage = Mqtt3Connect.builder() + .simpleAuth(Mqtt3SimpleAuth.builder() + .username(existingUsername) + .password(correctPassword) + .build()) + .build() + when: + subscriber.connect(connectMessage).join() + then: + noExceptionThrown() + cleanup: + subscriber.disconnect().join() + } + + def "should not be able to connect with wrong password using mqtt 5 client"() { + given: + def existingUsername = "user" + def wrongPassword = "password1" + and: + def subscriber = buildExternalMqtt5Client() + def connectMessage = Mqtt5Connect.builder() + .simpleAuth(Mqtt5SimpleAuth.builder() + .username(existingUsername) + .password(wrongPassword.getBytes(StandardCharsets.UTF_8)) + .build()) + .build() + when: + subscriber.connect(connectMessage).join() + then: + def e = thrown(CompletionException.class) + with(e.cause as Mqtt5ConnAckException) { + message == "CONNECT failed as CONNACK contained an Error Code: BAD_USER_NAME_OR_PASSWORD." + } + } + + def "should be able to connect with correct password using mqtt 5 client"() { + given: + def existingUsername = "user" + byte[] correctPassword = new byte[]{0x01, 0xBC, 0x2A} + and: + def subscriber = buildExternalMqtt5Client() + def connectMessage = Mqtt5Connect.builder() + .simpleAuth(Mqtt5SimpleAuth.builder() + .username(existingUsername) + .password(correctPassword) + .build()) + .build() + when: + subscriber.connect(connectMessage).join() + then: + noExceptionThrown() + cleanup: + subscriber.disconnect().join() + } + } +} diff --git a/application/src/test/groovy/javasabr/mqtt/broker/application/CredentialsSourceTestConfig.groovy b/application/src/test/groovy/javasabr/mqtt/broker/application/CredentialsSourceTestConfig.groovy new file mode 100644 index 00000000..8a6be36a --- /dev/null +++ b/application/src/test/groovy/javasabr/mqtt/broker/application/CredentialsSourceTestConfig.groovy @@ -0,0 +1,36 @@ +package javasabr.mqtt.broker.application + +import io.r2dbc.spi.ConnectionFactories +import io.r2dbc.spi.ConnectionFactory +import javasabr.mqtt.broker.application.config.CredentialsSourceDatabaseProperties +import javasabr.mqtt.service.auth.source.CredentialSource +import javasabr.mqtt.service.auth.source.R2dbcCredentialsSource +import org.springframework.context.annotation.Bean +import org.springframework.core.io.ClassPathResource +import org.springframework.r2dbc.connection.init.ConnectionFactoryInitializer +import org.springframework.r2dbc.connection.init.ResourceDatabasePopulator +import org.springframework.r2dbc.core.DatabaseClient + +class CredentialsSourceTestConfig { + + @Bean + CredentialSource credentialSource(DatabaseClient databaseClient, CredentialsSourceDatabaseProperties properties) { + return new R2dbcCredentialsSource(databaseClient, properties.credentialsQuery()) + } + + @Bean + ConnectionFactory connectionFactory() { + return ConnectionFactories.get("r2dbc:h2:mem:///testdb;DB_CLOSE_DELAY=-1") + } + + @Bean + ConnectionFactoryInitializer datasourceInitializer(ConnectionFactory connectionFactory) { + ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer() + initializer.setConnectionFactory(connectionFactory) + ResourceDatabasePopulator populator = new ResourceDatabasePopulator() + populator.addScript(new ClassPathResource("auth/user-credentials-schema.sql")) + populator.addScript(new ClassPathResource("auth/user-credentials-data.sql")) + initializer.setDatabasePopulator(populator) + return initializer + } +} diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index d3241f77..780e665e 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -1,4 +1,16 @@ authentication.allow.anonymous=true -credentials.source.file.name=credentials-test +credentials.source.file.name=auth/credentials-test mqtt.external.connection.shared.subscription.available=true mqtt.external.connection.wildcard.subscription.available=true +credentials.source.db.username=user +credentials.source.db.password=pass +credentials.source.db.driver=postgres +credentials.source.db.host=localhost +credentials.source.db.port=5432 +credentials.source.db.name=database-name +credentials.source.db.credentials-query=SELECT password FROM user_credentials WHERE username = $1 +credentials.source.db.max-idle-time=30m +credentials.source.db.initial-pool-size=5 +credentials.source.db.max-pool-size=10 +credentials.source.db.lock-timeout=10s +credentials.source.db.statement-timeout=5m diff --git a/application/src/test/resources/credentials-test b/application/src/test/resources/auth/credentials-test similarity index 100% rename from application/src/test/resources/credentials-test rename to application/src/test/resources/auth/credentials-test diff --git a/application/src/test/resources/auth/user-credentials-data.sql b/application/src/test/resources/auth/user-credentials-data.sql new file mode 100644 index 00000000..997d61f3 --- /dev/null +++ b/application/src/test/resources/auth/user-credentials-data.sql @@ -0,0 +1 @@ +INSERT INTO user_credentials(username, password) VALUES ('user', X'01 bc 2a'); diff --git a/application/src/test/resources/auth/user-credentials-schema.sql b/application/src/test/resources/auth/user-credentials-schema.sql new file mode 100644 index 00000000..90da78e3 --- /dev/null +++ b/application/src/test/resources/auth/user-credentials-schema.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS user_credentials; +CREATE TABLE user_credentials ( + username VARCHAR(255), + password BINARY(3) +); diff --git a/authentication-basic/build.gradle b/authentication-basic/build.gradle new file mode 100644 index 00000000..c92e1f2f --- /dev/null +++ b/authentication-basic/build.gradle @@ -0,0 +1,15 @@ +plugins { + id("java-library") + id("configure-java") + id("groovy") +} + +description = "Basic authentication service" + +dependencies { + api projects.base + api projects.authentication + + testImplementation projects.testSupport + testFixturesApi projects.testSupport +} diff --git a/authentication-basic/src/main/java/javasabr/mqtt/service/auth/PasswordBasedAuthenticationProvider.java b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/PasswordBasedAuthenticationProvider.java new file mode 100644 index 00000000..2db978e2 --- /dev/null +++ b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/PasswordBasedAuthenticationProvider.java @@ -0,0 +1,31 @@ +package javasabr.mqtt.service.auth; + +import javasabr.mqtt.service.auth.provider.AuthenticationProvider; +import javasabr.mqtt.service.auth.source.CredentialSource; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import org.jspecify.annotations.Nullable; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class PasswordBasedAuthenticationProvider implements AuthenticationProvider { + + CredentialSource credentialsSource; + + @Override + public String getAuthMethodName() { + return "basic"; + } + + @Override + public Mono authenticate(@Nullable String username, byte[] password, byte[] data) { + // processData(data); + if (username == null) { + return Mono.just(false); + } else { + return credentialsSource.isCredentialExists(username, password); + } + } +} diff --git a/authentication-basic/src/main/java/javasabr/mqtt/service/auth/package-info.java b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/package-info.java new file mode 100644 index 00000000..f70a6243 --- /dev/null +++ b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package javasabr.mqtt.service.auth; + +import org.jspecify.annotations.NullMarked; diff --git a/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/CredentialSource.java b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/CredentialSource.java new file mode 100644 index 00000000..29fc8bc8 --- /dev/null +++ b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/CredentialSource.java @@ -0,0 +1,10 @@ +package javasabr.mqtt.service.auth.source; + +import reactor.core.publisher.Mono; + +public interface CredentialSource { + + String getName(); + + Mono isCredentialExists(String user, byte[] pass); +} diff --git a/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/DatabaseProperties.java b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/DatabaseProperties.java new file mode 100644 index 00000000..db1a6bb4 --- /dev/null +++ b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/DatabaseProperties.java @@ -0,0 +1,18 @@ +package javasabr.mqtt.service.auth.source; + +import java.time.Duration; + +public interface DatabaseProperties { + String username(); + String password(); + String driver(); + String host(); + int port(); + String name(); + String credentialsQuery(); + Duration maxIdleTime(); + int initialPoolSize(); + int maxPoolSize(); + String lockTimeout(); + String statementTimeout(); +} diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/AbstractCredentialSource.java b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/InMemoryCredentialSource.java similarity index 70% rename from core-service/src/main/java/javasabr/mqtt/service/impl/AbstractCredentialSource.java rename to authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/InMemoryCredentialSource.java index 25bd229f..1c9d4056 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/AbstractCredentialSource.java +++ b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/InMemoryCredentialSource.java @@ -1,22 +1,22 @@ -package javasabr.mqtt.service.impl; +package javasabr.mqtt.service.auth.source; import java.util.Arrays; -import javasabr.mqtt.service.CredentialSource; import javasabr.rlib.collections.dictionary.DictionaryFactory; import javasabr.rlib.collections.dictionary.LockableRefToRefDictionary; import javasabr.rlib.collections.dictionary.RefToRefDictionary; import reactor.core.publisher.Mono; -public abstract class AbstractCredentialSource implements CredentialSource { +public abstract class InMemoryCredentialSource implements CredentialSource { private final LockableRefToRefDictionary credentials = DictionaryFactory.stampedLockBasedRefToRefDictionary(String.class, byte[].class); abstract void init(); - void putAll(RefToRefDictionary otherCredentials) { + void reset(RefToRefDictionary otherCredentials) { long stamp = credentials.writeLock(); try { + credentials.clear(); credentials.append(otherCredentials); } finally { credentials.writeUnlock(stamp); @@ -33,12 +33,7 @@ void put(String user, byte[] pass) { } @Override - public Mono check(String user, byte[] pass) { + public Mono isCredentialExists(String user, byte[] pass) { return Mono.just(Arrays.equals(pass, credentials.get(user))); } - - @Override - public Mono check(byte[] pass) { - return Mono.just(Boolean.FALSE); - } } diff --git a/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/package-info.java b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/package-info.java new file mode 100644 index 00000000..67648f86 --- /dev/null +++ b/authentication-basic/src/main/java/javasabr/mqtt/service/auth/source/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package javasabr.mqtt.service.auth.source; + +import org.jspecify.annotations.NullMarked; diff --git a/authentication/build.gradle b/authentication/build.gradle new file mode 100644 index 00000000..17b82b85 --- /dev/null +++ b/authentication/build.gradle @@ -0,0 +1,15 @@ +plugins { + id("java-library") + id("configure-java") + id("groovy") +} + +description = "Authentication service interface" + +dependencies { + api projects.base + api projects.model + + testImplementation projects.testSupport + testFixturesApi projects.testSupport +} diff --git a/authentication/src/main/java/javasabr/mqtt/service/auth/AuthenticationService.java b/authentication/src/main/java/javasabr/mqtt/service/auth/AuthenticationService.java new file mode 100644 index 00000000..8aa0a161 --- /dev/null +++ b/authentication/src/main/java/javasabr/mqtt/service/auth/AuthenticationService.java @@ -0,0 +1,8 @@ +package javasabr.mqtt.service.auth; + +import javasabr.mqtt.model.auth.AuthRequest; +import reactor.core.publisher.Mono; + +public interface AuthenticationService { + Mono authenticate(AuthRequest authRequest); +} diff --git a/authentication/src/main/java/javasabr/mqtt/service/auth/DefaultAuthenticationService.java b/authentication/src/main/java/javasabr/mqtt/service/auth/DefaultAuthenticationService.java new file mode 100644 index 00000000..2a79d0a1 --- /dev/null +++ b/authentication/src/main/java/javasabr/mqtt/service/auth/DefaultAuthenticationService.java @@ -0,0 +1,31 @@ +package javasabr.mqtt.service.auth; + +import javasabr.mqtt.model.auth.AuthRequest; +import javasabr.mqtt.service.auth.provider.AuthenticationProvider; +import javasabr.rlib.collections.dictionary.RefToRefDictionary; +import javasabr.rlib.common.util.StringUtils; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.experimental.FieldDefaults; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +public class DefaultAuthenticationService implements AuthenticationService { + + RefToRefDictionary providers; + AuthenticationProvider defaultProvider; + boolean allowAnonymousAuth; + + @Override + public Mono authenticate(AuthRequest request) { + String username = request.username(); + if (allowAnonymousAuth && StringUtils.isEmpty(username)) { + return Mono.just(true); + } else { + return providers + .getOrDefault(request.authenticationMethod(), defaultProvider) + .authenticate(username, request.password(), request.authenticationData()); + } + } +} diff --git a/authentication/src/main/java/javasabr/mqtt/service/auth/package-info.java b/authentication/src/main/java/javasabr/mqtt/service/auth/package-info.java new file mode 100644 index 00000000..f70a6243 --- /dev/null +++ b/authentication/src/main/java/javasabr/mqtt/service/auth/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package javasabr.mqtt.service.auth; + +import org.jspecify.annotations.NullMarked; diff --git a/authentication/src/main/java/javasabr/mqtt/service/auth/provider/AuthenticationProvider.java b/authentication/src/main/java/javasabr/mqtt/service/auth/provider/AuthenticationProvider.java new file mode 100644 index 00000000..f0e85634 --- /dev/null +++ b/authentication/src/main/java/javasabr/mqtt/service/auth/provider/AuthenticationProvider.java @@ -0,0 +1,11 @@ +package javasabr.mqtt.service.auth.provider; + +import org.jspecify.annotations.Nullable; +import reactor.core.publisher.Mono; + +public interface AuthenticationProvider { + + String getAuthMethodName(); + + Mono authenticate(@Nullable String username, byte[] password, byte[] data); +} diff --git a/core-service/build.gradle b/core-service/build.gradle index 04b7b9e8..8f5f1f40 100644 --- a/core-service/build.gradle +++ b/core-service/build.gradle @@ -9,7 +9,8 @@ description = "Provides interfaces and minimal implementation of all required se dependencies { api projects.network api projects.aclEngine + api projects.authenticationBasic testImplementation projects.testSupport testImplementation testFixtures(projects.network) -} \ No newline at end of file +} diff --git a/core-service/src/main/java/javasabr/mqtt/service/AuthenticationService.java b/core-service/src/main/java/javasabr/mqtt/service/AuthenticationService.java deleted file mode 100644 index 925d27dd..00000000 --- a/core-service/src/main/java/javasabr/mqtt/service/AuthenticationService.java +++ /dev/null @@ -1,7 +0,0 @@ -package javasabr.mqtt.service; - -import reactor.core.publisher.Mono; - -public interface AuthenticationService { - Mono auth(String userName, byte[] password); -} diff --git a/core-service/src/main/java/javasabr/mqtt/service/CredentialSource.java b/core-service/src/main/java/javasabr/mqtt/service/CredentialSource.java deleted file mode 100644 index 881361cc..00000000 --- a/core-service/src/main/java/javasabr/mqtt/service/CredentialSource.java +++ /dev/null @@ -1,10 +0,0 @@ -package javasabr.mqtt.service; - -import reactor.core.publisher.Mono; - -public interface CredentialSource { - - Mono check(String user, byte[] pass); - - Mono check(byte[] pass); -} diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/SimpleAuthenticationService.java b/core-service/src/main/java/javasabr/mqtt/service/impl/SimpleAuthenticationService.java deleted file mode 100644 index 6f18b39b..00000000 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/SimpleAuthenticationService.java +++ /dev/null @@ -1,22 +0,0 @@ -package javasabr.mqtt.service.impl; - -import javasabr.mqtt.service.AuthenticationService; -import javasabr.mqtt.service.CredentialSource; -import lombok.RequiredArgsConstructor; -import reactor.core.publisher.Mono; - -@RequiredArgsConstructor -public class SimpleAuthenticationService implements AuthenticationService { - - private final CredentialSource credentialSource; - private final boolean allowAnonymousAuth; - - @Override - public Mono auth(String userName, byte[] password) { - if (allowAnonymousAuth && userName.isEmpty()) { - return Mono.just(Boolean.TRUE); - } else { - return credentialSource.check(userName, password); - } - } -} diff --git a/core-service/src/main/java/javasabr/mqtt/service/message/converter/ConnectToAuthRequestConverter.java b/core-service/src/main/java/javasabr/mqtt/service/message/converter/ConnectToAuthRequestConverter.java new file mode 100644 index 00000000..7ee8408e --- /dev/null +++ b/core-service/src/main/java/javasabr/mqtt/service/message/converter/ConnectToAuthRequestConverter.java @@ -0,0 +1,16 @@ +package javasabr.mqtt.service.message.converter; + +import javasabr.mqtt.model.auth.AuthRequest; +import javasabr.mqtt.network.message.in.ConnectMqttInMessage; + +public class ConnectToAuthRequestConverter implements MessageConverter { + + @Override + public AuthRequest convert(ConnectMqttInMessage message) { + return new AuthRequest( + message.username(), + message.password(), + message.authenticationMethod(), + message.authenticationData()); + } +} diff --git a/core-service/src/main/java/javasabr/mqtt/service/message/converter/MessageConverter.java b/core-service/src/main/java/javasabr/mqtt/service/message/converter/MessageConverter.java new file mode 100644 index 00000000..d1e14cee --- /dev/null +++ b/core-service/src/main/java/javasabr/mqtt/service/message/converter/MessageConverter.java @@ -0,0 +1,6 @@ +package javasabr.mqtt.service.message.converter; + +public interface MessageConverter { + + Out convert(In message); +} diff --git a/core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/ConnectInMqttInMessageHandler.java b/core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/ConnectInMqttInMessageHandler.java index 9e5dedf3..968b874b 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/ConnectInMqttInMessageHandler.java +++ b/core-service/src/main/java/javasabr/mqtt/service/message/handler/impl/ConnectInMqttInMessageHandler.java @@ -15,6 +15,7 @@ import javasabr.mqtt.model.MqttServerConnectionConfig; import javasabr.mqtt.model.MqttVersion; import javasabr.mqtt.model.exception.ConnectionRejectException; +import javasabr.mqtt.model.auth.AuthRequest; import javasabr.mqtt.model.message.MqttMessageType; import javasabr.mqtt.model.reason.code.ConnectAckReasonCode; import javasabr.mqtt.network.MqttConnection; @@ -23,10 +24,11 @@ import javasabr.mqtt.network.message.out.MqttOutMessage; import javasabr.mqtt.network.session.NetworkMqttSession; import javasabr.mqtt.network.user.ConfigurableNetworkMqttUser; -import javasabr.mqtt.service.AuthenticationService; import javasabr.mqtt.service.ClientIdRegistry; import javasabr.mqtt.service.MessageOutFactoryService; import javasabr.mqtt.service.SubscriptionService; +import javasabr.mqtt.service.auth.AuthenticationService; +import javasabr.mqtt.service.message.converter.ConnectToAuthRequestConverter; import javasabr.mqtt.service.session.MqttSessionService; import javasabr.rlib.common.util.StringUtils; import lombok.AccessLevel; @@ -43,6 +45,7 @@ public class ConnectInMqttInMessageHandler AuthenticationService authenticationService; MqttSessionService sessionService; SubscriptionService subscriptionService; + ConnectToAuthRequestConverter connectToAuthRequestConverter; public ConnectInMqttInMessageHandler( ClientIdRegistry clientIdRegistry, @@ -55,6 +58,7 @@ public ConnectInMqttInMessageHandler( this.authenticationService = authenticationService; this.sessionService = sessionService; this.subscriptionService = subscriptionService; + this.connectToAuthRequestConverter = new ConnectToAuthRequestConverter(); } @Override @@ -73,8 +77,9 @@ protected void processValidMessage( ExternalNetworkMqttUser user, ConnectMqttInMessage message) { resolveClientConnectionConfig(user, message); + AuthRequest authRequest = connectToAuthRequestConverter.convert(message); authenticationService - .auth(message.username(), message.password()) + .authenticate(authRequest) .flatMap(ifTrue( user, message, this::registerClient, BAD_USER_NAME_OR_PASSWORD, connectAckReasonCode -> reject(user, connectAckReasonCode))) diff --git a/credentials-source-db/build.gradle b/credentials-source-db/build.gradle new file mode 100644 index 00000000..ca3c76fa --- /dev/null +++ b/credentials-source-db/build.gradle @@ -0,0 +1,19 @@ +plugins { + id("java-library") + id("configure-java") + id("groovy") +} + +description = "Database-based Credentials Source Provider" + +dependencies { + api projects.base + api projects.authenticationBasic + api libs.r2dbc.spi + api libs.r2dbc.pool + api libs.r2dbc.postgresql + api libs.spring.r2dbc + + testImplementation projects.testSupport + testFixturesApi projects.testSupport +} diff --git a/credentials-source-db/src/main/java/javasabr/mqtt/service/auth/source/R2dbcCredentialsSource.java b/credentials-source-db/src/main/java/javasabr/mqtt/service/auth/source/R2dbcCredentialsSource.java new file mode 100644 index 00000000..ee09847f --- /dev/null +++ b/credentials-source-db/src/main/java/javasabr/mqtt/service/auth/source/R2dbcCredentialsSource.java @@ -0,0 +1,34 @@ +package javasabr.mqtt.service.auth.source; + +import java.util.Arrays; +import lombok.RequiredArgsConstructor; +import org.springframework.r2dbc.core.DatabaseClient; +import reactor.core.publisher.Mono; + +@RequiredArgsConstructor +public class R2dbcCredentialsSource implements CredentialSource { + + private static final String CREDENTIALS_SOURCE_NAME = "database"; + private static final String PASSWORD_COLUMN = "password"; + private static final String USERNAME_BIND_PARAM = "$1"; + + private final DatabaseClient databaseClient; + private final String credentialsQuery; + + @Override + public String getName() { + return CREDENTIALS_SOURCE_NAME; + } + + @Override + public Mono isCredentialExists(String username, byte[] requestedPassword) { + return databaseClient + .sql(credentialsQuery) + .bind(USERNAME_BIND_PARAM, username) + .map(row -> row.get(PASSWORD_COLUMN, byte[].class)) + .all() + .singleOrEmpty() + .map(existingPassword -> Arrays.equals(existingPassword, requestedPassword)) + .defaultIfEmpty(false); + } +} diff --git a/credentials-source-db/src/main/java/javasabr/mqtt/service/auth/source/package-info.java b/credentials-source-db/src/main/java/javasabr/mqtt/service/auth/source/package-info.java new file mode 100644 index 00000000..67648f86 --- /dev/null +++ b/credentials-source-db/src/main/java/javasabr/mqtt/service/auth/source/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package javasabr.mqtt.service.auth.source; + +import org.jspecify.annotations.NullMarked; diff --git a/credentials-source-file/build.gradle b/credentials-source-file/build.gradle new file mode 100644 index 00000000..8be74f95 --- /dev/null +++ b/credentials-source-file/build.gradle @@ -0,0 +1,15 @@ +plugins { + id("java-library") + id("configure-java") + id("groovy") +} + +description = "File-based Credentials Source Provider" + +dependencies { + api projects.base + api projects.authenticationBasic + + testImplementation projects.testSupport + testFixturesApi projects.testSupport +} diff --git a/core-service/src/main/java/javasabr/mqtt/service/impl/FileCredentialsSource.java b/credentials-source-file/src/main/java/javasabr/mqtt/service/auth/source/FileCredentialsSource.java similarity index 70% rename from core-service/src/main/java/javasabr/mqtt/service/impl/FileCredentialsSource.java rename to credentials-source-file/src/main/java/javasabr/mqtt/service/auth/source/FileCredentialsSource.java index f7dd450f..e8b71d07 100644 --- a/core-service/src/main/java/javasabr/mqtt/service/impl/FileCredentialsSource.java +++ b/credentials-source-file/src/main/java/javasabr/mqtt/service/auth/source/FileCredentialsSource.java @@ -1,4 +1,4 @@ -package javasabr.mqtt.service.impl; +package javasabr.mqtt.service.auth.source; import java.io.FileInputStream; import java.io.IOException; @@ -7,11 +7,15 @@ import java.util.Properties; import javasabr.mqtt.model.exception.CredentialsSourceException; import javasabr.rlib.collections.dictionary.DictionaryCollectors; -import javasabr.rlib.collections.dictionary.RefToRefDictionary; +import lombok.AccessLevel; +import lombok.experimental.FieldDefaults; -public class FileCredentialsSource extends AbstractCredentialSource { +@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) +public class FileCredentialsSource extends InMemoryCredentialSource { - private final String fileName; + private static final String CREDENTIALS_SOURCE_NAME = "file"; + + String fileName; public FileCredentialsSource(String fileName) { this.fileName = fileName; @@ -32,16 +36,21 @@ void init() { var credentialsProperties = new Properties(); credentialsProperties.load(new FileInputStream(credentialUrl.getPath())); - RefToRefDictionary credentials = credentialsProperties + var credentials = credentialsProperties .entrySet() .stream() .collect(DictionaryCollectors.toRefToRefDictionary( entry -> entry.getKey().toString(), entry -> entry.getValue().toString().getBytes(StandardCharsets.UTF_8))); - putAll(credentials); + reset(credentials); } catch (IOException e) { throw new CredentialsSourceException(e); } } + + @Override + public String getName() { + return CREDENTIALS_SOURCE_NAME; + } } diff --git a/credentials-source-file/src/main/java/javasabr/mqtt/service/auth/source/package-info.java b/credentials-source-file/src/main/java/javasabr/mqtt/service/auth/source/package-info.java new file mode 100644 index 00000000..67648f86 --- /dev/null +++ b/credentials-source-file/src/main/java/javasabr/mqtt/service/auth/source/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package javasabr.mqtt.service.auth.source; + +import org.jspecify.annotations.NullMarked; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4295313c..342f92c2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,12 @@ junit-jupiter = "5.13.4" junit-platform-launcher = "1.13.4" # https://mvnrepository.com/artifact/io.projectreactor/reactor-core project-reactor = "3.7.8" +# https://mvnrepository.com/artifact/io.r2dbc/r2dbc-spi +r2dbc = "1.0.0.RELEASE" +# https://mvnrepository.com/artifact/io.r2dbc/r2dbc-pool +r2dbc-pool="1.0.2.RELEASE" +# https://mvnrepository.com/artifact/org.postgresql/r2dbc-postgresql +r2dbc-postgresql="1.1.1.RELEASE" # https://mvnrepository.com/artifact/org.spockframework/spock-core spock = "2.4-M6-groovy-4.0" # https://mvnrepository.com/artifact/org.apache.groovy/groovy-all @@ -45,12 +51,17 @@ springboot-starter-core = { module = "org.springframework.boot:spring-boot-start springboot-starter-autoconfigure = { module = "org.springframework.boot:spring-boot-autoconfigure", version.ref = "springboot" } springboot-starter-log4j2 = { module = "org.springframework.boot:spring-boot-starter-log4j2", version.ref = "springboot" } project-reactor-core = { module = "io.projectreactor:reactor-core", version.ref = "project-reactor" } +r2dbc-h2 = { module = "io.r2dbc:r2dbc-h2", version.ref = "r2dbc" } +r2dbc-spi = { module ='io.r2dbc:r2dbc-spi', version.ref = "r2dbc"} +r2dbc-pool = { module = 'io.r2dbc:r2dbc-pool', version.ref = "r2dbc-pool"} +r2dbc-postgresql= { module = 'org.postgresql:r2dbc-postgresql', version.ref = "r2dbc-postgresql"} jackson-core = { module = "tools.jackson.core:jackson-core", version.ref = "jackson" } jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" } jspecify = { module = "org.jspecify:jspecify", version.ref = "jspecify" } lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" } +spring-r2dbc = { module = "org.springframework:spring-r2dbc", version.ref = "spring" } spring-test = { module = "org.springframework:spring-test", version.ref = "spring" } spock-core = { module = "org.spockframework:spock-core", version.ref = "spock" } spock-spring = { module = "org.spockframework:spock-spring", version.ref = "spock" } diff --git a/model/src/main/java/javasabr/mqtt/model/auth/AuthRequest.java b/model/src/main/java/javasabr/mqtt/model/auth/AuthRequest.java new file mode 100644 index 00000000..989b53a1 --- /dev/null +++ b/model/src/main/java/javasabr/mqtt/model/auth/AuthRequest.java @@ -0,0 +1,3 @@ +package javasabr.mqtt.model.auth; + +public record AuthRequest(String username, byte[] password, String authenticationMethod, byte[] authenticationData) {} diff --git a/settings.gradle b/settings.gradle index e2843df4..4ad6e190 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,3 +13,7 @@ include(":test-coverage") include(":acl-groovy-dsl") include(":acl-engine") include(":acl-service") +include(":authentication") +include(":authentication-basic") +include(":credentials-source-file") +include(":credentials-source-db")