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