| /******************************************************************************* |
| * 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); |
| } |
| |
| } |
| |
| } |