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}