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;
+    }
+
+}