Whitelist of service operations that are excempt from authorization

In some cases however a service method requires no or very specialized
local authorization. In these cases this
annotation is used to whitelist and mark these business cases and
exclude from regular authorization.


Change-Id: I3ade8e7368f461be7c8afcf132879c372f3da522
Signed-off-by: Ivan Motsch <ivan.motsch@bsiag.com>
Reviewed-on: https://git.eclipse.org/r/92957
Tested-by: Hudson CI
Reviewed-by: Andi Bur <andi.bur@gmail.com>
diff --git a/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/services/RemoteServiceAccessTest.java b/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/services/RemoteServiceAccessDeniedTest.java
similarity index 99%
rename from org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/services/RemoteServiceAccessTest.java
rename to org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/services/RemoteServiceAccessDeniedTest.java
index 1881ee2..2cc6c3f 100644
--- a/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/services/RemoteServiceAccessTest.java
+++ b/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/services/RemoteServiceAccessDeniedTest.java
@@ -24,7 +24,7 @@
 import org.eclipse.scout.rt.shared.servicetunnel.RemoteServiceAccessDenied;
 import org.junit.Test;
 
-public class RemoteServiceAccessTest {
+public class RemoteServiceAccessDeniedTest {
 
   @Test
   public void testAnnotations() throws Exception {
diff --git a/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/services/RemoteServiceWithoutAuthorizationTest.java b/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/services/RemoteServiceWithoutAuthorizationTest.java
new file mode 100644
index 0000000..bb97fa3
--- /dev/null
+++ b/org.eclipse.scout.rt.server.test/src/test/java/org/eclipse/scout/rt/server/services/RemoteServiceWithoutAuthorizationTest.java
@@ -0,0 +1,180 @@
+/*******************************************************************************
+ * Copyright (c) 2010-2015 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.services;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Method;
+
+import org.eclipse.scout.rt.platform.IgnoreBean;
+import org.eclipse.scout.rt.platform.service.IService;
+import org.eclipse.scout.rt.server.ServiceOperationInvoker;
+import org.eclipse.scout.rt.server.services.common.ping.PingService;
+import org.eclipse.scout.rt.shared.services.common.ping.IPingService;
+import org.eclipse.scout.rt.shared.servicetunnel.RemoteServiceWithoutAuthorization;
+import org.junit.Test;
+
+public class RemoteServiceWithoutAuthorizationTest {
+
+  @Test
+  public void testMustAuthorize() throws Exception {
+    ServiceOperationInvokerMock bo = new ServiceOperationInvokerMock();
+    //
+    assertMustAuthorize(bo, IMockProcessService.class, Object.class.getMethod("hashCode"), IMockProcessService.class);
+    //
+    assertMustAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("hello"), IMockProcessService.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna1"), IMockProcessService.class);
+    assertMustAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna2"), IMockProcessService.class);
+    assertMustAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna3"), IMockProcessService.class);
+    //
+    assertMustAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("hello"), AbstractMockProcessService.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna1"), AbstractMockProcessService.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna2"), AbstractMockProcessService.class);
+    assertMustAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna3"), AbstractMockProcessService.class);
+    //
+    assertMustAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("hello"), MockProcessService1.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna1"), MockProcessService1.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna2"), MockProcessService1.class);
+    assertMustAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna3"), MockProcessService1.class);
+    //
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("hello"), MockProcessService2.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna1"), MockProcessService2.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna2"), MockProcessService2.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna3"), MockProcessService2.class);
+    //
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("hello"), MockProcessService2Sub.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna1"), MockProcessService2Sub.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna2"), MockProcessService2Sub.class);
+    assertNoAuthorize(bo, IMockProcessService.class, IMockProcessService.class.getMethod("interna3"), MockProcessService2Sub.class);
+    //
+    assertMustAuthorize(bo, IPingService.class, IPingService.class.getMethod("ping", String.class), PingService.class);
+
+    assertMustAuthorize(bo, IMockChildProcessService.class, IMockChildProcessService.class.getMethod("interna1"), MockChildProcessService.class);
+  }
+
+  private static void assertMustAuthorize(ServiceOperationInvokerMock bo, Class<?> serviceInterfaceClass, Method serviceOp, Class<?> serviceImplClass) throws Exception {
+    assertTrue(bo.test(serviceInterfaceClass, serviceOp, serviceImplClass));
+  }
+
+  private static void assertNoAuthorize(ServiceOperationInvokerMock bo, Class<?> serviceInterfaceClass, Method serviceOp, Class<?> serviceImplClass) throws Exception {
+    assertFalse(bo.test(serviceInterfaceClass, serviceOp, serviceImplClass));
+  }
+
+  @IgnoreBean
+  static class ServiceOperationInvokerMock extends ServiceOperationInvoker {
+
+    public ServiceOperationInvokerMock() {
+      super();
+    }
+
+    public boolean test(Class<?> interfaceClass, Method interfaceMethod, Class<?> implClass) throws Exception {
+      return mustAuthorize(interfaceClass, implClass, interfaceMethod, new Object[0]);
+    }
+  }
+
+  interface IMockProcessService extends IService {
+    void hello();
+
+    @RemoteServiceWithoutAuthorization
+    void interna1();
+
+    void interna2();
+
+    void interna3();
+  }
+
+  interface IMockChildProcessService extends IMockProcessService {
+    void internal4();
+
+    @Override
+    void interna1();
+  }
+
+  class MockChildProcessService implements IMockChildProcessService {
+
+    @Override
+    public void hello() {
+    }
+
+    @Override
+    public void interna1() {
+    }
+
+    @Override
+    public void interna2() {
+    }
+
+    @Override
+    public void interna3() {
+    }
+
+    @Override
+    public void internal4() {
+    }
+  }
+
+  abstract class AbstractMockProcessService implements IMockProcessService {
+
+    @Override
+    public void hello() {
+    }
+
+    @Override
+    public void interna1() {
+    }
+
+    @RemoteServiceWithoutAuthorization
+    @Override
+    public void interna2() {
+    }
+  }
+
+  class MockProcessService1 extends AbstractMockProcessService {
+
+    @Override
+    public void hello() {
+    }
+
+    @Override
+    public void interna2() {
+    }
+
+    @Override
+    public void interna3() {
+    }
+  }
+
+  @RemoteServiceWithoutAuthorization
+  class MockProcessService2 extends AbstractMockProcessService {
+
+    @Override
+    public void hello() {
+    }
+
+    @Override
+    public void interna3() {
+    }
+  }
+
+  class MockProcessService2Sub extends MockProcessService2 {
+
+    @RemoteServiceWithoutAuthorization
+    @Override
+    public void hello() {
+    }
+
+    @Override
+    public void interna3() {
+    }
+  }
+
+}
diff --git a/org.eclipse.scout.rt.server/src/main/java/org/eclipse/scout/rt/server/ServiceOperationInvoker.java b/org.eclipse.scout.rt.server/src/main/java/org/eclipse/scout/rt/server/ServiceOperationInvoker.java
index 0badfdf..5bd7e03 100644
--- a/org.eclipse.scout.rt.server/src/main/java/org/eclipse/scout/rt/server/ServiceOperationInvoker.java
+++ b/org.eclipse.scout.rt.server/src/main/java/org/eclipse/scout/rt/server/ServiceOperationInvoker.java
@@ -33,6 +33,7 @@
 import org.eclipse.scout.rt.shared.security.RemoteServiceAccessPermission;
 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.RemoteServiceWithoutAuthorization;
 import org.eclipse.scout.rt.shared.servicetunnel.ServiceTunnelRequest;
 import org.eclipse.scout.rt.shared.servicetunnel.ServiceTunnelResponse;
 import org.eclipse.scout.rt.shared.servicetunnel.ServiceUtility;
@@ -140,7 +141,9 @@
     checkServiceAvailable(serviceInterfaceClass, service);
     checkRemoteServiceAccessByInterface(serviceInterfaceClass, serviceOp, args);
     checkRemoteServiceAccessByAnnotations(serviceInterfaceClass, service.getClass(), serviceOp, args);
-    checkRemoteServiceAccessByPermission(serviceInterfaceClass, service.getClass(), serviceOp, args);
+    if (mustAuthorize(serviceInterfaceClass, service.getClass(), serviceOp, args)) {
+      checkRemoteServiceAccessByPermission(serviceInterfaceClass, service.getClass(), serviceOp, args);
+    }
     return service; // if we come there, the service is available and valid to call
   }
 
@@ -180,6 +183,8 @@
 
   /**
    * Check pass 2 on instance
+   * <p>
+   * Using blacklist {@link RemoteServiceAccessDenied}
    */
   protected void checkRemoteServiceAccessByAnnotations(Class<?> interfaceClass, Class<?> implClass, Method interfaceMethod, Object[] args) {
     //check: grant/deny annotation (type level is base, method level is finegrained)
@@ -221,15 +226,58 @@
    * <p>
    * Deny access by default.
    * <p>
-   * Accepts when a {@link RemoteServiceAccessPermission} was implied.
+   * Accepts when a {@link RemoteServiceAccessPermission} was implied or authorization was waved using whitelist
+   * {@link RemoteServiceWithoutAuthorization} in {@link #mustAuthorize(Class, Class, Method, Object[])}
    */
   protected void checkRemoteServiceAccessByPermission(Class<?> interfaceClass, Class<?> implClass, Method interfaceMethod, Object[] args) {
     if (ACCESS.check(new RemoteServiceAccessPermission(interfaceClass.getName(), interfaceMethod.getName()))) {
+      //granted
       return;
     }
     throw new SecurityException("access denied (code 3a).");
   }
 
+  /**
+   * @return true unless there is a {@link RemoteServiceWithoutAuthorization} on the called method or interface in the
+   *         class tree
+   * @since 6.1
+   */
+  protected boolean mustAuthorize(Class<?> interfaceClass, Class<?> implClass, Method interfaceMethod, Object[] args) {
+    //check: authorize/no-authorize 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 (NoSuchMethodException | RuntimeException t) {
+        LOG.debug("Could not lookup service method", t);
+      }
+      if (m != null && m.isAnnotationPresent(RemoteServiceWithoutAuthorization.class)) {
+        //granted
+        return false;
+      }
+
+      //type level
+      if (c.isAnnotationPresent(RemoteServiceWithoutAuthorization.class)) {
+        //granted
+        return false;
+      }
+
+      //next
+      if (c == interfaceClass) {
+        break;
+      }
+      c = c.getSuperclass();
+      if (c == Object.class) {
+        //use interface at last
+        c = interfaceClass;
+      }
+    }
+    return true;
+  }
+
   private CallInspector getCallInspector(ServiceTunnelRequest serviceReq, IServerSession serverSession) {
     if (serverSession != null) {
       SessionInspector sessionInspector = BEANS.get(ProcessInspector.class).getSessionInspector(serverSession, true);
diff --git a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/clientnotification/IClientNotificationService.java b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/clientnotification/IClientNotificationService.java
index be782d2..5a3900a 100644
--- a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/clientnotification/IClientNotificationService.java
+++ b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/clientnotification/IClientNotificationService.java
@@ -14,12 +14,14 @@
 
 import org.eclipse.scout.rt.platform.ApplicationScoped;
 import org.eclipse.scout.rt.shared.TunnelToServer;
+import org.eclipse.scout.rt.shared.servicetunnel.RemoteServiceWithoutAuthorization;
 
 /**
  * Service to consume notifications. Accessible from the client.
  */
 @ApplicationScoped
 @TunnelToServer
+@RemoteServiceWithoutAuthorization
 public interface IClientNotificationService {
 
   /**
diff --git a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/services/common/context/IRunMonitorCancelService.java b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/services/common/context/IRunMonitorCancelService.java
index 7286feb..1956b41 100644
--- a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/services/common/context/IRunMonitorCancelService.java
+++ b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/services/common/context/IRunMonitorCancelService.java
@@ -15,11 +15,13 @@
 import org.eclipse.scout.rt.platform.service.IService;
 import org.eclipse.scout.rt.platform.util.concurrent.ICancellable;
 import org.eclipse.scout.rt.shared.TunnelToServer;
+import org.eclipse.scout.rt.shared.servicetunnel.RemoteServiceWithoutAuthorization;
 
 /**
  * Provides cancellation support for operations initiated by the client.
  */
 @TunnelToServer
+@RemoteServiceWithoutAuthorization
 public interface IRunMonitorCancelService extends IService {
 
   /**
diff --git a/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/servicetunnel/RemoteServiceWithoutAuthorization.java b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/servicetunnel/RemoteServiceWithoutAuthorization.java
new file mode 100644
index 0000000..94f8c3f
--- /dev/null
+++ b/org.eclipse.scout.rt.shared/src/main/java/org/eclipse/scout/rt/shared/servicetunnel/RemoteServiceWithoutAuthorization.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2010-2015 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.shared.servicetunnel;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.eclipse.scout.rt.shared.security.RemoteServiceAccessPermission;
+
+/**
+ * By default remote service access must be authorized. Typically by a {@link RemoteServiceAccessPermission}
+ * <p>
+ * In some cases however a service method requires no or very specialized local authorization. In these cases this
+ * annotation is used to whitelist and mark these business cases and exclude from regular authorization.
+ * <p>
+ * Warning: This annotation therefore passes unauthorized calls to the annotated method!
+ *
+ * @since 6.1
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RemoteServiceWithoutAuthorization {
+}