1 /*
   2  * Copyright (c) 2005, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.
   8  *
   9  * This code is distributed in the hope that it will be useful, but WITHOUT
  10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  12  * version 2 for more details (a copy is included in the LICENSE file that
  13  * accompanied this code).
  14  *
  15  * You should have received a copy of the GNU General Public License version
  16  * 2 along with this work; if not, write to the Free Software Foundation,
  17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  18  *
  19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  20  * or visit www.oracle.com if you need additional information or have any
  21  * questions.
  22  */
  23 
  24 /*
  25  * @test
  26  * @bug 5106721
  27  * @summary Check the NotificationAccessController methods are properly called.
  28  * @author Luis-Miguel Alventosa
  29  * @modules java.management/com.sun.jmx.remote.security
  30  * @run clean NotificationAccessControllerTest
  31  * @run build NotificationAccessControllerTest
  32  * @run main NotificationAccessControllerTest
  33  */
  34 
  35 import com.sun.jmx.remote.security.NotificationAccessController;
  36 import java.util.ArrayList;
  37 import java.util.Collections;
  38 import java.util.HashMap;
  39 import java.util.List;
  40 import java.util.Map;
  41 import java.util.concurrent.CopyOnWriteArrayList;
  42 import java.util.concurrent.Semaphore;
  43 import javax.management.MBeanServer;
  44 import javax.management.MBeanServerConnection;
  45 import javax.management.MBeanServerFactory;
  46 import javax.management.Notification;
  47 import javax.management.NotificationBroadcasterSupport;
  48 import javax.management.NotificationListener;
  49 import javax.management.ObjectName;
  50 import javax.management.remote.JMXAuthenticator;
  51 import javax.management.remote.JMXConnector;
  52 import javax.management.remote.JMXConnectorFactory;
  53 import javax.management.remote.JMXConnectorServer;
  54 import javax.management.remote.JMXConnectorServerFactory;
  55 import javax.management.remote.JMXPrincipal;
  56 import javax.management.remote.JMXServiceURL;
  57 import javax.security.auth.Subject;
  58 
  59 public class NotificationAccessControllerTest {
  60 
  61     public class NAC implements NotificationAccessController {
  62         private final boolean throwException;
  63         public NAC(boolean throwException) {
  64             this.throwException = throwException;
  65         }
  66 
  67         @Override
  68         public void addNotificationListener(
  69             String connectionId,
  70             ObjectName name,
  71             Subject subject)
  72             throws SecurityException {
  73             echo("addNotificationListener:");
  74             echo("\tconnectionId: " +  connectionId);
  75             echo("\tname: " +  name);
  76             echo("\tsubject: " +
  77                  (subject == null ? null : subject.getPrincipals()));
  78             if (throwException)
  79                 if (name.getCanonicalName().equals("domain:name=1,type=NB")
  80                     &&
  81                     subject != null
  82                     &&
  83                     subject.getPrincipals().contains(new JMXPrincipal("role")))
  84                     throw new SecurityException();
  85         }
  86 
  87         @Override
  88         public void removeNotificationListener(
  89             String connectionId,
  90             ObjectName name,
  91             Subject subject)
  92             throws SecurityException {
  93             echo("removeNotificationListener:");
  94             echo("\tconnectionId: " +  connectionId);
  95             echo("\tname: " +  name);
  96             echo("\tsubject: " +
  97                  (subject == null ? null : subject.getPrincipals()));
  98             if (throwException)
  99                 if (name.getCanonicalName().equals("domain:name=2,type=NB")
 100                     &&
 101                     subject != null
 102                     &&
 103                     subject.getPrincipals().contains(new JMXPrincipal("role")))
 104                     throw new SecurityException();
 105         }
 106 
 107         @Override
 108         public void fetchNotification(
 109             String connectionId,
 110             ObjectName name,
 111             Notification notification,
 112             Subject subject)
 113             throws SecurityException {
 114             echo("fetchNotification:");
 115             echo("\tconnectionId: " +  connectionId);
 116             echo("\tname: " +  name);
 117             echo("\tnotification: " +  notification);
 118             echo("\tsubject: " +
 119                  (subject == null ? null : subject.getPrincipals()));
 120             if (!throwException)
 121                 if (name.getCanonicalName().equals("domain:name=2,type=NB")
 122                     &&
 123                     subject != null
 124                     &&
 125                     subject.getPrincipals().contains(new JMXPrincipal("role")))
 126                     throw new SecurityException();
 127         }
 128     }
 129 
 130     public class CustomJMXAuthenticator implements JMXAuthenticator {
 131         @Override
 132         public Subject authenticate(Object credentials) {
 133             String role = ((String[]) credentials)[0];
 134             echo("\nCreate principal with name = " + role);
 135             return new Subject(true,
 136                                Collections.singleton(new JMXPrincipal(role)),
 137                                Collections.EMPTY_SET,
 138                                Collections.EMPTY_SET);
 139         }
 140     }
 141 
 142     public interface NBMBean {
 143         public void emitNotification(int seqnum, ObjectName name);
 144     }
 145 
 146     public static class NB
 147         extends NotificationBroadcasterSupport
 148         implements NBMBean {
 149         @Override
 150         public void emitNotification(int seqnum, ObjectName name) {
 151             if (name == null) {
 152                 sendNotification(new Notification("nb", this, seqnum));
 153             } else {
 154                 sendNotification(new Notification("nb", name, seqnum));
 155             }
 156         }
 157     }
 158 
 159     public class Listener implements NotificationListener {
 160         public final List<Notification> notifs = new CopyOnWriteArrayList<>();
 161 
 162         private final Semaphore s;
 163         public Listener(Semaphore s) {
 164             this.s = s;
 165         }
 166         @Override
 167         public void handleNotification(Notification n, Object h) {
 168             echo("handleNotification:");
 169             echo("\tNotification = " + n);
 170             echo("\tNotification.SeqNum = " + n.getSequenceNumber());
 171             echo("\tHandback = " + h);
 172             notifs.add(n);
 173             s.release();
 174         }
 175     }
 176 
 177     /**
 178      * Check received notifications
 179      */
 180     public int checkNotifs(int size,
 181                            List<Notification> received,
 182                            List<ObjectName> expected) {
 183         if (received.size() != size) {
 184             echo("Error: expecting " + size + " notifications, got " +
 185                     received.size());
 186             return 1;
 187         } else {
 188             for (Notification n : received) {
 189                 echo("Received notification: " + n);
 190                 if (!n.getType().equals("nb")) {
 191                     echo("Notification type must be \"nb\"");
 192                     return 1;
 193                 }
 194                 ObjectName o = (ObjectName) n.getSource();
 195                 int index = (int) n.getSequenceNumber();
 196                 ObjectName nb = expected.get(index);
 197                 if (!o.equals(nb)) {
 198                     echo("Notification source must be " + nb);
 199                     return 1;
 200                 }
 201             }
 202         }
 203         return 0;
 204     }
 205 
 206     /**
 207      * Run test
 208      */
 209     public int runTest(boolean enableChecks, boolean throwException)
 210         throws Exception {
 211 
 212         echo("\n=-=-= " + (enableChecks ? "Enable" : "Disable") +
 213              " notification access control checks " +
 214              (!enableChecks ? "" : (throwException ? ": add/remove " :
 215              ": fetch ")) + "=-=-=");
 216 
 217         JMXConnectorServer server = null;
 218         JMXConnector client = null;
 219 
 220         /*
 221         * (!enableChecks)
 222         * - List must contain three notifs from sources nb1, nb2 and nb3
 223         * (enableChecks && !throwException)
 224         * - List must contain one notif from source nb1
 225         * (enableChecks && throwException)
 226         * - List must contain two notifs from sources nb2 and nb3
 227         */
 228         final int expected_notifs =
 229             (!enableChecks ? 3 : (throwException ? 2 : 1));
 230 
 231         // Create a new MBeanServer
 232         //
 233         final MBeanServer mbs = MBeanServerFactory.createMBeanServer();
 234 
 235         try {
 236             // Create server environment map
 237             //
 238             final Map<String,Object> env = new HashMap<>();
 239             env.put("jmx.remote.authenticator", new CustomJMXAuthenticator());
 240             if (enableChecks) {
 241                 env.put("com.sun.jmx.remote.notification.access.controller",
 242                         new NAC(throwException));
 243             }
 244 
 245             // Create the JMXServiceURL
 246             //
 247             final JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://");
 248 
 249             // Create a JMXConnectorServer
 250             //
 251             server = JMXConnectorServerFactory.newJMXConnectorServer(url,
 252                                                                      env,
 253                                                                      mbs);
 254 
 255             // Start the JMXConnectorServer
 256             //
 257             server.start();
 258 
 259             // Create server environment map
 260             //
 261             final Map<String,Object> cenv = new HashMap<>();
 262             String[] credentials = new String[] { "role" , "password" };
 263             cenv.put("jmx.remote.credentials", credentials);
 264 
 265             // Create JMXConnector and connect to JMXConnectorServer
 266             //
 267             client = JMXConnectorFactory.connect(server.getAddress(), cenv);
 268 
 269             // Get non-secure MBeanServerConnection
 270             //
 271             final MBeanServerConnection mbsc =
 272                 client.getMBeanServerConnection();
 273 
 274             // Create NB MBean
 275             //
 276             ObjectName nb1 = ObjectName.getInstance("domain:type=NB,name=1");
 277             ObjectName nb2 = ObjectName.getInstance("domain:type=NB,name=2");
 278             ObjectName nb3 = ObjectName.getInstance("domain:type=NB,name=3");
 279             mbsc.createMBean(NB.class.getName(), nb1);
 280             mbsc.createMBean(NB.class.getName(), nb2);
 281             mbsc.createMBean(NB.class.getName(), nb3);
 282 
 283             // Add notification listener
 284             //
 285             Semaphore s = new Semaphore(0);
 286 
 287             Listener li = new Listener(s);
 288             try {
 289                 mbsc.addNotificationListener(nb1, li, null, null);
 290                 if (enableChecks && throwException) {
 291                     echo("Didn't get expected exception");
 292                     return 1;
 293                 }
 294             } catch (SecurityException e) {
 295                 if (enableChecks && throwException) {
 296                     echo("Got expected exception: " + e);
 297                 } else {
 298                     echo("Got unexpected exception: " + e);
 299                     return 1;
 300                 }
 301             }
 302             mbsc.addNotificationListener(nb2, li, null, null);
 303 
 304             System.out.println("\n+++ Expecting to receive " + expected_notifs +
 305                                " notification" + (expected_notifs > 1 ? "s" : "") +
 306                                " +++");
 307             // Invoke the "sendNotification" method
 308             //
 309             mbsc.invoke(nb1, "emitNotification",
 310                 new Object[] {0, null},
 311                 new String[] {"int", "javax.management.ObjectName"});
 312             mbsc.invoke(nb2, "emitNotification",
 313                 new Object[] {1, null},
 314                 new String[] {"int", "javax.management.ObjectName"});
 315             mbsc.invoke(nb2, "emitNotification",
 316                 new Object[] {2, nb3},
 317                 new String[] {"int", "javax.management.ObjectName"});
 318 
 319             // Wait for notifications to be emitted
 320             //
 321             s.acquire(expected_notifs);
 322 
 323             // Remove notification listener
 324             //
 325             if (!throwException)
 326                 mbsc.removeNotificationListener(nb1, li);
 327             try {
 328                 mbsc.removeNotificationListener(nb2, li);
 329                 if (enableChecks && throwException) {
 330                     echo("Didn't get expected exception");
 331                     return 1;
 332                 }
 333             } catch (SecurityException e) {
 334                 if (enableChecks && throwException) {
 335                     echo("Got expected exception: " + e);
 336                 } else {
 337                     echo("Got unexpected exception: " + e);
 338                     return 1;
 339                 }
 340             }
 341 
 342             int result = 0;
 343             List<ObjectName> sources = new ArrayList();
 344             sources.add(nb1);
 345             sources.add(nb2);
 346             sources.add(nb3);
 347             result = checkNotifs(expected_notifs, li.notifs, sources);
 348             if (result > 0) {
 349                 return result;
 350             }
 351         } catch (Exception e) {
 352             echo("Failed to perform operation: " + e);
 353             e.printStackTrace();
 354             return 1;
 355         } finally {
 356             // Close the connection
 357             //
 358             if (client != null)
 359                 client.close();
 360 
 361             // Stop the connector server
 362             //
 363             if (server != null)
 364                 server.stop();
 365 
 366             // Release the MBeanServer
 367             //
 368             if (mbs != null)
 369                 MBeanServerFactory.releaseMBeanServer(mbs);
 370         }
 371 
 372         return 0;
 373     }
 374 
 375     /*
 376      * Print message
 377      */
 378     private static void echo(String message) {
 379         System.out.println(message);
 380     }
 381 
 382     public static void main(String[] args) throws Exception {
 383 
 384         System.out.println("\nTest notification access control.");
 385 
 386         NotificationAccessControllerTest nact =
 387             new NotificationAccessControllerTest();
 388 
 389         int error = 0;
 390 
 391         error += nact.runTest(false, false);
 392 
 393         error += nact.runTest(true, false);
 394 
 395         error += nact.runTest(true, true);
 396 
 397         if (error > 0) {
 398             final String msg = "\nTest FAILED! Got " + error + " error(s)";
 399             System.out.println(msg);
 400             throw new IllegalArgumentException(msg);
 401         } else {
 402             System.out.println("\nTest PASSED!");
 403         }
 404     }
 405 }