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