blob: 25c3484117e7f33d6e673bbc0935531595dd02ee [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
package org.eclipse.scout.rt.server;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.regex.Pattern;
import org.eclipse.scout.commons.exception.ProcessingException;
import org.eclipse.scout.commons.exception.VetoException;
import org.eclipse.scout.commons.logger.IScoutLogger;
import org.eclipse.scout.commons.logger.ScoutLogManager;
import org.eclipse.scout.commons.serialization.SerializationUtility;
import org.eclipse.scout.rt.server.admin.inspector.CallInspector;
import org.eclipse.scout.rt.server.admin.inspector.ProcessInspector;
import org.eclipse.scout.rt.server.admin.inspector.SessionInspector;
import org.eclipse.scout.rt.server.internal.Activator;
import org.eclipse.scout.rt.server.services.common.clientnotification.IClientNotificationService;
import org.eclipse.scout.rt.server.transaction.AbstractTransactionMember;
import org.eclipse.scout.rt.server.transaction.ITransaction;
import org.eclipse.scout.rt.shared.ScoutTexts;
import org.eclipse.scout.rt.shared.security.RemoteServiceAccessPermission;
import org.eclipse.scout.rt.shared.services.common.clientnotification.IClientNotification;
import org.eclipse.scout.rt.shared.services.common.exceptionhandler.IExceptionHandlerService;
import org.eclipse.scout.rt.shared.services.common.security.ACCESS;
import org.eclipse.scout.rt.shared.servicetunnel.RemoteServiceAccessDenied;
import org.eclipse.scout.rt.shared.servicetunnel.ServiceTunnelRequest;
import org.eclipse.scout.rt.shared.servicetunnel.ServiceTunnelResponse;
import org.eclipse.scout.rt.shared.servicetunnel.VersionMismatchException;
import org.eclipse.scout.rt.shared.validate.DefaultValidator;
import org.eclipse.scout.rt.shared.validate.IValidationStrategy;
import org.eclipse.scout.rt.shared.validate.InputValidation;
import org.eclipse.scout.rt.shared.validate.OutputValidation;
import org.eclipse.scout.service.IService;
import org.eclipse.scout.service.IService2;
import org.eclipse.scout.service.SERVICES;
import org.eclipse.scout.service.ServiceUtility;
import org.osgi.framework.Bundle;
import org.osgi.framework.Version;
/**
* Delegate for scout dynamic business op invocation
* <p>
* Subclass this type to change/add validation checks or call {@link #setValidateInput(true)} to activate them.
* <p>
* Override {@link #validateInput(IValidationStrategy, Object, Method, Object[])} and/or
* {@link #validateOutput(IValidationStrategy, Object, Method, Object, Object[])} to modifiy the default central
* validation. You may use {@link #defaultValidateInput(IValidationStrategy, Object, Method, Object[])} and
* {@link #defaultValidateOutput(IValidationStrategy, Object, Method, Object, Object[])}
* <p>
* Set the config.ini properties to activate default validation:
*
* <pre>
* org.eclipse.scout.rt.server.validateInput=true
* org.eclipse.scout.rt.server.validateOutput=false
* </pre>
*/
@SuppressWarnings("deprecation")
public class DefaultTransactionDelegate {
private static final IScoutLogger LOG = ScoutLogManager.getLogger(DefaultTransactionDelegate.class);
private static final Boolean VALIDATE_INPUT = "true".equals(Activator.getDefault().getBundle().getBundleContext().getProperty("org.eclipse.scout.rt.server.validateInput"));
private static final Boolean VALIDATE_OUTPUT = "true".equals(Activator.getDefault().getBundle().getBundleContext().getProperty("org.eclipse.scout.rt.server.validateOutput"));
public static final Pattern DEFAULT_QUERY_NAMES_PATTERN = Pattern.compile("(get|is|has|load|read|find|select)([A-Z].*)?");
public static final Pattern DEFAULT_PROCESS_NAMES_PATTERN = Pattern.compile("(set|put|add|remove|store|write|create|insert|update|delete)([A-Z].*)?");
private final Version m_requestMinVersion;
private final boolean m_debug;
private final Bundle[] m_loaderBundles;
private long m_requestStart;
private long m_requestEnd;
public DefaultTransactionDelegate(Bundle[] loaderBundles, Version requestMinVersion, boolean debug) {
m_loaderBundles = loaderBundles;
m_requestMinVersion = requestMinVersion;
m_debug = debug;
}
public ServiceTunnelResponse invoke(ServiceTunnelRequest serviceReq) throws Exception {
ServiceTunnelResponse response;
m_requestStart = System.nanoTime();
try {
response = invokeImpl(serviceReq);
}
catch (Throwable t) {
ITransaction transaction = ThreadContext.getTransaction();
try {
// cancel tx
if (transaction != null) {
transaction.addFailure(t);
}
}
catch (Throwable ignore) {
// nop
}
//log it
if (transaction == null || !transaction.isCancelled()) {
if (t instanceof ProcessingException) {
((ProcessingException) t).addContextMessage("invoking " + serviceReq.getServiceInterfaceClassName() + ":" + serviceReq.getOperation());
SERVICES.getService(IExceptionHandlerService.class).handleException((ProcessingException) t);
}
else {
LOG.error("invoking " + serviceReq.getServiceInterfaceClassName() + ":" + serviceReq.getOperation(), t);
}
}
Throwable p = replaceOutboundException(t);
response = new ServiceTunnelResponse(null, null, p);
}
finally {
if (m_debug) {
LOG.debug("TIME " + serviceReq.getServiceInterfaceClassName() + "." + serviceReq.getOperation() + " " + (m_requestEnd - m_requestStart) / 1000000L + "ms");
}
}
m_requestEnd = System.nanoTime();
response.setProcessingDuration((m_requestEnd - m_requestStart) / 1000000L);
return response;
}
/**
* security: do not send back original error and stack trace with details
* <p>
* default returns an empty exception or in case of a {@link VetoException} only its title, message, error code and
* severity
*/
protected Throwable replaceOutboundException(Throwable t) {
Throwable p;
if (t instanceof VetoException) {
VetoException ve = (VetoException) t;
p = new VetoException(ve.getStatus().getTitle(), ve.getMessage(), ve.getStatus().getCode(), ve.getStatus().getSeverity());
}
else {
p = new ProcessingException(ScoutTexts.get("RequestProblem"));
}
p.setStackTrace(new StackTraceElement[0]);
return p;
}
/**
* This method is executed within a {@link IServerSession} context using a {@link ServerJob}
*/
protected ServiceTunnelResponse invokeImpl(ServiceTunnelRequest serviceReq) throws Throwable {
String soapOperation = ServiceTunnelRequest.toSoapOperation(serviceReq.getServiceInterfaceClassName(), serviceReq.getOperation());
IServerSession serverSession = ThreadContext.getServerSession();
String authenticatedUser = serverSession.getUserId();
if (LOG.isDebugEnabled()) {
LOG.debug("started " + serviceReq.getServiceInterfaceClassName() + "." + serviceReq.getOperation() + " by " + authenticatedUser + " at " + new Date());
}
// version check of request
if (m_requestMinVersion != null) {
String v = serviceReq.getVersion();
if (v == null) {
v = "0.0.0";
}
Version requestVersion = Version.parseVersion(v);
if (requestVersion.compareTo(m_requestMinVersion) < 0) {
ServiceTunnelResponse serviceRes = new ServiceTunnelResponse(null, null, new VersionMismatchException(requestVersion.toString(), m_requestMinVersion.toString()));
return serviceRes;
}
}
CallInspector callInspector = null;
SessionInspector sessionInspector = ProcessInspector.getDefault().getSessionInspector(serverSession, true);
if (sessionInspector != null) {
callInspector = sessionInspector.requestCallInspector(serviceReq);
}
ServiceTunnelResponse serviceRes = null;
try {
//do checks
Class<?> serviceInterfaceClass = SerializationUtility.getClassLoader().loadClass(serviceReq.getServiceInterfaceClassName());
//check access: service proxy allowed
Method serviceOp = ServiceUtility.getServiceOperation(serviceInterfaceClass, serviceReq.getOperation(), serviceReq.getParameterTypes());
checkRemoteServiceAccessByInterface(serviceInterfaceClass, serviceOp, serviceReq.getArgs());
//check access: service impl exists
Object service = SERVICES.getService(serviceInterfaceClass);
if (service == null) {
throw new SecurityException("service registry does not contain a service of type " + serviceReq.getServiceInterfaceClassName());
}
checkRemoteServiceAccessByAnnotations(serviceInterfaceClass, service.getClass(), serviceOp, serviceReq.getArgs());
checkRemoteServiceAccessByPermission(serviceInterfaceClass, service.getClass(), serviceOp, serviceReq.getArgs());
//all checks done
//
//filter input
if (serviceReq.getArgs() != null && serviceReq.getArgs().length > 0) {
Class<? extends IValidationStrategy> inputValidationStrategyClass = findInputValidationStrategyByAnnotation(service, serviceOp);
if (inputValidationStrategyClass == null) {
inputValidationStrategyClass = findInputValidationStrategyByPolicy(service, serviceOp);
}
if (inputValidationStrategyClass == null) {
throw new SecurityException("input validation failed (no strategy defined)");
}
validateInput(inputValidationStrategyClass.newInstance(), service, serviceOp, serviceReq.getArgs());
}
//
Object data = ServiceUtility.invoke(serviceOp, service, serviceReq.getArgs());
Object[] outParameters = ServiceUtility.extractHolderArguments(serviceReq.getArgs());
//
//filter output
if (data != null || (outParameters != null && outParameters.length > 0)) {
Class<? extends IValidationStrategy> outputValidationStrategyClass = findOutputValidationStrategyByAnnotation(service, serviceOp);
if (outputValidationStrategyClass == null) {
outputValidationStrategyClass = findOutputValidationStrategyByPolicy(service, serviceOp);
}
if (outputValidationStrategyClass == null) {
throw new SecurityException("output validation failed");
}
validateOutput(outputValidationStrategyClass.newInstance(), service, serviceOp, data, outParameters);
}
//
serviceRes = new ServiceTunnelResponse(data, outParameters, null);
serviceRes.setSoapOperation(soapOperation);
ThreadContext.getTransaction().registerMember(new P_ClientNotificationTransactionMember(serviceRes));
return serviceRes;
}
finally {
if (callInspector != null) {
try {
callInspector.update();
}
catch (Throwable t) {
LOG.warn(null, t);
}
try {
callInspector.close(serviceRes);
}
catch (Throwable t) {
LOG.warn(null, t);
}
try {
callInspector.getSessionInspector().update();
}
catch (Throwable t) {
LOG.warn(null, t);
}
}
}
}
/**
* Check pass 1 on type
*/
protected void checkRemoteServiceAccessByInterface(Class<?> interfaceClass, Method interfaceMethod, Object[] args) {
//check: must be an interface
if (!interfaceClass.isInterface()) {
throw new SecurityException("access denied (code 1a).");
}
//check: must be a subclass of IService
if (!IService.class.isAssignableFrom(interfaceClass)) {
throw new SecurityException("access denied (code 1b).");
}
//check: method is defined on service interface itself
Method verifyMethod;
try {
verifyMethod = interfaceClass.getMethod(interfaceMethod.getName(), interfaceMethod.getParameterTypes());
}
catch (Throwable t) {
throw new SecurityException("access denied (code 1c).");
}
//exists
if (verifyMethod.getDeclaringClass() == IService.class || verifyMethod.getDeclaringClass() == IService2.class) {
throw new SecurityException("access denied (code 1d).");
}
//continue
}
/**
* Check pass 2 on instance
*/
protected void checkRemoteServiceAccessByAnnotations(Class<?> interfaceClass, Class<?> implClass, Method interfaceMethod, Object[] args) {
//check: grant/deny annotation (type level is base, method level is finegrained)
Class<?> c = implClass;
while (c != null) {
//method level
Method m = null;
try {
m = c.getMethod(interfaceMethod.getName(), interfaceMethod.getParameterTypes());
}
catch (Throwable t) {
//nop
}
if (m != null) {
for (Annotation ann : m.getAnnotations()) {
if (ann.annotationType() == RemoteServiceAccessDenied.class) {
throw new SecurityException("access denied (code 2b).");
}
}
}
//type level
for (Annotation ann : c.getAnnotations()) {
if (ann.annotationType() == RemoteServiceAccessDenied.class) {
throw new SecurityException("access denied (code 2c).");
}
}
//next
if (c == interfaceClass) {
break;
}
c = c.getSuperclass();
if (c == Object.class) {
//use interface at last
c = interfaceClass;
}
}
//continue
}
/**
* Check pass 3 {@link RemoteServiceAccessPermission} if a client (gui) is allowed to call this service from remote
* using a
* remote service proxy.
* <p>
* Deny access by default.
* <p>
* Accepts when a {@link RemoteServiceAccessPermission} was implied.
*/
protected void checkRemoteServiceAccessByPermission(Class<?> interfaceClass, Class<?> implClass, Method interfaceMethod, Object[] args) {
if (ACCESS.check(new RemoteServiceAccessPermission(interfaceClass.getName(), interfaceMethod.getName()))) {
return;
}
throw new SecurityException("access denied (code 3a).");
}
/**
* Validate inbound data.Called by {@link #invokeImpl(ServiceTunnelRequest)}.
* <p>
* For default handling use
*
* <pre>
* new {@link DefaultValidator#DefaultValidator(IValidationStrategy)}.validate()
* </pre>
* <p>
* Override this method to do central input validation inside the transaction context.
* <p>
* This method is part of the protected api and can be overridden.
*
* @param validationStrategy
* may be null, add corresponding null handling.
*/
protected void validateInput(IValidationStrategy validationStrategy, Object service, Method op, Object[] args) throws Exception {
//defaultValidateInput(validationStrategy, service, op, args);
}
protected void defaultValidateInput(IValidationStrategy validationStrategy, Object service, Method op, Object[] args) throws Exception {
new DefaultValidator(validationStrategy).validateMethodCall(op, args);
}
/**
* Validate outbound data. Default does nothing. Called by {@link #invokeImpl(ServiceTunnelRequest)}.
* Override this method to do central output validation inside the transaction context.
* <p>
* This method is part of the protected api and can be overridden.
*/
protected void validateOutput(IValidationStrategy validationStrategy, Object service, Method op, Object returnValue, Object[] outArgs) throws Exception {
//defaultValidateOutput(validationStrategy, service, op, returnValue, outArgs);
}
protected void defaultValidateOutput(IValidationStrategy validationStrategy, Object service, Method op, Object returnValue, Object[] outArgs) throws Exception {
if ((outArgs != null && outArgs.length > 0) || returnValue != null) {
DefaultValidator v = new DefaultValidator(validationStrategy);
if (outArgs != null && outArgs.length > 0) {
for (Object arg : outArgs) {
v.validateParameter(arg, null);
}
}
if (returnValue != null) {
v.validateParameter(returnValue, null);
}
}
}
/**
* Pass 1 tries to find a {@link InputValidation} annotation
*/
protected Class<? extends IValidationStrategy> findInputValidationStrategyByAnnotation(Object serviceImpl, Method op) {
Class<?> c = serviceImpl.getClass();
while (c != null) {
//method level
Method m = null;
try {
m = c.getMethod(op.getName(), op.getParameterTypes());
}
catch (Throwable t) {
//nop
}
if (m != null) {
InputValidation ann = m.getAnnotation(InputValidation.class);
if (ann != null) {
return ann.value();
}
}
//type level
InputValidation ann = c.getAnnotation(InputValidation.class);
if (ann != null) {
return ann.value();
}
//next
if (c == op.getDeclaringClass()) {
break;
}
c = c.getSuperclass();
if (c == Object.class) {
//use interface at last
c = op.getDeclaringClass();
}
}
//continue
return null;
}
/**
* Pass 2 decides the strategy by java bean, collections framework and business process naming
*
* <pre>
* <i>Java bean naming</i>
* {@link IValidationStrategy.QUERY}: get*, is*
* {@link IValidationStrategy.PROCESS}: set*
* <p/>
* <i>Collections framework naming</i>
* {@link IValidationStrategy.QUERY}: get*
* {@link IValidationStrategy.PROCESS}: put*, add*, remove*
* <p/>
* <i>Business process naming</i>
* {@link IValidationStrategy.QUERY}: load*, read*, find*, has*, select*
* {@link IValidationStrategy.PROCESS}: store*, write*, create*, insert*, update*, delete*
* </pre>
*/
protected Class<? extends IValidationStrategy> findInputValidationStrategyByPolicy(Object serviceImpl, Method op) {
if (DEFAULT_QUERY_NAMES_PATTERN.matcher(op.getName()).matches()) {
return IValidationStrategy.QUERY.class;
}
if (DEFAULT_PROCESS_NAMES_PATTERN.matcher(op.getName()).matches()) {
return IValidationStrategy.PROCESS.class;
}
//
warnMissingInputValidation(serviceImpl, op);
return IValidationStrategy.QUERY.class;
}
protected void warnMissingInputValidation(Object serviceImpl, Method op) {
LOG.warn("Legacy security hint for: " + op.getDeclaringClass().getName() + "#" + op.getName() + ": missing either annotation " + InputValidation.class.getSimpleName() + " or override of server-side " + getClass().getSimpleName() + "#findInputValidationStrategyByPolicy. To support legacy the QUERY strategy is used.");
}
/**
* Pass 1 tries to find a {@link OutputValidation} annotation
*/
protected Class<? extends IValidationStrategy> findOutputValidationStrategyByAnnotation(Object serviceImpl, Method op) {
Class<?> c = serviceImpl.getClass();
while (c != null) {
//method level
Method m = null;
try {
m = c.getMethod(op.getName(), op.getParameterTypes());
}
catch (Throwable t) {
//nop
}
if (m != null) {
OutputValidation ann = m.getAnnotation(OutputValidation.class);
if (ann != null) {
return ann.value();
}
}
//type level
OutputValidation ann = c.getAnnotation(OutputValidation.class);
if (ann != null) {
return ann.value();
}
//next
if (c == op.getDeclaringClass()) {
break;
}
c = c.getSuperclass();
if (c == Object.class) {
//use interface at last
c = op.getDeclaringClass();
}
}
//continue
return null;
}
/**
* Pass 2 decides the strategy by policy (custom override recommended)
* <p>
* Default does no checks
*/
protected Class<? extends IValidationStrategy> findOutputValidationStrategyByPolicy(Object serviceImpl, Method op) {
return IValidationStrategy.NO_CHECK.class;
}
/**
* This transaction member ensures that the retrieval of client notifications is done at the last possible moment, and
* not during the normal duration of the transaction. Notifications are added to the global notification queue at
* commit-time, so this is in fact needed.
*/
private static class P_ClientNotificationTransactionMember extends AbstractTransactionMember {
private static final String TRANSACTION_MEMBER_ID = P_ClientNotificationTransactionMember.class.getSimpleName();
private final ServiceTunnelResponse m_serviceTunnelResponse;
public P_ClientNotificationTransactionMember(ServiceTunnelResponse serviceRes) {
super(TRANSACTION_MEMBER_ID);
m_serviceTunnelResponse = serviceRes;
}
@Override
public boolean needsCommit() {
return true;
}
@Override
public boolean commitPhase1() {
return true;
}
@Override
public void commitPhase2() {
}
@Override
public void rollback() {
}
@Override
public void release() {
IClientNotification[] na = SERVICES.getService(IClientNotificationService.class).getNextNotifications(0);
m_serviceTunnelResponse.setClientNotifications(na);
}
}
}