org.fabric3.management.rest.runtime.ResourceHostImpl Maven / Gradle / Ivy
The newest version!
/*
* Fabric3
* Copyright (c) 2009-2015 Metaform Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.fabric3.management.rest.runtime;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import org.fabric3.api.Role;
import org.fabric3.api.annotation.monitor.Monitor;
import org.fabric3.api.host.Fabric3Exception;
import org.fabric3.management.rest.model.HttpStatus;
import org.fabric3.management.rest.model.ResourceException;
import org.fabric3.management.rest.model.Response;
import org.fabric3.management.rest.spi.ResourceHost;
import org.fabric3.management.rest.spi.ResourceMapping;
import org.fabric3.management.rest.spi.Verb;
import org.fabric3.spi.container.invocation.WorkContext;
import org.fabric3.spi.container.invocation.WorkContextCache;
import org.fabric3.spi.host.ServletHost;
import org.fabric3.spi.security.AuthenticationException;
import org.fabric3.spi.security.NoCredentialsException;
import org.fabric3.spi.transform.Transformer;
import org.oasisopen.sca.annotation.Destroy;
import org.oasisopen.sca.annotation.Init;
import org.oasisopen.sca.annotation.Property;
import org.oasisopen.sca.annotation.Reference;
/**
*
*/
@SuppressWarnings("NonSerializableFieldInSerializableClass")
public class ResourceHostImpl extends HttpServlet implements ResourceHost {
private static final long serialVersionUID = 5554150494161533656L;
private static final String MANAGEMENT_PATH = "/management/*";
private Marshaller marshaller;
private ServletHost servletHost;
private BasicAuthenticator authenticator;
private ManagementMonitor monitor;
private ManagementSecurity security = ManagementSecurity.DISABLED;
private Set roles = new HashSet<>();
private boolean disableHttp;
private Map getMappings = new ConcurrentHashMap<>();
private Map postMappings = new ConcurrentHashMap<>();
private Map putMappings = new ConcurrentHashMap<>();
private Map deleteMappings = new ConcurrentHashMap<>();
private Map> registered = new ConcurrentHashMap<>();
public ResourceHostImpl(@Reference Marshaller marshaller,
@Reference ServletHost servletHost,
@Reference BasicAuthenticator authenticator,
@Monitor ManagementMonitor monitor) {
this.marshaller = marshaller;
this.servletHost = servletHost;
this.authenticator = authenticator;
this.monitor = monitor;
}
@Property(required = false)
public void setSecurity(String level) throws Fabric3Exception {
try {
security = ManagementSecurity.valueOf(level.toUpperCase());
} catch (IllegalArgumentException e) {
throw new Fabric3Exception("Invalid management security setting:" + level);
}
}
@Property(required = false)
public void setRoles(String rolesAttribute) {
String[] rolesString = rolesAttribute.split(",");
for (String s : rolesString) {
roles.add(new Role(s.trim()));
}
}
@Property(required = false)
public void setDisableHttp(boolean disableHttp) {
this.disableHttp = disableHttp;
}
@Init
public void start() throws Fabric3Exception {
servletHost.registerMapping(MANAGEMENT_PATH, this);
if (ManagementSecurity.DISABLED == security) {
monitor.securityDisabled();
}
if (!disableHttp) {
monitor.httpEnabled();
}
}
@Destroy()
public void stop() throws Fabric3Exception {
servletHost.unregisterMapping(MANAGEMENT_PATH);
}
public void init() {
}
public boolean isPathRegistered(String path, Verb verb) {
if (verb == Verb.GET) {
return getMappings.containsKey(path);
} else if (verb == Verb.POST) {
return postMappings.containsKey(path);
} else if (verb == Verb.PUT) {
return putMappings.containsKey(path);
} else {
return verb == Verb.DELETE && deleteMappings.containsKey(path);
}
}
public void register(ResourceMapping mapping) throws Fabric3Exception {
Verb verb = mapping.getVerb();
if (verb == Verb.GET) {
register(mapping, getMappings);
} else if (verb == Verb.POST) {
register(mapping, postMappings);
} else if (verb == Verb.PUT) {
register(mapping, putMappings);
} else if (verb == Verb.DELETE) {
register(mapping, deleteMappings);
}
}
public void unregister(String identifier) {
List list = registered.remove(identifier);
if (list == null) {
return;
}
for (ResourceMapping mapping : list) {
String path = mapping.getPath();
Verb verb = mapping.getVerb();
if (verb == Verb.GET) {
getMappings.remove(path);
} else if (verb == Verb.POST) {
postMappings.remove(path);
} else if (verb == Verb.PUT) {
putMappings.remove(path);
} else if (verb == Verb.DELETE) {
deleteMappings.remove(path);
}
}
}
public void unregisterPath(String path, Verb verb) {
ResourceMapping mapping;
if (verb == Verb.GET) {
mapping = getMappings.remove(path);
} else if (verb == Verb.POST) {
mapping = postMappings.remove(path);
} else if (verb == Verb.PUT) {
mapping = putMappings.remove(path);
} else {
mapping = deleteMappings.remove(path);
}
if (mapping != null) {
String identifier = mapping.getIdentifier();
List mappings = registered.get(identifier);
mappings.remove(mapping);
}
}
public void dispatch(String path, Verb verb, Object[] params) {
ResourceMapping mapping = resolveMapping(verb, path);
if (mapping == null) {
// this should not happen
monitor.error("Mapping not found during zone broadcast: " + path);
return;
}
WorkContext workContext = WorkContextCache.getAndResetThreadWorkContext();
try {
invoke(mapping, params);
} catch (ResourceException e) {
monitor.error("Error replicating resource request: " + mapping.getMethod(), e);
} finally {
workContext.reset();
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
handle(Verb.GET, request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
handle(Verb.POST, request, response);
}
@Override
protected void doDelete(HttpServletRequest request, HttpServletResponse response) {
handle(Verb.DELETE, request, response);
}
@Override
protected void doPut(HttpServletRequest request, HttpServletResponse response) {
handle(Verb.PUT, request, response);
}
/**
* Registers a resource mapping with the servlet.
*
* @param mapping the resource mapping
* @param mappings the resource mappings for an HTTP verb
* @throws Fabric3Exception if a resource for the path is already registered
*/
private void register(ResourceMapping mapping, Map mappings) throws Fabric3Exception {
String path = mapping.getPath();
if (mappings.containsKey(path)) {
throw new Fabric3Exception("Resource already registered at: " + path);
}
String identifier = mapping.getIdentifier();
List registered = getRegistered(identifier);
registered.add(mapping);
mappings.put(path, mapping);
}
private List getRegistered(String name) {
List list = registered.get(name);
if (list == null) {
list = new ArrayList<>();
registered.put(name, list);
}
return list;
}
/**
* Resolves the resource mapping for a request and handles it. An exact path match will be attempted first when resolving the mapping and, if not found,
* resolution will be done using the parent path. For example, a parameterized path such as /messages/message/1 will first attempt to resolve using the full
* path and if not found will resolve using /messages/message.
*
* @param verb the HTTP verb
* @param request the current request
* @param response the current response
*/
private void handle(Verb verb, HttpServletRequest request, HttpServletResponse response) {
if (disableHttp && !request.isSecure()) {
response.setStatus(HttpStatus.FORBIDDEN.getCode());
try {
response.getWriter().write("Forbidden. Only HTTPS access is allowed");
} catch (IOException e) {
monitor.error("Error writing response");
}
return;
}
if (request.getPathInfo() == null) {
response.setStatus(HttpStatus.NOT_FOUND.getCode());
return;
}
String pathInfo = parsePathInfo(request);
ResourceMapping mapping = resolveMapping(verb, pathInfo);
if (mapping == null) {
response.setStatus(404);
try {
response.getWriter().print("Management resource not found");
} catch (IOException e) {
monitor.error("Error writing response", e);
}
return;
}
WorkContext workContext = WorkContextCache.getAndResetThreadWorkContext();
try {
if (!securityCheck(mapping, request, response, workContext)) {
return;
}
Object[] params = marshaller.deserialize(verb, request, mapping);
Object value = invoke(mapping, params);
respond(value, mapping, request, response);
} catch (ResourceException e) {
respondError(e, mapping, response);
} finally {
workContext.reset();
}
}
/**
* Parses the path info so it can be used to resolve the requested resource.
*
* @param request the current request
* @return the parsed path info
*/
private String parsePathInfo(HttpServletRequest request) {
String pathInfo = request.getPathInfo().toLowerCase();
if (pathInfo.endsWith("/")) {
// strip trailing '/'
pathInfo = pathInfo.substring(0, pathInfo.length() - 1);
}
if (pathInfo.startsWith("/management/")) {
// strip the leading '/management' path as some servlet containers (e.g. Tomcat) include it
pathInfo = pathInfo.substring(11);
}
return pathInfo;
}
/**
* Resolves the resource mapping for a verb/path pair
*
* @param verb the HTTP verb
* @param path the HTTP path
* @return the resource mapping or null if not found
*/
private ResourceMapping resolveMapping(Verb verb, String path) {
ResourceMapping mapping;
if (verb == Verb.GET) {
mapping = resolve(path, getMappings);
} else if (verb == Verb.POST) {
mapping = resolve(path, postMappings);
} else if (verb == Verb.PUT) {
mapping = resolve(path, putMappings);
} else {
mapping = resolve(path, deleteMappings);
}
return mapping;
}
/**
* Resolves a mapping by walking a path hierarchy and matching against registered mappings. For example, resolution of the path /foo/bar/baz will be done in
* the following order: /foo/bar/baz; /foo/bar; and /foo.
*
* @param path the path
* @param mappings the registered mappings
* @return a mating mapping or null
*/
private ResourceMapping resolve(String path, Map mappings) {
// flag to allow exact matching on non-parameterized mappings. Only true for the first iteration, when an exact match is attempted
boolean start = true;
while (path != null) {
ResourceMapping mapping = mappings.get(path);
if (mapping != null && (start || mapping.isParameterized())) {
return mapping;
}
start = false;
String current = PathHelper.getParentPath(path);
if (path.equals(current)) {
// reached the path hierarchy root
break;
}
path = current;
}
return null;
}
/**
* checks the current client has credentials to execute the management operation if security is enabled.
*
* @param mapping the resource mapping
* @param request the current request
* @param response the response
* @param workContext the current work context
* @return true if the clients has the required credentials
*/
private boolean securityCheck(ResourceMapping mapping, HttpServletRequest request, HttpServletResponse response, WorkContext workContext) {
if (ManagementSecurity.DISABLED == security) {
return true;
}
try {
authenticator.authenticate(request, workContext);
} catch (NoCredentialsException e) {
response.setStatus(HttpStatus.UNAUTHORIZED.getCode());
response.setHeader("WWW-Authenticate", "Basic realm=\"fabric3\"");
return false;
} catch (AuthenticationException e) {
setUnauthorizedResponse(response);
return false;
}
if (ManagementSecurity.AUTHORIZATION == security) {
// check access to management interface
if (!checkSubjectHasRole(workContext, roles)) {
setUnauthorizedResponse(response);
return false;
}
// check access to the specific operation
if (!checkSubjectHasRole(workContext, mapping.getRoles())) {
setUnauthorizedResponse(response);
return false;
}
}
return true;
}
/**
* Checks if the current subject has a role in the set of provided roles if the latter is not empty.
*
* @param workContext the current work context
* @param roles the roles to check
* @return true if the current subject has a role
*/
private boolean checkSubjectHasRole(WorkContext workContext, Set roles) {
if (roles.isEmpty()) {
return true;
}
boolean authorized = false;
for (Role role : workContext.getSubject().getRoles()) {
if (roles.contains(role)) {
authorized = true;
break;
}
}
return authorized;
}
/**
* Constructs an HTTP unauthorized response.
*
* @param response the response
*/
private void setUnauthorizedResponse(HttpServletResponse response) {
response.setStatus(HttpStatus.UNAUTHORIZED.getCode());
try {
response.getWriter().write("Unauthorized");
} catch (IOException e) {
monitor.error("Error writing response", e);
}
}
/**
* Invokes a resource.
*
* @param mapping the resource mapping
* @param params the deserialized request parameters
* @return a return value or null
* @throws ResourceException if an error invoking the resource occurs
*/
private Object invoke(ResourceMapping mapping, Object[] params) throws ResourceException {
ClassLoader oldLoader = Thread.currentThread().getContextClassLoader();
try {
Object instance = mapping.getInstance();
if (instance instanceof Supplier) {
instance = ((Supplier) instance).get();
}
Thread.currentThread().setContextClassLoader(instance.getClass().getClassLoader());
return mapping.getMethod().invoke(instance, params);
} catch (IllegalAccessException | Fabric3Exception e) {
monitor.error("Error invoking operation: " + mapping.getMethod(), e);
throw new ResourceException(HttpStatus.INTERNAL_SERVER_ERROR);
} catch (InvocationTargetException e) {
Throwable target = e.getTargetException();
if (target instanceof ResourceException) {
// resource exceptions are returned to the client
throw (ResourceException) target;
}
monitor.error("Error invoking operation: " + mapping.getMethod(), e);
throw new ResourceException(HttpStatus.INTERNAL_SERVER_ERROR);
} finally {
Thread.currentThread().setContextClassLoader(oldLoader);
}
}
/**
* Returns a response to the client.
*
* @param value the return value
* @param mapping the resource mapping
* @param request the current request
* @param response the current response
* @throws ResourceException if an error sending the response
*/
private void respond(Object value, ResourceMapping mapping, HttpServletRequest request, HttpServletResponse response) throws ResourceException {
response.setContentType("application/json");
if (value instanceof Response) {
Response resourceResponse = (Response) value;
for (Map.Entry entry : resourceResponse.getHeaders().entrySet()) {
response.setHeader(entry.getKey(), entry.getValue());
}
response.setStatus(resourceResponse.getStatus().getCode());
Object entity = resourceResponse.getEntity();
if (entity != null) {
marshaller.serialize(entity, mapping, request, response);
}
} else if (value != null) {
marshaller.serialize(value, mapping, request, response);
}
}
/**
* Returns an error response to the client.
*
* @param e the error
* @param mapping the resource mapping
* @param response the current response
*/
private void respondError(ResourceException e, ResourceMapping mapping, HttpServletResponse response) {
response.setContentType("application/json");
for (Map.Entry entry : e.getHeaders().entrySet()) {
response.setHeader(entry.getKey(), entry.getValue());
}
response.setStatus(e.getStatus().getCode());
try {
String message = e.getMessage();
Object entity = e.getEntity();
if (entity != null) {
// transform the error entity
Transformer