From f793e70612c47d16a95ef12287514c603613f2c0 Mon Sep 17 00:00:00 2001 From: David Bosschaert Date: Tue, 13 Aug 2013 17:17:10 +0100 Subject: [PATCH] Role based access control for JMX. This adds role-based access to JMX operations. Whenever a JMX operation is invoked the roles of the current user are checked against the required roles for this operation. This is done through a JMX interceptor which is added through the following command line option -Djavax.management.builder.initial=org.apache.karaf.management.boot.KarafMBeanServerBuilder The required roles for JMX operations are defined in configuration files in etc/ and read via OSGi ConfigAdmin, as with any other configuration in this location. The relevant configuration is prefixed with jmx.acl and based on the JMX ObjectName that it applies to. For example specific configuration for an MBean with the following objectName: foo.bar:type=Test can be placed in a configuration file called jmx.acl.foo.bar.Test.cfg. More generic configuration can be placed in the domain (e.g. jmx.acl.foo.bar.cfg) or at the top level (jmx.acl.cfg). A simple configuration file looks like this: test = admin getVal = manager, viewer The system looks for required roles using the following process: The most specific configuration file/pid is tried first. E.g. in the above example the jmx.acl.foo.bar.Test.cfg is looked at first. In this configuration, the system looks for a: 1. Specific match for the invocation, e.g. test(int)["17"] = role1 2. Reg exp match for the invocation, e.g. test(int)[/[0-9]/] = role2 In both cases the passed argument is converted to a String for the comparison. If any of the above match the search stops and the associated roles are used. 3. Signature match for the invocation, e.g. test(int) = role3. If matched the search stops and the associated roles are used. 4. Method name match for the invocation, e.g. test = role4. If matched the search stops and the associated roles are used. 5. A method name wildcard match, e.g. te* = role5. For all the wildcard matches found in the current configuration file, the roles associated with the longest match are used. So if you have te* and * and the method invoked is 'test', then the roles defined with te* are used, not the ones defined with * are used. If no matching definition is found in the configuration file, a more general configuration file is inspected. So jmx.acl.foo.bar.cfg is tried next, this matches the domain of the MBean. If there is no match found in the domain the most generic configuration file is consulted (jmx.acl.cfg). If a matching definition is found, this is used an the process will not look for any other matching definitions. So the most specific definition always takes precedence. For some example role definitions, see the following example configurations: jmx.acl.java.lang.Memory.cfg, a very simple configuration: gc = manager jmx.acl.org.apache.karaf.bundles.cfg, a more advanced example configuration, e.g. bundles 0-49 can only be stopped by an admin, others can be stopped my a manager: stop(java.lang.String)[/([1-4])?[0-9]/] = admin stop = manager ACLs are also included that control what can be modified through OSGi Configuration Admin. Especially, the modification of ACLs themselves is restricted. See jmx.acl.org.apache.karaf.config.cfg and jmx.acl.osgi.compendium.cm.cfg. jmx.acl.cfg a catchall configuration for invocations of any MBean that doesn't have a rule specified elsewhere: list* = viewer get* = viewer is* = viewer set* = admin * = admin Also included is a new MBean org.apache.karaf:type=security,area=jmx that can be used to check whether the current user can access a certain MBean or invoke a specific operation on it. This MBean can be used by management clients to decide whether to show certain MBeans or operations to the end user. This commit also contains bunch of unit tests to verify the above behaviour. --- assemblies/features/framework/pom.xml | 6 + .../main/filtered-resources/resources/bin/instance | 2 +- .../filtered-resources/resources/bin/instance.bat | 2 +- .../main/filtered-resources/resources/bin/karaf | 2 +- .../filtered-resources/resources/bin/karaf.bat | 2 +- .../main/filtered-resources/resources/bin/shell | 2 +- .../filtered-resources/resources/bin/shell.bat | 2 +- .../resources/etc/config.properties | 3 +- .../src/main/resources/resources/etc/jmx.acl.cfg | 5 + .../resources/etc/jmx.acl.java.lang.Memory.cfg | 1 + .../etc/jmx.acl.org.apache.karaf.bundles.cfg | 16 + .../etc/jmx.acl.org.apache.karaf.config.cfg | 15 + .../etc/jmx.acl.org.apache.karaf.security.jmx.cfg | 2 + .../resources/etc/jmx.acl.osgi.compendium.cm.cfg | 16 + management/boot/pom.xml | 70 + .../management/boot/KarafMBeanServerBuilder.java | 68 + .../boot/src/main/resources/OSGI-INF/bundle.info | 18 + .../boot/KarafMBeanServerBuilderTest.java | 163 +++ management/pom.xml | 3 +- management/server/pom.xml | 12 +- .../apache/karaf/management/JMXSecurityMBean.java | 143 ++ .../apache/karaf/management/JaasAuthenticator.java | 36 +- .../karaf/management/KarafMBeanServerGuard.java | 584 ++++++++ .../management/internal/JMXSecurityMBeanImpl.java | 123 ++ .../OSGI-INF/blueprint/karaf-management.xml | 22 +- .../management/KarafMBeanServerGuardTest.java | 1470 ++++++++++++++++++++ .../apache/karaf/management/TestRolePrincipal.java | 56 + .../internal/JMXSecurityMBeanImplTestCase.java | 235 ++++ pom.xml | 5 + 29 files changed, 3042 insertions(+), 42 deletions(-) create mode 100644 assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.cfg create mode 100644 assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.java.lang.Memory.cfg create mode 100644 assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.bundles.cfg create mode 100644 assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.config.cfg create mode 100644 assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.security.jmx.cfg create mode 100644 assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.osgi.compendium.cm.cfg create mode 100644 management/boot/pom.xml create mode 100644 management/boot/src/main/java/org/apache/karaf/management/boot/KarafMBeanServerBuilder.java create mode 100644 management/boot/src/main/resources/OSGI-INF/bundle.info create mode 100644 management/boot/src/test/java/org/apache/karaf/management/boot/KarafMBeanServerBuilderTest.java create mode 100644 management/server/src/main/java/org/apache/karaf/management/JMXSecurityMBean.java create mode 100644 management/server/src/main/java/org/apache/karaf/management/KarafMBeanServerGuard.java create mode 100644 management/server/src/main/java/org/apache/karaf/management/internal/JMXSecurityMBeanImpl.java create mode 100644 management/server/src/test/java/org/apache/karaf/management/KarafMBeanServerGuardTest.java create mode 100644 management/server/src/test/java/org/apache/karaf/management/TestRolePrincipal.java create mode 100644 management/server/src/test/java/org/apache/karaf/management/internal/JMXSecurityMBeanImplTestCase.java diff --git a/assemblies/features/framework/pom.xml b/assemblies/features/framework/pom.xml index ef7ff8a..612d4a9 100644 --- a/assemblies/features/framework/pom.xml +++ b/assemblies/features/framework/pom.xml @@ -311,6 +311,12 @@ karaf-jaas-boot.jar + org.apache.karaf.management + org.apache.karaf.management.boot + target/classes/resources/lib + karaf-jmx-boot.jar + + org.apache.karaf org.apache.karaf.exception target/classes/resources/lib/endorsed diff --git a/assemblies/features/framework/src/main/filtered-resources/resources/bin/instance b/assemblies/features/framework/src/main/filtered-resources/resources/bin/instance index 8d2ffe7..93f6260 100644 --- a/assemblies/features/framework/src/main/filtered-resources/resources/bin/instance +++ b/assemblies/features/framework/src/main/filtered-resources/resources/bin/instance @@ -323,7 +323,7 @@ run() { CLASSPATH=`cygpath --path --windows "$CLASSPATH"` fi - exec "$JAVA" $JAVA_OPTS -Dkaraf.instances="${KARAF_HOME}/instances" -Dkaraf.home="$KARAF_HOME" -Dkaraf.base="$KARAF_BASE" -Djava.io.tmpdir="$KARAF_DATA/tmp" -Djava.util.logging.config.file="$KARAF_BASE/etc/java.util.logging.properties" $KARAF_OPTS $OPTS -classpath "$CLASSPATH" org.apache.karaf.instance.main.Execute "$@" + exec "$JAVA" $JAVA_OPTS -Dkaraf.instances="${KARAF_HOME}/instances" -Dkaraf.home="$KARAF_HOME" -Dkaraf.base="$KARAF_BASE" -Djava.io.tmpdir="$KARAF_DATA/tmp" -Djava.util.logging.config.file="$KARAF_BASE/etc/java.util.logging.properties" -Djavax.management.builder.initial=org.apache.karaf.management.boot.KarafMBeanServerBuilder $KARAF_OPTS $OPTS -classpath "$CLASSPATH" org.apache.karaf.instance.main.Execute "$@" } main() { diff --git a/assemblies/features/framework/src/main/filtered-resources/resources/bin/instance.bat b/assemblies/features/framework/src/main/filtered-resources/resources/bin/instance.bat index d52f84c..98e4d65 100644 --- a/assemblies/features/framework/src/main/filtered-resources/resources/bin/instance.bat +++ b/assemblies/features/framework/src/main/filtered-resources/resources/bin/instance.bat @@ -117,7 +117,7 @@ set CLASSPATH=%KARAF_HOME%\system\org\apache\karaf\instance\org.apache.karaf.ins if "%SHIFT%" == "true" SET ARGS=%2 %3 %4 %5 %6 %7 %8 if not "%SHIFT%" == "true" SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8 rem Execute the Java Virtual Machine - "%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dkaraf.instances="%KARAF_HOME%\instances" -Dkaraf.home="%KARAF_HOME%" -Dkaraf.base="%KARAF_BASE%" -Djava.io.tmpdir="%KARAF_DATA%\tmp" -Djava.util.logging.config.file="%KARAF_BASE%\etc\java.util.logging.properties" %KARAF_OPTS% org.apache.karaf.instance.main.Execute %ARGS% + "%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dkaraf.instances="%KARAF_HOME%\instances" -Dkaraf.home="%KARAF_HOME%" -Dkaraf.base="%KARAF_BASE%" -Djava.io.tmpdir="%KARAF_DATA%\tmp" -Djava.util.logging.config.file="%KARAF_BASE%\etc\java.util.logging.properties" -Djavax.management.builder.initial=org.apache.karaf.management.boot.KarafMBeanServerBuilder %KARAF_OPTS% org.apache.karaf.instance.main.Execute %ARGS% rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # diff --git a/assemblies/features/framework/src/main/filtered-resources/resources/bin/karaf b/assemblies/features/framework/src/main/filtered-resources/resources/bin/karaf index 6df1e1d..afe01ab 100755 --- a/assemblies/features/framework/src/main/filtered-resources/resources/bin/karaf +++ b/assemblies/features/framework/src/main/filtered-resources/resources/bin/karaf @@ -389,7 +389,7 @@ run() { JAVA_EXT_DIRS=`cygpath --path --windows "$JAVA_EXT_DIRS"` fi cd "$KARAF_BASE" - exec "$JAVA" $JAVA_OPTS -Djava.endorsed.dirs="${JAVA_ENDORSED_DIRS}" -Djava.ext.dirs="${JAVA_EXT_DIRS}" -Dkaraf.instances="${KARAF_HOME}/instances" -Dkaraf.home="$KARAF_HOME" -Dkaraf.base="$KARAF_BASE" -Dkaraf.data="$KARAF_DATA" -Djava.io.tmpdir="$KARAF_DATA/tmp" -Djava.util.logging.config.file="$KARAF_BASE/etc/java.util.logging.properties" $KARAF_OPTS $OPTS -classpath "$CLASSPATH" $MAIN "$@" + exec "$JAVA" $JAVA_OPTS -Djava.endorsed.dirs="${JAVA_ENDORSED_DIRS}" -Djava.ext.dirs="${JAVA_EXT_DIRS}" -Dkaraf.instances="${KARAF_HOME}/instances" -Dkaraf.home="$KARAF_HOME" -Dkaraf.base="$KARAF_BASE" -Dkaraf.data="$KARAF_DATA" -Djava.io.tmpdir="$KARAF_DATA/tmp" -Djava.util.logging.config.file="$KARAF_BASE/etc/java.util.logging.properties" -Djavax.management.builder.initial=org.apache.karaf.management.boot.KarafMBeanServerBuilder $KARAF_OPTS $OPTS -classpath "$CLASSPATH" $MAIN "$@" } main() { diff --git a/assemblies/features/framework/src/main/filtered-resources/resources/bin/karaf.bat b/assemblies/features/framework/src/main/filtered-resources/resources/bin/karaf.bat index d8a2087..3c9b435 100644 --- a/assemblies/features/framework/src/main/filtered-resources/resources/bin/karaf.bat +++ b/assemblies/features/framework/src/main/filtered-resources/resources/bin/karaf.bat @@ -306,7 +306,7 @@ if "%KARAF_PROFILER%" == "" goto :RUN SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8 rem Execute the Java Virtual Machine cd %KARAF_BASE% - "%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Djava.endorsed.dirs="%JAVA_HOME%\jre\lib\endorsed;%JAVA_HOME%\lib\endorsed;%KARAF_HOME%\lib\endorsed" -Djava.ext.dirs="%JAVA_HOME%\jre\lib\ext;%JAVA_HOME%\lib\ext;%KARAF_HOME%\lib\ext" -Dkaraf.instances="%KARAF_HOME%\instances" -Dkaraf.home="%KARAF_HOME%" -Dkaraf.base="%KARAF_BASE%" -Djava.io.tmpdir="%KARAF_DATA%\tmp" -Dkaraf.data="%KARAF_DATA%" -Djava.util.logging.config.file="%KARAF_BASE%\etc\java.util.logging.properties" %KARAF_OPTS% %MAIN% %ARGS% + "%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Djava.endorsed.dirs="%JAVA_HOME%\jre\lib\endorsed;%JAVA_HOME%\lib\endorsed;%KARAF_HOME%\lib\endorsed" -Djava.ext.dirs="%JAVA_HOME%\jre\lib\ext;%JAVA_HOME%\lib\ext;%KARAF_HOME%\lib\ext" -Dkaraf.instances="%KARAF_HOME%\instances" -Dkaraf.home="%KARAF_HOME%" -Dkaraf.base="%KARAF_BASE%" -Djava.io.tmpdir="%KARAF_DATA%\tmp" -Dkaraf.data="%KARAF_DATA%" -Djava.util.logging.config.file="%KARAF_BASE%\etc\java.util.logging.properties" -Djavax.management.builder.initial=org.apache.karaf.management.boot.KarafMBeanServerBuilder %KARAF_OPTS% %MAIN% %ARGS% rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # diff --git a/assemblies/features/framework/src/main/filtered-resources/resources/bin/shell b/assemblies/features/framework/src/main/filtered-resources/resources/bin/shell index f4a3b2c..38c2539 100644 --- a/assemblies/features/framework/src/main/filtered-resources/resources/bin/shell +++ b/assemblies/features/framework/src/main/filtered-resources/resources/bin/shell @@ -317,7 +317,7 @@ run() { CLASSPATH=`cygpath --path --windows "$CLASSPATH"` fi - exec "$JAVA" $JAVA_OPTS -Dkaraf.instances="${KARAF_HOME}/instances" -Dkaraf.home="$KARAF_HOME" -Dkaraf.base="$KARAF_BASE" -Djava.io.tmpdir="$KARAF_DATA/tmp" -Djava.util.logging.config.file="$KARAF_BASE/etc/java.util.logging.properties" $KARAF_OPTS $OPTS -classpath "$CLASSPATH" org.apache.karaf.shell.console.impl.Main --classpath="$KARAF_HOME/system" "$@" + exec "$JAVA" $JAVA_OPTS -Dkaraf.instances="${KARAF_HOME}/instances" -Dkaraf.home="$KARAF_HOME" -Dkaraf.base="$KARAF_BASE" -Djava.io.tmpdir="$KARAF_DATA/tmp" -Djava.util.logging.config.file="$KARAF_BASE/etc/java.util.logging.properties" -Djavax.management.builder.initial=org.apache.karaf.management.boot.KarafMBeanServerBuilder $KARAF_OPTS $OPTS -classpath "$CLASSPATH" org.apache.karaf.shell.console.impl.Main --classpath="$KARAF_HOME/system" "$@" } main() { diff --git a/assemblies/features/framework/src/main/filtered-resources/resources/bin/shell.bat b/assemblies/features/framework/src/main/filtered-resources/resources/bin/shell.bat index 37a0b81..84fc709 100644 --- a/assemblies/features/framework/src/main/filtered-resources/resources/bin/shell.bat +++ b/assemblies/features/framework/src/main/filtered-resources/resources/bin/shell.bat @@ -119,7 +119,7 @@ set CLASSPATH=%CLASSPATH%;%KARAF_HOME%\system\jline\jline\${jline.version}\jline if "%SHIFT%" == "true" SET ARGS=%2 %3 %4 %5 %6 %7 %8 if not "%SHIFT%" == "true" SET ARGS=%1 %2 %3 %4 %5 %6 %7 %8 rem Execute the Java Virtual Machine - "%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dkaraf.instances="%KARAF_HOME%\instances" -Dkaraf.home="%KARAF_HOME%" -Dkaraf.base="%KARAF_BASE%" -Djava.io.tmpdir="%KARAF_DATA%\tmp" -Djava.util.logging.config.file="%KARAF_BASE%\etc\java.util.logging.properties" %KARAF_OPTS% org.apache.karaf.shell.console.impl.Main --classpath="%KARAF_HOME%\system" %ARGS% + "%JAVA%" %JAVA_OPTS% %OPTS% -classpath "%CLASSPATH%" -Dkaraf.instances="%KARAF_HOME%\instances" -Dkaraf.home="%KARAF_HOME%" -Dkaraf.base="%KARAF_BASE%" -Djava.io.tmpdir="%KARAF_DATA%\tmp" -Djava.util.logging.config.file="%KARAF_BASE%\etc\java.util.logging.properties" -Djavax.management.builder.initial=org.apache.karaf.management.boot.KarafMBeanServerBuilder %KARAF_OPTS% org.apache.karaf.shell.console.impl.Main --classpath="%KARAF_HOME%\system" %ARGS% rem # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # diff --git a/assemblies/features/framework/src/main/filtered-resources/resources/etc/config.properties b/assemblies/features/framework/src/main/filtered-resources/resources/etc/config.properties index ea0f397..cb912da 100644 --- a/assemblies/features/framework/src/main/filtered-resources/resources/etc/config.properties +++ b/assemblies/features/framework/src/main/filtered-resources/resources/etc/config.properties @@ -73,6 +73,7 @@ org.osgi.framework.system.packages= \ org.osgi.util.tracker;uses:="org.osgi.framework";version="1.5.1", \ org.apache.karaf.jaas.boot;version="${karaf.osgi.version}", \ org.apache.karaf.jaas.boot.principal;version="${karaf.osgi.version}", \ + org.apache.karaf.management.boot;version="${karaf.osgi.version}", \ org.apache.karaf.version;version="${karaf.osgi.version}", \ ${jre-${java.specification.version}} @@ -104,7 +105,7 @@ eecap-1.2= osgi.ee; osgi.ee="OSGi/Minimum"; version:List="1.0,1.1", \ osgi.ee; osgi.ee="JavaSE"; version:List="1.0,1.1,1.2" # javax.transaction is needed to avoid class loader constraint violation when using javax.sql -org.osgi.framework.bootdelegation=org.apache.karaf.jaas.boot,org.apache.karaf.jaas.boot.principal,sun.*,com.sun.*,javax.transaction,javax.transaction.* +org.osgi.framework.bootdelegation=org.apache.karaf.jaas.boot,org.apache.karaf.jaas.boot.principal,org.apache.karaf.management.boot,sun.*,com.sun.*,javax.transaction,javax.transaction.* # OSGi Execution Environment org.osgi.framework.executionenvironment=J2SE-1.7,JavaSE-1.7,J2SE-1.6,JavaSE-1.6,J2SE-1.5,JavaSE-1.5,J2SE-1.4,JavaSE-1.4,J2SE-1.3,JavaSE-1.3,J2SE-1.2,,JavaSE-1.2,CDC-1.1/Foundation-1.1,CDC-1.0/Foundation-1.0,J2ME,OSGi/Minimum-1.1,OSGi/Minimum-1.0 diff --git a/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.cfg b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.cfg new file mode 100644 index 0000000..1933d48 --- /dev/null +++ b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.cfg @@ -0,0 +1,5 @@ +list* = viewer +get* = viewer +is* = viewer +set* = admin +* = admin \ No newline at end of file diff --git a/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.java.lang.Memory.cfg b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.java.lang.Memory.cfg new file mode 100644 index 0000000..a828105 --- /dev/null +++ b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.java.lang.Memory.cfg @@ -0,0 +1 @@ +gc = manager diff --git a/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.bundles.cfg b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.bundles.cfg new file mode 100644 index 0000000..18e7893 --- /dev/null +++ b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.bundles.cfg @@ -0,0 +1,16 @@ +install = manager +list = viewer +refresh = manager +resolve = manager +restart = manager +setStartLevel(java.lang.String, int)[/([1-4])?[0-9]/,/.*/] = admin +setStartLevel = manager +start(java.lang.String)[/([1-4])?[0-9]/] = admin +start = manager +stop(java.lang.String)[/([1-4])?[0-9]/] = admin +stop = manager +uninstall(java.lang.String)["0"] = #this is a comment, no roles can perform this operation +uninstall = admin +update(java.lang.String)[/([1-4])?[0-9]/] = admin +update(java.lang.String,java.lang.String)[/([1-4])?[0-9]/,/.*/] = admin +update = manager diff --git a/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.config.cfg b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.config.cfg new file mode 100644 index 0000000..d06542f --- /dev/null +++ b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.config.cfg @@ -0,0 +1,15 @@ +# This configuration file configures the management of Config Admin +# Such that only an admin can make changes to the JMX ACL rules, but managers can make +# changes to other pids. +appendProperty(java.lang.String,java.lang.String,java.lang.String)[/^jmx[.]acl.*/,/.*/,/.*/] = admin +appendProperty(java.lang.String,java.lang.String,java.lang.String) = manager +create(java.lang.String)[/^jmx[.]acl.*/] = admin +create(java.lang.String) = manager +delete(java.lang.String)[/^jmx[.]acl.*/] = admin +delete(java.lang.String) = manager +deleteProperty(java.lang.String,java.lang.String)[/^jmx[.]acl.*/,/.*/] = admin +deleteProperty(java.lang.String,java.lang.String) = manager +setProperty(java.lang.String,java.lang.String,java.lang.String)[/^jmx[.]acl.*/,/.*/,/.*/] = admin +setProperty(java.lang.String,java.lang.String,java.lang.String) = manager +update(java.lang.String,java.util.Map)[/^jmx[.]acl.*/,/.*/] = admin +update(java.lang.String,java.util.Map) = manager diff --git a/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.security.jmx.cfg b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.security.jmx.cfg new file mode 100644 index 0000000..e8f5664 --- /dev/null +++ b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.org.apache.karaf.security.jmx.cfg @@ -0,0 +1,2 @@ +canInvoke = viewer + diff --git a/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.osgi.compendium.cm.cfg b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.osgi.compendium.cm.cfg new file mode 100644 index 0000000..476537f --- /dev/null +++ b/assemblies/features/framework/src/main/resources/resources/etc/jmx.acl.osgi.compendium.cm.cfg @@ -0,0 +1,16 @@ +# This configuration file configures the management of Config Admin via the standard Config Admin MBean +# Such that only an admin can make changes to the JMX ACL rules, but managers can make +# changes to other pids. +createFactoryConfiguration(java.lang.String)[/^jmx[.]acl.*/] = admin +createFactoryConfiguration(java.lang.String) = manager +createFactoryConfigurationForLocation(java.lang.String,java.lang.String)[/^jmx[.]acl.*/,/.*/] = admin +createFactoryConfigurationForLocation(java.lang.String,java.lang.String) = manager +delete(java.lang.String)[/^jmx[.]acl.*/] = admin +delete(java.lang.String) = manager +deleteConfigurations = admin +deleteForLocation(java.lang.String,java.lang.String)[/^jmx[.]acl.*/,/.*/] = admin +deleteForLocation(java.lang.String,java.lang.String) = manager +update(java.lang.String,javax.management.openmbean.TabularData)[/^jmx[.]acl.*/,/.*/] = admin +update(java.lang.String,javax.management.openmbean.TabularData) = manager +updateForLocation(java.lang.String,java.lang.String,javax.management.openmbean.TabularData)[/^jmx[.]acl.*/,/.*/,/.*/] = admin +updateForLocation(java.lang.String,java.lang.String,javax.management.openmbean.TabularData) = manager diff --git a/management/boot/pom.xml b/management/boot/pom.xml new file mode 100644 index 0000000..ae07f9f --- /dev/null +++ b/management/boot/pom.xml @@ -0,0 +1,70 @@ + + + + + + 4.0.0 + + + org.apache.karaf.management + management + 3.0.0-SNAPSHOT + ../pom.xml + + + org.apache.karaf.management.boot + bundle + Apache Karaf :: Management :: Boot + Provides the JMX classes loaded at boot of the process. + + + ${basedir}/../../etc/appended-resources + + + + + + ${project.basedir}/src/main/resources + + **/* + + + + ${project.basedir}/src/main/resources + true + + **/*.info + + + + + + org.apache.felix + maven-bundle-plugin + + + * + !* + + + + + + + diff --git a/management/boot/src/main/java/org/apache/karaf/management/boot/KarafMBeanServerBuilder.java b/management/boot/src/main/java/org/apache/karaf/management/boot/KarafMBeanServerBuilder.java new file mode 100644 index 0000000..0e9844d --- /dev/null +++ b/management/boot/src/main/java/org/apache/karaf/management/boot/KarafMBeanServerBuilder.java @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.management.boot; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.management.MBeanServer; +import javax.management.MBeanServerBuilder; +import javax.management.MBeanServerDelegate; + +public class KarafMBeanServerBuilder extends MBeanServerBuilder { + private static volatile InvocationHandler guard; + + public static InvocationHandler getGuard() { + return guard; + } + + public static void setGuard(InvocationHandler guardHandler) { + guard = guardHandler; + } + + @Override + public MBeanServer newMBeanServer(String defaultDomain, MBeanServer outer, MBeanServerDelegate delegate) { + InvocationHandler handler = new MBeanInvocationHandler(super.newMBeanServer(defaultDomain, outer, delegate)); + return (MBeanServer) Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] { MBeanServer.class }, handler); + } + + private static final class MBeanInvocationHandler implements InvocationHandler { + private final MBeanServer wrapped; + private final List guarded = Collections.unmodifiableList(Arrays.asList( + "invoke", "getAttribute", "getAttributes", "setAttribute", "setAttributes")); + + MBeanInvocationHandler(MBeanServer mbeanServer) { + wrapped = mbeanServer; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (guarded.contains(method.getName())) { + if (KarafMBeanServerBuilder.guard == null) { + throw new IllegalStateException("KarafMBeanServerBuilder not initialized"); + } + + guard.invoke(proxy, method, args); + } + return method.invoke(wrapped, args); + } + } +} diff --git a/management/boot/src/main/resources/OSGI-INF/bundle.info b/management/boot/src/main/resources/OSGI-INF/bundle.info new file mode 100644 index 0000000..5c13267 --- /dev/null +++ b/management/boot/src/main/resources/OSGI-INF/bundle.info @@ -0,0 +1,18 @@ +\u001B[1mSYNOPSIS\u001B[0m + ${project.name} + + ${project.description} + + Maven URL: + \u001B[33mmvn:${project.groupId}/${project.artifactId}/${project.version}\u001B[0m + +\u001B[1mDESCRIPTION\u001B[0m + This bundle provides JMX classes loaded during Karaf boot process. + + In particular it provides the KarafMBeanServerBuilder. + + The KarafMBeanServerBuilder builds a special MBeanServer that allows role-based authorization + for JMX access. + This class must be loadable from at startup by the JVM so this module is added to classpath, the + the boot delegation class path and made available via the system bundle. + diff --git a/management/boot/src/test/java/org/apache/karaf/management/boot/KarafMBeanServerBuilderTest.java b/management/boot/src/test/java/org/apache/karaf/management/boot/KarafMBeanServerBuilderTest.java new file mode 100644 index 0000000..e4dbc43 --- /dev/null +++ b/management/boot/src/test/java/org/apache/karaf/management/boot/KarafMBeanServerBuilderTest.java @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.management.boot; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.InstanceNotFoundException; +import javax.management.MBeanServer; +import javax.management.ObjectName; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; + +public class KarafMBeanServerBuilderTest extends TestCase { + public void testMBeanServerBuilderBlocking() throws Exception { + MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.replay(mbs); + + KarafMBeanServerBuilder mbsb = new KarafMBeanServerBuilder(); + MBeanServer kmbs = mbsb.newMBeanServer("test", mbs, null); + + final List handlerArgs = new ArrayList(); + InvocationHandler guard = new InvocationHandler() { + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + handlerArgs.add(proxy); + handlerArgs.add(method); + handlerArgs.add(args); + + throw new SecurityException("Access Denied"); + } + }; + + KarafMBeanServerBuilder.setGuard(guard); + + assertEquals("Precondition", 0, handlerArgs.size()); + ObjectName on = ObjectName.getInstance("foo.bar:type=TestObject"); + + try { + kmbs.getAttribute(on, "myAttr"); + fail("Should have access denied"); + } catch (SecurityException se) { + // good + assertEquals(3, handlerArgs.size()); + assertSame(kmbs, handlerArgs.get(0)); + assertEquals("getAttribute", ((Method) handlerArgs.get(1)).getName()); + Object[] args = (Object []) handlerArgs.get(2); + assertEquals(2, args.length); + assertSame(on, args[0]); + assertEquals("myAttr", args[1]); + } + + try { + kmbs.getAttributes(on, new String [] {"foo", "bar"}); + fail("Should have access denied"); + } catch (SecurityException se) { + // good + } + + try { + kmbs.setAttribute(on, new Attribute("goo", "far")); + fail("Should have access denied"); + } catch (SecurityException se) { + // good + } + + try { + kmbs.setAttributes(on, new AttributeList()); + fail("Should have access denied"); + } catch (SecurityException se) { + // good + } + + try { + kmbs.setAttributes(on, new AttributeList()); + fail("Should have access denied"); + } catch (SecurityException se) { + // good + } + + try { + kmbs.invoke(on, "foo", new Object [] {}, new String [] {}); + fail("Should have access denied"); + } catch (SecurityException se) { + // good + } + + // Try some MBeanServer operation that are not guarded + assertTrue(kmbs.getDomains().length > 0); + assertTrue(kmbs.getMBeanCount() > 0); + assertTrue(kmbs.getDefaultDomain().length() > 0); + } + + public void testMBeanServerBuilderNonBlocking() throws Exception { + MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.replay(mbs); + + KarafMBeanServerBuilder mbsb = new KarafMBeanServerBuilder(); + MBeanServer kmbs = mbsb.newMBeanServer("test", mbs, null); + + + final List handlerArgs = new ArrayList(); + InvocationHandler guard = new InvocationHandler() { + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + handlerArgs.add(proxy); + handlerArgs.add(method); + handlerArgs.add(args); + return null; + } + }; + + KarafMBeanServerBuilder.setGuard(guard); + + assertEquals("Precondition", 0, handlerArgs.size()); + ObjectName on = ObjectName.getInstance("foo.bar:type=TestObject"); + + try { + kmbs.getAttribute(on, "myAttr"); + } catch (Exception e) { + Throwable th = getInnermostException(e); + assertTrue("Expected exception as the object in question is not registered with the MBeanServer", + th instanceof InstanceNotFoundException); + + // good + assertEquals(3, handlerArgs.size()); + assertSame(kmbs, handlerArgs.get(0)); + assertEquals("getAttribute", ((Method) handlerArgs.get(1)).getName()); + Object[] args = (Object []) handlerArgs.get(2); + assertEquals(2, args.length); + assertSame(on, args[0]); + assertEquals("myAttr", args[1]); + } + } + + private Throwable getInnermostException(Throwable th) { + if (th.getCause() != null) { + return getInnermostException(th.getCause()); + } else { + return th; + } + } +} diff --git a/management/pom.xml b/management/pom.xml index da264f2..ed0f99a 100644 --- a/management/pom.xml +++ b/management/pom.xml @@ -34,7 +34,8 @@ Apache Karaf :: Management + boot server - \ No newline at end of file + diff --git a/management/server/pom.xml b/management/server/pom.xml index 3941f1d..5576f98 100644 --- a/management/server/pom.xml +++ b/management/server/pom.xml @@ -51,10 +51,20 @@ org.apache.karaf.jaas + org.apache.karaf.jaas.boot + provided + + + org.apache.karaf.jaas org.apache.karaf.jaas.config provided + org.apache.karaf.management + org.apache.karaf.management.boot + provided + + org.apache.servicemix.bundles org.apache.servicemix.bundles.junit test @@ -96,7 +106,7 @@ org.apache.karaf.management;version=${project.version};-split-package:=merge-first - org.apache.karaf.management + org.apache.karaf.management.internal diff --git a/management/server/src/main/java/org/apache/karaf/management/JMXSecurityMBean.java b/management/server/src/main/java/org/apache/karaf/management/JMXSecurityMBean.java new file mode 100644 index 0000000..0cd39c3 --- /dev/null +++ b/management/server/src/main/java/org/apache/karaf/management/JMXSecurityMBean.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.management; + +import java.util.List; +import java.util.Map; + +import javax.management.openmbean.CompositeType; +import javax.management.openmbean.OpenDataException; +import javax.management.openmbean.OpenType; +import javax.management.openmbean.SimpleType; +import javax.management.openmbean.TabularData; +import javax.management.openmbean.TabularType; + + +/** + * Security MBean. This MBean can be used to find out whether the currently logged in user can access + * certain MBeans or invoke operations on these MBeans. It can be used when building client-facing + * consoles to ensure that only operations appropriate for the current user are presented.

