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}