001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016 ForgeRock AS.
015 */
016package org.forgerock.opendj.security;
017
018import static java.util.Collections.singletonList;
019import static java.util.Collections.synchronizedMap;
020import static org.forgerock.opendj.ldap.Connections.newCachedConnectionPool;
021import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory;
022import static org.forgerock.opendj.ldap.LDAPConnectionFactory.AUTHN_BIND_REQUEST;
023import static org.forgerock.opendj.ldap.LdapException.newLdapException;
024import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
025import static org.forgerock.opendj.ldif.LDIF.copyTo;
026import static org.forgerock.opendj.ldif.LDIF.newEntryIteratorReader;
027import static org.forgerock.opendj.security.KeyStoreParameters.GLOBAL_PASSWORD;
028import static org.forgerock.opendj.security.KeyStoreParameters.newKeyStoreParameters;
029import static org.forgerock.opendj.security.OpenDJProviderSchema.SCHEMA;
030import static org.forgerock.util.Options.defaultOptions;
031
032import java.io.BufferedWriter;
033import java.io.File;
034import java.io.FileReader;
035import java.io.FileWriter;
036import java.io.IOException;
037import java.io.InputStreamReader;
038import java.io.Reader;
039import java.net.URI;
040import java.security.AccessController;
041import java.security.GeneralSecurityException;
042import java.security.KeyStore;
043import java.security.PrivilegedAction;
044import java.security.Provider;
045import java.security.ProviderException;
046import java.util.LinkedHashMap;
047import java.util.Map;
048import java.util.Properties;
049
050import org.forgerock.opendj.ldap.ConnectionFactory;
051import org.forgerock.opendj.ldap.DN;
052import org.forgerock.opendj.ldap.IntermediateResponseHandler;
053import org.forgerock.opendj.ldap.LDAPConnectionFactory;
054import org.forgerock.opendj.ldap.LdapException;
055import org.forgerock.opendj.ldap.LdapResultHandler;
056import org.forgerock.opendj.ldap.MemoryBackend;
057import org.forgerock.opendj.ldap.RequestContext;
058import org.forgerock.opendj.ldap.RequestHandler;
059import org.forgerock.opendj.ldap.ResultCode;
060import org.forgerock.opendj.ldap.SearchResultHandler;
061import org.forgerock.opendj.ldap.requests.AddRequest;
062import org.forgerock.opendj.ldap.requests.BindRequest;
063import org.forgerock.opendj.ldap.requests.CompareRequest;
064import org.forgerock.opendj.ldap.requests.DeleteRequest;
065import org.forgerock.opendj.ldap.requests.ExtendedRequest;
066import org.forgerock.opendj.ldap.requests.ModifyDNRequest;
067import org.forgerock.opendj.ldap.requests.ModifyRequest;
068import org.forgerock.opendj.ldap.requests.SearchRequest;
069import org.forgerock.opendj.ldap.responses.BindResult;
070import org.forgerock.opendj.ldap.responses.CompareResult;
071import org.forgerock.opendj.ldap.responses.ExtendedResult;
072import org.forgerock.opendj.ldap.responses.Result;
073import org.forgerock.opendj.ldif.LDIFEntryReader;
074import org.forgerock.opendj.ldif.LDIFEntryWriter;
075import org.forgerock.util.Factory;
076import org.forgerock.util.Options;
077
078/**
079 * The OpenDJ LDAP security provider which exposes an LDAP/LDIF based {@link java.security.KeyStore KeyStore}
080 * service, as well as providing utility methods facilitating construction of LDAP/LDIF based key stores. See the
081 * package documentation for more information.
082 */
083public final class OpenDJProvider extends Provider {
084    private static final long serialVersionUID = -1;
085    // Security provider configuration property names.
086    private static final String PREFIX = "org.forgerock.opendj.security.";
087    private static final String LDIF_PROPERTY = PREFIX + "ldif";
088    private static final String HOST_PROPERTY = PREFIX + "host";
089    private static final String PORT_PROPERTY = PREFIX + "port";
090    private static final String BIND_DN_PROPERTY = PREFIX + "bindDN";
091    private static final String BIND_PASSWORD_PROPERTY = PREFIX + "bindPassword";
092    private static final String KEYSTORE_BASE_DN_PROPERTY = PREFIX + "keyStoreBaseDN";
093    private static final String KEYSTORE_PASSWORD_PROPERTY = PREFIX + "keyStorePassword";
094    // Default key store configuration or null if key stores need explicit configuration.
095    private final KeyStoreParameters defaultConfig;
096
097    /** Creates a default LDAP security provider with no default key store configuration. */
098    public OpenDJProvider() {
099        this((KeyStoreParameters) null);
100    }
101
102    /**
103     * Creates a LDAP security provider with provided default key store configuration.
104     *
105     * @param configFile
106     *         The configuration file, which may be {@code null} indicating that key stores will be configured when they
107     *         are instantiated.
108     */
109    public OpenDJProvider(final String configFile) {
110        this(new File(configFile).toURI());
111    }
112
113    /**
114     * Creates a LDAP security provider with provided default key store configuration.
115     *
116     * @param configFile
117     *         The configuration file, which may be {@code null} indicating that key stores will be configured when they
118     *         are instantiated.
119     */
120    public OpenDJProvider(final URI configFile) {
121        this(configFile != null ? parseConfig(configFile) : null);
122    }
123
124    OpenDJProvider(final KeyStoreParameters defaultConfig) {
125        super("OpenDJ", 1.0D, "OpenDJ LDAP security provider");
126        this.defaultConfig = defaultConfig;
127        AccessController.doPrivileged(new PrivilegedAction<Void>() {
128            public Void run() {
129                putService(new KeyStoreService());
130                return null;
131            }
132        });
133    }
134
135    /**
136     * Creates a new LDAP key store with default options. The returned key store will already have been
137     * {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}.
138     *
139     * @param factory
140     *         The LDAP connection factory.
141     * @param baseDN
142     *         The DN of the subtree containing the LDAP key store.
143     * @return The LDAP key store.
144     */
145    public static KeyStore newLDAPKeyStore(final ConnectionFactory factory, final DN baseDN) {
146        return newLDAPKeyStore(factory, baseDN, defaultOptions());
147    }
148
149    /**
150     * Creates a new LDAP key store with custom options. The returned key store will already have been
151     * {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}.
152     *
153     * @param factory
154     *         The LDAP connection factory.
155     * @param baseDN
156     *         The DN of the subtree containing the LDAP key store.
157     * @param options
158     *         The optional key store parameters, including the cache configuration, key store password, and crypto
159     *         parameters.
160     * @return The LDAP key store.
161     * @see KeyStoreParameters For the list of available key store options.
162     */
163    public static KeyStore newLDAPKeyStore(final ConnectionFactory factory, final DN baseDN, final Options options) {
164        try {
165            final KeyStore keyStore = KeyStore.getInstance("LDAP", new OpenDJProvider());
166            keyStore.load(newKeyStoreParameters(factory, baseDN, options));
167            return keyStore;
168        } catch (GeneralSecurityException | IOException e) {
169            // Should not happen.
170            throw new RuntimeException(e);
171        }
172    }
173
174    /**
175     * Creates a new LDIF based key store which will read and write key store objects to the provided key store file.
176     * The LDIF file will be read during construction and re-written after each update. The returned key store will
177     * already have been {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}.
178     *
179     * @param ldifFile
180     *         The name of the LDIF file containing the key store objects.
181     * @param baseDN
182     *         The DN of the subtree containing the LDAP key store.
183     * @return The LDIF key store.
184     * @throws IOException
185     *         If an error occurred while reading the LDIF file.
186     */
187    public static KeyStore newLDIFKeyStore(final File ldifFile, final DN baseDN) throws IOException {
188        return newLDIFKeyStore(ldifFile, baseDN, defaultOptions());
189    }
190
191    /**
192     * Creates a new LDIF based key store which will read and write key store objects to the provided key store file.
193     * The LDIF file will be read during construction and re-written after each update. The returned key store will
194     * already have been {@link KeyStore#load(KeyStore.LoadStoreParameter) loaded}.
195     *
196     * @param ldifFile
197     *         The name of the LDIF file containing the key store objects.
198     * @param baseDN
199     *         The DN of the subtree containing the LDAP key store.
200     * @param options
201     *         The optional key store parameters, including the cache configuration, key store password, and crypto
202     *         parameters.
203     * @return The LDIF key store.
204     * @throws IOException
205     *         If an error occurred while reading the LDIF file.
206     */
207    public static KeyStore newLDIFKeyStore(final File ldifFile, final DN baseDN, final Options options)
208            throws IOException {
209        return newLDAPKeyStore(newLDIFConnectionFactory(ldifFile), baseDN, options);
210    }
211
212    private static ConnectionFactory newLDIFConnectionFactory(final File ldifFile) throws IOException {
213        try (LDIFEntryReader reader = new LDIFEntryReader(new FileReader(ldifFile)).setSchema(SCHEMA)) {
214            final MemoryBackend backend = new MemoryBackend(SCHEMA, reader).enableVirtualAttributes(true);
215            return newInternalConnectionFactory(new WriteLDIFOnUpdateRequestHandler(backend, ldifFile));
216        }
217    }
218
219    /**
220     * Creates a new key store object cache which will delegate to the provided {@link Map}. It is the responsibility
221     * of the map implementation to perform cache eviction if needed. The provided map MUST be thread-safe.
222     *
223     * @param map
224     *         The thread-safe {@link Map} implementation in which key store objects will be stored.
225     * @return The new key store object cache.
226     */
227    public static KeyStoreObjectCache newKeyStoreObjectCacheFromMap(final Map<String, KeyStoreObject> map) {
228        return new KeyStoreObjectCache() {
229            @Override
230            public void put(final KeyStoreObject keyStoreObject) {
231                map.put(keyStoreObject.getAlias(), keyStoreObject);
232            }
233
234            @Override
235            public KeyStoreObject get(final String alias) {
236                return map.get(alias);
237            }
238        };
239    }
240
241    /**
242     * Creates a new fixed capacity key store object cache which will evict objects once it reaches the
243     * provided capacity. This implementation is only intended for simple use cases and is not particularly scalable.
244     *
245     * @param capacity
246     *         The maximum number of key store objects that will be cached before eviction occurs.
247     * @return The new key store object cache.
248     */
249    public static KeyStoreObjectCache newCapacityBasedKeyStoreObjectCache(final int capacity) {
250        return newKeyStoreObjectCacheFromMap(synchronizedMap(new LinkedHashMap<String, KeyStoreObject>() {
251            private static final long serialVersionUID = -1;
252
253            @Override
254            protected boolean removeEldestEntry(final Map.Entry<String, KeyStoreObject> eldest) {
255                return size() > capacity;
256            }
257        }));
258    }
259
260    /**
261     * Returns a password factory which will return a copy of the provided password for each invocation of
262     * {@link Factory#newInstance()}, and which does not provide any protection of the in memory representation of
263     * the password.
264     *
265     * @param password
266     *         The password or {@code null} if no password should ever be returned.
267     * @return A password factory which will return a copy of the provided password.
268     */
269    public static Factory<char[]> newClearTextPasswordFactory(final char[] password) {
270        return new Factory<char[]>() {
271            private final char[] clonedPassword = password != null ? password.clone() : null;
272
273            @Override
274            public char[] newInstance() {
275                return clonedPassword != null ? clonedPassword.clone() : null;
276            }
277        };
278    }
279
280    KeyStoreParameters getDefaultConfig() {
281        return defaultConfig;
282    }
283
284    private static KeyStoreParameters parseConfig(final URI configFile) {
285        try (final Reader configFileReader = new InputStreamReader(configFile.toURL().openStream())) {
286            final Properties properties = new Properties();
287            properties.load(configFileReader);
288
289            final String keyStoreBaseDNProperty = properties.getProperty(KEYSTORE_BASE_DN_PROPERTY);
290            if (keyStoreBaseDNProperty == null) {
291                throw new IllegalArgumentException("missing key store base DN");
292            }
293            final DN keyStoreBaseDN = DN.valueOf(keyStoreBaseDNProperty);
294
295            final Options keystoreOptions = defaultOptions();
296            final String keystorePassword = properties.getProperty(KEYSTORE_PASSWORD_PROPERTY);
297            if (keystorePassword != null) {
298                keystoreOptions.set(GLOBAL_PASSWORD, newClearTextPasswordFactory(keystorePassword.toCharArray()));
299            }
300
301            final ConnectionFactory factory;
302            final String ldif = properties.getProperty(LDIF_PROPERTY);
303            if (ldif != null) {
304                factory = newLDIFConnectionFactory(new File(ldif));
305            } else {
306                final String host = properties.getProperty(HOST_PROPERTY, "localhost");
307                final int port = Integer.parseInt(properties.getProperty(PORT_PROPERTY, "389"));
308                final DN bindDN = DN.valueOf(properties.getProperty(BIND_DN_PROPERTY, ""));
309                final String bindPassword = properties.getProperty(BIND_PASSWORD_PROPERTY);
310
311                final Options factoryOptions = defaultOptions();
312                if (!bindDN.isRootDN()) {
313                    factoryOptions.set(AUTHN_BIND_REQUEST,
314                                       newSimpleBindRequest(bindDN.toString(), bindPassword.toCharArray()));
315                }
316                factory = newCachedConnectionPool(new LDAPConnectionFactory(host, port, factoryOptions));
317            }
318
319            return newKeyStoreParameters(factory, keyStoreBaseDN, keystoreOptions);
320        } catch (Exception e) {
321            throw new ProviderException("Error parsing configuration in file '" + configFile + "'", e);
322        }
323    }
324
325    private static final class WriteLDIFOnUpdateRequestHandler implements RequestHandler<RequestContext> {
326        private final MemoryBackend backend;
327        private final File ldifFile;
328
329        private WriteLDIFOnUpdateRequestHandler(final MemoryBackend backend, final File ldifFile) {
330            this.backend = backend;
331            this.ldifFile = ldifFile;
332        }
333
334        @Override
335        public void handleAdd(final RequestContext requestContext, final AddRequest request,
336                              final IntermediateResponseHandler intermediateResponseHandler,
337                              final LdapResultHandler<Result> resultHandler) {
338            backend.handleAdd(requestContext, request, intermediateResponseHandler, saveAndForwardTo(resultHandler));
339        }
340
341        @Override
342        public void handleBind(final RequestContext requestContext, final int version, final BindRequest request,
343                               final IntermediateResponseHandler intermediateResponseHandler,
344                               final LdapResultHandler<BindResult> resultHandler) {
345            backend.handleBind(requestContext, version, request, intermediateResponseHandler, resultHandler);
346        }
347
348        @Override
349        public void handleCompare(final RequestContext requestContext, final CompareRequest request,
350                                  final IntermediateResponseHandler intermediateResponseHandler,
351                                  final LdapResultHandler<CompareResult> resultHandler) {
352            backend.handleCompare(requestContext, request, intermediateResponseHandler, resultHandler);
353        }
354
355        @Override
356        public void handleDelete(final RequestContext requestContext, final DeleteRequest request,
357                                 final IntermediateResponseHandler intermediateResponseHandler,
358                                 final LdapResultHandler<Result> resultHandler) {
359            backend.handleDelete(requestContext, request, intermediateResponseHandler, saveAndForwardTo(resultHandler));
360        }
361
362        @Override
363        public <R extends ExtendedResult> void handleExtendedRequest(final RequestContext requestContext,
364                                                                     final ExtendedRequest<R> request,
365                                                                     final IntermediateResponseHandler
366                                                                                     intermediateResponseHandler,
367                                                                     final LdapResultHandler<R> resultHandler) {
368            backend.handleExtendedRequest(requestContext, request, intermediateResponseHandler, resultHandler);
369        }
370
371        @Override
372        public void handleModify(final RequestContext requestContext, final ModifyRequest request,
373                                 final IntermediateResponseHandler intermediateResponseHandler,
374                                 final LdapResultHandler<Result> resultHandler) {
375            backend.handleModify(requestContext, request, intermediateResponseHandler, saveAndForwardTo(resultHandler));
376        }
377
378        @Override
379        public void handleModifyDN(final RequestContext requestContext, final ModifyDNRequest request,
380                                   final IntermediateResponseHandler intermediateResponseHandler,
381                                   final LdapResultHandler<Result> resultHandler) {
382            backend.handleModifyDN(requestContext,
383                                   request,
384                                   intermediateResponseHandler,
385                                   saveAndForwardTo(resultHandler));
386        }
387
388        @Override
389        public void handleSearch(final RequestContext requestContext, final SearchRequest request,
390                                 final IntermediateResponseHandler intermediateResponseHandler,
391                                 final SearchResultHandler entryHandler,
392                                 final LdapResultHandler<Result> resultHandler) {
393            backend.handleSearch(requestContext, request, intermediateResponseHandler, entryHandler, resultHandler);
394        }
395
396        private LdapResultHandler<Result> saveAndForwardTo(final LdapResultHandler<Result> resultHandler) {
397            return new LdapResultHandler<Result>() {
398                @Override
399                public void handleException(final LdapException exception) {
400                    resultHandler.handleException(exception);
401                }
402
403                @Override
404                public void handleResult(final Result result) {
405                    try {
406                        writeLDIF(backend, ldifFile);
407                        resultHandler.handleResult(result);
408                    } catch (IOException e) {
409                        final LdapException ldapException =
410                                newLdapException(ResultCode.OTHER, "Unable to write LDIF file " + ldifFile, e);
411                        resultHandler.handleException(ldapException);
412                    }
413                }
414            };
415        }
416
417        private static void writeLDIF(final MemoryBackend backend, final File ldifFile) throws IOException {
418            try (final FileWriter fileWriter = new FileWriter(ldifFile);
419                 final BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
420                 final LDIFEntryWriter entryWriter = new LDIFEntryWriter(bufferedWriter)) {
421                copyTo(newEntryIteratorReader(backend.getAll().iterator()), entryWriter);
422            }
423        }
424    }
425
426    private final class KeyStoreService extends Service {
427        private KeyStoreService() {
428            super(OpenDJProvider.this, "KeyStore", "LDAP", KeyStoreImpl.class.getName(), singletonList("OpenDJ"), null);
429        }
430
431        // Override the default constructor so that we can pass in this provider and any file based configuration.
432        @Override
433        public Object newInstance(final Object constructorParameter) {
434            return new KeyStoreImpl(OpenDJProvider.this);
435        }
436    }
437}