+ * This MBean does not actually invoke any operations on the given objects, it only checks permissions. + */ +public interface JMXSecurityMBean { + /** + * The Tabular Type returned by the {@link #canInvoke(Map)} operation. The rows consist of + * {@link #CAN_INVOKE_RESULT_ROW_TYPE} entries. + * It has a composite key with consists of the "ObjectName" and "Method" columns. + */ + static final TabularType CAN_INVOKE_TABULAR_TYPE = SecurityMBeanOpenTypeInitializer.TABULAR_TYPE; + + /** + * A row as returned by the {@link #CAN_INVOKE_TABULAR_TYPE}. The columns of the row are defined + * by {@link #CAN_INVOKE_RESULT_COLUMNS}. + */ + static final CompositeType CAN_INVOKE_RESULT_ROW_TYPE = SecurityMBeanOpenTypeInitializer.ROW_TYPE; + + /** + * The columns contained in a {@link #CAN_INVOKE_RESULT_ROW_TYPE}. The data types for these columns are + * as follows: + *

    + *
  • "ObjectName" : {@link SimpleType#STRING}
  • + *
  • "Method" : {@link SimpleType#STRING}
  • + *
  • "CanInvoke" : {@link SimpleType#BOOLEAN}
  • + *
+ */ + static final String [] CAN_INVOKE_RESULT_COLUMNS = SecurityMBeanOpenTypeInitializer.COLUMNS; + + /** + * Checks whether the current user can invoke any methods on a JMX MBean. + * @param objectName The Object Name of the JMX MBean. + * @return {@code true} if there is at least one method on the MBean that the + * user can invoke. + */ + boolean canInvoke(String objectName) throws Exception; + + /** + * Checks whether the current user can invoke any overload of the given method. + * @param objectName The Object Name of the JMX MBean. + * @param methodName The name of the method to check. + * @return {@code true} if there is an overload of the specified method that the + * user can invoke. + */ + boolean canInvoke(String objectName, String methodName) throws Exception; + + /** + * Checks whether the current user can invoke the given method. + * @param objectName The Object Name of the JMX MBean. + * @param methodName The name of the method to check. + * @param argumentTypes The argument types of to method. + * @return {@code true} if the user is allowed to invoke the method, or any of the methods with + * the given name if {@code null} is used for the arguments. There may still + * be certain values that the user does not have permission to pass to the method. + */ + boolean canInvoke(String objectName, String methodName, String [] argumentTypes) throws Exception; + + /** + * Bulk operation to check whether the current user can access the requested MBeans or invoke the + * requested methods. + * + * @param bulkQuery A map of Object Name to requested operations. Operations can be specified + * with or without arguments types. An operation without arguments matches any overloaded method + * with this name. If an empty list is provided for the operation names, a check is done whether the + * current user can invoke any operation on the MBean.

+ * Example: + *

{@code
+     * Map> query = new HashMap<>();
+     * String objectName = "org.acme:type=SomeMBean";
+     * query.put(objectName, Arrays.asList(
+     *     "testMethod(long,java.lang.String)", // check this testMethod
+     *     "otherMethod"));                     // check any overload of otherMethod
+     * query.put("org.acme:type=SomeOtherMBean",
+     *     Collections.emptyList());    // check any method of SomeOtherMBean
+     * TabularData result = mb.canInvoke(query);
+     * }
+ * @return A Tabular Data object with the result. This object conforms the structure as defined + * in {@link #CAN_INVOKE_TABULAR_TYPE}. + */ + TabularData canInvoke(Map> bulkQuery) throws Exception; + + // A member class is used to initialize final fields, as this needs to do some exception handling... + static class SecurityMBeanOpenTypeInitializer { + private static final String[] COLUMNS = new String [] {"ObjectName", "Method", "CanInvoke"}; + private static final CompositeType ROW_TYPE; + static { + try { + ROW_TYPE = new CompositeType("CanInvokeRowType", + "The rows of a CanInvokeTabularType table.", + COLUMNS, + new String [] { + "The ObjectName of the MBean checked", + "The Method to checked. This can either be a bare method name which means 'any method with this name' " + + "or a specific overload such as foo(java.lang.String). If an empty String is returned this means 'any' method.", + "true if the method or mbean can potentially be invoked by the current user."}, + new OpenType[] {SimpleType.STRING, SimpleType.STRING, SimpleType.BOOLEAN}); + } catch (OpenDataException e) { + throw new RuntimeException(e); + } + } + + private static final TabularType TABULAR_TYPE; + static { + try { + TABULAR_TYPE = new TabularType("CanInvokeTabularType", "Result of canInvoke() bulk operation", ROW_TYPE, + new String [] {"ObjectName", "Method"}); + } catch (OpenDataException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/management/server/src/main/java/org/apache/karaf/management/JaasAuthenticator.java b/management/server/src/main/java/org/apache/karaf/management/JaasAuthenticator.java index 1a84104..5e694ca 100644 --- a/management/server/src/main/java/org/apache/karaf/management/JaasAuthenticator.java +++ b/management/server/src/main/java/org/apache/karaf/management/JaasAuthenticator.java @@ -17,7 +17,6 @@ package org.apache.karaf.management; import java.io.IOException; -import java.security.Principal; import javax.management.remote.JMXAuthenticator; import javax.security.auth.Subject; @@ -31,9 +30,7 @@ import javax.security.auth.login.LoginContext; import javax.security.auth.login.LoginException; public class JaasAuthenticator implements JMXAuthenticator { - private String realm; - private String role; public String getRealm() { return realm; @@ -43,19 +40,12 @@ public class JaasAuthenticator implements JMXAuthenticator { this.realm = realm; } - public String getRole() { - return role; - } - - public void setRole(String role) { - this.role = role; - } - public Subject authenticate(Object credentials) throws SecurityException { if (!(credentials instanceof String[])) { throw new IllegalArgumentException("Expected String[2], got " + (credentials != null ? credentials.getClass().getName() : null)); } + final String[] params = (String[]) credentials; if (params.length != 2) { throw new IllegalArgumentException("Expected String[2] but length was " + params.length); @@ -76,26 +66,12 @@ public class JaasAuthenticator implements JMXAuthenticator { } }); loginContext.login(); - if (role != null && role.length() > 0) { - String clazz = "org.apache.karaf.jaas.boot.principal.RolePrincipal"; - String name = role; - int idx = role.indexOf(':'); - if (idx > 0) { - clazz = role.substring(0, idx); - name = role.substring(idx + 1); - } - boolean found = false; - for (Principal p : subject.getPrincipals()) { - if (p.getClass().getName().equals(clazz) - && p.getName().equals(name)) { - found = true; - break; - } - } - if (!found) { - throw new FailedLoginException("User does not have the required role " + role); - } + + if (subject.getPrincipals().size() == 0) { + // There must be some Principals, but which ones required are tested later. + throw new FailedLoginException("User does not have the required role."); } + return subject; } catch (LoginException e) { throw new SecurityException("Authentication failed", e); diff --git a/management/server/src/main/java/org/apache/karaf/management/KarafMBeanServerGuard.java b/management/server/src/main/java/org/apache/karaf/management/KarafMBeanServerGuard.java new file mode 100644 index 0000000..6586c45 --- /dev/null +++ b/management/server/src/main/java/org/apache/karaf/management/KarafMBeanServerGuard.java @@ -0,0 +1,584 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.management; + +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.JMException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.security.auth.Subject; + +import org.apache.karaf.jaas.boot.principal.RolePrincipal; +import org.apache.karaf.management.boot.KarafMBeanServerBuilder; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +public class KarafMBeanServerGuard implements InvocationHandler { + private static final String JMX_ACL_PID_PREFIX = "jmx.acl"; + private ConfigurationAdmin configAdmin; + + public ConfigurationAdmin getConfigAdmin() { + return configAdmin; + } + + public void setConfigAdmin(ConfigurationAdmin configAdmin) { + this.configAdmin = configAdmin; + } + + public void init() { + KarafMBeanServerBuilder.setGuard(this); + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getParameterTypes().length == 0) + return null; + + if (!ObjectName.class.isAssignableFrom(method.getParameterTypes()[0])) + return null; + + ObjectName objectName = (ObjectName) args[0]; + if ("getAttribute".equals(method.getName())) { + handleGetAttribute((MBeanServer) proxy, objectName, (String) args[1]); + } else if ("getAttributes".equals(method.getName())) { + handleGetAttributes((MBeanServer) proxy, objectName, (String[]) args[1]); + } else if ("setAttribute".equals(method.getName())) { + handleSetAttribute((MBeanServer) proxy, objectName, (Attribute) args[1]); + } else if ("setAttributes".equals(method.getName())) { + handleSetAttributes((MBeanServer) proxy, objectName, (AttributeList) args[1]); + } else if ("invoke".equals(method.getName())) { + handleInvoke(objectName, (String) args[1], (Object[]) args[2], (String[]) args[3]); + } + + return null; + } + + /** + * Returns whether there is any method that the current user can invoke + * @param mbeanServer The MBeanServer where the object is registered. + * @param objectName The ObjectName to check. + * @return {@code true} if there is a method on the object that can be invoked. + */ + public boolean canInvoke(MBeanServer mbeanServer, ObjectName objectName) throws JMException, IOException { + MBeanInfo info = mbeanServer.getMBeanInfo(objectName); + + for (MBeanOperationInfo operation : info.getOperations()) { + List sig = new ArrayList(); + for (MBeanParameterInfo param : operation.getSignature()) { + sig.add(param.getType()); + } + if (canInvoke(objectName, operation.getName(), sig.toArray(new String [] {}))) { + return true; + } + } + + for (MBeanAttributeInfo attr : info.getAttributes()) { + if (attr.isReadable()) { + if (canInvoke(objectName, attr.isIs() ? "is" : "get" + attr.getName(), new String [] {})) + return true; + } + if (attr.isWritable()) { + if (canInvoke(objectName, "set" + attr.getName(), new String [] {attr.getType()})) + return true; + } + } + return false; + } + + /** + * Returns whether there is any overload of the specified method that can be invoked by the current user. + * @param mbeanServer The MBeanServer where the object is registered. + * @param objectName The MBean Object Name. + * @param methodName The name of the method. + * @return {@code true} if there is an overload of the method that can be invoked by the current user. + */ + public boolean canInvoke(MBeanServer mbeanServer, ObjectName objectName, String methodName) throws JMException, IOException { + methodName = methodName.trim(); + MBeanInfo info = mbeanServer.getMBeanInfo(objectName); + + for (MBeanOperationInfo op : info.getOperations()) { + if (!methodName.equals(op.getName())) { + continue; + } + + List sig = new ArrayList(); + for (MBeanParameterInfo param : op.getSignature()) { + sig.add(param.getType()); + } + if (canInvoke(objectName, op.getName(), sig.toArray(new String [] {}))) { + return true; + } + } + + for (MBeanAttributeInfo attr : info.getAttributes()) { + String attrName = attr.getName(); + if (methodName.equals("is" + attrName) || methodName.equals("get" + attrName)) { + return canInvoke(objectName, methodName, new String [] {}); + } + + if (methodName.equals("set" + attrName)) { + return canInvoke(objectName, methodName, new String [] {attr.getType()}); + } + } + return false; + } + + /** + * Returns true if the method on the MBean with the specified signature can be invoked. + * + * @param mbeanServer The MBeanServer where the object is registered. + * @param objectName The MBean Object Name. + * @param methodName The name of the method. + * @param signature The signature of the method. + * @return {@code true} if the method can be invoked. Note that if a method name or signature is provided + * that does not exist on the MBean the behaviour of this method is undefined. In other words, if you ask + * whether a method that does not exist can be invoked, the method may return {@code true} but actually + * invoking that method will obviously not work. + */ + public boolean canInvoke(MBeanServer mbeanServer, ObjectName objectName, String methodName, String[] signature) throws IOException { + // No checking done on the mbeanServer of whether the method actually exists... + return canInvoke(objectName, methodName, signature); + } + + private boolean canInvoke(ObjectName objectName, String methodName, String[] signature) throws IOException { + for (String role : getRequiredRoles(objectName, methodName, signature)) { + if (currentUserHasRole(role)) { + return true; + } + } + + return false; + } + + private void handleGetAttribute(MBeanServer proxy, ObjectName objectName, String attributeName) throws JMException, IOException { + MBeanInfo info = proxy.getMBeanInfo(objectName); + String prefix = null; + for (MBeanAttributeInfo attr : info.getAttributes()) { + if (attr.getName().equals(attributeName)) { + prefix = attr.isIs() ? "is" : "get"; + } + } + if (prefix == null) + throw new IllegalStateException("Attribute could not be found: " + attributeName); + + handleInvoke(objectName, prefix + attributeName, new Object [] {}, new String [] {}); + } + + private void handleGetAttributes(MBeanServer proxy, ObjectName objectName, String[] attributeNames) throws JMException, IOException { + for (String attr : attributeNames) { + handleGetAttribute(proxy, objectName, attr); + } + } + + private void handleSetAttribute(MBeanServer proxy, ObjectName objectName, Attribute attribute) throws JMException, IOException { + String dataType = null; + MBeanInfo info = proxy.getMBeanInfo(objectName); + for (MBeanAttributeInfo attr : info.getAttributes()) { + if (attr.getName().equals(attribute.getName())) { + dataType = attr.getType(); + break; + } + } + + if (dataType == null) + throw new IllegalStateException("Attribute data type could not be found"); + + handleInvoke(objectName, "set" + attribute.getName(), new Object [] {attribute.getValue()}, new String [] {dataType}); + } + + private void handleSetAttributes(MBeanServer proxy, ObjectName objectName, AttributeList attributes) throws JMException, IOException { + for (Attribute attr : attributes.asList()) { + handleSetAttribute(proxy, objectName, attr); + } + } + + void handleInvoke(ObjectName objectName, String operationName, Object[] params, String[] signature) throws IOException { + for (String role : getRequiredRoles(objectName, operationName, params, signature)) { + if (currentUserHasRole(role)) { + return; + } + } + throw new SecurityException("Insufficient credentials for operation."); + } + + List getRequiredRoles(ObjectName objectName, String methodName, String[] signature) throws IOException { + return getRequiredRoles(objectName, methodName, null, signature); + } + + List getRequiredRoles(ObjectName objectName, String methodName, Object[] params, String[] signature) throws IOException { + List roles = new ArrayList(); + + List allPids = new ArrayList(); + try { + for (Configuration config : configAdmin.listConfigurations("(service.pid=jmx.acl*)")) { + allPids.add(config.getPid()); + } + } catch (InvalidSyntaxException ise) { + throw new RuntimeException(ise); + } + + for (String pid : iterateDownPids(getNameSegments(objectName))) { + if (allPids.contains(pid)) { + Configuration config = configAdmin.getConfiguration(pid); + Dictionary properties = trimKeys(config.getProperties()); + + /* + 1. get all direct string matches + 2. get regexp matches + 3. get signature matches + 4. without signature + 5. method name wildcard + + We return immediately when a definition is found, so if a specific definition is found + we do not search for a more generic specification. + Regular expressions and exact matches are considered equally specific, so they are combined... + */ + + boolean foundExactOrRegExp = false; + if (params != null) { + Object exactArgMatchRoles = properties.get(getExactArgSignature(methodName, signature, params)); + if (exactArgMatchRoles instanceof String) { + roles.addAll(parseRoles((String) exactArgMatchRoles)); + foundExactOrRegExp = true; + } + + foundExactOrRegExp |= getRegExpRoles(properties, methodName, signature, params, roles); + + if (foundExactOrRegExp) { + // Since we have the actual parameters we can match them and if they do we won't look for any + // more generic rules... + return roles; + } + } else { + foundExactOrRegExp = getExactArgOrRegExpRoles(properties, methodName, signature, roles); + } + + Object signatureRoles = properties.get(getSignature(methodName, signature)); + if (signatureRoles instanceof String) { + roles.addAll(parseRoles((String) signatureRoles)); + return roles; + } + + if (foundExactOrRegExp) { + // We can get here if params == null and there were exact and/or regexp rules but no signature rules + return roles; + } + + Object methodRoles = properties.get(methodName); + if (methodRoles instanceof String) { + roles.addAll(parseRoles((String) methodRoles)); + return roles; + } + + if (getMethodNameWildcardRoles(properties, methodName, roles)) + return roles; + } + } + return roles; + } + + private Dictionary trimKeys(Dictionary properties) { + Dictionary d = new Hashtable(); + for (Enumeration e = properties.keys(); e.hasMoreElements(); ) { + String key = e.nextElement(); + Object value = properties.get(key); + + d.put(removeSpaces(key), value); + } + return d; + } + + private String removeSpaces(String key) { + StringBuilder sb = new StringBuilder(); + char quoteChar = 0; + for (int i = 0; i < key.length(); i++) { + char c = key.charAt(i); + + if (quoteChar == 0 && c == ' ') + continue; + + if (quoteChar == 0 && (c == '\"' || c == '/') && sb.length() > 0 && + (sb.charAt(sb.length() - 1) == '[' || sb.charAt(sb.length() - 1) == ',')) { + // we're in a quoted string + quoteChar = c; + } else if (quoteChar != 0 && c == quoteChar) { + // look ahead to see if the next non-space is the closing bracket or a comma, which ends the quoted string + for (int j = i + 1; j < key.length(); j++) { + if (key.charAt(j) == ' ') { + continue; + } + if (key.charAt(j) == ']' || key.charAt(j) == ',') { + quoteChar = 0; + } + break; + } + } + + sb.append(c); + } + + return sb.toString(); + } + + private List parseRoles(String roleStr) { + int hashIdx = roleStr.indexOf('#'); + if (hashIdx >= 0) { + // You can put a comment at the end + roleStr = roleStr.substring(0, hashIdx); + } + + List roles = new ArrayList(); + for (String role : roleStr.split("[,]")) { + String trimmed = role.trim(); + if (trimmed.length() > 0) + roles.add(trimmed); + } + return roles; + } + + private Object getExactArgSignature(String methodName, String[] signature, Object[] params) { + StringBuilder sb = new StringBuilder(getSignature(methodName, signature)); + sb.append('['); + boolean first = true; + for (Object param : params) { + if (first) + first = false; + else + sb.append(','); + sb.append('"'); + sb.append(param.toString().trim()); + sb.append('"'); + } + sb.append(']'); + return sb.toString(); + } + + private String getSignature(String methodName, String[] signature) { + StringBuilder sb = new StringBuilder(methodName); + sb.append('('); + boolean first = true; + for (String s : signature) { + if (first) + first = false; + else + sb.append(','); + + sb.append(s); + } + sb.append(')'); + return sb.toString(); + } + + private boolean getRegExpRoles(Dictionary properties, String methodName, String[] signature, Object[] params, List roles) { + boolean matchFound = false; + String methodSig = getSignature(methodName, signature); + String prefix = methodSig + "[/"; + for (Enumeration e = properties.keys(); e.hasMoreElements(); ) { + String key = e.nextElement().trim(); + if (key.startsWith(prefix) && key.endsWith("/]")) { + List regexpArgs = getRegExpDecl(key.substring(methodSig.length())); + if (allParamsMatch(regexpArgs, params)) { + matchFound = true; + Object roleStr = properties.get(key); + if (roleStr instanceof String) { + roles.addAll(parseRoles((String) roleStr)); + } + } + } + } + return matchFound; + } + + private boolean getExactArgOrRegExpRoles(Dictionary properties, String methodName, String[] signature, List roles) { + boolean matchFound = false; + String methodSig = getSignature(methodName, signature); + String prefix = methodSig + "["; + for (Enumeration e = properties.keys(); e.hasMoreElements(); ) { + String key = e.nextElement().trim(); + if (key.startsWith(prefix) && key.endsWith("]")) { + matchFound = true; + Object roleStr = properties.get(key); + if (roleStr instanceof String) { + roles.addAll(parseRoles((String) roleStr)); + } + } + } + return matchFound; + } + + private boolean getMethodNameWildcardRoles(Dictionary properties, String methodName, List roles) { + SortedMap wildcardRules = new TreeMap(new Comparator() { + public int compare(String s1, String s2) { + // Returns longer entries before shorter ones... + return s2.length() - s1.length(); + } + }); + for (Enumeration e = properties.keys(); e.hasMoreElements(); ) { + String key = e.nextElement(); + if (key.endsWith("*")) { + String prefix = key.substring(0, key.length() - 1); + if (methodName.startsWith(prefix)) { + wildcardRules.put(prefix, properties.get(key).toString()); + } + } + } + + if (wildcardRules.size() != 0) { + roles.addAll(parseRoles(wildcardRules.values().iterator().next())); + return true; + } else { + return false; + } + } + + private boolean allParamsMatch(List regexpArgs, Object[] params) { + if (regexpArgs.size() != params.length) + return false; + + for (int i = 0; i < regexpArgs.size(); i++) { + if (!params[i].toString().trim().matches(regexpArgs.get(i))) { + return false; + } + } + return true; + } + + private List getRegExpDecl(String key) { + List l = new ArrayList(); + + boolean inRegExp = false; + StringBuilder curRegExp = new StringBuilder(); + for (int i = 0; i < key.length(); i++) { + if (!inRegExp) { + if (key.length() > i+1) { + String s = key.substring(i, i+2); + + if ("[/".equals(s) || ",/".equals(s)) { + inRegExp = true; + i++; + continue; + } + } + } else { + String s = key.substring(i, i+2); + if ("/]".equals(s) || "/,".equals(s)) { + l.add(curRegExp.toString()); + curRegExp = new StringBuilder(); + inRegExp = false; + continue; + } + curRegExp.append(key.charAt(i)); + } + + } + return l; + } + + private List getNameSegments(ObjectName objectName) { + List segs = new ArrayList(); + segs.add(objectName.getDomain()); + + // TODO can an object name property contain a comma as key or value? + // TODO support quoting as described in http://docs.oracle.com/javaee/1.4/api/javax/management/ObjectName.html + for (String s : objectName.getKeyPropertyListString().split("[,]")) { + int idx = s.indexOf('='); + if (idx < 0) + continue; + + segs.add(objectName.getKeyProperty(s.substring(0, idx))); + } + + return segs; + } + + /** + * Given a list of segments return a list of pids that are searched in this order. + * For example given the following segements: org.foo, bar, test + * the following list of pids will be generated (in this order): + * jmx.acl.org.foo.bar.test + * jmx.acl.org.foo.bar + * jmx.acl.org.foo + * jmx.acl + * The order is used as a search order, in which the most specific pid is searched first. + * @param segs the segments + * @return the pids in the above order. + */ + private List iterateDownPids(List segs) { + List res = new ArrayList(); + for (int i = segs.size(); i > 0; i--) { + StringBuilder sb = new StringBuilder(); + sb.append(JMX_ACL_PID_PREFIX); + for (int j = 0; j < i; j++) { + sb.append('.'); + sb.append(segs.get(j)); + } + res.add(sb.toString()); + } + res.add(JMX_ACL_PID_PREFIX); // This is the topmost PID + return res; + } + + static boolean currentUserHasRole(String reqRole) { + String clazz; + String role; + int idx = reqRole.indexOf(':'); + if (idx > 0) { + clazz = reqRole.substring(0, idx); + role = reqRole.substring(idx + 1); + } else { + clazz = RolePrincipal.class.getName(); + role = reqRole; + } + + AccessControlContext acc = AccessController.getContext(); + if (acc == null) { + return false; + } + Subject subject = Subject.getSubject(acc); + + if (subject == null) { + return false; + } + + for (Principal p : subject.getPrincipals()) { + if (clazz.equals(p.getClass().getName()) && role.equals(p.getName())) { + return true; + } + } + return false; + } +} diff --git a/management/server/src/main/java/org/apache/karaf/management/internal/JMXSecurityMBeanImpl.java b/management/server/src/main/java/org/apache/karaf/management/internal/JMXSecurityMBeanImpl.java new file mode 100644 index 0000000..6c21fac --- /dev/null +++ b/management/server/src/main/java/org/apache/karaf/management/internal/JMXSecurityMBeanImpl.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.management.internal; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.management.MBeanServer; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; +import javax.management.StandardMBean; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.CompositeDataSupport; +import javax.management.openmbean.TabularData; +import javax.management.openmbean.TabularDataSupport; + +import org.apache.karaf.management.KarafMBeanServerGuard; +import org.apache.karaf.management.JMXSecurityMBean; +import org.apache.karaf.management.boot.KarafMBeanServerBuilder; + +public class JMXSecurityMBeanImpl extends StandardMBean implements JMXSecurityMBean { + private MBeanServer mbeanServer; + + public JMXSecurityMBeanImpl() throws NotCompliantMBeanException { + super(JMXSecurityMBean.class); + } + + public boolean canInvoke(String objectName) throws Exception { + KarafMBeanServerGuard guard = (KarafMBeanServerGuard) KarafMBeanServerBuilder.getGuard(); + if (guard == null) + return true; + + return guard.canInvoke(mbeanServer, new ObjectName(objectName)); + } + + public boolean canInvoke(String objectName, String methodName) throws Exception { + KarafMBeanServerGuard guard = (KarafMBeanServerGuard) KarafMBeanServerBuilder.getGuard(); + if (guard == null) + return true; + + return guard.canInvoke(mbeanServer, new ObjectName(objectName), methodName); + } + + public boolean canInvoke(String objectName, String methodName, String[] argumentTypes) throws Exception { + ObjectName on = new ObjectName(objectName); + + KarafMBeanServerGuard guard = (KarafMBeanServerGuard) KarafMBeanServerBuilder.getGuard(); + if (guard == null) + return true; + + return guard.canInvoke(mbeanServer, on, methodName, argumentTypes); + } + + public TabularData canInvoke(Map> bulkQuery) throws Exception { + TabularData table = new TabularDataSupport(CAN_INVOKE_TABULAR_TYPE); + + for (Map.Entry> entry : bulkQuery.entrySet()) { + String objectName = entry.getKey(); + List methods = entry.getValue(); + if (methods.size() == 0) { + boolean res = canInvoke(objectName); + CompositeData data = new CompositeDataSupport(CAN_INVOKE_RESULT_ROW_TYPE, + CAN_INVOKE_RESULT_COLUMNS, + new Object [] {objectName, "", res}); + table.put(data); + } else { + for (String method : methods) { + List argTypes = new ArrayList(); + String name = parseMethodName(method, argTypes); + + boolean res; + if (name.equals(method)) { + res = canInvoke(objectName, name); + } else { + res = canInvoke(objectName, name, argTypes.toArray(new String [] {})); + } + CompositeData data = new CompositeDataSupport(CAN_INVOKE_RESULT_ROW_TYPE, + CAN_INVOKE_RESULT_COLUMNS, + new Object [] {objectName, method, res}); + table.put(data); + } + } + } + + return table; + } + + private String parseMethodName(String method, List argTypes) { + method = method.trim(); + int idx = method.indexOf('('); + if (idx < 0) + return method; + + String args = method.substring(idx + 1, method.length() - 1); + for (String arg : args.split(",")) { + argTypes.add(arg); + } + return method.substring(0, idx); + } + + public MBeanServer getMBeanServer() { + return this.mbeanServer; + } + + public void setMBeanServer(MBeanServer mbeanServer) { + this.mbeanServer = mbeanServer; + } +} diff --git a/management/server/src/main/resources/OSGI-INF/blueprint/karaf-management.xml b/management/server/src/main/resources/OSGI-INF/blueprint/karaf-management.xml index ebcd02b..0457e79 100644 --- a/management/server/src/main/resources/OSGI-INF/blueprint/karaf-management.xml +++ b/management/server/src/main/resources/OSGI-INF/blueprint/karaf-management.xml @@ -37,7 +37,6 @@ - @@ -105,8 +104,25 @@ - - + + + + + + + + + + + + + + + + + + + diff --git a/management/server/src/test/java/org/apache/karaf/management/KarafMBeanServerGuardTest.java b/management/server/src/test/java/org/apache/karaf/management/KarafMBeanServerGuardTest.java new file mode 100644 index 0000000..a408d80 --- /dev/null +++ b/management/server/src/test/java/org/apache/karaf/management/KarafMBeanServerGuardTest.java @@ -0,0 +1,1470 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.management; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.security.Principal; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.management.Attribute; +import javax.management.AttributeList; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanParameterInfo; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.LoginException; +import javax.security.auth.spi.LoginModule; + +import junit.framework.TestCase; + +import org.apache.karaf.jaas.boot.principal.RolePrincipal; +import org.easymock.EasyMock; +import org.osgi.framework.Constants; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; + +public class KarafMBeanServerGuardTest extends TestCase { + public void testRequiredRolesMethodNameOnly() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("doit", "master"); + configuration.put("fryIt", "editor, viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("master"), + guard.getRequiredRoles(on, "doit", new Object[] {}, new String [] {})); + assertEquals(Arrays.asList("editor", "viewer"), + guard.getRequiredRoles(on, "fryIt", new Object[] {"blah"}, new String [] {"java.lang.String"})); + } + + @SuppressWarnings("unchecked") + public void testRequiredRolesMethodNameEmpty() throws Exception { + Dictionary conf1 = new Hashtable(); + conf1.put("doit", ""); + conf1.put("fryIt", "editor, viewer"); + conf1.put(Constants.SERVICE_PID, "jmx.acl.foo.bar.Test"); + Dictionary conf2 = new Hashtable(); + conf2.put("doit", "editor"); + conf2.put(Constants.SERVICE_PID, "jmx.acl.foo.bar"); + ConfigurationAdmin ca = getMockConfigAdmin2(conf1, conf2); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.emptyList(), + guard.getRequiredRoles(on, "doit", new Object[] {}, new String [] {})); + assertEquals(Arrays.asList("editor", "viewer"), + guard.getRequiredRoles(on, "fryIt", new Object[] {"blah"}, new String [] {"java.lang.String"})); + } + + public void testRequiredRolesSignature() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("testIt", "master"); + configuration.put("testIt( java.lang.String)", "viewer"); + configuration.put("testIt( java.lang.String ,java.lang.String)", "editor"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("editor"), + guard.getRequiredRoles(on, "testIt", new Object[] {"test", "toast"}, new String [] {"java.lang.String", "java.lang.String"})); + } + + public void testRequiredRolesSignatureEmpty() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("testIt", "master"); + configuration.put("testIt(java.lang.String)", "viewer"); + configuration.put("testIt(java.lang.String,java.lang.String)", ""); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.emptyList(), + guard.getRequiredRoles(on, "testIt", new Object[] {"test", "toast"}, new String [] {"java.lang.String", "java.lang.String"})); + } + + public void testRequiredRolesExact() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("testIt", "master"); + configuration.put("testIt( java.lang.String)", "viewer"); + configuration.put("testIt( java.lang.String ,java.lang.String)", "editor"); + configuration.put("testIt( java.lang.String ) [\"ab\"]", "manager"); + configuration.put("testIt( java.lang.String )[\"a b\" ]", "admin"); + configuration.put("testIt( java.lang.String )[ \"cd\"] ", "tester"); + configuration.put("testIt(java.lang.String)[\"cd/\"]", "monkey"); + configuration.put("testIt(java.lang.String)[\"cd\"\"]", "donkey"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("manager"), + guard.getRequiredRoles(on, "testIt", new Object[] {"ab"}, new String [] {"java.lang.String"})); + assertEquals(Collections.singletonList("admin"), + guard.getRequiredRoles(on, "testIt", new Object[] {" a b "}, new String [] {"java.lang.String"})); + assertEquals("The arguments are trimmed before checking", + Collections.singletonList("admin"), + guard.getRequiredRoles(on, "testIt", new Object[] {"a b"}, new String [] {"java.lang.String"})); + assertEquals(Collections.singletonList("tester"), + guard.getRequiredRoles(on, "testIt", new Object[] {"cd"}, new String [] {"java.lang.String"})); + assertEquals(Collections.singletonList("monkey"), + guard.getRequiredRoles(on, "testIt", new Object[] {"cd/"}, new String [] {"java.lang.String"})); + assertEquals(Collections.singletonList("donkey"), + guard.getRequiredRoles(on, "testIt", new Object[] {"cd\""}, new String [] {"java.lang.String"})); + } + + public void testRequiredRolesExact2() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("foo(java.lang.String,java.lang.String)[\"a\",\",\"]", "editor #this is the editor rule"); + configuration.put("foo(java.lang.String,java.lang.String)[\",\" , \"a\"]", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("editor"), + guard.getRequiredRoles(on, "foo", new Object[] {"a", ","}, new String [] {"java.lang.String", "java.lang.String"})); + assertEquals(Collections.singletonList("viewer"), + guard.getRequiredRoles(on, "foo", new Object[] {",", "a"}, new String [] {"java.lang.String", "java.lang.String"})); + assertEquals(Collections.emptyList(), + guard.getRequiredRoles(on, "foo", new Object[] {"a", "a"}, new String [] {"java.lang.String", "java.lang.String"})); + } + + public void testRequiredRolesNumeric() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("bar(int)[\"17\"]", "editor #this is the editor rule"); + configuration.put("bar", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("editor"), + guard.getRequiredRoles(on, "bar", new Object[] {new Integer(17)}, new String [] {"int"})); + assertEquals(Collections.singletonList("viewer"), + guard.getRequiredRoles(on, "bar", new Object[] {new Integer(18)}, new String [] {"int"})); + } + + public void testRequiredRolesExactNobody() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("foo(java.lang.String)[\"a\"]", ""); + configuration.put("foo(java.lang.String)[\"aa\"]", "#hello"); + configuration.put("foo", "test"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.emptyList(), + guard.getRequiredRoles(on, "foo", new Object[] {"a"}, new String [] {"java.lang.String"})); + assertEquals(Collections.emptyList(), + guard.getRequiredRoles(on, "foo", new Object[] {"aa"}, new String [] {"java.lang.String"})); + } + + public void testRequiredRolesRegExp() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put(" testIt (java.lang.String) [ /ab/]", "manager"); + configuration.put("testIt(java.lang.String)[/c\"d/]", "tester"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("manager"), + guard.getRequiredRoles(on, "testIt", new Object[] {"ab"}, new String [] {"java.lang.String"})); + assertEquals(Collections.singletonList("manager"), + guard.getRequiredRoles(on, "testIt", new Object[] {" ab "}, new String [] {"java.lang.String"})); + assertEquals(Collections.emptyList(), + guard.getRequiredRoles(on, "testIt", new Object[] {" a b "}, new String [] {"java.lang.String"})); + assertEquals(Collections.singletonList("tester"), + guard.getRequiredRoles(on, "testIt", new Object[] {" c\"d "}, new String [] {"java.lang.String"})); + + } + + public void testRequiredRolesRegExpNobody() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("testIt(java.lang.String)[/ab/]", ""); + configuration.put("test*", "tester"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.emptyList(), + guard.getRequiredRoles(on, "testIt", new Object[] {"ab"}, new String [] {"java.lang.String"})); + } + + public void testRequiredRolesRegExp2() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("foo(java.lang.String,java.lang.String)[/a/,/b/]", "editor"); + configuration.put("foo(java.lang.String,java.lang.String)[/[bc]/ , /[^b]/]", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("editor"), + guard.getRequiredRoles(on, "foo", new Object[] {"a", "b"}, new String [] {"java.lang.String", "java.lang.String"})); + assertEquals(Collections.singletonList("viewer"), + guard.getRequiredRoles(on, "foo", new Object[] {"b", "a"}, new String [] {"java.lang.String", "java.lang.String"})); + assertEquals(Collections.singletonList("viewer"), + guard.getRequiredRoles(on, "foo", new Object[] {"c", "c"}, new String [] {"java.lang.String", "java.lang.String"})); + assertEquals(Collections.emptyList(), + guard.getRequiredRoles(on, "foo", new Object[] {"b", "b"}, new String [] {"java.lang.String", "java.lang.String"})); + } + + @SuppressWarnings("unchecked") + public void testRequiredRolesHierarchy() throws Exception { + Dictionary conf1 = new Hashtable(); + conf1.put("foo", "editor"); + conf1.put(Constants.SERVICE_PID, "jmx.acl.foo.bar.Test"); + Dictionary conf2 = new Hashtable(); + conf2.put("bar", "viewer"); + conf2.put("foo", "viewer"); + conf2.put(Constants.SERVICE_PID, "jmx.acl.foo.bar"); + Dictionary conf3 = new Hashtable(); + conf3.put("tar", "admin"); + conf3.put(Constants.SERVICE_PID, "jmx.acl.foo"); + Dictionary conf4 = new Hashtable(); + conf4.put("zar", "visitor"); + conf4.put(Constants.SERVICE_PID, "jmx.acl"); + + ConfigurationAdmin ca = getMockConfigAdmin2(conf1, conf2, conf3, conf4); + assertEquals("Precondition", 4, ca.listConfigurations("(service.pid=jmx.acl*)").length); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals("Should only return the most specific definition", + Collections.singletonList("editor"), + guard.getRequiredRoles(on, "foo", new Object[] {}, new String [] {})); + assertEquals(Collections.singletonList("viewer"), + guard.getRequiredRoles(on, "bar", new Object[] {"test"}, new String [] {"java.lang.String"})); + assertEquals("The top-level is the domain, subsections of the domain should not be searched", + Collections.emptyList(), + guard.getRequiredRoles(on, "tar", new Object[] {}, new String [] {})); + assertEquals(Collections.singletonList("visitor"), + guard.getRequiredRoles(on, "zar", new Object[] {}, new String [] {})); + } + + public void testRequiredRolesMethodNameWildcard() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("getFoo", "viewer"); + configuration.put("get*", " tester , editor,manager"); + configuration.put("*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("viewer"), + guard.getRequiredRoles(on, "getFoo", new Object[] {}, new String [] {})); + assertEquals(Arrays.asList("tester", "editor", "manager"), + guard.getRequiredRoles(on, "getBar", new Object[] {}, new String [] {})); + assertEquals(Collections.singletonList("admin"), + guard.getRequiredRoles(on, "test", new Object[] {new Long(17)}, new String [] {"java.lang.Long"})); + } + + public void testRequiredRolesMethodNameWildcard2() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("ge", "janitor"); + configuration.put("get", "admin"); + configuration.put("get*", "viewer"); + configuration.put("*", "manager"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("viewer"), + guard.getRequiredRoles(on, "getFoo", new Object[] {}, new String [] {})); + assertEquals(Collections.singletonList("admin"), + guard.getRequiredRoles(on, "get", new Object[] {}, new String [] {})); + assertEquals(Collections.singletonList("janitor"), + guard.getRequiredRoles(on, "ge", new Object[] {}, new String [] {})); + } + + public void testRequiredRolesMethodNameWildcard3() throws Exception { + Dictionary configuration = new Hashtable(); + configuration.put("get*", "viewer"); + configuration.put("*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.singletonList("viewer"), + guard.getRequiredRoles(on, "getFoo", new Object[] {}, new String [] {})); + assertEquals(Collections.singletonList("viewer"), + guard.getRequiredRoles(on, "get", new Object[] {}, new String [] {})); + assertEquals(Collections.singletonList("admin"), + guard.getRequiredRoles(on, "ge", new Object[] {}, new String [] {})); + } + + @SuppressWarnings("unchecked") + public void testRequiredRolesMethodNameWildcardEmpty() throws Exception { + Dictionary conf1 = new Hashtable(); + conf1.put("get*", " "); + conf1.put("*", "admin"); + conf1.put(Constants.SERVICE_PID, "jmx.acl.foo.bar.Test"); + Dictionary conf2 = new Hashtable(); + conf2.put("get*", "viewer"); + conf2.put(Constants.SERVICE_PID, "jmx.acl"); + ConfigurationAdmin ca = getMockConfigAdmin2(conf1, conf2); + + KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + assertEquals(Collections.emptyList(), + guard.getRequiredRoles(on, "getBar", new Object[] {}, new String [] {})); + assertEquals(Collections.singletonList("admin"), + guard.getRequiredRoles(on, "test", new Object[] {new Long(17)}, new String [] {"java.lang.Long"})); + } + + @SuppressWarnings("unchecked") + private ConfigurationAdmin getMockConfigAdmin(Dictionary configuration) throws IOException, + InvalidSyntaxException { + configuration.put(Constants.SERVICE_PID, "jmx.acl.foo.bar.Test"); + return getMockConfigAdmin2(configuration); + } + + private ConfigurationAdmin getMockConfigAdmin2(Dictionary ... configurations) throws IOException, + InvalidSyntaxException { + List allConfigs = new ArrayList(); + for (Dictionary configuration : configurations) { + Configuration conf = EasyMock.createMock(Configuration.class); + EasyMock.expect(conf.getPid()).andReturn((String) configuration.get(Constants.SERVICE_PID)).anyTimes(); + EasyMock.expect(conf.getProperties()).andReturn(configuration).anyTimes(); + EasyMock.replay(conf); + allConfigs.add(conf); + } + + ConfigurationAdmin ca = EasyMock.createMock(ConfigurationAdmin.class); + for (Configuration c : allConfigs) { + EasyMock.expect(ca.getConfiguration(c.getPid())).andReturn(c).anyTimes(); + } + EasyMock.expect(ca.listConfigurations(EasyMock.eq("(service.pid=jmx.acl*)"))).andReturn( + allConfigs.toArray(new Configuration [] {})).anyTimes(); + EasyMock.replay(ca); + return ca; + } + + public void testCurrentUserHasRole() throws Exception { + Subject subject = loginWithTestRoles("test"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + assertTrue(KarafMBeanServerGuard.currentUserHasRole("test")); + assertFalse(KarafMBeanServerGuard.currentUserHasRole("toast")); + return null; + } + }); + } + + public void testCurrentUserHasCustomRole() throws Exception { + Subject subject = new Subject(); + LoginModule lm = new TestLoginModule(new TestRolePrincipal("foo")); + lm.initialize(subject, null, null, null); + lm.login(); + lm.commit(); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + assertTrue(KarafMBeanServerGuard.currentUserHasRole(TestRolePrincipal.class.getCanonicalName() + ":foo")); + assertFalse(KarafMBeanServerGuard.currentUserHasRole("foo")); + return null; + } + }); + } + + public void testInvoke() throws Throwable { + Dictionary configuration = new Hashtable(); + configuration.put("someMethod", "editor"); + configuration.put("someOtherMethod", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("editor", "admin"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + Method im = MBeanServer.class.getMethod("invoke", ObjectName.class, String.class, Object[].class, String[].class); + + ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + // The following operation should not throw an exception + guard.invoke(null, im, new Object [] {on, "someMethod", new Object [] {"test"}, new String [] {"java.lang.String"}}); + + try { + guard.invoke(null, im, new Object [] {on, "someOtherMethod", new Object [] {}, new String [] {}}); + fail("Should not have allowed the invocation"); + } catch (SecurityException se) { + // good + } + + try { + guard.invoke(null, im, new Object [] {on, "somemethingElse", new Object [] {}, new String [] {}}); + fail("Should not have allowed the invocation"); + } catch (SecurityException se) { + // good + } + return null; + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + } + }); + } + + public void testGetAttributeIs() throws Throwable { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("Toast", "boolean", "", true, false, true); + MBeanAttributeInfo attr2 = new MBeanAttributeInfo("TestAttr", "java.lang.String", "", true, false, false); + MBeanAttributeInfo attr3 = new MBeanAttributeInfo("Butter", "int", "", true, true, false); + + MBeanInfo mbeanInfo = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(mbeanInfo.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr, attr2, attr3}).anyTimes(); + EasyMock.replay(mbeanInfo); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(mbeanInfo).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("getToast", "admin"); + configuration.put("isToast", "editor"); + configuration.put("getTest*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("editor", "admin"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + Method im = MBeanServer.class.getMethod("getAttribute", ObjectName.class, String.class); + + // The following operations should not throw an exception + guard.invoke(mbs, im, new Object [] {on, "Toast"}); + guard.invoke(mbs, im, new Object [] {on, "TestAttr"}); + + try { + guard.invoke(mbs, im, new Object [] {on, "Butter"}); + fail("Should not have allowed the invocation"); + } catch (SecurityException se) { + // good + } + + return null; + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + } + }); + } + + public void testGetAttributes() throws Throwable { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("Toast", "boolean", "", true, false, false); + MBeanAttributeInfo attr2 = new MBeanAttributeInfo("TestSomething", "java.lang.String", "", true, true, false); + MBeanAttributeInfo attr3 = new MBeanAttributeInfo("Butter", "int", "", true, true, false); + + MBeanInfo mbeanInfo = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(mbeanInfo.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr, attr2, attr3}).anyTimes(); + EasyMock.replay(mbeanInfo); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(mbeanInfo).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("getToast", "editor"); + configuration.put("getTest*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("editor", "admin"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + Method im = MBeanServer.class.getMethod("getAttributes", ObjectName.class, String[].class); + + // The following operations should not throw an exception + guard.invoke(mbs, im, new Object [] {on, new String [] {"Toast"}}); + guard.invoke(mbs, im, new Object [] {on, new String [] {"TestSomething", "Toast"}}); + + try { + guard.invoke(mbs, im, new Object [] {on, new String [] {"Butter", "Toast"}}); + fail("Should not have allowed the invocation"); + } catch (SecurityException se) { + // good + } + + return null; + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + } + }); + } + + public void testGetAttributes2() throws Throwable { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("Toast", "boolean", "", true, false, true); + MBeanAttributeInfo attr2 = new MBeanAttributeInfo("TestSomething", "boolean", "", true, false, true); + MBeanAttributeInfo attr3 = new MBeanAttributeInfo("Butter", "boolean", "", true, true, true); + + MBeanInfo mbeanInfo = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(mbeanInfo.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr, attr2, attr3}).anyTimes(); + EasyMock.replay(mbeanInfo); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(mbeanInfo).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("isT*", "editor"); + configuration.put("getToast", "admin"); + configuration.put("getButter", "editor"); + configuration.put("getTest*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("editor", "admin"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + Method im = MBeanServer.class.getMethod("getAttributes", ObjectName.class, String[].class); + + // The following operations should not throw an exception + guard.invoke(mbs, im, new Object [] {on, new String [] {"Toast"}}); + guard.invoke(mbs, im, new Object [] {on, new String [] {"TestSomething", "Toast"}}); + + try { + guard.invoke(mbs, im, new Object [] {on, new String [] {"Butter", "Toast"}}); + fail("Should not have allowed the invocation"); + } catch (SecurityException se) { + // good + } + + return null; + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + } + }); + } + + public void testSetAttribute() throws Throwable { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo a1 = new MBeanAttributeInfo("Something", "java.lang.String", "Something Attribute", true, true, false); + MBeanAttributeInfo a2 = new MBeanAttributeInfo("Value", "long", "Value Attribute", true, true, false); + MBeanAttributeInfo a3 = new MBeanAttributeInfo("Other", "boolean", "Other Attribute", true, true, false); + MBeanAttributeInfo[] attrs = new MBeanAttributeInfo[] {a1, a2, a3}; + + MBeanInfo mbeanInfo = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(mbeanInfo.getAttributes()).andReturn(attrs).anyTimes(); + EasyMock.replay(mbeanInfo); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(mbeanInfo).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("setSomething", "editor"); + configuration.put("setValue*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("editor", "admin"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + Method im = MBeanServer.class.getMethod("setAttribute", ObjectName.class, Attribute.class); + + // The following operations should not throw an exception + guard.invoke(mbs, im, new Object [] {on, new Attribute("Something", "v1")}); + guard.invoke(mbs, im, new Object [] {on, new Attribute("Value", 42L)}); + + try { + guard.invoke(mbs, im, new Object [] {on, new Attribute("Other", Boolean.TRUE)}); + fail("Should not have allowed the invocation"); + } catch (SecurityException se) { + // good + } + + try { + guard.invoke(mbs, im, new Object [] {on, new Attribute("NonExistent", "v4")}); + fail("Should not have found the MBean Declaration"); + } catch (IllegalStateException ise) { + // good + } + + return null; + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + } + }); + } + + public void testSetAttributes() throws Throwable { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo mba1 = new MBeanAttributeInfo("Something", "java.lang.String", "Something Attribute", true, true, false); + MBeanAttributeInfo mba2 = new MBeanAttributeInfo("Value", "long", "Value Attribute", true, true, false); + MBeanAttributeInfo mba3 = new MBeanAttributeInfo("Other", "boolean", "Other Attribute", true, true, false); + MBeanAttributeInfo[] attrs = new MBeanAttributeInfo[] {mba1, mba2, mba3}; + + MBeanInfo mbeanInfo = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(mbeanInfo.getAttributes()).andReturn(attrs).anyTimes(); + EasyMock.replay(mbeanInfo); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(mbeanInfo).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("setSomething", "editor"); + configuration.put("setValue*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("editor", "admin"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + Method im = MBeanServer.class.getMethod("setAttributes", ObjectName.class, AttributeList.class); + + // The following operations should not throw an exception + Attribute a1 = new Attribute("Something", "v1"); + Attribute a2 = new Attribute("Value", 42L); + guard.invoke(mbs, im, new Object [] {on, new AttributeList(Arrays.asList(a1))}); + guard.invoke(mbs, im, new Object [] {on, new AttributeList(Arrays.asList(a2, a1))}); + + Attribute a3 = new Attribute("Other", Boolean.TRUE); + try { + guard.invoke(mbs, im, new Object [] {on, new AttributeList(Arrays.asList(a1, a3))}); + fail("Should not have allowed the invocation"); + } catch (SecurityException se) { + // good + } + + try { + Attribute a4 = new Attribute("NonExistent", "v4"); + guard.invoke(mbs, im, new Object [] {on, new AttributeList(Arrays.asList(a4))}); + fail("Should not have found the MBean Declaration"); + } catch (IllegalStateException ise) { + // good + } + + return null; + } catch (Throwable ex) { + throw new RuntimeException(ex); + } + } + }); + } + + public void testCanInvokeMBean() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + final ObjectName on2 = ObjectName.getInstance("foo.bar:type=Toast"); + + MBeanParameterInfo[] sig = new MBeanParameterInfo[] {new MBeanParameterInfo("arg1", "java.lang.String", "")}; + MBeanOperationInfo op = new MBeanOperationInfo("doit", "", sig, "int", MBeanOperationInfo.INFO); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {op}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {}).anyTimes(); + EasyMock.replay(info); + MBeanInfo info2 = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info2.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info2.getAttributes()).andReturn(new MBeanAttributeInfo[] {}).anyTimes(); + EasyMock.replay(info2); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.expect(mbs.getMBeanInfo(on2)).andReturn(info2).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("doit(java.lang.String)[/11/]", "admin"); + configuration.put("doit(java.lang.String)", "viewer"); + configuration.put("doit(java.lang.String,java.lang.String)", "viewer"); + configuration.put("doit(int)[\"12\"]", "admin"); + configuration.put("doit", "admin"); + configuration.put("do*", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(mbs, on)); + assertFalse(guard.canInvoke(mbs, on2)); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeMBean2() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanParameterInfo[] sig = new MBeanParameterInfo[] {new MBeanParameterInfo("arg1", "java.lang.String", "")}; + MBeanOperationInfo op = new MBeanOperationInfo("doit", "", sig, "int", MBeanOperationInfo.INFO); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {op}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("doit(java.lang.String)[/11/]", "admin"); + configuration.put("doit(java.lang.String)", "admin"); + configuration.put("doit(java.lang.String,java.lang.String)", "admin"); + configuration.put("doit(int)[\"12\"]", "admin"); + configuration.put("doit", "admin"); + configuration.put("do*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertFalse(guard.canInvoke(mbs, on)); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeAnyOverload() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanParameterInfo[] sig = new MBeanParameterInfo[] {new MBeanParameterInfo("arg1", "java.lang.String", "")}; + MBeanOperationInfo op = new MBeanOperationInfo("doit", "", sig, "int", MBeanOperationInfo.INFO); + MBeanParameterInfo[] sig2 = new MBeanParameterInfo[] { + new MBeanParameterInfo("arg1", "java.lang.String", ""), + new MBeanParameterInfo("arg2", "java.lang.String", "")}; + MBeanOperationInfo op2 = new MBeanOperationInfo("doit", "", sig2, "int", MBeanOperationInfo.INFO); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {op, op2}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("doit(java.lang.String)", "admin"); + configuration.put("doit(java.lang.String,java.lang.String)", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(mbs, on, "doit")); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeAnyOverload2() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanParameterInfo[] sig = new MBeanParameterInfo[] {new MBeanParameterInfo("arg1", "java.lang.String", "")}; + MBeanOperationInfo op = new MBeanOperationInfo("foit", "", sig, "int", MBeanOperationInfo.INFO); + MBeanParameterInfo[] sig2 = new MBeanParameterInfo[] { + new MBeanParameterInfo("arg1", "java.lang.String", ""), + new MBeanParameterInfo("arg2", "java.lang.String", "")}; + MBeanOperationInfo op2 = new MBeanOperationInfo("doit", "", sig2, "int", MBeanOperationInfo.INFO); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {op, op2}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo [] {}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("foit(java.lang.String)", "viewer"); + configuration.put("doit(java.lang.String,java.lang.String)", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertFalse(guard.canInvoke(mbs, on, "doit")); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeAnyOverload3() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo [] {}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("doit(java.lang.String)", "admin"); + configuration.put("doit(java.lang.String,java.lang.String)", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertFalse(guard.canInvoke(mbs, on, "doit")); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanGetAttributeAnyOverload() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("Foo", "int", "", true, true, false); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("getFoo(java.lang.String)", "admin"); + configuration.put("getFoo()", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(mbs, on, "getFoo")); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanGetAttributeAnyOverload2() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("Foo", "int", "", true, true, false); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("getFoo(java.lang.String)", "viewer"); + configuration.put("getFoo()", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertFalse(guard.canInvoke(mbs, on, "getFoo")); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanGetAttributeAnyOverload3() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("Foo", "boolean", "", true, true, true); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("getFoo(java.lang.String)", "admin"); + configuration.put("getFoo()", "admin"); + configuration.put("isFoo()", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(mbs, on, "isFoo")); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanGetAttributeAnyOverload4() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("Foo", "boolean", "", true, true, true); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("getFoo(java.lang.String)", "viewer"); + configuration.put("getFoo()", "viewer"); + configuration.put("isFoo()", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertFalse(guard.canInvoke(mbs, on, "isFoo")); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanSetAttributeAnyOverload() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("Foo", "boolean", "", true, true, true); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("setFoo(java.lang.String)", "admin"); + configuration.put("setFoo(boolean)", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(mbs, on, "setFoo")); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanSetAttributeAnyOverload2() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("Foo", "boolean", "", true, true, true); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("setFoo(java.lang.String)", "viewer"); + configuration.put("setFoo(boolean)", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertFalse(guard.canInvoke(mbs, on, "setFoo")); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeMBeanGetter() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("a1", "boolean", "", true, false, true); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("get*", "admin"); + configuration.put("is*", "viewer"); + configuration.put("*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(mbs, on)); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeMBeanGetter2() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("a1", "boolean", "", true, false, false); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("get*", "admin"); + configuration.put("is*", "viewer"); + configuration.put("*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertFalse(guard.canInvoke(mbs, on)); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeMBeanGetter3() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("A1", "boolean", "", true, false, false); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("getA1", "viewer"); + configuration.put("is*", "admin"); + configuration.put("*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(mbs, on)); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeMBeanSetter() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("A2", "java.lang.String", "", true, true, false); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("get*", "admin"); + configuration.put("setA2", "viewer"); + configuration.put("*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(mbs, on)); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeMBeanSetter2() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + MBeanAttributeInfo attr = new MBeanAttributeInfo("A2", "java.lang.String", "", true, true, false); + + MBeanInfo info = EasyMock.createMock(MBeanInfo.class); + EasyMock.expect(info.getOperations()).andReturn(new MBeanOperationInfo[] {}).anyTimes(); + EasyMock.expect(info.getAttributes()).andReturn(new MBeanAttributeInfo[] {attr}).anyTimes(); + EasyMock.replay(info); + + final MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.expect(mbs.getMBeanInfo(on)).andReturn(info).anyTimes(); + EasyMock.replay(mbs); + + Dictionary configuration = new Hashtable(); + configuration.put("get*", "admin"); + configuration.put("setA2", "admin"); + configuration.put("*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertFalse(guard.canInvoke(mbs, on)); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeMethod() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + Dictionary configuration = new Hashtable(); + configuration.put("doit(java.lang.String)[/11/]", "admin"); + configuration.put("doit(java.lang.String)", "viewer"); + configuration.put("doit(java.lang.String,java.lang.String)", "viewer"); + configuration.put("doit(int)[\"12\"]", "admin"); + configuration.put("doit", "admin"); + configuration.put("do*", "viewer"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(null, on, "dodo", new String [] {"java.lang.String"})); + assertTrue(guard.canInvoke(null, on, "doit", new String [] {"java.lang.String", "java.lang.String"})); + assertTrue(guard.canInvoke(null, on, "doit", new String [] {"java.lang.String"})); + assertFalse(guard.canInvoke(null, on, "doit", new String [] {"int"})); + assertFalse(guard.canInvoke(null, on, "doit", new String [] {})); + assertFalse(guard.canInvoke(null, on, "uuuh", new String [] {"java.lang.String"})); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + public void testCanInvokeMethod2() throws Exception { + final ObjectName on = ObjectName.getInstance("foo.bar:type=Test"); + + Dictionary configuration = new Hashtable(); + configuration.put("doit(java.lang.String)[/11/]", "viewer"); + configuration.put("doit(java.lang.String)", "admin"); + configuration.put("doit(java.lang.String,java.lang.String)", "admin"); + configuration.put("doit(int)[\"12\"]", "viewer"); + configuration.put("doit", "viewer"); + configuration.put("do*", "admin"); + ConfigurationAdmin ca = getMockConfigAdmin(configuration); + + final KarafMBeanServerGuard guard = new KarafMBeanServerGuard(); + guard.setConfigAdmin(ca); + + Subject subject = loginWithTestRoles("viewer"); + + Subject.doAs(subject, new PrivilegedAction() { + public Void run() { + try { + assertTrue(guard.canInvoke(null, on, "doit", new String [] {"java.lang.String"})); + assertTrue(guard.canInvoke(null, on, "doit", new String [] {})); + assertTrue(guard.canInvoke(null, on, "doit", new String [] {"int"})); + assertFalse(guard.canInvoke(null, on, "doit", new String [] {"java.lang.String", "java.lang.String"})); + assertFalse(guard.canInvoke(null, on, "dodo", new String [] {"java.lang.String"})); + assertFalse(guard.canInvoke(null, on, "uuuh", new String [] {"java.lang.String"})); + + return null; + } catch (Throwable th) { + throw new RuntimeException(th); + } + } + }); + } + + private Subject loginWithTestRoles(String ... roles) throws LoginException { + Subject subject = new Subject(); + LoginModule lm = new TestLoginModule(roles); + lm.initialize(subject, null, null, null); + lm.login(); + lm.commit(); + return subject; + } + + private static class TestLoginModule implements LoginModule { + private final Principal [] principals; + private Subject subject; + + private static Principal [] getPrincipals(String... roles) { + List principals = new ArrayList(); + for (String role : roles) { + principals.add(new RolePrincipal(role)); + } + return principals.toArray(new Principal [] {}); + } + + + public TestLoginModule(String ... roles) { + this(getPrincipals(roles)); + } + + public TestLoginModule(Principal ... principals) { + this.principals = principals; + } + + public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { + this.subject = subject; + } + + public boolean login() throws LoginException { + return true; + } + + public boolean commit() throws LoginException { + Set sp = subject.getPrincipals(); + sp.addAll(Arrays.asList(principals)); + return true; + } + + public boolean abort() throws LoginException { + return true; + } + + public boolean logout() throws LoginException { + Set sp = subject.getPrincipals(); + sp.removeAll(Arrays.asList(principals)); + return true; + } + } +} diff --git a/management/server/src/test/java/org/apache/karaf/management/TestRolePrincipal.java b/management/server/src/test/java/org/apache/karaf/management/TestRolePrincipal.java new file mode 100644 index 0000000..1a416fe --- /dev/null +++ b/management/server/src/test/java/org/apache/karaf/management/TestRolePrincipal.java @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.management; + +import java.security.Principal; + +public class TestRolePrincipal implements Principal { + private final String name; + + public TestRolePrincipal(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TestRolePrincipal other = (TestRolePrincipal) obj; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } +} diff --git a/management/server/src/test/java/org/apache/karaf/management/internal/JMXSecurityMBeanImplTestCase.java b/management/server/src/test/java/org/apache/karaf/management/internal/JMXSecurityMBeanImplTestCase.java new file mode 100644 index 0000000..bdb8ae4 --- /dev/null +++ b/management/server/src/test/java/org/apache/karaf/management/internal/JMXSecurityMBeanImplTestCase.java @@ -0,0 +1,235 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.karaf.management.internal; + +import java.io.IOException; +import java.lang.reflect.InvocationHandler; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.openmbean.CompositeData; +import javax.management.openmbean.TabularData; + +import junit.framework.TestCase; + +import org.apache.karaf.management.KarafMBeanServerGuard; +import org.apache.karaf.management.boot.KarafMBeanServerBuilder; +import org.easymock.EasyMock; + +public class JMXSecurityMBeanImplTestCase extends TestCase { + public void testMBeanServerAccessors() throws Exception { + MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.replay(mbs); + + JMXSecurityMBeanImpl mb = new JMXSecurityMBeanImpl(); + mb.setMBeanServer(mbs); + assertSame(mbs, mb.getMBeanServer()); + } + + public void testCanInvokeMBean() throws Exception { + InvocationHandler prevGuard = KarafMBeanServerBuilder.getGuard(); + try { + MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.replay(mbs); + + String objectName = "foo.bar.testing:type=SomeMBean"; + KarafMBeanServerGuard testGuard = EasyMock.createMock(KarafMBeanServerGuard.class); + EasyMock.expect(testGuard.canInvoke(mbs, new ObjectName(objectName))).andReturn(true); + EasyMock.replay(testGuard); + KarafMBeanServerBuilder.setGuard(testGuard); + + JMXSecurityMBeanImpl mb = new JMXSecurityMBeanImpl(); + mb.setMBeanServer(mbs); + assertTrue(mb.canInvoke(objectName)); + } finally { + KarafMBeanServerBuilder.setGuard(prevGuard); + } + } + + public void testCanInvokeMBean2() throws Exception { + InvocationHandler prevGuard = KarafMBeanServerBuilder.getGuard(); + try { + MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.replay(mbs); + + String objectName = "foo.bar.testing:type=SomeMBean"; + KarafMBeanServerGuard testGuard = EasyMock.createMock(KarafMBeanServerGuard.class); + EasyMock.expect(testGuard.canInvoke(mbs, new ObjectName(objectName))).andReturn(false); + EasyMock.replay(testGuard); + KarafMBeanServerBuilder.setGuard(testGuard); + + JMXSecurityMBeanImpl mb = new JMXSecurityMBeanImpl(); + mb.setMBeanServer(mbs); + assertFalse(mb.canInvoke(objectName)); + } finally { + KarafMBeanServerBuilder.setGuard(prevGuard); + } + } + + public void testCanInvokeMBeanThrowsException() throws Exception { + InvocationHandler prevGuard = KarafMBeanServerBuilder.getGuard(); + try { + MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.replay(mbs); + + String objectName = "foo.bar.testing:type=SomeMBean"; + KarafMBeanServerGuard testGuard = EasyMock.createMock(KarafMBeanServerGuard.class); + EasyMock.expect(testGuard.canInvoke(mbs, new ObjectName(objectName))).andThrow(new IOException()); + EasyMock.replay(testGuard); + KarafMBeanServerBuilder.setGuard(testGuard); + + JMXSecurityMBeanImpl mb = new JMXSecurityMBeanImpl(); + mb.setMBeanServer(mbs); + mb.canInvoke(objectName); + fail("Should have thrown an exception"); + } catch (IOException ioe) { + // good! + } finally { + KarafMBeanServerBuilder.setGuard(prevGuard); + } + } + + public void testCanInvokeMBeanNoGuard() throws Exception { + InvocationHandler prevGuard = KarafMBeanServerBuilder.getGuard(); + try { + JMXSecurityMBeanImpl mb = new JMXSecurityMBeanImpl(); + assertTrue(mb.canInvoke("foo.bar.testing:type=SomeMBean")); + } finally { + KarafMBeanServerBuilder.setGuard(prevGuard); + } + } + + public void testCanInvokeMethod() throws Exception { + InvocationHandler prevGuard = KarafMBeanServerBuilder.getGuard(); + try { + MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.replay(mbs); + + String objectName = "foo.bar.testing:type=SomeMBean"; + KarafMBeanServerGuard testGuard = EasyMock.createMock(KarafMBeanServerGuard.class); + String[] la = new String [] {"long"}; + String[] sa = new String [] {"java.lang.String"}; + String[] sa2 = new String [] {"java.lang.String", "java.lang.String"}; + EasyMock.expect(testGuard.canInvoke(mbs, new ObjectName(objectName), "testMethod", la)).andReturn(true); + EasyMock.expect(testGuard.canInvoke(mbs, new ObjectName(objectName), "testMethod", sa)).andReturn(true); + EasyMock.expect(testGuard.canInvoke(mbs, new ObjectName(objectName), "otherMethod", sa2)).andReturn(false); + EasyMock.replay(testGuard); + KarafMBeanServerBuilder.setGuard(testGuard); + + JMXSecurityMBeanImpl mb = new JMXSecurityMBeanImpl(); + mb.setMBeanServer(mbs); + assertTrue(mb.canInvoke(objectName, "testMethod", la)); + assertTrue(mb.canInvoke(objectName, "testMethod", sa)); + assertFalse(mb.canInvoke(objectName, "otherMethod", sa2)); + } finally { + KarafMBeanServerBuilder.setGuard(prevGuard); + } + } + + public void testCanInvokeMethodException() throws Exception { + InvocationHandler prevGuard = KarafMBeanServerBuilder.getGuard(); + try { + MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.replay(mbs); + + String objectName = "foo.bar.testing:type=SomeMBean"; + KarafMBeanServerGuard testGuard = EasyMock.createMock(KarafMBeanServerGuard.class); + String[] ea = new String [] {}; + EasyMock.expect(testGuard.canInvoke(mbs, new ObjectName(objectName), "testMethod", ea)).andThrow(new IOException()); + EasyMock.replay(testGuard); + KarafMBeanServerBuilder.setGuard(testGuard); + + JMXSecurityMBeanImpl mb = new JMXSecurityMBeanImpl(); + mb.setMBeanServer(mbs); + mb.canInvoke(objectName, "testMethod", ea); + fail("Should have thrown an exception"); + } catch (IOException ioe) { + // good + } finally { + KarafMBeanServerBuilder.setGuard(prevGuard); + } + } + + public void testCanInvokeMethodNoGuard() throws Exception { + InvocationHandler prevGuard = KarafMBeanServerBuilder.getGuard(); + try { + JMXSecurityMBeanImpl mb = new JMXSecurityMBeanImpl(); + assertTrue(mb.canInvoke("foo.bar.testing:type=SomeMBean", "someMethod", new String [] {})); + } finally { + KarafMBeanServerBuilder.setGuard(prevGuard); + } + } + + public void testCanInvokeBulk() throws Exception { + InvocationHandler prevGuard = KarafMBeanServerBuilder.getGuard(); + try { + MBeanServer mbs = EasyMock.createMock(MBeanServer.class); + EasyMock.replay(mbs); + + KarafMBeanServerGuard testGuard = EasyMock.createMock(KarafMBeanServerGuard.class); + String objectName = "foo.bar.testing:type=SomeMBean"; + final String[] la = new String [] {"long"}; + final String[] sa = new String [] {"java.lang.String"}; + EasyMock.expect(testGuard.canInvoke(EasyMock.eq(mbs), EasyMock.eq(new ObjectName(objectName)), EasyMock.eq("testMethod"), EasyMock.aryEq(la))).andReturn(true).anyTimes(); + EasyMock.expect(testGuard.canInvoke(EasyMock.eq(mbs), EasyMock.eq(new ObjectName(objectName)), EasyMock.eq("testMethod"), EasyMock.aryEq(sa))).andReturn(false).anyTimes(); + EasyMock.expect(testGuard.canInvoke(EasyMock.eq(mbs), EasyMock.eq(new ObjectName(objectName)), EasyMock.eq("otherMethod"))).andReturn(true).anyTimes(); + String objectName2 = "foo.bar.testing:type=SomeOtherMBean"; + EasyMock.expect(testGuard.canInvoke(EasyMock.eq(mbs), EasyMock.eq(new ObjectName(objectName2)))).andReturn(true).anyTimes(); + String objectName3 = "foo.bar.foo.testing:type=SomeOtherMBean"; + EasyMock.expect(testGuard.canInvoke(EasyMock.eq(mbs), EasyMock.eq(new ObjectName(objectName3)))).andReturn(false).anyTimes(); + EasyMock.replay(testGuard); + KarafMBeanServerBuilder.setGuard(testGuard); + + JMXSecurityMBeanImpl mb = new JMXSecurityMBeanImpl(); + mb.setMBeanServer(mbs); + Map> query = new HashMap>(); + query.put(objectName, Arrays.asList("otherMethod", "testMethod(long)", "testMethod(java.lang.String)")); + query.put(objectName2, Collections.emptyList()); + query.put(objectName3, Collections.emptyList()); + TabularData result = mb.canInvoke(query); + assertEquals(5, result.size()); + + CompositeData cd = result.get(new Object [] {objectName, "testMethod(long)"}); + assertEquals(objectName, cd.get("ObjectName")); + assertEquals("testMethod(long)", cd.get("Method")); + assertEquals(true, cd.get("CanInvoke")); + CompositeData cd2 = result.get(new Object [] {objectName, "testMethod(java.lang.String)"}); + assertEquals(objectName, cd2.get("ObjectName")); + assertEquals("testMethod(java.lang.String)", cd2.get("Method")); + assertEquals(false, cd2.get("CanInvoke")); + CompositeData cd3 = result.get(new Object [] {objectName, "otherMethod"}); + assertEquals(objectName, cd3.get("ObjectName")); + assertEquals("otherMethod", cd3.get("Method")); + assertEquals(true, cd3.get("CanInvoke")); + CompositeData cd4 = result.get(new Object [] {objectName2, ""}); + assertEquals(objectName2, cd4.get("ObjectName")); + assertEquals("", cd4.get("Method")); + assertEquals(true, cd4.get("CanInvoke")); + CompositeData cd5 = result.get(new Object [] {objectName3, ""}); + assertEquals(objectName3, cd5.get("ObjectName")); + assertEquals("", cd5.get("Method")); + assertEquals(false, cd5.get("CanInvoke")); + } finally { + KarafMBeanServerBuilder.setGuard(prevGuard); + } + } +} diff --git a/pom.xml b/pom.xml index 09f2533..45a5e36 100644 --- a/pom.xml +++ b/pom.xml @@ -483,6 +483,11 @@ org.apache.karaf.management + org.apache.karaf.management.boot + ${project.version} + + + org.apache.karaf.management org.apache.karaf.management.server ${project.version} -- 1.7.10.2 (Apple Git-33)