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.opends.server.core;
017
018import static org.forgerock.http.routing.RouteMatchers.requestUriMatcher;
019import static org.forgerock.opendj.rest2ldap.authz.Authorization.newAuthorizationFilter;
020import static org.forgerock.util.Reject.checkNotNull;
021import static org.opends.messages.ConfigMessages.ERR_CONFIG_HTTPENDPOINT_CONFLICTING_AUTHZ_DN;
022import static org.opends.messages.ConfigMessages.ERR_CONFIG_HTTPENDPOINT_INITIALIZATION_FAILED;
023import static org.opends.messages.ConfigMessages.ERR_CONFIG_HTTPENDPOINT_INVALID_AUTHZ_DN;
024import static org.opends.messages.ConfigMessages.ERR_CONFIG_HTTPENDPOINT_UNABLE_TO_START;
025import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
026
027import java.util.Collection;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Set;
032import java.util.SortedSet;
033import java.util.TreeSet;
034
035import org.forgerock.http.Handler;
036import org.forgerock.http.HttpApplication;
037import org.forgerock.http.HttpApplicationException;
038import org.forgerock.http.handler.Handlers;
039import org.forgerock.http.protocol.Request;
040import org.forgerock.http.protocol.Response;
041import org.forgerock.http.protocol.Status;
042import org.forgerock.http.routing.Router;
043import org.forgerock.http.routing.RoutingMode;
044import org.forgerock.i18n.LocalizableMessage;
045import org.forgerock.i18n.slf4j.LocalizedLogger;
046import org.forgerock.opendj.config.server.ConfigChangeResult;
047import org.forgerock.opendj.config.server.ConfigException;
048import org.forgerock.opendj.config.server.ConfigurationAddListener;
049import org.forgerock.opendj.config.server.ConfigurationChangeListener;
050import org.forgerock.opendj.config.server.ConfigurationDeleteListener;
051import org.forgerock.opendj.ldap.DN;
052import org.forgerock.opendj.ldap.ResultCode;
053import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter;
054import org.forgerock.opendj.server.config.meta.HTTPEndpointCfgDefn;
055import org.forgerock.opendj.server.config.server.HTTPAuthorizationMechanismCfg;
056import org.forgerock.opendj.server.config.server.HTTPEndpointCfg;
057import org.forgerock.opendj.server.config.server.RootCfg;
058import org.forgerock.services.context.Context;
059import org.forgerock.services.routing.RouteMatcher;
060import org.forgerock.util.Pair;
061import org.forgerock.util.promise.NeverThrowsException;
062import org.forgerock.util.promise.Promise;
063import org.opends.server.api.HttpEndpoint;
064import org.opends.server.protocols.http.authz.HttpAuthorizationMechanism;
065import org.opends.server.protocols.http.authz.HttpAuthorizationMechanismFactory;
066import org.opends.server.types.InitializationException;
067
068/**
069 * This class defines a utility that will be used to manage the set of HTTP
070 * endpoints defined in the Directory Server. It will initialize the HTTP
071 * endpoints when the server starts, and then will manage any additions,
072 * removals, or modifications to any HTTP endpoints while the server is running.
073 */
074public class HttpEndpointConfigManager implements ConfigurationChangeListener<HTTPEndpointCfg>,
075                                                  ConfigurationAddListener<HTTPEndpointCfg>,
076                                                  ConfigurationDeleteListener<HTTPEndpointCfg>
077{
078  private static final LocalizedLogger LOGGER = LocalizedLogger.getLoggerForThisClass();
079
080  private final AuthorizationMechanismManager auhtzFilterManager;
081  private final ServerContext serverContext;
082  private final Router router;
083  private final Map<DN, Pair<HttpApplication, Handler>> startedApplications;
084
085  /**
086   * Creates a new instance of this HTTP endpoint config manager.
087   *
088   * @param serverContext
089   *          The server context.
090   */
091  public HttpEndpointConfigManager(ServerContext serverContext)
092  {
093    this.serverContext = checkNotNull(serverContext, "serverContext cannot be null");
094    this.auhtzFilterManager = new AuthorizationMechanismManager();
095    this.router = serverContext.getHTTPRouter();
096    this.startedApplications = new HashMap<>();
097  }
098
099  /**
100   * Initializes all HTTP endpoints currently defined in the Directory Server
101   * configuration. This should only be called at Directory Server startup.
102   *
103   * @param rootConfiguration
104   *          The root configuration containing the {@link HttpEndpoint}
105   *          configurations.
106   * @throws ConfigException
107   *           If a configuration problem causes the {@link HttpEndpoint}
108   *           initialization process to fail.
109   */
110  public void registerTo(RootCfg rootConfiguration) throws ConfigException
111  {
112    auhtzFilterManager.registerTo(rootConfiguration);
113
114    rootConfiguration.addHTTPEndpointAddListener(this);
115    rootConfiguration.addHTTPEndpointDeleteListener(this);
116
117    for (String endpointName : rootConfiguration.listHTTPEndpoints())
118    {
119      final HTTPEndpointCfg configuration = rootConfiguration.getHTTPEndpoint(endpointName);
120      configuration.addChangeListener(this);
121
122      if (configuration.isEnabled())
123      {
124        final ConfigChangeResult result = applyConfigurationAdd(configuration);
125        if (!result.getResultCode().equals(ResultCode.SUCCESS))
126        {
127          LOGGER.error(result.getMessages().get(0));
128        }
129      }
130    }
131  }
132
133  @Override
134  public boolean isConfigurationAddAcceptable(HTTPEndpointCfg configuration,
135      List<LocalizableMessage> unacceptableReasons)
136  {
137    try
138    {
139      // Check that endpoint's authorization filters are valid.
140      auhtzFilterManager.getFilters(configuration.dn(), configuration.getAuthorizationMechanismDNs());
141      return loadEndpoint(configuration).isConfigurationValid(unacceptableReasons);
142    }
143    catch (InitializationException | ConfigException e)
144    {
145      unacceptableReasons.add(e.getMessageObject());
146      return false;
147    }
148  }
149
150  @Override
151  public ConfigChangeResult applyConfigurationAdd(HTTPEndpointCfg configuration)
152  {
153    final ConfigChangeResult ccr = new ConfigChangeResult();
154    configuration.addChangeListener(this);
155    if (!configuration.isEnabled())
156    {
157      return ccr;
158    }
159
160    final RouteMatcher<Request> route = newRoute(configuration.getBasePath());
161    try
162    {
163      final HttpApplication application = loadEndpoint(configuration).newHttpApplication();
164      final Handler handler = application.start();
165      startedApplications.put(configuration.dn(), Pair.of(application, handler));
166      bindApplication(auhtzFilterManager.getFilters(configuration.dn(), configuration.getAuthorizationMechanismDNs()),
167                      handler, configuration.getBasePath());
168    }
169    catch (HttpApplicationException e)
170    {
171      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
172      ccr.addMessage(ERR_CONFIG_HTTPENDPOINT_UNABLE_TO_START.get(configuration.dn(), stackTraceToSingleLineString(e)));
173      router.addRoute(route, ErrorHandler.INTERNAL_SERVER_ERROR);
174    }
175    catch (InitializationException | ConfigException ie)
176    {
177      ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
178      ccr.addMessage(ie.getMessageObject());
179      router.addRoute(route, ErrorHandler.INTERNAL_SERVER_ERROR);
180    }
181    return ccr;
182  }
183
184  private void bindApplication(final Iterable<? extends ConditionalFilter> conditionalAuthorizationFilters,
185      final Handler applicationHandler, final String basePath)
186  {
187    router.addRoute(newRoute(basePath),
188                    Handlers.chainOf(applicationHandler, newAuthorizationFilter(conditionalAuthorizationFilters)));
189  }
190
191  @Override
192  public boolean isConfigurationDeleteAcceptable(HTTPEndpointCfg configuration,
193      List<LocalizableMessage> unacceptableReasons)
194  {
195    return true;
196  }
197
198  @Override
199  public ConfigChangeResult applyConfigurationDelete(HTTPEndpointCfg configuration)
200  {
201    router.removeRoute(newRoute(configuration.getBasePath()));
202    final Pair<HttpApplication, Handler> startedApplication = startedApplications.remove(configuration.dn());
203    if (startedApplication != null)
204    {
205      startedApplication.getFirst().stop();
206    }
207    return new ConfigChangeResult();
208  }
209
210  @Override
211  public boolean isConfigurationChangeAcceptable(HTTPEndpointCfg configuration,
212      List<LocalizableMessage> unacceptableReasons)
213  {
214    return isConfigurationAddAcceptable(configuration, unacceptableReasons);
215  }
216
217  @Override
218  public ConfigChangeResult applyConfigurationChange(HTTPEndpointCfg configuration)
219  {
220    final Pair<HttpApplication, Handler> startedApplication = startedApplications.remove(configuration.dn());
221    if (startedApplication != null)
222    {
223      router.addRoute(newRoute(configuration.getBasePath()), ErrorHandler.SERVICE_UNAVAILABLE);
224      startedApplication.getFirst().stop();
225    }
226    return applyConfigurationAdd(configuration);
227  }
228
229  @SuppressWarnings("unchecked")
230  private HttpEndpoint<?> loadEndpoint(HTTPEndpointCfg configuration) throws InitializationException
231  {
232    try
233    {
234      final Class<? extends HttpEndpoint<?>> endpointClass =
235          (Class<? extends HttpEndpoint<?>>) HTTPEndpointCfgDefn.getInstance().getJavaClassPropertyDefinition()
236              .loadClass(configuration.getJavaClass(), HttpEndpoint.class);
237      return endpointClass.getDeclaredConstructor(configuration.configurationClass(), ServerContext.class)
238                          .newInstance(configuration, serverContext);
239    }
240    catch (Exception e)
241    {
242      throw new InitializationException(ERR_CONFIG_HTTPENDPOINT_INITIALIZATION_FAILED.get(configuration.getJavaClass(),
243          configuration.dn(), stackTraceToSingleLineString(e)), e);
244    }
245  }
246
247  private static String removeLeadingAndTrailingSlashes(String path)
248  {
249    // Remove leading /
250    int start = 0;
251    while (path.charAt(start) == '/')
252    {
253      start++;
254    }
255
256    // Remove trailing /
257    int end = path.length();
258    while (path.charAt(end - 1) == '/')
259    {
260      end--;
261    }
262
263    return path.substring(start, end);
264  }
265
266  private static RouteMatcher<Request> newRoute(String basePath) {
267    return requestUriMatcher(RoutingMode.STARTS_WITH, removeLeadingAndTrailingSlashes(basePath));
268  }
269
270  /**
271   * Manages the {@link AuthorizationMechanism}. When a configuration is updated, all the {@link HttpEndpoint}s
272   * referencing the updated {@link AuthorizationMechanism} will be removed and re-added to the {@link Router} once the
273   * filter chain has been reconfigured.
274   */
275  private final class AuthorizationMechanismManager implements
276      ConfigurationChangeListener<HTTPAuthorizationMechanismCfg>,
277      ConfigurationAddListener<HTTPAuthorizationMechanismCfg>,
278      ConfigurationDeleteListener<HTTPAuthorizationMechanismCfg>
279  {
280    private final HttpAuthorizationMechanismFactory authzFilterFactory =
281        new HttpAuthorizationMechanismFactory(serverContext);
282    private final Map<DN, HttpAuthorizationMechanism<?>> authzFilters = new HashMap<>();
283
284    public void registerTo(RootCfg rootConfiguration) throws ConfigException
285    {
286      rootConfiguration.addHTTPAuthorizationMechanismAddListener(this);
287      rootConfiguration.addHTTPAuthorizationMechanismDeleteListener(this);
288
289      for (String authorizationName : rootConfiguration.listHTTPAuthorizationMechanisms())
290      {
291        final HTTPAuthorizationMechanismCfg configuration =
292            rootConfiguration.getHTTPAuthorizationMechanism(authorizationName);
293        configuration.addChangeListener(this);
294
295        final ConfigChangeResult result = applyConfigurationAdd(configuration);
296        if (!result.getResultCode().equals(ResultCode.SUCCESS))
297        {
298          throw new ConfigException(result.getMessages().get(0));
299        }
300      }
301    }
302
303    Collection<? extends ConditionalFilter> getFilters(DN endpointConfigDN, Set<DN> authzFilterDNs)
304        throws ConfigException
305    {
306      final SortedSet<HttpAuthorizationMechanism<?>> endpointAuthzMechanisms = new TreeSet<>();
307      for (DN dn : authzFilterDNs)
308      {
309        final HttpAuthorizationMechanism<?> authzMechanism = authzFilters.get(dn);
310        if (authzMechanism == null)
311        {
312          throw new ConfigException(ERR_CONFIG_HTTPENDPOINT_INVALID_AUTHZ_DN.get(endpointConfigDN, dn));
313        }
314        if (!endpointAuthzMechanisms.add(authzMechanism))
315        {
316          throw new ConfigException(ERR_CONFIG_HTTPENDPOINT_CONFLICTING_AUTHZ_DN.get(
317              endpointConfigDN, dn.rdn(0), endpointAuthzMechanisms.tailSet(authzMechanism).first()));
318        }
319      }
320      return endpointAuthzMechanisms;
321    }
322
323    private void rebindStartedApplications(DN authorizationFilterDN, ConfigChangeResult ccr)
324    {
325      final RootCfg rootConfiguration = serverContext.getRootConfig();
326      for (String endpointName : rootConfiguration.listHTTPEndpoints())
327      {
328        try
329        {
330          final HTTPEndpointCfg configuration = rootConfiguration.getHTTPEndpoint(endpointName);
331          if (configuration.getAuthorizationMechanismDNs().contains(authorizationFilterDN))
332          {
333            final Pair<HttpApplication, Handler> startedApplication = startedApplications.get(configuration.dn());
334            if (startedApplication != null)
335            {
336              bindApplication(getFilters(configuration.dn(), configuration.getAuthorizationMechanismDNs()),
337                              startedApplication.getSecond(),
338                              configuration.getBasePath());
339            }
340          }
341        }
342        catch (ConfigException e)
343        {
344          ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
345          ccr.addMessage(ERR_CONFIG_HTTPENDPOINT_UNABLE_TO_START.get(endpointName, stackTraceToSingleLineString(e)));
346          continue;
347        }
348      }
349    }
350
351    @Override
352    public boolean isConfigurationDeleteAcceptable(HTTPAuthorizationMechanismCfg configuration,
353        List<LocalizableMessage> unacceptableReasons)
354    {
355      return true;
356    }
357
358    @Override
359    public ConfigChangeResult applyConfigurationDelete(HTTPAuthorizationMechanismCfg configuration)
360    {
361      doConfigurationDelete(configuration);
362      final ConfigChangeResult ccr = new ConfigChangeResult();
363      rebindStartedApplications(configuration.dn(), ccr);
364      return ccr;
365    }
366
367    private void doConfigurationDelete(HTTPAuthorizationMechanismCfg configuration)
368    {
369      authzFilters.remove(configuration.dn());
370    }
371
372    @Override
373    public boolean isConfigurationAddAcceptable(HTTPAuthorizationMechanismCfg configuration,
374        List<LocalizableMessage> unacceptableReasons)
375    {
376      try
377      {
378        return authzFilterFactory.newInstance(configuration) != null;
379      }
380      catch (InitializationException ie)
381      {
382        unacceptableReasons.add(ie.getMessageObject());
383        return false;
384      }
385    }
386
387    @Override
388    public ConfigChangeResult applyConfigurationAdd(HTTPAuthorizationMechanismCfg configuration)
389    {
390      final ConfigChangeResult ccr = new ConfigChangeResult();
391      if (!configuration.isEnabled())
392      {
393        return ccr;
394      }
395      try
396      {
397        authzFilters.put(configuration.dn(), authzFilterFactory.newInstance(configuration));
398        rebindStartedApplications(configuration.dn(), ccr);
399      }
400      catch (InitializationException ie)
401      {
402        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
403        ccr.addMessage(ie.getMessageObject());
404      }
405      return ccr;
406    }
407
408    @Override
409    public boolean isConfigurationChangeAcceptable(HTTPAuthorizationMechanismCfg configuration,
410        List<LocalizableMessage> unacceptableReasons)
411    {
412      return !configuration.isEnabled() ||
413             ( isConfigurationDeleteAcceptable(configuration, unacceptableReasons)
414             && isConfigurationAddAcceptable(configuration, unacceptableReasons) );
415    }
416
417    @Override
418    public ConfigChangeResult applyConfigurationChange(HTTPAuthorizationMechanismCfg configuration)
419    {
420      doConfigurationDelete(configuration);
421      return applyConfigurationAdd(configuration);
422    }
423  }
424
425  /**
426   * {@link Handler} returning error status. This is used when {@link HttpApplication} failed to start or while a
427   * configuration is updated.
428   */
429  private static final class ErrorHandler implements Handler
430  {
431    private final static Handler SERVICE_UNAVAILABLE = new ErrorHandler(Status.SERVICE_UNAVAILABLE);
432    private final static Handler INTERNAL_SERVER_ERROR = new ErrorHandler(Status.INTERNAL_SERVER_ERROR);
433
434    private final Status status;
435
436    ErrorHandler(Status status)
437    {
438      this.status = status;
439    }
440
441    @Override
442    public Promise<Response, NeverThrowsException> handle(Context context, Request request)
443    {
444      return Response.newResponsePromise(new Response(status));
445    }
446  }
447}