001/* 002 * Copyright 2012-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.autoconfigure.ldap.embedded; 018 019import java.io.InputStream; 020import java.util.Collections; 021import java.util.HashMap; 022import java.util.List; 023import java.util.Map; 024 025import javax.annotation.PreDestroy; 026 027import com.unboundid.ldap.listener.InMemoryDirectoryServer; 028import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; 029import com.unboundid.ldap.listener.InMemoryListenerConfig; 030import com.unboundid.ldap.sdk.LDAPException; 031import com.unboundid.ldap.sdk.schema.Schema; 032import com.unboundid.ldif.LDIFReader; 033 034import org.springframework.boot.autoconfigure.AutoConfigureBefore; 035import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 036import org.springframework.boot.autoconfigure.condition.ConditionMessage; 037import org.springframework.boot.autoconfigure.condition.ConditionMessage.Builder; 038import org.springframework.boot.autoconfigure.condition.ConditionOutcome; 039import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 040import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 041import org.springframework.boot.autoconfigure.condition.SpringBootCondition; 042import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration; 043import org.springframework.boot.autoconfigure.ldap.LdapProperties; 044import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapProperties.Credential; 045import org.springframework.boot.context.properties.EnableConfigurationProperties; 046import org.springframework.boot.context.properties.bind.Bindable; 047import org.springframework.boot.context.properties.bind.Binder; 048import org.springframework.context.ApplicationContext; 049import org.springframework.context.ConfigurableApplicationContext; 050import org.springframework.context.annotation.Bean; 051import org.springframework.context.annotation.ConditionContext; 052import org.springframework.context.annotation.Conditional; 053import org.springframework.context.annotation.Configuration; 054import org.springframework.context.annotation.DependsOn; 055import org.springframework.core.env.Environment; 056import org.springframework.core.env.MapPropertySource; 057import org.springframework.core.env.MutablePropertySources; 058import org.springframework.core.env.PropertySource; 059import org.springframework.core.io.Resource; 060import org.springframework.core.type.AnnotatedTypeMetadata; 061import org.springframework.ldap.core.support.LdapContextSource; 062import org.springframework.util.StringUtils; 063 064/** 065 * {@link EnableAutoConfiguration Auto-configuration} for Embedded LDAP. 066 * 067 * @author EddĂș MelĂ©ndez 068 * @author Mathieu Ouellet 069 * @author Raja Kolli 070 * @since 1.5.0 071 */ 072@Configuration 073@EnableConfigurationProperties({ LdapProperties.class, EmbeddedLdapProperties.class }) 074@AutoConfigureBefore(LdapAutoConfiguration.class) 075@ConditionalOnClass(InMemoryDirectoryServer.class) 076@Conditional(EmbeddedLdapAutoConfiguration.EmbeddedLdapCondition.class) 077public class EmbeddedLdapAutoConfiguration { 078 079 private static final String PROPERTY_SOURCE_NAME = "ldap.ports"; 080 081 private final EmbeddedLdapProperties embeddedProperties; 082 083 private final LdapProperties properties; 084 085 private final ConfigurableApplicationContext applicationContext; 086 087 private final Environment environment; 088 089 private InMemoryDirectoryServer server; 090 091 public EmbeddedLdapAutoConfiguration(EmbeddedLdapProperties embeddedProperties, 092 LdapProperties properties, ConfigurableApplicationContext applicationContext, 093 Environment environment) { 094 this.embeddedProperties = embeddedProperties; 095 this.properties = properties; 096 this.applicationContext = applicationContext; 097 this.environment = environment; 098 } 099 100 @Bean 101 @DependsOn("directoryServer") 102 @ConditionalOnMissingBean 103 public LdapContextSource ldapContextSource() { 104 LdapContextSource source = new LdapContextSource(); 105 if (hasCredentials(this.embeddedProperties.getCredential())) { 106 source.setUserDn(this.embeddedProperties.getCredential().getUsername()); 107 source.setPassword(this.embeddedProperties.getCredential().getPassword()); 108 } 109 source.setUrls(this.properties.determineUrls(this.environment)); 110 return source; 111 } 112 113 @Bean 114 public InMemoryDirectoryServer directoryServer() throws LDAPException { 115 String[] baseDn = StringUtils.toStringArray(this.embeddedProperties.getBaseDn()); 116 InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(baseDn); 117 if (hasCredentials(this.embeddedProperties.getCredential())) { 118 config.addAdditionalBindCredentials( 119 this.embeddedProperties.getCredential().getUsername(), 120 this.embeddedProperties.getCredential().getPassword()); 121 } 122 setSchema(config); 123 InMemoryListenerConfig listenerConfig = InMemoryListenerConfig 124 .createLDAPConfig("LDAP", this.embeddedProperties.getPort()); 125 config.setListenerConfigs(listenerConfig); 126 this.server = new InMemoryDirectoryServer(config); 127 importLdif(); 128 this.server.startListening(); 129 setPortProperty(this.applicationContext, this.server.getListenPort()); 130 return this.server; 131 } 132 133 private void setSchema(InMemoryDirectoryServerConfig config) { 134 if (!this.embeddedProperties.getValidation().isEnabled()) { 135 config.setSchema(null); 136 return; 137 } 138 Resource schema = this.embeddedProperties.getValidation().getSchema(); 139 if (schema != null) { 140 setSchema(config, schema); 141 } 142 } 143 144 private void setSchema(InMemoryDirectoryServerConfig config, Resource resource) { 145 try { 146 Schema defaultSchema = Schema.getDefaultStandardSchema(); 147 Schema schema = Schema.getSchema(resource.getInputStream()); 148 config.setSchema(Schema.mergeSchemas(defaultSchema, schema)); 149 } 150 catch (Exception ex) { 151 throw new IllegalStateException( 152 "Unable to load schema " + resource.getDescription(), ex); 153 } 154 } 155 156 private boolean hasCredentials(Credential credential) { 157 return StringUtils.hasText(credential.getUsername()) 158 && StringUtils.hasText(credential.getPassword()); 159 } 160 161 private void importLdif() throws LDAPException { 162 String location = this.embeddedProperties.getLdif(); 163 if (StringUtils.hasText(location)) { 164 try { 165 Resource resource = this.applicationContext.getResource(location); 166 if (resource.exists()) { 167 try (InputStream inputStream = resource.getInputStream()) { 168 this.server.importFromLDIF(true, new LDIFReader(inputStream)); 169 } 170 } 171 } 172 catch (Exception ex) { 173 throw new IllegalStateException("Unable to load LDIF " + location, ex); 174 } 175 } 176 } 177 178 private void setPortProperty(ApplicationContext context, int port) { 179 if (context instanceof ConfigurableApplicationContext) { 180 MutablePropertySources sources = ((ConfigurableApplicationContext) context) 181 .getEnvironment().getPropertySources(); 182 getLdapPorts(sources).put("local.ldap.port", port); 183 } 184 if (context.getParent() != null) { 185 setPortProperty(context.getParent(), port); 186 } 187 } 188 189 @SuppressWarnings("unchecked") 190 private Map<String, Object> getLdapPorts(MutablePropertySources sources) { 191 PropertySource<?> propertySource = sources.get(PROPERTY_SOURCE_NAME); 192 if (propertySource == null) { 193 propertySource = new MapPropertySource(PROPERTY_SOURCE_NAME, new HashMap<>()); 194 sources.addFirst(propertySource); 195 } 196 return (Map<String, Object>) propertySource.getSource(); 197 } 198 199 @PreDestroy 200 public void close() { 201 if (this.server != null) { 202 this.server.shutDown(true); 203 } 204 } 205 206 /** 207 * {@link SpringBootCondition} to determine when to apply embedded LDAP 208 * auto-configuration. 209 */ 210 static class EmbeddedLdapCondition extends SpringBootCondition { 211 212 private static final Bindable<List<String>> STRING_LIST = Bindable 213 .listOf(String.class); 214 215 @Override 216 public ConditionOutcome getMatchOutcome(ConditionContext context, 217 AnnotatedTypeMetadata metadata) { 218 Builder message = ConditionMessage.forCondition("Embedded LDAP"); 219 Environment environment = context.getEnvironment(); 220 if (environment != null && !Binder.get(environment) 221 .bind("spring.ldap.embedded.base-dn", STRING_LIST) 222 .orElseGet(Collections::emptyList).isEmpty()) { 223 return ConditionOutcome.match(message.because("Found base-dn property")); 224 } 225 return ConditionOutcome.noMatch(message.because("No base-dn property found")); 226 } 227 228 } 229 230}