Skip to content

Commit

Permalink
feat: Added support for anonymous access
Browse files Browse the repository at this point in the history
Closes #164
  • Loading branch information
devatherock committed Dec 3, 2024
1 parent f9c4bb1 commit 27eb537
Show file tree
Hide file tree
Showing 10 changed files with 123 additions and 41 deletions.
16 changes: 4 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
# Changelog

## [Unreleased]
### Added
- [#164](https://github.com/devatherock/ldap-search-api/issues/164): Support for anonymous access

### Changed
- chore(deps): update plugin io.micronaut.application to v4.4.3
- fix(deps): update dependency ch.qos.logback:logback-classic to v1.5.9
- fix(deps): update dependency net.bytebuddy:byte-buddy to v1.15.4
- fix(deps): update dependency ch.qos.logback:logback-classic to v1.5.10
- fix(deps): update dependency ch.qos.logback:logback-classic to v1.5.11
- fix(deps): update dependency net.bytebuddy:byte-buddy to v1.15.5
- fix(deps): update dependency ch.qos.logback:logback-classic to v1.5.12
- fix(deps): update dependency net.bytebuddy:byte-buddy to v1.15.7
- fix(deps): update dependency net.bytebuddy:byte-buddy to v1.15.8
- fix(deps): update dependency net.bytebuddy:byte-buddy to v1.15.9
- chore(deps): update plugin io.micronaut.application to v4.4.4
- fix(deps): update dependency net.bytebuddy:byte-buddy to v1.15.10
- fix(deps): update dependency org.apache.groovy:groovy-json to v4.0.24
- fix(deps): update dependency org.projectlombok:lombok to v1.18.36
- chore(deps): update dependency gradle to v8.11
- chore(deps): update dependency gradle to v8.11.1
- chore(deps): update plugin org.sonarqube to v6
- chore(deps): update plugin org.sonarqube to v6.0.1.5171

## [2.3.0] - 2024-09-29
Expand Down Expand Up @@ -131,4 +123,4 @@ with `LOGGER_LEVELS` prefix supported out of the box by micronaut
- Initial version. REST endpoint to query a LDAP server

### Changed
- The response to a `List` of maps from a `Map`
- The response to a `List` of maps from a `Map`
39 changes: 20 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,25 @@ docker run --rm \

### Configurable properties

| Environment Variable Name | YAML Variable Name | Required | Default | Description |
|---------------------------------------|---------------------------------------|----------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| LDAP_HOST | ldap.host | true | (None) | The full host name of the LDAP server. Example: `ldaps://ldap.jumpcloud.com:636` |
| LDAP_USERNAME | ldap.username | true | (None) | The LDAP bind username. Could be a simple username like `devatherock` or a DN like `uid=devatherock,ou=Users,dc=jumpcloud,dc=com` depending on how the LDAP server is configured |
| LDAP_PASSWORD | ldap.password | true | (None) | The LDAP bind password |
| LDAP_BASE_DN | ldap.base-dn | false | (None) | The default base DN to search against |
| LDAP_BINARY_ATTRIBUTES | ldap.binary-attributes | false | (None) | Attributes in the search result that have binary values |
| LDAP_READ_TIMEOUT_MILLIS | ldap.read-timeout-millis | false | 10000 | Read timeout for the search, in milliseconds. Defaults to 10 seconds |
| LDAP_CONNECTION_POOL_ENABLED | ldap.connection-pool.enabled | false | true | Indicates if a connection pool should be used |
| LDAP_CONNECTION_POOL_CORE_SIZE | ldap.connection-pool.core-size | false | 8 | Initial size of the connection pool |
| LDAP_CONNECTION_POOL_MAX_SIZE | ldap.connection-pool.max-size | false | 8 | Maximum size of the connection pool |
| LDAP_CONNECTION_POOL_TIME_TO_LIVE_MILLIS | ldap.connection-pool.timeToLiveMillis | false | 1,800,000 | The total time a connection in the pool will be kept open, in milliseconds. Defaults to 30 minutes |
| LOGGER_LEVELS_ROOT | (None) | false | INFO | [SLF4J](http://www.slf4j.org/api/org/apache/commons/logging/Log.html) log level, for all(framework and custom) code |
| LOGGER_LEVELS_IO_GITHUB_DEVATHEROCK | (None) | false | INFO | [SLF4J](http://www.slf4j.org/api/org/apache/commons/logging/Log.html) log level, for custom code |
| MICRONAUT_SERVER_PORT | micronaut.server.port | false | 8080 | Port in which the app listens on |
| MICRONAUT_CONFIG_FILES | (None) | false | (None) | Path to YAML config files. The YAML files can be used to specify complex, object and array properties |
| JACKSON_SERIALIZATION_INDENT_OUTPUT | jackson.serialization.indent-output | false | (None) | Set to `true` to enable JSON pretty-print of response |
| LOGBACK_CONFIGURATION_FILE | (None) | false | (None) | Path to logback configuration file |
| Environment Variable Name | YAML Variable Name | Required | Default | Description |
|------------------------------------------|---------------------------------------|----------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| LDAP_HOST | ldap.host | true | (None) | The full host name of the LDAP server. Example: `ldaps://ldap.jumpcloud.com:636` |
| LDAP_USERNAME | ldap.username | false | (None) | The LDAP bind username. Could be a simple username like `devatherock` or a DN like `uid=devatherock,ou=Users,dc=jumpcloud,dc=com` depending on how the LDAP server is configured |
| LDAP_PASSWORD | ldap.password | false | (None) | The LDAP bind password |
| LDAP_AUTH_TYPE | ldap.auth-type | false | simple | Value for the `java.naming.security.authentication` property. Supported values are `none`, `simple`. Defaults to `simple` |
| LDAP_BASE_DN | ldap.base-dn | false | (None) | The default base DN to search against |
| LDAP_BINARY_ATTRIBUTES | ldap.binary-attributes | false | (None) | Attributes in the search result that have binary values |
| LDAP_READ_TIMEOUT_MILLIS | ldap.read-timeout-millis | false | 10000 | Read timeout for the search, in milliseconds. Defaults to 10 seconds |
| LDAP_CONNECTION_POOL_ENABLED | ldap.connection-pool.enabled | false | true | Indicates if a connection pool should be used |
| LDAP_CONNECTION_POOL_CORE_SIZE | ldap.connection-pool.core-size | false | 8 | Initial size of the connection pool |
| LDAP_CONNECTION_POOL_MAX_SIZE | ldap.connection-pool.max-size | false | 8 | Maximum size of the connection pool |
| LDAP_CONNECTION_POOL_TIME_TO_LIVE_MILLIS | ldap.connection-pool.timeToLiveMillis | false | 1,800,000 | The total time a connection in the pool will be kept open, in milliseconds. Defaults to 30 minutes |
| LOGGER_LEVELS_ROOT | (None) | false | INFO | [SLF4J](http://www.slf4j.org/api/org/apache/commons/logging/Log.html) log level, for all(framework and custom) code |
| LOGGER_LEVELS_IO_GITHUB_DEVATHEROCK | (None) | false | INFO | [SLF4J](http://www.slf4j.org/api/org/apache/commons/logging/Log.html) log level, for custom code |
| MICRONAUT_SERVER_PORT | micronaut.server.port | false | 8080 | Port in which the app listens on |
| MICRONAUT_CONFIG_FILES | (None) | false | (None) | Path to YAML config files. The YAML files can be used to specify complex, object and array properties |
| JACKSON_SERIALIZATION_INDENT_OUTPUT | jackson.serialization.indent-output | false | (None) | Set to `true` to enable JSON pretty-print of response |
| LOGBACK_CONFIGURATION_FILE | (None) | false | (None) | Path to logback configuration file |

### API spec
When the app is running, detailed API documentation can be accessed at `{host}/swagger-ui` or `{host}/swagger/ldap-search-api-{version}.yml`. The available endpoints are listed below for reference:
Expand Down Expand Up @@ -80,4 +81,4 @@ and set the environment variable `LOGBACK_CONFIGURATION_FILE` to `/path/to/custo

### JSON logs

Refer [logstash-logback-encoder](https://github.com/logstash/logstash-logback-encoder) documentation to customize the field names and formats in the log. To output logs as JSON, set the environment variable `LOGBACK_CONFIGURATION_FILE` to `logback-json.xml`
Refer [logstash-logback-encoder](https://github.com/logstash/logstash-logback-encoder) documentation to customize the field names and formats in the log. To output logs as JSON, set the environment variable `LOGBACK_CONFIGURATION_FILE` to `logback-json.xml`
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
import io.micronaut.context.annotation.ConfigurationProperties;
import io.micronaut.context.annotation.Context;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.util.StringUtils;
import jakarta.annotation.PostConstruct;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import lombok.AccessLevel;
import lombok.Getter;
Expand Down Expand Up @@ -37,15 +39,19 @@ public class LdapProperties {
* or a DN like {@code uid=devatherock,ou=Users,dc=jumpcloud,dc=com} depending
* on how the LDAP server is configured
*/
@NotBlank(message = "ldap.username not specified")
private String username;

/**
* The LDAP bind password
*/
@NotBlank(message = "ldap.password not specified")
private String password;

/**
* Value for the {@code java.naming.security.authentication} property. Defaults
* to {@code simple}
*/
private AuthenticationType authType = AuthenticationType.SIMPLE;

/**
* The default base DN to search against
*/
Expand All @@ -65,6 +71,12 @@ public class LdapProperties {

private LdapConnectionPoolProperties connectionPool = new LdapConnectionPoolProperties();

@AssertTrue(message = "ldap.username or ldap.password not specified")
public boolean isValidLdapCredentials() {
return AuthenticationType.NONE.equals(authType) ||
(StringUtils.hasText(username) && StringUtils.hasText(password));
}

/**
* Connection pool configuration
*
Expand Down Expand Up @@ -97,6 +109,18 @@ public static class LdapConnectionPoolProperties {
private long timeToLiveMillis = 1_800_000;
}

@Getter
public enum AuthenticationType {
NONE("none"),
SIMPLE("simple");

private final String code;

AuthenticationType(String code) {
this.code = code;
}
}

@PostConstruct
public void init() {
combinedBinaryAttributes = String.join(" ", binaryAttributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import javax.naming.ldap.LdapContext;

import io.github.devatherock.ldapsearch.config.LdapProperties;
import io.github.devatherock.ldapsearch.config.LdapProperties.AuthenticationType;

import io.micronaut.core.util.CollectionUtils;
import jakarta.inject.Singleton;
Expand Down Expand Up @@ -98,9 +99,13 @@ private Hashtable<String, String> initializeLdapEnvironment() {
Hashtable<String, String> ldapEnv = new Hashtable<String, String>();
ldapEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
ldapEnv.put(Context.PROVIDER_URL, config.getHost());
ldapEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
ldapEnv.put(Context.SECURITY_PRINCIPAL, config.getUsername());
ldapEnv.put(Context.SECURITY_CREDENTIALS, config.getPassword());
ldapEnv.put(Context.SECURITY_AUTHENTICATION, config.getAuthType().getCode());

if (AuthenticationType.SIMPLE.equals(config.getAuthType())) {
ldapEnv.put(Context.SECURITY_PRINCIPAL, config.getUsername());
ldapEnv.put(Context.SECURITY_CREDENTIALS, config.getPassword());
}

ldapEnv.put("com.sun.jndi.ldap.connect.pool", String.valueOf(config.getConnectionPool().isEnabled()));
ldapEnv.put("com.sun.jndi.ldap.read.timeout", String.valueOf(config.getReadTimeoutMillis()));

Expand Down Expand Up @@ -197,4 +202,4 @@ private Object formatAttributeValue(Object attributeValue) {
return attributeValue;
}
}
}
}
5 changes: 3 additions & 2 deletions src/main/resources/logback-json.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<logger name="io.micronaut.http.server.netty.NettyHttpServer"
level="TRACE" />
<logger
name="io.micronaut.http.server.netty.NettyRequestLifecycle"
level="DEBUG" />
</configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package io.github.devatherock.ldapsearch.config

import org.slf4j.LoggerFactory

import io.github.devatherock.ldapsearch.config.LdapProperties.AuthenticationType

import ch.qos.logback.classic.Level
import spock.lang.Specification
import spock.lang.Subject
Expand Down Expand Up @@ -36,4 +38,27 @@ class LdapPropertiesSpec extends Specification {
logger.setLevel(Level.INFO)
System.clearProperty(connectionPoolDebugProperty)
}

void 'test is valid ldap credentials'() {
given:
config.authType = authType
config.username = username
config.password = password

when:
boolean actual = config.isValidLdapCredentials()

then:
actual == expected

where:
username | password | authType || expected
'dummy' | 'dummy' | AuthenticationType.SIMPLE || true
'' | '' | AuthenticationType.SIMPLE || false
'dummy' | null | AuthenticationType.SIMPLE || false
null | 'dummy' | AuthenticationType.SIMPLE || false
'' | '' | AuthenticationType.NONE || true
'dummy' | 'dummy' | AuthenticationType.NONE || true
null | null | AuthenticationType.NONE || true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.github.devatherock.ldapsearch.controllers

import io.micronaut.test.extensions.spock.annotation.MicronautTest

/**
* Unit test for {@link LdapSearchController} without auth
*/
@MicronautTest(propertySources = 'classpath:application-test_no_auth.yml')
class LdapSearchControllerNoAuthTestSpec extends LdapSearchControllerSpec {

protected String getExpectedJson() {
'[{"userPassword":"YWJjZGU=","uid":"sclaus","objectClass":["top","person","organizationalPerson","inetOrgPerson"],"sn":"Claus","cn":"Santa Claus"}]'
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.devatherock.ldapsearch.controllers

import groovy.json.JsonSlurper
import groovy.util.logging.Slf4j

import com.unboundid.ldap.listener.InMemoryDirectoryServer
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig
Expand All @@ -17,6 +18,7 @@ import spock.lang.Specification
/**
* Test class for {@link LdapSearchController}
*/
@Slf4j
abstract class LdapSearchControllerSpec extends Specification {

@Shared
Expand All @@ -31,17 +33,21 @@ abstract class LdapSearchControllerSpec extends Specification {
void setupSpec() {
InMemoryDirectoryServerConfig config =
new InMemoryDirectoryServerConfig('dc=example,dc=com')
config.addAdditionalBindCredentials('cn=Directory Manager', 'testpwd')
config.setListenerConfigs(
new InMemoryListenerConfig('testListener', null, 33389,
null, null, null))
customizeLdapConfig(config)

directoryServer = new InMemoryDirectoryServer(config)
directoryServer.importFromLDIF(true,
new File(LdapSearchControllerSpec.class.classLoader.getResource('test.ldif').toURI()))
directoryServer.startListening()
}

protected void customizeLdapConfig(InMemoryDirectoryServerConfig config) {
log.debug("Customizing ldap config {}", config) // To suppress codeNarc unused method parameter violation
}

void cleanupSpec() {
directoryServer.shutDown(true)
}
Expand Down
Loading

0 comments on commit 27eb537

Please sign in to comment.