You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by ff...@apache.org on 2014/04/29 11:23:12 UTC
[1/4] [KARAF-2934]Role-based security for Shell/Console commands -
backport to 2.x branch #1st commit
Repository: karaf
Updated Branches:
refs/heads/karaf-2.x 8a35113ea -> 4b8cc1022
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/test/java/org/apache/karaf/shell/security/impl/SecuredCommandProcessorImplTest.java
----------------------------------------------------------------------
diff --git a/shell/console/src/test/java/org/apache/karaf/shell/security/impl/SecuredCommandProcessorImplTest.java b/shell/console/src/test/java/org/apache/karaf/shell/security/impl/SecuredCommandProcessorImplTest.java
new file mode 100644
index 0000000..351a89f
--- /dev/null
+++ b/shell/console/src/test/java/org/apache/karaf/shell/security/impl/SecuredCommandProcessorImplTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.shell.security.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.security.PrivilegedAction;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+
+import org.apache.felix.gogo.api.CommandSessionListener;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.Converter;
+import org.apache.felix.service.threadio.ThreadIO;
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Test;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+
+public class SecuredCommandProcessorImplTest {
+ @Test
+ public void testCommandProcessor() throws Exception {
+ ThreadIO tio = EasyMock.createMock(ThreadIO.class);
+ EasyMock.replay(tio);
+
+ @SuppressWarnings("unchecked")
+ ServiceReference<ThreadIO> tioRef = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(tioRef);
+
+ final BundleContext bc = EasyMock.createMock(BundleContext.class);
+ EasyMock.expect(bc.getServiceReference(ThreadIO.class)).andReturn(tioRef).anyTimes();
+ EasyMock.expect(bc.getService(tioRef)).andReturn(tio).anyTimes();
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.expect(bc.getServiceReferences((String) EasyMock.anyObject(), (String) EasyMock.anyObject())).andReturn(null).anyTimes();
+
+ // Capture the listeners
+ final Map<String, ServiceListener> listeners = new HashMap<String, ServiceListener>();
+
+ // Here are the expected calls
+ final String commandFilter = "(&(osgi.command.scope=*)(osgi.command.function=*)" +
+ "(|(org.apache.karaf.service.guard.roles=aaabbbccc)(!(org.apache.karaf.service.guard.roles=*))))";
+ expectServiceTracker(bc, commandFilter, listeners);
+ expectServiceTracker(bc, "(objectClass=" + Converter.class.getName() + ")", listeners);
+ expectServiceTracker(bc, "(objectClass=" + CommandSessionListener.class.getName() + ")", listeners);
+ EasyMock.replay(bc);
+
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new RolePrincipal("aaabbbccc"));
+
+ Subject.doAs(subject, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ MySecuredCommandProcessorImpl scp = new MySecuredCommandProcessorImpl(bc) {};
+
+ assertEquals(3, scp.getCommands().size());
+ assertTrue(scp.getCommands().contains("osgi:addcommand"));
+ assertTrue(scp.getCommands().contains("osgi:removecommand"));
+ assertTrue(scp.getCommands().contains("osgi:eval"));
+ assertEquals(1, scp.getConstants().size());
+ assertEquals(bc, scp.getConstants().get(".context"));
+
+ // Now let's make a command appear...
+ ServiceListener commandListener = listeners.get(commandFilter);
+
+ ServiceReference<?> cdRef = EasyMock.createMock(ServiceReference.class);
+ EasyMock.expect(cdRef.getProperty(CommandProcessor.COMMAND_SCOPE)).andReturn("foo");
+ EasyMock.expect(cdRef.getProperty(CommandProcessor.COMMAND_FUNCTION)).andReturn("bar");
+ EasyMock.replay(cdRef);
+
+ ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, cdRef);
+ commandListener.serviceChanged(event);
+ assertEquals(4, scp.getCommands().size());
+ assertTrue(scp.getCommands().contains("foo:bar"));
+
+ ServiceReference<?> cd2Ref = EasyMock.createMock(ServiceReference.class);
+ EasyMock.expect(cd2Ref.getProperty(CommandProcessor.COMMAND_SCOPE)).andReturn("xxx");
+ EasyMock.expect(cd2Ref.getProperty(CommandProcessor.COMMAND_FUNCTION)).andReturn(
+ new String[] {"aaa", "bbb"});
+ EasyMock.replay(cd2Ref);
+
+ ServiceEvent event2 = new ServiceEvent(ServiceEvent.REGISTERED, cd2Ref);
+ commandListener.serviceChanged(event2);
+ assertEquals(6, scp.getCommands().size());
+ assertTrue(scp.getCommands().contains("xxx:aaa"));
+ assertTrue(scp.getCommands().contains("xxx:bbb"));
+
+ return null;
+ }
+ });
+ }
+
+ void expectServiceTracker(final BundleContext bc, final String expectedFilter, final Map<String, ServiceListener> listeners) throws InvalidSyntaxException {
+ bc.addServiceListener(EasyMock.isA(ServiceListener.class), EasyMock.eq(expectedFilter));
+ EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ listeners.put(expectedFilter, (ServiceListener) EasyMock.getCurrentArguments()[0]);
+ return null;
+ }
+ }).once();
+ }
+
+ // Subclass to provide access to some protected members
+ static class MySecuredCommandProcessorImpl extends SecuredCommandProcessorImpl {
+ public MySecuredCommandProcessorImpl(BundleContext bc) {
+ super(bc);
+ }
+
+ Map<String, Object> getConstants() {
+ return constants;
+ }
+ };
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/KarafJaasAuthenticator.java
----------------------------------------------------------------------
diff --git a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/KarafJaasAuthenticator.java b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/KarafJaasAuthenticator.java
index 50eb5d1..0005892 100644
--- a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/KarafJaasAuthenticator.java
+++ b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/KarafJaasAuthenticator.java
@@ -19,7 +19,6 @@
package org.apache.karaf.shell.ssh;
import java.io.IOException;
-import java.security.Principal;
import java.security.PublicKey;
import javax.security.auth.Subject;
@@ -28,7 +27,6 @@ import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
-import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginContext;
import org.apache.karaf.jaas.modules.publickey.PublickeyCallback;
@@ -45,8 +43,7 @@ public class KarafJaasAuthenticator implements PasswordAuthenticator, PublickeyA
private final Logger LOGGER = LoggerFactory.getLogger(KarafJaasAuthenticator.class);
private String realm;
- private String role;
-
+
public String getRealm() {
return realm;
}
@@ -55,13 +52,7 @@ public class KarafJaasAuthenticator implements PasswordAuthenticator, PublickeyA
this.realm = realm;
}
- public String getRole() {
- return role;
- }
-
- public void setRole(String role) {
- this.role = role;
- }
+
public boolean authenticate(final String username, final String password, final ServerSession session) {
try {
@@ -80,26 +71,7 @@ public class KarafJaasAuthenticator implements PasswordAuthenticator, PublickeyA
}
});
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);
- }
- }
+
session.setAttribute(SUBJECT_ATTRIBUTE_KEY, subject);
return true;
} catch (Exception e) {
@@ -125,26 +97,7 @@ public class KarafJaasAuthenticator implements PasswordAuthenticator, PublickeyA
}
});
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);
- }
- }
+
session.setAttribute(SUBJECT_ATTRIBUTE_KEY, subject);
return true;
} catch (Exception e) {
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/ShellFactoryImpl.java
----------------------------------------------------------------------
diff --git a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/ShellFactoryImpl.java b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/ShellFactoryImpl.java
index b4d3ad8..37fb37c 100644
--- a/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/ShellFactoryImpl.java
+++ b/shell/ssh/src/main/java/org/apache/karaf/shell/ssh/ShellFactoryImpl.java
@@ -42,6 +42,7 @@ import org.apache.sshd.server.Environment;
import org.apache.sshd.server.ExitCallback;
import org.apache.sshd.server.SessionAware;
import org.apache.sshd.server.session.ServerSession;
+import org.osgi.framework.BundleContext;
import org.osgi.service.blueprint.container.ReifiedType;
/**
@@ -57,6 +58,7 @@ public class ShellFactoryImpl implements Factory<Command> {
private CommandProcessor commandProcessor;
private ThreadIO threadIO;
+ private BundleContext bundleContext;
public void setCommandProcessor(CommandProcessor commandProcessor) {
this.commandProcessor = commandProcessor;
@@ -65,6 +67,10 @@ public class ShellFactoryImpl implements Factory<Command> {
public void setThreadIO(ThreadIO threadIO) {
this.threadIO = threadIO;
}
+
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
public Command create() {
return new ShellImpl();
@@ -122,7 +128,8 @@ public class ShellFactoryImpl implements Factory<Command> {
public void run() {
destroy();
}
- });
+ },
+ bundleContext);
final CommandSession session = console.getSession();
session.put("APPLICATION", System.getProperty("karaf.name", "root"));
for (Map.Entry<String,String> e : env.getEnv().entrySet()) {
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/ssh/src/main/resources/OSGI-INF/blueprint/shell-ssh.xml
----------------------------------------------------------------------
diff --git a/shell/ssh/src/main/resources/OSGI-INF/blueprint/shell-ssh.xml b/shell/ssh/src/main/resources/OSGI-INF/blueprint/shell-ssh.xml
index 6ad80bd..54c3660 100644
--- a/shell/ssh/src/main/resources/OSGI-INF/blueprint/shell-ssh.xml
+++ b/shell/ssh/src/main/resources/OSGI-INF/blueprint/shell-ssh.xml
@@ -31,7 +31,6 @@
<ext:property-placeholder placeholder-prefix="$[" placeholder-suffix="]">
<ext:default-properties>
<ext:property name="startRemoteShell" value="true" />
- <ext:property name="karaf.admin.role" value="admin" />
</ext:default-properties>
</ext:property-placeholder>
@@ -41,7 +40,6 @@
<cm:property name="sshHost" value="0.0.0.0"/>
<cm:property name="sshIdleTimeout" value="1800000"/>
<cm:property name="sshRealm" value="karaf"/>
- <cm:property name="sshRole" value="$[karaf.admin.role]"/>
<cm:property name="hostKey" value="$[karaf.etc]/host.key"/>
<cm:property name="authorizedKeys" value="$[karaf.etc]/authorized_keys"/>
<cm:property name="authMethods" value="keyboard-interactive,password,publickey"/>
@@ -95,6 +93,7 @@
<bean class="org.apache.karaf.shell.ssh.ShellFactoryImpl">
<property name="commandProcessor" ref="commandProcessor"/>
<property name="threadIO" ref="threadIO"/>
+ <property name="bundleContext" ref="blueprintBundleContext"/>
</bean>
</property>
<property name="commandFactory">
@@ -137,7 +136,6 @@
</bean>
<bean id="authenticator" class="org.apache.karaf.shell.ssh.KarafJaasAuthenticator">
<property name="realm" value="${sshRealm}"/>
- <property name="role" value="${sshRole}"/>
</bean>
<bean id="sshServerFactory" class="org.apache.karaf.shell.ssh.SshServerFactory" init-method="start"
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/GogoPlugin.java
----------------------------------------------------------------------
diff --git a/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/GogoPlugin.java b/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/GogoPlugin.java
index 93c8655..0937d31 100644
--- a/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/GogoPlugin.java
+++ b/webconsole/gogo/src/main/java/org/apache/karaf/webconsole/gogo/GogoPlugin.java
@@ -191,7 +191,8 @@ public class GogoPlugin extends AbstractWebConsolePlugin {
pipedOut,
new WebTerminal(TERM_WIDTH, TERM_HEIGHT),
null,
- null);
+ null,
+ bundleContext);
CommandSession session = console.getSession();
session.put("APPLICATION", System.getProperty("karaf.name", "root"));
session.put("USER", "karaf");
[2/4] [KARAF-2934]Role-based security for Shell/Console commands -
backport to 2.x branch #1st commit
Posted by ff...@apache.org.
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardingEventHookTest.java
----------------------------------------------------------------------
diff --git a/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardingEventHookTest.java b/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardingEventHookTest.java
new file mode 100644
index 0000000..31d683d
--- /dev/null
+++ b/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardingEventHookTest.java
@@ -0,0 +1,253 @@
+/*
+ * 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.service.guard.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.hooks.service.ListenerHook.ListenerInfo;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+public class GuardingEventHookTest {
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testEventHookEvents() throws Exception {
+ BundleContext frameworkBC = mockBundleContext(0L);
+
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put("service.guard", "(service.id=*)");
+ BundleContext hookBC = mockConfigAdminBundleContext(config);
+ GuardProxyCatalog gpc = new GuardProxyCatalog(hookBC);
+
+ Filter serviceFilter = FrameworkUtil.createFilter("(foo=bar)");
+ GuardingEventHook geh = new GuardingEventHook(hookBC, gpc, serviceFilter);
+
+ Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put(Constants.SERVICE_ID, 13L);
+ ServiceReference<?> sref = mockServiceReference(props);
+
+ BundleContext client1BC = mockBundleContext(123L);
+ Map<BundleContext, Collection<ListenerInfo>> listeners = new HashMap<BundleContext, Collection<ListenerInfo>>();
+ listeners.put(client1BC, Collections.<ListenerInfo>emptyList());
+
+ // Send the event. It should have no effect because the service doens't match the filter
+ assertEquals("Precondition", 0, gpc.proxyMap.size());
+ geh.event(new ServiceEvent(ServiceEvent.REGISTERED, sref), listeners);
+ assertEquals("No proxy should have been created because the service doesn't match the filter", 0, gpc.proxyMap.size());
+ assertEquals("Nothing should have been removed from the listeners", 1, listeners.size());
+
+ long service2ID = 887L;
+ Dictionary<String, Object> props2 = new Hashtable<String, Object>();
+ props2.put(Constants.SERVICE_ID, service2ID);
+ props2.put("a", "b");
+ props2.put("foo", "bar");
+ ServiceReference<?> sref2 = mockServiceReference(props2);
+ ServiceEvent se2 = new ServiceEvent(ServiceEvent.REGISTERED, sref2);
+
+ // Send the event to the system bundle and the bundle that contains the hook, should have no effect
+ Map<BundleContext, Collection<ListenerInfo>> listeners2 = new Hashtable<BundleContext, Collection<ListenerInfo>>();
+ listeners2.put(frameworkBC, Collections.<ListenerInfo>emptyList());
+ listeners2.put(hookBC, Collections.<ListenerInfo>emptyList());
+ geh.event(se2, listeners2);
+ assertEquals("No proxies to be created for the hook bundle or the system bundle", 0, gpc.proxyMap.size());
+ assertEquals("Nothing should have been removed from the listeners", 2, listeners2.size());
+
+ // This is the first time that a proxy actually does get created
+ Map<BundleContext, Collection<ListenerInfo>> listeners3 = new HashMap<BundleContext, Collection<ListenerInfo>>();
+ listeners3.put(client1BC, Collections.<ListenerInfo>emptyList());
+ geh.event(se2, listeners3);
+ assertEquals("The service should be hidden from these listeners", 0, listeners3.size());
+ assertEquals("Proxy should have been created for this client", 1, gpc.proxyMap.size());
+ assertEquals(new Long(service2ID), gpc.proxyMap.keySet().iterator().next());
+
+ // Update the service, now an additional client is interested
+ props2.put("a", "c"); // Will change the properties of sref
+ Map<BundleContext, Collection<ListenerInfo>> listeners4 = new HashMap<BundleContext, Collection<ListenerInfo>>();
+
+ BundleContext client2BC = mockBundleContext(11);
+ listeners4.put(client2BC, Collections.<ListenerInfo>emptyList());
+ listeners4.put(client1BC, Collections.<ListenerInfo>emptyList());
+ geh.event(new ServiceEvent(ServiceEvent.MODIFIED, sref2), listeners4);
+ assertEquals("The service should be hidden from these listeners", 0, listeners4.size());
+ assertEquals("There should not be an additional proxy for client 2", 1, gpc.proxyMap.size());
+ assertNotNull(gpc.proxyMap.get(service2ID));
+
+ long service3ID = 1L;
+ Dictionary<String, Object> props3 = new Hashtable<String, Object>();
+ props3.put(Constants.SERVICE_ID, service3ID);
+ props3.put("foo", "bar");
+ ServiceReference<?> sref3 = mockServiceReference(props3);
+
+ // An event for a new service
+ Map<BundleContext, Collection<ListenerInfo>> listeners5 = new HashMap<BundleContext, Collection<ListenerInfo>>();
+ listeners5.put(client1BC, Collections.<ListenerInfo>emptyList());
+ listeners5.put(client1BC, Collections.<ListenerInfo>emptyList()); // Should be ignored
+ geh.event(new ServiceEvent(ServiceEvent.REGISTERED, sref3), listeners5);
+ assertEquals("There should be an additional procy for client1 to the new service", 2, gpc.proxyMap.size());
+ assertEquals("The service should be hidden from these listeners", 0, listeners5.size());
+ assertNotNull(gpc.proxyMap.get(service2ID));
+ assertNotNull(gpc.proxyMap.get(service3ID));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testEventHookProxyEvents() throws Exception {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put("service.guard", "(service.id=*)");
+ BundleContext hookBC = mockConfigAdminBundleContext(config);
+ GuardProxyCatalog gpc = new GuardProxyCatalog(hookBC);
+
+ Filter serviceFilter = FrameworkUtil.createFilter("(service.id=*)"); // any service will match
+ GuardingEventHook geh = new GuardingEventHook(hookBC, gpc, serviceFilter);
+
+ BundleContext client1BC = mockBundleContext(123L);
+
+ // Create a proxy service mock
+ Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put(Constants.SERVICE_ID, 13L);
+ props.put(GuardProxyCatalog.PROXY_SERVICE_KEY, Boolean.TRUE);
+ ServiceReference<?> sref = mockServiceReference(props);
+ Map<BundleContext, Collection<ListenerInfo>> listeners = new HashMap<BundleContext, Collection<ListenerInfo>>();
+ listeners.put(client1BC, Collections.<ListenerInfo>emptyList());
+
+ // Send the event. It should have no effect because the service is already a proxy for the client
+ assertEquals("Precondition", 0, gpc.proxyMap.size());
+ geh.event(new ServiceEvent(ServiceEvent.REGISTERED, sref), listeners);
+ assertEquals("No changes expected for the proxy map.", 0, gpc.proxyMap.size());
+ assertEquals("The event should be delivered to the client", 1, listeners.size());
+
+ // send the event to a different client, this client should also see the event as the proxy Service Factory is shared
+ Map<BundleContext, Collection<ListenerInfo>> listeners2 = new HashMap<BundleContext, Collection<ListenerInfo>>();
+ listeners2.put(mockBundleContext(51L), Collections.<ListenerInfo>emptyList());
+ geh.event(new ServiceEvent(ServiceEvent.REGISTERED, sref), listeners2);
+ assertEquals("No changes expected for the proxy map.", 0, gpc.proxyMap.size());
+ assertEquals("The event should be delivered to the client", 1, listeners2.size());
+ }
+
+ @Test
+ public void testEventHookNoFilter() throws Exception {
+ BundleContext hookBC = mockBundleContext(5L);
+ GuardProxyCatalog gpc = new GuardProxyCatalog(hookBC);
+
+ GuardingEventHook geh = new GuardingEventHook(hookBC, gpc, null);
+ geh.event(null, null); // Should do nothing
+ }
+
+ private BundleContext mockBundleContext(long id) throws Exception {
+ Bundle bundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(bundle.getBundleId()).andReturn(id).anyTimes();
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(bc.getBundle()).andReturn(bundle).anyTimes();
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.replay(bc);
+
+ EasyMock.expect(bundle.getBundleContext()).andReturn(bc).anyTimes();
+ EasyMock.replay(bundle);
+
+ return bc;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private BundleContext mockConfigAdminBundleContext(Dictionary<String, Object> ... configs) throws IOException,
+ InvalidSyntaxException {
+ Configuration [] configurations = new Configuration[configs.length];
+
+ for (int i = 0; i < configs.length; i++) {
+ Configuration conf = EasyMock.createMock(Configuration.class);
+ EasyMock.expect(conf.getProperties()).andReturn(configs[i]).anyTimes();
+ EasyMock.expect(conf.getPid()).andReturn((String) configs[i].get(Constants.SERVICE_PID)).anyTimes();
+ EasyMock.replay(conf);
+ configurations[i] = conf;
+ }
+
+ ConfigurationAdmin ca = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(ca.listConfigurations("(&(service.pid=org.apache.karaf.service.acl.*)(service.guard=*))")).andReturn(configurations).anyTimes();
+ EasyMock.replay(ca);
+
+ final ServiceReference caSR = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(caSR);
+
+ Bundle b = EasyMock.createMock(Bundle.class);
+ EasyMock.expect(b.getBundleId()).andReturn(877342449L).anyTimes();
+ EasyMock.replay(b);
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(bc.getBundle()).andReturn(b).anyTimes();
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ String cmFilter = "(&(objectClass=" + ConfigurationAdmin.class.getName() + ")"
+ + "(!(" + GuardProxyCatalog.PROXY_SERVICE_KEY + "=*)))";
+ bc.addServiceListener(EasyMock.isA(ServiceListener.class), EasyMock.eq(cmFilter));
+ EasyMock.expectLastCall().anyTimes();
+ EasyMock.expect(bc.getServiceReferences(EasyMock.anyObject(String.class), EasyMock.eq(cmFilter))).
+ andReturn(new ServiceReference<?> [] {caSR}).anyTimes();
+ EasyMock.expect(bc.getService(caSR)).andReturn(ca).anyTimes();
+ EasyMock.replay(bc);
+ return bc;
+ }
+
+ private ServiceReference<?> mockServiceReference(final Dictionary<String, Object> props) {
+ ServiceReference<?> sr = EasyMock.createMock(ServiceReference.class);
+
+ // Make sure the properties are 'live' in that if they change the reference changes too
+ EasyMock.expect(sr.getPropertyKeys()).andAnswer(new IAnswer<String[]>() {
+ @Override
+ public String[] answer() throws Throwable {
+ return Collections.list(props.keys()).toArray(new String [] {});
+ }
+ }).anyTimes();
+ EasyMock.expect(sr.getProperty(EasyMock.isA(String.class))).andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ return props.get(EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.replay(sr);
+ return sr;
+ }
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardingFindHookTest.java
----------------------------------------------------------------------
diff --git a/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardingFindHookTest.java b/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardingFindHookTest.java
new file mode 100644
index 0000000..69df8e8
--- /dev/null
+++ b/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardingFindHookTest.java
@@ -0,0 +1,241 @@
+/*
+ * 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.service.guard.impl;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+public class GuardingFindHookTest {
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testFindHook() throws Exception {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put("service.guard", "(|(moo=foo)(foo=*))");
+
+ BundleContext hookBC = mockConfigAdminBundleContext(config);
+ GuardProxyCatalog gpc = new GuardProxyCatalog(hookBC);
+
+ Filter serviceFilter = FrameworkUtil.createFilter("(foo=*)");
+ GuardingFindHook gfh = new GuardingFindHook(hookBC, gpc, serviceFilter);
+
+ BundleContext clientBC = mockBundleContext(31L);
+
+ Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put(Constants.SERVICE_ID, 16L);
+ props.put("moo", "foo");
+ ServiceReference<?> sref = mockServiceReference(props);
+
+ Collection<ServiceReference<?>> refs = new ArrayList<ServiceReference<?>>();
+ refs.add(sref);
+
+ assertEquals("Precondition", 0, gpc.proxyMap.size());
+ gfh.find(clientBC, null, null, true, refs);
+ assertEquals("The service doesn't match the filter so should have no effect", 0, gpc.proxyMap.size());
+ assertEquals("The service doesn't match the filter so should be presented to the client",
+ Collections.singletonList(sref), refs);
+
+ long service2ID = 17L;
+ Dictionary<String, Object> props2 = new Hashtable<String, Object>();
+ props2.put(Constants.SERVICE_ID, service2ID);
+ props2.put("foo", new Object());
+ ServiceReference<?> sref2 = mockServiceReference(props2);
+
+ Collection<ServiceReference<?>> refs2 = new ArrayList<ServiceReference<?>>();
+ refs2.add(sref2);
+
+ gfh.find(clientBC, null, null, true, refs2);
+ assertEquals("The service should be hidden from the client", 0, refs2.size());
+ assertEquals("The service should have caused a proxy creation", 1, gpc.proxyMap.size());
+ assertEquals("A proxy creation job should have been created", 1, gpc.createProxyQueue.size());
+ assertEquals(sref2.getProperty(Constants.SERVICE_ID), gpc.proxyMap.keySet().iterator().next());
+
+ Collection<ServiceReference<?>> refs3 = new ArrayList<ServiceReference<?>>();
+ refs3.add(sref2);
+
+ // Ensure that the hook bundle has nothing hidden
+ gfh.find(hookBC, null, null, true, refs3);
+ assertEquals("The service should not be hidden from the hook bundle", Collections.singletonList(sref2), refs3);
+ assertEquals("No proxy creation caused in this case", 1, gpc.proxyMap.size());
+ assertEquals("No change expected", sref2.getProperty(Constants.SERVICE_ID), gpc.proxyMap.keySet().iterator().next());
+
+ // Ensure that the system bundle has nothing hidden
+ gfh.find(mockBundleContext(0L), null, null, true, refs3);
+ assertEquals("The service should not be hidden from the framework bundle", Collections.singletonList(sref2), refs3);
+ assertEquals("No proxy creation caused in this case", 1, gpc.proxyMap.size());
+ assertEquals("No change expected", sref2.getProperty(Constants.SERVICE_ID), gpc.proxyMap.keySet().iterator().next());
+
+ // Ensure that if we ask for the same client again, it will not create another proxy
+ gpc.createProxyQueue.clear(); // Manually empty the queue
+ gfh.find(clientBC, null, null, true, refs3);
+ assertEquals("The service should be hidden from the client", 0, refs3.size());
+ assertEquals("There is already a proxy for this client, no need for an additional one", 1, gpc.proxyMap.size());
+ assertEquals("No additional jobs should have been scheduled", 0, gpc.createProxyQueue.size());
+ assertEquals("No change expected", sref2.getProperty(Constants.SERVICE_ID), gpc.proxyMap.keySet().iterator().next());
+
+ Collection<ServiceReference<?>> refs4 = new ArrayList<ServiceReference<?>>();
+ refs4.add(sref2);
+
+ // another client should not get another proxy
+ BundleContext client2BC = mockBundleContext(32768L);
+ gfh.find(client2BC, null, null, true, refs4);
+ assertEquals("The service should be hidden for this new client", 0, refs4.size());
+ assertEquals("No proxy creation job should have been created", 0, gpc.createProxyQueue.size());
+ assertEquals("No proxy creation caused in this case", 1, gpc.proxyMap.size());
+ assertEquals("No change expected", sref2.getProperty(Constants.SERVICE_ID), gpc.proxyMap.keySet().iterator().next());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testFindHookProxyServices() throws Exception {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put("service.guard", "(service.id=*)");
+
+ BundleContext hookBC = mockConfigAdminBundleContext(config);
+ GuardProxyCatalog gpc = new GuardProxyCatalog(hookBC);
+
+ Filter serviceFilter = FrameworkUtil.createFilter("(service.id=*)"); // any service
+ GuardingFindHook gfh = new GuardingFindHook(hookBC, gpc, serviceFilter);
+
+ BundleContext clientBC = mockBundleContext(31L);
+
+ Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put(Constants.SERVICE_ID, 16L);
+ props.put(GuardProxyCatalog.PROXY_SERVICE_KEY, Boolean.TRUE);
+ ServiceReference<?> sref = mockServiceReference(props);
+
+ Collection<ServiceReference<?>> refs = new ArrayList<ServiceReference<?>>();
+ refs.add(sref);
+ gfh.find(clientBC, null, null, false, refs);
+ assertEquals("No proxy should have been created for the proxy find", 0, gpc.proxyMap.size());
+ assertEquals("As the proxy is for this bundle is should be visible and remain on the list",
+ Collections.singletonList(sref), refs);
+ }
+
+ @Test
+ public void testNullFilter() throws Exception {
+ BundleContext hookBC = mockBundleContext(5L);
+ GuardProxyCatalog gpc = new GuardProxyCatalog(hookBC);
+
+ GuardingFindHook gfh = new GuardingFindHook(hookBC, gpc, null);
+ gfh.find(null, null, null, true, null); // should just do nothing
+ }
+
+ private BundleContext mockBundleContext(long id) throws Exception {
+ Bundle bundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(bundle.getBundleId()).andReturn(id).anyTimes();
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(bc.getBundle()).andReturn(bundle).anyTimes();
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ Filter filter = FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ return filter;
+ }
+ }).anyTimes();
+ EasyMock.replay(bc);
+
+ EasyMock.expect(bundle.getBundleContext()).andReturn(bc).anyTimes();
+ EasyMock.replay(bundle);
+
+ return bc;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private BundleContext mockConfigAdminBundleContext(Dictionary<String, Object> ... configs) throws IOException,
+ InvalidSyntaxException {
+ Configuration [] configurations = new Configuration[configs.length];
+
+ for (int i = 0; i < configs.length; i++) {
+ Configuration conf = EasyMock.createMock(Configuration.class);
+ EasyMock.expect(conf.getProperties()).andReturn(configs[i]).anyTimes();
+ EasyMock.expect(conf.getPid()).andReturn((String) configs[i].get(Constants.SERVICE_PID)).anyTimes();
+ EasyMock.replay(conf);
+ configurations[i] = conf;
+ }
+
+ ConfigurationAdmin ca = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(ca.listConfigurations("(&(service.pid=org.apache.karaf.service.acl.*)(service.guard=*))")).andReturn(configurations).anyTimes();
+ EasyMock.replay(ca);
+
+ final ServiceReference caSR = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(caSR);
+
+ Bundle b = EasyMock.createMock(Bundle.class);
+ EasyMock.expect(b.getBundleId()).andReturn(877342449L).anyTimes();
+ EasyMock.replay(b);
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(bc.getBundle()).andReturn(b).anyTimes();
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ String cmFilter = "(&(objectClass=" + ConfigurationAdmin.class.getName() + ")"
+ + "(!(" + GuardProxyCatalog.PROXY_SERVICE_KEY + "=*)))";
+ bc.addServiceListener(EasyMock.isA(ServiceListener.class), EasyMock.eq(cmFilter));
+ EasyMock.expectLastCall().anyTimes();
+ EasyMock.expect(bc.getServiceReferences(EasyMock.anyObject(String.class), EasyMock.eq(cmFilter))).
+ andReturn(new ServiceReference<?> [] {caSR}).anyTimes();
+ EasyMock.expect(bc.getService(caSR)).andReturn(ca).anyTimes();
+ EasyMock.replay(bc);
+ return bc;
+ }
+
+ private ServiceReference<Object> mockServiceReference(final Dictionary<String, Object> props) {
+ @SuppressWarnings("unchecked")
+ ServiceReference<Object> sr = EasyMock.createMock(ServiceReference.class);
+
+ // Make sure the properties are 'live' in that if they change the reference changes too
+ EasyMock.expect(sr.getPropertyKeys()).andAnswer(new IAnswer<String[]>() {
+ @Override
+ public String[] answer() throws Throwable {
+ return Collections.list(props.keys()).toArray(new String [] {});
+ }
+ }).anyTimes();
+ EasyMock.expect(sr.getProperty(EasyMock.isA(String.class))).andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ return props.get(EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.replay(sr);
+ return sr;
+ }
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/test/java/org/apache/karaf/service/guard/tools/ACLConfigurationParserTest.java
----------------------------------------------------------------------
diff --git a/service/guard/src/test/java/org/apache/karaf/service/guard/tools/ACLConfigurationParserTest.java b/service/guard/src/test/java/org/apache/karaf/service/guard/tools/ACLConfigurationParserTest.java
new file mode 100644
index 0000000..cf0c15d
--- /dev/null
+++ b/service/guard/src/test/java/org/apache/karaf/service/guard/tools/ACLConfigurationParserTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.service.guard.tools;
+
+import static org.junit.Assert.assertEquals;
+
+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 org.apache.karaf.service.guard.tools.ACLConfigurationParser.Specificity;
+import org.junit.Test;
+
+public class ACLConfigurationParserTest {
+ @Test
+ public void testParseRoles() {
+ assertEquals(Arrays.asList("some_role"),
+ ACLConfigurationParser.parseRoles(" some_role "));
+ assertEquals(Arrays.asList("a","b","C"),
+ ACLConfigurationParser.parseRoles("a,b,C"));
+ assertEquals(Collections.emptyList(),
+ ACLConfigurationParser.parseRoles("# test comment"));
+ }
+
+ @Test
+ public void testGetRolesForInvocation() {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put("foo", "r1, r2");
+ config.put("bar(java.lang.String, int)[/aa/,/42/]", "ra");
+ config.put("bar(java.lang.String, int)[/bb/,/42/]", "rb");
+ config.put("bar(java.lang.String, int)[\"cc\", \"17\"]", "rc");
+ config.put("bar(java.lang.String, int)", "rd");
+ config.put("bar(java.lang.String)", "re");
+ config.put("bar", "rf");
+ config.put("ba*", "rg #Wildcard");
+
+ List<String> roles1 = new ArrayList<String>();
+ assertEquals(Specificity.NAME_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("foo", new Object [] {}, new String [] {}, config, roles1));
+ assertEquals(Arrays.asList("r1", "r2"), roles1);
+
+ List<String> roles2 = new ArrayList<String>();
+ assertEquals(Specificity.NAME_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("foo", new Object [] {"test"}, new String [] {"java.lang.String"}, config, roles2));
+ assertEquals(Arrays.asList("r1", "r2"), roles2);
+
+ List<String> roles3 = new ArrayList<String>();
+ assertEquals(Specificity.NO_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("test", new Object [] {}, new String [] {}, config, roles3));
+ assertEquals(0, roles3.size());
+
+ List<String> roles4 = new ArrayList<String>();
+ assertEquals(Specificity.ARGUMENT_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("bar", new Object [] {"aa", 42}, new String [] {"java.lang.String", "int"}, config, roles4));
+ assertEquals(Arrays.asList("ra"), roles4);
+
+ List<String> roles5 = new ArrayList<String>();
+ assertEquals(Specificity.ARGUMENT_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("bar", new Object [] {"bb", 42}, new String [] {"java.lang.String", "int"}, config, roles5));
+ assertEquals(Arrays.asList("rb"), roles5);
+
+ List<String> roles6 = new ArrayList<String>();
+ assertEquals(Specificity.ARGUMENT_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("bar", new Object [] {"cc", 17}, new String [] {"java.lang.String", "int"}, config, roles6));
+ assertEquals(Arrays.asList("rc"), roles6);
+
+ List<String> roles7 = new ArrayList<String>();
+ assertEquals(Specificity.SIGNATURE_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("bar", new Object [] {"aaa", 42}, new String [] {"java.lang.String", "int"}, config, roles7));
+ assertEquals(Arrays.asList("rd"), roles7);
+
+ List<String> roles8 = new ArrayList<String>();
+ assertEquals(Specificity.SIGNATURE_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("bar", new Object [] {"aa"}, new String [] {"java.lang.String"}, config, roles8));
+ assertEquals(Arrays.asList("re"), roles8);
+
+ List<String> roles9 = new ArrayList<String>();
+ assertEquals(Specificity.NAME_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("bar", new Object [] {42}, new String [] {"int"}, config, roles9));
+ assertEquals(Arrays.asList("rf"), roles9);
+
+ List<String> roles10 = new ArrayList<String>();
+ assertEquals(Specificity.WILDCARD_MATCH,
+ ACLConfigurationParser.getRolesForInvocation("barr", new Object [] {42}, new String [] {"int"}, config, roles10));
+ assertEquals(Arrays.asList("rg"), roles10);
+ }
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/pom.xml
----------------------------------------------------------------------
diff --git a/service/pom.xml b/service/pom.xml
new file mode 100644
index 0000000..a6fc156
--- /dev/null
+++ b/service/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <!--
+
+ 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.
+ -->
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.karaf</groupId>
+ <artifactId>karaf</artifactId>
+ <version>2.4.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <groupId>org.apache.karaf.service</groupId>
+ <artifactId>service</artifactId>
+ <packaging>pom</packaging>
+ <name>Apache Karaf :: Service</name>
+
+ <modules>
+ <module>guard</module>
+ </modules>
+
+</project>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/pom.xml
----------------------------------------------------------------------
diff --git a/shell/console/pom.xml b/shell/console/pom.xml
index 59b6950..a2ebe1f 100644
--- a/shell/console/pom.xml
+++ b/shell/console/pom.xml
@@ -126,6 +126,7 @@
*
</Import-Package>
<Export-Package>
+ org.apache.felix.gogo.runtime.*;version=${felix.gogo.version};-split-package:=merge-first,
org.apache.felix.gogo.commands.*;version=${felix.gogo.version};-split-package:=merge-first,
org.apache.karaf.shell.console*;version=${project.version},
org.fusesource.jansi;version=${jansi.version};-split-package:=merge-first,
@@ -134,6 +135,7 @@
org.fusesource.hawtjni*;version=1.0;-split-package:=merge-first
</Export-Package>
<Private-Package>
+ org.apache.karaf.shell.security.impl,
org.fusesource.jansi.internal;-split-package:=merge-first,
!org.apache.karaf.util.properties,
org.apache.karaf.util*;-split-package:=merge-first,
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/main/java/org/apache/karaf/shell/console/Main.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/console/Main.java b/shell/console/src/main/java/org/apache/karaf/shell/console/Main.java
index 3f57884..a9befcf 100644
--- a/shell/console/src/main/java/org/apache/karaf/shell/console/Main.java
+++ b/shell/console/src/main/java/org/apache/karaf/shell/console/Main.java
@@ -236,7 +236,7 @@ public class Main {
* @throws Exception
*/
protected Console createConsole(CommandProcessorImpl commandProcessor, ThreadIO threadIO, InputStream in, PrintStream out, PrintStream err, Terminal terminal) throws Exception {
- return new Console(commandProcessor, threadIO, in, out, err, terminal, null, null);
+ return new Console(commandProcessor, threadIO, in, out, err, terminal, null, null, null);
}
/**
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java b/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java
index 94f6d6d..70c46e9 100644
--- a/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java
+++ b/shell/console/src/main/java/org/apache/karaf/shell/console/jline/Console.java
@@ -27,6 +27,7 @@ import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.PrintStream;
import java.io.Reader;
+import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
@@ -47,8 +48,10 @@ import org.apache.felix.service.threadio.ThreadIO;
import org.apache.karaf.shell.console.CloseShellException;
import org.apache.karaf.shell.console.Completer;
import org.apache.karaf.shell.console.completer.CommandsCompleter;
+import org.apache.karaf.shell.security.impl.SecuredCommandProcessorImpl;
import org.apache.karaf.shell.console.util.Branding;
import org.fusesource.jansi.Ansi;
+import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -80,6 +83,7 @@ public class Console implements Runnable
private PrintStream out;
private PrintStream err;
private Thread thread;
+ private final BundleContext bundleContext;
public Console(CommandProcessor processor,
ThreadIO threadIO,
@@ -88,7 +92,8 @@ public class Console implements Runnable
PrintStream err,
Terminal term,
String encoding,
- Runnable closeCallback) throws Exception
+ Runnable closeCallback,
+ BundleContext bc) throws Exception
{
this.threadIO = threadIO;
this.in = in;
@@ -97,9 +102,10 @@ public class Console implements Runnable
this.queue = new ArrayBlockingQueue<Integer>(1024);
this.terminal = term == null ? new UnsupportedTerminal() : term;
this.consoleInput = new ConsoleInputStream();
- this.session = processor.createSession(this.consoleInput, this.out, this.err);
+ this.session = new DelegateSession();
this.session.put("SCOPE", "shell:osgi:*");
this.closeCallback = closeCallback;
+ this.bundleContext = bc;
reader = new ConsoleReader(null,
this.consoleInput,
@@ -147,7 +153,6 @@ public class Console implements Runnable
}
public void close(boolean closedByUser) {
- //System.err.println("Closing");
if (!running) {
return;
}
@@ -172,6 +177,7 @@ public class Console implements Runnable
{
try {
threadIO.setStreams(consoleInput, out, err);
+ SecuredCommandProcessorImpl secCP = createSecuredCommandProcessor();
thread = Thread.currentThread();
CommandSessionHolder.setSession(session);
running = true;
@@ -205,11 +211,28 @@ public class Console implements Runnable
logException(t);
}
}
+ secCP.close();
close(true);
} finally {
threadIO.close();
}
}
+
+ SecuredCommandProcessorImpl createSecuredCommandProcessor() {
+ if (!(session instanceof DelegateSession)) {
+ throw new IllegalStateException("Should be an Delegate Session here, about to set the delegate");
+ }
+ DelegateSession is = (DelegateSession)session;
+
+ // make it active
+ SecuredCommandProcessorImpl secCP = new SecuredCommandProcessorImpl(bundleContext);
+ CommandSession s = secCP.createSession(consoleInput, out, err);
+
+ // before the session is activated attributes may have been set on it. Pass these on to the real
+ // session now
+ is.setDelegate(s);
+ return secCP;
+ }
private void logException(Throwable t) {
try {
@@ -411,6 +434,88 @@ public class Console implements Runnable
thread.interrupt();
}
+ static class DelegateSession implements CommandSession {
+ final Map<String, Object> attrs = new HashMap<String, Object>();
+ volatile CommandSession delegate;
+
+ @Override
+ public Object execute(CharSequence commandline) throws Exception {
+ if (delegate != null)
+ return delegate.execute(commandline);
+
+ throw new UnsupportedOperationException();
+ }
+
+ void setDelegate(CommandSession s) {
+ synchronized (this) {
+ for (Map.Entry<String, Object> entry : attrs.entrySet()) {
+ s.put(entry.getKey(), entry.getValue());
+ }
+ }
+ delegate = s;
+ }
+
+ @Override
+ public void close() {
+ if (delegate != null)
+ delegate.close();
+ }
+
+ @Override
+ public InputStream getKeyboard() {
+ if (delegate != null)
+ return delegate.getKeyboard();
+
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public PrintStream getConsole() {
+ if (delegate != null)
+ return delegate.getConsole();
+
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object get(String name) {
+ if (delegate != null)
+ return delegate.get(name);
+
+ return attrs.get(name);
+ }
+
+ // you can put attributes on this session before it's delegate is set...
+ @Override
+ public void put(String name, Object value) {
+ if (delegate != null) {
+ delegate.put(name, value);
+ return;
+ }
+
+ // there is no delegate yet, so we'll keep the attributes locally
+ synchronized (this) {
+ attrs.put(name, value);
+ }
+ }
+
+ @Override
+ public CharSequence format(Object target, int level) {
+ if (delegate != null)
+ return delegate.format(target, level);
+
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object convert(Class<?> type, Object instance) {
+ if (delegate != null)
+ return delegate.convert(type, instance);
+
+ throw new UnsupportedOperationException();
+ }
+ }
+
private class ConsoleInputStream extends InputStream
{
private int read(boolean wait) throws IOException
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/main/java/org/apache/karaf/shell/console/jline/ConsoleFactory.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/console/jline/ConsoleFactory.java b/shell/console/src/main/java/org/apache/karaf/shell/console/jline/ConsoleFactory.java
index a7af22c..0bc2e3d 100644
--- a/shell/console/src/main/java/org/apache/karaf/shell/console/jline/ConsoleFactory.java
+++ b/shell/console/src/main/java/org/apache/karaf/shell/console/jline/ConsoleFactory.java
@@ -38,6 +38,7 @@ import org.apache.felix.service.command.CommandProcessor;
import org.apache.felix.service.command.CommandSession;
import org.apache.felix.service.command.Function;
import org.apache.felix.service.threadio.ThreadIO;
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
import org.apache.karaf.jaas.boot.principal.UserPrincipal;
import org.apache.sshd.agent.SshAgent;
import org.apache.sshd.agent.local.AgentImpl;
@@ -66,6 +67,8 @@ public class ConsoleFactory {
private boolean start;
private ServiceRegistration registration;
+
+
public void setBundleContext(BundleContext bundleContext) {
this.bundleContext = bundleContext;
}
@@ -97,6 +100,13 @@ public class ConsoleFactory {
Subject subject = new Subject();
final String user = "karaf";
subject.getPrincipals().add(new UserPrincipal(user));
+ String roles = System.getProperty("karaf.local.roles");
+ if (roles != null) {
+ for (String role : roles.split("[,]")) {
+ subject.getPrincipals().add(new RolePrincipal(role.trim()));
+ }
+ }
+
JaasHelper.doAs(subject, new PrivilegedExceptionAction<Object>() {
public Object run() throws Exception {
doStart(user);
@@ -156,7 +166,8 @@ public class ConsoleFactory {
wrap(err),
terminal,
encoding,
- callback);
+ callback,
+ bundleContext);
CommandSession session = console.getSession();
for (Object o : System.getProperties().keySet()) {
String key = o.toString();
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/main/java/org/apache/karaf/shell/security/impl/SecuredCommandConfigTransformer.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/security/impl/SecuredCommandConfigTransformer.java b/shell/console/src/main/java/org/apache/karaf/shell/security/impl/SecuredCommandConfigTransformer.java
new file mode 100644
index 0000000..d61911d
--- /dev/null
+++ b/shell/console/src/main/java/org/apache/karaf/shell/security/impl/SecuredCommandConfigTransformer.java
@@ -0,0 +1,164 @@
+/*
+ * 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.shell.security.impl;
+
+import org.apache.felix.service.command.CommandProcessor;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.ConfigurationEvent;
+import org.osgi.service.cm.ConfigurationListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.*;
+
+public class SecuredCommandConfigTransformer implements ConfigurationListener {
+
+ static final String PROXY_COMMAND_ACL_PID_PREFIX = "org.apache.karaf.command.acl.";
+ static final String PROXY_SERVICE_ACL_PID_PREFIX = "org.apache.karaf.service.acl.command.";
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SecuredCommandConfigTransformer.class);
+ private static final String CONFIGURATION_FILTER =
+ "(" + Constants.SERVICE_PID + "=" + PROXY_COMMAND_ACL_PID_PREFIX + "*)";
+
+ private ConfigurationAdmin configAdmin;
+
+ public void setConfigAdmin(ConfigurationAdmin configAdmin) {
+ this.configAdmin = configAdmin;
+ }
+
+ public void init() throws Exception {
+ Configuration[] configs = configAdmin.listConfigurations(CONFIGURATION_FILTER);
+ if (configs == null)
+ return;
+
+ for (Configuration config : configs) {
+ generateServiceGuardConfig(config);
+ }
+ }
+
+ void generateServiceGuardConfig(Configuration config) throws IOException {
+ if (!config.getPid().startsWith(PROXY_COMMAND_ACL_PID_PREFIX)) {
+ // not a command scope configuration file
+ return;
+ }
+
+ String scopeName = config.getPid().substring(PROXY_COMMAND_ACL_PID_PREFIX.length());
+ if (scopeName.indexOf('.') >= 0) {
+ // scopes don't contains dots, not a command scope
+ return;
+ }
+ scopeName = scopeName.trim();
+
+ Map<String, Dictionary<String, Object>> configMaps = new HashMap<String, Dictionary<String, Object>>();
+ for (Enumeration<String> e = config.getProperties().keys(); e.hasMoreElements(); ) {
+ String key = e.nextElement();
+ String bareCommand = key;
+ String arguments = "";
+ int idx = bareCommand.indexOf('[');
+ if (idx >= 0) {
+ arguments = convertArgs(bareCommand.substring(idx));
+ bareCommand = bareCommand.substring(0, idx);
+ }
+ if (bareCommand.indexOf('.') >= 0) {
+ // not a command
+ continue;
+ }
+ bareCommand = bareCommand.trim();
+
+ String pid = PROXY_SERVICE_ACL_PID_PREFIX + scopeName + "." + bareCommand;
+ Dictionary<String, Object> map;
+ if (!configMaps.containsKey(pid)) {
+ map = new Hashtable<String, Object>();
+ map.put("service.guard", "(&(" +
+ CommandProcessor.COMMAND_SCOPE + "=" + scopeName + ")(" +
+ CommandProcessor.COMMAND_FUNCTION + "=" + bareCommand + "))");
+ configMaps.put(pid, map);
+ } else {
+ map = configMaps.get(pid);
+ }
+
+ // put rules on the map twice, once for commands that 'execute' (implement Function) and
+ // once for commands that are invoked directly
+ Object roleString = config.getProperties().get(key);
+ map.put("execute" + arguments, roleString);
+ map.put(key, roleString);
+ map.put("*", "*"); // any other method may be invoked by anyone
+ }
+
+ LOGGER.info("Generating command ACL config {} into service ACL configs {}",
+ config.getPid(), configMaps.keySet());
+
+ // update config admin with the generated configuration
+ for (Map.Entry<String, Dictionary<String, Object>> entry : configMaps.entrySet()) {
+ Configuration genConfig = configAdmin.getConfiguration(entry.getKey());
+ genConfig.update(entry.getValue());
+ }
+ }
+
+ private String convertArgs(String commandACLArgs) {
+ if (!commandACLArgs.startsWith("[/")) {
+ throw new IllegalStateException("Badly formatted argument match: " + commandACLArgs + " Should start with '[/'");
+ }
+ if (!commandACLArgs.endsWith("/]")) {
+ throw new IllegalStateException("Badly formatted argument match: " + commandACLArgs + " Should end with '/]'");
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.append("[/.*/,"); // add a wildcard argument since the Function execute method has the arguments as second arg
+ sb.append(commandACLArgs.substring(1));
+ return sb.toString();
+ }
+
+ void deleteServiceGuardConfig(String originatingPid, String scope) throws IOException, InvalidSyntaxException {
+ if (scope.contains("."))
+ // This is not a command scope as that should be a single word without any further dots
+ return;
+
+ // Delete all the generated configurations for this scope
+ Configuration[] configs = configAdmin.listConfigurations("(service.pid=" + PROXY_SERVICE_ACL_PID_PREFIX + scope + ".*)");
+ if (configs == null)
+ return;
+
+ LOGGER.info("Config ACL deleted: {}. Deleting generated service ACL configs {}", originatingPid, configs);
+ for (Configuration config : configs) {
+ config.delete();
+ }
+ }
+
+ @Override
+ public void configurationEvent(ConfigurationEvent event) {
+ if (!event.getPid().startsWith(PROXY_COMMAND_ACL_PID_PREFIX))
+ return;
+
+ try {
+ switch (event.getType()) {
+ case ConfigurationEvent.CM_DELETED:
+ deleteServiceGuardConfig(event.getPid(), event.getPid().substring(PROXY_COMMAND_ACL_PID_PREFIX.length()));
+ break;
+ case ConfigurationEvent.CM_UPDATED:
+ generateServiceGuardConfig(configAdmin.getConfiguration(event.getPid()));
+ break;
+ }
+ } catch (Exception e) {
+ LOGGER.error("Problem processing Configuration Event {}", event, e);
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/main/java/org/apache/karaf/shell/security/impl/SecuredCommandProcessorImpl.java
----------------------------------------------------------------------
diff --git a/shell/console/src/main/java/org/apache/karaf/shell/security/impl/SecuredCommandProcessorImpl.java b/shell/console/src/main/java/org/apache/karaf/shell/security/impl/SecuredCommandProcessorImpl.java
new file mode 100644
index 0000000..84224c4
--- /dev/null
+++ b/shell/console/src/main/java/org/apache/karaf/shell/security/impl/SecuredCommandProcessorImpl.java
@@ -0,0 +1,190 @@
+/*
+ * 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.shell.security.impl;
+
+import org.apache.felix.gogo.api.CommandSessionListener;
+import org.apache.felix.gogo.runtime.CommandProcessorImpl;
+import org.apache.felix.gogo.runtime.CommandProxy;
+import org.apache.felix.gogo.runtime.activator.Activator;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.Converter;
+import org.apache.felix.service.command.Function;
+import org.apache.felix.service.threadio.ThreadIO;
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+import javax.security.auth.Subject;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class SecuredCommandProcessorImpl extends CommandProcessorImpl {
+
+ private final BundleContext bundleContext;
+ private final ServiceReference<ThreadIO> threadIOServiceReference;
+ private final ServiceTracker<Object, Object> commandTracker;
+ private final ServiceTracker<Converter, Converter> converterTracker;
+ private final ServiceTracker<CommandSessionListener, CommandSessionListener> listenerTracker;
+
+ public SecuredCommandProcessorImpl(BundleContext bc) {
+ this(bc, bc.getServiceReference(ThreadIO.class));
+ }
+
+ private SecuredCommandProcessorImpl(BundleContext bc, ServiceReference<ThreadIO> sr) {
+ super(bc.getService(sr));
+ bundleContext = bc;
+ threadIOServiceReference = sr;
+
+ AccessControlContext acc = AccessController.getContext();
+ Subject sub = Subject.getSubject(acc);
+ if (sub == null)
+ throw new SecurityException("No current Subject in the Access Control Context");
+
+ Set<RolePrincipal> rolePrincipals = sub.getPrincipals(RolePrincipal.class);
+ if (rolePrincipals.size() == 0)
+ throw new SecurityException("Current user has no associated roles.");
+
+ // TODO cater for custom roles
+ StringBuilder sb = new StringBuilder();
+ sb.append("(|");
+ for (RolePrincipal rp : rolePrincipals) {
+ sb.append('(');
+ sb.append("org.apache.karaf.service.guard.roles");
+ sb.append('=');
+ sb.append(rp.getName());
+ sb.append(')');
+ }
+ sb.append("(!(org.apache.karaf.service.guard.roles=*))"); // Or no roles specified at all
+ sb.append(')');
+ String roleClause = sb.toString();
+
+ addConstant(Activator.CONTEXT, bc);
+ addCommand("osgi", this, "addCommand");
+ addCommand("osgi", this, "removeCommand");
+ addCommand("osgi", this, "eval");
+
+ try {
+ // The role clause is used to only display commands that the current user can invoke.
+ commandTracker = trackCommands(bc, roleClause);
+ commandTracker.open();
+
+ converterTracker = trackConverters(bc);
+ converterTracker.open();
+ listenerTracker = trackListeners(bc);
+ listenerTracker.open();
+ } catch (InvalidSyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void close() {
+ commandTracker.close();
+ converterTracker.close();
+ listenerTracker.close();
+ bundleContext.ungetService(threadIOServiceReference);
+ }
+
+ private ServiceTracker<Object, Object> trackCommands(final BundleContext context, String roleClause) throws InvalidSyntaxException {
+ Filter filter = context.createFilter(String.format("(&(%s=*)(%s=*)%s)",
+ CommandProcessor.COMMAND_SCOPE, CommandProcessor.COMMAND_FUNCTION, roleClause));
+
+ return new ServiceTracker<Object, Object>(context, filter, null) {
+ @Override
+ public Object addingService(ServiceReference<Object> reference) {
+ Object scope = reference.getProperty(CommandProcessor.COMMAND_SCOPE);
+ Object function = reference.getProperty(CommandProcessor.COMMAND_FUNCTION);
+ List<Object> commands = new ArrayList<Object>();
+
+ if (scope != null && function != null) {
+ if (function.getClass().isArray()) {
+ for (Object f : ((Object[]) function)) {
+ Function target = new CommandProxy(context, reference,
+ f.toString());
+ addCommand(scope.toString(), target, f.toString());
+ commands.add(target);
+ }
+ } else {
+ Function target = new CommandProxy(context, reference,
+ function.toString());
+ addCommand(scope.toString(), target, function.toString());
+ commands.add(target);
+ }
+ return commands;
+ }
+ return null;
+ }
+
+ @Override
+ public void removedService(ServiceReference<Object> reference, Object service) {
+ Object scope = reference.getProperty(CommandProcessor.COMMAND_SCOPE);
+ Object function = reference.getProperty(CommandProcessor.COMMAND_FUNCTION);
+
+ if (scope != null && function != null) {
+ if (!function.getClass().isArray()) {
+ removeCommand(scope.toString(), function.toString());
+ } else {
+ for (Object func : (Object[]) function) {
+ removeCommand(scope.toString(), func.toString());
+ }
+ }
+ }
+ super.removedService(reference, service);
+ }
+ };
+ }
+
+ private ServiceTracker<Converter, Converter> trackConverters(BundleContext context) {
+ return new ServiceTracker<Converter, Converter>(context, Converter.class.getName(), null) {
+ @Override
+ public Converter addingService(ServiceReference<Converter> reference) {
+ Converter converter = super.addingService(reference);
+ addConverter(converter);
+ return converter;
+ }
+
+ @Override
+ public void removedService(ServiceReference<Converter> reference, Converter service) {
+ removeConverter(service);
+ super.removedService(reference, service);
+ }
+ };
+ }
+
+ private ServiceTracker<CommandSessionListener, CommandSessionListener> trackListeners(BundleContext context) {
+ return new ServiceTracker<CommandSessionListener, CommandSessionListener>(context, CommandSessionListener.class.getName(), null) {
+ @Override
+ public CommandSessionListener addingService(ServiceReference<CommandSessionListener> reference) {
+ CommandSessionListener listener = super.addingService(reference);
+ addListener(listener);
+ return listener;
+ }
+
+ @Override
+ public void removedService(ServiceReference<CommandSessionListener> reference, CommandSessionListener service) {
+ removeListener(service);
+ super.removedService(reference, service);
+ }
+ };
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
----------------------------------------------------------------------
diff --git a/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml b/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
index 983d3ce..48eae07 100644
--- a/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
+++ b/shell/console/src/main/resources/OSGI-INF/blueprint/karaf-console.xml
@@ -22,7 +22,7 @@
<ext:property-placeholder placeholder-prefix="$[" placeholder-suffix="]">
<ext:default-properties>
- <ext:property name="karaf.startLocalConsole" value="true" />
+ <ext:property name="karaf.startLocalConsole" value="true"/>
</ext:default-properties>
</ext:property-placeholder>
@@ -108,4 +108,14 @@
<service ref="editorFactory" interface="org.jledit.EditorFactory"/>
+ <!-- Get a reference to the Configuration Admin Service -->
+ <reference id="configAdmin" interface="org.osgi.service.cm.ConfigurationAdmin"/>
+
+ <!-- For role-based security on the shell commands -->
+ <bean id="secureCommandConfigTransformer"
+ class="org.apache.karaf.shell.security.impl.SecuredCommandConfigTransformer"
+ init-method="init">
+ <property name="configAdmin" ref="configAdmin"/>
+ </bean>
+ <service ref="secureCommandConfigTransformer" interface="org.osgi.service.cm.ConfigurationListener"/>
</blueprint>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/test/java/org/apache/karaf/shell/console/ExampleSubclassMain.java
----------------------------------------------------------------------
diff --git a/shell/console/src/test/java/org/apache/karaf/shell/console/ExampleSubclassMain.java b/shell/console/src/test/java/org/apache/karaf/shell/console/ExampleSubclassMain.java
index 4af458b..ccc99b5 100644
--- a/shell/console/src/test/java/org/apache/karaf/shell/console/ExampleSubclassMain.java
+++ b/shell/console/src/test/java/org/apache/karaf/shell/console/ExampleSubclassMain.java
@@ -44,7 +44,7 @@ public class ExampleSubclassMain extends Main {
@Override
protected Console createConsole(CommandProcessorImpl commandProcessor, ThreadIO threadIO, InputStream in, PrintStream out, PrintStream err, Terminal terminal) throws Exception {
- return new Console(commandProcessor, threadIO, in, out, err, terminal, null, null) {
+ return new Console(commandProcessor, threadIO, in, out, err, terminal, null, null, null) {
/**
* If you don't overwrite, then karaf will use the welcome message found in the
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/test/java/org/apache/karaf/shell/console/jline/ConsoleTest.java
----------------------------------------------------------------------
diff --git a/shell/console/src/test/java/org/apache/karaf/shell/console/jline/ConsoleTest.java b/shell/console/src/test/java/org/apache/karaf/shell/console/jline/ConsoleTest.java
new file mode 100644
index 0000000..34546c3
--- /dev/null
+++ b/shell/console/src/test/java/org/apache/karaf/shell/console/jline/ConsoleTest.java
@@ -0,0 +1,195 @@
+/*
+ * 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.shell.console.jline;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.InputStream;
+import java.io.PrintStream;
+import java.security.PrivilegedAction;
+
+import javax.security.auth.Subject;
+
+import org.apache.felix.gogo.api.CommandSessionListener;
+import org.apache.felix.gogo.runtime.threadio.ThreadIOImpl;
+import org.apache.felix.service.command.CommandProcessor;
+import org.apache.felix.service.command.CommandSession;
+import org.apache.felix.service.command.Converter;
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
+import org.apache.karaf.shell.console.jline.Console.DelegateSession;
+import org.apache.karaf.shell.security.impl.SecuredCommandProcessorImpl;
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Test;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+
+public class ConsoleTest {
+ @Test
+ public void testConsoleImpl() throws Exception {
+ ServiceReference<?> cmRef = EasyMock.createMock(ServiceReference.class);
+ EasyMock.expect(cmRef.getProperty(CommandProcessor.COMMAND_SCOPE)).andReturn("myscope").anyTimes();
+ EasyMock.expect(cmRef.getProperty(CommandProcessor.COMMAND_FUNCTION)).andReturn("myfunction").anyTimes();
+ EasyMock.replay(cmRef);
+ ServiceReference<?>[] cmRefs = new ServiceReference[] {cmRef};
+
+ BundleContext bc = EasyMock.createMock(BundleContext.class);
+ EasyMock.expect(bc.getServiceReference((Class<?>) EasyMock.anyObject())).andReturn(null).anyTimes();
+ EasyMock.expect(bc.getService((ServiceReference<?>) EasyMock.anyObject())).andReturn(null).anyTimes();
+ bc.addServiceListener(EasyMock.isA(ServiceListener.class), EasyMock.isA(String.class));
+ EasyMock.expectLastCall().anyTimes();
+ EasyMock.expect(bc.getServiceReferences((String) null,
+ "(&(osgi.command.scope=*)(osgi.command.function=*)" +
+ "(|(org.apache.karaf.service.guard.roles=myrole)(!(org.apache.karaf.service.guard.roles=*))))")).andReturn(cmRefs).anyTimes();
+ EasyMock.expect(bc.getServiceReferences(Converter.class.getName(), null)).andReturn(null).anyTimes();
+ EasyMock.expect(bc.getServiceReferences(CommandSessionListener.class.getName(), null)).andReturn(null).anyTimes();
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.replay(bc);
+
+ final Console console = new Console(null, new ThreadIOImpl(), System.in, System.out, System.err, null, "UTF-8", null, bc);
+ assertTrue(console.session instanceof DelegateSession);
+
+ console.session.put("foo", "bar");
+
+ final DelegateSession ds = (DelegateSession) console.session;
+ assertNull("Precondition", ds.delegate);
+
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new RolePrincipal("myrole"));
+
+ Subject.doAs(subject, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ SecuredCommandProcessorImpl secCP = console.createSecuredCommandProcessor();
+ assertNotNull(ds.delegate);
+ assertEquals("Attributes set before the delegate was set should have been transferred",
+ "bar", ds.get("foo"));
+ assertEquals("Attributes set before the delegate was set should have been transferred",
+ "bar", ds.delegate.get("foo"));
+ assertSame(System.out, ds.delegate.getConsole());
+ assertSame(System.out, ds.getConsole());
+
+ assertTrue(secCP.getCommands().contains("myscope:myfunction"));
+
+ return null;
+ }
+ });
+ }
+
+ @Test
+ public void testDelegateSession() throws Exception {
+ DelegateSession ds = new DelegateSession();
+
+ ds.put("a", "b");
+ assertEquals("b", ds.get("a"));
+
+ TestSession ts = new TestSession();
+
+ assertNull("Precondition", ts.lastInvoked);
+
+ ds.setDelegate(ts);
+ assertEquals("put(a,b)", ts.lastInvoked);
+
+ ds.put("c", "d");
+ assertEquals("put(c,d)", ts.lastInvoked);
+
+ ds.execute("hello 1234");
+ assertEquals("execute(hello 1234)", ts.lastInvoked);
+
+ ds.close();
+ assertEquals("close", ts.lastInvoked);
+
+ ds.getKeyboard();
+ assertEquals("getKeyboard", ts.lastInvoked);
+
+ ds.getConsole();
+ assertEquals("getConsole", ts.lastInvoked);
+
+ ds.get("xyz");
+ assertEquals("get(xyz)", ts.lastInvoked);
+
+ ds.format("foo", 12);
+ assertEquals("format(foo,12)", ts.lastInvoked);
+
+ ds.convert(TestSession.class, "a string");
+ assertEquals("convert(TestSession,a string)", ts.lastInvoked);
+ }
+
+ static class TestSession implements CommandSession {
+ String lastInvoked;
+
+ @Override
+ public Object execute(CharSequence commandline) throws Exception {
+ lastInvoked = "execute(" + commandline + ")";
+ return null;
+ }
+
+ @Override
+ public void close() {
+ lastInvoked = "close";
+ }
+
+ @Override
+ public InputStream getKeyboard() {
+ lastInvoked = "getKeyboard";
+ return null;
+ }
+
+ @Override
+ public PrintStream getConsole() {
+ lastInvoked = "getConsole";
+ return null;
+ }
+
+ @Override
+ public Object get(String name) {
+ lastInvoked = "get(" + name + ")";
+ return null;
+ }
+
+ @Override
+ public void put(String name, Object value) {
+ lastInvoked = "put(" + name + "," + value + ")";
+ }
+
+ @Override
+ public CharSequence format(Object target, int level) {
+ lastInvoked = "format(" + target + "," + level + ")";
+ return null;
+ }
+
+ @Override
+ public Object convert(Class<?> type, Object instance) {
+ lastInvoked = "convert(" + type.getSimpleName() + "," + instance + ")";
+ return null;
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/shell/console/src/test/java/org/apache/karaf/shell/security/impl/SecuredCommandConfigTransformerTest.java
----------------------------------------------------------------------
diff --git a/shell/console/src/test/java/org/apache/karaf/shell/security/impl/SecuredCommandConfigTransformerTest.java b/shell/console/src/test/java/org/apache/karaf/shell/security/impl/SecuredCommandConfigTransformerTest.java
new file mode 100644
index 0000000..9fadcd4
--- /dev/null
+++ b/shell/console/src/test/java/org/apache/karaf/shell/security/impl/SecuredCommandConfigTransformerTest.java
@@ -0,0 +1,278 @@
+/*
+ * 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.shell.security.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Test;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.ConfigurationEvent;
+
+public class SecuredCommandConfigTransformerTest {
+ @Test
+ public void testTransformation() throws Exception {
+ Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put("foo", "a,b,c");
+ props.put("bar[/.*[a]+*/]", "d");
+ props.put("bar", "e");
+ props.put("zar[/.*HiThere*/]", "f");
+ props.put("service.pid", SecuredCommandConfigTransformer.PROXY_COMMAND_ACL_PID_PREFIX + "abc");
+ Configuration commandConfig = mockConfiguration(props);
+
+ Dictionary<String, Object> props2 = new Hashtable<String, Object>();
+ props2.put("xxx", "yyy");
+ props2.put("service.pid", SecuredCommandConfigTransformer.PROXY_COMMAND_ACL_PID_PREFIX + "xyz.123");
+ Configuration commandConfig2 = mockConfiguration(props2);
+
+ Dictionary<String, Object> props3 = new Hashtable<String, Object>();
+ props3.put("test", "toast");
+ props3.put("service.pid", "xyz.123");
+ Configuration otherConfig = mockConfiguration(props3);
+
+ final Map<String, Configuration> configurations = new HashMap<String, Configuration>();
+
+ ConfigurationAdmin ca = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(ca.listConfigurations(
+ "(service.pid=" + SecuredCommandConfigTransformer.PROXY_COMMAND_ACL_PID_PREFIX + "*)")).
+ andReturn(new Configuration [] {commandConfig, commandConfig2, otherConfig}).anyTimes();
+ EasyMock.expect(ca.getConfiguration(EasyMock.isA(String.class))).andAnswer(new IAnswer<Configuration>() {
+ @Override
+ public Configuration answer() throws Throwable {
+ String pid = (String) EasyMock.getCurrentArguments()[0];
+ Configuration c = configurations.get(pid);
+ if (c == null) {
+ c = EasyMock.createMock(Configuration.class);
+
+ // Put some expectations in the various mocks
+ Dictionary<String, Object> m = new Hashtable<String, Object>();
+ if ("org.apache.karaf.service.acl.command.abc.foo".equals(pid)) {
+ m.put("service.guard", "(&(osgi.command.scope=abc)(osgi.command.function=foo))");
+ m.put("execute", "a,b,c");
+ m.put("foo", "a,b,c");
+ } else if ("org.apache.karaf.service.acl.command.abc.bar".equals(pid)) {
+ m.put("service.guard", "(&(osgi.command.scope=abc)(osgi.command.function=bar))");
+ m.put("execute[/.*/,/.*[a]+*/]", "d");
+ m.put("execute", "e");
+ m.put("bar[/.*[a]+*/]", "d");
+ m.put("bar", "e");
+ } else if ("org.apache.karaf.service.acl.command.abc.zar".equals(pid)) {
+ m.put("service.guard", "(&(osgi.command.scope=abc)(osgi.command.function=zar))");
+ m.put("execute[/.*/,/.*HiThere*/]", "f");
+ m.put("zar[/.*HiThere*/]", "f");
+ } else {
+ fail("Unexpected PID: " + pid);
+ }
+ m.put("*", "*");
+ c.update(m);
+ EasyMock.expectLastCall().once();
+
+ EasyMock.replay(c);
+ configurations.put(pid, c);
+ }
+ return c;
+ }
+ }).anyTimes();
+ EasyMock.replay(ca);
+
+ SecuredCommandConfigTransformer scct = new SecuredCommandConfigTransformer();
+ scct.setConfigAdmin(ca);
+ scct.init();
+
+ assertEquals(3, configurations.size());
+
+ boolean foundFoo = false;
+ boolean foundBar = false;
+ boolean foundZar = false;
+ for (Map.Entry<String, Configuration> entry : configurations.entrySet()) {
+ Configuration c = entry.getValue();
+ EasyMock.verify(c);
+ if ("org.apache.karaf.service.acl.command.abc.foo".equals(entry.getKey())) {
+ foundFoo = true;
+ } else if ("org.apache.karaf.service.acl.command.abc.bar".equals(entry.getKey())) {
+ foundBar = true;
+ } else if ("org.apache.karaf.service.acl.command.abc.zar".equals(entry.getKey())) {
+ foundZar = true;
+ }
+ }
+
+ assertTrue(foundFoo);
+ assertTrue(foundBar);
+ assertTrue(foundZar);
+
+ }
+
+ Configuration mockConfiguration(Dictionary<String, Object> props) {
+ Configuration commandConfig = EasyMock.createMock(Configuration.class);
+ EasyMock.expect(commandConfig.getPid()).
+ andReturn((String) props.get(Constants.SERVICE_PID)).anyTimes();
+ EasyMock.expect(commandConfig.getProperties()).andReturn(props).anyTimes();
+ EasyMock.replay(commandConfig);
+ return commandConfig;
+ }
+
+ @Test
+ public void testConfigurationEventAdded() throws Exception {
+ String testPid = SecuredCommandConfigTransformer.PROXY_COMMAND_ACL_PID_PREFIX + "test123";
+ Configuration conf = EasyMock.createMock(Configuration.class);
+ EasyMock.expect(conf.getPid()).andReturn(testPid).anyTimes();
+ EasyMock.replay(conf);
+
+ ConfigurationAdmin cm = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(cm.listConfigurations(EasyMock.isA(String.class))).andReturn(null).anyTimes();
+ EasyMock.expect(cm.getConfiguration(testPid)).andReturn(conf).anyTimes();
+ EasyMock.replay(cm);
+
+ final List<String> generateCalled = new ArrayList<String>();
+ SecuredCommandConfigTransformer scct = new SecuredCommandConfigTransformer() {
+ @Override
+ void generateServiceGuardConfig(Configuration config) throws IOException {
+ generateCalled.add(config.getPid());
+ }
+ };
+ scct.setConfigAdmin(cm);
+ scct.init();
+
+ @SuppressWarnings("unchecked")
+ ServiceReference<ConfigurationAdmin> cmRef = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(cmRef);
+
+ ConfigurationEvent event = new ConfigurationEvent(cmRef, ConfigurationEvent.CM_UPDATED, null, testPid);
+
+ assertEquals("Precondition", 0, generateCalled.size());
+ scct.configurationEvent(event);
+ assertEquals(1, generateCalled.size());
+ assertEquals(testPid, generateCalled.iterator().next());
+ }
+
+ @Test
+ public void testConfigurationEventAddedNonCommand() throws Exception {
+ ConfigurationAdmin cm = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(cm.listConfigurations(EasyMock.isA(String.class))).andReturn(null).anyTimes();
+ EasyMock.replay(cm);
+
+ SecuredCommandConfigTransformer scct = new SecuredCommandConfigTransformer();
+ scct.setConfigAdmin(cm);
+ scct.init();
+
+ @SuppressWarnings("unchecked")
+ ServiceReference<ConfigurationAdmin> cmRef = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(cmRef);
+ ConfigurationEvent event = new ConfigurationEvent(cmRef, ConfigurationEvent.CM_UPDATED, null, "test123");
+
+ scct.configurationEvent(event);
+ EasyMock.verify(cm); // Ensure that this doesn't cause any unwanted calls on ConfigAdmin
+ }
+
+ @Test
+ public void testConfigurationEventDeleted() throws Exception {
+ String testPid = SecuredCommandConfigTransformer.PROXY_COMMAND_ACL_PID_PREFIX + "test123";
+
+ ConfigurationAdmin cm = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(cm.listConfigurations(EasyMock.isA(String.class))).andReturn(null).anyTimes();
+ EasyMock.replay(cm);
+
+ SecuredCommandConfigTransformer scct = new SecuredCommandConfigTransformer();
+ scct.setConfigAdmin(cm);
+ scct.init();
+
+ @SuppressWarnings("unchecked")
+ ServiceReference<ConfigurationAdmin> cmRef = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(cmRef);
+ ConfigurationEvent event = new ConfigurationEvent(cmRef, ConfigurationEvent.CM_DELETED, null, testPid);
+
+ Configuration c1 = EasyMock.createMock(Configuration.class);
+ c1.delete();
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(c1);
+ Configuration c2 = EasyMock.createMock(Configuration.class);
+ c2.delete();
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(c2);
+
+ EasyMock.reset(cm);
+ EasyMock.expect(cm.listConfigurations("(service.pid=org.apache.karaf.service.acl.command.test123.*)")).
+ andReturn(new Configuration[] {c1, c2}).once();
+ EasyMock.replay(cm);
+
+ scct.configurationEvent(event);
+
+ EasyMock.verify(cm);
+ EasyMock.verify(c1);
+ EasyMock.verify(c2);
+ }
+
+ @Test
+ public void testConfigurationEventDeletedNonScope() throws Exception {
+ String testPid = SecuredCommandConfigTransformer.PROXY_COMMAND_ACL_PID_PREFIX + "abc.def";
+
+ ConfigurationAdmin cm = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(cm.listConfigurations(EasyMock.isA(String.class))).andReturn(null).anyTimes();
+ EasyMock.replay(cm);
+
+ SecuredCommandConfigTransformer scct = new SecuredCommandConfigTransformer();
+ scct.setConfigAdmin(cm);
+ scct.init();
+
+ @SuppressWarnings("unchecked")
+ ServiceReference<ConfigurationAdmin> cmRef = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(cmRef);
+ ConfigurationEvent event = new ConfigurationEvent(cmRef, ConfigurationEvent.CM_DELETED, null, testPid);
+
+ EasyMock.reset(cm);
+ // Do not expect any further calls to cm...
+ EasyMock.replay(cm);
+
+ scct.configurationEvent(event);
+ EasyMock.verify(cm);
+ }
+
+ @Test
+ public void testConfigurationLocationChangedEventNoEffect() throws Exception {
+ String testPid = SecuredCommandConfigTransformer.PROXY_COMMAND_ACL_PID_PREFIX + "test123";
+
+ ConfigurationAdmin cm = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(cm.listConfigurations(EasyMock.isA(String.class))).andReturn(null).anyTimes();
+ EasyMock.replay(cm);
+
+ SecuredCommandConfigTransformer scct = new SecuredCommandConfigTransformer();
+ scct.setConfigAdmin(cm);
+ scct.init();
+
+ @SuppressWarnings("unchecked")
+ ServiceReference<ConfigurationAdmin> cmRef = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(cmRef);
+ ConfigurationEvent event = new ConfigurationEvent(cmRef, ConfigurationEvent.CM_LOCATION_CHANGED, null, testPid);
+
+ scct.configurationEvent(event);
+ EasyMock.verify(cm); // Ensure that this doesn't cause any unwanted calls on ConfigAdmin
+ }
+}
[3/4] [KARAF-2934]Role-based security for Shell/Console commands -
backport to 2.x branch #1st commit
Posted by ff...@apache.org.
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardProxyCatalogTest.java
----------------------------------------------------------------------
diff --git a/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardProxyCatalogTest.java b/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardProxyCatalogTest.java
new file mode 100644
index 0000000..010bc29
--- /dev/null
+++ b/service/guard/src/test/java/org/apache/karaf/service/guard/impl/GuardProxyCatalogTest.java
@@ -0,0 +1,1652 @@
+/*
+ * 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.service.guard.impl;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.security.PrivilegedAction;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.security.auth.Subject;
+
+import org.apache.aries.proxy.ProxyManager;
+import org.apache.aries.proxy.impl.AsmProxyManager;
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
+import org.apache.karaf.service.guard.impl.GuardProxyCatalog.CreateProxyRunnable;
+import org.apache.karaf.service.guard.impl.GuardProxyCatalog.ServiceRegistrationHolder;
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceFactory;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+
+public class GuardProxyCatalogTest {
+ // Some assertions fail when run under a code coverage tool, they are skipped when this is set to true
+ private static final boolean runningUnderCoverage = false; // set to false before committing any changes
+
+ @Test
+ public void testGuardProxyCatalog() throws Exception {
+ Bundle b = EasyMock.createMock(Bundle.class);
+ EasyMock.expect(b.getBundleId()).andReturn(9823L).anyTimes();
+ EasyMock.replay(b);
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(bc.getBundle()).andReturn(b).anyTimes();
+ bc.addServiceListener(EasyMock.isA(ServiceListener.class));
+ EasyMock.expectLastCall().once();
+ String caFilter = "(&(objectClass=org.osgi.service.cm.ConfigurationAdmin)"
+ + "(!(" + GuardProxyCatalog.PROXY_SERVICE_KEY + "=*)))";
+ EasyMock.expect(bc.createFilter(caFilter)).andReturn(FrameworkUtil.createFilter(caFilter)).anyTimes();
+ String pmFilter = "(&(objectClass=org.apache.aries.proxy.ProxyManager)"
+ + "(!(" + GuardProxyCatalog.PROXY_SERVICE_KEY + "=*)))";
+ EasyMock.expect(bc.createFilter(pmFilter)).andReturn(FrameworkUtil.createFilter(pmFilter)).anyTimes();
+ EasyMock.replay(bc);
+
+ GuardProxyCatalog gpc = new GuardProxyCatalog(bc);
+ assertTrue("Service Tracker for ConfigAdmin should be opened", gpc.configAdminTracker.getTrackingCount() != -1);
+ assertTrue("Service Tracker for ProxyManager should be opened", gpc.proxyManagerTracker.getTrackingCount() != -1);
+
+ EasyMock.verify(bc);
+
+ // Add some more behaviour checks to the bundle context
+ EasyMock.reset(bc);
+ bc.removeServiceListener(EasyMock.isA(ServiceListener.class));
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(bc);
+
+ gpc.close();
+ assertEquals("Service Tracker for ConfigAdmin should be closed", -1, gpc.configAdminTracker.getTrackingCount());
+ assertEquals("Service Tracker for ProxyManager should be closed", -1, gpc.proxyManagerTracker.getTrackingCount());
+
+ EasyMock.verify(bc);
+ }
+
+ @Test
+ public void testIsProxy() throws Exception {
+ BundleContext bc = mockBundleContext();
+
+ GuardProxyCatalog gpc = new GuardProxyCatalog(bc);
+
+ Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put(GuardProxyCatalog.PROXY_SERVICE_KEY, Boolean.TRUE);
+ assertTrue(gpc.isProxy(mockServiceReference(props)));
+ assertFalse(gpc.isProxy(mockServiceReference(new Hashtable<String, Object>())));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testHandleProxificationForHook() throws Exception {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put(Constants.SERVICE_PID, GuardProxyCatalog.SERVICE_ACL_PREFIX + "foo");
+ config.put(GuardProxyCatalog.SERVICE_GUARD_KEY, "(a>=5)");
+ BundleContext bc = mockConfigAdminBundleContext(config);
+ GuardProxyCatalog gpc = new GuardProxyCatalog(bc);
+
+ Dictionary<String, Object> props = new Hashtable<String, Object>();
+ props.put(Constants.SERVICE_ID, 13L);
+ props.put("a", "6");
+ props.put(GuardProxyCatalog.PROXY_SERVICE_KEY, Boolean.TRUE);
+ ServiceReference<?> sref2 = mockServiceReference(props);
+ assertFalse("Should not hide an existing proxy for this client",
+ gpc.handleProxificationForHook(sref2));
+ assertEquals("No proxy should have been created", 0, gpc.proxyMap.size());
+
+ Dictionary<String, Object> props4 = new Hashtable<String, Object>();
+ props4.put(Constants.SERVICE_ID, 15L);
+ props4.put("a", "7");
+ ServiceReference<?> sref4 = mockServiceReference(props4);
+ assertTrue("Should hide a service that needs to be proxied",
+ gpc.handleProxificationForHook(sref4));
+ assertEquals("Should trigger proxy creation", 1, gpc.proxyMap.size());
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Test
+ public void testHandleServiceUnregistering() throws Exception {
+ BundleContext clientBC = openStrictMockBundleContext(mockBundle(12345L));
+ BundleContext client2BC = openStrictMockBundleContext(mockBundle(6L));
+ EasyMock.replay(clientBC);
+ EasyMock.replay(client2BC);
+
+ Hashtable<String, Object> props = new Hashtable<String, Object>();
+ long originalServiceID = 12345678901234L;
+ props.put(Constants.SERVICE_ID, new Long(originalServiceID));
+ props.put("foo", "bar");
+ ServiceReference<?> originalRef = mockServiceReference(props);
+
+ Hashtable<String, Object> props2 = new Hashtable<String, Object>();
+ long anotherServiceID = 5123456789012345L;
+ props2.put(Constants.SERVICE_ID, anotherServiceID);
+ ServiceReference<?> anotherRef = mockServiceReference(props2);
+
+ GuardProxyCatalog gpc = new GuardProxyCatalog(mockBundleContext());
+
+ ServiceRegistration<?> proxyReg = EasyMock.createMock(ServiceRegistration.class);
+ EasyMock.expect(proxyReg.getReference()).andReturn((ServiceReference)
+ mockServiceReference(props)).anyTimes();
+ proxyReg.unregister();
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(proxyReg);
+ ServiceRegistrationHolder srh = new GuardProxyCatalog.ServiceRegistrationHolder();
+ srh.registration = proxyReg;
+
+ ServiceRegistration<?> proxy2Reg = EasyMock.createMock(ServiceRegistration.class);
+ EasyMock.replay(proxy2Reg);
+ ServiceRegistrationHolder srh2 = new GuardProxyCatalog.ServiceRegistrationHolder();
+ srh2.registration = proxy2Reg;
+
+ gpc.proxyMap.put(originalServiceID, srh);
+ gpc.proxyMap.put(anotherServiceID, srh2);
+ assertEquals("Precondition", 2, gpc.proxyMap.size());
+
+ gpc.createProxyQueue.put(new MockCreateProxyRunnable(originalServiceID));
+ gpc.createProxyQueue.put(new MockCreateProxyRunnable(777));
+ assertEquals("Precondition", 2, gpc.createProxyQueue.size());
+
+ gpc.serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, originalRef));
+ assertEquals("Registered events should be ignored", 2, gpc.proxyMap.size());
+ assertEquals("Registered events should be ignored", 2, gpc.createProxyQueue.size());
+
+ Hashtable<String, Object> proxyProps = new Hashtable<String, Object>(props);
+ proxyProps.put(GuardProxyCatalog.PROXY_SERVICE_KEY, Boolean.TRUE);
+ ServiceReference<?> proxyRef = mockServiceReference(proxyProps);
+ gpc.serviceChanged(new ServiceEvent(ServiceEvent.UNREGISTERING, proxyRef));
+ assertEquals("Unregistering the proxy should be ignored by the listener", 2, gpc.proxyMap.size());
+ assertEquals("Unregistering the proxy should be ignored by the listener", 2, gpc.createProxyQueue.size());
+
+ gpc.serviceChanged(new ServiceEvent(ServiceEvent.UNREGISTERING, originalRef));
+ assertEquals("The proxy for this service should have been removed", 1, gpc.proxyMap.size());
+ assertEquals(anotherRef.getProperty(Constants.SERVICE_ID), gpc.proxyMap.keySet().iterator().next());
+ assertEquals("The create proxy job for this service should have been removed", 1, gpc.createProxyQueue.size());
+ assertEquals(777, gpc.createProxyQueue.iterator().next().getOriginalServiceID());
+
+ EasyMock.verify(proxyReg);
+ EasyMock.verify(proxy2Reg);
+ }
+
+ @Test
+ public void testCreateProxy() throws Exception {
+ // This method tests proxy creation for various service implementation types.
+
+ testCreateProxy(TestServiceAPI.class, new TestService());
+ testCreateProxy(TestServiceAPI.class, new DescendantTestService());
+ testCreateProxy(TestServiceAPI.class, new PrivateTestService());
+ testCreateProxy(TestServiceAPI.class, new PrivateTestServiceNoDirectInterfaces());
+ testCreateProxy(TestServiceAPI.class, new FinalTestService());
+ testCreateProxy(TestObjectWithoutInterface.class, new TestObjectWithoutInterface());
+ testCreateProxy(TestServiceAPI.class, new CombinedTestService());
+ testCreateProxy(PrivateTestService.class, new PrivateTestService());
+ testCreateProxy(PrivateTestServiceNoDirectInterfaces.class, new PrivateTestServiceNoDirectInterfaces());
+ testCreateProxy(Object.class, new TestService());
+ testCreateProxy(Object.class, new DescendantTestService());
+ testCreateProxy(Object.class, new PrivateTestService());
+ testCreateProxy(Object.class, new TestObjectWithoutInterface());
+ testCreateProxy(Object.class, new CombinedTestService());
+ testCreateProxy(Object.class, new FinalTestService());
+ testCreateProxy(TestServiceAPI.class, new TestServiceAPI() {
+ @Override
+ public String doit() {
+ return "Doing it";
+ }
+ });
+ testCreateProxy(Object.class, new ClassWithFinalMethod());
+ testCreateProxy(Object.class, new ClassWithPrivateMethod());
+ }
+
+ @Test
+ public void testCreateProxyMultipleObjectClasses() throws Exception {
+ testCreateProxy(new Class [] {TestServiceAPI.class, TestService.class}, new TestService());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testAssignRoles() throws Exception {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put(Constants.SERVICE_PID, "foobar");
+ config.put("service.guard", "(objectClass=" + TestServiceAPI.class.getName() + ")");
+ config.put("somemethod", "a,b");
+ config.put("someOtherMethod(int)", "c");
+ config.put("someOtherMethod(int)[/12/]", "d");
+ config.put("someOtherMethod(int)[\"42\"]", "e");
+ config.put("someOtherMethod[/.*[x][y][z].*/]", "f");
+ config.put("someFoo*", "g");
+
+ BundleContext bc = mockConfigAdminBundleContext(config);
+
+ Dictionary<String, Object> proxyProps = testCreateProxy(bc, TestServiceAPI.class, new TestService());
+ assertEquals(new HashSet<String>(Arrays.asList("a", "b", "c", "d", "e", "f", "g")),
+ new HashSet<String>((Collection<String>) proxyProps.get(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY)));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testAssignRoles2() throws Exception {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put(Constants.SERVICE_PID, "foobar");
+ config.put("service.guard", "(objectClass=" + TestServiceAPI2.class.getName() + ")");
+ config.put("doit", "X");
+
+ BundleContext bc = mockConfigAdminBundleContext(config);
+
+ Dictionary<String, Object> proxyProps = testCreateProxy(bc, TestServiceAPI.class, new TestService());
+ assertNull("No security defined for this API, so no roles should be specified at all",
+ proxyProps.get(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY));
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Test
+ public void testAssignRoles3() throws Exception {
+ abstract class MyAbstractClass implements TestServiceAPI, TestServiceAPI2 {};
+
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put(Constants.SERVICE_PID, "foobar");
+ config.put("service.guard", "(objectClass=" + TestServiceAPI2.class.getName() + ")");
+ config.put("doit", "X");
+
+ BundleContext bc = mockConfigAdminBundleContext(config);
+
+ Map<ServiceReference, Object> serviceMap = new HashMap<ServiceReference, Object>();
+ testCreateProxy(bc, new Class [] {TestServiceAPI.class, TestServiceAPI2.class}, new MyAbstractClass() {
+ @Override
+ public String doit() {
+ return null;
+ }
+
+ @Override
+ public String doit(String s) {
+ return null;
+ }
+ }, serviceMap);
+ assertEquals(1, serviceMap.size());
+ assertEquals(Collections.singleton("X"), serviceMap.keySet().iterator().next().
+ getProperty(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testAssignRoles4() throws Exception {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put(Constants.SERVICE_PID, "foobar");
+ config.put("service.guard", "(objectClass=" + TestServiceAPI.class.getName() + ")");
+ config.put("somemethod", "b");
+ config.put("someOtherMethod", "b");
+ config.put("somethingelse", "*");
+
+ BundleContext bc = mockConfigAdminBundleContext(config);
+
+ Dictionary<String, Object> proxyProps = testCreateProxy(bc, TestServiceAPI.class, new TestService());
+ Collection<String> result = (Collection<String>) proxyProps.get(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY);
+ assertEquals(1, result.size());
+ assertEquals("b", result.iterator().next());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testInvocationBlocking1() throws Exception {
+ Dictionary<String, Object> c1 = new Hashtable<String, Object>();
+ c1.put(Constants.SERVICE_PID, "foobar");
+ c1.put("service.guard", "(objectClass=" + TestServiceAPI.class.getName() + ")");
+ c1.put("doit", "a,b");
+ Dictionary<String, Object> c2 = new Hashtable<String, Object>();
+ c2.put(Constants.SERVICE_PID, "barfoobar");
+ c2.put("service.guard", "(objectClass=" + TestObjectWithoutInterface.class.getName() + ")");
+ c2.put("compute", "c");
+
+ BundleContext bc = mockConfigAdminBundleContext(c1, c2);
+
+ final Object proxy = testCreateProxy(bc, new Class [] {TestServiceAPI.class, TestObjectWithoutInterface.class}, new CombinedTestService());
+
+ // Run with the right credentials so we can test the expected roles
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new RolePrincipal("b"));
+ Subject.doAs(subject, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ assertEquals("Doing it", ((TestServiceAPI) proxy).doit());
+ if (!runningUnderCoverage) {
+ try {
+ ((TestObjectWithoutInterface) proxy).compute(44L);
+ fail("Should have been blocked");
+ } catch (SecurityException se) {
+ // good
+ }
+ }
+
+ return null;
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testInvocationBlocking2() throws Exception {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put(Constants.SERVICE_PID, "barfoobar");
+ config.put("service.guard", "(objectClass=" + TestObjectWithoutInterface.class.getName() + ")");
+ config.put("compute(long)[\"42\"]", "b");
+ config.put("compute(long)", "c");
+
+ BundleContext bc = mockConfigAdminBundleContext(config);
+
+ final Object proxy = testCreateProxy(bc, new Class [] {TestServiceAPI.class, TestObjectWithoutInterface.class}, new CombinedTestService());
+
+ // Run with the right credentials so we can test the expected roles
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new RolePrincipal("b"));
+ Subject.doAs(subject, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ if (!runningUnderCoverage) {
+ assertEquals(-42L, ((TestObjectWithoutInterface) proxy).compute(42L));
+ try {
+ ((TestObjectWithoutInterface) proxy).compute(44L);
+ fail("Should have been blocked");
+ } catch (SecurityException se) {
+ // good
+ }
+ }
+
+ return null;
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testInvocationBlocking3() throws Exception {
+ class MyService implements TestServiceAPI, TestServiceAPI2 {
+ public String doit(String s) {
+ return new StringBuilder(s).reverse().toString();
+ }
+
+ public String doit() {
+ return "Doing it";
+ }
+ };
+
+ Dictionary<String, Object> c1 = new Hashtable<String, Object>();
+ c1.put(Constants.SERVICE_PID, "foobar");
+ c1.put("service.guard", "(objectClass=" + TestServiceAPI.class.getName() + ")");
+ c1.put("do*", "c");
+ Dictionary<String, Object> c2 = new Hashtable<String, Object>();
+ c2.put(Constants.SERVICE_PID, "foobar2");
+ c2.put("service.guard", "(objectClass=" + TestServiceAPI2.class.getName() + ")");
+ c2.put("doit(java.lang.String)[/[tT][a]+/]", "b,d # a regex rule");
+ c2.put("doit(java.lang.String)", "a");
+
+ BundleContext bc = mockConfigAdminBundleContext(c1, c2);
+
+ final Object proxy = testCreateProxy(bc, new Class [] {TestServiceAPI.class, TestServiceAPI2.class}, new MyService());
+
+ // Run with the right credentials so we can test the expected roles
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new RolePrincipal("c"));
+ Subject.doAs(subject, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ assertEquals("Doing it", ((TestServiceAPI) proxy).doit());
+ return null;
+ }
+ });
+
+ Subject subject2 = new Subject();
+ subject2.getPrincipals().add(new RolePrincipal("b"));
+ subject2.getPrincipals().add(new RolePrincipal("f"));
+ Subject.doAs(subject2, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ try {
+ assertEquals("Doing it", ((TestServiceAPI) proxy).doit());
+ fail("Should have been blocked");
+ } catch (SecurityException se) {
+ // good
+ }
+ assertEquals("aaT", ((TestServiceAPI2) proxy).doit("Taa"));
+ try {
+ ((TestServiceAPI2) proxy).doit("t");
+ fail("Should have been blocked");
+ } catch (SecurityException se) {
+ // good
+ }
+ return null;
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testInvocationBlocking4() throws Exception {
+ BundleContext bc = mockConfigAdminBundleContext();
+
+ final Object proxy = testCreateProxy(bc, new Class [] {TestServiceAPI.class, TestObjectWithoutInterface.class}, new CombinedTestService());
+
+ // Run with the right credentials so we can test the expected roles
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new RolePrincipal("b"));
+ Subject.doAs(subject, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ assertEquals("Doing it", ((TestServiceAPI) proxy).doit());
+ if (!runningUnderCoverage) {
+ assertEquals(42L, ((TestObjectWithoutInterface) proxy).compute(-42L));
+ assertEquals(-44L, ((TestObjectWithoutInterface) proxy).compute(44L));
+ }
+
+ return null;
+ }
+ });
+ }
+
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testInvocationBlocking5() throws Exception {
+ Dictionary<String, Object> c1 = new Hashtable<String, Object>();
+ c1.put(Constants.SERVICE_PID, "foobar");
+ c1.put("service.guard", "(objectClass=" + TestServiceAPI.class.getName() + ")");
+ c1.put("doit", "a,b");
+
+ BundleContext bc = mockConfigAdminBundleContext(c1);
+
+ final Object proxy = testCreateProxy(bc, new Class [] {TestServiceAPI2.class}, new TestServiceAPI2() {
+ @Override
+ public String doit(String s) {
+ return s.toUpperCase();
+ }
+ });
+
+ // Invoke the service with role 'c'.
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new RolePrincipal("c"));
+ Subject.doAs(subject, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ assertEquals("The invocation under role 'c' should be ok, as there are no rules specified "
+ + "for this service at all.", "HELLO", ((TestServiceAPI2) proxy).doit("hello"));
+ return null;
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testInvocationBlocking6() throws Exception {
+ Dictionary<String, Object> c1 = new Hashtable<String, Object>();
+ c1.put(Constants.SERVICE_PID, "foobar");
+ c1.put("service.guard", "(objectClass=" + TestServiceAPI.class.getName() + ")");
+ c1.put("doit", "a,b");
+ Dictionary<String, Object> c2 = new Hashtable<String, Object>();
+ c2.put(Constants.SERVICE_PID, "foobar2");
+ c2.put("service.guard", "(objectClass=" + TestServiceAPI2.class.getName() + ")");
+ c2.put("bar", "c");
+
+ BundleContext bc = mockConfigAdminBundleContext(c1, c2);
+
+ final Object proxy = testCreateProxy(bc, new Class [] {TestServiceAPI2.class}, new TestServiceAPI2() {
+ @Override
+ public String doit(String s) {
+ return s.toUpperCase();
+ }
+ });
+
+ // Invoke the service with role 'c'.
+ Subject subject = new Subject();
+ subject.getPrincipals().add(new RolePrincipal("a"));
+ subject.getPrincipals().add(new RolePrincipal("b"));
+ subject.getPrincipals().add(new RolePrincipal("c"));
+ Subject.doAs(subject, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ try {
+ ((TestServiceAPI2) proxy).doit("hello");
+ fail("The invocation should not process as the 'doit' operation has no roles associated with it");
+ } catch (SecurityException se) {
+ // good
+ }
+ return null;
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testInvocationBlocking7() throws Exception {
+ Dictionary<String, Object> c1 = new Hashtable<String, Object>();
+ c1.put(Constants.SERVICE_PID, "foobar");
+ c1.put("service.guard", "(objectClass=" + TestServiceAPI3.class.getName() + ")");
+ c1.put("foo()", "a");
+ c1.put("bar", "b");
+ c1.put("*", "*");
+
+ BundleContext bc = mockConfigAdminBundleContext(c1);
+
+ final Object proxy = testCreateProxy(bc, new Class [] {TestServiceAPI3.class}, new TestService3());
+
+ Subject s1 = new Subject();
+ Subject.doAs(s1, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ TestServiceAPI3 obj = (TestServiceAPI3) proxy;
+ assertEquals("Should have allowed this invocation for any (or no) role", -7, obj.foo(7));
+ try {
+ obj.foo();
+ fail("Should have been blocked");
+ } catch (SecurityException se) {
+ // good
+ }
+ try {
+ obj.bar();
+ fail("Should have been blocked");
+ } catch (SecurityException se) {
+ // good
+ }
+
+ return null;
+ }
+ });
+
+ Subject s2 = new Subject();
+ s2.getPrincipals().add(new RolePrincipal("a"));
+ s2.getPrincipals().add(new RolePrincipal("b"));
+ s2.getPrincipals().add(new RolePrincipal("d"));
+ Subject.doAs(s2, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ TestServiceAPI3 obj = (TestServiceAPI3) proxy;
+ assertEquals(42, obj.foo());
+ assertEquals(99, obj.bar());
+ assertEquals(-32767, obj.foo(32767));
+ return null;
+ }
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testCustomRole() throws Exception {
+ class MyRolePrincipal implements Principal {
+ @Override
+ public String getName() {
+ return "role1";
+ }
+ };
+
+ Dictionary<String, Object> c1 = new Hashtable<String, Object>();
+ c1.put(Constants.SERVICE_PID, "foobar");
+ c1.put("service.guard", "(objectClass=" + TestServiceAPI.class.getName() + ")");
+ c1.put("doit", MyRolePrincipal.class.getName() + ":role1");
+ BundleContext bc = mockConfigAdminBundleContext(c1);
+
+ final Object proxy = testCreateProxy(bc, new Class [] {TestServiceAPI.class}, new TestService());
+
+ Subject s1 = new Subject();
+ s1.getPrincipals().add(new RolePrincipal("role1"));
+ Subject.doAs(s1, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ try {
+ ((TestServiceAPI) proxy).doit();
+ fail("Should have prevented this invocation as the custom role is required");
+ } catch (SecurityException se) {
+ // good
+ }
+ return null;
+ }
+ });
+
+
+ Subject s2 = new Subject();
+ s2.getPrincipals().add(new MyRolePrincipal());
+ Subject.doAs(s2, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ ((TestServiceAPI) proxy).doit(); // Should work, the custom role is there
+ return null;
+ }
+ });
+
+ Subject s3 = new Subject();
+ s3.getPrincipals().add(new MyRolePrincipal());
+ s3.getPrincipals().add(new RolePrincipal("role1"));
+ Subject.doAs(s3, new PrivilegedAction<Object>() {
+ @Override
+ public Object run() {
+ ((TestServiceAPI) proxy).doit(); // Should work, the custom role is there
+ return null;
+ }
+ });
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Test
+ public void testProxyCreationThread() throws Exception {
+ ProxyManager proxyManager = getProxyManager();
+
+ ConfigurationAdmin ca = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(ca.listConfigurations(EasyMock.anyObject(String.class))).andReturn(null).anyTimes();
+ EasyMock.replay(ca);
+
+ ServiceReference pmSref = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(pmSref);
+ ServiceReference pmSref2 = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(pmSref2);
+ ServiceReference cmSref = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(cmSref);
+
+ Bundle b = EasyMock.createMock(Bundle.class);
+ EasyMock.expect(b.getBundleId()).andReturn(23992734L).anyTimes();
+ EasyMock.replay(b);
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(bc.getBundle()).andReturn(b).anyTimes();
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ final ServiceListener [] pmListenerHolder = new ServiceListener [1];
+ String pmFilter = "(&(objectClass=" + ProxyManager.class.getName() + ")" +
+ "(!(" + GuardProxyCatalog.PROXY_SERVICE_KEY + "=*)))";
+ bc.addServiceListener(EasyMock.isA(ServiceListener.class), EasyMock.eq(pmFilter));
+ EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ pmListenerHolder[0] = (ServiceListener) EasyMock.getCurrentArguments()[0];
+ return null;
+ }
+ }).anyTimes();
+ EasyMock.expect(bc.getServiceReferences(EasyMock.anyObject(String.class),
+ EasyMock.contains(ConfigurationAdmin.class.getName()))).andReturn(new ServiceReference[] {cmSref}).anyTimes();
+ EasyMock.expect(bc.getService(pmSref)).andReturn(proxyManager).anyTimes();
+ EasyMock.expect(bc.getService(pmSref2)).andReturn(proxyManager).anyTimes();
+ EasyMock.expect(bc.getService(cmSref)).andReturn(ca).anyTimes();
+ EasyMock.replay(bc);
+
+ // This should put a ServiceListener in the pmListenerHolder, the ServiceTracker does that
+ GuardProxyCatalog gpc = new GuardProxyCatalog(bc);
+
+ // The service being proxied has these properties
+ final Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
+ serviceProps.put(Constants.OBJECTCLASS, new String [] {TestServiceAPI.class.getName()});
+ serviceProps.put(Constants.SERVICE_ID, 162L);
+
+ final Map<ServiceReference<?>, Object> serviceMap = new HashMap<ServiceReference<?>, Object>();
+
+ // The mock bundle context for the bundle providing the service is set up here
+ BundleContext providerBC = EasyMock.createMock(BundleContext.class);
+ // These are the expected service properties of the proxy registration. Note the proxy marker...
+ final Hashtable<String, Object> expectedProxyProps = new Hashtable<String, Object>(serviceProps);
+ expectedProxyProps.put(GuardProxyCatalog.PROXY_SERVICE_KEY, Boolean.TRUE);
+ EasyMock.expect(providerBC.registerService(
+ EasyMock.isA(String[].class),
+ EasyMock.anyObject(),
+ EasyMock.isA(Dictionary.class))).andAnswer(new IAnswer() {
+ @Override
+ public ServiceRegistration answer() throws Throwable {
+ Dictionary<String,Object> props = (Dictionary<String, Object>) EasyMock.getCurrentArguments()[2];
+ ServiceRegistration reg = EasyMock.createMock(ServiceRegistration.class);
+ ServiceReference sr = mockServiceReference(props);
+ EasyMock.expect(reg.getReference()).andReturn(sr).anyTimes();
+ reg.unregister();
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(reg);
+
+ serviceMap.put(sr, EasyMock.getCurrentArguments()[1]);
+
+ return reg;
+ }
+ }).once();
+ EasyMock.expect(providerBC.getService(EasyMock.isA(ServiceReference.class))).andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ return serviceMap.get(EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.replay(providerBC);
+
+ // In some cases the proxy-creating code is looking for a classloader (e.g. when run through
+ // a coverage tool such as EclEmma). This will satisfy that.
+ BundleWiring bw = EasyMock.createMock(BundleWiring.class);
+ EasyMock.expect(bw.getClassLoader()).andReturn(getClass().getClassLoader()).anyTimes();
+ EasyMock.replay(bw);
+
+ // The mock bundle that provides the original service (and also the proxy is registered with this)
+ Bundle providerBundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(providerBundle.getBundleContext()).andReturn(providerBC).anyTimes();
+ EasyMock.expect(providerBundle.adapt(BundleWiring.class)).andReturn(bw).anyTimes();
+ EasyMock.replay(providerBundle);
+
+ ServiceReference sr = mockServiceReference(providerBundle, serviceProps);
+
+ assertEquals("Precondition", 0, gpc.proxyMap.size());
+ assertEquals("Precondition", 0, gpc.createProxyQueue.size());
+ // Create the proxy for the service
+ gpc.proxyIfNotAlreadyProxied(sr);
+ assertEquals(1, gpc.proxyMap.size());
+ assertEquals(1, gpc.createProxyQueue.size());
+
+ // The actual proxy creation is done asynchronously.
+ GuardProxyCatalog.ServiceRegistrationHolder holder = gpc.proxyMap.get(162L);
+ assertNull("The registration shouldn't have happened yet", holder.registration);
+ assertEquals(1, gpc.createProxyQueue.size());
+
+ Thread[] tarray = new Thread[Thread.activeCount()];
+ Thread.enumerate(tarray);
+ for (Thread t : tarray) {
+ if (t != null) {
+ assertTrue(!GuardProxyCatalog.PROXY_CREATOR_THREAD_NAME.equals(t.getName()));
+ }
+ }
+
+ // make the proxy manager appear
+ pmListenerHolder[0].serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, pmSref));
+ Thread.sleep(400); // give the system some time to send the events...
+
+ Thread ourThread = null;
+ Thread[] tarray2 = new Thread[Thread.activeCount()];
+ Thread.enumerate(tarray2);
+ for (Thread t : tarray2) {
+ if (t != null) {
+ if (t.getName().equals(GuardProxyCatalog.PROXY_CREATOR_THREAD_NAME)) {
+ ourThread = t;
+ }
+ }
+ }
+ assertNotNull(ourThread);
+ assertTrue(ourThread.isDaemon());
+ assertTrue(ourThread.isAlive());
+ assertNotNull(holder.registration);
+
+ assertEquals(0, gpc.createProxyQueue.size());
+
+ int numProxyThreads = 0;
+ pmListenerHolder[0].serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED, pmSref2));
+ Thread.sleep(300); // give the system some time to send the events...
+
+ Thread[] tarray3 = new Thread[Thread.activeCount()];
+ Thread.enumerate(tarray3);
+ for (Thread t : tarray3) {
+ if (t != null) {
+ if (t.getName().equals(GuardProxyCatalog.PROXY_CREATOR_THREAD_NAME)) {
+ numProxyThreads++;
+ }
+ }
+ }
+ assertEquals("Maximum 1 proxy thread, even if there is more than 1 proxy service", 1, numProxyThreads);
+
+ // Clean up thread
+ pmListenerHolder[0].serviceChanged(new ServiceEvent(ServiceEvent.UNREGISTERING, pmSref));
+ Thread.sleep(300); // Give the system some time to stop the threads...
+ Thread[] tarray4 = new Thread[Thread.activeCount()];
+ Thread.enumerate(tarray4);
+ for (Thread t : tarray4) {
+ if (t != null) {
+ assertTrue(!GuardProxyCatalog.PROXY_CREATOR_THREAD_NAME.equals(t.getName()));
+ }
+ }
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Test
+ public void testHandleServiceModified() throws Exception {
+ Dictionary<String, Object> config = new Hashtable<String, Object>();
+ config.put(Constants.SERVICE_PID, "test.1.2.3");
+ config.put("service.guard", "(objectClass=" + TestServiceAPI.class.getName() + ")");
+ config.put("doit", "role.1");
+ Dictionary<String, Object> config2 = new Hashtable<String, Object>();
+ config2.put(Constants.SERVICE_PID, "test.1.2.4");
+ config2.put("service.guard", "(objectClass=" + TestServiceAPI2.class.getName() + ")");
+ config2.put("doit", "role.2");
+
+ BundleContext bc = mockConfigAdminBundleContext(config, config2);
+ GuardProxyCatalog gpc = new GuardProxyCatalog(bc);
+ // The service being proxied has these properties
+ long serviceID = 1L;
+ final Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
+ serviceProps.put(Constants.OBJECTCLASS, new String [] {TestServiceAPI.class.getName(), TestServiceAPI2.class.getName()});
+ serviceProps.put(Constants.SERVICE_ID, serviceID);
+ serviceProps.put(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY, Arrays.asList("someone")); // will be overwritten
+ Object myObject = new Object();
+ serviceProps.put("foo.bar", myObject);
+
+ BundleContext providerBC = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(providerBC.registerService(
+ EasyMock.aryEq(new String [] {TestServiceAPI.class.getName(), TestServiceAPI2.class.getName()}),
+ EasyMock.anyObject(),
+ EasyMock.anyObject(Dictionary.class))).andAnswer(new IAnswer() {
+ @Override
+ public Object answer() throws Throwable {
+ final Dictionary props = (Dictionary) EasyMock.getCurrentArguments()[2];
+ assertEquals(Boolean.TRUE, props.get(GuardProxyCatalog.PROXY_SERVICE_KEY));
+
+ ServiceRegistration reg = EasyMock.createMock(ServiceRegistration.class);
+ ServiceReference sr = mockServiceReference(props);
+ EasyMock.expect(reg.getReference()).andReturn(sr).anyTimes();
+ reg.setProperties(EasyMock.isA(Dictionary.class));
+ EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ // Push the update into the service reference
+ ArrayList<String> oldKeys = Collections.list(props.keys());
+ for (String key : oldKeys) {
+ props.remove(key);
+ }
+ Dictionary<String, Object> newProps = (Dictionary<String, Object>) EasyMock.getCurrentArguments()[0];
+ for (String key : Collections.list(newProps.keys())) {
+ props.put(key, newProps.get(key));
+ }
+ return null;
+ }
+ }).once();
+ EasyMock.replay(reg);
+
+ return reg;
+ }
+ }).anyTimes();
+
+ EasyMock.replay(providerBC);
+
+ // In some cases the proxy-creating code is looking for a classloader (e.g. when run through
+ // a coverage tool such as EclEmma). This will satisfy that.
+ BundleWiring bw = EasyMock.createMock(BundleWiring.class);
+ EasyMock.expect(bw.getClassLoader()).andReturn(getClass().getClassLoader()).anyTimes();
+ EasyMock.replay(bw);
+
+ // The mock bundle that provides the original service (and also the proxy is registered with this)
+ Bundle providerBundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(providerBundle.getBundleContext()).andReturn(providerBC).anyTimes();
+ EasyMock.expect(providerBundle.adapt(BundleWiring.class)).andReturn(bw).anyTimes();
+ EasyMock.replay(providerBundle);
+
+ ServiceReference sr = mockServiceReference(providerBundle, serviceProps);
+
+ gpc.proxyIfNotAlreadyProxied(sr);
+ GuardProxyCatalog.CreateProxyRunnable runnable = gpc.createProxyQueue.take();
+ runnable.run(getProxyManager());
+
+ ServiceRegistrationHolder holder = gpc.proxyMap.get(serviceID);
+ ServiceRegistration<?> reg = holder.registration;
+
+ for (String key : serviceProps.keySet()) {
+ if (GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY.equals(key)) {
+ assertEquals(new HashSet(Arrays.asList("role.1", "role.2")), reg.getReference().getProperty(key));
+ } else {
+ assertEquals(serviceProps.get(key), reg.getReference().getProperty(key));
+ }
+ }
+ assertEquals(Boolean.TRUE, reg.getReference().getProperty(GuardProxyCatalog.PROXY_SERVICE_KEY));
+
+ // now change the original service and let the proxy react
+ serviceProps.put("test", "property");
+ assertEquals("Precondition, the mocked reference should have picked up this change",
+ "property", sr.getProperty("test"));
+
+ gpc.serviceChanged(new ServiceEvent(ServiceEvent.MODIFIED, sr));
+ assertEquals("Changing the service should not change the number of proxies", 1, gpc.proxyMap.size());
+
+ for (String key : serviceProps.keySet()) {
+ if (GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY.equals(key)) {
+ assertEquals(new HashSet(Arrays.asList("role.1", "role.2")), reg.getReference().getProperty(key));
+ } else {
+ assertEquals(serviceProps.get(key), reg.getReference().getProperty(key));
+ }
+ }
+ assertEquals("property", reg.getReference().getProperty("test"));
+ assertEquals(Boolean.TRUE, reg.getReference().getProperty(GuardProxyCatalog.PROXY_SERVICE_KEY));
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Test
+ public void testHandleServiceModified2() throws Exception {
+ BundleContext bc = mockConfigAdminBundleContext(); // no configuration used in this test...
+ GuardProxyCatalog gpc = new GuardProxyCatalog(bc);
+
+ // The service being proxied has these properties
+ long serviceID = 1L;
+ final Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
+ serviceProps.put(Constants.OBJECTCLASS, new String [] {TestServiceAPI.class.getName()});
+ serviceProps.put(Constants.SERVICE_ID, serviceID);
+
+ BundleContext providerBC = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(providerBC.registerService(
+ EasyMock.aryEq(new String [] {TestServiceAPI.class.getName()}),
+ EasyMock.anyObject(),
+ EasyMock.anyObject(Dictionary.class))).andAnswer(new IAnswer() {
+ @Override
+ public Object answer() throws Throwable {
+ final Dictionary props = (Dictionary) EasyMock.getCurrentArguments()[2];
+ assertEquals(Boolean.TRUE, props.get(GuardProxyCatalog.PROXY_SERVICE_KEY));
+
+ ServiceRegistration reg = EasyMock.createMock(ServiceRegistration.class);
+ ServiceReference sr = mockServiceReference(props);
+ EasyMock.expect(reg.getReference()).andReturn(sr).anyTimes();
+ reg.setProperties(EasyMock.isA(Dictionary.class));
+ EasyMock.expectLastCall().andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ // Push the update into the service reference
+ ArrayList<String> oldKeys = Collections.list(props.keys());
+ for (String key : oldKeys) {
+ props.remove(key);
+ }
+ Dictionary<String, Object> newProps = (Dictionary<String, Object>) EasyMock.getCurrentArguments()[0];
+ for (String key : Collections.list(newProps.keys())) {
+ props.put(key, newProps.get(key));
+ }
+ return null;
+ }
+ }).once();
+ EasyMock.replay(reg);
+
+ return reg;
+ }
+ }).anyTimes();
+
+ EasyMock.replay(providerBC);
+
+ // In some cases the proxy-creating code is looking for a classloader (e.g. when run through
+ // a coverage tool such as EclEmma). This will satisfy that.
+ BundleWiring bw = EasyMock.createMock(BundleWiring.class);
+ EasyMock.expect(bw.getClassLoader()).andReturn(getClass().getClassLoader()).anyTimes();
+ EasyMock.replay(bw);
+
+ // The mock bundle that provides the original service (and also the proxy is registered with this)
+ Bundle providerBundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(providerBundle.getBundleContext()).andReturn(providerBC).anyTimes();
+ EasyMock.expect(providerBundle.adapt(BundleWiring.class)).andReturn(bw).anyTimes();
+ EasyMock.replay(providerBundle);
+
+ ServiceReference sr = mockServiceReference(providerBundle, serviceProps);
+
+ gpc.proxyIfNotAlreadyProxied(sr);
+ GuardProxyCatalog.CreateProxyRunnable runnable = gpc.createProxyQueue.take();
+ runnable.run(getProxyManager());
+
+ ServiceRegistrationHolder holder = gpc.proxyMap.get(serviceID);
+ ServiceRegistration<?> reg = holder.registration;
+
+ assertFalse("No roles defined for this service using configuration, so roles property should not be set",
+ Arrays.asList(reg.getReference().getPropertyKeys()).contains(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY));
+ for (String key : serviceProps.keySet()) {
+ assertEquals(serviceProps.get(key), reg.getReference().getProperty(key));
+ }
+ assertEquals(Boolean.TRUE, reg.getReference().getProperty(GuardProxyCatalog.PROXY_SERVICE_KEY));
+
+ // now change the original service and let the proxy react
+ serviceProps.put(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY, "foobar");
+ assertEquals("Precondition, the mocked reference should have picked up this change",
+ "foobar", sr.getProperty(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY));
+
+ gpc.serviceChanged(new ServiceEvent(ServiceEvent.MODIFIED, sr));
+ assertEquals("Changing the service should not change the number of proxies", 1, gpc.proxyMap.size());
+
+ assertFalse("The roles property set on the modified service should have been removed",
+ Arrays.asList(reg.getReference().getPropertyKeys()).contains(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY));
+ assertEquals(Boolean.TRUE, reg.getReference().getProperty(GuardProxyCatalog.PROXY_SERVICE_KEY));
+ }
+
+ @Test
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public void testServiceFactoryBehaviour() throws Exception {
+ final Map<ServiceReference, Object> serviceMap = new HashMap<ServiceReference, Object>();
+ TestServiceAPI testService = new TestService();
+
+ BundleContext bc = mockConfigAdminBundleContext();
+ GuardProxyCatalog gpc = new GuardProxyCatalog(bc);
+
+ // The service being proxied has these properties
+ long serviceID = 117L;
+ final Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
+ serviceProps.put(Constants.OBJECTCLASS, new String [] {TestServiceAPI.class.getName()});
+ serviceProps.put(Constants.SERVICE_ID, serviceID);
+ serviceProps.put("bar", 42L);
+
+ BundleContext providerBC = EasyMock.createMock(BundleContext.class);
+ EasyMock.expect(providerBC.registerService(
+ EasyMock.isA(String[].class),
+ EasyMock.anyObject(),
+ EasyMock.isA(Dictionary.class))).andAnswer(new IAnswer() {
+ @Override
+ public ServiceRegistration answer() throws Throwable {
+ Dictionary<String,Object> props = (Dictionary<String, Object>) EasyMock.getCurrentArguments()[2];
+ ServiceRegistration reg = EasyMock.createMock(ServiceRegistration.class);
+ ServiceReference sr = mockServiceReference(props);
+ EasyMock.expect(reg.getReference()).andReturn(sr).anyTimes();
+ EasyMock.replay(reg);
+
+ serviceMap.put(sr, EasyMock.getCurrentArguments()[1]);
+
+ return reg;
+ }
+ }).once();
+ EasyMock.expect(providerBC.getService(EasyMock.isA(ServiceReference.class))).andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ return serviceMap.get(EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.replay(providerBC);
+
+ // In some cases the proxy-creating code is looking for a classloader (e.g. when run through
+ // a coverage tool such as EclEmma). This will satisfy that.
+ BundleWiring bw = EasyMock.createMock(BundleWiring.class);
+ EasyMock.expect(bw.getClassLoader()).andReturn(getClass().getClassLoader()).anyTimes();
+ EasyMock.replay(bw);
+
+ // The mock bundle that provides the original service (and also the proxy is registered with this)
+ Bundle providerBundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(providerBundle.getBundleContext()).andReturn(providerBC).anyTimes();
+ EasyMock.expect(providerBundle.adapt(BundleWiring.class)).andReturn(bw).anyTimes();
+ EasyMock.replay(providerBundle);
+
+ ServiceReference sr = mockServiceReference(providerBundle, serviceProps);
+
+ // The mock bundle context for the client bundle
+ BundleContext clientBC = EasyMock.createMock(BundleContext.class);
+ EasyMock.expect(clientBC.getService(sr)).andReturn(testService).anyTimes();
+ EasyMock.replay(clientBC);
+
+ // The mock bundle that consumes the service
+ Bundle clientBundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(clientBundle.getBundleContext()).andReturn(clientBC).anyTimes();
+ EasyMock.replay(clientBundle);
+
+ gpc.proxyIfNotAlreadyProxied(sr);
+
+ // The actual proxy creation is done asynchronously.
+ GuardProxyCatalog.ServiceRegistrationHolder holder = gpc.proxyMap.get(serviceID);
+ // Mimic the thread that works the queue to create the proxy
+ GuardProxyCatalog.CreateProxyRunnable runnable = gpc.createProxyQueue.take();
+ assertEquals(117L, runnable.getOriginalServiceID());
+
+ ProxyManager pm = getProxyManager();
+ runnable.run(pm);
+
+ // The runnable should have put the actual registration in the holder
+ ServiceReference<?> proxySR = holder.registration.getReference();
+
+ // Check that the proxy registration was done on the original provider bundle's context
+ EasyMock.verify(providerBC);
+
+ // Test that the actual proxy invokes the original service...
+ ServiceFactory proxyServiceSF = (ServiceFactory) serviceMap.get(proxySR);
+ TestServiceAPI proxyService = (TestServiceAPI) proxyServiceSF.getService(clientBundle, null);
+
+ assertNotSame("The proxy should not be the same object as the original service", testService, proxyService);
+ assertEquals("Doing it", proxyService.doit());
+
+ EasyMock.reset(clientBC);
+ EasyMock.expect(clientBC.ungetService(sr)).andReturn(true).once();
+ EasyMock.replay(clientBC);
+
+ proxyServiceSF.ungetService(clientBundle, null, proxyService);
+
+ EasyMock.verify(clientBC);
+ }
+
+ @SuppressWarnings("unchecked")
+ public Dictionary<String, Object> testCreateProxy(Class<?> intf, Object testService) throws Exception {
+ return testCreateProxy(mockConfigAdminBundleContext(), intf, intf, testService);
+ }
+
+ public Dictionary<String, Object> testCreateProxy(BundleContext bc, Class<?> intf, Object testService) throws Exception {
+ return testCreateProxy(bc, intf, intf, testService);
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public Dictionary<String, Object> testCreateProxy(BundleContext bc, Class intf, final Class proxyRegClass, Object testService) throws Exception {
+ // Create the object that is actually being tested here
+ GuardProxyCatalog gpc = new GuardProxyCatalog(bc);
+
+ // The service being proxied has these properties
+ long serviceID = 456L;
+ final Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
+ serviceProps.put(Constants.OBJECTCLASS, new String [] {intf.getName()});
+ serviceProps.put(Constants.SERVICE_ID, serviceID);
+ serviceProps.put(".foo", 123L);
+
+ final Map<ServiceReference<?>, Object> serviceMap = new HashMap<ServiceReference<?>, Object>();
+
+ // The mock bundle context for the bundle providing the service is set up here
+ BundleContext providerBC = EasyMock.createMock(BundleContext.class);
+ // These are the expected service properties of the proxy registration. Note the proxy marker...
+ final Hashtable<String, Object> expectedProxyProps = new Hashtable<String, Object>(serviceProps);
+ expectedProxyProps.put(GuardProxyCatalog.PROXY_SERVICE_KEY, Boolean.TRUE);
+ // This will check that the right proxy is being registered.
+ EasyMock.expect(providerBC.registerService(
+ EasyMock.isA(String[].class),
+ EasyMock.anyObject(),
+ EasyMock.isA(Dictionary.class))).andAnswer(new IAnswer() {
+ @Override
+ public ServiceRegistration answer() throws Throwable {
+ if (!runningUnderCoverage) {
+ // Some of these checks don't work when running under coverage
+ assertArrayEquals(new String [] {proxyRegClass.getName()},
+ (String []) EasyMock.getCurrentArguments()[0]);
+
+ Object svc = EasyMock.getCurrentArguments()[1];
+ assertTrue(svc instanceof ServiceFactory);
+ }
+
+ Dictionary<String,Object> props = (Dictionary<String, Object>) EasyMock.getCurrentArguments()[2];
+ for (String key : expectedProxyProps.keySet()) {
+ assertEquals(expectedProxyProps.get(key), props.get(key));
+ }
+
+ ServiceRegistration reg = EasyMock.createMock(ServiceRegistration.class);
+ ServiceReference sr = mockServiceReference(props);
+ EasyMock.expect(reg.getReference()).andReturn(sr).anyTimes();
+ reg.unregister();
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(reg);
+
+ serviceMap.put(sr, EasyMock.getCurrentArguments()[1]);
+
+ return reg;
+ }
+ }).once();
+ EasyMock.expect(providerBC.getService(EasyMock.isA(ServiceReference.class))).andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ return serviceMap.get(EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.replay(providerBC);
+
+ // In some cases the proxy-creating code is looking for a classloader (e.g. when run through
+ // a coverage tool such as EclEmma). This will satisfy that.
+ BundleWiring bw = EasyMock.createMock(BundleWiring.class);
+ EasyMock.expect(bw.getClassLoader()).andReturn(getClass().getClassLoader()).anyTimes();
+ EasyMock.replay(bw);
+
+ // The mock bundle that provides the original service (and also the proxy is registered with this)
+ Bundle providerBundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(providerBundle.getBundleContext()).andReturn(providerBC).anyTimes();
+ EasyMock.expect(providerBundle.adapt(BundleWiring.class)).andReturn(bw).anyTimes();
+ EasyMock.replay(providerBundle);
+
+ ServiceReference sr = mockServiceReference(providerBundle, serviceProps);
+
+ assertEquals("Precondition", 0, gpc.proxyMap.size());
+ assertEquals("Precondition", 0, gpc.createProxyQueue.size());
+ // Create the proxy for the service
+ gpc.proxyIfNotAlreadyProxied(sr);
+ assertEquals(1, gpc.proxyMap.size());
+
+ // The actual proxy creation is done asynchronously.
+ GuardProxyCatalog.ServiceRegistrationHolder holder = gpc.proxyMap.get(serviceID);
+ assertNull("The registration shouldn't have happened yet", holder.registration);
+ assertEquals(1, gpc.createProxyQueue.size());
+
+ // Mimic the thread that works the queue to create the proxy
+ GuardProxyCatalog.CreateProxyRunnable runnable = gpc.createProxyQueue.take();
+ ProxyManager pm = getProxyManager();
+ runnable.run(pm);
+
+ // The runnable should have put the actual registration in the holder
+ ServiceReference<?> proxySR = holder.registration.getReference();
+ for (String key : expectedProxyProps.keySet()) {
+ assertEquals(expectedProxyProps.get(key), proxySR.getProperty(key));
+ }
+
+ // Check that the proxy registration was done on the original provider bundle's context
+ EasyMock.verify(providerBC);
+
+ // Test that the actual proxy invokes the original service...
+ Object proxyService = serviceMap.get(proxySR);
+ assertNotSame("The proxy should not be the same object as the original service", testService, proxyService);
+
+ // Attempt to proxy the service again, make sure that no re-proxying happens
+ assertEquals("Precondition", 1, gpc.proxyMap.size());
+ assertEquals("Precondition", 0, gpc.createProxyQueue.size());
+ gpc.proxyIfNotAlreadyProxied(sr);
+ assertEquals("No additional proxy should have been created", 1, gpc.proxyMap.size());
+ assertEquals("No additional work on the queue is expected", 0, gpc.createProxyQueue.size());
+
+ Dictionary<String, Object> proxyProps = getServiceReferenceProperties(proxySR);
+
+ gpc.close();
+ EasyMock.verify(holder.registration); // checks that the unregister call was made
+
+ return proxyProps;
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public Object testCreateProxy(Class<?> [] objectClasses, Object testService) throws Exception {
+ return testCreateProxy(mockConfigAdminBundleContext(), objectClasses, objectClasses, testService, new HashMap<ServiceReference, Object>());
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Object testCreateProxy(BundleContext bc, Class<?> [] objectClasses, Object testService) throws Exception {
+ return testCreateProxy(bc, objectClasses, objectClasses, testService, new HashMap<ServiceReference, Object>());
+ }
+
+ @SuppressWarnings("rawtypes")
+ public Object testCreateProxy(BundleContext bc, Class<?> [] objectClasses, Object testService, Map<ServiceReference, Object> serviceMap) throws Exception {
+ return testCreateProxy(bc, objectClasses, objectClasses, testService, serviceMap);
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public Object testCreateProxy(BundleContext bc, Class [] objectClasses, final Class [] proxyRegClasses, Object testService, final Map<ServiceReference, Object> serviceMap) throws Exception {
+ // A linked hash map to keep iteration order over the keys predictable
+ final LinkedHashMap<String, Class> objClsMap = new LinkedHashMap<String, Class>();
+ for (Class cls : objectClasses) {
+ objClsMap.put(cls.getName(), cls);
+ }
+
+ // A linked hash map to keep iteration order over the keys predictable
+ final LinkedHashMap<String, Class> proxyRegClsMap = new LinkedHashMap<String, Class>();
+ for (Class cls : proxyRegClasses) {
+ proxyRegClsMap.put(cls.getName(), cls);
+ }
+
+ // Create the object that is actually being tested here
+ GuardProxyCatalog gpc = new GuardProxyCatalog(bc);
+
+ // The service being proxied has these properties
+ long serviceID = Long.MAX_VALUE;
+ final Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
+ serviceProps.put(Constants.OBJECTCLASS, objClsMap.keySet().toArray(new String [] {}));
+ serviceProps.put(Constants.SERVICE_ID, serviceID);
+ serviceProps.put(GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY, Arrays.asList("everyone")); // will be overwritten
+ serviceProps.put("bar", "foo");
+
+ // The mock bundle context for the bundle providing the service is set up here
+ BundleContext providerBC = EasyMock.createMock(BundleContext.class);
+ // These are the expected service properties of the proxy registration. Note the proxy marker...
+ final Hashtable<String, Object> expectedProxyProps = new Hashtable<String, Object>(serviceProps);
+ expectedProxyProps.put(GuardProxyCatalog.PROXY_SERVICE_KEY, Boolean.TRUE);
+ // This will check that the right proxy is being registered.
+ EasyMock.expect(providerBC.registerService(
+ EasyMock.isA(String[].class),
+ EasyMock.anyObject(),
+ EasyMock.isA(Dictionary.class))).andAnswer(new IAnswer() {
+ @Override
+ public ServiceRegistration answer() throws Throwable {
+ if (!runningUnderCoverage) {
+ // Some of these checks don't work when running under coverage
+ assertArrayEquals(proxyRegClsMap.keySet().toArray(new String [] {}),
+ (String []) EasyMock.getCurrentArguments()[0]);
+
+ Object svc = EasyMock.getCurrentArguments()[1];
+ assertTrue(svc instanceof ServiceFactory);
+ }
+
+ Dictionary<String,Object> props = (Dictionary<String, Object>) EasyMock.getCurrentArguments()[2];
+ for (String key : expectedProxyProps.keySet()) {
+ if (GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY.equals(key)) {
+ assertTrue("The roles property should have been overwritten",
+ !Arrays.asList("everyone").equals(props.get(key)));
+ } else {
+ assertEquals(expectedProxyProps.get(key), props.get(key));
+ }
+ }
+
+ ServiceRegistration reg = EasyMock.createMock(ServiceRegistration.class);
+ ServiceReference sr = mockServiceReference(props);
+ EasyMock.expect(reg.getReference()).andReturn(sr).anyTimes();
+ reg.unregister();
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(reg);
+
+ serviceMap.put(sr, EasyMock.getCurrentArguments()[1]);
+
+ return reg;
+ }
+ }).once();
+ EasyMock.expect(providerBC.getService(EasyMock.isA(ServiceReference.class))).andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ return serviceMap.get(EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.replay(providerBC);
+
+ // In some cases the proxy-creating code is looking for a classloader (e.g. when run through
+ // a coverage tool such as EclEmma). This will satisfy that.
+ BundleWiring bw = EasyMock.createMock(BundleWiring.class);
+ EasyMock.expect(bw.getClassLoader()).andReturn(getClass().getClassLoader()).anyTimes();
+ EasyMock.replay(bw);
+
+ // The mock bundle that provides the original service (and also the proxy is registered with this)
+ Bundle providerBundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(providerBundle.getBundleContext()).andReturn(providerBC).anyTimes();
+ EasyMock.expect(providerBundle.adapt(BundleWiring.class)).andReturn(bw).anyTimes();
+ EasyMock.replay(providerBundle);
+
+ ServiceReference sr = mockServiceReference(providerBundle, serviceProps);
+
+ // The mock bundle context for the client bundle
+ BundleContext clientBC = EasyMock.createMock(BundleContext.class);
+ EasyMock.expect(clientBC.getService(sr)).andReturn(testService).anyTimes();
+ EasyMock.replay(clientBC);
+
+ // The mock bundle that consumes the service
+ Bundle clientBundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(clientBundle.getBundleId()).andReturn(2999L).anyTimes();
+ EasyMock.expect(clientBundle.getBundleContext()).andReturn(clientBC).anyTimes();
+ EasyMock.expect(clientBundle.loadClass(EasyMock.isA(String.class))).andAnswer(new IAnswer() {
+ @Override
+ public Class answer() throws Throwable {
+ return objClsMap.get(EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.replay(clientBundle);
+
+ assertEquals("Precondition", 0, gpc.proxyMap.size());
+ assertEquals("Precondition", 0, gpc.createProxyQueue.size());
+ // Create the proxy for the service
+ gpc.proxyIfNotAlreadyProxied(sr);
+ assertEquals(1, gpc.proxyMap.size());
+
+ // The actual proxy creation is done asynchronously.
+ GuardProxyCatalog.ServiceRegistrationHolder holder = gpc.proxyMap.get(serviceID);
+ assertNull("The registration shouldn't have happened yet", holder.registration);
+ assertEquals(1, gpc.createProxyQueue.size());
+
+ // Mimic the thread that works the queue to create the proxy
+ GuardProxyCatalog.CreateProxyRunnable runnable = gpc.createProxyQueue.take();
+ ProxyManager pm = getProxyManager();
+ runnable.run(pm);
+
+ // The runnable should have put the actual registration in the holder
+ ServiceReference<?> proxySR = holder.registration.getReference();
+ for (String key : expectedProxyProps.keySet()) {
+ if (GuardProxyCatalog.SERVICE_GUARD_ROLES_PROPERTY.equals(key)) {
+ assertTrue("The roles property should have been overwritten",
+ !Arrays.asList("everyone").equals(proxySR.getProperty(key)));
+ } else {
+ assertEquals(expectedProxyProps.get(key), proxySR.getProperty(key));
+ }
+ }
+
+ // Check that the proxy registration was done on the original provider bundle's context
+ EasyMock.verify(providerBC);
+
+ // Test that the actual proxy invokes the original service...
+ ServiceFactory proxyServiceSF = (ServiceFactory) serviceMap.get(proxySR);
+ Object proxyService = proxyServiceSF.getService(clientBundle, null);
+ assertNotSame("The proxy should not be the same object as the original service", testService, proxyService);
+
+ return proxyService;
+ }
+
+ private ProxyManager getProxyManager() {
+ return new AsmProxyManager();
+ }
+
+ private Dictionary<String, Object> getServiceReferenceProperties(ServiceReference<?> sr) {
+ Dictionary<String, Object> dict = new Hashtable<String, Object>();
+
+ for (String key : sr.getPropertyKeys()) {
+ dict.put(key, sr.getProperty(key));
+ }
+
+ return dict;
+ }
+
+ private Bundle mockBundle(long id) {
+ Bundle bundle = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(bundle.getBundleId()).andReturn(id).anyTimes();
+ EasyMock.replay(bundle);
+ return bundle;
+ }
+
+ private BundleContext mockBundleContext() throws InvalidSyntaxException {
+ return mockBundleContext(null);
+ }
+
+ private BundleContext mockBundleContext(Bundle b) throws InvalidSyntaxException {
+ if (b == null) {
+ b = EasyMock.createNiceMock(Bundle.class);
+ EasyMock.expect(b.getBundleId()).andReturn(89334L).anyTimes();
+ EasyMock.replay(b);
+ }
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ EasyMock.expect(bc.getBundle()).andReturn(b).anyTimes();
+ EasyMock.replay(bc);
+ return bc;
+ }
+
+ private BundleContext openStrictMockBundleContext(Bundle b) throws InvalidSyntaxException {
+ BundleContext bc = EasyMock.createMock(BundleContext.class);
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ if (b != null) {
+ EasyMock.expect(bc.getBundle()).andReturn(b).anyTimes();
+ }
+ return bc;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private BundleContext mockConfigAdminBundleContext(Dictionary<String, Object> ... configs) throws IOException,
+ InvalidSyntaxException {
+ Configuration [] configurations = new Configuration[configs.length];
+
+ for (int i = 0; i < configs.length; i++) {
+ Configuration conf = EasyMock.createMock(Configuration.class);
+ EasyMock.expect(conf.getProperties()).andReturn(configs[i]).anyTimes();
+ EasyMock.expect(conf.getPid()).andReturn((String) configs[i].get(Constants.SERVICE_PID)).anyTimes();
+ EasyMock.replay(conf);
+ configurations[i] = conf;
+ }
+ if (configurations.length == 0) {
+ configurations = null;
+ }
+
+ ConfigurationAdmin ca = EasyMock.createMock(ConfigurationAdmin.class);
+ EasyMock.expect(ca.listConfigurations("(&(service.pid=org.apache.karaf.service.acl.*)(service.guard=*))")).andReturn(configurations).anyTimes();
+ EasyMock.replay(ca);
+
+ final ServiceReference caSR = EasyMock.createMock(ServiceReference.class);
+ EasyMock.replay(caSR);
+
+ Bundle b = EasyMock.createMock(Bundle.class);
+ EasyMock.expect(b.getBundleId()).andReturn(877342449L).anyTimes();
+ EasyMock.replay(b);
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(bc.getBundle()).andReturn(b).anyTimes();
+ EasyMock.expect(bc.createFilter(EasyMock.isA(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ String cmFilter = "(&(objectClass=" + ConfigurationAdmin.class.getName() + ")"
+ + "(!(" + GuardProxyCatalog.PROXY_SERVICE_KEY + "=*)))";
+ bc.addServiceListener(EasyMock.isA(ServiceListener.class), EasyMock.eq(cmFilter));
+ EasyMock.expectLastCall().anyTimes();
+ EasyMock.expect(bc.getServiceReferences(EasyMock.anyObject(String.class), EasyMock.eq(cmFilter))).
+ andReturn(new ServiceReference<?> [] {caSR}).anyTimes();
+ EasyMock.expect(bc.getService(caSR)).andReturn(ca).anyTimes();
+ EasyMock.replay(bc);
+ return bc;
+ }
+
+ private ServiceReference<?> mockServiceReference(final Dictionary<String, Object> props) {
+ return mockServiceReference(props, Object.class);
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T> ServiceReference<T> mockServiceReference(Dictionary<String, Object> props, Class<T> cls) {
+ return (ServiceReference<T>) mockServiceReference(null, props);
+ }
+
+ private ServiceReference<?> mockServiceReference(Bundle providerBundle,
+ final Dictionary<String, Object> serviceProps) {
+ ServiceReference<?> sr = EasyMock.createMock(ServiceReference.class);
+
+ // Make sure the properties are 'live' in that if they change the reference changes too
+ EasyMock.expect(sr.getPropertyKeys()).andAnswer(new IAnswer<String[]>() {
+ @Override
+ public String[] answer() throws Throwable {
+ return Collections.list(serviceProps.keys()).toArray(new String [] {});
+ }
+ }).anyTimes();
+ EasyMock.expect(sr.getProperty(EasyMock.isA(String.class))).andAnswer(new IAnswer<Object>() {
+ @Override
+ public Object answer() throws Throwable {
+ return serviceProps.get(EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+ if (providerBundle != null) {
+ EasyMock.expect(sr.getBundle()).andReturn(providerBundle).anyTimes();
+ }
+ EasyMock.replay(sr);
+ return sr;
+ }
+
+ class MockCreateProxyRunnable implements CreateProxyRunnable {
+ private final long orgServiceID;
+
+ public MockCreateProxyRunnable(long serviceID) {
+ orgServiceID = serviceID;
+ }
+
+ @Override
+ public long getOriginalServiceID() {
+ return orgServiceID;
+ }
+
+ @Override
+ public void run(ProxyManager pm) throws Exception {}
+ }
+
+ public interface TestServiceAPI {
+ String doit();
+ }
+
+ public class TestService implements TestServiceAPI {
+ @Override
+ public String doit() {
+ return "Doing it";
+ }
+ }
+
+ public interface TestServiceAPI2 {
+ String doit(String s);
+ }
+
+ public interface TestServiceAPI3 {
+ int foo();
+ int foo(int f);
+ int bar();
+ }
+
+ class TestService3 implements TestServiceAPI3 {
+ public int foo() {
+ return 42;
+ }
+
+ public int foo(int f) {
+ return -f;
+ }
+
+ public int bar() {
+ return 99;
+ }
+ }
+
+ public class TestObjectWithoutInterface {
+ public long compute(long l) {
+ return -l;
+ }
+ }
+
+ public class CombinedTestService extends TestObjectWithoutInterface implements TestServiceAPI {
+ @Override
+ public String doit() {
+ return "Doing it";
+ }
+ }
+
+ private abstract class AbstractService implements TestServiceAPI {
+ @Override
+ public String doit() {
+ return "Doing it";
+ }
+ }
+
+ public class EmptyPublicTestService extends AbstractService {}
+
+ public class DescendantTestService extends EmptyPublicTestService {}
+
+ private class PrivateTestService implements TestServiceAPI {
+ @Override
+ public String doit() {
+ return "Doing it";
+ }
+ }
+
+ private class PrivateTestServiceNoDirectInterfaces extends PrivateTestService {}
+
+ public final class FinalTestService extends AbstractService implements TestServiceAPI {}
+
+ public class ClassWithFinalMethod {
+ public void foo() {}
+ public final String bar() { return "Bar"; }
+ }
+
+ public class ClassWithPrivateMethod {
+ public void foo() {}
+ @SuppressWarnings("unused")
+ private void bar() {}
+ }
+}
[4/4] git commit: [KARAF-2934]Role-based security for Shell/Console
commands - backport to 2.x branch #1st commit
Posted by ff...@apache.org.
[KARAF-2934]Role-based security for Shell/Console commands - backport to 2.x branch #1st commit
Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/4b8cc102
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/4b8cc102
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/4b8cc102
Branch: refs/heads/karaf-2.x
Commit: 4b8cc102226adce53f4d74f2c39843ab7f683dbd
Parents: 8a35113
Author: Freeman Fang <fr...@gmail.com>
Authored: Tue Apr 29 17:22:01 2014 +0800
Committer: Freeman Fang <fr...@gmail.com>
Committed: Tue Apr 29 17:22:01 2014 +0800
----------------------------------------------------------------------
assemblies/apache-karaf/pom.xml | 5 +
.../src/main/descriptors/unix-bin-release.xml | 12 +
.../src/main/descriptors/unix-bin-snapshot.xml | 11 +
.../main/descriptors/windows-bin-release.xml | 11 +
.../main/descriptors/windows-bin-snapshot.xml | 11 +
.../distribution/text/etc/system.properties | 13 +-
.../filtered-resources/etc/startup.properties | 1 +
.../standard/src/main/resources/features.xml | 1 +
.../apache/karaf/itests/KarafTestSupport.java | 44 +
management/server/pom.xml | 5 +
pom.xml | 6 +
service/guard/NOTICE | 71 +
service/guard/pom.xml | 115 ++
.../karaf/service/guard/impl/Activator.java | 67 +
.../service/guard/impl/GuardProxyCatalog.java | 598 +++++++
.../service/guard/impl/GuardingEventHook.java | 63 +
.../service/guard/impl/GuardingFindHook.java | 62 +
.../guard/tools/ACLConfigurationParser.java | 337 ++++
.../src/main/resources/OSGI-INF/bundle.info | 53 +
.../karaf/service/guard/impl/ActivatorTest.java | 128 ++
.../guard/impl/GuardProxyCatalogTest.java | 1652 ++++++++++++++++++
.../guard/impl/GuardingEventHookTest.java | 253 +++
.../guard/impl/GuardingFindHookTest.java | 241 +++
.../guard/tools/ACLConfigurationParserTest.java | 104 ++
service/pom.xml | 40 +
shell/console/pom.xml | 2 +
.../org/apache/karaf/shell/console/Main.java | 2 +-
.../karaf/shell/console/jline/Console.java | 111 +-
.../shell/console/jline/ConsoleFactory.java | 13 +-
.../impl/SecuredCommandConfigTransformer.java | 164 ++
.../impl/SecuredCommandProcessorImpl.java | 190 ++
.../OSGI-INF/blueprint/karaf-console.xml | 12 +-
.../shell/console/ExampleSubclassMain.java | 2 +-
.../karaf/shell/console/jline/ConsoleTest.java | 195 +++
.../SecuredCommandConfigTransformerTest.java | 278 +++
.../impl/SecuredCommandProcessorImplTest.java | 142 ++
.../karaf/shell/ssh/KarafJaasAuthenticator.java | 55 +-
.../karaf/shell/ssh/ShellFactoryImpl.java | 9 +-
.../resources/OSGI-INF/blueprint/shell-ssh.xml | 4 +-
.../karaf/webconsole/gogo/GogoPlugin.java | 3 +-
40 files changed, 5018 insertions(+), 68 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/assemblies/apache-karaf/pom.xml
----------------------------------------------------------------------
diff --git a/assemblies/apache-karaf/pom.xml b/assemblies/apache-karaf/pom.xml
index 966eab8..8053957 100644
--- a/assemblies/apache-karaf/pom.xml
+++ b/assemblies/apache-karaf/pom.xml
@@ -71,6 +71,11 @@
<artifactId>org.apache.karaf.deployer.wrap</artifactId>
</dependency>
<dependency>
+ <groupId>org.apache.karaf.service</groupId>
+ <artifactId>org.apache.karaf.service.guard</artifactId>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.karaf.features</groupId>
<artifactId>org.apache.karaf.features.core</artifactId>
</dependency>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/assemblies/apache-karaf/src/main/descriptors/unix-bin-release.xml
----------------------------------------------------------------------
diff --git a/assemblies/apache-karaf/src/main/descriptors/unix-bin-release.xml b/assemblies/apache-karaf/src/main/descriptors/unix-bin-release.xml
index f3d27e1..1c4657c 100644
--- a/assemblies/apache-karaf/src/main/descriptors/unix-bin-release.xml
+++ b/assemblies/apache-karaf/src/main/descriptors/unix-bin-release.xml
@@ -182,6 +182,18 @@
<unpack>false</unpack>
<useProjectArtifact>false</useProjectArtifact>
<outputFileNameMapping>
+ org/apache/karaf/service/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}
+ </outputFileNameMapping>
+ <includes>
+ <include>org.apache.karaf.service:org.apache.karaf.service.guard</include>
+ </includes>
+ </dependencySet>
+
+ <dependencySet>
+ <outputDirectory>/system</outputDirectory>
+ <unpack>false</unpack>
+ <useProjectArtifact>false</useProjectArtifact>
+ <outputFileNameMapping>
org/apache/karaf/diagnostic/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}
</outputFileNameMapping>
<includes>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/assemblies/apache-karaf/src/main/descriptors/unix-bin-snapshot.xml
----------------------------------------------------------------------
diff --git a/assemblies/apache-karaf/src/main/descriptors/unix-bin-snapshot.xml b/assemblies/apache-karaf/src/main/descriptors/unix-bin-snapshot.xml
index 9b2849f..27b0142 100644
--- a/assemblies/apache-karaf/src/main/descriptors/unix-bin-snapshot.xml
+++ b/assemblies/apache-karaf/src/main/descriptors/unix-bin-snapshot.xml
@@ -182,6 +182,17 @@
<unpack>false</unpack>
<useProjectArtifact>false</useProjectArtifact>
<outputFileNameMapping>
+ org/apache/karaf/service/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}
+ </outputFileNameMapping>
+ <includes>
+ <include>org.apache.karaf.service:org.apache.karaf.service.guard</include>
+ </includes>
+ </dependencySet>
+ <dependencySet>
+ <outputDirectory>/system</outputDirectory>
+ <unpack>false</unpack>
+ <useProjectArtifact>false</useProjectArtifact>
+ <outputFileNameMapping>
org/apache/karaf/diagnostic/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}
</outputFileNameMapping>
<includes>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/assemblies/apache-karaf/src/main/descriptors/windows-bin-release.xml
----------------------------------------------------------------------
diff --git a/assemblies/apache-karaf/src/main/descriptors/windows-bin-release.xml b/assemblies/apache-karaf/src/main/descriptors/windows-bin-release.xml
index 3227b30..ce68a81 100644
--- a/assemblies/apache-karaf/src/main/descriptors/windows-bin-release.xml
+++ b/assemblies/apache-karaf/src/main/descriptors/windows-bin-release.xml
@@ -190,6 +190,17 @@
<unpack>false</unpack>
<useProjectArtifact>false</useProjectArtifact>
<outputFileNameMapping>
+ org/apache/karaf/service/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}
+ </outputFileNameMapping>
+ <includes>
+ <include>org.apache.karaf.service:org.apache.karaf.service.guard</include>
+ </includes>
+ </dependencySet>
+ <dependencySet>
+ <outputDirectory>/system</outputDirectory>
+ <unpack>false</unpack>
+ <useProjectArtifact>false</useProjectArtifact>
+ <outputFileNameMapping>
org/apache/karaf/diagnostic/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}
</outputFileNameMapping>
<includes>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/assemblies/apache-karaf/src/main/descriptors/windows-bin-snapshot.xml
----------------------------------------------------------------------
diff --git a/assemblies/apache-karaf/src/main/descriptors/windows-bin-snapshot.xml b/assemblies/apache-karaf/src/main/descriptors/windows-bin-snapshot.xml
index 984f3d1..9b4c0b8 100644
--- a/assemblies/apache-karaf/src/main/descriptors/windows-bin-snapshot.xml
+++ b/assemblies/apache-karaf/src/main/descriptors/windows-bin-snapshot.xml
@@ -190,6 +190,17 @@
<unpack>false</unpack>
<useProjectArtifact>false</useProjectArtifact>
<outputFileNameMapping>
+ org/apache/karaf/service/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}
+ </outputFileNameMapping>
+ <includes>
+ <include>org.apache.karaf.service:org.apache.karaf.service.guard</include>
+ </includes>
+ </dependencySet>
+ <dependencySet>
+ <outputDirectory>/system</outputDirectory>
+ <unpack>false</unpack>
+ <useProjectArtifact>false</useProjectArtifact>
+ <outputFileNameMapping>
org/apache/karaf/diagnostic/${artifact.artifactId}/${artifact.baseVersion}/${artifact.artifactId}-${artifact.baseVersion}${dashClassifier?}.${artifact.extension}
</outputFileNameMapping>
<includes>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/assemblies/apache-karaf/src/main/distribution/text/etc/system.properties
----------------------------------------------------------------------
diff --git a/assemblies/apache-karaf/src/main/distribution/text/etc/system.properties b/assemblies/apache-karaf/src/main/distribution/text/etc/system.properties
index 066c9ef..99ce56d 100644
--- a/assemblies/apache-karaf/src/main/distribution/text/etc/system.properties
+++ b/assemblies/apache-karaf/src/main/distribution/text/etc/system.properties
@@ -56,7 +56,7 @@ karaf.shell.init.script = ${karaf.etc}/shell.init.script
# karaf.shell.history.maxSize = 0
#
-# Default role name used for console authorization (JMX, SSH and WEB)
+# Roles to use when logging into a local Karaf console.
# The syntax is the following:
# [classname:]principal
# where classname is the class name of the principal object
@@ -64,10 +64,7 @@ karaf.shell.init.script = ${karaf.etc}/shell.init.script
# and principal is the name of the principal of that class
# (defaults to admin).
#
-# Note that this value can be overriden using the various ConfigAdmin
-# configurations for JMX, SSH or the WebConsole.
-#
-karaf.admin.role = admin
+karaf.local.roles=admin,manager,viewer
#
# Set this empty property to avoid errors when validating xml documents.
@@ -106,6 +103,12 @@ org.apache.aries.proxy.weaving.enabled = none
org.apache.aries.proxy.weaving.disabled = org.objectweb.asm.*,org.slf4j.*,org.apache.log4j.*,javax.*,org.apache.xerces.*
#
+# By default, only Karaf shell commands are secured, but additional services can be
+# secured by expanding this filter
+#
+karaf.secured.services=(&(osgi.command.scope=*)(osgi.command.function=*))
+
+#
# Security properties
#
#java.security.policy=${karaf.home}/etc/all.policy
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/assemblies/apache-karaf/src/main/filtered-resources/etc/startup.properties
----------------------------------------------------------------------
diff --git a/assemblies/apache-karaf/src/main/filtered-resources/etc/startup.properties b/assemblies/apache-karaf/src/main/filtered-resources/etc/startup.properties
index 2abc48a..138a26f 100644
--- a/assemblies/apache-karaf/src/main/filtered-resources/etc/startup.properties
+++ b/assemblies/apache-karaf/src/main/filtered-resources/etc/startup.properties
@@ -32,6 +32,7 @@ org/ops4j/pax/url/pax-url-mvn/${pax.url.version}/pax-url-mvn-${pax.url.version}.
org/ops4j/pax/url/pax-url-wrap/${pax.url.version}/pax-url-wrap-${pax.url.version}.jar=5
org/ops4j/pax/logging/pax-logging-api/${pax.logging.version}/pax-logging-api-${pax.logging.version}.jar=8
org/ops4j/pax/logging/pax-logging-service/${pax.logging.version}/pax-logging-service-${pax.logging.version}.jar=8
+org/apache/karaf/service/org.apache.karaf.service.guard/${project.version}/org.apache.karaf.service.guard-${project.version}.jar=10
org/apache/felix/org.apache.felix.configadmin/${felix.configadmin.version}/org.apache.felix.configadmin-${felix.configadmin.version}.jar=10
org/apache/felix/org.apache.felix.fileinstall/${felix.fileinstall.version}/org.apache.felix.fileinstall-${felix.fileinstall.version}.jar=11
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/assemblies/features/standard/src/main/resources/features.xml
----------------------------------------------------------------------
diff --git a/assemblies/features/standard/src/main/resources/features.xml b/assemblies/features/standard/src/main/resources/features.xml
index 593992b..10e55a6 100644
--- a/assemblies/features/standard/src/main/resources/features.xml
+++ b/assemblies/features/standard/src/main/resources/features.xml
@@ -25,6 +25,7 @@
<bundle start-level="5">mvn:org.ops4j.pax.url/pax-url-wrap/${pax.url.version}</bundle>
<bundle start-level="8">mvn:org.ops4j.pax.logging/pax-logging-api/${pax.logging.version}</bundle>
<bundle start-level="8">mvn:org.ops4j.pax.logging/pax-logging-service/${pax.logging.version}</bundle>
+ <bundle start-level="10">mvn:org.apache.karaf.service/org.apache.karaf.service.guard/${project.version}</bundle>
<bundle start-level="10">mvn:org.apache.felix/org.apache.felix.configadmin/${felix.configadmin.version}</bundle>
<bundle start-level="11">mvn:org.apache.felix/org.apache.felix.fileinstall/${felix.fileinstall.version}</bundle>
<bundle start-level="20">mvn:org.ow2.asm/asm-all/${asm.version}</bundle>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/itests/src/test/java/org/apache/karaf/itests/KarafTestSupport.java
----------------------------------------------------------------------
diff --git a/itests/src/test/java/org/apache/karaf/itests/KarafTestSupport.java b/itests/src/test/java/org/apache/karaf/itests/KarafTestSupport.java
index 06a8810..19c7d1c 100644
--- a/itests/src/test/java/org/apache/karaf/itests/KarafTestSupport.java
+++ b/itests/src/test/java/org/apache/karaf/itests/KarafTestSupport.java
@@ -13,6 +13,7 @@
*/
package org.apache.karaf.itests;
+import static org.junit.Assert.assertTrue;
import static org.ops4j.pax.exam.CoreOptions.maven;
import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.karafDistributionConfiguration;
import static org.ops4j.pax.exam.karaf.options.KarafDistributionOption.keepRuntimeFolder;
@@ -107,6 +108,7 @@ public class KarafTestSupport {
* @return
*/
protected String executeCommand(final String command, final Long timeout, final Boolean silent) {
+ waitForCommandService(command);
String response;
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
final PrintStream printStream = new PrintStream(byteArrayOutputStream);
@@ -139,6 +141,48 @@ public class KarafTestSupport {
return response;
}
+ private void waitForCommandService(String command) {
+ // the commands are represented by services. Due to the asynchronous nature of services they may not be
+ // immediately available. This code waits the services to be available, in their secured form. It
+ // means that the code waits for the command service to appear with the roles defined.
+
+ if (command == null || command.length() == 0) {
+ return;
+ }
+
+ int spaceIdx = command.indexOf(' ');
+ if (spaceIdx > 0) {
+ command = command.substring(0, spaceIdx);
+ }
+ int colonIndx = command.indexOf(':');
+
+ try {
+ if (colonIndx > 0) {
+ String scope = command.substring(0, colonIndx);
+ String function = command.substring(colonIndx + 1);
+ waitForService("(&(osgi.command.scope=" + scope + ")(osgi.command.function=" + function + ")(org.apache.karaf.service.guard.roles=*))", SERVICE_TIMEOUT);
+ } else {
+ waitForService("(&(osgi.command.function=" + command + ")(org.apache.karaf.service.guard.roles=*))", SERVICE_TIMEOUT);
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void waitForService(String filter, long timeout) throws InvalidSyntaxException,
+ InterruptedException {
+
+ ServiceTracker st = new ServiceTracker(bundleContext,
+ bundleContext.createFilter(filter),
+ null);
+
+ try {
+ st.open();
+ st.waitForService(timeout);
+ } finally {
+ st.close();
+ }
+ }
protected <T> T getOsgiService(Class<T> type, long timeout) {
return getOsgiService(type, null, timeout);
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/management/server/pom.xml
----------------------------------------------------------------------
diff --git a/management/server/pom.xml b/management/server/pom.xml
index 7003102..b933a1b 100644
--- a/management/server/pom.xml
+++ b/management/server/pom.xml
@@ -64,6 +64,11 @@
<scope>provided</scope>
</dependency>
<dependency>
+ <groupId>org.apache.karaf.service</groupId>
+ <artifactId>org.apache.karaf.service.guard</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
<groupId>org.easymock</groupId>
<artifactId>easymockclassextension</artifactId>
<version>${easymock.version}</version>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 1430e72..14baf41 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,7 @@
<module>demos</module>
<module>tooling</module>
<module>archetypes</module>
+ <module>service</module>
<module>diagnostic</module>
<module>itests</module>
</modules>
@@ -503,6 +504,11 @@
<version>${project.version}</version>
</dependency>
<dependency>
+ <groupId>org.apache.karaf.service</groupId>
+ <artifactId>org.apache.karaf.service.guard</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.apache.karaf.webconsole</groupId>
<artifactId>org.apache.karaf.webconsole.console</artifactId>
<version>${project.version}</version>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/NOTICE
----------------------------------------------------------------------
diff --git a/service/guard/NOTICE b/service/guard/NOTICE
new file mode 100644
index 0000000..b70f1f9
--- /dev/null
+++ b/service/guard/NOTICE
@@ -0,0 +1,71 @@
+Apache Karaf
+Copyright 2010-2014 The Apache Software Foundation
+
+
+I. Included Software
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+Licensed under the Apache License 2.0.
+
+This product uses software developed at
+The OSGi Alliance (http://www.osgi.org/).
+Copyright (c) OSGi Alliance (2000, 2010).
+Licensed under the Apache License 2.0.
+
+This product includes software developed at
+OW2 (http://www.ow2.org/).
+Licensed under the BSD License.
+
+This product includes software developed at
+OPS4J (http://www.ops4j.org/).
+Licensed under the Apache License 2.0.
+
+This product includes software developed at
+Eclipse Foundation (http://www.eclipse.org/).
+Licensed under the EPL.
+
+This product includes software written by
+Antony Lesuisse.
+Licensed under Public Domain.
+
+
+II. Used Software
+
+This product uses software developed at
+FUSE Source (http://www.fusesource.org/).
+Licensed under the Apache License 2.0.
+
+This product uses software developed at
+AOP Alliance (http://aopalliance.sourceforge.net/).
+Licensed under the Public Domain.
+
+This product uses software developed at
+Tanuki Software (http://www.tanukisoftware.com/).
+Licensed under the Apache License 2.0.
+
+This product uses software developed at
+Jasypt (http://jasypt.sourceforge.net/).
+Licensed under the Apache License 2.0.
+
+This product uses software developed at
+JLine (http://jline.sourceforge.net).
+Licensed under the BSD License.
+
+This product uses software developed at
+SLF4J (http://www.slf4j.org/).
+Licensed under the MIT License.
+
+This product uses software developed at
+SpringSource (http://www.springsource.org/).
+Licensed under the Apache License 2.0.
+
+This product includes software from http://www.json.org.
+Copyright (c) 2002 JSON.org
+
+
+III. License Summary
+- Apache License 2.0
+- BSD License
+- EPL License
+- MIT License
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/pom.xml
----------------------------------------------------------------------
diff --git a/service/guard/pom.xml b/service/guard/pom.xml
new file mode 100644
index 0000000..3fdbb9e
--- /dev/null
+++ b/service/guard/pom.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+ <!--
+
+ 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.
+ -->
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.karaf.service</groupId>
+ <artifactId>service</artifactId>
+ <version>2.4.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+
+ <artifactId>org.apache.karaf.service.guard</artifactId>
+ <packaging>bundle</packaging>
+ <name>Apache Karaf :: Service :: Guard</name>
+ <description>Adds role-based access control to OSGi services</description>
+
+ <properties>
+ <appendedResourcesDirectory>${basedir}/../../etc/appended-resources</appendedResourcesDirectory>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.aries.proxy</groupId>
+ <artifactId>org.apache.aries.proxy.api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.karaf.jaas</groupId>
+ <artifactId>org.apache.karaf.jaas.boot</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.osgi</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.aries.proxy</groupId>
+ <artifactId>org.apache.aries.proxy.impl</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.ow2.asm</groupId>
+ <artifactId>asm-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-nop</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <includes>
+ <include>**/*</include>
+ </includes>
+ </resource>
+ <resource>
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ <includes>
+ <include>**/*.info</include>
+ </includes>
+ </resource>
+ </resources>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Bundle-Activator>org.apache.karaf.service.guard.impl.Activator</Bundle-Activator>
+ <Export-Package>org.apache.karaf.service.guard.tools</Export-Package>
+ <Private-Package>org.apache.karaf.service.guard.impl</Private-Package>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/main/java/org/apache/karaf/service/guard/impl/Activator.java
----------------------------------------------------------------------
diff --git a/service/guard/src/main/java/org/apache/karaf/service/guard/impl/Activator.java b/service/guard/src/main/java/org/apache/karaf/service/guard/impl/Activator.java
new file mode 100644
index 0000000..ea067db
--- /dev/null
+++ b/service/guard/src/main/java/org/apache/karaf/service/guard/impl/Activator.java
@@ -0,0 +1,67 @@
+/*
+ * 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.service.guard.impl;
+
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+// This bundle is quite low-level and benefits from starting early in the process. Therefore it does not depend
+// on Blueprint but rather uses direct OSGi framework APIs and Service Trackers ...
+public class Activator implements BundleActivator {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ GuardingEventHook guardingEventHook;
+ GuardingFindHook guardingFindHook;
+ GuardProxyCatalog guardProxyCatalog;
+
+ @Override
+ public void start(BundleContext bundleContext) throws Exception {
+ String f = System.getProperty(GuardProxyCatalog.KARAF_SECURED_SERVICES_SYSPROP);
+ Filter securedServicesFilter;
+ if (f == null) {
+ // no services need to be secured
+ logger.info("No role-based security for services as its system property is not set: {}", GuardProxyCatalog.KARAF_SECURED_SERVICES_SYSPROP);
+ return;
+ } else {
+ securedServicesFilter = bundleContext.createFilter(f);
+ logger.info("Adding role-based security to services with filter: {}", f);
+ }
+
+ guardProxyCatalog = new GuardProxyCatalog(bundleContext);
+
+ guardingEventHook = new GuardingEventHook(bundleContext, guardProxyCatalog, securedServicesFilter);
+ bundleContext.registerService(EventListenerHook.class, guardingEventHook, null);
+
+ guardingFindHook = new GuardingFindHook(bundleContext, guardProxyCatalog, securedServicesFilter);
+ bundleContext.registerService(FindHook.class, guardingFindHook, null);
+
+ }
+
+ @Override
+ public void stop(BundleContext bundleContext) throws Exception {
+ if (guardProxyCatalog != null) {
+ guardProxyCatalog.close();
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardProxyCatalog.java
----------------------------------------------------------------------
diff --git a/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardProxyCatalog.java b/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardProxyCatalog.java
new file mode 100644
index 0000000..92e0ec6
--- /dev/null
+++ b/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardProxyCatalog.java
@@ -0,0 +1,598 @@
+/*
+ * 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.service.guard.impl;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.regex.Pattern;
+
+import javax.security.auth.Subject;
+
+import org.apache.aries.proxy.InvocationListener;
+import org.apache.aries.proxy.ProxyManager;
+import org.apache.aries.proxy.UnableToProxyException;
+import org.apache.karaf.jaas.boot.principal.RolePrincipal;
+import org.apache.karaf.service.guard.tools.ACLConfigurationParser;
+import org.apache.karaf.service.guard.tools.ACLConfigurationParser.Specificity;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceFactory;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+import org.osgi.util.tracker.ServiceTrackerCustomizer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class GuardProxyCatalog implements ServiceListener {
+ public static final String KARAF_SECURED_SERVICES_SYSPROP = "karaf.secured.services";
+ public static final String SERVICE_GUARD_ROLES_PROPERTY = "org.apache.karaf.service.guard.roles";
+
+ static final String PROXY_CREATOR_THREAD_NAME = "Secure OSGi Service Proxy Creator";
+ static final String PROXY_SERVICE_KEY = "." + GuardProxyCatalog.class.getName(); // The only currently used value is Boolean.TRUE
+ static final String SERVICE_ACL_PREFIX = "org.apache.karaf.service.acl.";
+ static final String SERVICE_GUARD_KEY = "service.guard";
+ static final Logger LOG = LoggerFactory.getLogger(GuardProxyCatalog.class);
+
+ private static final Pattern JAVA_METHOD_NAME_PATTERN = Pattern.compile("[a-zA-Z_$][a-zA-Z0-9_$]*");
+ private static final String ROLE_WILDCARD = "*";
+
+ private final BundleContext myBundleContext;
+
+ final ServiceTracker<ConfigurationAdmin, ConfigurationAdmin> configAdminTracker;
+ final ServiceTracker<ProxyManager, ProxyManager> proxyManagerTracker;
+ final ConcurrentMap<Long, ServiceRegistrationHolder> proxyMap = new ConcurrentHashMap<Long, ServiceRegistrationHolder>();
+ final BlockingQueue<CreateProxyRunnable> createProxyQueue = new LinkedBlockingQueue<CreateProxyRunnable>();
+
+ // These two variables control the proxy creator thread, which is started as soon as a ProxyManager Service
+ // becomes available.
+ volatile boolean runProxyCreator = true;
+ volatile Thread proxyCreatorThread = null;
+
+ GuardProxyCatalog(BundleContext bc) throws Exception {
+ LOG.trace("Starting GuardProxyCatalog");
+ myBundleContext = bc;
+
+ // The service listener is used to update/unregister proxies if the backing service changes/goes away
+ bc.addServiceListener(this);
+
+ Filter caFilter = getNonProxyFilter(bc, ConfigurationAdmin.class);
+ LOG.trace("Creating Config Admin Tracker using filter {}", caFilter);
+ configAdminTracker = new ServiceTracker<ConfigurationAdmin, ConfigurationAdmin>(bc, caFilter, null);
+ configAdminTracker.open();
+
+ Filter pmFilter = getNonProxyFilter(bc, ProxyManager.class);
+ LOG.trace("Creating Proxy Manager Tracker using filter {}", pmFilter);
+ proxyManagerTracker = new ServiceTracker<ProxyManager, ProxyManager>(bc, pmFilter, new ServiceProxyCreatorCustomizer());
+ proxyManagerTracker.open();
+ }
+
+ static Filter getNonProxyFilter(BundleContext bc, Class<?> clazz) throws InvalidSyntaxException {
+ Filter caFilter = bc.createFilter(
+ "(&(" + Constants.OBJECTCLASS + "=" + clazz.getName() +
+ ")(!(" + PROXY_SERVICE_KEY + "=*)))");
+ return caFilter;
+ }
+
+ void close() {
+ LOG.trace("Stopping GuardProxyCatalog");
+ stopProxyCreator();
+ proxyManagerTracker.close();
+ configAdminTracker.close();
+
+ myBundleContext.removeServiceListener(this);
+
+ // Remove all proxy registrations
+ for (ServiceRegistrationHolder holder : proxyMap.values()) {
+ ServiceRegistration<?> reg = holder.registration;
+ if (reg != null) {
+ LOG.info("Unregistering proxy service of {} with properties {}",
+ reg.getReference().getProperty(Constants.OBJECTCLASS), copyProperties(reg.getReference()));
+ reg.unregister();
+ }
+ }
+ proxyMap.clear();
+ }
+
+ @Override
+ public void serviceChanged(ServiceEvent event) {
+ // This method is to ensure that proxied services follow the original service. I.e. if the original service
+ // goes away the proxies should go away too. If the original service is modified, the proxies should be
+ // modified accordingly
+
+ ServiceReference<?> sr = event.getServiceReference();
+ if (event.getType() == ServiceEvent.REGISTERED) {
+ // Nothing to do for new services
+ return;
+ }
+
+ if (isProxy(sr)) {
+ // Ignore proxies, we only react to real service changes
+ return;
+ }
+
+ Long orgServiceID = (Long) sr.getProperty(Constants.SERVICE_ID);
+ if (event.getType() == ServiceEvent.UNREGISTERING) {
+ handleOriginalServiceUnregistering(orgServiceID);
+ }
+
+ if ((event.getType() & (ServiceEvent.MODIFIED | ServiceEvent.MODIFIED_ENDMATCH)) > 0) {
+ handleOriginalServiceModifed(orgServiceID, sr);
+ }
+ }
+
+ private void handleOriginalServiceUnregistering(Long orgServiceID) {
+ // If the service queued up to be proxied, remove it.
+ for (Iterator<CreateProxyRunnable> i = createProxyQueue.iterator(); i.hasNext(); ) {
+ CreateProxyRunnable cpr = i.next();
+ if (orgServiceID.equals(cpr.getOriginalServiceID())) {
+ i.remove();
+ }
+ }
+
+ ServiceRegistrationHolder holder = proxyMap.remove(orgServiceID);
+ if (holder != null) {
+ if (holder.registration != null) {
+ holder.registration.unregister();
+ }
+ }
+ }
+
+ private void handleOriginalServiceModifed(Long orgServiceID, ServiceReference<?> orgServiceRef) {
+ // We don't need to do anything for services that are queued up to be proxied, as the
+ // properties are only taken at the point of proxyfication...
+
+ ServiceRegistrationHolder holder = proxyMap.get(orgServiceID);
+ if (holder != null) {
+ ServiceRegistration<?> reg = holder.registration;
+ if (reg != null) {
+ // Preserve the roles as they are expensive to compute
+ Object roles = reg.getReference().getProperty(SERVICE_GUARD_ROLES_PROPERTY);
+ Dictionary<String, Object> newProxyProps = proxyProperties(orgServiceRef);
+ if (roles != null) {
+ newProxyProps.put(SERVICE_GUARD_ROLES_PROPERTY, roles);
+ } else {
+ newProxyProps.remove(SERVICE_GUARD_ROLES_PROPERTY);
+ }
+ reg.setProperties(newProxyProps);
+ }
+ }
+ }
+
+ boolean isProxy(ServiceReference<?> sr) {
+ return sr.getProperty(PROXY_SERVICE_KEY) != null;
+ }
+
+ // Called by hooks to find out whether the service should be hidden.
+ // Also handles the proxy creation of services if applicable.
+ // Return true if the hook should hide the service for the bundle
+ boolean handleProxificationForHook(ServiceReference<?> sr) {
+ // Note that when running under an OSGi R6 framework the number of proxies created
+ // can be limited by looking at the new 'service.scope' property. If the value is
+ // 'singleton' then the same proxy can be shared across all clients.
+ // Pre OSGi R6 it is not possible to find out whether a service is backed by a
+ // Service Factory, so we assume that every service is.
+
+ if (isProxy(sr)) {
+ return false;
+ }
+
+ proxyIfNotAlreadyProxied(sr); // Note does most of the work async
+ return true;
+ }
+
+ void proxyIfNotAlreadyProxied(final ServiceReference<?> originalRef) {
+ final long orgServiceID = (Long) originalRef.getProperty(Constants.SERVICE_ID);
+
+ // make sure it's on the map before the proxy is registered, as that can trigger
+ // another call into this method, and we need to make sure that it doesn't proxy
+ // the service again.
+ final ServiceRegistrationHolder registrationHolder = new ServiceRegistrationHolder();
+ ServiceRegistrationHolder previousHolder = proxyMap.putIfAbsent(orgServiceID, registrationHolder);
+ if (previousHolder != null) {
+ // There is already a proxy for this service
+ return;
+ }
+
+ LOG.trace("Will create proxy of service {}({})", originalRef.getProperty(Constants.OBJECTCLASS), orgServiceID);
+
+ // Instead of immediately creating the proxy, we add the code that creates the proxy to the proxyQueue.
+ // This means that we can better react to the fact that the ProxyManager service might arrive
+ // later. As soon as the Proxy Manager is available, the queue is emptied and the proxies created.
+ CreateProxyRunnable cpr = new CreateProxyRunnable() {
+ @Override
+ public long getOriginalServiceID() {
+ return orgServiceID;
+ }
+
+ @Override
+ public void run(final ProxyManager pm) throws Exception {
+ String[] objectClassProperty = (String[]) originalRef.getProperty(Constants.OBJECTCLASS);
+ ServiceFactory<Object> sf = new ProxyServiceFactory(pm, originalRef);
+ registrationHolder.registration = originalRef.getBundle().getBundleContext().registerService(
+ objectClassProperty, sf, proxyPropertiesRoles());
+
+ Dictionary<String, Object> actualProxyProps = copyProperties(registrationHolder.registration.getReference());
+ LOG.debug("Created proxy of service {} under {} with properties {}",
+ orgServiceID, actualProxyProps.get(Constants.OBJECTCLASS), actualProxyProps);
+ }
+
+ private Dictionary<String, Object> proxyPropertiesRoles() throws Exception {
+ Dictionary<String, Object> p = proxyProperties(originalRef);
+
+ Set<String> roles = getServiceInvocationRoles(originalRef);
+ if (roles != null) {
+ roles.remove(ROLE_WILDCARD); // we don't expose that on the service property
+ p.put(SERVICE_GUARD_ROLES_PROPERTY, roles);
+ } else {
+ // In this case there are no roles defined for the service so anyone can invoke it
+ p.remove(SERVICE_GUARD_ROLES_PROPERTY);
+ }
+ return p;
+ }
+ };
+
+ try {
+ createProxyQueue.put(cpr);
+ } catch (InterruptedException e) {
+ LOG.warn("Problem scheduling a proxy creator for service {}({})",
+ originalRef.getProperty(Constants.OBJECTCLASS), orgServiceID, e);
+ e.printStackTrace();
+ }
+ }
+
+ private static Dictionary<String, Object> proxyProperties(ServiceReference<?> sr) {
+ Dictionary<String, Object> p = copyProperties(sr);
+ p.put(PROXY_SERVICE_KEY, Boolean.TRUE);
+ return p;
+ }
+
+ private static Dictionary<String, Object> copyProperties(ServiceReference<?> sr) {
+ Dictionary<String, Object> p = new Hashtable<String, Object>();
+
+ for (String key : sr.getPropertyKeys()) {
+ p.put(key, sr.getProperty(key));
+ }
+ return p;
+ }
+
+ // Returns what roles can possibly ever invoke this service. Note that not every invocation may be successful
+ // as there can be different roles for different methods and also roles based on arguments passed in.
+ Set<String> getServiceInvocationRoles(ServiceReference<?> serviceReference) throws Exception {
+ boolean definitionFound = false;
+ Set<String> allRoles = new HashSet<String>();
+
+ // This can probably be optimized. Maybe we can cache the config object relevant instead of
+ // walking through all of the ones that have 'service.guard'.
+ for (Configuration config : getServiceGuardConfigs()) {
+ Object guardFilter = config.getProperties().get(SERVICE_GUARD_KEY);
+ if (guardFilter instanceof String) {
+ Filter filter = myBundleContext.createFilter((String) guardFilter);
+ if (filter.match(serviceReference)) {
+ definitionFound = true;
+ for (Enumeration<String> e = config.getProperties().keys(); e.hasMoreElements(); ) {
+ String key = e.nextElement();
+ String bareKey = key;
+ int idx = bareKey.indexOf('(');
+ if (idx >= 0) {
+ bareKey = bareKey.substring(0, idx);
+ }
+ int idx1 = bareKey.indexOf('[');
+ if (idx1 >= 0) {
+ bareKey = bareKey.substring(0, idx1);
+ }
+ int idx2 = bareKey.indexOf('*');
+ if (idx2 >= 0) {
+ bareKey = bareKey.substring(0, idx2);
+ }
+ if (!isValidMethodName(bareKey)) {
+ continue;
+ }
+ Object value = config.getProperties().get(key);
+ if (value instanceof String) {
+ allRoles.addAll(ACLConfigurationParser.parseRoles((String) value));
+ }
+ }
+ }
+ }
+ }
+ return definitionFound ? allRoles : null;
+ }
+
+ // Ensures that it never returns null
+ private Configuration[] getServiceGuardConfigs() throws IOException, InvalidSyntaxException {
+ ConfigurationAdmin ca = null;
+ try {
+ ca = configAdminTracker.waitForService(5000);
+ } catch (InterruptedException e) {
+ }
+ if (ca == null) {
+ throw new IllegalStateException("Role based access for services requires the OSGi Configuration Admin Service to be present");
+ }
+
+ Configuration[] configs = ca.listConfigurations(
+ "(&(" + Constants.SERVICE_PID + "=" + SERVICE_ACL_PREFIX + "*)(" + SERVICE_GUARD_KEY + "=*))");
+ if (configs == null) {
+ return new Configuration [] {};
+ }
+ return configs;
+ }
+
+ private boolean isValidMethodName(String name) {
+ return JAVA_METHOD_NAME_PATTERN.matcher(name).matches();
+ }
+
+ void stopProxyCreator() {
+ runProxyCreator = false; // Will end the proxy creation thread
+ if (proxyCreatorThread != null) {
+ proxyCreatorThread.interrupt();
+ }
+ }
+
+ static boolean currentUserHasRole(String reqRole) {
+ if (ROLE_WILDCARD.equals(reqRole)) {
+ return true;
+ }
+
+ 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;
+ }
+
+ static class ServiceRegistrationHolder {
+ volatile ServiceRegistration<?> registration;
+ }
+
+ class ProxyServiceFactory implements ServiceFactory<Object> {
+ private final ProxyManager pm;
+ private final ServiceReference<?> originalRef;
+
+ ProxyServiceFactory(ProxyManager pm, ServiceReference<?> originalRef) {
+ this.pm = pm;
+ this.originalRef = originalRef;
+ }
+
+ @Override
+ public Object getService(Bundle bundle, ServiceRegistration<Object> registration) {
+ Set<Class<?>> allClasses = new HashSet<Class<?>>();
+
+ // This needs to be done on the Client BundleContext since the bundle might be backed by a Service Factory
+ // in which case it needs to be given a chance to produce the right service for this client.
+ Object svc = bundle.getBundleContext().getService(originalRef);
+ Class<?> curClass = svc.getClass();
+ while (!Object.class.equals(curClass)) {
+ allClasses.add(curClass);
+ allClasses.addAll(Arrays.asList(curClass.getInterfaces()));
+ curClass = curClass.getSuperclass(); // Collect super types too
+ }
+
+ for (Iterator<Class<?>> i = allClasses.iterator(); i.hasNext(); ) {
+ Class<?> cls = i.next();
+ if (((cls.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED)) == 0) ||
+ ((cls.getModifiers() & Modifier.FINAL) > 0) ||
+ cls.isAnonymousClass() || cls.isLocalClass()) {
+ // Do not attempt to proxy private, package-default, final, anonymous or local classes
+ i.remove();
+ } else {
+ for (Method m : cls.getDeclaredMethods()) {
+ if ((m.getModifiers() & Modifier.FINAL) > 0) {
+ // If the class contains final methods, don't attempt to proxy it
+ i.remove();
+ break;
+ }
+ }
+ }
+ }
+
+ InvocationListener il = new ProxyInvocationListener(originalRef);
+ try {
+ return pm.createInterceptingProxy(originalRef.getBundle(), allClasses, svc, il);
+ } catch (UnableToProxyException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void ungetService(Bundle bundle, ServiceRegistration<Object> registration, Object service) {
+ bundle.getBundleContext().ungetService(originalRef);
+ }
+ }
+
+ class ProxyInvocationListener implements InvocationListener {
+ private final ServiceReference<?> serviceReference;
+
+ ProxyInvocationListener(ServiceReference<?> sr) {
+ this.serviceReference = sr;
+ }
+
+ @Override
+ public Object preInvoke(Object proxy, Method m, Object[] args) throws Throwable {
+ String[] sig = new String[m.getParameterTypes().length];
+ for (int i = 0; i < m.getParameterTypes().length; i++) {
+ sig[i] = m.getParameterTypes()[i].getName();
+ }
+
+ // The ordering of the keys is important because the first value when iterating has the highest specificity
+ TreeMap<Specificity, List<String>> roleMappings = new TreeMap<ACLConfigurationParser.Specificity, List<String>>();
+ boolean foundMatchingConfig = false;
+
+ // This can probably be optimized. Maybe we can cache the config object relevant instead of
+ // walking through all of the ones that have 'service.guard'.
+ for (Configuration config : getServiceGuardConfigs()) {
+ Object guardFilter = config.getProperties().get(SERVICE_GUARD_KEY);
+ if (guardFilter instanceof String) {
+ Filter filter = myBundleContext.createFilter((String) guardFilter);
+ if (filter.match(serviceReference)) {
+ foundMatchingConfig = true;
+ List<String> roles = new ArrayList<String>();
+ Specificity s = ACLConfigurationParser.
+ getRolesForInvocation(m.getName(), args, sig, config.getProperties(), roles);
+ if (s != Specificity.NO_MATCH) {
+ roleMappings.put(s, roles);
+ if (s == Specificity.ARGUMENT_MATCH) {
+ // No more specific mapping can be found
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (!foundMatchingConfig) {
+ // No mappings for this service, anyone can invoke
+ return null;
+ }
+
+ if (roleMappings.size() == 0) {
+ LOG.info("Service {} has role mapping, but assigned no roles to method {}", serviceReference, m);
+ throw new SecurityException("Insufficient credentials.");
+ }
+
+ // The first entry on the map has the highest significance because the keys are sorted in the order of
+ // the Specificity enum.
+ List<String> allowedRoles = roleMappings.values().iterator().next();
+ for (String role : allowedRoles) {
+ if (currentUserHasRole(role)) {
+ LOG.trace("Allow user with role {} to invoke service {} method {}", role, serviceReference, m);
+ return null;
+ }
+ }
+
+ // The current user does not have the required roles to invoke the service.
+ LOG.info("Current user does not have required roles ({}) for service {} method {} and/or arguments",
+ allowedRoles, serviceReference, m);
+ throw new SecurityException("Insufficient credentials.");
+ }
+
+
+ @Override
+ public void postInvokeExceptionalReturn(Object token, Object proxy, Method m, Throwable exception) throws Throwable {
+ }
+
+ @Override
+ public void postInvoke(Object token, Object proxy, Method m, Object returnValue) throws Throwable {
+ }
+ }
+
+ // This customizer comes into action as the ProxyManager service arrives.
+ class ServiceProxyCreatorCustomizer implements ServiceTrackerCustomizer<ProxyManager, ProxyManager> {
+ @Override
+ public ProxyManager addingService(ServiceReference<ProxyManager> reference) {
+ runProxyCreator = true;
+ final ProxyManager svc = myBundleContext.getService(reference);
+ if (proxyCreatorThread == null && svc != null) {
+ proxyCreatorThread = newProxyProducingThread(svc);
+ }
+ return svc;
+ }
+
+ private Thread newProxyProducingThread(final ProxyManager proxyManager) {
+ Thread t = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ while (runProxyCreator) {
+ CreateProxyRunnable proxyCreator = null;
+ try {
+ proxyCreator = createProxyQueue.take(); // take waits until there is something on the queue
+ } catch (InterruptedException ie) {
+ // part of normal behaviour
+ }
+
+ if (proxyCreator != null) {
+ try {
+ proxyCreator.run(proxyManager);
+ } catch (Exception e) {
+ LOG.warn("Problem creating secured service proxy", e);
+ }
+ }
+ }
+ // finished running
+ proxyCreatorThread = null;
+ }
+ });
+ t.setName(PROXY_CREATOR_THREAD_NAME);
+ t.setDaemon(true);
+ t.start();
+
+ return t;
+ }
+
+ @Override
+ public void modifiedService(ServiceReference<ProxyManager> reference, ProxyManager service) {
+ // no need to react
+ }
+
+ @Override
+ public void removedService(ServiceReference<ProxyManager> reference, ProxyManager service) {
+ stopProxyCreator();
+ }
+ }
+
+ interface CreateProxyRunnable {
+ long getOriginalServiceID();
+ void run(ProxyManager pm) throws Exception;
+ }
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardingEventHook.java
----------------------------------------------------------------------
diff --git a/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardingEventHook.java b/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardingEventHook.java
new file mode 100644
index 0000000..a37e863
--- /dev/null
+++ b/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardingEventHook.java
@@ -0,0 +1,63 @@
+/*
+ * 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.service.guard.impl;
+
+import org.osgi.framework.*;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.ListenerHook;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+public class GuardingEventHook implements EventListenerHook {
+
+ private final BundleContext bundleContext;
+ private final GuardProxyCatalog guardProxyCatalog;
+ private final Filter servicesFilter;
+
+ GuardingEventHook(BundleContext bundleContext, GuardProxyCatalog guardProxyCatalog, Filter securedServicesFilter) throws InvalidSyntaxException {
+ this.bundleContext = bundleContext;
+ this.guardProxyCatalog = guardProxyCatalog;
+ this.servicesFilter = securedServicesFilter;
+ }
+
+ @Override
+ public void event(ServiceEvent event, Map<BundleContext, Collection<ListenerHook.ListenerInfo>> listeners) {
+ if (servicesFilter == null) {
+ return;
+ }
+
+ ServiceReference<?> sr = event.getServiceReference();
+ if (!servicesFilter.match(sr)) {
+ return;
+ }
+
+ for (Iterator<BundleContext> i = listeners.keySet().iterator(); i.hasNext(); ) {
+ BundleContext bc = i.next();
+ if (bundleContext.equals(bc) || bc.getBundle().getBundleId() == 0L) {
+ // don't hide anything from this bundle or the system bundle
+ continue;
+ }
+
+ if (guardProxyCatalog.handleProxificationForHook(sr)) {
+ i.remove();
+ }
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardingFindHook.java
----------------------------------------------------------------------
diff --git a/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardingFindHook.java b/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardingFindHook.java
new file mode 100644
index 0000000..bf1497c
--- /dev/null
+++ b/service/guard/src/main/java/org/apache/karaf/service/guard/impl/GuardingFindHook.java
@@ -0,0 +1,62 @@
+/*
+ * 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.service.guard.impl;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.hooks.service.FindHook;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+public class GuardingFindHook implements FindHook {
+
+ private final BundleContext bundleContext;
+ private final GuardProxyCatalog guardProxyCatalog;
+ private final Filter servicesFilter;
+
+ GuardingFindHook(BundleContext bundleContext, GuardProxyCatalog guardProxyCatalog, Filter securedServicesFilter) {
+ this.bundleContext = bundleContext;
+ this.guardProxyCatalog = guardProxyCatalog;
+ this.servicesFilter = securedServicesFilter;
+ }
+
+ public void find(BundleContext bundleContext, String name, String filter, boolean allServices, Collection<ServiceReference<?>> references) {
+ if (servicesFilter == null) {
+ return;
+ }
+
+ if (this.bundleContext.equals(bundleContext) || bundleContext.getBundle().getBundleId() == 0) {
+ // don't hide anything from this bundle or the system bundle
+ return;
+ }
+
+ for (Iterator<ServiceReference<?>> i = references.iterator(); i.hasNext(); ) {
+ ServiceReference<?> sr = i.next();
+
+ if (!servicesFilter.match(sr)) {
+ continue;
+ }
+
+ if (guardProxyCatalog.handleProxificationForHook(sr)) {
+ i.remove();
+ }
+ }
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/main/java/org/apache/karaf/service/guard/tools/ACLConfigurationParser.java
----------------------------------------------------------------------
diff --git a/service/guard/src/main/java/org/apache/karaf/service/guard/tools/ACLConfigurationParser.java b/service/guard/src/main/java/org/apache/karaf/service/guard/tools/ACLConfigurationParser.java
new file mode 100644
index 0000000..5a5af3e
--- /dev/null
+++ b/service/guard/src/main/java/org/apache/karaf/service/guard/tools/ACLConfigurationParser.java
@@ -0,0 +1,337 @@
+/*
+ * 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.service.guard.tools;
+
+import java.util.*;
+
+public class ACLConfigurationParser {
+
+ // note that the order of the enums is important. Needs to be from most specific to least specific.
+ public enum Specificity {
+ ARGUMENT_MATCH,
+ SIGNATURE_MATCH,
+ NAME_MATCH,
+ WILDCARD_MATCH,
+ NO_MATCH
+ };
+
+ /**
+ * Returns the roles that can invoke the given operation. This is determined by matching the
+ * operation details against configuration provided.<p/>
+ *
+ * The following configuration is supported. Keys are used to match an invocation against. The value can contain
+ * a comma-separated list of roles. Spaces are ignored for the role values. Note that comments are allowed in the
+ * value field after the hash {@code #} character:
+ * <pre>
+ * {@code
+ * myMethod = role1, role2
+ * methodName(int)[/17/] = role1 # regex match, assume it's surrounded by ^ and $
+ * methodName(int)[/[01]8/] = role2
+ * methodName(int)["19"] = role3 # exact value match
+ * methodName(int) = role4 # signature match
+ * methodName(java.lang.String, int) = role5 # signature match
+ * methodName = # no roles can invoke this command
+ * method* = role6 # name prefix/wildcard match. The asterisk must appear at the end.
+ * }
+ * </pre>
+ *
+ * The following algorithm is used to find matching roles:
+ * <ol>
+ * <li>Find all regex and exact value matches. For all parameters these matches are found by calling {@code toString()}
+ * on the parameters passed in. If there are multiple matches in this category all the matching roles are collected.
+ * If any is found return these roles.
+ * </li>
+ * <li>Find a signature match. If found return the associated roles.</li>
+ * <li>Find a method name match. If found return the associated roles.</li>
+ * <li>Find a method name prefix/wildcard match. If more than one prefix match, the roles associated with the longest
+ * prefix is used. So for example, if there are rules for {@code get*} and {@code *} only the roles associated with
+ * {@code get*} are returned.
+ * </li>
+ * <li>If none of the above criteria match, this method returns {@code null}.</li>
+ * </ol>
+ *
+ * @param methodName the method name to be invoked.
+ * @param params the parameters provided for the invocation. May be {@code null} for cases there the parameters are not yet
+ * known. In this case the roles that can <em>potentially</em> invoke the method are returned, although based on
+ * parameter values the actual invocation may still be denied.
+ * @param signature the signature of the method specified as an array of class name. For simple types, the simple type name
+ * is used (e.g. "int").
+ * @param config the configuration to check against.
+ * @param addToRoles the list of roles (which may be empty) if a matching configuration iteam has been found.
+ * @return the specificity
+ */
+ public static Specificity getRolesForInvocation(String methodName, Object[] params, String[] signature,
+ Dictionary<String, Object> config, List<String> addToRoles) {
+ Dictionary<String, Object> properties = trimKeys(config);
+
+ Specificity s = getRolesBasedOnSignature(methodName, params, signature, properties, addToRoles);
+ if (s != Specificity.NO_MATCH) {
+ return s;
+ }
+
+ s = getRolesBasedOnSignature(methodName, params, null, properties, addToRoles);
+ if (s != Specificity.NO_MATCH) {
+ return s;
+ }
+
+ List<String> roles = getMethodNameWildcardRoles(properties, methodName);
+ if (roles != null) {
+ addToRoles.addAll(roles);
+ return Specificity.WILDCARD_MATCH;
+ } else {
+ return Specificity.NO_MATCH;
+ }
+ }
+
+ private static Specificity getRolesBasedOnSignature(String methodName, Object[] params, String[] signature,
+ Dictionary<String, Object> properties, List<String> roles) {
+ if (params != null) {
+ boolean foundExactOrRegex = false;
+ Object exactArgMatchRoles = properties.get(getExactArgSignature(methodName, signature, params));
+ if (exactArgMatchRoles instanceof String) {
+ roles.addAll(parseRoles((String) exactArgMatchRoles));
+ foundExactOrRegex = true;
+ }
+
+ List<String> r = getRegexRoles(properties, methodName, signature, params);
+ if (r != null) {
+ foundExactOrRegex = true;
+ roles.addAll(r);
+ }
+
+ if (foundExactOrRegex) {
+ // since we have the actual parameters we can match them and if they do we won't look for any
+ // more generic rules...
+ return Specificity.ARGUMENT_MATCH;
+ }
+ } else {
+ // this is used in the case where parameters aren't known yet and the system wants to find out
+ // what roles in principle can invoke this method
+ List<String> r = getExactArgOrRegexRoles(properties, methodName, signature);
+ if (r != null) {
+ roles.addAll(r);
+ }
+ }
+
+ Object signatureRoles = properties.get(getSignature(methodName, signature));
+ if (signatureRoles instanceof String) {
+ roles.addAll(parseRoles((String) signatureRoles));
+ return signature == null ? Specificity.NAME_MATCH : Specificity.SIGNATURE_MATCH;
+ }
+
+ return Specificity.NO_MATCH;
+ }
+
+ private static Dictionary<String, Object> trimKeys(Dictionary<String, Object> properties) {
+ Dictionary<String, Object> d = new Hashtable<String, Object>();
+ for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
+ String key = e.nextElement();
+ Object value = properties.get(key);
+ d.put(removeSpaces(key), value);
+ }
+ return d;
+ }
+
+ private static 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();
+ }
+
+ public static List<String> parseRoles(String roleStr) {
+ int hashIdx = roleStr.indexOf('#');
+ if (hashIdx >= 0) {
+ // you can put a comment at the end
+ roleStr = roleStr.substring(0, hashIdx);
+ }
+
+ List<String> roles = new ArrayList<String>();
+ for (String role : roleStr.split("[,]")) {
+ String trimmed = role.trim();
+ if (trimmed.length() > 0) {
+ roles.add(trimmed);
+ }
+ }
+
+ return roles;
+ }
+
+ private static 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('"');
+ if (param != null)
+ sb.append(param.toString().trim());
+ sb.append('"');
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ private static String getSignature(String methodName, String[] signature) {
+ StringBuilder sb = new StringBuilder(methodName);
+ if (signature == null)
+ return sb.toString();
+
+ 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 static List<String> getRegexRoles(Dictionary<String, Object> properties, String methodName, String[] signature, Object[] params) {
+ List<String> roles = new ArrayList<String>();
+ boolean matchFound = false;
+ String methodSig = getSignature(methodName, signature);
+ String prefix = methodSig + "[/";
+ for (Enumeration<String> e = properties.keys(); e.hasMoreElements(); ) {
+ String key = e.nextElement().trim();
+ if (key.startsWith(prefix) && key.endsWith("/]")) {
+ List<String> regexArgs = getRegexDecl(key.substring(methodSig.length()));
+ if (allParamsMatch(regexArgs, params)) {
+ matchFound = true;
+ Object roleStr = properties.get(key);
+ if (roleStr instanceof String) {
+ roles.addAll(parseRoles((String) roleStr));
+ }
+ }
+ }
+ }
+ return matchFound ? roles : null;
+ }
+
+ private static List<String> getExactArgOrRegexRoles(Dictionary<String, Object> properties, String methodName, String[] signature) {
+ List<String> roles = new ArrayList<String>();
+ boolean matchFound = false;
+ String methodSig = getSignature(methodName, signature);
+ String prefix = methodSig + "[";
+ for (Enumeration<String> 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 ? roles : null;
+ }
+
+ private static List<String> getMethodNameWildcardRoles(Dictionary<String, Object> properties, String methodName) {
+ SortedMap<String, String> wildcardRules = new TreeMap<String, String>(new Comparator<String>() {
+ public int compare(String s1, String s2) {
+ // returns longer entries before shorter ones...
+ return s2.length() - s1.length();
+ }
+ });
+ for (Enumeration<String> 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) {
+ return parseRoles(wildcardRules.values().iterator().next());
+ } else {
+ return null;
+ }
+ }
+
+ private static boolean allParamsMatch(List<String> regexArgs, Object[] params) {
+ if (regexArgs.size() != params.length)
+ return false;
+
+ for (int i = 0; i < regexArgs.size(); i++) {
+ if (params[i] == null)
+ return false;
+ if (!params[i].toString().trim().matches(regexArgs.get(i))) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static List<String> getRegexDecl(String key) {
+ List<String> l = new ArrayList<String>();
+
+ boolean inRegex = false;
+ StringBuilder curRegex = new StringBuilder();
+ for (int i = 0; i < key.length(); i++) {
+ if (!inRegex) {
+ if (key.length() > i + 1) {
+ String s = key.substring(i, i + 2);
+ if ("[/".equals(s) || ",/".equals(s)) {
+ inRegex = true;
+ i++;
+ continue;
+ }
+ }
+ } else {
+ String s = key.substring(i, i + 2);
+ if ("/]".equals(s) || "/,".equals(s)) {
+ l.add(curRegex.toString());
+ curRegex = new StringBuilder();
+ inRegex = false;
+ continue;
+ }
+ curRegex.append(key.charAt(i));
+ }
+ }
+ return l;
+ }
+}
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/main/resources/OSGI-INF/bundle.info
----------------------------------------------------------------------
diff --git a/service/guard/src/main/resources/OSGI-INF/bundle.info b/service/guard/src/main/resources/OSGI-INF/bundle.info
new file mode 100644
index 0000000..be2ec64
--- /dev/null
+++ b/service/guard/src/main/resources/OSGI-INF/bundle.info
@@ -0,0 +1,53 @@
+h1. Synopsis
+
+${project.name}
+
+${project.description}
+
+Maven URL:
+ [mvn:${project.groupId}/${project.artifactId}/${project.version}]
+
+h1. Description
+
+Add Role-based access to any OSGi Service
+
+Role-based access is added to existing OSGi services by hiding the
+original service from a clients through service registry hooks and
+presenting a proxy to the service that checks the required roles before
+an invocation is made.
+
+To enable the Role-based access for services, the service needs to match
+the filter in the karaf.secured.services system property (typically set
+in etc/system.properties), e.g:
+
+ karaf.secured.services = (|(objectClass=org.acme*)(foo=bar))
+
+Only services that match this filter have Role-based access applied,
+other services are left alone.
+
+Required roles are registered through the OSGi Configuration Admin
+Service. The service PID must start with org.apache.karaf.service.acl.
+For enabled services configurations with matching PIDs defining a
+service.guard property that matches are looked up.
+Then roles associated with the current method, its signature and/or the
+passed arguments are looked up. For example, take the following
+configuration:
+
+ service.guard = (objectClass=org.acme.MyServiceAPI)
+ myMethod = admin, manager
+
+A special role declaration of '*' means that role checking is disabled
+for this method.
+If a matching ACL is found, but no matching definition for the method
+being invoked can be found, no user can invoke the method.
+If no matching ACL is found, role checking is not enabled for the
+service.
+As with the role-based JMX access, method signatures and arguments
+(either literal or regex-based) can be matched.
+
+When roles are checked, these are obtained from the Subject associated
+with the current AccessControlContext. By default the Karaf
+RolePrincipal is checked for, but other role implementations can be
+supported through the following syntax:
+
+ my.custom.RoleImpl:someRole
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/karaf/blob/4b8cc102/service/guard/src/test/java/org/apache/karaf/service/guard/impl/ActivatorTest.java
----------------------------------------------------------------------
diff --git a/service/guard/src/test/java/org/apache/karaf/service/guard/impl/ActivatorTest.java b/service/guard/src/test/java/org/apache/karaf/service/guard/impl/ActivatorTest.java
new file mode 100644
index 0000000..9670eb3
--- /dev/null
+++ b/service/guard/src/test/java/org/apache/karaf/service/guard/impl/ActivatorTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.service.guard.impl;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.hooks.service.EventListenerHook;
+import org.osgi.framework.hooks.service.FindHook;
+
+import java.util.Dictionary;
+import java.util.Properties;
+
+public class ActivatorTest {
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testStartActivator() throws Exception {
+ // keep the old properties. Note that the Properties 'copy constructor' new Properties(props)
+ // doesn't actually copy, hence the awkward setup here...
+ Properties oldProps = new Properties();
+ oldProps.putAll(System.getProperties());
+
+ try {
+ System.setProperty(GuardProxyCatalog.KARAF_SECURED_SERVICES_SYSPROP, "(foo=bar)");
+ Bundle b = EasyMock.createMock(Bundle.class);
+ EasyMock.expect(b.getBundleId()).andReturn(768L).anyTimes();
+ EasyMock.replay(b);
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.expect(bc.getBundle()).andReturn(b).anyTimes();
+ EasyMock.expect(bc.createFilter(EasyMock.anyObject(String.class))).andAnswer(new IAnswer<Filter>() {
+ @Override
+ public Filter answer() throws Throwable {
+ return FrameworkUtil.createFilter((String) EasyMock.getCurrentArguments()[0]);
+ }
+ }).anyTimes();
+
+ EasyMock.expect(bc.registerService(
+ EasyMock.eq(EventListenerHook.class), EasyMock.isA(EventListenerHook.class), EasyMock.isNull(Dictionary.class)))
+ .andReturn(null);
+ EasyMock.expect(bc.registerService(
+ EasyMock.eq(FindHook.class), EasyMock.isA(FindHook.class), EasyMock.isNull(Dictionary.class)))
+ .andReturn(null);
+
+ EasyMock.replay(bc);
+
+ Activator a = new Activator();
+ a.start(bc);
+
+ assertNotNull(a.guardProxyCatalog);
+ assertNotNull(a.guardingEventHook);
+ assertNotNull(a.guardingFindHook);
+
+ EasyMock.verify(bc);
+ } finally {
+ System.setProperties(oldProps);
+ }
+ }
+
+ @Test
+ public void testStartActivatorNoServicesSecured() throws Exception {
+ // keep the old properties. Note that the Properties 'copy constructor' new Properties(props)
+ // doesn't actually copy, hence the awkward setup here...
+ Properties oldProps = new Properties();
+ oldProps.putAll(System.getProperties());
+
+ try {
+ Properties newProps = removeProperties(System.getProperties(), GuardProxyCatalog.KARAF_SECURED_SERVICES_SYSPROP);
+ System.setProperties(newProps);
+
+ BundleContext bc = EasyMock.createNiceMock(BundleContext.class);
+ EasyMock.replay(bc);
+
+ Activator a = new Activator();
+ a.start(bc);
+
+ assertNull(a.guardProxyCatalog);
+ } finally {
+ System.setProperties(oldProps);
+ }
+ }
+
+ @Test
+ public void testStopActivator() throws Exception {
+ Activator a = new Activator();
+
+ a.guardProxyCatalog = EasyMock.createMock(GuardProxyCatalog.class);
+ a.guardProxyCatalog.close();
+ EasyMock.expectLastCall().once();
+ EasyMock.replay(a.guardProxyCatalog);
+
+ a.stop(EasyMock.createMock(BundleContext.class));
+
+ EasyMock.verify(a.guardProxyCatalog);
+ }
+
+ private Properties removeProperties(Properties props, String ... keys) {
+ Properties p = new Properties();
+ p.putAll(props);
+ for (String key : keys) {
+ p.remove(key);
+ }
+ return p;
+ }
+
+}