FELIX-3600: imported new UserAdmin contribution.

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1391437 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/useradmin/filestore/pom.xml b/useradmin/filestore/pom.xml
new file mode 100644
index 0000000..4cd68c2
--- /dev/null
+++ b/useradmin/filestore/pom.xml
@@ -0,0 +1,100 @@
+<!--
+    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.
+-->
+<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">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>felix-parent</artifactId>
+		<groupId>org.apache.felix</groupId>
+		<version>1.2.0</version>
+        <relativePath>../../../pom/pom.xml</relativePath>
+	</parent>
+	<artifactId>org.apache.felix.useradmin.filestore</artifactId>
+	<version>1.0.1</version>
+	<packaging>bundle</packaging>
+	<description>Provides a file-based repository store for the UserAdmin OSGi compendium service.</description>
+	<dependencies>
+		<dependency>
+			<groupId>org.osgi</groupId>
+			<artifactId>org.osgi.core</artifactId>
+			<version>4.0.0</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.osgi</groupId>
+			<artifactId>org.osgi.compendium</artifactId>
+			<version>4.0.0</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.felix</groupId>
+			<artifactId>org.apache.felix.useradmin</artifactId>
+			<version>1.0.2</version>
+			<type>bundle</type>
+		</dependency>
+	</dependencies>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.felix</groupId>
+				<artifactId>maven-bundle-plugin</artifactId>
+				<extensions>true</extensions>
+				<configuration>
+					<instructions>
+						<Bundle-Name>Apache Felix User Admin file-based repository store</Bundle-Name>
+						<Bundle-Description>
+							A file-based repository store for User Admin Service of Apache Felix.
+						</Bundle-Description>
+						<Bundle-Activator>
+							${project.artifactId}.osgi.Activator
+						</Bundle-Activator>
+						<Bundle-SymbolicName>
+							${project.artifactId}
+						</Bundle-SymbolicName>
+						<Bundle-Vendor>The Apache Software Foundation</Bundle-Vendor>
+						<Import-Package>
+							org.osgi.service.useradmin; version="[1.1,1.2)",
+							org.apache.felix.useradmin; version="[1.0,1.1)",
+							*
+						</Import-Package>
+						<Export-Package>
+						</Export-Package>
+						<Private-Package>
+							${project.artifactId}.*
+						</Private-Package>
+					</instructions>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>rat-maven-plugin</artifactId>
+				<configuration>
+					<excludeSubProjects>false</excludeSubProjects>
+					<useEclipseDefaultExcludes>true</useEclipseDefaultExcludes>
+					<useMavenDefaultExcludes>true</useMavenDefaultExcludes>
+					<excludes>
+						<param>doc/*</param>
+						<param>maven-eclipse.xml</param>
+						<param>.checkstyle</param>
+						<param>.externalToolBuilders/*</param>
+					</excludes>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/ResettableTimer.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/ResettableTimer.java
new file mode 100644
index 0000000..b093e44
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/ResettableTimer.java
@@ -0,0 +1,114 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Provides a timer that can be reset.
+ */
+final class ResettableTimer {
+    private final ScheduledExecutorService m_executor;
+    private final Runnable m_task;
+    private final long m_timeout;
+    private final TimeUnit m_timeUnit;
+    private final AtomicReference m_futureRef;
+    
+    /**
+     * Creates a new {@link ResettableTimer} calling a given task when a given 
+     * timeout exceeds.
+     * 
+     * @param task the task to execute upon timout, cannot be <code>null</code>;
+     * @param timeout the timeout value, > 0;
+     * @param unit the time unit of the timeout value, cannot be <code>null</code>.
+     */
+    public ResettableTimer(Runnable task, long timeout, TimeUnit unit) {
+        this(new ScheduledThreadPoolExecutor(1), task, timeout, unit);
+    }
+    
+    /**
+     * Creates a new {@link ResettableTimer} calling a given task when a given 
+     * timeout exceeds.
+     * 
+     * @param executor the executor to use to execute the task, cannot be <code>null</code>;
+     * @param task the task to execute upon timout, cannot be <code>null</code>;
+     * @param timeout the timeout value, > 0;
+     * @param unit the time unit of the timeout value, cannot be <code>null</code>.
+     */
+    public ResettableTimer(ScheduledExecutorService executor, Runnable task, long timeout, TimeUnit unit) {
+        if (executor == null) { 
+            throw new IllegalArgumentException("Executor cannot be null!");
+        }
+        if (task == null) {
+            throw new IllegalArgumentException("Task cannot be null!");
+        }
+        if (timeout <= 0) { 
+            throw new IllegalArgumentException("Timeout cannot be negative!");
+        }
+        if (unit == null) { 
+            throw new IllegalArgumentException("TimeUnit cannot be null!"); 
+        }
+        
+        m_executor = executor;
+        m_task = task;
+        m_timeout = timeout;
+        m_timeUnit = unit;
+        
+        m_futureRef = new AtomicReference();
+    }
+
+    /**
+     * Schedules the task for execution with the contained timeout. If a task 
+     * is already pending or running, it will be cancelled (not interrupted). 
+     * The new task will be scheduled to run in now + timeout.
+     */
+    public ScheduledFuture schedule() {
+        ScheduledFuture currentTask = cancelCurrentTask();
+        ScheduledFuture newTask = m_executor.schedule(m_task, m_timeout, m_timeUnit);
+        m_futureRef.compareAndSet(currentTask, newTask);
+        return newTask;
+    }
+    
+    /**
+     * Shuts down this timer, allowing any pending tasks to execute. After this 
+     * method is called, {@link #schedule()} may no longer be called.
+     */
+    public void shutDown() {
+        m_executor.shutdown();
+        try {
+            m_executor.awaitTermination(2 * m_timeout, m_timeUnit);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    /**
+     * @return the current task, or <code>null</code> if no task is available.
+     */
+    private ScheduledFuture cancelCurrentTask() {
+        ScheduledFuture currentTask = (ScheduledFuture) m_futureRef.get();
+        if (currentTask != null) {
+            // Doesn't matter for completed tasks...
+            currentTask.cancel(false /* mayInterruptIfRunning */);
+        }
+        return currentTask;
+    }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStore.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStore.java
new file mode 100644
index 0000000..115d223
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStore.java
@@ -0,0 +1,258 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+
+/**
+ * Provides an implementation of {@link RoleRepositoryStore} using Java Serialization.
+ */
+public class RoleRepositoryFileStore extends RoleRepositoryMemoryStore implements Runnable, UserAdminListener, ManagedService {
+
+    /** The PID for this service to allow its configuration to be updated. */
+    public static final String PID = "org.apache.felix.useradmin.rolerepositoryfilestore";
+
+    static final String KEY_WRITE_DISABLED = "background.write.disabled";
+    static final String KEY_WRITE_DELAY_VALUE = "background.write.delay.value";
+    static final String KEY_WRITE_DELAY_TIMEUNIT = "background.write.delay.timeunit";
+
+    private static final boolean DEFAULT_WRITE_DISABLED = Boolean.parseBoolean(System.getProperty(KEY_WRITE_DISABLED, "false"));
+    private static final int DEFAULT_WRITE_DELAY_VALUE = Integer.parseInt(System.getProperty(KEY_WRITE_DELAY_VALUE, "500"));
+    private static final TimeUnit DEFAULT_WRITE_DELAY_TIMEUNIT = TimeUnit.MILLISECONDS;
+
+    private static final String FILE_NAME = "ua_repo.dat";
+
+    private final File m_file;
+    private final AtomicReference m_timerRef;
+
+    /**
+     * Creates a new {@link RoleRepositoryStore} instance.
+     * 
+     * @param baseDir the base directory where we can store our serialized data, cannot be <code>null</code>.
+     */
+    public RoleRepositoryFileStore(File baseDir) {
+        this(baseDir, !DEFAULT_WRITE_DISABLED);
+    }
+    
+    /**
+     * Creates a new {@link RoleRepositoryStore} instance.
+     * 
+     * @param baseDir the base directory where we can store our serialized data, cannot be <code>null</code>;
+     * @param backgroundWriteEnabled <code>true</code> if background writing should be enabled, <code>false</code> to disable it. 
+     */
+    public RoleRepositoryFileStore(File baseDir, boolean backgroundWriteEnabled) {
+        m_file = new File(baseDir, FILE_NAME);
+        
+        m_timerRef = new AtomicReference();
+
+        if (backgroundWriteEnabled) {
+            m_timerRef.set(new ResettableTimer(this, DEFAULT_WRITE_DELAY_VALUE, DEFAULT_WRITE_DELAY_TIMEUNIT));
+        }
+    }
+    
+    public void initialize() throws IOException {
+        m_entries.putAll(retrieve());
+    }
+
+    public void roleChanged(UserAdminEvent event) {
+        scheduleTask();
+    }
+
+    /**
+     * {@inheritDoc}
+     * 
+     * <p>Will be called by m_timer!</p>
+     */
+    public void run() {
+        try {
+            // Persist everything to disk...
+            flush();
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Stops this store service.
+     */
+    public void stop()  {
+        ResettableTimer timer = (ResettableTimer) m_timerRef.get();
+        if (timer != null) {
+            // Shutdown and await termination...
+            timer.shutDown();
+            // Clear reference...
+            m_timerRef.compareAndSet(timer, null);
+        }
+        
+        try {
+            // Write the latest version to disk...
+            flush();
+        }
+        catch (IOException e) {
+            // Nothing we can do about this here...
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void updated(Dictionary properties) throws ConfigurationException {
+        boolean writeDisabled = DEFAULT_WRITE_DISABLED;
+        int writeDelayValue = DEFAULT_WRITE_DELAY_VALUE;
+        TimeUnit writeDelayUnit = DEFAULT_WRITE_DELAY_TIMEUNIT;
+
+        if (properties != null) {
+            Object wd = properties.get(KEY_WRITE_DISABLED);
+            if (wd == null) {
+                throw new ConfigurationException(KEY_WRITE_DISABLED, "Missing write disabled value!");
+            }
+            try {
+                writeDisabled = Boolean.parseBoolean((String) wd);
+            } catch (Exception e) {
+                throw new ConfigurationException(KEY_WRITE_DISABLED, "Invalid write disabled value!");
+            }
+
+            if (!writeDisabled) {
+                Object wdv = properties.get(KEY_WRITE_DELAY_VALUE);
+                if (wdv == null) {
+                    throw new ConfigurationException(KEY_WRITE_DELAY_VALUE, "Missing write delay value!");
+                }
+                try {
+                    writeDelayValue = Integer.parseInt((String) wdv);
+                } catch (Exception e) {
+                    throw new ConfigurationException(KEY_WRITE_DELAY_VALUE, "Invalid write delay value!");
+                }
+                if (writeDelayValue <= 0) {
+                    throw new ConfigurationException(KEY_WRITE_DELAY_VALUE, "Invalid write delay value!");
+                }
+
+                Object wdu = properties.get(KEY_WRITE_DELAY_TIMEUNIT);
+                if (wdu != null) {
+                    try {
+                        writeDelayUnit = TimeUnit.valueOf(((String) wdu).toUpperCase());
+                    } catch (Exception e) {
+                        throw new ConfigurationException(KEY_WRITE_DELAY_TIMEUNIT, "Invalid write delay unit!");
+                    }
+                }
+            }
+        }
+
+        ResettableTimer timer = (ResettableTimer) m_timerRef.get();
+        if (timer != null) {
+            timer.shutDown();
+        }
+        m_timerRef.compareAndSet(timer, writeDisabled ? null : new ResettableTimer(this, writeDelayValue, writeDelayUnit));
+    }
+
+    /**
+     * Retrieves the serialized repository from disk.
+     * 
+     * @return the retrieved repository, never <code>null</code>.
+     * @throws IOException in case the retrieval of the repository failed.
+     */
+    protected Map retrieve() throws IOException {
+        InputStream is = null;
+
+        try {
+            is = new BufferedInputStream(new FileInputStream(m_file));
+
+            return new RoleRepositorySerializer().deserialize(is);
+        } catch (FileNotFoundException exception) {
+            // Don't bother; file does not exist...
+            return Collections.emptyMap();
+        } finally {
+            closeSafely(is);
+        }
+    }
+
+    /**
+     * Stores the given repository to disk as serialized objects.
+     * 
+     * @param roleRepository the repository to store, cannot be <code>null</code>.
+     * @throws IOException in case storing the repository failed.
+     */
+    protected void store(Map roleRepository) throws IOException {
+        OutputStream os = null;
+
+        try {
+            os = new BufferedOutputStream(new FileOutputStream(m_file));
+
+            new RoleRepositorySerializer().serialize(roleRepository, os);
+        } finally {
+            closeSafely(os);
+        }
+    }
+
+    /**
+     * Closes a given resource, ignoring any exceptions that may come out of this.
+     * 
+     * @param resource the resource to close, can be <code>null</code>.
+     */
+    private void closeSafely(Closeable resource) {
+        if (resource != null) {
+            try {
+                resource.close();
+            } catch (IOException e) {
+                // Ignore
+            }
+        }
+    }
+
+    /**
+     * Flushes the current repository to disk.
+     * 
+     * @throws IOException in case of problems storing the repository.
+     */
+    private void flush() throws IOException {
+        store(new HashMap(m_entries));
+    }
+
+    /**
+     * Notifies the background timer to schedule a task for storing the 
+     * contents of this store to disk.
+     */
+    private void scheduleTask() {
+        ResettableTimer timer = (ResettableTimer) m_timerRef.get();
+        if (timer != null) {
+            timer.schedule();
+        }
+    }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryMemoryStore.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryMemoryStore.java
new file mode 100644
index 0000000..f064f79
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositoryMemoryStore.java
@@ -0,0 +1,70 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.service.useradmin.Role;
+
+
+/**
+ * Provides a thread-safe in-memory role repository store.
+ */
+public class RoleRepositoryMemoryStore implements RoleRepositoryStore {
+    
+    protected final ConcurrentMap m_entries = new ConcurrentHashMap();
+
+    public boolean addRole(Role role) throws IOException {
+        if (role == null) {
+            throw new IllegalArgumentException("Role cannot be null!");
+        }
+        Object result = m_entries.putIfAbsent(role.getName(), role);
+        return result == null;
+    }
+
+    public void close() throws IOException {
+        // Nop
+    }
+
+    public Role[] getAllRoles() throws IOException {
+        Collection roles = m_entries.values();
+        Role[] result = new Role[roles.size()];
+        return (Role[]) roles.toArray(result);
+    }
+
+    public Role getRoleByName(String roleName) throws IOException {
+        if (roleName == null) {
+            throw new IllegalArgumentException("Role name cannot be null!");
+        }
+        return (Role) m_entries.get(roleName);
+    }
+    
+    public void initialize() throws IOException {
+        // Nop
+    }
+
+    public boolean removeRole(Role role) throws IOException {
+        if (role == null) {
+            throw new IllegalArgumentException("Role cannot be null!");
+        }
+        return m_entries.remove(role.getName(), role);
+    }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializer.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializer.java
new file mode 100644
index 0000000..57aca3e
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializer.java
@@ -0,0 +1,454 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+
+import org.apache.felix.useradmin.RoleFactory;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Provides a serializer for a role repository.
+ */
+final class RoleRepositorySerializer {
+    
+    private static final int VALUE_TYPE_STRING = 1;
+    private static final int VALUE_TYPE_BARRAY = 2; 
+
+    /**
+     * Deserializes a given input stream.
+     * 
+     * @param is the input stream to deserialize, cannot be <code>null</code>.
+     * @return a {@link Map} representing the role repository. It provides a 
+     *         mapping between the name of the role as key and the associated
+     *         role as value.
+     * @throws IOException in case of I/O problems;
+     * @throws IllegalArgumentException in case the given stream was <code>null</code>.
+     */
+    public Map deserialize(InputStream is) throws IOException {
+        if (is == null) {
+            throw new IllegalArgumentException("InputStream cannot be null!");
+        }
+        return readRepository(new DataInputStream(is));
+    }
+
+    /**
+     * Serializes a given map to the given output stream.
+     * 
+     * @param roleRepository the repository to serialize, cannot be <code>null</code>;
+     * @param os the output stream to serialize to, cannot be  <code>null</code>.
+     * @throws IOException in case of I/O problems;
+     * @throws IllegalArgumentException in case the given parameter was <code>null</code>.
+     */
+    public void serialize(Map roleRepository, OutputStream os) throws IOException {
+        if (roleRepository == null) {
+            throw new IllegalArgumentException("Map cannot be null!");
+        }
+        if (os == null) {
+            throw new IllegalArgumentException("OutputStream cannot be null!");
+        }
+        writeRepository(roleRepository, new DataOutputStream(os));
+    }
+    
+    /**
+     * Adds all groups, based on the given stub groups.
+     * 
+     * @param repository the repository to add the groups to, cannot be <code>null</code>;
+     * @param stubGroups the list with stub groups to replace, cannot be <code>null</code>.
+     * @throws IOException in case a referenced role was not found in the repository.
+     */
+    private void addGroups(Map repository, List stubGroups) throws IOException {
+        // First create "empty" groups in the repository; we'll fill them in later on...
+        Iterator sgIter = stubGroups.iterator();
+        while (sgIter.hasNext()) {
+            StubGroupImpl stubGroup = (StubGroupImpl) sgIter.next();
+
+            Group group = (Group) RoleFactory.createRole(Role.GROUP, stubGroup.getName());
+            copyDictionary(stubGroup.getProperties(), group.getProperties());
+            copyDictionary(stubGroup.getCredentials(), group.getCredentials());
+
+            repository.put(group.getName(), group);
+        }
+        
+        int origSize = stubGroups.size();
+        while (!stubGroups.isEmpty()) {
+            List copy = new ArrayList(stubGroups);
+            
+            int size = copy.size();
+            for (int i = 0; i < size; i++) {
+                StubGroupImpl stubGroup = (StubGroupImpl) copy.get(i);
+
+                Group group = (Group) repository.get(stubGroup.getName());
+                if (group != null) {
+                    resolveGroupMembers(stubGroup, group, repository);
+                    stubGroups.remove(stubGroup);
+                }
+            }
+
+            // In case we didn't resolve any groups; we should fail...
+            if (origSize == stubGroups.size()) {
+                throw new IOException("Failed to resolve groups: " + stubGroups);
+            }
+
+            origSize = stubGroups.size();
+        }
+    }
+    
+    /**
+     * Converts a given {@link Dictionary} implementation to a {@link Map} implementation.
+     * 
+     * @param dictionary the dictionary to convert, cannot be <code>null</code>.
+     * @return a {@link Map} instance with all the same key-value pairs as the given dictionary, never <code>null</code>.
+     */
+    private Map convertToMap(Dictionary dictionary) {
+        Map result = new HashMap();
+        if (dictionary instanceof Map) {
+            result.putAll((Map) dictionary);
+        } else {
+            Enumeration keyEnum = dictionary.keys();
+            while (keyEnum.hasMoreElements()) {
+                Object key = keyEnum.nextElement();
+                result.put(key, dictionary.get(key));
+            }
+        }
+        return result;
+    }
+    
+    /**
+     * Copies the contents of a given dictionary to a given other dictionary.
+     * 
+     * @param source the dictionary to copy from;
+     * @param dest the dictionary to copy to.
+     */
+    private void copyDictionary(Dictionary source, Dictionary dest) {
+        Enumeration keys = source.keys();
+        while (keys.hasMoreElements()) {
+            Object key = keys.nextElement();
+            Object value = source.get(key);
+            dest.put(key, value);
+        }
+    }
+
+    /**
+     * Reads and fills a given dictionary.
+     * 
+     * @param dict the dictionary to read & fill, cannot be <code>null</code>;
+     * @param dis the input stream to read the data from, cannot be <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private void readDictionary(Dictionary dict, DataInputStream dis) throws IOException {
+        // Read the number of entries...
+        int count = dis.readInt();
+        while (count-- > 0) {
+            // Read the name of the key...
+            String key = dis.readUTF();
+            // Read the type of the value...
+            int type = dis.read();
+            // Read the value & add the actual entry...
+            if (VALUE_TYPE_BARRAY == type) {
+                int length = dis.readInt();
+                byte[] value = new byte[length];
+                if (dis.read(value, 0, length) != length) {
+                    throw new IOException("Invalid repository; failed to correctly read dictionary!");
+                }
+                dict.put(key, value);
+            } else if (VALUE_TYPE_STRING == type) {
+                dict.put(key, dis.readUTF());
+            }
+        }
+    }
+    
+    /**
+     * Reads a (stub) group from the given input stream.
+     * 
+     * @param dis the input stream to read the data from, cannot be <code>null</code>.
+     * @return the read (stub) group, never <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private StubGroupImpl readGroup(DataInputStream dis) throws IOException {
+        StubGroupImpl group = new StubGroupImpl(dis.readUTF());
+        
+        readDictionary(group.getProperties(), dis);
+        readDictionary(group.getCredentials(), dis);
+        
+        // Read the number of basic members...
+        int count = dis.readInt();
+        while (count-- > 0) {
+            group.addMember(dis.readUTF());
+        }
+        
+        // Read the number of required members...
+        count = dis.readInt();
+        while (count-- > 0) {
+            group.addRequiredMember(dis.readUTF());
+        }
+        
+        return group;
+    }
+
+    /**
+     * Reads the entire repository from the given input stream.
+     * 
+     * @param dis the input stream to read the data from, cannot be <code>null</code>.
+     * @return the repository {@link Map}, never <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private Map readRepository(DataInputStream dis) throws IOException {
+        Map repository = new HashMap();
+        
+        int entryCount = dis.readInt();
+        
+        List stubGroups = new ArrayList();
+        
+        // Keep reading until no more types can be read...
+        while (entryCount-- > 0) {
+            int type = dis.readInt();
+            
+            Role role = null;
+            if (Role.GROUP == type) {
+                stubGroups.add(readGroup(dis));
+            } else if (Role.USER == type) {
+                role = readUser(dis);
+            } else {
+                role = readRole(dis);
+            }
+            
+            if (role != null) {
+                repository.put(role.getName(), role);
+            }
+        }
+        
+        // Post processing stage: replace all stub groups with real group implementations...
+        addGroups(repository, stubGroups);
+        
+        return repository;
+    }
+
+    /**
+     * Reads a role from the given input stream.
+     * 
+     * @param dis the input stream to read the data from, cannot be <code>null</code>.
+     * @return the read role, never <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private Role readRole(DataInputStream dis) throws IOException {
+        Role role = RoleFactory.createRole(Role.ROLE, dis.readUTF());
+        
+        readDictionary(role.getProperties(), dis);
+        
+        return role;
+    }
+    
+    /**
+     * Reads a user from the given input stream.
+     * 
+     * @param dis the input stream to read the data from, cannot be <code>null</code>.
+     * @return the read user, never <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private User readUser(DataInputStream dis) throws IOException {
+        User user = (User) RoleFactory.createRole(Role.USER, dis.readUTF());
+        
+        readDictionary(user.getProperties(), dis);
+        readDictionary(user.getCredentials(), dis);
+        
+        return user;
+    }
+
+    /**
+     * Resolves all basic and required group members for a given group, based on the names from the given stub group.
+     * 
+     * @param stubGroup the stub group to convert, cannot be <code>null</code>;
+     * @param repository the repository to take the roles from, cannot be <code>null</code>.
+     * @return a concrete {@link Group} instance with all members resolved, or <code>null</code> if not all members could be resolved.
+     * @throws IOException in case a referenced role was not found in the repository.
+     */
+    private void resolveGroupMembers(StubGroupImpl stubGroup, Group group, Map repository) throws IOException {
+        List names = stubGroup.getMemberNames();
+        int size = names.size();
+
+        for (int i = 0; i < size; i++) {
+            String name = (String) names.get(i);
+            Role role = (Role) repository.get(name);
+            if (role == null) {
+                throw new IOException("Unable to find referenced basic member: " + name);
+            }
+            group.addMember(role);
+        }
+        
+        names = stubGroup.getRequiredMemberNames();
+        size = names.size();
+        
+        for (int i = 0; i < size; i++) {
+            String name = (String) names.get(i);
+            Role role = (Role) repository.get(name);
+            if (role == null) {
+                throw new IOException("Unable to find referenced required member: " + name);
+            }
+            group.addRequiredMember(role);
+        }
+    }
+
+    /**
+     * Writes a given dictionary to the given output stream.
+     * 
+     * @param dict the dictionary to write, cannot be <code>null</code>;
+     * @param dos the output stream to write the data to, cannot be <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private void writeDictionary(Dictionary dict, DataOutputStream dos) throws IOException {
+        Map properties = convertToMap(dict);
+        
+        Set entries = properties.entrySet();
+        int size = entries.size();
+
+        // Write the number of entries...
+        dos.writeInt(size);
+        
+        Iterator entriesIter = entries.iterator();
+        while (entriesIter.hasNext()) {
+            Map.Entry entry = (Entry) entriesIter.next();
+            
+            dos.writeUTF((String) entry.getKey());
+            
+            Object value = entry.getValue();
+            if (value instanceof String) {
+                dos.write(VALUE_TYPE_STRING);
+                dos.writeUTF((String) value);
+            } else if (value instanceof byte[]) {
+                dos.write(VALUE_TYPE_BARRAY);
+                dos.writeInt(((byte[]) value).length);
+                dos.write((byte[]) value);
+            }
+        }
+    }
+
+    /**
+     * Writes a given group to the given output stream.
+     * 
+     * @param group the group to write, cannot be <code>null</code>.
+     * @param dos the output stream to write the data to, cannot be <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private void writeGroup(Group group, DataOutputStream dos) throws IOException {
+        dos.writeUTF(group.getName());
+
+        writeDictionary(group.getProperties(), dos);
+        writeDictionary(group.getCredentials(), dos);
+        
+        Role[] m = group.getMembers();
+
+        if (m == null) {
+            dos.writeInt(0);
+        } else {
+            // Write the number of basic members...
+            dos.writeInt(m.length);
+            // Write the names of the basic members...
+            for (int i = 0; i < m.length; i++) {
+                dos.writeUTF(m[i].getName());
+            }
+        }
+
+        m = group.getRequiredMembers();
+        
+        if (m == null) {
+            dos.writeInt(0);
+        } else {
+            // Write the number of required members...
+            dos.writeInt(m.length);
+            // Write the names of the required members...
+            for (int i = 0; i < m.length; i++) {
+                dos.writeUTF(m[i].getName());
+            }
+        }
+    }
+    
+    /**
+     * Writes the given repository to the given output stream.
+     * 
+     * @param repository the repository to write, cannot be <code>null</code>;
+     * @param dos the output stream to write the data to, cannot be <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private void writeRepository(Map repository, DataOutputStream dos) throws IOException {
+        Collection values = repository.values();
+        Iterator valuesIter = values.iterator();
+        
+        // Write the total number of entries in our repository first...
+        dos.writeInt(values.size());
+        
+        while (valuesIter.hasNext()) {
+            Role role = (Role) valuesIter.next();
+            
+            int type = role.getType();
+            
+            dos.writeInt(type);
+            
+            if (Role.GROUP == type) {
+                writeGroup((Group) role, dos);
+            } else if (Role.USER == type) {
+                writeUser((User) role, dos);
+            } else {
+                writeRole(role, dos);
+            }
+        }
+    }
+    
+    /**
+     * Writes a given role to the given output stream.
+     * 
+     * @param role the role to write, cannot be <code>null</code>.
+     * @param dos the output stream to write the data to, cannot be <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private void writeRole(Role role, DataOutputStream dos) throws IOException {
+        dos.writeUTF(role.getName());
+
+        writeDictionary(role.getProperties(), dos);
+    }
+    
+    /**
+     * Writes a given user to the given output stream.
+     * 
+     * @param user the user to write, cannot be <code>null</code>.
+     * @param dos the output stream to write the data to, cannot be <code>null</code>.
+     * @throws IOException in case of I/O problems.
+     */
+    private void writeUser(User user, DataOutputStream dos) throws IOException {
+        dos.writeUTF(user.getName());
+
+        writeDictionary(user.getProperties(), dos);
+        writeDictionary(user.getCredentials(), dos);
+    }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/StubGroupImpl.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/StubGroupImpl.java
new file mode 100644
index 0000000..cb0a768
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/StubGroupImpl.java
@@ -0,0 +1,184 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Properties;
+
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides a stub group that has "weak" references (based on their names) to its basic/required members.
+ */
+final class StubGroupImpl implements Group {
+    
+    private final String m_name;
+    private final List m_basicMembers;
+    private final List m_requiredMembers;
+    private final Dictionary m_credentials;
+    private final Dictionary m_properties;
+
+    /**
+     * Creates a new {@link StubGroupImpl} instance.
+     */
+    public StubGroupImpl(String name) {
+        m_name = name;
+        m_properties = new Properties();
+        m_credentials = new Properties();
+        m_basicMembers = new ArrayList();
+        m_requiredMembers = new ArrayList();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean addMember(Role role) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean addMember(String roleName) {
+        return m_basicMembers.add(roleName);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean addRequiredMember(Role role) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean addRequiredMember(String roleName) {
+        return m_requiredMembers.add(roleName);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((m_name == null) ? 0 : m_name.hashCode());
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        StubGroupImpl other = (StubGroupImpl) obj;
+        if (m_name == null) {
+            if (other.m_name != null) {
+                return false;
+            }
+        } else if (!m_name.equals(other.m_name)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Dictionary getCredentials() {
+        return m_credentials;
+    }
+
+    /**
+     * Returns the names of all basic members.
+     * 
+     * @return a list with basic member names (as String!), never <code>null</code>.
+     */
+    public List getMemberNames() {
+        return m_basicMembers;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Role[] getMembers() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getName() {
+        return m_name;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Dictionary getProperties() {
+        return m_properties;
+    }
+
+    /**
+     * Returns the names of all required members.
+     * 
+     * @return a list with required member names (as String!), never <code>null</code>.
+     */
+    public List getRequiredMemberNames() {
+        return m_requiredMembers;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Role[] getRequiredMembers() {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int getType() {
+        return Role.GROUP;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean hasCredential(String key, Object value) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean removeMember(Role role) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git a/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/osgi/Activator.java b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/osgi/Activator.java
new file mode 100644
index 0000000..edef63c
--- /dev/null
+++ b/useradmin/filestore/src/main/java/org/apache/felix/useradmin/filestore/osgi/Activator.java
@@ -0,0 +1,53 @@
+/**
+ *  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.felix.useradmin.filestore.osgi;
+
+import java.util.Properties;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.apache.felix.useradmin.filestore.RoleRepositoryFileStore;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Registers the {@link RoleRepositoryFileStore} as {@link RoleRepositoryStore} service.
+ */
+public class Activator implements BundleActivator {
+
+    private RoleRepositoryFileStore m_store;
+
+    public void start(BundleContext context) throws Exception {
+        m_store = new RoleRepositoryFileStore(context.getDataFile(""));
+        
+        String[] interfaces = { RoleRepositoryStore.class.getName(), UserAdminListener.class.getName(), ManagedService.class.getName() };
+        
+        Properties props = new Properties();
+        props.put(Constants.SERVICE_PID, RoleRepositoryFileStore.PID);
+
+        context.registerService(interfaces, m_store, props);
+    }
+
+    public void stop(BundleContext context) throws Exception {
+        if (m_store != null) {
+            m_store.stop();
+            m_store = null;
+        }
+    }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/ResettableTimerTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/ResettableTimerTest.java
new file mode 100644
index 0000000..9193054
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/ResettableTimerTest.java
@@ -0,0 +1,201 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import junit.framework.TestCase;
+
+/**
+ * Test case for {@link ResettableTimer}.
+ */
+public class ResettableTimerTest extends TestCase {
+
+    private ResettableTimer m_timer;
+    private CountDownLatch m_latch;
+
+    /**
+     * Tests that a executor service is mandatory.
+     */
+    public void testExecutorSerivceIsMandatory() throws Exception {
+        Runnable task = createStubTask();
+
+        try {
+            new ResettableTimer(null, task, 10, TimeUnit.HOURS);
+            fail("IllegalArgumentException expected!");
+        } catch (Exception e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that multiple calls to {@link ResettableTimer#schedule()} causes 
+     * pending tasks to be cancelled.
+     */
+    public void testScheduleCancelsPendingTasksOk() throws Exception {
+        final AtomicInteger m_counter = new AtomicInteger(0);
+
+        Runnable task = new Runnable() {
+            public void run() {
+                m_counter.incrementAndGet();
+            }
+        };
+        
+        m_timer = new ResettableTimer(task, 100, TimeUnit.MILLISECONDS);
+        m_timer.schedule();
+
+        TimeUnit.MILLISECONDS.sleep(75);
+        
+        Future f = m_timer.schedule();
+        f.get();
+
+        assertEquals(1, m_counter.get());
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.ResettableTimer#schedule()}.
+     */
+    public void testScheduleMultipleTasksOk() throws Exception {
+        final AtomicInteger m_counter = new AtomicInteger(0);
+
+        Runnable task = new Runnable() {
+            public void run() {
+                m_counter.incrementAndGet();
+            }
+        };
+
+        m_timer = new ResettableTimer(task, 100, TimeUnit.MILLISECONDS);
+        
+        Future f = m_timer.schedule();
+        f.get();
+
+        f = m_timer.schedule();
+        f.get();
+
+        assertEquals(2, m_counter.get());
+    }
+
+    /**
+     * Tests that a task is invoked as single shot.
+     */
+    public void testScheduleSingleShotOk() throws Exception {
+        m_latch = new CountDownLatch(1);
+
+        Runnable task = new Runnable() {
+            public void run() {
+                m_latch.countDown();
+            }
+        };
+
+        m_timer = new ResettableTimer(task, 100, TimeUnit.MILLISECONDS);
+        m_timer.schedule();
+
+        assertTrue(m_latch.await(200, TimeUnit.MILLISECONDS));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.ResettableTimer#shutDown()}.
+     */
+    public void testShutDownOk() throws Exception {
+        final AtomicInteger m_counter = new AtomicInteger(0);
+
+        Runnable task = new Runnable() {
+            public void run() {
+                m_counter.incrementAndGet();
+            }
+        };
+        
+        m_timer = new ResettableTimer(task, 100, TimeUnit.MILLISECONDS);
+        m_timer.schedule();
+
+        TimeUnit.MILLISECONDS.sleep(75);
+        
+        m_timer.schedule();
+        m_timer.shutDown();
+
+        assertEquals(1, m_counter.get());
+    }
+
+    /**
+     * Tests that a task is mandatory.
+     */
+    public void testTaskIsMandatory() throws Exception {
+        try {
+            new ResettableTimer(null, 10, TimeUnit.HOURS);
+            fail("IllegalArgumentException expected!");
+        } catch (Exception e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that a timeout cannot be zero.
+     */
+    public void testTimeoutCannotBeNegative() throws Exception {
+        Runnable task = createStubTask();
+
+        try {
+            new ResettableTimer(task, -1, TimeUnit.MILLISECONDS);
+            fail("IllegalArgumentException expected!");
+        } catch (Exception e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that a timeout cannot be zero.
+     */
+    public void testTimeoutCannotBeZero() throws Exception {
+        Runnable task = createStubTask();
+
+        try {
+            new ResettableTimer(task, 0, TimeUnit.MILLISECONDS);
+            fail("IllegalArgumentException expected!");
+        } catch (Exception e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that a time unit is mandatory.
+     */
+    public void testTimeUnitIsMandatory() throws Exception {
+        Runnable task = createStubTask();
+
+        try {
+            new ResettableTimer(task, 10, null);
+            fail("IllegalArgumentException expected!");
+        } catch (Exception e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * @return a {@link Runnable} that does nothing, never <code>null</code>.
+     */
+    private Runnable createStubTask() {
+        return new Runnable() {
+            public void run() {
+                // No-op
+            }
+        };
+    }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStorePerformanceTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStorePerformanceTest.java
new file mode 100644
index 0000000..c19699f
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStorePerformanceTest.java
@@ -0,0 +1,168 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Test case for {@link RoleRepositorySerializer}. 
+ */
+public class RoleRepositoryFileStorePerformanceTest extends TestCase {
+
+    private static final int USER_COUNT = 15000;
+    private static final int GROUP_COUNT = 500;
+
+    private Group[] m_groups;
+    private User[] m_users;
+    
+    private Map m_repository;
+    private RoleRepositoryFileStore m_store;
+
+    /**
+     * Executes the performance test.
+     */
+    public void testPerformanceOk() throws Exception {
+        allocateMemory();
+
+        writeRepositoryPerformanceTest();
+
+        releaseMemory();
+        
+        readRepositoryPerformanceTest();
+    }
+
+    /**
+     * Does a very simple performance test for a large number of users spread over several groups.
+     */
+    protected void readRepositoryPerformanceTest() throws Exception {
+        long r_st = System.currentTimeMillis();
+        Map result = m_store.retrieve();
+        long r_time = System.currentTimeMillis() - r_st;
+
+        assertNotNull(result);
+        assertEquals(GROUP_COUNT + USER_COUNT + 1, result.size());
+
+        System.out.println("Read time : " + (r_time / 1000.0) + "s.");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        m_store = new RoleRepositoryFileStore(new File(System.getProperty("java.io.tmpdir")), false /* disable background writes */);
+
+        m_repository = new HashMap(USER_COUNT + GROUP_COUNT + 1);
+
+        addToRepository(RoleRepository.USER_ANYONE);
+    }
+    
+    /**
+     * Does a very simple performance test for writing a large number of users spread over several groups.
+     */
+    protected void writeRepositoryPerformanceTest() throws Exception {
+        long w_st = System.currentTimeMillis();
+        m_store.store(m_repository);
+        long w_time = System.currentTimeMillis() - w_st;
+
+        System.out.println("Write time: " + (w_time / 1000.0) + "s.");
+    }
+
+    private void addToRepository(Role role) {
+        m_repository.put(role.getName(), role);
+    }
+
+    /**
+     * 
+     */
+    private void allocateMemory() {
+        m_groups = new Group[GROUP_COUNT];
+        for (int i = 0; i < m_groups.length; i++) {
+            m_groups[i] = createGroup(i+1);
+            m_groups[i].addRequiredMember(RoleRepository.USER_ANYONE);
+
+            addToRepository(m_groups[i]);
+        }
+
+        m_users = new User[USER_COUNT];
+        for (int i = 0; i < m_users.length; i++) {
+            m_users[i] = createUser(i+1);
+            
+            int groupIdx = (i % m_groups.length);
+            m_groups[groupIdx].addMember(m_users[i]);
+            
+            addToRepository(m_users[i]);
+        }
+    }
+
+    private Group createGroup(int idx) {
+        String name = "Group" + idx;
+        
+        Group result = new GroupImpl(name);
+
+        setCredentials(result);
+        setProperties(result);
+        
+        return result;
+    }
+
+    private User createUser(int idx) {
+        String name = "User" + idx;
+        
+        User result = new UserImpl(name);
+
+        setCredentials(result);
+        setProperties(result);
+        
+        return result;
+    }
+
+    /**
+     * 
+     */
+    private void releaseMemory() {
+        m_groups = null;
+        m_users = null;
+
+        System.gc();
+        System.gc();
+        System.gc();
+    }
+
+    private void setCredentials(User user) {
+        user.getCredentials().put("name", user.getName());
+        user.getCredentials().put("password", "secret");
+        user.getCredentials().put("certificate", new byte[] { (byte) 0x55, (byte) 0xAA } );
+    }
+    
+    private void setProperties(Role role) {
+        role.getProperties().put("key1", "value1");
+        role.getProperties().put("key2", "hello world".getBytes());
+    }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStoreTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStoreTest.java
new file mode 100644
index 0000000..30754bf
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositoryFileStoreTest.java
@@ -0,0 +1,189 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import java.io.File;
+import java.util.Properties;
+
+import junit.framework.TestCase;
+
+import org.osgi.service.cm.ConfigurationException;
+
+/**
+ * Test cases for {@link RoleRepositoryFileStore}.
+ */
+public class RoleRepositoryFileStoreTest extends TestCase {
+
+    private RoleRepositoryFileStore m_store;
+    
+    /**
+     * Tests that calling updated without the key "background.write.disabled" fails.
+     */
+    public void testUpdateConfigurationWithoutKeyWriteDisabledFail() throws Exception {
+        try {
+            m_store.updated(new Properties());
+            fail("ConfigurationException expected!");
+        } catch (ConfigurationException e) {
+            // Ok; expected
+        }
+    }
+    
+    /**
+     * Tests that calling updated with the key "background.write.disabled" set to "false" succeeds.
+     */
+    public void testUpdateConfigurationWithKeyWriteDisabledOk() throws Exception {
+        Properties properties = new Properties();
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "true");
+
+        m_store.updated(properties);
+    }
+    
+    /**
+     * Tests that calling updated with the key "background.write.disabled" set to a numeric value fails.
+     */
+    public void testUpdateConfigurationWithKeyWriteDisabledInvalidValueFail() throws Exception {
+        Properties properties = new Properties();
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, Integer.valueOf(1));
+
+        try {
+            m_store.updated(properties);
+            fail("ConfigurationException expected!");
+        } catch (ConfigurationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that calling updated without the key "background.write.delay.value" fails.
+     */
+    public void testUpdateConfigurationWithoutKeyWriteDelayValueFail() throws Exception {
+        Properties properties = new Properties();
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+        try {
+            m_store.updated(properties);
+            fail("ConfigurationException expected!");
+        } catch (ConfigurationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that calling updated with the key "background.write.delay.value" set to a non-numeric value fails.
+     */
+    public void testUpdateConfigurationWithKeyWriteDelayValueInvalidValueTypeFail() throws Exception {
+        Properties properties = new Properties();
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "seconds");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+        try {
+            m_store.updated(properties);
+            fail("ConfigurationException expected!");
+        } catch (ConfigurationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that calling updated with the key "background.write.delay.value" set to zero fails.
+     */
+    public void testUpdateConfigurationWithKeyWriteDelayValueZeroValueFail() throws Exception {
+        Properties properties = new Properties();
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "0");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+        try {
+            m_store.updated(properties);
+            fail("ConfigurationException expected!");
+        } catch (ConfigurationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that calling updated with the key "background.write.delay.value" set to a negative value fails.
+     */
+    public void testUpdateConfigurationWithKeyWriteDelayValueNegativeValueFail() throws Exception {
+        Properties properties = new Properties();
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "-1");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+        try {
+            m_store.updated(properties);
+            fail("ConfigurationException expected!");
+        } catch (ConfigurationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that calling updated without the key "background.write.delay.timeunit" succeeds as it is optional.
+     */
+    public void testUpdateConfigurationWithoutKeyWriteDelayTimeUnitOk() throws Exception {
+        Properties properties = new Properties();
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "1");
+
+        m_store.updated(properties);
+    }
+
+    /**
+     * Tests that calling updated with the key "background.write.delay.timeunit" set to an invalid value fails.
+     */
+    public void testUpdateConfigurationWithKeyWriteDelayTimeUnitInvalidValueFail() throws Exception {
+        Properties properties = new Properties();
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "1");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "1");
+
+        try {
+            m_store.updated(properties);
+            fail("ConfigurationException expected!");
+        } catch (ConfigurationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that calling updated with all keys succeeds.
+     */
+    public void testUpdateConfigurationWithAllKeysOk() throws Exception {
+        Properties properties = new Properties();
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DISABLED, "false");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_VALUE, "1");
+        properties.put(RoleRepositoryFileStore.KEY_WRITE_DELAY_TIMEUNIT, "seconds");
+
+        m_store.updated(properties);
+    }
+    
+    /**
+     * Tests that calling updated with a <code>null</code>-dictionary causes the default settings to be applied.
+     */
+    public void testUpdateConfigurationWithoutPropertiesOk() throws Exception {
+        m_store.updated(null);
+    }
+
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        m_store = new RoleRepositoryFileStore(new File(System.getProperty("java.io.tmpdir")), false /* disable background writes */);
+    }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializerTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializerTest.java
new file mode 100644
index 0000000..5f1aadb
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/RoleRepositorySerializerTest.java
@@ -0,0 +1,413 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Test case for {@link RoleRepositorySerializer}. 
+ */
+public class RoleRepositorySerializerTest extends TestCase {
+
+    private Role m_userAnyone;
+    private User m_user1;
+    private User m_user2;
+    private User m_user3;
+    private User m_user4;
+    private Group m_group1;
+    private Group m_group2;
+    private Group m_group3;
+    private Map m_repository;
+    
+    private RoleRepositorySerializer m_serializer;
+
+    /**
+     * Tests that writing and reading a repository with a role, four users and a three group works as expected.
+     */
+    public void testRWRepositoryWithRolesUsersAndGroupsOk() throws Exception {
+        m_group1.addRequiredMember(m_userAnyone);
+        m_group1.addMember(m_user1);
+
+        m_group2.addMember(m_group1);
+        m_group2.addMember(m_user2);
+        m_group2.addRequiredMember(m_user1);
+        
+        m_group3.addMember(m_user3);
+        m_group3.addMember(m_user4);
+        m_group3.addRequiredMember(m_userAnyone);
+        
+        addToRepository(m_userAnyone);
+        addToRepository(m_user1);
+        addToRepository(m_user2);
+        addToRepository(m_user3);
+        addToRepository(m_user4);
+        addToRepository(m_group1);
+        addToRepository(m_group2);
+        addToRepository(m_group3);
+        
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(baos);
+        
+        m_serializer.serialize(m_repository, dos); // should succeed!
+        
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        DataInputStream dis = new DataInputStream(bais);
+        
+        Map result = m_serializer.deserialize(dis);
+        assertNotNull(result);
+
+        assertEquals(8, result.size());
+        assertEquals(m_userAnyone, (Role) result.get(m_userAnyone.getName()));
+        assertEquals(m_user1, (User) result.get(m_user1.getName()));
+        assertEquals(m_user2, (User) result.get(m_user2.getName()));
+        assertEquals(m_user3, (User) result.get(m_user3.getName()));
+        assertEquals(m_user4, (User) result.get(m_user4.getName()));
+        assertEquals(m_group1, (Group) result.get(m_group1.getName()));
+        assertEquals(m_group2, (Group) result.get(m_group2.getName()));
+        assertEquals(m_group3, (Group) result.get(m_group3.getName()));
+    }
+
+    /**
+     * Tests that writing and reading a repository with a single group works as expected.
+     */
+    public void testRWRepositoryWithSingleGroupOk() throws Exception {
+        addToRepository(m_group1);
+        
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(baos);
+        
+        m_serializer.serialize(m_repository, dos); // should succeed!
+
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        DataInputStream dis = new DataInputStream(bais);
+        
+        Map result = m_serializer.deserialize(dis);
+        assertNotNull(result);
+        
+        assertEquals(1, result.size());
+
+        assertEquals(m_group1, (Role) result.get(m_group1.getName()));
+    }
+
+    /**
+     * Tests that writing and reading a repository with a single role works as expected.
+     */
+    public void testRWRepositoryWithSingleRoleOk() throws Exception {
+        addToRepository(m_userAnyone);
+        
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(baos);
+        
+        m_serializer.serialize(m_repository, dos); // should succeed!
+        
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        DataInputStream dis = new DataInputStream(bais);
+        
+        Map result = m_serializer.deserialize(dis);
+        assertNotNull(result);
+        
+        assertEquals(1, result.size());
+
+        assertEquals(m_userAnyone, (Role) result.get(m_userAnyone.getName()));
+    }
+
+    /**
+     * Tests that writing and reading a repository with a single user role works as expected.
+     */
+    public void testRWRepositoryWithSingleUserOk() throws Exception {
+        addToRepository(m_user1);
+        
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(baos);
+        
+        m_serializer.serialize(m_repository, dos); // should succeed!
+        
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        DataInputStream dis = new DataInputStream(bais);
+        
+        Map result = m_serializer.deserialize(dis);
+        assertNotNull(result);
+        
+        assertEquals(1, result.size());
+
+        assertEquals(m_user1, (User) result.get(m_user1.getName()));
+    }
+
+    /**
+     * Tests that writing and reading a repository with two user roles works as expected.
+     */
+    public void testRWRepositoryWithTwoUsersOk() throws Exception {
+        addToRepository(m_user1);
+        addToRepository(m_user2);
+        
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(baos);
+        
+        m_serializer.serialize(m_repository, dos); // should succeed!
+        
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        DataInputStream dis = new DataInputStream(bais);
+        
+        Map result = m_serializer.deserialize(dis);
+        assertNotNull(result);
+        
+        assertEquals(2, result.size());
+        assertEquals(m_user1, (User) result.get(m_user1.getName()));
+        assertEquals(m_user2, (User) result.get(m_user2.getName()));
+    }
+
+    /**
+     * Tests that reading a repository that has a cyclic group reference can be read ok.
+     */
+    public void testRWRepositoryWithCyclicGroupReference() throws Exception {
+        m_group1.addMember(m_group2);
+        m_group2.addMember(m_group1);
+        m_group2.addMember(m_user1);
+
+        addToRepository(m_user1);
+        addToRepository(m_group2);
+        addToRepository(m_group1);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(baos);
+        
+        m_serializer.serialize(m_repository, dos); // should succeed!
+        
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        DataInputStream dis = new DataInputStream(bais);
+        
+        Map result = m_serializer.deserialize(dis);
+        assertNotNull(result);
+        
+        assertEquals(3, result.size());
+        assertEquals(m_group1, (Group) result.get(m_group1.getName()));
+        assertEquals(m_group2, (Group) result.get(m_group2.getName()));
+        assertEquals(m_user1, (User) result.get(m_user1.getName()));
+    }
+
+    /**
+     * Tests that reading a repository that is missing a referenced basic role from a group fails.
+     */
+    public void testRWRepositoryWithUnreferencedBasicRoleInGroupFail() throws Exception {
+        m_group1.addMember(m_userAnyone);
+        m_group1.addRequiredMember(m_user1);
+
+        // "Forget" to add the user.anyone!
+        addToRepository(m_user1);
+        addToRepository(m_group1);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(baos);
+        
+        m_serializer.serialize(m_repository, dos); // should succeed!
+        
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        DataInputStream dis = new DataInputStream(bais);
+        
+        try {
+            m_serializer.deserialize(dis);
+            
+            fail("IOException expected!");
+        } catch (IOException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that reading a repository that is missing a referenced required role from a group fails.
+     */
+    public void testRWRepositoryWithUnreferencedRequiredRoleInGroupFail() throws Exception {
+        m_group1.addRequiredMember(m_userAnyone);
+        m_group1.addMember(m_user1);
+        
+        // "Forget" to add the user.anyone!
+        addToRepository(m_user1);
+        addToRepository(m_group1);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream dos = new DataOutputStream(baos);
+        
+        m_serializer.serialize(m_repository, dos); // should succeed!
+        
+        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+        DataInputStream dis = new DataInputStream(bais);
+        
+        try {
+            m_serializer.deserialize(dis);
+            
+            fail("IOException expected!");
+        } catch (IOException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        m_serializer = new RoleRepositorySerializer();
+
+        m_repository = new HashMap();
+
+        m_userAnyone = RoleRepository.USER_ANYONE;
+        
+        setProperties(m_userAnyone);
+        
+        m_user1 = createUser(1);
+        m_user2 = createUser(2);
+        m_user3 = createUser(3);
+        m_user4 = createUser(4);
+
+        m_group1 = createGroup(1);
+        m_group2 = createGroup(2);
+        m_group3 = createGroup(3);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+    
+    private void addToRepository(Role role) {
+        m_repository.put(role.getName(), role);
+    }
+    
+    private void assertEquals(Dictionary expected, Dictionary obtained) {
+        assertNotNull(expected);
+        assertNotNull(obtained);
+        
+        assertEquals(expected.size(), obtained.size());
+        
+        Enumeration enumeration = expected.keys();
+        while (enumeration.hasMoreElements()) {
+            Object expectedKey = enumeration.nextElement();
+            Object expectedValue = expected.get(expectedKey);
+
+            Object obtainedValue = obtained.get(expectedKey);
+            if (expectedValue == null) {
+                assertNull(obtainedValue);
+            } else if (expectedValue.getClass().isArray()) {
+                assertTrue(Arrays.equals((byte[]) expectedValue, (byte[]) obtainedValue));
+            } else {
+                assertEquals(expectedValue, obtainedValue);
+            }
+        }
+    }
+
+    private void assertEquals(Group expected, Group obtained) {
+        assertEquals((Object) expected, (Object) obtained);
+        
+        assertEquals(expected.getProperties(), obtained.getProperties());
+        assertEquals(expected.getCredentials(), obtained.getCredentials());
+        assertEquals(expected.getMembers(), obtained.getMembers());
+        assertEquals(expected.getRequiredMembers(), obtained.getRequiredMembers());
+    }
+
+    private void assertEquals(Role expected, Role obtained) {
+        assertEquals((Object) expected, (Object) obtained);
+        
+        assertEquals(expected.getProperties(), obtained.getProperties());
+    }
+
+    private void assertEquals(User expected, User obtained) {
+        assertEquals((Object) expected, (Object) obtained);
+        
+        assertEquals(expected.getProperties(), obtained.getProperties());
+        assertEquals(expected.getCredentials(), obtained.getCredentials());
+    }
+
+    private void assertEquals(Role[] expected, Role[] obtained) {
+        if (expected != null) {
+            assertNotNull(obtained);
+            assertEquals(expected.length, obtained.length);
+            
+            List e = new ArrayList(Arrays.asList(expected));
+            e.removeAll(Arrays.asList(obtained));
+            
+            assertTrue("Roles not obtained: " + e, e.isEmpty());
+            
+            Map m = new HashMap();
+            for (int i = 0; i < expected.length; i++) {
+                m.put(expected[i].getName(), expected[i]);
+            }
+            
+            for (int i = 0; i < obtained.length; i++) {
+                assertEquals(m.get(obtained[i].getName()), obtained[i]);
+            }
+        } else {
+            assertNull(obtained);
+        }
+    }
+
+    private User createUser(int idx) {
+        String name = "User" + idx;
+        
+        User result = new UserImpl(name);
+
+        setCredentials(result);
+        setProperties(result);
+        
+        return result;
+    }
+
+    private Group createGroup(int idx) {
+        String name = "Group" + idx;
+        
+        Group result = new GroupImpl(name);
+
+        setCredentials(result);
+        setProperties(result);
+        
+        return result;
+    }
+
+    private void setCredentials(User user) {
+        user.getCredentials().put(user.getName(), user.getName());
+        user.getCredentials().put("password", user.getName());
+        user.getCredentials().put("certificate", new byte[] { (byte) 0x55, (byte) 0xAA } );
+    }
+
+    private void setProperties(Role role) {
+        role.getProperties().put(role.getName(), role.getName());
+        role.getProperties().put("key1", role.getName());
+        role.getProperties().put("key2", "hello world".getBytes());
+    }
+}
diff --git a/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/StubGroupImplTest.java b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/StubGroupImplTest.java
new file mode 100644
index 0000000..b19d53e
--- /dev/null
+++ b/useradmin/filestore/src/test/java/org/apache/felix/useradmin/filestore/StubGroupImplTest.java
@@ -0,0 +1,125 @@
+/**
+ *  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.felix.useradmin.filestore;
+
+import junit.framework.TestCase;
+
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Test cases for {@link StubGroupImpl}.
+ */
+public class StubGroupImplTest extends TestCase {
+
+    /**
+     * Tests that {@link StubGroupImpl#addMember(Role)} always fails.
+     */
+    public void testAddMemberRoleAlwaysFails() {
+        try {
+            new StubGroupImpl("test").addMember((Role) null);
+            fail("Expected UnsupportedOperationException!");
+        } catch (UnsupportedOperationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that {@link StubGroupImpl#addRequiredMember(Role)} always fails.
+     */
+    public void testAddRequiredMemberRoleAlwaysFails() {
+        try {
+            new StubGroupImpl("test").addRequiredMember((Role) null);
+            fail("Expected UnsupportedOperationException!");
+        } catch (UnsupportedOperationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that {@link StubGroupImpl#getMembers()} always fails.
+     */
+    public void testGetMembersAlwaysFails() {
+        try {
+            new StubGroupImpl("test").getMembers();
+            fail("Expected UnsupportedOperationException!");
+        } catch (UnsupportedOperationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that {@link StubGroupImpl#getRequiredMembers()} always fails.
+     */
+    public void testGetRequiredMembersAlwaysFails() {
+        try {
+            new StubGroupImpl("test").getRequiredMembers();
+            fail("Expected UnsupportedOperationException!");
+        } catch (UnsupportedOperationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that {@link StubGroupImpl#hasCredential(String, Object)} always fails.
+     */
+    public void testHasCredentialAlwaysFails() {
+        try {
+            new StubGroupImpl("test").hasCredential("foo", "bar");
+            fail("Expected UnsupportedOperationException!");
+        } catch (UnsupportedOperationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that {@link StubGroupImpl#removeMember(Role)} always fails.
+     */
+    public void testRemoveMemberAlwaysFails() {
+        try {
+            new StubGroupImpl("test").removeMember((Role) null);
+            fail("Expected UnsupportedOperationException!");
+        } catch (UnsupportedOperationException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that {@link StubGroupImpl#hashCode()} and {@link StubGroupImpl#equals(Object)} works.
+     */
+    public void testHashCodeAndEqualsOk() {
+        StubGroupImpl g1 = new StubGroupImpl("foo");
+        StubGroupImpl g2 = new StubGroupImpl("bar");
+        StubGroupImpl g3 = new StubGroupImpl("foo");
+        StubGroupImpl g4 = new StubGroupImpl(null);
+
+        assertTrue(g1.hashCode() == g3.hashCode());
+        assertFalse(g1.hashCode() == g2.hashCode());
+        assertFalse(g1.hashCode() == g4.hashCode());
+
+        assertTrue(g1.equals(g3));
+        assertTrue(g3.equals(g1));
+        assertFalse(g1.equals(g2));
+        assertFalse(g1.equals(g4));
+
+        assertTrue(g4.equals(g4));
+        assertFalse(g4.equals(g1));
+
+        assertFalse(g1.equals(null));
+        assertFalse(g1.equals("qux"));
+    }
+}
diff --git a/useradmin/mongodb/pom.xml b/useradmin/mongodb/pom.xml
new file mode 100644
index 0000000..1d110b1
--- /dev/null
+++ b/useradmin/mongodb/pom.xml
@@ -0,0 +1,93 @@
+<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">
+	<modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>felix-parent</artifactId>
+        <groupId>org.apache.felix</groupId>
+        <version>1.2.0</version>
+        <relativePath>../../../pom/pom.xml</relativePath>
+    </parent>
+    <artifactId>org.apache.felix.useradmin.mongodb</artifactId>
+	<version>1.0.0</version>
+    <packaging>bundle</packaging>
+    <description>Provides a MongoDB store implementation for the User Admin service.</description>
+    <dependencies>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+            <version>4.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+            <version>4.0.0</version>
+        </dependency>
+        <dependency>
+        	<groupId>org.mongodb</groupId>
+        	<artifactId>mongo-java-driver</artifactId>
+        	<version>2.8.0</version>
+        </dependency>
+        <dependency>
+        	<groupId>org.apache.felix</groupId>
+        	<artifactId>org.apache.felix.useradmin</artifactId>
+        	<version>1.0.1</version>
+        	<type>bundle</type>
+        </dependency>
+    </dependencies>
+	<build>
+		<plugins>
+			<plugin>
+				<artifactId>maven-compiler-plugin</artifactId>
+				<configuration>
+					<source>1.6</source>
+					<target>1.6</target>
+				</configuration>
+			</plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Bundle-Name>Apache Felix User Admin Service MongoDB repository</Bundle-Name>
+                        <Bundle-Description>
+                            A MongoDB repository implementation for User Admin Compendium Service
+                        </Bundle-Description>
+                        <Bundle-Activator>
+                            ${project.artifactId}.osgi.Activator
+                        </Bundle-Activator>
+                        <Bundle-SymbolicName>
+                            ${project.artifactId}
+                        </Bundle-SymbolicName>
+                        <Bundle-Vendor>The Apache Software Foundation</Bundle-Vendor>
+                        <Import-Package>
+                             org.osgi.service.useradmin; version="[1.1,1.2)",
+                             *
+                        </Import-Package>
+                        <Export-Package>
+                        </Export-Package>
+                        <Private-Package>
+                            ${project.artifactId}.*
+                        </Private-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>rat-maven-plugin</artifactId>
+                <configuration>
+                    <excludeSubProjects>false</excludeSubProjects>
+                    <useEclipseDefaultExcludes>true</useEclipseDefaultExcludes>
+                    <useMavenDefaultExcludes>true</useMavenDefaultExcludes>
+                    <excludes>
+                        <param>doc/*</param>
+                        <param>maven-eclipse.xml</param>
+                        <param>.checkstyle</param>
+                        <param>.externalToolBuilders/*</param>
+                    </excludes>
+                </configuration>
+            </plugin>
+		</plugins>
+	</build>
+</project>
\ No newline at end of file
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/KeyCodec.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/KeyCodec.java
new file mode 100644
index 0000000..fe6cdec
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/KeyCodec.java
@@ -0,0 +1,143 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.felix.useradmin.mongodb;
+
+
+/**
+ * Provides a small utility class for encoding/decoding keys used directly in 
+ * MongoDB.
+ * <p>
+ * MongoDB does not allow keys to have '.' and '$' (amongst others?) in their 
+ * name, so we need to ensure that those characters are somehow encoded.
+ * </p>
+ * 
+ * @see http://www.mongodb.org/display/DOCS/Legal+Key+Names
+ * @see http://www.yoyobrain.com/flashcards/show/261984
+ */
+final class KeyCodec {
+    
+    private static final String ENCODED_NULL = "%00";
+    private static final String ENCODED_UNDERSCORE = "%5F";
+    private static final String ENCODED_DOT = "%2E";
+    private static final String ENCODED_DOLLAR = "%24"; 
+    
+    /**
+     * Encodes a given key by replacing all '.' and '$' with their URL encoded variants.
+     * 
+     * @param input the input to encode, may be <code>null</code>.
+     * @return the encoded input, can be <code>null</code>.
+     */
+    public static String encode(String input) {
+        if (input == null) {
+            return null;
+        }
+        StringBuilder sb = new StringBuilder(input.length());
+        for (int i = 0; i < input.length(); i++) {
+            char c = input.charAt(i);
+            if (c == 0) {
+                sb.append(ENCODED_NULL);
+            } else if (c == '_') {
+                sb.append(ENCODED_UNDERSCORE);
+            } else if (c == '.') {
+                sb.append(ENCODED_DOT);
+            } else if (c == '$') {
+                sb.append(ENCODED_DOLLAR);
+            } else if (c == '%') {
+                // escape all '%' as well...
+                sb.append("%%");
+            } else {
+                sb.append((char) c);
+            }
+        }
+        return sb.toString();
+    }
+    
+    /**
+     * Decodes a given key by replacing all URL encoded '.' and '$' entities with their real characters.
+     * 
+     * @param input the input to decode, may be <code>null</code>.
+     * @return the decoded input, can be <code>null</code>.
+     */
+    public static String decode(String input) {
+        if (input == null) {
+            return null;
+        }
+        
+        boolean percentSeen = false;
+        int length = input.length();
+        char oldC = input.charAt(0);
+        
+        StringBuilder sb = new StringBuilder(input.length());
+        for (int i = 1; i < length; i++) {
+            char c = input.charAt(i);
+            if (oldC == '%') {
+                if (c == '%') {
+                    // Escaped percent...
+                    sb.append("%");
+                    c = 0; // erase percent...
+                    percentSeen = false;
+                } else {
+                    percentSeen = true;
+                }
+            } else if (c == '%') {
+                percentSeen = true;
+            } else if (percentSeen) {
+                if (oldC == '0' && c == '0') {
+                    // Encoded null-character
+                    sb.append((char) 0);
+                    percentSeen = false;
+                } else if (oldC == '2' && c == '4') {
+                    // Encoded dollar...
+                    sb.append("$");
+                    percentSeen = false;
+                } else if (oldC == '2' && c == 'E') {
+                    // Encoded dot...
+                    sb.append(".");
+                    percentSeen = false;
+                } else if (oldC == '5' && c == 'F') {
+                    // Encoded underscore...
+                    sb.append("_");
+                    percentSeen = false;
+                } else {
+                    // Unknown encoded entity...
+                    sb.append("%").append(oldC).append(c);
+                    percentSeen = false;
+                }
+            } else {
+                if (i == 1) {
+                    sb.append(oldC);
+                }
+                sb.append(c);
+            }
+
+            if (percentSeen && (i == length - 1)) {
+                // At the end; incomplete entity found...
+                if (oldC != '%' && c != '%') {
+                    sb.append("%").append(oldC).append(c);
+                } else if (oldC == '%' && c != '%') {
+                    sb.append("%").append(c);
+                } else if (c == '%') {
+                    sb.append("%");
+                }
+            }
+
+            oldC = c;
+        }
+
+        return sb.toString();
+    }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDB.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDB.java
new file mode 100644
index 0000000..4eafab3
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDB.java
@@ -0,0 +1,155 @@
+/**
+ *  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.felix.useradmin.mongodb;
+
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import com.mongodb.DB;
+import com.mongodb.DBCollection;
+import com.mongodb.Mongo;
+import com.mongodb.MongoException;
+import com.mongodb.ServerAddress;
+
+/**
+ * Provides a simple facade for accessing MongoDB.
+ */
+final class MongoDB {
+
+    private final List<ServerAddress> m_servers;
+    private final String m_dbName;
+    private final String m_collectionName;
+    
+    private final AtomicReference<Mongo> m_mongoRef;
+    
+    /**
+     * Creates a new {@link MongoDB} instance.
+     * 
+     * @param serverNames the space separated list of Mongo servers, cannot be <code>null</code> or empty;
+     * @param dbName the name of the MongoDB to connect to, cannot be <code>null</code> or empty; 
+     * @param collectionName the name of the collection to use, cannot be <code>null</code> or empty.
+     */
+    public MongoDB(String serverNames, String dbName, String collectionName) {
+        if (serverNames == null || "".equals(serverNames.trim())) {
+            throw new IllegalArgumentException("ServerNames cannot be null or empty!");
+        }
+        if (dbName == null || "".equals(dbName.trim())) {
+            throw new IllegalArgumentException("DbName cannot be null or empty!");
+        }
+        if (collectionName == null || "".equals(collectionName.trim())) {
+            throw new IllegalArgumentException("CollectionName cannot be null or empty!");
+        }
+        
+        m_mongoRef = new AtomicReference<Mongo>();
+
+        m_servers = parseServers(serverNames);
+        m_dbName = dbName;
+        m_collectionName = collectionName;
+    }
+
+    /**
+     * Parses the space separated list of server names.
+     * 
+     * @param serverNames the server names, cannot be <code>null</code>.
+     * @return a list of {@link ServerAddress}es to connect to, never <code>null</code>.
+     * @throws IllegalArgumentException in case the given server names was invalid.
+     */
+    private static List<ServerAddress> parseServers(String serverNames) {
+        String[] parts = serverNames.split("\\s+");
+
+        List<ServerAddress> servers = new ArrayList<ServerAddress>();
+        for (int i = 0; i < parts.length; i++) {
+            String part = parts[i];
+            try {
+                int colonPos = part.indexOf(":");
+                if (colonPos > 0 && (colonPos < part.length() - 1)) {
+                    String name = part.substring(0, colonPos);
+                    String portStr = part.substring(colonPos + 1);
+                    servers.add(new ServerAddress(name, Integer.valueOf(portStr)));
+                }
+            }
+            catch (NumberFormatException e) {
+                throw new IllegalArgumentException("Illegal port number in: " + part);
+            }
+            catch (UnknownHostException e) {
+                throw new IllegalArgumentException("Unknown host: " + part);
+            }
+        }
+
+        if (servers.isEmpty()) {
+            throw new IllegalArgumentException("No (valid) servers defined!");
+        }
+
+        return servers;
+    }
+
+    /**
+     * Connects to the MongoDB with the supplied credentials.
+     * 
+     * @param userName the optional user name to use;
+     * @param password the optional password to use.
+     * @return <code>true</code> if the connection was succesful, <code>false</code> otherwise.
+     */
+    public boolean connect(String userName, String password) {
+        Mongo newMongo = new Mongo(m_servers);
+
+        Mongo oldMongo;
+        do {
+            oldMongo = m_mongoRef.get();
+        } while (!m_mongoRef.compareAndSet(oldMongo, newMongo));
+        
+        DB db = newMongo.getDB(m_dbName);
+        if ((userName != null) && (password != null)) {
+            if (!db.authenticate(userName, password.toCharArray())) {
+                return false;
+            }
+        }
+        
+        return true;
+    }
+    
+    /**
+     * Returns the database collection to work in.
+     * 
+     * @return the {@link DBCollection}, never <code>null</code>.
+     * @throws MongoException in case no connection to Mongo exists.
+     */
+    public DBCollection getCollection() {
+        Mongo mongo = m_mongoRef.get();
+        if (mongo == null) {
+            throw new MongoException("Not connected to MongoDB!");
+        }
+        DB db = mongo.getDB(m_dbName);
+        return db.getCollection(m_collectionName);
+    }
+
+    /**
+     * Disconnects from the MongoDB.
+     */
+    public void disconnect() {
+        Mongo mongo = m_mongoRef.get();
+        if (mongo != null) {
+            try {
+                mongo.close();
+            } finally {
+                m_mongoRef.compareAndSet(mongo, null);
+            }
+        }
+    }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDBStore.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDBStore.java
new file mode 100644
index 0000000..18d722c
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoDBStore.java
@@ -0,0 +1,429 @@
+/**
+ *  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.felix.useradmin.mongodb;
+
+import static org.apache.felix.useradmin.mongodb.MongoSerializerHelper.NAME;
+import static org.apache.felix.useradmin.mongodb.MongoSerializerHelper.TYPE;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.log.LogService;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBCollection;
+import com.mongodb.DBCursor;
+import com.mongodb.DBObject;
+import com.mongodb.MongoException;
+import com.mongodb.WriteResult;
+
+/**
+ * Provides a repository store that uses MongoDB for storing the role information.
+ * <p>
+ * This service can also be configured at runtime by using the PID {@value #PID}.<br/>
+ * The configuration options recognized by this service are:
+ * </p>
+ * <dl>
+ * <dt>"useradmin.mongodb.server"</dt>
+ * <dd>A space separated string containing the MongoDB servers. The format for this string is: "<code>&lt;host1:port1&gt; &lt;host2:port2&gt;</code>". This value is mandatory;</dd>
+ * <dt>"useradmin.mongodb.name"</dt>
+ * <dd>A string containing the name of the database to use for this store. This value is mandatory;</dd>
+ * <dt>"useradmin.mongodb.collection"</dt>
+ * <dd>The name of the database collection to use for this store. This value is mandatory;</dd>
+ * <dt>"useradmin.mongodb.username"</dt>
+ * <dd>An optional string value representing the name of the user to authenticate against MongoDB;</dd>
+ * <dt>"useradmin.mongodb.password"</dt>
+ * <dd>An optional string value representing the password to authenticate against MongoDB.</dd>
+ * </dl>
+ * <p>
+ * Alternatively, one can also supply the above mentioned configuration keys as system properties. However,
+ * this implies that only a single store can be configured on a system!
+ * </p>
+ * <p>
+ * By default, the following values are used:
+ * </p>
+ * <table>
+ * <tr><td>"<tt>useradmin.mongodb.server</tt>"</td><td>"<tt>localhost:27017</tt>"</td></tr>
+ * <tr><td>"<tt>useradmin.mongodb.name</tt>"</td><td>"<tt>ua_repo</tt>"</td></tr>
+ * <tr><td>"<tt>useradmin.mongodb.collection</tt>"</td><td>"<tt>useradmin</tt>"</td></tr>
+ * <tr><td>"<tt>useradmin.mongodb.username</tt>"</td><td>&lt;none&gt;</td></tr>
+ * <tr><td>"<tt>useradmin.mongodb.password</tt>"</td><td>&lt;none&gt;</td></tr>
+ * </table>
+ * <p>
+ * This class is thread-safe.
+ * </p>
+ */
+public class MongoDBStore implements RoleProvider, RoleRepositoryStore, UserAdminListener, ManagedService {
+    
+    /** The PID for the managed service reference. */
+    public static final String PID = "org.apache.felix.useradmin.mongodb"; 
+                    
+    /** 
+     * A space-separated array with server definitions to access MongoDB. 
+     * Format = "&lt;host1:port1&gt; &lt;host2:port2&gt;". 
+     * */
+    private static final String KEY_MONGODB_SERVER = "useradmin.mongodb.server";
+    /** The name of the MongoDB database instance. */
+    private static final String KEY_MONGODB_NAME = "useradmin.mongodb.name";
+    /** The username of the MongoDB database instance. */
+    private static final String KEY_MONGODB_USERNAME = "useradmin.mongodb.username";
+    /** The password of the MongoDB database instance. */
+    private static final String KEY_MONGODB_PASSWORD = "useradmin.mongodb.password";
+    /** The name of the MongoDB collection to use. */
+    private static final String KEY_MONGODB_COLLECTION_NAME = "useradmin.mongodb.collection";
+
+    /** Default MongoDB server; first checks a system property */
+    private static final String DEFAULT_MONGODB_SERVER = System.getProperty(KEY_MONGODB_SERVER, "localhost:27017");
+    /** Default MongoDB name */
+    private static final String DEFAULT_MONGODB_NAME = System.getProperty(KEY_MONGODB_NAME, "ua_repo");
+    /** Default MongoDB collection */
+    private static final String DEFAULT_MONGODB_COLLECTION = System.getProperty(KEY_MONGODB_COLLECTION_NAME, "useradmin");
+    /** Default MongoDB username */
+    private static final String DEFAULT_MONGODB_USERNAME = System.getProperty(KEY_MONGODB_USERNAME);
+    /** Default MongoDB password */
+    private static final String DEFAULT_MONGODB_PASSWORD = System.getProperty(KEY_MONGODB_PASSWORD);
+
+    private final AtomicReference<MongoDB> m_mongoDbRef;
+    private final MongoSerializerHelper m_helper;
+    
+    private volatile LogService m_log;
+
+    /**
+     * Creates a new {@link MongoDBStore} instance.
+     */
+    public MongoDBStore() {
+        m_mongoDbRef = new AtomicReference<MongoDB>();
+        m_helper = new MongoSerializerHelper(this);
+    }
+
+    @Override
+    public boolean addRole(Role role) throws IOException {
+        if (role == null) {
+            throw new IllegalArgumentException("Role cannot be null!");
+        }
+        
+        try {
+            DBCollection coll = getCollection();
+            
+            DBCursor cursor = coll.find(getTemplateObject(role));
+            try {
+                if (cursor.hasNext()) {
+                    // Role already exists...
+                    return false;
+                }
+            } finally {
+                cursor.close();
+            }
+            
+            // Role does not exist; insert it...
+            DBObject data = m_helper.serialize(role);
+            
+            WriteResult result = coll.insert(data);
+            
+            if (result.getLastError() != null) {
+                result.getLastError().throwOnError();
+            }
+
+            return true;
+        }
+        catch (MongoException e) {
+            m_log.log(LogService.LOG_WARNING, "Add role failed!", e);
+            throw new IOException("AddRole failed!", e);
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        MongoDB mongoDB = m_mongoDbRef.get();
+        if (mongoDB != null) {
+            mongoDB.disconnect();
+        }
+    }
+
+    @Override
+    public void flush() throws IOException {
+        // Nop
+    }
+
+    @Override
+    public Role[] getAllRoles() throws IOException {
+        try {
+            List<Role> roles = new ArrayList<Role>();
+            
+            DBCollection coll = getCollection();
+
+            DBCursor cursor = coll.find();
+            try {
+                while (cursor.hasNext()) {
+                    roles.add(m_helper.deserialize(cursor.next()));
+                }
+            } finally {
+                cursor.close();
+            }
+
+            return roles.toArray(new Role[roles.size()]);
+        }
+        catch (MongoException e) {
+            m_log.log(LogService.LOG_WARNING, "Get all roles failed!", e);
+            throw new IOException("GetAllRoles failed!", e);
+        }
+    }
+
+    @Override
+    public Role getRole(String name) {
+        DBCollection coll = getCollection();
+
+        DBCursor cursor = coll.find(getTemplateObject(name));
+        try {
+            if (cursor.hasNext()) {
+                return m_helper.deserialize(cursor.next());
+            }
+        } finally {
+            cursor.close();
+        }
+
+        return null;
+    }
+
+    @Override
+    public Role getRoleByName(String name) throws IOException {
+        try {
+            return getRole(name);
+        }
+        catch (MongoException e) {
+            m_log.log(LogService.LOG_WARNING, "Get role by name failed!", e);
+            throw new IOException("GetRoleByName failed!", e);
+        }
+    }
+    
+    @Override
+    public void initialize() throws IOException {
+        // Check whether we need to connect to MongoDB, or that this is
+        // already done by the #updated method...
+        MongoDB oldMongoDB = m_mongoDbRef.get();
+        if (oldMongoDB == null) {
+            MongoDB mongoDB = new MongoDB(DEFAULT_MONGODB_SERVER, DEFAULT_MONGODB_NAME, DEFAULT_MONGODB_COLLECTION);
+            
+            do {
+                oldMongoDB = m_mongoDbRef.get();
+            } 
+            while (!m_mongoDbRef.compareAndSet(oldMongoDB, mongoDB));
+            
+            try {
+                connectToDB(mongoDB, DEFAULT_MONGODB_USERNAME, DEFAULT_MONGODB_PASSWORD);
+            }
+            catch (MongoException e) {
+                m_log.log(LogService.LOG_WARNING, "Initialization failed!", e);
+                throw new IOException("Initialization failed!", e);
+            }
+        }
+    }
+
+    @Override
+    public boolean removeRole(Role role) throws IOException {
+        try {
+            DBCollection coll = getCollection();
+
+            WriteResult result = coll.remove(getTemplateObject(role));
+
+            if (result.getLastError() != null) {
+                result.getLastError().throwOnError();
+            }
+
+            return true;
+        }
+        catch (MongoException e) {
+            m_log.log(LogService.LOG_WARNING, "Remove role failed!", e);
+            throw new IOException("RemoveRole failed!", e);
+        }
+    }
+    
+    @Override
+    public void roleChanged(UserAdminEvent event) {
+        if (UserAdminEvent.ROLE_CHANGED == event.getType()) {
+            // Only the changes are interesting, as the creation and 
+            // removal are already caught by #addRole and #removeRole.... 
+            Role changedRole = event.getRole();
+
+            try {
+                DBCollection coll = getCollection();
+
+                DBObject query = getTemplateObject(changedRole);
+                DBObject update = m_helper.serializeUpdate(changedRole);
+
+                WriteResult result = coll.update(query, update, false /* upsert */, false /* multi */);
+
+                if (result.getLastError() != null) {
+                    result.getLastError().throwOnError();
+                }
+            }
+            catch (MongoException e) {
+                m_log.log(LogService.LOG_WARNING, "Failed to update changed role: " + changedRole.getName(), e);
+            }
+        }
+    }
+    
+    /**
+     * @param log the log-service to set, cannot be <code>null</code>.
+     */
+    public void setLogService(LogService log) {
+        m_log = log;
+    }
+    
+    @Override
+    public void updated(Dictionary properties) throws ConfigurationException {
+        String newServers = DEFAULT_MONGODB_SERVER;
+        String newDbName = DEFAULT_MONGODB_NAME;
+        String newCollectionName = DEFAULT_MONGODB_COLLECTION;
+        String newUsername = DEFAULT_MONGODB_USERNAME;
+        String newPassword = DEFAULT_MONGODB_PASSWORD;
+        
+        if (properties != null) {
+            // Use values supplied...
+            newServers = getMandatoryProperty(properties, KEY_MONGODB_SERVER);
+            newDbName = getMandatoryProperty(properties, KEY_MONGODB_NAME);
+            newCollectionName = getMandatoryProperty(properties, KEY_MONGODB_COLLECTION_NAME);
+            
+            newUsername = getProperty(properties, KEY_MONGODB_USERNAME);
+            newPassword = getProperty(properties, DEFAULT_MONGODB_PASSWORD);
+        }
+
+        MongoDB newMongoDb = new MongoDB(newServers, newDbName, newCollectionName);
+
+        MongoDB oldMongoDb;
+        do {
+            oldMongoDb = m_mongoDbRef.get();
+        }
+        while (!m_mongoDbRef.compareAndSet(oldMongoDb, newMongoDb));
+
+        try {
+            oldMongoDb.disconnect();
+        }
+        catch (MongoException e) {
+            m_log.log(LogService.LOG_WARNING, "Failed to disconnect from (old) MongoDB!", e);
+        }
+
+        try {
+            connectToDB(newMongoDb, newUsername, newPassword);
+        }
+        catch (MongoException e) {
+            m_log.log(LogService.LOG_WARNING, "Failed to connect to (new) MongoDB!", e);
+            throw new ConfigurationException(DEFAULT_MONGODB_USERNAME, "Failed to connect!", e);
+        }
+    }
+
+    /**
+     * Creates a connection to MongoDB using the given credentials.
+     * 
+     * @param mongoDB the {@link MongoDB} facade to connect to;
+     * @param userName the (optional) user name to use;
+     * @param password the (optional) password to use.
+     * @throws MongoException in case the connection or authentication failed.
+     */
+    private void connectToDB(MongoDB mongoDB, String userName, String password) throws MongoException {
+        if (!mongoDB.connect(userName, password)) {
+            throw new MongoException("Failed to connect to MongoDB! Authentication failed!");
+        }
+        
+        DBCollection collection = mongoDB.getCollection();
+        if (collection == null) {
+            throw new MongoException("Failed to connect to MongoDB! No collection returned!");
+        }
+
+        collection.ensureIndex(new BasicDBObject(NAME, 1).append("unique", true));
+    }
+    
+    /**
+     * Returns the current database collection.
+     * 
+     * @return the database collection to work with, cannot be <code>null</code>.
+     * @throws MongoException in case no connection to MongoDB exists.
+     */
+    private DBCollection getCollection() {
+        MongoDB mongoDB = m_mongoDbRef.get();
+        if (mongoDB == null) {
+            throw new MongoException("No connection to MongoDB?!");
+        }
+        return mongoDB.getCollection();
+    }
+
+    /**
+     * Returns the mandatory value for the given key.
+     * 
+     * @param properties the properties to get the mandatory value from;
+     * @param key the key of the value to retrieve;
+     * @return the value, never <code>null</code>.
+     * @throws ConfigurationException in case the given key had no value.
+     */
+    private String getMandatoryProperty(Dictionary properties, String key) throws ConfigurationException {
+        String result = getProperty(properties, key);
+        if (result == null || "".equals(result.trim())) {
+            throw new ConfigurationException(key, "cannot be null or empty!");
+        }
+        return result;
+    }
+    
+    /**
+     * Returns the value for the given key.
+     * 
+     * @param properties the properties to get the value from;
+     * @param key the key of the value to retrieve;
+     * @return the value, can be <code>null</code> in case no such key is present.
+     * @throws ConfigurationException in case the given key had no value.
+     */
+    private String getProperty(Dictionary properties, String key) throws ConfigurationException {
+        Object result = properties.get(key);
+        if (result == null || !(result instanceof String)) {
+            return null;
+        }
+        return (String) result;
+    }
+    
+    /**
+     * Creates a template object for the given role.
+     * 
+     * @param role the role to create a template object for, cannot be <code>null</code>.
+     * @return a template object for MongoDB, never <code>null</code>.
+     */
+    private DBObject getTemplateObject(Role role) {
+        BasicDBObject query = new BasicDBObject();
+        query.put(TYPE, role.getType());
+        query.put(NAME, role.getName());
+        return query;
+    }
+    
+    /**
+     * Creates a template object for the given (role)name.
+     * 
+     * @param name the name of the role to create a template object for, cannot be <code>null</code>.
+     * @return a template object for MongoDB, never <code>null</code>.
+     */
+    private DBObject getTemplateObject(String name) {
+        BasicDBObject query = new BasicDBObject();
+        query.put(NAME, name);
+        return query;
+    }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoSerializerHelper.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoSerializerHelper.java
new file mode 100644
index 0000000..31af118
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/MongoSerializerHelper.java
@@ -0,0 +1,220 @@
+/**
+ *  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.felix.useradmin.mongodb;
+
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.List;
+
+import org.apache.felix.useradmin.RoleFactory;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+import com.mongodb.BasicDBList;
+import com.mongodb.BasicDBObject;
+import com.mongodb.DBObject;
+import com.mongodb.MongoException;
+
+/**
+ * Provides a helper class for (de)serializing data to/from MongoDB.
+ */
+final class MongoSerializerHelper {
+    
+    static final String TYPE = "type";
+    static final String NAME = "name";
+    
+    static final String PROPERTIES = "properties";
+    static final String CREDENTIALS = "credentials";
+    static final String MEMBERS = "members";
+    static final String REQUIRED_MEMBERS = "requiredMembers";
+    
+    static final String SET = "$set";
+    
+    private final RoleProvider m_roleProvider;
+    
+    /**
+     * Creates a new {@link MongoSerializerHelper} instance.
+     * 
+     * @param roleProvider the role provider to use, cannot be <code>null</code>.
+     */
+    public MongoSerializerHelper(RoleProvider roleProvider) {
+        m_roleProvider = roleProvider;
+    }
+
+    /**
+     * Converts a given {@link DBObject} to a {@link Role} instance.
+     * 
+     * @param object the {@link DBObject} to convert, cannot be <code>null</code>.
+     * @return a {@link Role} instance, never <code>null</code>.
+     */
+    public Role deserialize(DBObject object) {
+        int type = ((Integer) object.get(TYPE)).intValue();
+        String name = (String) object.get(NAME);
+
+        Role result = RoleFactory.createRole(type, name);
+        // Read the generic properties of the role...
+        deserializeDictionary(result.getProperties(), (DBObject) object.get(PROPERTIES));
+        
+        if ((Role.GROUP == type) || (Role.USER == type)) {
+            // This is safe, as Group extends from User...
+            deserializeDictionary(((User) result).getCredentials(), (DBObject) object.get(CREDENTIALS));
+
+            if (Role.GROUP == type) {
+                for (Role member : getRoles((BasicDBList) object.get(MEMBERS))) {
+                    ((Group) result).addMember(member);
+                }
+
+                for (Role member : getRoles((BasicDBList) object.get(REQUIRED_MEMBERS))) {
+                    ((Group) result).addRequiredMember(member);
+                }
+            }
+        }
+        
+        return result;
+    }
+
+    /**
+     * Serializes the given {@link Role} to a {@link DBObject} instance.
+     * 
+     * @param role the {@link Role} to serialize, cannot be <code>null</code> (unchecked!).
+     * @return a {@link DBObject} representing the given {@link Role}, never <code>null</code>.
+     */
+    public DBObject serialize(Role role) {
+        BasicDBObject data = new BasicDBObject();
+        
+        int type = role.getType();
+        
+        data.put(TYPE, type);
+        data.put(NAME, role.getName());
+
+        data.put(PROPERTIES, serializeDictionary(role.getProperties()));
+        if ((Role.GROUP == type) || (Role.USER == type)) {
+            data.put(CREDENTIALS, serializeDictionary(((User) role).getCredentials()));
+
+            if (Role.GROUP == type) {
+                data.put(MEMBERS, getRoleNames(((Group) role).getMembers()));
+                data.put(REQUIRED_MEMBERS, getRoleNames(((Group) role).getRequiredMembers()));
+            }
+        }
+        
+        return data;
+    }
+    
+    /**
+     * Creates a serialized version of the given {@link Role} to be used in an update statement.
+     * 
+     * @param role the {@link Role} to update, cannot be <code>null</code>.
+     * @return a {@link DBObject} representing an update statement for the given {@link Role}.
+     */
+    public DBObject serializeUpdate(Role role) {
+        int type = role.getType();
+        
+        BasicDBObject changeSet = new BasicDBObject();
+        
+        changeSet.put(PROPERTIES, serializeDictionary(role.getProperties()));
+        if ((Role.GROUP == type) || (Role.USER == type)) {
+            changeSet.put(CREDENTIALS, serializeDictionary(((User) role).getCredentials()));
+
+            if (Role.GROUP == type) {
+                changeSet.put(MEMBERS, getRoleNames(((Group) role).getMembers()));
+                changeSet.put(REQUIRED_MEMBERS, getRoleNames(((Group) role).getRequiredMembers()));
+            }
+        }
+        
+        return new BasicDBObject(SET, changeSet);
+    }
+
+    /**
+     * Finds an existing member by its name.
+     * 
+     * @param name the name of the member to return, cannot be <code>null</code>.
+     * @return a member instance, never <code>null</code>.
+     * @throws MongoException in case the requested member was not found (or any other MongoDB exception).
+     */
+    final Role findExistingMember(String name) {
+        Role result = m_roleProvider.getRole(name);
+        if (result == null) {
+            throw new MongoException("No such role: " + name);
+        }
+        return result;
+    }
+
+    /**
+     * Deserializes the given {@link DBObject} into the given {@link Dictionary}.
+     * 
+     * @param dictionary the dictionary to fill;
+     * @param object the {@link DBObject} to deserialize.
+     */
+    private void deserializeDictionary(Dictionary dictionary, DBObject object) {
+        for (String key : object.keySet()) {
+            dictionary.put(KeyCodec.decode(key), object.get(key));
+        }
+    }
+    
+    /**
+     * Serializes a given array of {@link Role}s to an list for storing in a {@link DBObject}.
+     * 
+     * @param members the {@link Role}s to serialize, cannot be <code>null</code>.
+     * @return the "serialized" array, never <code>null</code>.
+     */
+    private List<String> getRoleNames(Role[] members) {
+        List<String> result = new ArrayList<String>();
+        if (members != null) {
+            for (Role member : members) {
+                result.add(member.getName());
+            }
+        }
+        return result;
+    }
+    
+    /**
+     * Returns all roles mentioned in the given list.
+     * 
+     * @param list the list with role names to convert.
+     * @return a list with {@link Role}s, never <code>null</code>.
+     */
+    private List<Role> getRoles(BasicDBList list) {
+        List<Role> result = new ArrayList<Role>();
+        for (int i = 0, size = list.size(); i < size; i++) {
+            final String memberName = (String) list.get(i);
+            result.add(findExistingMember(memberName));
+        }
+        return result;
+    }
+
+    /**
+     * Serializes a given {@link Dictionary} into a {@link DBObject}.
+     * 
+     * @param properties the {@link Dictionary} to serialize, cannot be <code>null</code>.
+     * @return the serialized dictionary, never <code>null</code>. 
+     */
+    private DBObject serializeDictionary(Dictionary properties) {
+        BasicDBObject result = new BasicDBObject();
+        
+        Enumeration<String> keysEnum = properties.keys();
+        while (keysEnum.hasMoreElements()) {
+            String key = keysEnum.nextElement();
+            Object value = properties.get(key);
+            
+            result.append(KeyCodec.encode(key), value);
+        }
+        
+        return result;
+    }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/RoleProvider.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/RoleProvider.java
new file mode 100644
index 0000000..82f8b23
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/RoleProvider.java
@@ -0,0 +1,33 @@
+/**
+ *  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.felix.useradmin.mongodb;
+
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides access to a {@link Role}.
+ */
+public interface RoleProvider {
+
+    /**
+     * Returns the {@link Role} with the given name.
+     * 
+     * @param name the name of the role to return, cannot be <code>null</code>.
+     * @return a {@link Role} with the given name, or <code>null</code> if no such role exists.
+     */
+    Role getRole(String name);
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/Activator.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/Activator.java
new file mode 100644
index 0000000..632af63
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/Activator.java
@@ -0,0 +1,60 @@
+/**
+ *  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.felix.useradmin.mongodb.osgi;
+
+import java.util.Properties;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.apache.felix.useradmin.mongodb.MongoDBStore;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.cm.ManagedService;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Registers {@link MongoDBStore} as service.
+ */
+public class Activator implements BundleActivator {
+
+    private LogServiceHelper m_logServiceHelper;
+
+    @Override
+    public void start(BundleContext context) throws Exception {
+        m_logServiceHelper = new LogServiceHelper(context);
+        m_logServiceHelper.open();
+        
+        MongoDBStore store = new MongoDBStore();
+        store.setLogService(m_logServiceHelper);
+        
+        Properties props = new Properties();
+        props.put(Constants.SERVICE_PID, MongoDBStore.PID);
+
+        String[] serviceNames = { RoleRepositoryStore.class.getName(), 
+            UserAdminListener.class.getName(), ManagedService.class.getName() };
+
+        context.registerService(serviceNames, store, props);
+    }
+
+    @Override
+    public void stop(BundleContext context) throws Exception {
+        if (m_logServiceHelper != null) {
+            m_logServiceHelper.close();
+            m_logServiceHelper = null;
+        }
+    }
+}
diff --git a/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/LogServiceHelper.java b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/LogServiceHelper.java
new file mode 100644
index 0000000..5397cf9
--- /dev/null
+++ b/useradmin/mongodb/src/main/java/org/apache/felix/useradmin/mongodb/osgi/LogServiceHelper.java
@@ -0,0 +1,77 @@
+/**
+ *  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.felix.useradmin.mongodb.osgi;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.log.LogService;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Provides a facade for {@link LogService} allowing it to be not 
+ * available as actual service without its users knowing about this.
+ */
+public class LogServiceHelper extends ServiceTracker implements LogService {
+
+    /**
+     * Creates a new {@link LogServiceHelper} instance.
+     * 
+     * @param context the bundle context to use, cannot be <code>null</code>.
+     */
+    public LogServiceHelper(BundleContext context) {
+        super(context, LogService.class.getName(), null);
+    }
+    
+    @Override
+    public void log(int level, String message) {
+        LogService logService = getLogService();
+        if (logService != null) {
+            logService.log(level, message);
+        }
+    }
+
+    @Override
+    public void log(int level, String message, Throwable exception) {
+        LogService logService = getLogService();
+        if (logService != null) {
+            logService.log(level, message, exception);
+        }
+    }
+
+    @Override
+    public void log(ServiceReference sr, int level, String message) {
+        LogService logService = getLogService();
+        if (logService != null) {
+            logService.log(sr, level, message);
+        }
+    }
+
+    @Override
+    public void log(ServiceReference sr, int level, String message, Throwable exception) {
+        LogService logService = getLogService();
+        if (logService != null) {
+            logService.log(sr, level, message, exception);
+        }
+    }
+    
+    /**
+     * @return a {@link LogService} instance, or <code>null</code> if not available.
+     */
+    private LogService getLogService() {
+        return (LogService) getService();
+    }
+}
diff --git a/useradmin/mongodb/src/test/java/org/apache/felix/useradmin/mongodb/KeyCodecTest.java b/useradmin/mongodb/src/test/java/org/apache/felix/useradmin/mongodb/KeyCodecTest.java
new file mode 100644
index 0000000..ca9ba58
--- /dev/null
+++ b/useradmin/mongodb/src/test/java/org/apache/felix/useradmin/mongodb/KeyCodecTest.java
@@ -0,0 +1,202 @@
+/**
+ *  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.felix.useradmin.mongodb;
+
+import junit.framework.TestCase;
+
+/**
+ * Test cases for {@link KeyCodec}.
+ */
+public class KeyCodecTest extends TestCase {
+
+    /**
+     * Tests that we can decode a string with an encoded dollar.
+     */
+    public void testDecodeDollarOk() {
+        String input = "%24hello %24world";
+        assertEquals("$hello $world", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that we can decode a string with encoded dot and dollars.
+     */
+    public void testDecodeDotAndDollarsOk() {
+        String input = "%24hello %24world%2E";
+        assertEquals("$hello $world.", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that we can decode a string with an encoded dot.
+     */
+    public void testDecodeDotOk() {
+        String input = "hello world%2E";
+        assertEquals("hello world.", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that encoding a string with a dot works.
+     */
+    public void testDecodeEncodeOk() {
+        String key = "%25hello world.";
+        assertEquals(key, KeyCodec.decode(KeyCodec.encode(key)));
+    }
+
+    /**
+     * Tests that we can decode a string with an incorrect entity at the end.
+     */
+    public void testDecodeIncorrectEntityAtEndOk() {
+        String input = "hello world%1";
+        assertEquals("hello world%1", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that we can decode a string with an incorrect entity at the end.
+     */
+    public void testDecodeIncorrectEntityOk() {
+        String input = "hello%1world";
+        assertEquals("hello%1world", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that we can decode a string with an encoded null.
+     */
+    public void testDecodeNullEntityOk() {
+        String input = "%00hello%00world";
+        assertEquals("\0hello\0world", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that we can decode a null value.
+     */
+    public void testDecodeNullOk() {
+        assertNull(KeyCodec.decode(null));
+    }
+
+    /**
+     * Tests that we can decode a string with a percent sign at the end.
+     */
+    public void testDecodePercentAtEndOk() {
+        String input = "hello world%";
+        assertEquals("hello world%", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that we can decode a string with an escaped percent.
+     */
+    public void testDecodePercentOk() {
+        String input = "%%1%%%2E";
+        assertEquals("%1%.", KeyCodec.decode(input));
+        
+        input = "%%Hello %%World%2E";
+        assertEquals("%Hello %World.", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that we can decode a string with an encoded underscore.
+     */
+    public void testDecodeUnderscoreOk() {
+        String input = "%5Fhello%5Fworld";
+        assertEquals("_hello_world", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that we can decode a string with an unknown entity at the end.
+     */
+    public void testDecodeUnknownEntityAtEndOk() {
+        String input = "hello world%10";
+        assertEquals("hello world%10", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that we can decode a string with an unknown entity.
+     */
+    public void testDecodeUnknownEntityOk() {
+        String input = "%25hello %25world%2F";
+        assertEquals("%25hello %25world%2F", KeyCodec.decode(input));
+    }
+
+    /**
+     * Tests that encoding a string with dollars works.
+     */
+    public void testEncodeDollarOk() {
+        String key = "$hello $world";
+        assertEquals("%24hello %24world", KeyCodec.encode(key));
+    }
+
+    /**
+     * Tests that encoding a string with dollars and a dot works.
+     */
+    public void testEncodeDotAndDollarOk() {
+        String key = "$hello $world.";
+        assertEquals("%24hello %24world%2E", KeyCodec.encode(key));
+    }
+
+    /**
+     * Tests that encoding a string with a dot works.
+     */
+    public void testEncodeDotOk() {
+        String key = "hello world.";
+        assertEquals("hello world%2E", KeyCodec.encode(key));
+    }
+
+    /**
+     * Tests that encoding a string with null-characters works.
+     */
+    public void testEncodeNullEntityOk() {
+        String key = "\0hello\0world";
+        assertEquals("%00hello%00world", KeyCodec.encode(key));
+    }
+
+    /**
+     * Tests that we can encode a null value.
+     */
+    public void testEncodeNullInputOk() {
+        assertNull(KeyCodec.encode(null));
+    }
+
+    /**
+     * Tests that encoding a string with percents works.
+     */
+    public void testEncodePercentOk() {
+        String key = "%hello %world.";
+        assertEquals("%%hello %%world%2E", KeyCodec.encode(key));
+    }
+
+    /**
+     * Tests that encoding a string without dots or dollars works.
+     */
+    public void testEncodePlainStringOk() {
+        String key = "hello world!";
+        assertEquals(key, KeyCodec.encode(key));
+    }
+
+    /**
+     * Tests that encoding a string with underscores works.
+     */
+    public void testEncodeUnderscoreOk() {
+        String key = "_hello_world";
+        assertEquals("%5Fhello%5Fworld", KeyCodec.encode(key));
+    }
+
+    /**
+     * Tests that encoding a string with a dot works.
+     */
+    public void testEncodeUnknownEntitiesOk() {
+        String key = "%25hello world.";
+        assertEquals("%%25hello world%2E", KeyCodec.encode(key));
+    }
+}
diff --git a/useradmin/pom.xml b/useradmin/pom.xml
new file mode 100644
index 0000000..7ddf619
--- /dev/null
+++ b/useradmin/pom.xml
@@ -0,0 +1,36 @@
+<!--
+    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.
+-->
+<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/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>felix-parent</artifactId>
+        <version>1.2.0</version>
+        <relativePath>../../pom/pom.xml</relativePath>
+    </parent>
+    <packaging>pom</packaging>
+    <artifactId>useradmin-reactor</artifactId>
+	<description>UserAdmin OSGi compendium service subproject.</description>
+    <version>1</version>
+    <modules>
+        <module>useradmin</module>
+        <module>filestore</module>
+        <module>mongodb</module>
+    </modules>
+</project>
diff --git a/useradmin/useradmin/pom.xml b/useradmin/useradmin/pom.xml
new file mode 100644
index 0000000..daaae11
--- /dev/null
+++ b/useradmin/useradmin/pom.xml
@@ -0,0 +1,100 @@
+<!--
+    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.
+-->
+<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">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<artifactId>felix-parent</artifactId>
+		<groupId>org.apache.felix</groupId>
+		<version>1.2.0</version>
+        <relativePath>../../../pom/pom.xml</relativePath>
+	</parent>
+	<artifactId>org.apache.felix.useradmin</artifactId>
+    <version>1.0.2</version>
+	<packaging>bundle</packaging>
+	<description>Provides an implementation for the UserAdmin OSGi compendium service.</description>
+	<dependencies>
+		<dependency>
+			<groupId>org.osgi</groupId>
+			<artifactId>org.osgi.core</artifactId>
+			<version>4.0.0</version>
+			<scope>provided</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.osgi</groupId>
+			<artifactId>org.osgi.compendium</artifactId>
+			<version>4.0.0</version>
+		</dependency>
+		<dependency>
+            <groupId>org.apache.felix</groupId>
+			<artifactId>org.apache.felix.framework</artifactId>
+			<version>4.0.2</version>
+			<scope>test</scope>
+		</dependency>
+	</dependencies>
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.felix</groupId>
+				<artifactId>maven-bundle-plugin</artifactId>
+				<extensions>true</extensions>
+				<configuration>
+					<instructions>
+						<Bundle-Name>Apache Felix User Admin Service</Bundle-Name>
+						<Bundle-Description>
+							An implementation of the OSGi User Admin Compendium Service
+						</Bundle-Description>
+						<Bundle-Activator>
+							${project.artifactId}.osgi.Activator
+						</Bundle-Activator>
+						<Bundle-SymbolicName>
+							${project.artifactId}
+						</Bundle-SymbolicName>
+						<Bundle-Vendor>The Apache Software Foundation</Bundle-Vendor>
+						<Import-Package>
+						     org.osgi.service.useradmin; version="[1.1,1.2)",
+						     *
+						</Import-Package>
+						<Export-Package>
+							${project.artifactId}; version="1.0.0"
+						</Export-Package>
+						<Private-Package>
+							${project.artifactId}.*
+						</Private-Package>
+					</instructions>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>rat-maven-plugin</artifactId>
+				<configuration>
+					<excludeSubProjects>false</excludeSubProjects>
+					<useEclipseDefaultExcludes>true</useEclipseDefaultExcludes>
+					<useMavenDefaultExcludes>true</useMavenDefaultExcludes>
+					<excludes>
+						<param>doc/*</param>
+						<param>maven-eclipse.xml</param>
+						<param>.checkstyle</param>
+						<param>.externalToolBuilders/*</param>
+					</excludes>
+				</configuration>
+			</plugin>
+		</plugins>
+	</build>
+</project>
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/BackendException.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/BackendException.java
new file mode 100644
index 0000000..be24d47
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/BackendException.java
@@ -0,0 +1,53 @@
+/**
+ *  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.felix.useradmin;
+
+/**
+ * Represents a runtime exception for {@link RoleRepositoryBackend}s.
+ */
+public class BackendException extends RuntimeException {
+
+    private static final long serialVersionUID = 5566633157189079557L;
+
+    /**
+     * Creates a new {@link BackendException} instance.
+     * 
+     * @param cause the originating cause of this exception.
+     */
+    public BackendException(Exception cause) {
+        super(cause);
+    }
+
+    /**
+     * Creates a new {@link BackendException} instance.
+     * 
+     * @param message the message of this exception.
+     */
+    public BackendException(String message) {
+        super(message);
+    }
+
+    /**
+     * Creates a new {@link BackendException} instance.
+     * 
+     * @param message the message of this exception;
+     * @param cause the originating cause of this exception.
+     */
+    public BackendException(String message, Exception cause) {
+        super(message, cause);
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleFactory.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleFactory.java
new file mode 100644
index 0000000..da54c9e
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleFactory.java
@@ -0,0 +1,57 @@
+/**
+ *  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.felix.useradmin;
+
+
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides a factory for creating the various role instances, can be used by external 
+ * implementations to create new role instances.
+ */
+public final class RoleFactory {
+
+    /**
+     * Creates a new instance of {@link RoleFactory}, not used.
+     */
+    private RoleFactory() {
+        // Nop
+    }
+
+    /**
+     * Creates a new role instance.
+     * 
+     * @param type the type of the role to create;
+     * @param name the name of the role to create.
+     * @return a new {@link RoleImpl} instance denoting the requested role, never <code>null</code>.
+     */
+    public static Role createRole(int type, String name) {
+        if (type == Role.USER) {
+            UserImpl result = new UserImpl(name);
+            return result;
+        } else if (type == Role.GROUP) {
+            GroupImpl result = new GroupImpl(name);
+            return result;
+        } else {
+            RoleImpl result = new RoleImpl(name);
+            return result;
+        }
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleRepositoryStore.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleRepositoryStore.java
new file mode 100644
index 0000000..3296f59
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/RoleRepositoryStore.java
@@ -0,0 +1,94 @@
+/**
+ *  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.felix.useradmin;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides an abstraction to store and retrieve a role repository.
+ */
+public interface RoleRepositoryStore extends Closeable {
+    
+    /**
+     * Adds a given role to this backend.
+     * <p>
+     * If the given role is already contained by this backed, this method 
+     * should not do anything and return <code>false</code> to denote this.
+     * </p>
+     * 
+     * @param role the role to add, cannot be <code>null</code>.
+     * @return <code>true</code> if the role was successfully added, <code>false</code> otherwise.
+     * @throws IllegalArgumentException in case the given argument was <code>null</code>;
+     * @throws IOException in case of I/O problems.
+     */
+    boolean addRole(Role role) throws IOException;
+
+    /**
+     * Closes this store, allowing implementations to free up resources, close
+     * connections, and so on.
+     * 
+     * @throws IOException in case of I/O problems.
+     */
+    void close() throws IOException;
+
+    /**
+     * Returns all available roles in this backend.
+     * 
+     * @return an array with all roles, never <code>null</code>, but can be empty.
+     * @throws IOException in case of I/O problems.
+     */
+    Role[] getAllRoles() throws IOException;
+    
+    /**
+     * Returns a {@link Role} by its name.
+     * 
+     * @param roleName the name of the role to return, cannot be <code>null</code>.
+     * @return the role with the given name, or <code>null</code> if no such role exists.
+     * @throws IllegalArgumentException in case the given argument was <code>null</code>;
+     * @throws IOException in case of I/O problems.
+     */
+    Role getRoleByName(String roleName) throws IOException;
+
+    /**
+     * Called once before any other method of this interface is being called.
+     * <p>
+     * Implementations can use this method to create a connection to the 
+     * backend, or load the initial set of roles, and so on.
+     * </p>
+     * 
+     * @throws IOException in case of I/O problems.
+     */
+    void initialize() throws IOException;
+
+    /**
+     * Removes a given role from this backend.
+     * <p>
+     * If the given role is not contained by this backed, this method 
+     * should not do anything and return <code>false</code> to denote this.
+     * </p>
+     * 
+     * @param role the role to remove, cannot be <code>null</code>.
+     * @return <code>true</code> if the role was successfully removed, <code>false</code> otherwise.
+     * @throws IllegalArgumentException in case the given argument was <code>null</code>;
+     * @throws IOException in case of I/O problems.
+     */
+    boolean removeRole(Role role) throws IOException;
+    
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/AuthorizationImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/AuthorizationImpl.java
new file mode 100644
index 0000000..2234ebc
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/AuthorizationImpl.java
@@ -0,0 +1,97 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Provides an implementation for {@link Authorization}.
+ */
+public class AuthorizationImpl implements Authorization {
+
+    private final String m_name;
+    private final User m_user;
+    private final RoleRepository m_roleManager;
+    private final RoleChecker m_roleChecker;
+
+    /**
+     * Creates a new {@link AuthorizationImpl} instance for the given {@link User}.
+     * 
+     * @param roleManager the role manager to use for obtaining the roles, cannot be <code>null</code>.
+     */
+    public AuthorizationImpl(RoleRepository roleManager) {
+        this(null, roleManager);
+    }
+
+    /**
+     * Creates a new {@link AuthorizationImpl} instance for the given {@link User}.
+     * 
+     * @param user the {@link User} to authorize, may be <code>null</code> for the anonymous user;
+     * @param roleManager the role manager to use for obtaining the roles, cannot be <code>null</code>.
+     */
+    public AuthorizationImpl(User user, RoleRepository roleManager) {
+        m_user = user;
+        m_roleManager = roleManager;
+        m_name = (user != null) ? user.getName() : null;
+        m_roleChecker = new RoleChecker();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getName() {
+        return m_name;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean hasRole(String name) {
+        Role role = m_roleManager.getRoleByName(name);
+        if (role == null) {
+            // No role found, so it is never implied...
+            return false;
+        }
+        return m_roleChecker.isImpliedBy(role, m_user);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String[] getRoles() {
+        List result = new ArrayList();
+
+        Iterator rolesIter = m_roleManager.getRoles(null /* filter */).iterator();
+        while (rolesIter.hasNext()) {
+            RoleImpl role = (RoleImpl) rolesIter.next();
+            if (!Role.USER_ANYONE.equals(role.getName()) && m_roleChecker.isImpliedBy(role, m_user)) {
+                result.add(role.getName());
+            }
+        }
+
+        return result.isEmpty() ? null : (String[]) result.toArray(new String[result.size()]);
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/EventDispatcher.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/EventDispatcher.java
new file mode 100644
index 0000000..d769bee
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/EventDispatcher.java
@@ -0,0 +1,226 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.util.Properties;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.event.EventConstants;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Provides an event dispatcher for delivering {@link UserAdminEvent}s asynchronously. 
+ */
+public final class EventDispatcher implements Runnable {
+    
+    private static final String TOPIC_BASE = "org/osgi/service/useradmin/UserAdmin/";
+
+    private final EventAdmin m_eventAdmin;
+    private final UserAdminListenerList m_listenerList;
+    private final BlockingQueue m_eventQueue;
+    private final Thread m_backgroundThread;
+
+    /**
+     * Creates a new {@link EventDispatcher} instance, and starts a background thread to deliver all events.
+     * 
+     * @param eventAdmin the event admin to use, cannot be <code>null</code>;
+     * @param listenerList the list with {@link UserAdminListener}s, cannot be <code>null</code>.
+     * @throws IllegalArgumentException in case one of the given parameters was <code>null</code>.
+     */
+    public EventDispatcher(EventAdmin eventAdmin, UserAdminListenerList listenerList) {
+        if (eventAdmin == null) {
+            throw new IllegalArgumentException("EventAdmin cannot be null!");
+        }
+        if (listenerList == null) {
+            throw new IllegalArgumentException("ListenerList cannot be null!");
+        }
+
+        m_eventAdmin = eventAdmin;
+        m_listenerList = listenerList;
+        m_eventQueue = new LinkedBlockingQueue();
+
+        m_backgroundThread = new Thread(this, "UserAdmin event dispatcher");
+    }
+
+    /**
+     * Dispatches a given event for asynchronous delivery to all interested listeners, 
+     * including those using the {@link EventAdmin} service.
+     * <p>
+     * This method will perform a best-effort to dispatch the event to all listeners, i.e., 
+     * there is no guarantee that the listeners will actually obtain the event, nor any
+     * notification is given in case delivery fails.
+     * </p>
+     * 
+     * @param event the event to dispatch, cannot be <code>null</code>.
+     * @throws IllegalStateException in case this dispatcher is already stopped.
+     */
+    public void dispatch(UserAdminEvent event) {
+        if (!isRunning()) {
+            return;
+        }
+
+        try {
+            m_eventQueue.put(event);
+        } catch (InterruptedException e) {
+            // Restore interrupt flag...
+            Thread.currentThread().interrupt();
+        }
+    }
+    
+    /**
+     * Starts this event dispatcher, allowing it to pick up events and deliver them.
+     */
+    public void start() {
+        if (!isRunning()) {
+            m_backgroundThread.start();
+        }
+    }
+
+    /**
+     * Signals this event dispatcher to stop its work and clean up all running threads.
+     */
+    public void stop() {
+        if (!isRunning()) {
+            return;
+        }
+
+        // Add poison object to queue to let the background thread terminate...
+        m_eventQueue.add(EventDispatcher.this);
+
+        try {
+            m_backgroundThread.join();
+        } catch (InterruptedException e) {
+            // We're already stopping; so don't bother... 
+        }
+    }
+
+    /**
+     * Returns whether or not the background thread is running.
+     * 
+     * @return <code>true</code> if the background thread is running (alive), <code>false</code> otherwise.
+     */
+    final boolean isRunning() {
+        return m_backgroundThread.isAlive();
+    }
+    
+    /**
+     * Provides the main event loop, which waits until an event is enqueued in order 
+     * to deliver it to any interested listener.
+     */
+    public void run() {
+        try {
+            while (true) {
+                // Blocks until a event is dispatched...
+                Object event = m_eventQueue.take();
+
+                if (event instanceof UserAdminEvent) {
+                    // Got a "normal" user admin event; lets dispatch it further...
+                    deliverEventSynchronously((UserAdminEvent) event);
+                } else {
+                    // Got a "poison" object; this means we must stop running...
+                    return;
+                }
+            }
+        } catch (InterruptedException e) {
+            // Restore interrupt flag, and terminate thread...
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    /**
+     * Converts a given {@link UserAdminEvent} to a {@link Event} that can be
+     * dispatched through the {@link EventAdmin} service.
+     * 
+     * @param event
+     *            the event to convert, cannot be <code>null</code>.
+     * @return a new {@link Event} instance containing the same set of
+     *         information as the given event, never <code>null</code>.
+     */
+    private Event convertEvent(UserAdminEvent event) {
+        String topic = getTopicName(event.getType());
+        Role role = event.getRole();
+        ServiceReference serviceRef = event.getServiceReference();
+
+        Properties props = new Properties();
+        props.put(EventConstants.EVENT_TOPIC, TOPIC_BASE.concat(topic));
+        props.put(EventConstants.EVENT, event);
+        props.put("role", role);
+        props.put("role.name", role.getName());
+        props.put("role.type", new Integer(role.getType()));
+        if (serviceRef != null) {
+            props.put(EventConstants.SERVICE, serviceRef);
+            Object property;
+            
+            property = serviceRef.getProperty(Constants.SERVICE_ID);
+            if (property != null) {
+                props.put(EventConstants.SERVICE_ID, property);
+            }
+            property = serviceRef.getProperty(Constants.OBJECTCLASS);
+            if (property != null) {
+                props.put(EventConstants.SERVICE_OBJECTCLASS, property);
+            }
+            property = serviceRef.getProperty(Constants.SERVICE_PID);
+            if (property != null) {
+                props.put(EventConstants.SERVICE_PID, property);
+            }
+        }
+
+        return new Event(topic, props);
+    }
+
+    /**
+     * Delivers the given event synchronously to all interested listeners.
+     * 
+     * @param event the event to deliver, cannot be <code>null</code>.
+     */
+    private void deliverEventSynchronously(UserAdminEvent event) {
+        // Asynchronously deliver an event to the EventAdmin service...
+        m_eventAdmin.postEvent(convertEvent(event));
+
+        // Synchronously call all UserAdminListeners to deliver the event...
+        UserAdminListener[] listeners = m_listenerList.getListeners();
+        for (int i = 0; i < listeners.length; i++) {
+            listeners[i].roleChanged(event);
+        }
+    }
+    
+    /**
+     * Converts a topic name for the given event-type.
+     * 
+     * @param type the type of event to get the topic name for.
+     * @return a topic name, never <code>null</code>.
+     */
+    private String getTopicName(int type) {
+        switch (type) {
+            case UserAdminEvent.ROLE_CREATED:
+                return "ROLE_CREATED";
+            case UserAdminEvent.ROLE_CHANGED:
+                return "ROLE_CHANGED";
+            case UserAdminEvent.ROLE_REMOVED:
+                return "ROLE_REMOVED";
+            default:
+                return null;
+        }
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChangeListener.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChangeListener.java
new file mode 100644
index 0000000..f9a914b
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChangeListener.java
@@ -0,0 +1,66 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Provides a callback for listening to role changes.
+ */
+public interface RoleChangeListener {
+
+    /**
+     * Called when a new role is added.
+     * 
+     * @param role the role that is added.
+     */
+    void roleAdded(Role role);
+    
+    /**
+     * Called when a role is removed.
+     * 
+     * @param role the role that is removed.
+     */
+    void roleRemoved(Role role);
+    
+    /**
+     * Called when a new property-entry is added to a role.
+     * 
+     * @param role the role that changed;
+     * @param key the key of the entry;
+     * @param value the value associated to the key.
+     */
+    void propertyAdded(Role role, Object key, Object value);
+
+    /**
+     * Called when an property-entry is removed from a role.
+     * 
+     * @param role the role that changed;
+     * @param key the key of the entry.
+     */
+    void propertyRemoved(Role role, Object key);
+
+    /**
+     * Called when an property-entry is changed for a role.
+     * 
+     * @param role the role that changed;
+     * @param key the key of the entry;
+     * @param oldValue the old value associated to the key;
+     * @param newValue the new value associated to the key.
+     */
+    void propertyChanged(Role role, Object key, Object oldValue, Object newValue);
+}
\ No newline at end of file
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChecker.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChecker.java
new file mode 100644
index 0000000..ced47ba
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleChecker.java
@@ -0,0 +1,132 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+
+/**
+ * Helper class to check for implied role memberships.
+ */
+final class RoleChecker {
+
+    /**
+     * Verifies whether the given role is implied by the memberships of the given user.
+     * 
+     * @param user the user to check the roles for, cannot be <code>null</code>;
+     * @param impliedRole the implied role to check for, cannot be <code>null</code>.
+     * @return <code>true</code> if the given user has the implied role, <code>false</code> otherwise.
+     */
+    public boolean isImpliedBy(Role role, Role impliedRole) {
+        if (role instanceof Group) {
+            return isGroupImpliedBy((Group) role, impliedRole, new ArrayList());
+        } else if (role instanceof User) {
+            return isUserImpliedBy((User) role, impliedRole);
+        } else {
+            return isRoleImpliedBy(role, impliedRole);
+        }
+    }
+
+    /**
+     * Verifies whether the given group is implied by the given role.
+     * 
+     * @param group the group to check, cannot be <code>null</code>;
+     * @param impliedRole the implied role to check for, cannot be <code>null</code>;
+     * @param seenGroups a list of all seen groups, used for detecting cycles in groups, cannot be <code>null</code>.
+     * @return <code>true</code> if the given group has the implied role, <code>false</code> otherwise.
+     */
+    private boolean isGroupImpliedBy(Group group, Role impliedRole, List seenGroups) {
+        Role[] basicRoles = group.getMembers();
+        Role[] requiredRoles = group.getRequiredMembers();
+
+        boolean isImplied = true;
+        
+        // Check whether all required roles are implied...
+        for (int i = 0; (requiredRoles != null) && isImplied && (i < requiredRoles.length); i++) {
+            Role requiredRole = requiredRoles[i];
+            if (seenGroups.contains(requiredRole)) {
+                // Found a cycle between groups; always yield false!
+                return false;
+            }
+            
+            if (requiredRole instanceof Group) {
+                seenGroups.add(requiredRole);
+                isImplied = isGroupImpliedBy((Group) requiredRole, impliedRole, seenGroups);
+            } else if (requiredRole instanceof User) {
+                isImplied  = isUserImpliedBy((User) requiredRole, impliedRole);
+            } else /* if (requiredRoles[i] instanceof RoleImpl) */ {
+                isImplied = isRoleImpliedBy(requiredRole, impliedRole);
+            }
+        }
+
+        // Required role is not implied by the given role; we can stop now...
+        if (!isImplied) {
+            return false;
+        }
+
+        // Ok; all required roles are implied, let's verify whether a least one basic role is implied...
+        isImplied = false;
+
+        // Check whether at least one basic role is implied...
+        for (int i = 0; (basicRoles != null) && !isImplied && (i < basicRoles.length); i++) {
+            Role basicRole = (Role) basicRoles[i];
+            if (seenGroups.contains(basicRole)) {
+                // Found a cycle between groups; always yield false!
+                return false;
+            }
+
+            if (basicRole instanceof Group) {
+                seenGroups.add(basicRole);
+                isImplied = isGroupImpliedBy((Group) basicRole, impliedRole, seenGroups);
+            } else if (basicRole instanceof UserImpl) {
+                isImplied = isUserImpliedBy((User) basicRole, impliedRole);
+            } else /* if (requiredRoles[i] instanceof RoleImpl) */ {
+                isImplied = isRoleImpliedBy(basicRole, impliedRole);
+            }
+        }
+
+        return isImplied;
+    }
+
+    /**
+     * Verifies whether the given user is implied by the given role.
+     * 
+     * @param user the user to check, cannot be <code>null</code>;
+     * @param impliedRole the implied role to check for, cannot be <code>null</code>;
+     * @return <code>true</code> if the given user is implied by the given role, <code>false</code> otherwise.
+     */
+    private boolean isUserImpliedBy(User user, Role impliedRole) {
+        return Role.USER_ANYONE.equals(user.getName()) || (impliedRole != null && impliedRole.getName().equals(user.getName()));
+    }
+
+    /**
+     * Verifies whether the given role is implied by the given role.
+     * 
+     * @param role the role to check, cannot be <code>null</code>;
+     * @param impliedRole the implied role to check for, cannot be <code>null</code>;
+     * @return <code>true</code> if the given role is implied by the given role, <code>false</code> otherwise.
+     */
+    private boolean isRoleImpliedBy(Role role, Role impliedRole) {
+        return Role.USER_ANYONE.equals(role.getName()) || (impliedRole != null && impliedRole.getName().equals(role.getName()));
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleRepository.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleRepository.java
new file mode 100644
index 0000000..2348b55
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/RoleRepository.java
@@ -0,0 +1,367 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Dictionary;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.felix.useradmin.BackendException;
+import org.apache.felix.useradmin.RoleFactory;
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides a manager and entry-point for accessing {@link Role}s.
+ */
+public final class RoleRepository {
+
+    /**
+     * Hands off all obtained role change event to a local set of listeners.
+     */
+    final class RoleChangeReflector implements RoleChangeListener {
+        /**
+         * {@inheritDoc}
+         */
+        public void roleAdded(Role role) {
+            Iterator iterator = createListenerIterator();
+            while (iterator.hasNext()) {
+                ((RoleChangeListener) iterator.next()).roleAdded(role);
+            }
+        }
+        
+        /**
+         * {@inheritDoc}
+         */
+        public void roleRemoved(Role role) {
+            Iterator iterator = createListenerIterator();
+            while (iterator.hasNext()) {
+                ((RoleChangeListener) iterator.next()).roleRemoved(role);
+            }
+        }
+        
+        /**
+         * {@inheritDoc}
+         */
+        public void propertyAdded(Role role, Object key, Object value) {
+            Iterator iterator = createListenerIterator();
+            while (iterator.hasNext()) {
+                ((RoleChangeListener) iterator.next()).propertyAdded(role, key, value);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public void propertyRemoved(Role role, Object key) {
+            Iterator iterator = createListenerIterator();
+            while (iterator.hasNext()) {
+                ((RoleChangeListener) iterator.next()).propertyRemoved(role, key);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+            Iterator iterator = createListenerIterator();
+            while (iterator.hasNext()) {
+                ((RoleChangeListener) iterator.next()).propertyChanged(role, key, oldValue, newValue);
+            }
+        }
+    }
+
+    /** The single predefined role. */
+    public static final Role USER_ANYONE = RoleFactory.createRole(Role.ROLE, Role.USER_ANYONE);
+
+    private final RoleRepositoryStore m_store;
+    private final CopyOnWriteArrayList m_listeners;
+    private final RoleChangeReflector m_roleChangeReflector;
+    
+    /**
+     * Creates a new {@link RoleRepository} instance.
+     * 
+     * @param store the {@link RoleRepositoryStore} to use, cannot be <code>null</code>.
+     */
+    public RoleRepository(RoleRepositoryStore store) {
+        m_store = store;
+        
+        m_listeners = new CopyOnWriteArrayList();
+        m_roleChangeReflector = new RoleChangeReflector();
+    }
+
+    /**
+     * Adds a given role to this manager.
+     * 
+     * @param role the role to add, cannot be <code>null</code>. If it is already contained by this manager, this method will not do anything.
+     * @return the given role if added, <code>null</code> otherwise.
+     */
+    public Role addRole(Role role) {
+        if (role == null) {
+            throw new IllegalArgumentException("Role cannot be null!");
+        }
+        if (!(role instanceof RoleImpl)) {
+            throw new IllegalArgumentException("Invalid role type!");
+        }
+
+        checkPermissions();
+
+        try {
+            if (m_store.addRole(role)) {
+                m_roleChangeReflector.roleAdded(role);
+                return wireChangeListener(role);
+            }
+
+            return null;
+        }
+        catch (IOException e) {
+            throw new BackendException("Adding role " + role.getName() + " failed!", e);
+        }
+    }
+
+    /**
+     * Adds the given role change listener to be called for upcoming changes in roles.
+     * 
+     * @param listener the listener to register, cannot be <code>null</code>.
+     * @throws IllegalArgumentException in case the given listener was <code>null</code>.
+     */
+    public void addRoleChangeListener(RoleChangeListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("RoleChangeListener cannot be null!");
+        }
+
+        m_listeners.addIfAbsent(listener);
+    }
+
+    /**
+     * Returns the by its given name.
+     * 
+     * @param roleName the name of the role to return, cannot be <code>null</code>.
+     * @return the role matching the given name, or <code>null</code> if no role matched the given name.
+     */
+    public Role getRoleByName(String roleName) {
+        try {
+            return wireChangeListener(m_store.getRoleByName(roleName));
+        }
+        catch (IOException e) {
+            throw new BackendException("Failed to get role by name: " + roleName + "!", e);
+        }
+    }
+
+    /**
+     * Returns a collection with all roles matching a given filter.
+     * 
+     * @param filter the filter to match the individual roles against, can be <code>null</code> if all roles should be returned.
+     * @return a list with all matching roles, can be empty, but never <code>null</code>.
+     */
+    public List getRoles(Filter filter) {
+        List matchingRoles = new ArrayList();
+
+        try {
+            Role[] roles = m_store.getAllRoles();
+            for (int i = 0; i < roles.length; i++) {
+                Role role = roles[i];
+                if (!isPredefinedRole(role) && ((filter == null) || filter.match(role.getProperties()))) {
+                    matchingRoles.add(wireChangeListener(role));
+                }
+            }
+        }
+        catch (IOException e) {
+            throw new BackendException("Failed to get roles!", e);
+        }
+
+        return matchingRoles;
+    }
+
+    /**
+     * Returns a collection with all roles matching a given key-value pair.
+     * 
+     * @param key the key to search for;
+     * @param value the value to search for.
+     * @return a list with all matching roles, can be empty, but never <code>null</code>.
+     */
+    public List getRoles(String key, String value) {
+        if (key == null) {
+            throw new IllegalArgumentException("Key cannot be null!");
+        }
+        if (value == null) {
+            throw new IllegalArgumentException("Value cannot be null!");
+        }
+
+        List matchingRoles = new ArrayList();
+
+        try {
+            Role[] roles = m_store.getAllRoles();
+            for (int i = 0; i < roles.length; i++) {
+                Role role = roles[i];
+                Dictionary dict = role.getProperties();
+                if (!isPredefinedRole(role) && value.equals(dict.get(key))) {
+                    matchingRoles.add(wireChangeListener(role));
+                }
+            }
+        }
+        catch (IOException e) {
+            throw new BackendException("Failed to get roles!", e);
+        }
+
+        return matchingRoles;
+    }
+
+    /**
+     * Removes a given role from this manager.
+     * 
+     * @param role the role to remove, cannot be <code>null</code>.
+     * @return <code>true</code> if the role was removed (i.e., it was managed by this manager), or <code>false</code> if it was not found.
+     */
+    public boolean removeRole(Role role) {
+        if (role == null) {
+            throw new IllegalArgumentException("Role cannot be null!");
+        }
+        if (!(role instanceof RoleImpl)) {
+            throw new IllegalArgumentException("Invalid role type!");
+        }
+
+        checkPermissions();
+
+        // Cannot remove predefined roles...
+        if (isPredefinedRole(role)) {
+            return false;
+        }
+
+        try {
+            if (m_store.removeRole(role)) {
+                unwireChangeListener(role);
+                m_roleChangeReflector.roleRemoved(role);
+                
+                return true;
+            }
+
+            return false;
+        }
+        catch (IOException e) {
+            throw new BackendException("Failed to remove role " + role.getName() + "!", e);
+        }
+    }
+
+    /**
+     * Removes the given role change listener from be called for changes in roles.
+     * 
+     * @param listener the listener to unregister, cannot be <code>null</code>.
+     * @throws IllegalArgumentException in case the given listener was <code>null</code>.
+     */
+    public void removeRoleChangeListener(RoleChangeListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("RoleChangeListener cannot be null!");
+        }
+
+        m_listeners.remove(listener);
+    }
+
+    /**
+     * Starts this repository.
+     */
+    public void start() {
+        try {
+            // The sole predefined role we've got...
+            m_store.addRole(USER_ANYONE);
+
+            m_store.initialize();
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+    
+    /**
+     * Stops this repository, allowing it to clean up.
+     */
+    public void stop() {
+        try {
+            m_store.close();
+        }
+        catch (IOException e) {
+            // Ignore; nothing we can do about this here...
+        }
+    }
+
+    /**
+     * Creates a new iterator for iterating over all listeners.
+     * 
+     * @return a new {@link Iterator} instance, never <code>null</code>. 
+     */
+    final Iterator createListenerIterator() {
+        return m_listeners.iterator();
+    }
+
+    /**
+     * Verifies whether the caller has the right permissions to add or remove roles.
+     * 
+     * @throws SecurityException in case the caller has not the right permissions to perform the action.
+     */
+    private void checkPermissions() throws SecurityException {
+        SecurityManager securityManager = System.getSecurityManager();
+        if (securityManager != null) {
+            securityManager.checkPermission(new UserAdminPermission(UserAdminPermission.ADMIN, null));
+        }
+    }
+    
+    /**
+     * Returns whether or not the given role is a predefined role.
+     * <p>
+     * Currently, there's only a single predefined role: {@link Role#USER_ANYONE}.
+     * </p>
+     * 
+     * @param role the role to check, may be <code>null</code>.
+     * @return <code>true</code> if the given role is predefined, <code>false</code> otherwise.
+     */
+    private boolean isPredefinedRole(Role role) {
+        return Role.USER_ANYONE.equals(role.getName());
+    }
+
+    /**
+     * Wires the given role to this repository so it can listen for its changes.
+     * 
+     * @param role the role to listen for its changes, cannot be <code>null</code>.
+     * @return the given role.
+     * @throws IllegalArgumentException in case the given object was not a {@link RoleImpl} instance.
+     */
+    private Role wireChangeListener(Object role) {
+        RoleImpl result = (RoleImpl) role;
+        if (result != null) {
+            result.setRoleChangeListener(m_roleChangeReflector);
+        }
+        return result;
+    }
+
+    /**
+     * Unwires the given role to this repository so it no longer listens for its changes.
+     * 
+     * @param role the role to unwire, cannot be <code>null</code>.
+     * @throws IllegalArgumentException in case the given object was not a {@link RoleImpl} instance.
+     */
+    private void unwireChangeListener(Object role) {
+        RoleImpl result = (RoleImpl) role;
+        result.setRoleChangeListener(null);
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminImpl.java
new file mode 100644
index 0000000..9e51c52
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminImpl.java
@@ -0,0 +1,210 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.util.List;
+
+import org.apache.felix.useradmin.RoleFactory;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceFactory;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdmin;
+import org.osgi.service.useradmin.UserAdminEvent;
+
+/**
+ * Provides the implementation for {@link UserAdmin}.
+ */
+public class UserAdminImpl implements ServiceFactory, UserAdmin, RoleChangeListener {
+    
+    private final RoleRepository m_roleRepository;
+    private final EventDispatcher m_eventDispatcher;
+
+    private volatile ServiceReference m_serviceRef;
+    
+    /**
+     * Creates a new {@link UserAdminImpl} implementation.
+     * 
+     * @param roleRepository the repository with roles to use for this service;
+     * @param eventDispatcher the event dispatcher to use for this service.
+     * 
+     * @throws IllegalArgumentException in case one of the given parameters was <code>null</code>.
+     */
+    public UserAdminImpl(RoleRepository roleRepository, EventDispatcher eventDispatcher) {
+        if (roleRepository == null) {
+            throw new IllegalArgumentException("RoleRepository cannot be null!");
+        }
+        if (eventDispatcher == null) {
+            throw new IllegalArgumentException("EventDispatcher cannot be null!");
+        }
+
+        m_roleRepository = roleRepository;
+        m_eventDispatcher = eventDispatcher;
+
+        m_roleRepository.addRoleChangeListener(this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Role createRole(String name, int type) {
+        if ((type != Role.USER) && (type != Role.GROUP)) {
+            throw new IllegalArgumentException("Invalid type, must by either Role.USER or Role.GROUP!");
+        }
+        if (name == null) {
+            throw new IllegalArgumentException("Invalid name, must be non-null and non-empty!");
+        }
+
+        return m_roleRepository.addRole(RoleFactory.createRole(type, name));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Authorization getAuthorization(User user) {
+        return new AuthorizationImpl(user, m_roleRepository);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Role getRole(String name) {
+        return m_roleRepository.getRoleByName(name);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Role[] getRoles(String filter) throws InvalidSyntaxException {
+        List roles = m_roleRepository.getRoles(createFilter(filter));
+        if (roles.isEmpty()) {
+            return null;
+        }
+        return (Role[]) roles.toArray(new Role[roles.size()]);
+    }
+    
+    /**
+     * {@inheritDoc}
+     * 
+     * <p>Overridden in order to get hold of our service reference.</p>
+     */
+    public Object getService(Bundle bundle, ServiceRegistration registration) {
+        m_serviceRef = registration.getReference();
+        return this;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public User getUser(String key, String value) {
+        User result = null;
+        List roles = m_roleRepository.getRoles(key, value);
+        if (roles.size() == 1) {
+            Role foundRole = (Role) roles.get(0);
+            if (foundRole.getType() == Role.USER) {
+                result = (User) foundRole;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void propertyAdded(Role role, Object key, Object value) {
+        m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_CHANGED, role));
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+        m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_CHANGED, role));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void propertyRemoved(Role role, Object key) {
+        m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_CHANGED, role));
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public boolean removeRole(String name) {
+        Role role = getRole(name);
+        if (role == null) {
+            return false;
+        }
+
+        return m_roleRepository.removeRole(role);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void roleAdded(Role role) {
+        m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_CREATED, role));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void roleRemoved(Role role) {
+        m_eventDispatcher.dispatch(createUserAdminEvent(UserAdminEvent.ROLE_REMOVED, role));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void ungetService(Bundle bundle, ServiceRegistration registration, Object service) {
+        // Nop; we leave the service as-is...
+    }
+    
+    /**
+     * Creates a {@link Filter} instance for the given OSGi/LDAP filter.
+     * 
+     * @param filter the filter to convert to a {@link Filter} instance.
+     * @return a {@link Filter} instance corresponding to the given filter string, never <code>null</code>.
+     * @throws InvalidSyntaxException in case the given filter was invalid.
+     */
+    protected Filter createFilter(String filter) throws InvalidSyntaxException {
+        if (filter == null || "".equals(filter.trim())) {
+            return null;
+        }
+        return FrameworkUtil.createFilter(filter);
+    }
+
+    /**
+     * Creates a new {@link UserAdminEvent} instance for the given type and role.
+     * 
+     * @param type the type of event to create;
+     * @param role the role to create the event for.
+     * @return a new {@link UserAdminEvent} instance, never <code>null</code>.
+     */
+    private UserAdminEvent createUserAdminEvent(int type, Role role) {
+        return new UserAdminEvent(m_serviceRef, type, role);
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminListenerList.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminListenerList.java
new file mode 100644
index 0000000..9afa6a0
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/UserAdminListenerList.java
@@ -0,0 +1,33 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Provides an abstraction for a list of {@link UserAdminListener}s.
+ */
+public interface UserAdminListenerList {
+
+    /**
+     * Returns all current listeners.
+     * 
+     * @return an array of {@link UserAdminListener}s, never <code>null</code>,
+     *         but can be an empty array.
+     */
+    UserAdminListener[] getListeners();
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/GroupImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/GroupImpl.java
new file mode 100644
index 0000000..6786a95
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/GroupImpl.java
@@ -0,0 +1,181 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides an implementation of {@link Group}. 
+ */
+public class GroupImpl extends UserImpl implements Group {
+
+    private static final String BASIC_MEMBER = "basicMember";
+    private static final String REQUIRED_MEMBER = "requiredMember";
+
+    private final Object m_lock = new Object();
+
+    private final Map m_members;
+    private final Map m_requiredMembers;
+
+    /**
+     * Creates a new {@link GroupImpl} instance of type {@link Role#GROUP}.
+     * 
+     * @param name the name of this group role, cannot be <code>null</code> or empty.
+     */
+    public GroupImpl(String name) {
+        super(Role.GROUP, name);
+        
+        m_members = new HashMap();
+        m_requiredMembers = new HashMap();
+    }
+
+    /**
+     * Creates a new {@link GroupImpl} instance of type {@link Role#GROUP}.
+     * 
+     * @param name the name of this group role, cannot be <code>null</code> or empty.
+     */
+    public GroupImpl(String name, Dictionary properties, Dictionary credentials) {
+        super(Role.GROUP, name, properties, credentials);
+
+        m_members = new HashMap();
+        m_requiredMembers = new HashMap();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean addMember(Role role) {
+        checkPermissions();
+        
+        Object result;
+        synchronized (m_lock) {
+            if (m_members.containsKey(role.getName()) || m_requiredMembers.containsKey(role.getName())) {
+                return false;
+            }
+            result = m_members.put(role.getName(), role);
+        }
+
+        if (result == null) {
+            // Notify our (optional) listener...
+            entryAdded(BASIC_MEMBER, role);
+        }
+        
+        return (result == null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean addRequiredMember(Role role) {
+        checkPermissions();
+
+        Object result;
+        synchronized (m_lock) {
+            if (m_requiredMembers.containsKey(role.getName()) || m_members.containsKey(role.getName())) {
+                return false;
+            }
+            result = m_requiredMembers.put(role.getName(), role);
+        }
+
+        if (result == null) {
+            // Notify our (optional) listener...
+            entryAdded(REQUIRED_MEMBER, role);
+        }
+        
+        return (result == null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Role[] getMembers() {
+        Role[] roles;
+        synchronized (m_lock) {
+            Collection values = m_members.values();
+            roles = (Role[]) values.toArray(new Role[values.size()]);
+        }
+
+        return (roles.length == 0) ? null : roles;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Role[] getRequiredMembers() {
+        Role[] roles;
+        synchronized (m_lock) {
+            Collection values = m_requiredMembers.values();
+            roles = (Role[]) values.toArray(new Role[values.size()]);
+        }
+
+        return (roles.length == 0) ? null : roles;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean removeMember(Role role) {
+        checkPermissions();
+
+        String key = null;
+        Object result = null;
+        
+        synchronized (m_lock) {
+            if (m_requiredMembers.containsKey(role.getName())) {
+                key = REQUIRED_MEMBER;
+                result = m_requiredMembers.remove(role.getName());
+            }
+            else if (m_members.containsKey(role.getName())) {
+                key = BASIC_MEMBER;
+                result = m_members.remove(role.getName());
+            }
+        }
+
+        if (result != null) {
+            // Notify our (optional) listener...
+            entryRemoved(key);
+        }
+        
+        return result != null;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String toString() {
+        return "Group(" + getName() + "): R{" + m_requiredMembers + "}, B{" + m_members + "}";
+    }
+
+    /**
+     * Verifies whether the caller has the right permissions to get or change the given key.
+     * 
+     * @throws SecurityException in case the caller has not the right permissions to perform the action.
+     */
+    private void checkPermissions() throws SecurityException {
+        SecurityManager securityManager = System.getSecurityManager();
+        if (securityManager != null) {
+            securityManager.checkPermission(new UserAdminPermission(UserAdminPermission.ADMIN, null));
+        }
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableDictionary.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableDictionary.java
new file mode 100644
index 0000000..a41ae14
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableDictionary.java
@@ -0,0 +1,311 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides an observable {@link Dictionary} implementation that emits change 
+ * events for the put and remove operations aside checking for security 
+ * permissions for all accessor methods.
+ */
+class ObservableDictionary extends Dictionary implements Serializable {
+
+    private static final long serialVersionUID = 9223154895541178975L;
+
+    /**
+     * Provides a listener for changes to a {@link ObservableDictionary}.
+     */
+    static interface DictionaryChangeListener {
+
+        /**
+         * Called when a new entry is added.
+         * 
+         * @param key the key of the entry;
+         * @param value the value associated to the key.
+         */
+        void entryAdded(Object key, Object value);
+        
+        /**
+         * Called when an entry is changed.
+         * 
+         * @param key the key of the entry;
+         * @param oldValue the old value associated to the key;
+         * @param newValue the new value associated to the key.
+         */
+        void entryChanged(Object key, Object oldValue, Object newValue);
+        
+        /**
+         * Called when an entry is removed.
+         * 
+         * @param key the key of the entry.
+         */
+        void entryRemoved(Object key);
+    }
+
+    /**
+     * Provides a wrapper to convert an {@link Iterator} to an {@link Enumeration} implementation.
+     */
+    static final class IteratorEnumeration implements Enumeration {
+        
+        private final Iterator m_iterator;
+
+        /**
+         * Creates a new {@link IteratorEnumeration}.
+         * 
+         * @param iterator the {@link Iterator} to convert to a {@link Enumeration}, cannot be <code>null</code>.
+         */
+        public IteratorEnumeration(Iterator iterator) {
+            m_iterator = iterator;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public boolean hasMoreElements() {
+            return m_iterator.hasNext();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public Object nextElement() {
+            return m_iterator.next();
+        }
+    }
+    
+    /**
+     * Converts a given {@link Dictionary} implementation to a {@link Map} implementation.
+     * 
+     * @param dictionary the dictionary to convert, cannot be <code>null</code>.
+     * @return a {@link Map} instance with all the same key-value pairs as the given dictionary, never <code>null</code>.
+     */
+    private static ConcurrentMap convertToMap(Dictionary dictionary) {
+        ConcurrentMap result = new ConcurrentHashMap();
+        if (dictionary instanceof Map) {
+            result.putAll((Map) dictionary);
+        } else {
+            Enumeration keyEnum = dictionary.keys();
+            while (keyEnum.hasMoreElements()) {
+                Object key = keyEnum.nextElement();
+                result.put(key, dictionary.get(key));
+            }
+        }
+        return result;
+    }
+    
+    private final ConcurrentMap m_properties;
+    private final String m_getAction;
+    private final String m_changeAction;
+
+    private transient volatile DictionaryChangeListener m_listener;
+
+    /**
+     * Creates a new, empty, {@link ObservableDictionary} instance.
+     */
+    public ObservableDictionary(String getAction, String changeAction) {
+        m_getAction = getAction;
+        m_changeAction = changeAction;
+        m_properties = new ConcurrentHashMap();
+    }
+
+    /**
+     * Creates a new {@link ObservableDictionary} instance with the given dictionary as defaults.
+     * 
+     * @param dictionary the defaults to set for this properties, cannot be <code>null</code>.
+     */
+    public ObservableDictionary(String getAction, String changeAction, Dictionary dictionary) {
+        if (dictionary == null) {
+            throw new IllegalArgumentException("Dictionary cannot be null!");
+        }
+        m_getAction = getAction;
+        m_changeAction = changeAction;
+        m_properties = convertToMap(dictionary);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Enumeration elements() {
+        Collection values = m_properties.values();
+        return new IteratorEnumeration(values.iterator());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean equals(Object object) {
+        if (this == object) {
+            return true;
+        }
+        if (object == null || (getClass() != object.getClass())) {
+            return false;
+        }
+
+        ObservableDictionary other = (ObservableDictionary) object;
+        if (m_properties == null) {
+            if (other.m_properties != null) {
+                return false;
+            }
+        } else if (!m_properties.equals(other.m_properties)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Object get(Object key) {
+        if (key == null) {
+            throw new IllegalArgumentException("Key cannot be null!");
+        }
+
+        if (m_getAction != null) {
+            checkPermissions(getAsPermissionKey(key), m_getAction);
+        }
+
+        return m_properties.get(key);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int hashCode() {
+        final int prime = 37;
+        int result = 1;
+        result = prime * result + ((m_properties == null) ? 0 : m_properties.hashCode());
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isEmpty() {
+        return m_properties.isEmpty();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Enumeration keys() {
+        Collection keys = m_properties.keySet();
+        return new IteratorEnumeration(keys.iterator());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Object put(Object key, Object value) {
+        if (key == null) {
+            throw new IllegalArgumentException("Key cannot be null!");
+        }
+        if (value == null) {
+            throw new IllegalArgumentException("Value cannot be null!");
+        }
+
+        if (m_changeAction != null) {
+            checkPermissions(getAsPermissionKey(key), m_changeAction);
+        }
+
+        Object oldValue = m_properties.put(key, value);
+        
+        final DictionaryChangeListener listener = m_listener;
+        if (listener != null) {
+            if (oldValue == null) {
+                listener.entryAdded(key, value);
+            } else {
+                listener.entryChanged(key, oldValue, value);
+            }
+        }
+
+        return oldValue;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Object remove(Object key) {
+        if (key == null) {
+            throw new IllegalArgumentException("Key cannot be null!");
+        }
+
+        if (m_changeAction != null) {
+            checkPermissions(getAsPermissionKey(key), m_changeAction);
+        }
+
+        Object oldValue = m_properties.remove(key);
+        final DictionaryChangeListener listener = m_listener;
+        if (listener != null) {
+            listener.entryRemoved(key);
+        }
+        
+        return oldValue;
+    }
+
+    /**
+     * Sets a new {@link DictionaryChangeListener} to observe changes to this dictionary.
+     * 
+     * @param listener the listener to add, can be <code>null</code> to stop listening for changes.
+     */
+    public void setDictionaryChangeListener(DictionaryChangeListener listener) {
+        m_listener = listener;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public int size() {
+        return m_properties.size();
+    }
+
+    /**
+     * @param key
+     * @return
+     */
+    protected String getAsPermissionKey(Object key) {
+        String k = UserAdminPermission.ADMIN;
+        if (key instanceof String) {
+            k = (String) key;
+        }
+        return k;
+    }
+
+    /**
+     * Verifies whether the caller has the right permissions to get or change the given key.
+     * 
+     * @param key the name of the property that is to be accessed or changed, cannot be <code>null</code>;
+     * @param action the action name to perform, cannot be <code>null</code>.
+     * @throws SecurityException in case the caller has not the right permissions to perform the action.
+     */
+    private void checkPermissions(String key, String action) throws SecurityException {
+        SecurityManager securityManager = System.getSecurityManager();
+        if (securityManager != null) {
+            securityManager.checkPermission(new UserAdminPermission(key, action));
+        }
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableProperties.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableProperties.java
new file mode 100644
index 0000000..de05dc9
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/ObservableProperties.java
@@ -0,0 +1,80 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.util.Dictionary;
+
+/**
+ * Provides an stricter variant of the {@link ObservableDictionary} that only 
+ * permits string keys and values of either String or byte[]. 
+ */
+final class ObservableProperties extends ObservableDictionary {
+
+    private static final long serialVersionUID = -2513082903921734796L;
+
+    /**
+     * Creates a new, empty, {@link ObservableProperties} instance.
+     */
+    public ObservableProperties(String getAction, String changeAction) {
+        super(getAction, changeAction);
+    }
+
+    /**
+     * Creates a new {@link ObservableProperties} instance with the given dictionary as defaults.
+     * 
+     * @param dictionary the defaults to set for this properties, cannot be <code>null</code>.
+     */
+    public ObservableProperties(String getAction, String changeAction, Dictionary dictionary) {
+        super(getAction, changeAction, dictionary);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Object get(Object key) {
+        if (!(key instanceof String)) {
+            throw new IllegalArgumentException("Key must be of type String!");
+        }
+
+        return super.get(key);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Object put(Object key, Object value) {
+        if (!(key instanceof String)) {
+            throw new IllegalArgumentException("Key must be of type String!");
+        }
+        if (!(value instanceof String) && !(value instanceof byte[])) {
+            throw new IllegalArgumentException("Value must be of type String or byte[]!");
+        }
+
+        return super.put(key, value);
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Object remove(Object key) {
+        if (!(key instanceof String)) {
+            throw new IllegalArgumentException("Key must be of type String!");
+        }
+
+        return super.remove(key);
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/RoleImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/RoleImpl.java
new file mode 100644
index 0000000..2541968
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/RoleImpl.java
@@ -0,0 +1,185 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.util.Dictionary;
+
+
+import org.apache.felix.useradmin.impl.RoleChangeListener;
+import org.apache.felix.useradmin.impl.role.ObservableDictionary.DictionaryChangeListener;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides an implementation of {@link Role}.
+ */
+public class RoleImpl implements Role, DictionaryChangeListener {
+
+    private final ObservableProperties m_properties;
+    private final String m_name;
+    private final int m_type;
+    
+    private volatile RoleChangeListener m_listener;
+
+    /**
+     * Creates a new {@link RoleImpl} instance of type {@link Role#ROLE} and a given name.
+     * 
+     * @param name the name of this role, cannot be <code>null</code> or empty.
+     */
+    public RoleImpl(String name) {
+        this(Role.ROLE, name);
+    }
+
+    /**
+     * Creates a new {@link RoleImpl} instance with a given type and name.
+     * 
+     * @param type the type of this role, should be {@link Role#ROLE}, {@link Role#USER} or {@link Role#GROUP};
+     * @param name the name of this role, cannot be <code>null</code> or empty.
+     */
+    protected RoleImpl(int type, String name) {
+        if (name == null || "".equals(name.trim())) {
+            throw new IllegalArgumentException("Name cannot be null or empty!");
+        }
+        m_type = type;
+        m_name = name;
+        m_properties = new ObservableProperties(null, UserAdminPermission.CHANGE_PROPERTY);
+        m_properties.setDictionaryChangeListener(this);
+    }
+
+    /**
+     * Creates a new {@link RoleImpl} instance with a given type, name and properties.
+     * 
+     * @param type the type of this role, should be {@link Role#ROLE}, {@link Role#USER} or {@link Role#GROUP};
+     * @param name the name of this role, cannot be <code>null</code> or empty;
+     * @param properties the initial properties of this role, cannot be <code>null</code>.
+     */
+    protected RoleImpl(int type, String name, Dictionary properties) {
+        if (name == null || "".equals(name.trim())) {
+            throw new IllegalArgumentException("Name cannot be null or empty!");
+        }
+        m_type = type;
+        m_name = name;
+        m_properties = new ObservableProperties(null, UserAdminPermission.CHANGE_PROPERTY, properties);
+        m_properties.setDictionaryChangeListener(this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public final void entryAdded(Object key, Object value) {
+        RoleChangeListener listener = m_listener;
+        if (listener != null) {
+            listener.propertyAdded(this, key, value);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public final void entryRemoved(Object key) {
+        RoleChangeListener listener = m_listener;
+        if (listener != null) {
+            listener.propertyRemoved(this, key);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public final void entryChanged(Object key, Object oldValue, Object newValue) {
+        RoleChangeListener listener = m_listener;
+        if (listener != null) {
+            listener.propertyChanged(this, key, oldValue, newValue);
+        }
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if ((obj == null) || (getClass() != obj.getClass())) {
+            return false;
+        }
+
+        RoleImpl other = (RoleImpl) obj;
+        if (m_name == null) {
+            if (other.m_name != null) {
+                return false;
+            }
+        } else if (!m_name.equals(other.m_name)) {
+            return false;
+        }
+
+        if (m_type != other.m_type) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String getName() {
+        return m_name;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Dictionary getProperties() {
+        return m_properties;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int getType() {
+        return m_type;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((m_name == null) ? 0 : m_name.hashCode());
+        result = prime * result + m_type;
+        return result;
+    }
+
+    /**
+     * Sets the {@link RoleChangeListener} for this role implementation.
+     * 
+     * @param listener the listener to set, may be <code>null</code> to stop listening.
+     */
+    public void setRoleChangeListener(RoleChangeListener listener) {
+        m_listener = listener;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String toString() {
+        return "Role(" + getName() + ")";
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/UserImpl.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/UserImpl.java
new file mode 100644
index 0000000..d8cffc4
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/impl/role/UserImpl.java
@@ -0,0 +1,119 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.util.Arrays;
+import java.util.Dictionary;
+
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+/**
+ * Provides an implementation of {@link User}.
+ */
+public class UserImpl extends RoleImpl implements User {
+
+    private final ObservableProperties m_credentials;
+
+    /**
+     * Creates a new {@link UserImpl} instance with type {@link Role#USER}.
+     * 
+     * @param name the name of this user role, cannot be <code>null</code> or empty.
+     */
+    public UserImpl(String name) {
+        this(Role.USER, name);
+    }
+
+    /**
+     * Creates a new {@link UserImpl} instance with a given type.
+     *
+     * @param type the type of this role;
+     * @param name the name of this role, cannot be <code>null</code> or empty.
+     */
+    protected UserImpl(int type, String name) {
+        super(type, name);
+
+        m_credentials = new ObservableProperties(UserAdminPermission.GET_CREDENTIAL, UserAdminPermission.CHANGE_CREDENTIAL);
+        m_credentials.setDictionaryChangeListener(this);
+    }
+
+    /**
+     * Creates a new {@link UserImpl} instance with type {@link Role#USER}.
+     * 
+     * @param name the name of this user role, cannot be <code>null</code> or empty.
+     */
+    protected UserImpl(int type, String name, Dictionary properties, Dictionary credentials) {
+        super(type, name, properties);
+
+        m_credentials = new ObservableProperties(UserAdminPermission.GET_CREDENTIAL, UserAdminPermission.CHANGE_CREDENTIAL, credentials);
+        m_credentials.setDictionaryChangeListener(this);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Dictionary getCredentials() {
+        return m_credentials;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean hasCredential(String key, Object value) {
+        // Will throw a SecurityException if we're not allowed to do this!
+        Object result = m_credentials.get(key);
+
+        // Be a bit more lenient with the various results we can get...
+        if (result instanceof String) {
+            String s1 = (String) result;
+            String s2;
+            if (value instanceof byte[]) {
+                s2 = new String((byte[]) value);
+            } else if (value instanceof String) {
+                s2 = (String) value;
+            } else {
+                // Not a string or a byte-array!
+                return false;
+            }
+            
+            return s1.equals(s2);
+        } else if (result instanceof byte[]) {
+            byte[] b1 = (byte[]) result;
+            byte[] b2;
+            if (value instanceof byte[]) {
+                b2 = (byte[]) value;
+            } else if (value instanceof String) {
+                b2 = ((String) value).getBytes();
+            } else {
+                // Not a string or a byte-array!
+                return false;
+            }
+
+            return Arrays.equals(b1, b2);
+        }
+
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String toString() {
+        return "User(" + getName() + ")";
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/Activator.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/Activator.java
new file mode 100644
index 0000000..1b99b89
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/Activator.java
@@ -0,0 +1,76 @@
+/**
+ *  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.felix.useradmin.osgi;
+
+import org.apache.felix.useradmin.impl.EventDispatcher;
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.apache.felix.useradmin.impl.UserAdminImpl;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * Provides the bundle activator for the UserAdmin service.
+ */
+public class Activator implements BundleActivator {
+
+    private volatile ServiceContext m_context;
+
+    /**
+     * {@inheritDoc}
+     */
+    public void start(BundleContext context) throws Exception {
+        m_context = createServiceContext(context);
+        
+        // The actual service itself...
+        UserAdminImpl service = new UserAdminImpl(m_context.m_roleRepository, m_context.m_eventDispatcher);
+        
+        // Register the actual service...
+        context.registerService(UserAdmin.class.getName(), service, null);
+        
+        // Start/open all helper classes...
+        m_context.start();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void stop(BundleContext context) throws Exception {
+        if (m_context != null) {
+            // Stop/close all helper classes...
+            m_context.stop();
+        }
+    }
+    
+    /**
+     * Creates a new service context in which all helper classes are kept.
+     * 
+     * @param context the bundle context to use, cannot be <code>null</code>.
+     * @return a new, initialized, ServiceContext instance, never <code>null</code>.
+     */
+    private ServiceContext createServiceContext(BundleContext context) {
+        // Create all services...
+        EventAdminHelper eventAdmin = new EventAdminHelper(context);
+        UserAdminListenerListHelper listenerList = new UserAdminListenerListHelper(context);
+        EventDispatcher eventDispatcher = new EventDispatcher(eventAdmin, listenerList);
+        RoleRepositoryStoreHelper store = new RoleRepositoryStoreHelper(context);
+
+        RoleRepository roleRepository = new RoleRepository(store);
+
+        return new ServiceContext(eventAdmin, listenerList, eventDispatcher, roleRepository, store);
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/EventAdminHelper.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/EventAdminHelper.java
new file mode 100644
index 0000000..fc73e3f
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/EventAdminHelper.java
@@ -0,0 +1,68 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.felix.useradmin.osgi;
+
+import org.osgi.framework.BundleContext;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Wraps a {@link ServiceTracker} as an {@link EventAdmin} allowing it to be 
+ * used as a "normal" service without having to worry about the actual presence 
+ * of the {@link EventAdmin} service itself. 
+ */
+final class EventAdminHelper extends ServiceTracker implements EventAdmin {
+
+    /**
+     * Creates a new {@link EventAdminHelper} instance.
+     * 
+     * @param context the bundle context to use, cannot be <code>null</code>.
+     */
+    public EventAdminHelper(BundleContext context) {
+        super(context, EventAdmin.class.getName(), null /* customizer */);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void postEvent(Event event) {
+        EventAdmin eventAdmin = getEventAdmin();
+        if (eventAdmin != null) {
+            eventAdmin.postEvent(event);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void sendEvent(Event event) {
+        EventAdmin eventAdmin = getEventAdmin();
+        if (eventAdmin != null) {
+            eventAdmin.sendEvent(event);
+        }
+    }
+
+    /**
+     * Returns the event admin service, if available.
+     * 
+     * @return the current event admin service, or <code>null</code> if it is not present.
+     */
+    private EventAdmin getEventAdmin() {
+        return (EventAdmin) getService();
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/RoleRepositoryStoreHelper.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/RoleRepositoryStoreHelper.java
new file mode 100644
index 0000000..d817ca7
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/RoleRepositoryStoreHelper.java
@@ -0,0 +1,123 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.felix.useradmin.osgi;
+
+import java.io.IOException;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.useradmin.Role;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Provides an OSGi service tracker for {@link RoleRepositoryStore}.
+ * <p>
+ * This helper allows us to use {@link RoleRepositoryStore} without having to 
+ * worry about the possible absence of the actual store implementation.
+ * </p>
+ */
+class RoleRepositoryStoreHelper extends ServiceTracker implements RoleRepositoryStore {
+    
+    /**
+     * Creates a new {@link RoleRepositoryStoreHelper} instance.
+     * 
+     * @param context the bundle context to use, cannot be <code>null</code>.
+     */
+    public RoleRepositoryStoreHelper(BundleContext context) {
+        super(context, RoleRepositoryStore.class.getName(), null /* customizer */);
+    }
+
+    public boolean addRole(Role role) throws IOException {
+        RoleRepositoryStore store = getStore();
+        if (store != null) {
+            return store.addRole(role);
+        }
+
+        return false;
+    }
+    
+    public synchronized void close() {
+        try {
+            RoleRepositoryStore store = getStore();
+            if (store != null) {
+                store.close();
+            }
+        }
+        catch (IOException e) {
+            // Ignore; nothing we can do about this here...
+        } finally {
+            super.close();
+        }
+    }
+    
+    public Role[] getAllRoles() throws IOException {
+        RoleRepositoryStore store = getStore();
+        if (store != null) {
+            return store.getAllRoles();
+        }
+
+        return new Role[0];
+    }
+
+    public Role getRoleByName(String roleName) throws IOException {
+        RoleRepositoryStore store = getStore();
+        if (store != null) {
+            return store.getRoleByName(roleName);
+        }
+
+        return null;
+    }
+
+    public void initialize() throws IOException {
+        RoleRepositoryStore store = getStore();
+        if (store != null) {
+            store.initialize();
+        }
+    }
+
+    public void removedService(ServiceReference reference, Object service) {
+        RoleRepositoryStore removedStore = (RoleRepositoryStore) service;
+        try {
+            removedStore.close();
+        }
+        catch (IOException e) {
+            // Ignore; nothing we can do about this here...
+        }
+
+        super.removedService(reference, service);
+    }
+
+    public boolean removeRole(Role role) throws IOException {
+        // and possibly also from our tracked store...
+        RoleRepositoryStore store = getStore();
+        if (store != null) {
+            return store.removeRole(role);
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the tracked {@link RoleRepositoryStore}.
+     * 
+     * @return the {@link RoleRepositoryStore}, can be <code>null</code>.
+     */
+    private RoleRepositoryStore getStore() {
+        return (RoleRepositoryStore) getService();
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/ServiceContext.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/ServiceContext.java
new file mode 100644
index 0000000..1d87ffd
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/ServiceContext.java
@@ -0,0 +1,68 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.felix.useradmin.osgi;
+
+
+import org.apache.felix.useradmin.impl.EventDispatcher;
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.osgi.service.useradmin.UserAdmin;
+
+/**
+ * Provides a convenience class for all helpers that are used for this
+ * implementation of the {@link UserAdmin} service.
+ */
+final class ServiceContext {
+
+    final EventAdminHelper m_eventAdmin;
+    final UserAdminListenerListHelper m_listenerList;
+    final EventDispatcher m_eventDispatcher;
+    final RoleRepository m_roleRepository;
+    final RoleRepositoryStoreHelper m_store;
+
+    /**
+     * Creates a new ServiceContext instance.
+     */
+    public ServiceContext(EventAdminHelper eventAdmin, UserAdminListenerListHelper listenerList, EventDispatcher eventDispatcher, RoleRepository roleRepository, RoleRepositoryStoreHelper store) {
+        m_eventAdmin = eventAdmin;
+        m_listenerList = listenerList;
+        m_eventDispatcher = eventDispatcher;
+        m_roleRepository = roleRepository;
+        m_store = store;
+    }
+
+    /**
+     * Starts/opens all helpers.
+     */
+    public void start() {
+        m_eventAdmin.open();
+        m_listenerList.open(true /* trackAllServices */);
+        m_eventDispatcher.start();
+        m_roleRepository.start();
+        m_store.open();
+    }
+    
+    /**
+     * Stops/closes all helpers.
+     */
+    public void stop() {
+        m_roleRepository.stop();
+        m_eventDispatcher.stop();
+        m_listenerList.close();
+        m_eventAdmin.close();
+        m_store.close();
+    }
+}
diff --git a/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/UserAdminListenerListHelper.java b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/UserAdminListenerListHelper.java
new file mode 100644
index 0000000..1b19f74
--- /dev/null
+++ b/useradmin/useradmin/src/main/java/org/apache/felix/useradmin/osgi/UserAdminListenerListHelper.java
@@ -0,0 +1,57 @@
+/**
+ *  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.felix.useradmin.osgi;
+
+
+import org.apache.felix.useradmin.impl.UserAdminListenerList;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.useradmin.UserAdminListener;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * Wraps a {@link ServiceTracker} as an {@link UserAdminListenerList} to allow 
+ * it to be used as a "normal" service without having to worry about the 
+ * existence of any {@link UserAdminListener}s.
+ */
+final class UserAdminListenerListHelper extends ServiceTracker implements UserAdminListenerList {
+
+    /**
+     * Creates a new {@link UserAdminListenerListHelper} instance.
+     * 
+     * @param context the bundle context to use, cannot be <code>null</code>.
+     */
+    public UserAdminListenerListHelper(BundleContext context) {
+        super(context, UserAdminListener.class.getName(), null /* customizer */);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public UserAdminListener[] getListeners() {
+        Object[] services = getServices();
+        if (services == null) {
+            return new UserAdminListener[0];
+        }
+
+        UserAdminListener[] result = new UserAdminListener[services.length];
+        for (int i = 0; i < services.length; i++) {
+            result[i] = (UserAdminListener) services[i];
+        }
+
+        return result;
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/AuthorizationImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/AuthorizationImplTest.java
new file mode 100644
index 0000000..7cc1c39
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/AuthorizationImplTest.java
@@ -0,0 +1,285 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Group;
+
+/**
+ * Test cases for {@link AuthorizationImpl}.
+ */
+public class AuthorizationImplTest extends TestCase {
+
+    private RoleRepository m_roleManager;
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+        
+        m_roleManager = new RoleRepository(new MemoryRoleRepositoryStore());
+    }
+    
+    /**
+     * Test for example presented in section 107.3.2 of OSGi compendium v4.2.
+     */
+    public void testAuthorizationExampleOk() {
+        // Action groups...
+        Group alarmSystemControl = createGroup("AlarmSystemControl");
+        Group internetAccess = createGroup("InternetAccess");
+        Group temperatureControl = createGroup("TemperatureControl");
+        Group photoAlbumEdit = createGroup("PhotoAlbumEdit");
+        Group photoAlbumView = createGroup("PhotoAlbumView");
+        Group portForwarding = createGroup("PortForwarding");
+        
+        // System user groups...
+        Group administrators = createGroup("Administrators");
+        Group buddies = createGroup("Buddies");
+        Group children = createGroup("Children");
+        Group adults = createGroup("Adults");
+        Group residents = createGroup("Residents");
+        
+        // Users
+        UserImpl elmer = new UserImpl("Elmer");
+        UserImpl fudd = new UserImpl("Fudd");
+        UserImpl marvin = new UserImpl("Marvin");
+        UserImpl pepe = new UserImpl("Pepe");
+        UserImpl daffy = new UserImpl("Daffy");
+        UserImpl foghorn = new UserImpl("Foghorn");
+        
+        // Not explicitly mentioned; but needed to comply with the semantics
+        alarmSystemControl.addRequiredMember(RoleRepository.USER_ANYONE);
+        internetAccess.addRequiredMember(RoleRepository.USER_ANYONE);
+        temperatureControl.addRequiredMember(RoleRepository.USER_ANYONE);
+        photoAlbumEdit.addRequiredMember(RoleRepository.USER_ANYONE);
+        photoAlbumView.addRequiredMember(RoleRepository.USER_ANYONE);
+        portForwarding.addRequiredMember(RoleRepository.USER_ANYONE);
+
+        administrators.addRequiredMember(RoleRepository.USER_ANYONE);
+        buddies.addRequiredMember(RoleRepository.USER_ANYONE);
+        children.addRequiredMember(RoleRepository.USER_ANYONE);
+        adults.addRequiredMember(RoleRepository.USER_ANYONE);
+        residents.addRequiredMember(RoleRepository.USER_ANYONE);
+
+        // Table 107.1
+        residents.addMember(elmer);
+        residents.addMember(fudd);
+        residents.addMember(marvin);
+        residents.addMember(pepe);
+        
+        buddies.addMember(daffy);
+        buddies.addMember(foghorn);
+        
+        children.addMember(marvin);
+        children.addMember(pepe);
+        
+        adults.addMember(elmer);
+        adults.addMember(fudd);
+        
+        administrators.addMember(elmer);
+        
+        // Table 107.2
+        alarmSystemControl.addMember(residents);
+        alarmSystemControl.addRequiredMember(administrators);
+        
+        internetAccess.addMember(residents);
+        internetAccess.addRequiredMember(adults);
+        
+        temperatureControl.addMember(residents);
+        temperatureControl.addRequiredMember(adults);
+        
+        photoAlbumEdit.addMember(residents);
+        photoAlbumEdit.addMember(children);
+        photoAlbumEdit.addMember(adults);
+        
+        photoAlbumView.addMember(residents);
+        photoAlbumView.addMember(buddies);
+        
+        portForwarding.addMember(residents);
+        portForwarding.addRequiredMember(administrators);
+
+        // Test with the user "foghorn"...
+        AuthorizationImpl auth = new AuthorizationImpl(foghorn, m_roleManager);
+
+        assertFalse(auth.hasRole(alarmSystemControl.getName()));
+        assertFalse(auth.hasRole(internetAccess.getName()));
+        assertFalse(auth.hasRole(temperatureControl.getName()));
+        assertFalse(auth.hasRole(photoAlbumEdit.getName()));
+        assertTrue(auth.hasRole(photoAlbumView.getName()));
+        assertFalse(auth.hasRole(portForwarding.getName()));
+
+        // Test with the user "fudd"...
+        auth = new AuthorizationImpl(fudd, m_roleManager);
+
+        assertFalse(auth.hasRole(alarmSystemControl.getName()));
+        assertTrue(auth.hasRole(internetAccess.getName()));
+        assertTrue(auth.hasRole(temperatureControl.getName()));
+        assertTrue(auth.hasRole(photoAlbumEdit.getName()));
+        assertTrue(auth.hasRole(photoAlbumView.getName()));
+        assertFalse(auth.hasRole(portForwarding.getName()));
+
+        // Test with the user "elmer"...
+        auth = new AuthorizationImpl(elmer, m_roleManager);
+
+        assertTrue(auth.hasRole(alarmSystemControl.getName()));
+        assertTrue(auth.hasRole(internetAccess.getName()));
+        assertTrue(auth.hasRole(temperatureControl.getName()));
+        assertTrue(auth.hasRole(photoAlbumEdit.getName()));
+        assertTrue(auth.hasRole(photoAlbumView.getName()));
+        assertTrue(auth.hasRole(portForwarding.getName()));
+    }
+
+    /**
+     * Test that the tests for membership work correctly. 
+     */
+    public void testHasRoleOk() {
+        GroupImpl citizens = createGroup("citizen");
+        citizens.addRequiredMember(RoleRepository.USER_ANYONE);
+        
+        GroupImpl adults = createGroup("adult");
+        adults.addRequiredMember(RoleRepository.USER_ANYONE);
+        
+        GroupImpl voters = createGroup("voter");
+        voters.addRequiredMember(citizens);
+        voters.addRequiredMember(adults);
+        voters.addMember(RoleRepository.USER_ANYONE);
+        
+        // Elmer belongs to the citizens and adults...
+        UserImpl elmer = createUser("elmer");
+        citizens.addMember(elmer);
+        adults.addMember(elmer);
+        
+        // Pepe belongs to the citizens, but is not an adult...
+        UserImpl pepe = createUser("pepe");
+        citizens.addMember(pepe);
+        
+        // Bugs is an adult, but is not a citizen...
+        UserImpl bugs = createUser("bugs");
+        adults.addMember(bugs);
+        
+        // Daffy is not an adult, neither a citizen...
+        UserImpl daffy = createUser("daffy");
+        
+        AuthorizationImpl auth;
+
+        auth = new AuthorizationImpl(elmer, m_roleManager);
+        assertTrue(auth.hasRole("adult"));
+        assertTrue(auth.hasRole("citizen"));
+        assertTrue(auth.hasRole("voter"));
+        assertFalse(auth.hasRole("non-existing-role"));
+
+        auth = new AuthorizationImpl(pepe, m_roleManager);
+        assertFalse(auth.hasRole("adult"));
+        assertTrue(auth.hasRole("citizen"));
+        assertFalse(auth.hasRole("voter"));
+        assertFalse(auth.hasRole("non-existing-role"));
+
+        auth = new AuthorizationImpl(bugs, m_roleManager);
+        assertTrue(auth.hasRole("adult"));
+        assertFalse(auth.hasRole("citizen"));
+        assertFalse(auth.hasRole("voter"));
+        assertFalse(auth.hasRole("non-existing-role"));
+
+        auth = new AuthorizationImpl(daffy, m_roleManager);
+        assertFalse(auth.hasRole("adult"));
+        assertFalse(auth.hasRole("citizen"));
+        assertFalse(auth.hasRole("voter"));
+        assertFalse(auth.hasRole("non-existing-role"));
+    }
+
+    /**
+     * Test that the tests for membership work correctly. 
+     */
+    public void testGetRolesOk() {
+        GroupImpl citizens = createGroup("citizen");
+        citizens.addRequiredMember(RoleRepository.USER_ANYONE);
+        
+        GroupImpl adults = createGroup("adult");
+        adults.addRequiredMember(RoleRepository.USER_ANYONE);
+        
+        GroupImpl voters = createGroup("voter");
+        voters.addRequiredMember(citizens);
+        voters.addRequiredMember(adults);
+        voters.addMember(RoleRepository.USER_ANYONE);
+        
+        // Elmer belongs to the citizens and adults...
+        UserImpl elmer = createUser("elmer");
+        citizens.addMember(elmer);
+        adults.addMember(elmer);
+        
+        // Pepe belongs to the citizens, but is not an adult...
+        UserImpl pepe = createUser("pepe");
+        citizens.addMember(pepe);
+        
+        // Bugs is an adult, but is not a citizen...
+        UserImpl bugs = createUser("bugs");
+        adults.addMember(bugs);
+
+        // Daffy is not an adult, neither a citizen...
+        UserImpl daffy = createUser("daffy");
+
+        // Daffy is not an adult, neither a citizen...
+        UserImpl donald = new UserImpl("donald");
+        
+        AuthorizationImpl auth;
+
+        auth = new AuthorizationImpl(elmer, m_roleManager);
+        assertSameRoles(new String[]{ "elmer", "adult", "citizen", "voter" }, auth.getRoles());
+
+        auth = new AuthorizationImpl(pepe, m_roleManager);
+        assertSameRoles(new String[]{ "pepe", "citizen" }, auth.getRoles());
+
+        auth = new AuthorizationImpl(bugs, m_roleManager);
+        assertSameRoles(new String[]{ "bugs", "adult" }, auth.getRoles());
+
+        auth = new AuthorizationImpl(daffy, m_roleManager);
+        assertSameRoles(new String[]{ "daffy" }, auth.getRoles());
+
+        auth = new AuthorizationImpl(donald, m_roleManager);
+        assertNull(auth.getRoles());
+    }
+
+    private void assertSameRoles(String[] expected, String[] roles) {
+        assertTrue("Expected " + expected.length + " roles, got " + roles.length + "!", expected.length == roles.length);
+        
+        List e = new ArrayList(Arrays.asList(expected));
+        List r = new ArrayList(Arrays.asList(roles));
+        e.removeAll(r);
+        
+        assertTrue("Not seen: " + e, e.isEmpty());
+    }
+
+    private GroupImpl createGroup(String name) {
+        GroupImpl result = new GroupImpl(name);
+        m_roleManager.addRole(result);
+        return result;
+    }
+
+    private UserImpl createUser(String name) {
+        UserImpl result = new UserImpl(name);
+        m_roleManager.addRole(result);
+        return result;
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/EventDispatcherTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/EventDispatcherTest.java
new file mode 100644
index 0000000..4f89d12
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/EventDispatcherTest.java
@@ -0,0 +1,296 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import junit.framework.TestCase;
+
+
+import org.apache.felix.useradmin.impl.EventDispatcher;
+import org.apache.felix.useradmin.impl.UserAdminListenerList;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.UserAdminEvent;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Test case for {@link EventDispatcher}.
+ */
+public class EventDispatcherTest extends TestCase {
+    
+    /**
+     * Provides a {@link EventAdmin} that counts the number of times 
+     * {@link #postEvent(Event)} is called.
+     */
+    static class CountingEventAdmin implements EventAdmin {
+        
+        private final CountDownLatch m_latch;
+        
+        public CountingEventAdmin(CountDownLatch latch) {
+            m_latch = latch;
+        }
+        
+        public void postEvent(Event event) {
+            if (m_latch != null) {
+                m_latch.countDown();
+            }
+        }
+
+        public void sendEvent(Event event) {
+            throw new RuntimeException("Should not be called for asynchronous delivery!");
+        }
+    }
+    
+    /**
+     * Provides a {@link UserAdminListener} that counts the number of times 
+     * {@link #roleChanged(UserAdminEvent)} is called.
+     */
+    static class CountingUserAdminListener implements UserAdminListener {
+        
+        private final CountDownLatch m_latch;
+        
+        public CountingUserAdminListener(CountDownLatch latch) {
+            m_latch = latch;
+        }
+
+        public void roleChanged(UserAdminEvent event) {
+            if (m_latch != null) {
+                m_latch.countDown();
+            }
+        }
+    }
+
+    /**
+     * Implements a fake service reference.
+     */
+    static class FakeServiceReference implements ServiceReference {
+        
+        private final Properties m_props;
+
+        public FakeServiceReference() {
+            this(new String[0]);
+        }
+
+        public FakeServiceReference(String[] properties) {
+            m_props = new Properties();
+            
+            for (int i = 0; i < properties.length; i += 2) {
+                m_props.put(properties[i], properties[i + 1]);
+            }
+        }
+
+        public Bundle getBundle() {
+            return null;
+        }
+
+        public Object getProperty(String key) {
+            return key;
+        }
+
+        public String[] getPropertyKeys() {
+            return new String[0];
+        }
+
+        public Bundle[] getUsingBundles() {
+            return null;
+        }
+
+        public boolean isAssignableTo(Bundle bundle, String className) {
+            return false;
+        }
+    }
+
+    private EventDispatcher m_dispatcher;
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#dispatch(org.osgi.service.useradmin.UserAdminEvent)}.
+     */
+    public void testDispatchEventCallsEventAdminOk() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(5);
+        
+        m_dispatcher = createEventDispatcher(new CountingEventAdmin(latch));
+        m_dispatcher.start();
+
+        for (int i = 0; i < 5; i++) {
+            m_dispatcher.dispatch(createMockEvent(UserAdminEvent.ROLE_CHANGED));
+        }
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#dispatch(org.osgi.service.useradmin.UserAdminEvent)}.
+     */
+    public void testDispatchEventCallsUserAdminListenersOk() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(5);
+        
+        m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+        m_dispatcher.start();
+
+        for (int i = 0; i < 5; i++) {
+            m_dispatcher.dispatch(createMockEvent(UserAdminEvent.ROLE_CHANGED));
+        }
+
+        assertTrue(latch.await(5, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#start()}.
+     */
+    public void testStartTwiceDoesNotMatter() throws Exception {
+        final CountDownLatch latch = new CountDownLatch(1);
+        
+        m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+        m_dispatcher.start();
+        
+        assertTrue(m_dispatcher.isRunning());
+
+        m_dispatcher.start();
+            
+        assertTrue(m_dispatcher.isRunning());
+
+        assertEquals("Latch should not be decremented?!", 1L, latch.getCount());
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#stop()}.
+     */
+    public void testStopOk() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        
+        m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+        m_dispatcher.start();
+
+        m_dispatcher.stop();
+
+        m_dispatcher.dispatch(createMockEvent(UserAdminEvent.ROLE_CHANGED));
+        
+        assertFalse(m_dispatcher.isRunning());
+
+        assertEquals("Latch should not be decremented?!", 1L, latch.getCount());
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#stop()}.
+     */
+    public void testStopUnstartedDoesNotMatter() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        
+        m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+        
+        assertFalse(m_dispatcher.isRunning());
+
+        m_dispatcher.stop();
+        
+        assertFalse(m_dispatcher.isRunning());
+
+        assertEquals("Latch should not be decremented?!", 1L, latch.getCount());
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.EventDispatcher#stop()}.
+     */
+    public void testStopTwiceDoesNotMatter() {
+        final CountDownLatch latch = new CountDownLatch(1);
+        
+        m_dispatcher = createEventDispatcher(new CountingUserAdminListener(latch));
+        m_dispatcher.start();
+        
+        assertTrue(m_dispatcher.isRunning());
+
+        m_dispatcher.stop();
+        
+        assertFalse(m_dispatcher.isRunning());
+        
+        m_dispatcher.stop();
+
+        assertEquals("Latch should not be decremented?!", 1L, latch.getCount());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void tearDown() throws Exception {
+        if (m_dispatcher != null) {
+            try {
+                m_dispatcher.stop();
+                m_dispatcher = null;
+            } catch (IllegalStateException e) {
+                // Not a problem; already stopped...
+            }
+        }
+    }
+
+    /**
+     * @param eventAdmin
+     * @param listenerList
+     * @return
+     */
+    private EventDispatcher createEventDispatcher(EventAdmin eventAdmin) {
+        return new EventDispatcher(eventAdmin, createListenerList());        
+    }
+
+    /**
+     * @param eventAdmin
+     * @param listenerList
+     * @return
+     */
+    private EventDispatcher createEventDispatcher(UserAdminListener listener) {
+        return new EventDispatcher(new CountingEventAdmin(null), createListenerList(listener));        
+    }
+    
+    /**
+     * @param listener
+     * @return
+     */
+    private UserAdminListenerList createListenerList() {
+        return new UserAdminListenerList() {
+            public UserAdminListener[] getListeners() {
+                return new UserAdminListener[0];
+            }
+        };
+    }
+
+    /**
+     * @param listener
+     * @return
+     */
+    private UserAdminListenerList createListenerList(final UserAdminListener listener) {
+        return new UserAdminListenerList() {
+            public UserAdminListener[] getListeners() {
+                return new UserAdminListener[] { listener };
+            }
+        };
+    }
+    
+    /**
+     * @param type
+     * @return
+     */
+    private UserAdminEvent createMockEvent(int type) {
+        Role user = new UserImpl("user-" + System.currentTimeMillis());
+        ServiceReference ref = new FakeServiceReference();
+        return new UserAdminEvent(ref, type, user);
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/MemoryRoleRepositoryStore.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/MemoryRoleRepositoryStore.java
new file mode 100644
index 0000000..8a0b7c3
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/MemoryRoleRepositoryStore.java
@@ -0,0 +1,74 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.felix.useradmin.RoleRepositoryStore;
+import org.osgi.service.useradmin.Role;
+
+
+/**
+ * Provides a simple in-memory role repository store.
+ */
+public class MemoryRoleRepositoryStore implements RoleRepositoryStore {
+    
+    protected final ConcurrentMap m_entries = new ConcurrentHashMap();
+
+    public boolean addRole(Role role) throws IOException {
+        if (role == null) {
+            throw new IllegalArgumentException("Role cannot be null!");
+        }
+        Object result = m_entries.putIfAbsent(role.getName(), role);
+        return result == null;
+    }
+
+    public void close() throws IOException {
+        // Nop
+    }
+
+    public void flush() throws IOException {
+        // Nop
+    }
+
+    public Role[] getAllRoles() throws IOException {
+        Collection roles = m_entries.values();
+        Role[] result = new Role[roles.size()];
+        return (Role[]) roles.toArray(result);
+    }
+
+    public Role getRoleByName(String roleName) throws IOException {
+        if (roleName == null) {
+            throw new IllegalArgumentException("Role name cannot be null!");
+        }
+        return (Role) m_entries.get(roleName);
+    }
+    
+    public void initialize() throws IOException {
+        // Nop
+    }
+
+    public boolean removeRole(Role role) throws IOException {
+        if (role == null) {
+            throw new IllegalArgumentException("Role cannot be null!");
+        }
+        return m_entries.remove(role.getName(), role);
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleCheckerTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleCheckerTest.java
new file mode 100644
index 0000000..191f6f1
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleCheckerTest.java
@@ -0,0 +1,214 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import org.apache.felix.useradmin.impl.RoleChecker;
+import org.apache.felix.useradmin.impl.RoleRepository;
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Test cases for {@link RoleChecker}.
+ */
+public class RoleCheckerTest extends TestCase {
+    
+    private RoleChecker m_roleChecker;
+
+    /**
+     * Tests that a user always implies itself.
+     */
+    public void testUserAlwaysImpliesItself() {
+        UserImpl user = new UserImpl("foo");
+        
+        assertTrue(m_roleChecker.isImpliedBy(user, user));
+    }
+
+    /**
+     * Test that a user does never imply a group to which it is not a member.
+     */
+    public void testUserDoesNotImplyNotImpliedGroup() {
+        UserImpl user = new UserImpl("foo");
+        GroupImpl group = new GroupImpl("bar");
+        
+        assertFalse(m_roleChecker.isImpliedBy(user, group));
+    }
+
+    /**
+     * Test that a user does never imply a group to which it is not a member.
+     */
+    public void testUserImpliesImpliedGroup() {
+        UserImpl user = new UserImpl("foo");
+        
+        GroupImpl group = new GroupImpl("bar");
+        group.addRequiredMember(RoleRepository.USER_ANYONE);
+        group.addMember(user);
+
+        assertTrue(m_roleChecker.isImpliedBy(group, user));
+    }
+
+    /**
+     * Test that a user does never imply a group to which it is not a member.
+     */
+    public void testGroupDoesNotImplyNotImpliedUser() {
+        UserImpl user = new UserImpl("foo");
+        
+        GroupImpl group = new GroupImpl("bar");
+        group.addMember(user);
+        
+        assertFalse(m_roleChecker.isImpliedBy(user, group));
+    }
+
+    /**
+     * Test that a group does never imply a group to which it is a required member.
+     */
+    public void testGroupDoesNotImplySameRequiredGroup() {
+        UserImpl user = new UserImpl("foo");
+        
+        GroupImpl group = new GroupImpl("bar");
+        group.addRequiredMember(group);
+        group.addMember(user);
+        
+        assertFalse(m_roleChecker.isImpliedBy(group, group));
+    }
+
+    /**
+     * Test that a group does never imply a group to which it is a basic member.
+     */
+    public void testGroupDoesNotImplySameGroup() {
+        UserImpl user = new UserImpl("foo");
+        
+        GroupImpl group = new GroupImpl("bar");
+        group.addMember(group);
+        group.addMember(user);
+        
+        assertFalse(m_roleChecker.isImpliedBy(group, group));
+    }
+
+    /**
+     * Test that a membership can be implied for users belonging to multiple required groups.
+     */
+    public void testRequiredRolesMultipleRequiredGroupsOk() {
+        UserImpl elmer = new UserImpl("elmer");
+        UserImpl pepe = new UserImpl("pepe");
+        UserImpl bugs = new UserImpl("bugs");
+        UserImpl daffy = new UserImpl("daffy");
+        
+        GroupImpl administrators = new GroupImpl("administrators");
+        administrators.addRequiredMember(RoleRepository.USER_ANYONE);
+        administrators.addMember(elmer);
+        administrators.addMember(pepe);
+        administrators.addMember(bugs);
+
+        GroupImpl family = new GroupImpl("family");
+        family.addRequiredMember(RoleRepository.USER_ANYONE);
+        family.addMember(elmer);
+        family.addMember(pepe);
+        family.addMember(daffy);
+
+        GroupImpl alarmSystemActivation = new GroupImpl("alarmSystemActivation");
+        alarmSystemActivation.addMember(RoleRepository.USER_ANYONE);
+        alarmSystemActivation.addRequiredMember(administrators);
+        alarmSystemActivation.addRequiredMember(family);
+
+        assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, elmer));
+        assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, pepe));
+        assertFalse(m_roleChecker.isImpliedBy(alarmSystemActivation, bugs));
+        assertFalse(m_roleChecker.isImpliedBy(alarmSystemActivation, daffy));
+    }
+
+    /**
+     * Test that a membership can be implied for users belonging to multiple non-required groups.
+     */
+    public void testRequiredRolesMultipleGroupsOk() {
+        UserImpl elmer = new UserImpl("elmer");
+        UserImpl pepe = new UserImpl("pepe");
+        UserImpl bugs = new UserImpl("bugs");
+        UserImpl daffy = new UserImpl("daffy");
+        
+        GroupImpl administrators = new GroupImpl("administrators");
+        administrators.addRequiredMember(RoleRepository.USER_ANYONE);
+        administrators.addMember(elmer);
+        administrators.addMember(pepe);
+        administrators.addMember(bugs);
+
+        GroupImpl family = new GroupImpl("family");
+        family.addRequiredMember(RoleRepository.USER_ANYONE);
+        family.addMember(elmer);
+        family.addMember(pepe);
+        family.addMember(daffy);
+
+        GroupImpl alarmSystemActivation = new GroupImpl("alarmSystemActivation");
+        alarmSystemActivation.addRequiredMember(RoleRepository.USER_ANYONE);
+        alarmSystemActivation.addMember(administrators);
+        alarmSystemActivation.addMember(family);
+
+        assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, elmer));
+        assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, pepe));
+        assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, bugs));
+        assertTrue(m_roleChecker.isImpliedBy(alarmSystemActivation, daffy));
+    }
+
+    /**
+     * Test that a membership can be implied for users belonging to multiple non-required groups.
+     */
+    public void testVotersRequiredMembersOk() {
+        GroupImpl citizens = new GroupImpl("citizen");
+        citizens.addRequiredMember(RoleRepository.USER_ANYONE);
+        
+        GroupImpl adults = new GroupImpl("adult");
+        adults.addRequiredMember(RoleRepository.USER_ANYONE);
+        
+        GroupImpl voters = new GroupImpl("voter");
+        voters.addRequiredMember(citizens);
+        voters.addRequiredMember(adults);
+        voters.addMember(RoleRepository.USER_ANYONE);
+        
+        
+        // Elmer belongs to the citizens and adults...
+        UserImpl elmer = new UserImpl("elmer");
+        citizens.addMember(elmer);
+        adults.addMember(elmer);
+        
+        // Pepe belongs to the citizens, but is not an adult...
+        UserImpl pepe = new UserImpl("pepe");
+        citizens.addMember(pepe);
+        
+        // Bugs is an adult, but is not a citizen...
+        UserImpl bugs = new UserImpl("bugs");
+        adults.addMember(bugs);
+        
+        // Daffy is not an adult, neither a citizen...
+        UserImpl daffy = new UserImpl("daffy");
+
+        assertTrue(m_roleChecker.isImpliedBy(voters, elmer));
+        assertFalse(m_roleChecker.isImpliedBy(voters, pepe));
+        assertFalse(m_roleChecker.isImpliedBy(voters, bugs));
+        assertFalse(m_roleChecker.isImpliedBy(voters, daffy));
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+        
+        m_roleChecker = new RoleChecker();
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositorySecurityTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositorySecurityTest.java
new file mode 100644
index 0000000..c6f1185
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositorySecurityTest.java
@@ -0,0 +1,143 @@
+/**
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.felix.useradmin.impl;
+
+import java.security.Permission;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.framework.FilterImpl;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Test cases for {@link RoleRepository}.
+ */
+public class RoleRepositorySecurityTest extends TestCase {
+
+    private RoleRepository m_roleManager;
+    private TestSecurityManager m_securityManager;
+
+    /**
+     * Tests that adding a role with the right permissions works.
+     */
+    public void testAddRoleWithPermissionsOk() throws SecurityException {
+        m_securityManager.m_allowed = true;
+       
+        m_roleManager.addRole(RoleRepository.USER_ANYONE);
+    }
+
+    /**
+     * Tests that adding a role without the right permissions does not work.
+     */
+    public void testAddRoleWithoutPermissionsFails() throws SecurityException {
+        try {
+            m_roleManager.addRole(RoleRepository.USER_ANYONE);
+            
+            fail("Expected SecurityException!");
+        } catch (SecurityException e) {
+            // Ok; expected...
+        }
+    }
+
+    /**
+     * Tests that getting roles by their name works without any permissions.
+     */
+    public void testGetRoleByNameOk() throws SecurityException {
+        m_roleManager.getRoleByName(Role.USER_ANYONE);
+    }
+
+    /**
+     * Tests that getting roles by key-value pairs works without any permissions.
+     */
+    public void testGetRolesWithKeyValueOk() throws SecurityException {
+        m_roleManager.getRoles("key", "value");
+    }
+
+    /**
+     * Tests that getting roles with filters works without any permissions.
+     */
+    public void testGetRolesWithFilterOk() throws Exception {
+        m_roleManager.getRoles(new FilterImpl("(key=value)"));
+    }
+
+    /**
+     * Tests that removing a role with the right permissions works.
+     */
+    public void testRemoveRoleWithPermissionsOk() throws SecurityException {
+        m_securityManager.m_allowed = true;
+        
+        m_roleManager.removeRole(RoleRepository.USER_ANYONE);
+    }
+
+    /**
+     * Tests that remove a role without the right permissions does not work.
+     */
+    public void testRemoveRoleWithoutPermissionsFails() throws SecurityException {
+        try {
+            m_roleManager.removeRole(RoleRepository.USER_ANYONE);
+            
+            fail("Expected SecurityException!");
+        } catch (SecurityException e) {
+            // Ok; expected...
+        }
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        m_roleManager = new RoleRepository(new MemoryRoleRepositoryStore());
+        
+        m_securityManager = new TestSecurityManager();
+        System.setSecurityManager(m_securityManager);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        
+        System.setSecurityManager(null);
+    }
+
+    /**
+     * Provides a test security manager.
+     */
+    static final class TestSecurityManager extends SecurityManager {
+        
+        volatile boolean m_allowed = false;
+
+        public void checkPermission(Permission perm) {
+            // Cannot use instanceof as it requires a special permission as well...
+            if ("UserAdminPermission".equals(perm.getClass().getSimpleName())) {
+                String name = perm.getName();
+                if ("admin".equals(name) && !m_allowed) {
+                    throw new SecurityException("Not allowed!");
+                }
+            }
+            // Do not check for other permissions...
+        }
+
+        public void checkPermission(Permission perm, Object context) {
+            // Do not check for other permissions...
+        }
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositoryTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositoryTest.java
new file mode 100644
index 0000000..1003bfa
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/RoleRepositoryTest.java
@@ -0,0 +1,570 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.framework.FilterImpl;
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.framework.Filter;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Test cases for {@link RoleRepository}.
+ */
+public class RoleRepositoryTest extends TestCase {
+
+    private RoleRepository m_roleManager;
+    private CountDownLatch m_latch;
+    
+    /**
+     * Tests whether adding a new role to a group causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testAddBasicRoleYieldsEventOk() throws Exception {
+        final GroupImpl role = (GroupImpl) m_roleManager.addRole(new GroupImpl("foo"));
+        
+        m_latch = new CountDownLatch(1);
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.addMember(RoleRepository.USER_ANYONE);
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Tests that adding a null {@link RoleChangeListener} instance does not work.
+     */
+    public void testAddNullRoleChangeListenerFail() throws Exception {
+        try {
+            m_roleManager.addRoleChangeListener(null);
+            fail("Expected IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected...
+        }
+    }
+
+    /**
+     * Tests that adding a predefined role is not allowed.
+     */
+    public void testAddPredefineRoleFails() {
+        Role role = RoleRepository.USER_ANYONE;
+        assertNull(m_roleManager.addRole(role));
+        assertEquals(0, m_roleManager.getRoles(null).size());
+    }
+
+    /**
+     * Tests whether adding a new role to a group causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testAddRequiredRoleYieldsEventOk() throws Exception {
+        final GroupImpl role = (GroupImpl) m_roleManager.addRole(new GroupImpl("foo"));
+
+        m_latch = new CountDownLatch(1);
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.addRequiredMember(RoleRepository.USER_ANYONE);
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Tests that adding a valid {@link RoleChangeListener} instance works.
+     */
+    public void testAddRoleChangeListenerOk() throws Exception {
+        // Should succeed...
+        m_roleManager.addRoleChangeListener(new RoleChangeListener() {
+            public void propertyAdded(Role role, Object key, Object value) {
+            }
+            
+            public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+            }
+            
+            public void propertyRemoved(Role role, Object key) {
+            }
+            
+            public void roleAdded(Role role) {
+            }
+            
+            public void roleRemoved(Role role) {
+            }
+        });
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#addRole(Role)}.
+     */
+    public void testAddRoleOfSameTypeTwiceFail() {
+        UserImpl role = new UserImpl("foo");
+
+        assertSame(role, m_roleManager.addRole(role));
+        assertEquals(1, m_roleManager.getRoles(null).size());
+
+        assertNull(m_roleManager.addRole(role));
+        assertEquals(1, m_roleManager.getRoles(null).size());
+    }
+
+    /**
+     * Tests that adding a role works.
+     */
+    public void testAddRoleOk() {
+        UserImpl role = new UserImpl("foo");
+        assertSame(role, m_roleManager.addRole(role));
+        assertEquals(1, m_roleManager.getRoles(null).size());
+    }
+
+    /**
+     * Tests whether adding a new property to a role causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testAddRolePropertyYieldsEventOk() throws Exception {
+        m_latch = new CountDownLatch(1);
+        
+        final Role role = m_roleManager.addRole(new UserImpl("john.doe"));
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.getProperties().put("key", "value");
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Tests that adding a role that does not inherit from RoleImpl does not work and yields an exception.
+     */
+    public void testAddRoleWithInvalidRoleFail() {
+        try {
+            m_roleManager.addRole(new Role() {
+                public String getName() {
+                    return "A User";
+                }
+                public Dictionary getProperties() {
+                    return new Properties();
+                }
+                public int getType() {
+                    return Role.USER;
+                }
+            });
+            
+            fail("Exception expected!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that adding a null-role does not work and yields an exception.
+     */
+    public void testAddRoleWithNullRoleFail() {
+        try {
+            m_roleManager.addRole(null);
+            
+            fail("Exception expected!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#addRole(Role)}.
+     */
+    public void testAddRoleWithSameNameTwiceFail() {
+        UserImpl role1 = new UserImpl("foo");
+        GroupImpl role2 = new GroupImpl("foo");
+
+        assertSame(role1, m_roleManager.addRole(role1));
+        assertEquals(1, m_roleManager.getRoles(null).size());
+
+        assertNull(m_roleManager.addRole(role2));
+        assertEquals(1, m_roleManager.getRoles(null).size());
+    }
+
+    /**
+     * Tests whether adding a new credential to a user causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testAddUserCredentialYieldsEventOk() throws Exception {
+        m_latch = new CountDownLatch(1);
+        
+        final UserImpl role = (UserImpl) m_roleManager.addRole(new UserImpl("john.doe"));
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.getCredentials().put("key", "value");
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Tests whether changing an existing property to a role causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testChangeRolePropertyYieldsEventOk() throws Exception {
+        final Role role = m_roleManager.addRole(new UserImpl("john.doe"));
+        role.getProperties().put("key", "value");
+        
+        m_latch = new CountDownLatch(1);
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.getProperties().put("key", "other-value");
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Tests whether changing an existing credential for a user causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testChangeUserCredentialYieldsEventOk() throws Exception {
+        final UserImpl role = (UserImpl) m_roleManager.addRole(new UserImpl("john.doe"));
+        role.getCredentials().put("key", "value");
+        
+        m_latch = new CountDownLatch(1);
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.getCredentials().put("key", "other-value");
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoleByName(java.lang.String)}.
+     */
+    public void testGetRoleByName() {
+        Role role1 = m_roleManager.addRole(new UserImpl("foo"));
+        Role role2 = m_roleManager.addRole(new GroupImpl("bar"));
+
+        assertSame(role1, m_roleManager.getRoleByName("foo"));
+        assertSame(role2, m_roleManager.getRoleByName("bar"));
+        assertNull(m_roleManager.getRoleByName("qux"));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoles(org.osgi.framework.Filter)}.
+     */
+    public void testGetRolesWithFilterOk() throws Exception {
+        Role role1 = m_roleManager.addRole(new UserImpl("foo"));
+        role1.getProperties().put("key", "value1");
+        role1.getProperties().put("keyA", "valueA");
+        Role role2 = m_roleManager.addRole(new GroupImpl("bar"));
+        role2.getProperties().put("key", "value2");
+        role2.getProperties().put("keyB", "value1");
+        
+        Filter filter;
+
+        filter = new FilterImpl("(key=value1)");
+        assertSameRoles(new Role[]{ role1 }, m_roleManager.getRoles(filter));
+
+        filter = new FilterImpl("(key=value2)");
+        assertSameRoles(new Role[]{ role2 }, m_roleManager.getRoles(filter));
+
+        filter = new FilterImpl("(key=value*)");
+        assertSameRoles(new Role[]{ role1, role2 }, m_roleManager.getRoles(filter));
+
+        filter = new FilterImpl("(|(key=value1)(keyB=value1))");
+        assertSameRoles(new Role[]{ role1, role2 }, m_roleManager.getRoles(filter));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoles(String, String)}.
+     */
+    public void testGetRolesWithKeyValuePairOk() throws Exception {
+        Role role1 = m_roleManager.addRole(new UserImpl("foo"));
+        role1.getProperties().put("key", "value1");
+        role1.getProperties().put("keyA", "valueA");
+        Role role2 = m_roleManager.addRole(new GroupImpl("bar"));
+        role2.getProperties().put("key", "value2");
+        role2.getProperties().put("keyB", "value1");
+        
+        assertSameRoles(new Role[]{ role1 }, m_roleManager.getRoles("key", "value1"));
+        assertSameRoles(new Role[]{ role2 }, m_roleManager.getRoles("key", "value2"));
+        assertSameRoles(new Role[0], m_roleManager.getRoles("key", "value"));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoles(org.osgi.framework.Filter)}.
+     */
+    public void testGetRolesWithoutFilterOk() {
+        Role role1 = m_roleManager.addRole(new UserImpl("foo"));
+        Role role2 = m_roleManager.addRole(new GroupImpl("bar"));
+        
+        assertSameRoles(new Role[]{ role2, role1 }, m_roleManager.getRoles(null));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#getRoleByName(java.lang.String)}.
+     */
+    public void testGetUserAnyoneRoleByName() {
+        assertSame(RoleRepository.USER_ANYONE, m_roleManager.getRoleByName(Role.USER_ANYONE));
+    }
+
+    /**
+     * Tests whether removing a role from a group causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testRemoveBasicRoleYieldsEventOk() throws Exception {
+        final GroupImpl role = (GroupImpl) m_roleManager.addRole(new GroupImpl("foo"));
+        role.addMember(RoleRepository.USER_ANYONE);
+        
+        m_latch = new CountDownLatch(1);
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.removeMember(RoleRepository.USER_ANYONE);
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#removeRole(Role)}.
+     */
+    public void testRemoveExistingRoleOk() {
+        UserImpl role = new UserImpl("foo");
+        assertSame(role, m_roleManager.addRole(role));
+        
+        assertTrue(m_roleManager.removeRole(role));
+        assertEquals(0, m_roleManager.getRoles(null).size());
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#removeRole(Role)}.
+     */
+    public void testRemoveNonExistingRoleOk() {
+        UserImpl role1 = new UserImpl("foo");
+        UserImpl role2 = new UserImpl("bar");
+        assertSame(role1, m_roleManager.addRole(role1));
+
+        assertFalse(m_roleManager.removeRole(role2));
+        assertEquals(1, m_roleManager.getRoles(null).size());
+    }
+
+    /**
+     * Tests that removing a null {@link RoleChangeListener} instance does not work.
+     */
+    public void testRemoveNullRoleChangeListenerFail() throws Exception {
+        try {
+            m_roleManager.removeRoleChangeListener(null);
+            fail("Expected IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected...
+        }
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.RoleRepository#removeRole(Role)}.
+     */
+    public void testRemovePredefinedRoleFails() {
+        m_roleManager.addRole(new UserImpl("foo"));
+
+        Role role = RoleRepository.USER_ANYONE;
+        assertFalse(m_roleManager.removeRole(role));
+
+        assertEquals(1, m_roleManager.getRoles(null).size());
+    }
+
+    /**
+     * Tests whether removing a role from a group causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testRemoveRequiredRoleYieldsEventOk() throws Exception {
+        final GroupImpl role = (GroupImpl) m_roleManager.addRole(new GroupImpl("foo"));
+        role.addRequiredMember(RoleRepository.USER_ANYONE);
+        
+        m_latch = new CountDownLatch(1);
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.removeMember(RoleRepository.USER_ANYONE);
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Tests that removing a valid {@link RoleChangeListener} instance works.
+     */
+    public void testRemoveRoleChangeListenerOk() throws Exception {
+        // Should succeed...
+        m_roleManager.removeRoleChangeListener(new RoleChangeListener() {
+            public void propertyAdded(Role role, Object key, Object value) {
+            }
+            
+            public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+            }
+            
+            public void propertyRemoved(Role role, Object key) {
+            }
+            
+            public void roleAdded(Role role) {
+            }
+            
+            public void roleRemoved(Role role) {
+            }
+        });
+    }
+
+    /**
+     * Tests whether removing a property from a role causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testRemoveRolePropertyYieldsEventOk() throws Exception {
+        final Role role = m_roleManager.addRole(new UserImpl("john.doe"));
+        role.getProperties().put("key", "value");
+        
+        m_latch = new CountDownLatch(1);
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.getProperties().remove("key");
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * Tests that adding a role that does not inherit from RoleImpl does not work and yields an exception.
+     */
+    public void testRemoveRoleWithInvalidRoleFail() {
+        try {
+            m_roleManager.removeRole(new Role() {
+                public String getName() {
+                    return "A User";
+                }
+                public Dictionary getProperties() {
+                    return new Properties();
+                }
+                public int getType() {
+                    return Role.USER;
+                }
+            });
+            
+            fail("Exception expected!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that removing a null-role does not work and yields an exception.
+     */
+    public void testRemoveRoleWithNullRoleFail() {
+        try {
+            m_roleManager.removeRole(null);
+            
+            fail("Exception expected!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests whether removing a credential from a user causes an event to be emitted to the {@link RoleRepository}.
+     */
+    public void testRemoveUserCredentialYieldsEventOk() throws Exception {
+        final UserImpl role = (UserImpl) m_roleManager.addRole(new UserImpl("john.doe"));
+        role.getCredentials().put("key", "value");
+        
+        m_latch = new CountDownLatch(1);
+        
+        new Thread(new Runnable() {
+            public void run() {
+                role.getCredentials().remove("key");
+            };
+        }).start();
+
+        assertTrue(m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        m_roleManager = new RoleRepository(new MemoryRoleRepositoryStore());
+        m_roleManager.addRoleChangeListener(new RoleChangeListener() {
+            public void propertyAdded(Role role, Object key, Object value) {
+                if (m_latch != null) {
+                    m_latch.countDown();
+                }
+            }
+
+            public void propertyChanged(Role role, Object key, Object oldValue, Object newValue) {
+                if (m_latch != null) {
+                    m_latch.countDown();
+                }
+            }
+            
+            public void propertyRemoved(Role role, Object key) {
+                if (m_latch != null) {
+                    m_latch.countDown();
+                }
+            }
+
+            public void roleAdded(Role role) {
+                if (m_latch != null) {
+                    m_latch.countDown();
+                }
+            }
+
+            public void roleRemoved(Role role) {
+                if (m_latch != null) {
+                    m_latch.countDown();
+                }
+            }
+        });
+        
+        m_roleManager.start();
+    }
+
+    /**
+     * Asserts that a given collection of roles has the correct expected contents.
+     * 
+     * @param expected
+     * @param roles
+     */
+    private void assertSameRoles(Role[] expected, Collection roles) {
+        assertTrue("Expected " + expected.length + " roles, got " + roles.size() + "!", expected.length == roles.size());
+        
+        List e = new ArrayList(Arrays.asList(expected));
+        e.removeAll(roles);
+        
+        assertTrue("Not seen: " + e, e.isEmpty());
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/UserAdminImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/UserAdminImplTest.java
new file mode 100644
index 0000000..4d895bf
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/UserAdminImplTest.java
@@ -0,0 +1,814 @@
+/**
+ *  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.felix.useradmin.impl;
+
+import java.util.Arrays;
+import java.util.List;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.framework.FilterImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.framework.Filter;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.service.useradmin.Authorization;
+import org.osgi.service.useradmin.Group;
+import org.osgi.service.useradmin.Role;
+import org.osgi.service.useradmin.User;
+import org.osgi.service.useradmin.UserAdminListener;
+
+/**
+ * Test cases for {@link UserAdminImpl}. 
+ */
+public class UserAdminImplTest extends TestCase {
+    
+    /**
+     * Provides a stub implementation for {@link EventAdmin}.
+     */
+    static class StubEventAdmin implements EventAdmin {
+        
+        public void postEvent(Event event) {
+        }
+
+        public void sendEvent(Event event) {
+        }
+    }
+
+    /**
+     * Provides a stub implementation for {@link UserAdminListenerList}.
+     */
+    static class StubUserAdminListenerList implements UserAdminListenerList {
+
+        public UserAdminListener[] getListeners() {
+            return new UserAdminListener[0];
+        }
+    }
+
+    private UserAdminImpl m_userAdmin;
+    private RoleRepository m_roleRepository;
+    private EventDispatcher m_dispatcher;
+
+    /**
+     * Tests that adding a basic member to a group works.
+     */
+    public void testAddGroupMemberOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+        
+        assertTrue(group1.addMember(user1));
+        assertFalse(group1.addMember(user1));
+    }
+
+    /**
+     * Tests that adding a required member to a group works.
+     */
+    public void testAddRequiredGroupMemberOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+        
+        assertTrue(group1.addRequiredMember(user1));
+        assertFalse(group1.addRequiredMember(user1));
+    }
+    
+    /**
+     * Tests that adding a property of an invalid type to a role does not work and yields an exception.
+     */
+    public void testAddRolePropertyOfInvalidTypeFail() {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        
+        try {
+            user1.getProperties().put("key", Integer.valueOf(1));
+            
+            fail("Expected IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that adding a property to a role works.
+     */
+    public void testAddRolePropertyOk() {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+
+        assertNull(user1.getProperties().get("key"));
+        
+        user1.getProperties().put("key", "value");
+
+        assertEquals("value", user1.getProperties().get("key"));
+    }
+
+    /**
+     * Tests that adding a credential of an invalid type to a user does not work and yields an exception.
+     */
+    public void testAddUserCredentialInvalidTypeFails() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        
+        try {
+            user1.getCredentials().put("key", Integer.valueOf(1));
+            
+            fail("Expected IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that adding a String credential to a user works.
+     */
+    public void testAddUserCredentialOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+
+        assertNull(user1.getCredentials().get("key"));
+
+        user1.getCredentials().put("key", "value");
+
+        assertEquals("value", user1.getCredentials().get("key"));
+        
+        user1.getCredentials().put("key2", "value2".getBytes());
+
+        assertTrue(Arrays.equals("value2".getBytes(), (byte[]) user1.getCredentials().get("key2")));
+    }
+
+    /**
+     * Tests that testing for basic group membership works.
+     */
+    public void testBasicGroupMembershipOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        User user2 = (User) m_userAdmin.createRole("user2", Role.USER);
+        User user3 = (User) m_userAdmin.createRole("user3", Role.USER);
+
+        Group reqGroup = (Group) m_userAdmin.createRole("reqGroup", Role.GROUP);
+        reqGroup.addMember(user1);
+        reqGroup.addMember(user2);
+        reqGroup.addMember(user3);
+        
+        Group group = (Group) m_userAdmin.createRole("group", Role.GROUP);
+        group.addRequiredMember(reqGroup);
+        group.addMember(user1);
+        group.addMember(user2);
+
+        Authorization auth = m_userAdmin.getAuthorization(user1);
+        assertTrue(auth.hasRole("group"));
+        
+        auth = m_userAdmin.getAuthorization(user2);
+        assertTrue(auth.hasRole("group"));
+        
+        auth = m_userAdmin.getAuthorization(user3);
+        assertFalse(auth.hasRole("group"));
+    }
+
+    /**
+     * Tests that changing a property to an invalid type does not work and yields an exception.
+     */
+    public void testChangeRolePropertyOfInvalidTypeFail() {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key", "value");
+        
+        try {
+            user1.getProperties().put("key", Integer.valueOf(1));
+            
+            fail("Expected IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that changing a property of a role works.
+     */
+    public void testChangeRolePropertyOk() {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key", "value");
+
+        assertEquals("value", user1.getProperties().get("key"));
+        
+        user1.getProperties().put("key", "changed");
+
+        assertEquals("changed", user1.getProperties().get("key"));
+    }
+
+    /**
+     * Tests that changing a credential of a user works.
+     */
+    public void testChangeUserCredentialOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        user1.getCredentials().put("key", "value");
+
+        assertEquals("value", user1.getCredentials().get("key"));
+        
+        user1.getCredentials().put("key", "changed");
+
+        assertEquals("changed", user1.getCredentials().get("key"));
+    }
+
+    /**
+     * Tests that creating an existing group does not succeed and yields null.
+     */
+    public void testCreateExistingGroupFail() {
+        Role role = null;
+
+        role = m_userAdmin.createRole("group1", Role.GROUP);
+        assertNotNull(role);
+
+        role = m_userAdmin.createRole("group1", Role.GROUP);
+        assertNull(role);
+    }
+
+    /**
+     * Tests that creating an existing role does not succeed and yields null.
+     */
+    public void testCreateExistingUserFail() {
+        Role role = null;
+
+        role = m_userAdmin.createRole("user1", Role.USER);
+        assertNotNull(role);
+
+        role = m_userAdmin.createRole("user1", Role.USER);
+        assertNull(role);
+    }
+
+    /**
+     * Tests that creating a role with an invalid type does not succeed and yields an exception.
+     */
+    public void testCreateInvalidRoleTypeFail() {
+        try {
+            m_userAdmin.createRole("role1", Role.ROLE);
+            
+            fail("Expected an IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that creating a role without a name does not succeed and yields an exception.
+     */
+    public void testCreateInvalidRoleNameFail() {
+        try {
+            m_userAdmin.createRole(null, Role.USER);
+            
+            fail("Expected an IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that creating a role without a name does not succeed and yields an exception.
+     */
+    public void testCreateRoleWithEmptyNameFail() {
+        try {
+            m_userAdmin.createRole("", Role.USER);
+            
+            fail("Expected an IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that creating a non-existing group succeeds and yields a valid role instance.
+     */
+    public void testCreateNonExistingGroupOk() {
+        Role role = null;
+        
+        role = m_userAdmin.createRole("group1", Role.GROUP);
+        assertNotNull(role);
+        assertEquals("group1", role.getName());
+    }
+
+    /**
+     * Tests that creating a non-existing role succeeds and yields a valid role instance.
+     */
+    public void testCreateNonExistingUserOk() {
+        Role role = null;
+        
+        role = m_userAdmin.createRole("user1", Role.USER);
+        assertNotNull(role);
+        assertEquals("user1", role.getName());
+    }
+
+    /**
+     * Tests that creating a {@link UserAdminImpl} with a null event dispatcher fails.
+     */
+    public void testCreateUserAdminImplWithNullDispatcherFail() {
+        try {
+            new UserAdminImpl(m_roleRepository, null);
+            
+            fail("Expected IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that creating a {@link UserAdminImpl} with a null role repository fails.
+     */
+    public void testCreateUserAdminImplWithNullRepositoryFail() {
+        try {
+            new UserAdminImpl(null, m_dispatcher);
+            
+            fail("Expected IllegalArgumentException!");
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that obtaining the authorization for a non-existing user yields null.
+     */
+    public void testGetAuthorizationForAnonymousUserOk() {
+        Authorization auth = m_userAdmin.getAuthorization(null);
+        
+        assertNotNull(auth);
+
+        assertNull(auth.getRoles());
+        assertNull(auth.getName());
+    }
+
+    /**
+     * Tests that obtaining the authorization for a non-existing user yields null.
+     */
+    public void testGetAuthorizationForNonExistingUserOk() {
+        Authorization auth = m_userAdmin.getAuthorization(new UserImpl("foo"));
+
+        assertNotNull(auth);
+
+        assertNull(auth.getRoles());
+        assertNotNull(auth.getName());
+    }
+
+    /**
+     * Tests that getting a existing group with an unique key-value pair does not work and yields null.
+     */
+    public void testGetExistingGroupFail() {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key1", "value1");
+        user1.getProperties().put("key2", "constant");
+        Role user2 = m_userAdmin.createRole("user2", Role.USER);
+        user2.getProperties().put("key1", "value2");
+        user1.getProperties().put("key2", "constant");
+        Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+        group1.getProperties().put("key1", "value3");
+
+        assertNull(m_userAdmin.getUser("key1", "value3"));
+    }
+
+    /**
+     * Tests that getting roles based on existing names works correctly.
+     */
+    public void testGetExistingRolesOk() {
+        m_userAdmin.createRole("user1", Role.USER);
+        m_userAdmin.createRole("user2", Role.USER);
+        m_userAdmin.createRole("user3", Role.USER);
+        m_userAdmin.createRole("group1", Role.GROUP);
+        
+        assertEquals("user1", m_userAdmin.getRole("user1").getName());
+        assertEquals("user2", m_userAdmin.getRole("user2").getName());
+        assertEquals("user3", m_userAdmin.getRole("user3").getName());
+        assertEquals("group1", m_userAdmin.getRole("group1").getName());
+    }
+
+    /**
+     * Tests that getting an existing user with a non unique key-value pair does not work and yields null.
+     */
+    public void testGetExistingUserWithNonUniqueKeyValueFail() {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key1", "value1");
+        user1.getProperties().put("key2", "constant");
+        Role user2 = m_userAdmin.createRole("user2", Role.USER);
+        user2.getProperties().put("key1", "value2");
+        user2.getProperties().put("key2", "constant");
+        Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+        group1.getProperties().put("key1", "value3");
+
+        assertNull(m_userAdmin.getUser("key2", "constant"));
+    }
+
+    /**
+     * Tests that getting an existing user with an unique key-value pair works and yields the expected result.
+     */
+    public void testGetExistingUserWithUniqueKeyValueOk() {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key1", "value1");
+        user1.getProperties().put("key2", "constant");
+        Role user2 = m_userAdmin.createRole("user2", Role.USER);
+        user2.getProperties().put("key1", "value2");
+        user2.getProperties().put("key2", "constant");
+        Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+        group1.getProperties().put("key1", "value3");
+
+        assertEquals(user1, m_userAdmin.getUser("key1", "value1"));
+        assertEquals(user2, m_userAdmin.getUser("key1", "value2"));
+    }
+
+    /**
+     * Tests that retrieving the basic members from a group works.
+     */
+    public void testGetGroupMemberOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        
+        Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+        assertNull(group1.getMembers());
+        
+        assertTrue(group1.addMember(user1));
+        
+        assertEquals(1, group1.getMembers().length);
+    }
+
+    /**
+     * Tests that getting a non existing role by its name does not work and yields null.
+     */
+    public void testGetNonExistingRoleFails() {
+        assertNull(m_userAdmin.getRole("user1"));
+    }
+
+    /**
+     * Tests that getting a non-existing user does not work and yields null.
+     */
+    public void testGetNonExistingUserFail() {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key1", "value1");
+        user1.getProperties().put("key2", "constant");
+        Role user2 = m_userAdmin.createRole("user2", Role.USER);
+        user2.getProperties().put("key1", "value2");
+        user1.getProperties().put("key2", "constant");
+        Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+        group1.getProperties().put("key1", "value3");
+
+        assertNull(m_userAdmin.getUser("key1", "value4"));
+    }
+
+    /**
+     * Tests that getting roles based on a OSGi-filter that does not match any roles yields null.
+     */
+    public void testGetNonMatchingRolesOk() throws Exception {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key", "value1");
+        Role user2 = m_userAdmin.createRole("user2", Role.USER);
+        user2.getProperties().put("key", "value2");
+        Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+        group1.getProperties().put("key", "value3");
+
+        Role[] roles = m_userAdmin.getRoles("(nonExisting=value*)");
+        assertNull(roles);
+    }
+
+    /**
+     * Tests that getting predefined roles based on their names works correctly.
+     */
+    public void testGetPredefinedRolesOk() {
+        assertEquals(Role.USER_ANYONE, m_userAdmin.getRole(Role.USER_ANYONE).getName());
+    }
+
+    /**
+     * Tests that getting a removed role yields null.
+     */
+    public void testGetRemovedRoleFail() {
+        m_userAdmin.createRole("user1", Role.USER);
+        assertEquals("user1", m_userAdmin.getRole("user1").getName());
+        
+        assertTrue(m_userAdmin.removeRole("user1"));
+        assertNull(m_userAdmin.getRole("user1"));
+    }
+
+    /**
+     * Tests that retrieving the required members from a group works.
+     */
+    public void testGetRequiredGroupMemberOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        
+        Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+        assertNull(group1.getRequiredMembers());
+        
+        assertTrue(group1.addRequiredMember(user1));
+        
+        assertEquals(1, group1.getRequiredMembers().length);
+    }
+
+    /**
+     * Tests that getting the various names of defined roles works and yields the expected result.
+     */
+    public void testGetRoleNamesOk() {
+        m_userAdmin.createRole("user1", Role.USER);
+        m_userAdmin.createRole("group1", Role.GROUP);
+
+        assertEquals(Role.USER_ANYONE, m_userAdmin.getRole(Role.USER_ANYONE).getName());
+        assertEquals("user1", m_userAdmin.getRole("user1").getName());
+        assertEquals("group1", m_userAdmin.getRole("group1").getName());
+    }
+
+    /**
+     * Tests that getting multiple roles based on a OSGi-filter works and yields the correct result.
+     */
+    public void testGetRolesWithFilterOk() throws Exception {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key", "value1");
+        Role user2 = m_userAdmin.createRole("user2", Role.USER);
+        user2.getProperties().put("key", "value2");
+        Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+        group1.getProperties().put("key", "value3");
+        Role group2 = m_userAdmin.createRole("group2", Role.GROUP);
+        group2.getProperties().put("key", "otherValue3");
+
+        Role[] roles = m_userAdmin.getRoles("(key=value*)");
+        assertNotNull(roles);
+
+        assertEquals(3, roles.length);
+        
+        List roleList = Arrays.asList(roles);
+        assertTrue(roleList.contains(user1));
+        assertTrue(roleList.contains(user2));
+        assertTrue(roleList.contains(group1));
+    }
+
+    /**
+     * Tests that getting roles based on an invalid OSGi-filter yields an exception.
+     */
+    public void testGetRolesWithInvalidFilterFails() throws Exception {
+        try {
+            m_userAdmin.getRoles("(nonExisting=value*");
+
+            fail("Expected an InvalidSyntaxException!");
+        } catch (InvalidSyntaxException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that getting multiple roles based on a OSGi-filter works and yields the correct result.
+     */
+    public void testGetRolesWithoutFilterOk() throws Exception {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key", "value1");
+        Role user2 = m_userAdmin.createRole("user2", Role.USER);
+        user2.getProperties().put("key", "value2");
+        Role group1 = m_userAdmin.createRole("group1", Role.GROUP);
+        group1.getProperties().put("key", "value3");
+        Role group2 = m_userAdmin.createRole("group2", Role.GROUP);
+        group2.getProperties().put("key", "otherValue3");
+
+        Role[] roles = m_userAdmin.getRoles(null);
+        assertNotNull(roles);
+
+        assertEquals(4, roles.length);
+        
+        List roleList = Arrays.asList(roles);
+        assertTrue(roleList.contains(user1));
+        assertTrue(roleList.contains(user2));
+        assertTrue(roleList.contains(group1));
+        assertTrue(roleList.contains(group2));
+    }
+
+    /**
+     * Tests that getting the various types of defined roles works and yields the expected result.
+     */
+    public void testGetRoleTypesOk() {
+        m_userAdmin.createRole("user1", Role.USER);
+        m_userAdmin.createRole("group1", Role.GROUP);
+
+        assertEquals(Role.ROLE, m_userAdmin.getRole(Role.USER_ANYONE).getType());
+        assertEquals(Role.USER, m_userAdmin.getRole("user1").getType());
+        assertEquals(Role.GROUP, m_userAdmin.getRole("group1").getType());
+    }
+
+    /**
+     * Tests that testing for group membership with anonymous users works.
+     */
+    public void testGroupMembershipWithAnonymousUserOk() {
+        Role user = m_userAdmin.createRole("user", Role.USER);
+
+        Group group = (Group) m_userAdmin.createRole("group", Role.GROUP);
+        group.addMember(user);
+
+        Authorization auth = m_userAdmin.getAuthorization(null);
+        assertTrue(auth.hasRole(Role.USER_ANYONE));
+        assertFalse(auth.hasRole("group"));
+    }
+
+    /**
+     * Tests that testing for group membership with "user.anyone" works.
+     */
+    public void testGroupMembershipWithUserAnyoneOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        User user2 = (User) m_userAdmin.createRole("user2", Role.USER);
+        User user3 = (User) m_userAdmin.createRole("user3", Role.USER);
+        User user4 = (User) m_userAdmin.createRole("user4", Role.USER);
+
+        Group reqGroup = (Group) m_userAdmin.createRole("reqGroup", Role.GROUP);
+        reqGroup.addMember(user1);
+        reqGroup.addMember(user2);
+        
+        Group group = (Group) m_userAdmin.createRole("group", Role.GROUP);
+        group.addRequiredMember(reqGroup);
+        group.addMember(m_userAdmin.getRole(Role.USER_ANYONE));
+
+        Authorization auth = m_userAdmin.getAuthorization(user1);
+        assertTrue(auth.hasRole("group"));
+
+        auth = m_userAdmin.getAuthorization(user2);
+        assertTrue(auth.hasRole("group"));
+
+        auth = m_userAdmin.getAuthorization(user3);
+        assertFalse(auth.hasRole("group"));
+
+        auth = m_userAdmin.getAuthorization(user4);
+        assertFalse(auth.hasRole("group"));
+    }
+
+    /**
+     * Tests that testing the credentials for a user works and yields the correct result.
+     */
+    public void testHasUserCredentialOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        user1.getCredentials().put("key1", "value");
+        user1.getCredentials().put("key2", "value".getBytes());
+
+        assertTrue(user1.hasCredential("key1", "value"));
+        assertTrue(user1.hasCredential("key1", "value".getBytes()));
+        assertTrue(user1.hasCredential("key2", "value"));
+        assertTrue(user1.hasCredential("key2", "value".getBytes()));
+        assertFalse(user1.hasCredential("otherKey", "value"));
+    }
+
+    /**
+     * Tests that removing an existing role works.
+     */
+    public void testRemoveExistingRoleOk() {
+        Role role = null;
+
+        role = m_userAdmin.createRole("group1", Role.GROUP);
+        assertNotNull(role);
+
+        assertTrue(m_userAdmin.removeRole("group1"));
+        assertFalse(m_userAdmin.removeRole("group1"));
+    }
+
+    /**
+     * Tests that removing a non existing role does not work, yields a false result.
+     */
+    public void testRemoveNonExistingRoleOk() {
+        assertFalse(m_userAdmin.removeRole("group1"));
+    }
+
+    /**
+     * Tests that removing a predefined role does not work, and yields a false result.
+     */
+    public void testRemovePredefinedRoleOk() {
+        assertFalse(m_userAdmin.removeRole(Role.USER_ANYONE));
+    }
+
+    /**
+     * Tests that remove a property of a role works.
+     */
+    public void testRemoveRolePropertyOk() {
+        Role user1 = m_userAdmin.createRole("user1", Role.USER);
+        user1.getProperties().put("key", "value");
+
+        assertEquals("value", user1.getProperties().get("key"));
+        
+        user1.getProperties().remove("key");
+
+        assertNull(user1.getProperties().get("key"));
+    }
+
+    /**
+     * Tests that remove a credential of a user works.
+     */
+    public void testRemoveUserCredentialOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        user1.getCredentials().put("key", "value");
+
+        assertEquals("value", user1.getCredentials().get("key"));
+        
+        user1.getCredentials().remove("key");
+
+        assertNull(user1.getCredentials().get("key"));
+    }
+
+    /**
+     * Tests that removing a basic member from a group works.
+     */
+    public void testRemovingGroupMemberOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+        
+        assertTrue(group1.addMember(user1));
+        
+        assertTrue(group1.removeMember(user1));
+        assertFalse(group1.removeMember(user1));
+    }
+
+    /**
+     * Tests that removing a required member from a group works.
+     */
+    public void testRemovingRequiredGroupMemberOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+        
+        assertTrue(group1.addRequiredMember(user1));
+        
+        assertTrue(group1.removeMember(user1));
+        assertFalse(group1.removeMember(user1));
+    }
+
+    /**
+     * Tests that testing for required group membership works.
+     */
+    public void testRequiredGroupMembershipOk() {
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        User user2 = (User) m_userAdmin.createRole("user2", Role.USER);
+        User user3 = (User) m_userAdmin.createRole("user3", Role.USER);
+
+        Group reqGroup = (Group) m_userAdmin.createRole("reqGroup", Role.GROUP);
+        reqGroup.addMember(user1);
+        reqGroup.addMember(user2);
+        reqGroup.addMember(user3);
+        
+        Group group = (Group) m_userAdmin.createRole("group", Role.GROUP);
+        group.addRequiredMember(reqGroup);
+
+        Authorization auth = m_userAdmin.getAuthorization(user1);
+        assertFalse(auth.hasRole("group"));
+
+        auth = m_userAdmin.getAuthorization(user2);
+        assertFalse(auth.hasRole("group"));
+        
+        auth = m_userAdmin.getAuthorization(user3);
+        assertFalse(auth.hasRole("group"));
+    }
+
+    /**
+     * Tests that the list of roles in an {@link Authorization} does not contain the any-user, although it is defined as group member.
+     */
+    public void testUserAnyoneIsNotPartOfAuthorizedRolesOk() {
+        Role userAnyone = m_userAdmin.getRole(Role.USER_ANYONE);
+        User user1 = (User) m_userAdmin.createRole("user1", Role.USER);
+        Group group1 = (Group) m_userAdmin.createRole("group1", Role.GROUP);
+
+        assertTrue(group1.addRequiredMember(user1));
+        assertTrue(group1.addMember(userAnyone));
+        
+        Authorization auth = m_userAdmin.getAuthorization(user1);
+        assertNotNull(auth);
+        
+        assertTrue(auth.hasRole("group1"));
+        
+        String[] roles = auth.getRoles();
+        assertNotNull(roles);
+        
+        for (int i = 0; i < roles.length; i++) {
+            assertFalse(Role.USER_ANYONE.equals(roles[i]));
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+        
+        m_roleRepository = new RoleRepository(new MemoryRoleRepositoryStore());
+        m_dispatcher = new EventDispatcher(new StubEventAdmin(), new StubUserAdminListenerList());
+
+        m_userAdmin = new UserAdminImpl(m_roleRepository, m_dispatcher) {
+            protected Filter createFilter(String filter) throws InvalidSyntaxException {
+                if (filter == null || "".equals(filter.trim())) {
+                    return null;
+                }
+                return new FilterImpl(filter);
+            }
+        };
+
+        m_roleRepository.start();
+        m_dispatcher.start();
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    protected void tearDown() throws Exception {
+        m_roleRepository.stop();
+        m_dispatcher.stop();
+        
+        super.tearDown();
+    }
+
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplSecurityTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplSecurityTest.java
new file mode 100644
index 0000000..652c3a7
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplSecurityTest.java
@@ -0,0 +1,160 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.security.Permission;
+
+import junit.framework.TestCase;
+
+
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Security-related test cases for {@link GroupImpl}. 
+ */
+public class GroupImplSecurityTest extends TestCase {
+
+    private TestSecurityManager m_securityManager;
+    private GroupImpl m_group;
+
+    /**
+     * Tests that with permission, the {@link GroupImpl#addMember(Role)} method can be accessed.
+     */
+    public void testAddMemberWithPermissionsOk() throws SecurityException {
+        m_securityManager.m_allowed = true;
+        
+        m_group.addMember(new RoleImpl(Role.USER_ANYONE));
+    }
+
+    /**
+     * Tests that without permission, the {@link GroupImpl#addMember(Role)} method can not be accessed.
+     */
+    public void testAddMemberWithoutPermissionsFail() throws SecurityException {
+        try {
+            m_group.addMember(new RoleImpl(Role.USER_ANYONE));
+            
+            fail("Security exception expected!");
+        } catch (SecurityException e) {
+            // Ok; expected 
+        }
+    }
+
+    /**
+     * Tests that with permission, the {@link GroupImpl#addRequiredMember(Role)} method can be accessed.
+     */
+    public void testAddRequiredMemberWithPermissionsOk() throws SecurityException {
+        m_securityManager.m_allowed = true;
+        
+        m_group.addRequiredMember(new RoleImpl(Role.USER_ANYONE));
+    }
+
+    /**
+     * Tests that without permission, the {@link GroupImpl#addRequiredMember(Role)} method can not be accessed.
+     */
+    public void testAddRequiredMemberWithoutPermissionsFail() throws SecurityException {
+        try {
+            m_group.addRequiredMember(new RoleImpl(Role.USER_ANYONE));
+            
+            fail("Security exception expected!");
+        } catch (SecurityException e) {
+            // Ok; expected 
+        }
+    }
+
+    /**
+     * Tests that no special permissions are needed to access the {@link GroupImpl#getMembers()} method.
+     */
+    public void testGetMembers() throws SecurityException {
+        assertNull(m_group.getMembers());
+    }
+
+    /**
+     * Tests that no special permissions are needed to access the {@link GroupImpl#getRequiredMembers()} method.
+     */
+    public void testGetRequiredMembers() throws SecurityException {
+        assertNull(m_group.getRequiredMembers());
+    }
+
+    /**
+     * Tests that with permission, the {@link GroupImpl#removeMember(Role)} method can be accessed.
+     */
+    public void testRemoveMemberWithPermissionsOk() throws SecurityException {
+        m_securityManager.m_allowed = true;
+        
+        assertFalse(m_group.removeMember(new RoleImpl(Role.USER_ANYONE)));
+    }
+
+    /**
+     * Tests that without permission, the {@link GroupImpl#removeMember(Role)} method can not be accessed.
+     */
+    public void testRemoveMemberWithoutPermissionsFail() throws SecurityException {
+        try {
+            assertFalse(m_group.removeMember(new RoleImpl(Role.USER_ANYONE)));
+
+            fail("Security exception expected!");
+        } catch (SecurityException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+        
+        m_securityManager = new TestSecurityManager();
+        System.setSecurityManager(m_securityManager);
+
+        m_group = new GroupImpl("group");
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        
+        System.setSecurityManager(null);
+    }
+    
+    /**
+     * Provides a test security manager.
+     */
+    static final class TestSecurityManager extends SecurityManager {
+        
+        volatile boolean m_allowed = false;
+
+        public void checkPermission(Permission perm) {
+            // Cannot use instanceof as it requires a special permission as well...
+            if ("UserAdminPermission".equals(perm.getClass().getSimpleName())) {
+                String name = perm.getName();
+                if ("admin".equals(name) && !m_allowed) {
+                    throw new SecurityException("Not allowed!");
+                }
+            }
+            // Do not check for other permissions...
+        }
+
+        public void checkPermission(Permission perm, Object context) {
+            // Do not check for other permissions...
+        }
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplTest.java
new file mode 100644
index 0000000..14a2246
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/GroupImplTest.java
@@ -0,0 +1,196 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+
+import org.apache.felix.useradmin.impl.role.GroupImpl;
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Role;
+
+import junit.framework.TestCase;
+
+/**
+ * Test case for {@link GroupImpl}.
+ */
+public class GroupImplTest extends TestCase {
+
+    /**
+     * Tests that adding a role as basic member twice does not cause duplication.
+     */
+    public void testAddBasicMemberWithExistingBasicMemberOk() {
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addMember(new UserImpl("bar")));
+        assertFalse(group.addMember(new UserImpl("bar"))); // should be ignored...
+
+        assertEquals(1, group.getMembers().length);
+        assertNull(group.getRequiredMembers());
+    }
+
+    /**
+     * Tests that adding a role as required member works if it is not contained at all. 
+     */
+    public void testAddBasicMemberWithExistingRequiredMemberOk() {
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addRequiredMember(new UserImpl("bar")));
+        assertFalse(group.addMember(new UserImpl("bar"))); // should be ignored...
+
+        assertNull(group.getMembers());
+        assertEquals(1, group.getRequiredMembers().length);
+    }
+
+    /**
+     * Tests that adding a role as basic member while another role with the same name exists does not cause duplication.
+     */
+    public void testAddBasicMemberWithExistingRoleOk() {
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addMember(new UserImpl("bar")));
+        assertFalse(group.addMember(new RoleImpl("bar"))); // should be ignored...
+
+        assertEquals(1, group.getMembers().length);
+        assertNull(group.getRequiredMembers());
+    }
+
+    /**
+     * Tests that adding a role as basic member works if it is not contained at all.
+     */
+    public void testAddNonExistingMemberOk() {
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addMember(new UserImpl("bar")));
+        
+        assertEquals(1, group.getMembers().length);
+        assertNull(group.getRequiredMembers());
+    }
+
+    /**
+     * Tests that adding a role as basic member while it exists as required member does not cause duplication. 
+     */
+    public void testAddNonExistingRequiredMemberOk() {
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addRequiredMember(new UserImpl("bar")));
+
+        assertNull(group.getMembers());
+        assertEquals(1, group.getRequiredMembers().length);
+    }
+
+    /**
+     * Tests that adding a role as required member works if it is not contained at all. 
+     */
+    public void testAddRequiredMemberWithExistingBasicMemberOk() {
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addMember(new UserImpl("bar")));
+        assertFalse(group.addRequiredMember(new UserImpl("bar"))); // should be ignored...
+
+        assertEquals(1, group.getMembers().length);
+        assertNull(group.getRequiredMembers());
+    }
+
+    /**
+     * Tests that adding a role as required member while another role with the same name exists does not cause duplication.
+     */
+    public void testAddRequiredMemberWithExistingRoleOk() {
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addRequiredMember(new UserImpl("bar")));
+        assertFalse(group.addRequiredMember(new RoleImpl("bar"))); // should be ignored...
+
+        assertNull(group.getMembers());
+        assertEquals(1, group.getRequiredMembers().length);
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.RoleImpl#getType()}.
+     */
+    public void testGetType() {
+        GroupImpl group = new GroupImpl("foo");
+        
+        assertEquals(Role.GROUP, group.getType());
+    }
+
+    /**
+     * Tests that {@link GroupImpl#hashCode()} yields predictable results.
+     */
+    public void testHashCodeOk() {
+        GroupImpl group1 = new GroupImpl("foo");
+        GroupImpl group2 = new GroupImpl("foo");
+        GroupImpl group3 = new GroupImpl("bar");
+        
+        assertTrue(group1.hashCode() == group2.hashCode());
+        assertFalse(group1.hashCode() == group3.hashCode());
+        assertFalse(group2.hashCode() == group3.hashCode());
+    }
+
+    /**
+     * Tests that removing an basic required member works.
+     */
+    public void testRemoveExistingBasicMemberOk() {
+        UserImpl role1 = new UserImpl("bar");
+        UserImpl role2 = new UserImpl("qux");
+        
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addMember(role1));
+        assertTrue(group.addRequiredMember(role2));
+
+        assertEquals(1, group.getMembers().length);
+        assertEquals(1, group.getRequiredMembers().length);
+        
+        assertTrue(group.removeMember(role1));
+
+        assertNull(group.getMembers());
+        assertEquals(1, group.getRequiredMembers().length);
+    }
+
+    /**
+     * Tests that removing an existing required member works.
+     */
+    public void testRemoveExistingRequiredMemberOk() {
+        UserImpl role1 = new UserImpl("bar");
+        UserImpl role2 = new UserImpl("qux");
+        
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addMember(role1));
+        assertTrue(group.addRequiredMember(role2));
+
+        assertEquals(1, group.getMembers().length);
+        assertEquals(1, group.getRequiredMembers().length);
+        
+        assertTrue(group.removeMember(role2));
+
+        assertEquals(1, group.getMembers().length);
+        assertNull(group.getRequiredMembers());
+    }
+
+    /**
+     * Tests that removing an basic required member works.
+     */
+    public void testRemoveNonExistingMemberOk() {
+        UserImpl role1 = new UserImpl("bar");
+        UserImpl role2 = new UserImpl("qux");
+        UserImpl role3 = new UserImpl("quu");
+        
+        GroupImpl group = new GroupImpl("foo");
+        assertTrue(group.addMember(role1));
+        assertTrue(group.addRequiredMember(role2));
+
+        assertEquals(1, group.getMembers().length);
+        assertEquals(1, group.getRequiredMembers().length);
+        
+        assertFalse(group.removeMember(role3));
+
+        assertEquals(1, group.getMembers().length);
+        assertEquals(1, group.getRequiredMembers().length);
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionarySecurityTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionarySecurityTest.java
new file mode 100644
index 0000000..200bbe4
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionarySecurityTest.java
@@ -0,0 +1,173 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.security.Permission;
+import java.util.Enumeration;
+
+
+import org.apache.felix.useradmin.impl.role.ObservableDictionary;
+import org.osgi.service.useradmin.UserAdminPermission;
+
+import junit.framework.TestCase;
+
+/**
+ * Security-related test cases for {@link ObservableDictionary}.
+ */
+public class ObservableDictionarySecurityTest extends TestCase {
+
+    private static final String GET_ACTION = UserAdminPermission.GET_CREDENTIAL;
+    private static final String CHANGE_ACTION = UserAdminPermission.CHANGE_CREDENTIAL;
+    
+    private TestSecurityManager m_securityManager;
+    private ObservableDictionary m_dict;
+
+    /**
+     * Tests that no special permissions are needed to access the {@link ObservableDictionary#size()} method.
+     */
+    public void testSize() throws SecurityException {
+        assertEquals(0, m_dict.size());
+    }
+
+    /**
+     * Tests that no special permissions are needed to access the {@link ObservableDictionary#isEmpty()} method.
+     */
+    public void testIsEmpty() throws SecurityException {
+        assertTrue(m_dict.isEmpty());
+    }
+
+    /**
+     * Tests that no special permissions are needed to access the {@link ObservableDictionary#elements()} method.
+     */
+    public void testElements() throws SecurityException {
+        Enumeration elements = m_dict.elements();
+        assertNotNull(elements);
+        assertFalse(elements.hasMoreElements());
+    }
+
+    /**
+     * Tests that with permission, the {@link ObservableDictionary#get(Object)} method can be accessed.
+     */
+    public void testGetObjectWithPermissionsOk() throws SecurityException {
+        assertNull(m_dict.get("permFoo"));
+    }
+
+    /**
+     * Tests that without permission, the {@link ObservableDictionary#get(Object)} method can not be accessed.
+     */
+    public void testGetObjectWithoutPermissionsFail() throws SecurityException {
+        try {
+            assertNull(m_dict.get("bar"));
+            
+            fail("Security exception expected!");
+        } catch (SecurityException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that no special permissions are needed to access the {@link ObservableDictionary#keys()} method.
+     */
+    public void testKeys() throws SecurityException {
+        Enumeration keys = m_dict.keys();
+        assertNotNull(keys);
+        assertFalse(keys.hasMoreElements());
+    }
+
+    /**
+     * Tests that with permission, the {@link ObservableDictionary#put(Object, Object)} method can be accessed.
+     */
+    public void testPutObjectWithPermissionsOk() throws SecurityException {
+        assertNull(m_dict.put("permKey", "value"));
+    }
+
+    /**
+     * Tests that without permission, the {@link ObservableDictionary#put(Object, Object)} method can not be accessed.
+     */
+    public void testPutObjectWithoutPermissionsFail() throws SecurityException {
+        try {
+            assertNull(m_dict.put("key", "value"));
+
+            fail("Security exception expected!");
+        } catch (SecurityException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that with permission, the {@link ObservableDictionary#remove(Object)} method can be accessed.
+     */
+    public void testRemoveObjectWithPermissionsOk() throws SecurityException {
+        assertNull(m_dict.remove("permKey"));
+    }
+
+    /**
+     * Tests that without permission, the {@link ObservableDictionary#remove(Object)} method can not be accessed.
+     */
+    public void testRemoveObjectWithoutPermissionsFail() throws SecurityException {
+        try {
+            assertNull(m_dict.remove("key"));
+            
+            fail("Security exception expected!");
+        } catch (SecurityException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+        
+        m_securityManager = new TestSecurityManager();
+        System.setSecurityManager(m_securityManager);
+        
+        m_dict = new ObservableDictionary(GET_ACTION, CHANGE_ACTION);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        
+        System.setSecurityManager(null);
+    }
+    
+    /**
+     * Provides a test security manager.
+     */
+    static final class TestSecurityManager extends SecurityManager {
+
+        public void checkPermission(Permission perm) {
+            // Cannot use instanceof as it requires a special permission as well...
+            if ("UserAdminPermission".equals(perm.getClass().getSimpleName())) {
+                String name = perm.getName();
+                if ((name != null) && !name.startsWith("perm")) {
+                    throw new SecurityException("Not allowed!");
+                }
+            }
+            // Do not check for other permissions...
+        }
+
+        public void checkPermission(Permission perm, Object context) {
+            // Do not check for other permissions...
+        }
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionaryTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionaryTest.java
new file mode 100644
index 0000000..497d6a5
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservableDictionaryTest.java
@@ -0,0 +1,247 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.util.Date;
+
+import org.apache.felix.useradmin.impl.role.ObservableDictionary;
+import org.apache.felix.useradmin.impl.role.ObservableDictionary.DictionaryChangeListener;
+
+import junit.framework.TestCase;
+
+
+/**
+ * Test case for {@link ObservableDictionary}.
+ */
+public class ObservableDictionaryTest extends TestCase {
+    
+    /**
+     * Implementation of {@link DictionaryChangeListener} that keeps track of 
+     * which callback methods are being called at least once.
+     */
+    static class StateCheckingDictionaryChangeListener implements DictionaryChangeListener {
+        
+        volatile boolean m_addedCalled = false;
+        volatile boolean m_removedCalled = false;
+        volatile boolean m_changedCalled = false;
+
+        public void entryAdded(Object key, Object value) {
+            m_addedCalled = true;
+        }
+
+        public void entryChanged(Object key, Object oldValue, Object newValue) {
+            m_changedCalled = true;
+        }
+
+        public void entryRemoved(Object key) {
+            m_removedCalled = true;
+        }
+    }
+    
+    private ObservableDictionary m_dict;
+    private StateCheckingDictionaryChangeListener m_listener;
+
+    /**
+     * Tests that adding an entry emits a change event.
+     */
+    public void testAddEntryEmitsEvent() {
+        m_dict.setDictionaryChangeListener(m_listener);
+        
+        m_dict.put("key", "value");
+        
+        assertTrue(m_listener.m_addedCalled);
+        assertFalse(m_listener.m_changedCalled);
+        assertFalse(m_listener.m_removedCalled);
+    }
+
+    /**
+     * Tests that changing an entry emits a change event.
+     */
+    public void testChangeEntryEmitsEvent() {
+        m_dict.put("key", "value1");
+        
+        m_dict.setDictionaryChangeListener(m_listener);
+        
+        m_dict.put("key", "value2");
+        
+        assertTrue(m_listener.m_changedCalled);
+        assertFalse(m_listener.m_addedCalled);
+        assertFalse(m_listener.m_removedCalled);
+    }
+
+    /**
+     * Tests that creating a new {@link ObservableDictionary} with a valid dictionary succeeds. 
+     */
+    public void testCreateWithNonNullDictionaryFail() {
+        m_dict.put("foo", "bar");
+        m_dict.put("bar", "foo");
+        
+        m_dict = new ObservableDictionary("foo", "bar", m_dict);
+        
+        assertEquals("bar", m_dict.get("foo"));
+        assertEquals("foo", m_dict.get("bar"));
+    }
+
+    /**
+     * Tests that creating a new {@link ObservableDictionary} with a <code>null</code> dictionary fails. 
+     */
+    public void testCreateWithNullDictionaryFail() {
+        try {
+            new ObservableDictionary("foo", "bar", null);
+            fail("Expected IllegalArgumentException!");
+        } catch (Exception e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that we can get any kind of object from an {@link ObservableDictionary}.
+     */
+    public void testGetAnyKindOfObject() {
+        assertNull(m_dict.get("non-existing"));
+        
+        Integer key = Integer.valueOf(3);
+        Date value = new Date();
+
+        m_dict.put(key, value);
+        assertEquals(value, m_dict.get(key));
+    }
+
+    /**
+     * Tests that we cannot get a null-key form a {@link ObservableDictionary}.
+     */
+    public void testGetNullKeyFail() {
+        try {
+            m_dict.get(null);
+            
+            fail();
+        } catch (Exception e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Test that we can put any kind of object in a {@link ObservableDictionary}.
+     */
+    public void testPutAnyKindOfObject() {
+        Integer key = Integer.valueOf(3);
+        Date value = new Date();
+
+        m_dict.put(key, value);
+        assertEquals(value, m_dict.get(key));
+        
+        m_dict.put(key, "other-value");
+        assertEquals("other-value", m_dict.get(key));
+    }
+
+    /**
+     * Test that we cannot put a null-key into a {@link ObservableDictionary}.
+     */
+    public void testPutNullKeyFail() {
+        try {
+            m_dict.put(null, "value");
+            
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Test that we cannot put a null-value into a {@link ObservableDictionary}.
+     */
+    public void testPutNullValueFail() {
+        try {
+            m_dict.put("key", null);
+            
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that removing a key emits a change event.
+     */
+    public void testRemoveEmitsEvent() {
+        m_dict.setDictionaryChangeListener(m_listener);
+        
+        m_dict.remove("key");
+        
+        assertTrue(m_listener.m_removedCalled);
+        assertFalse(m_listener.m_addedCalled);
+        assertFalse(m_listener.m_changedCalled);
+    }
+
+    /**
+     * Tests that we cannot remove a null-key from a {@link ObservableDictionary}.
+     */
+    public void testRemoveNullKeyFail() {
+        try {
+            m_dict.remove(null);
+            
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that we can remove any kind of object from a {@link ObservableDictionary}.
+     */
+    public void testRemoveObject() {
+        Integer key = Integer.valueOf(3);
+        Date value = new Date();
+
+        m_dict.put(key, value);
+        assertEquals(value, m_dict.get(key));
+        
+        m_dict.remove(key);
+        assertNull(m_dict.get(key));
+    }
+
+    /**
+     * Tests that {@link ObservableDictionary#equals(Object)} and {@link ObservableDictionary#hashCode()} work correctly.
+     */
+    public void testEqualsAndHashcode() {
+        ObservableDictionary d1 = new ObservableDictionary(null, null);
+        ObservableDictionary d2 = new ObservableDictionary(null, null);
+        
+        assertTrue(d1.hashCode() == d2.hashCode());
+        assertTrue(d1.equals(d2));
+        assertTrue(d1.equals(d1));
+        
+        d2.put("foo", "bar");
+        
+        assertFalse(d1.hashCode() == d2.hashCode());
+        assertFalse(d1.equals(d2));
+        assertFalse(d1.equals(null));
+        assertFalse(d1.equals("bar"));
+        assertTrue(d1.equals(d1));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+        
+        m_dict = new ObservableDictionary(null, null);
+        m_listener = new StateCheckingDictionaryChangeListener();
+
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservablePropertiesTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservablePropertiesTest.java
new file mode 100644
index 0000000..122c329
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/ObservablePropertiesTest.java
@@ -0,0 +1,119 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import org.apache.felix.useradmin.impl.role.ObservableProperties;
+
+import junit.framework.TestCase;
+
+/**
+ * Test case for {@link ObservableProperties}.
+ */
+public class ObservablePropertiesTest extends TestCase {
+    
+    private ObservableProperties m_dict;
+
+    /**
+     * Tests that calling get with a non-string value yields an exception.
+     */
+    public void testGetNonStringKeyFail() {
+        try {
+            m_dict.get(Integer.valueOf(1));
+            
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that calling get with a string value does not yield an exception.
+     */
+    public void testGetStringKeyOk() {
+        assertNull(m_dict.get("key"));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#put(java.lang.Object, java.lang.Object)}.
+     */
+    public void testPutByteArrayValueOk() {
+        assertNull(m_dict.put("key", "value".getBytes()));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#put(java.lang.Object, java.lang.Object)}.
+     */
+    public void testPutNonStringKeyFail() {
+        try {
+            m_dict.put(Integer.valueOf(1), "value");
+
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#put(java.lang.Object, java.lang.Object)}.
+     */
+    public void testPutNonStringValueFail() {
+        try {
+            m_dict.put("key", Integer.valueOf(1));
+
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#put(java.lang.Object, java.lang.Object)}.
+     */
+    public void testPutStringKeyValueOk() {
+        assertNull(m_dict.put("key", "value"));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#remove(java.lang.Object)}.
+     */
+    public void testRemoveNonStringKeyFail() {
+        try {
+            m_dict.remove(Integer.valueOf(1));
+
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.ObservableProperties#remove(java.lang.Object)}.
+     */
+    public void testRemoveStringKeyOk() {
+        assertNull(m_dict.remove("foo"));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    protected void setUp() throws Exception {
+        super.setUp();
+        
+        m_dict = new ObservableProperties(null, null);
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/RoleImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/RoleImplTest.java
new file mode 100644
index 0000000..a393d13
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/RoleImplTest.java
@@ -0,0 +1,96 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.util.Dictionary;
+
+import junit.framework.TestCase;
+
+
+import org.apache.felix.useradmin.impl.role.RoleImpl;
+import org.osgi.service.useradmin.Role;
+
+/**
+ * Test case for {@link RoleImpl}.
+ */
+public class RoleImplTest extends TestCase {
+
+    /**
+     * Tests that we must enter a valid name upon constructing a {@link RoleImpl}.
+     */
+    public void testCreateNullNameFail() {
+        try {
+            new RoleImpl(null);
+            
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that we must enter a valid name upon constructing a {@link RoleImpl}.
+     */
+    public void testCreateEmptyNameFail() {
+        try {
+            new RoleImpl(" ");
+            
+            fail();
+        } catch (IllegalArgumentException e) {
+            // Ok; expected
+        }
+    }
+
+    /**
+     * Tests that we can obtain the name of a {@link RoleImpl} as set in its constructor.
+     */
+    public void testGetName() {
+        RoleImpl role = new RoleImpl("foo");
+        assertEquals("foo", role.getName());
+    }
+
+    /**
+     * Tests that we can obtain the properties of a {@link RoleImpl}.
+     */
+    public void testGetProperties() {
+        RoleImpl role = new RoleImpl("foo");
+
+        Dictionary dict = role.getProperties();
+        assertNotNull(dict);
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.RoleImpl#getType()}.
+     */
+    public void testGetType() {
+        RoleImpl role = new RoleImpl("foo");
+        assertEquals(Role.ROLE, role.getType());
+    }
+
+    /**
+     * Tests that {@link RoleImpl#hashCode()} yields predictable results.
+     */
+    public void testHashCodeOk() {
+        RoleImpl role1 = new RoleImpl("foo");
+        RoleImpl role2 = new RoleImpl("foo");
+        RoleImpl role3 = new RoleImpl("bar");
+        
+        assertTrue(role1.hashCode() == role2.hashCode());
+        assertFalse(role1.hashCode() == role3.hashCode());
+        assertFalse(role2.hashCode() == role3.hashCode());
+    }
+}
diff --git a/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/UserImplTest.java b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/UserImplTest.java
new file mode 100644
index 0000000..4a308b5
--- /dev/null
+++ b/useradmin/useradmin/src/test/java/org/apache/felix/useradmin/impl/role/UserImplTest.java
@@ -0,0 +1,100 @@
+/**
+ *  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.felix.useradmin.impl.role;
+
+import java.util.Dictionary;
+
+
+import org.apache.felix.useradmin.impl.role.UserImpl;
+import org.osgi.service.useradmin.Role;
+
+import junit.framework.TestCase;
+
+/**
+ * Test case for {@link UserImpl}. 
+ */
+public class UserImplTest extends TestCase {
+
+    /**
+     * Tests that we can get the credentials for a {@link UserImpl}.
+     */
+    public void testGetCredentials() {
+        UserImpl user = new UserImpl("foo");
+        
+        Dictionary dict = user.getCredentials();
+        assertNotNull(dict);
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.RoleImpl#getType()}.
+     */
+    public void testGetType() {
+        UserImpl user = new UserImpl("foo");
+        
+        assertEquals(Role.USER, user.getType());
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.UserImpl#hasCredential(java.lang.String, java.lang.Object)}.
+     */
+    public void testHasExistingCredentialAlternativeValueTypeOk() {
+        UserImpl user = new UserImpl("foo");
+        
+        Dictionary dict = user.getCredentials();
+        dict.put("password", "secret");
+
+        // Direct comparison...
+        assertTrue(user.hasCredential("password", "secret"));
+
+        // In case the given value is a byte[]...
+        assertTrue(user.hasCredential("password", "secret".getBytes()));
+
+        dict.put("password", "otherSecret".getBytes());
+        
+        // Direct comparison...
+        assertTrue(user.hasCredential("password", "otherSecret".getBytes()));
+
+        // In case the stored value is a byte[]...
+        assertTrue(user.hasCredential("password", "otherSecret"));
+    }
+
+    /**
+     * Test method for {@link org.apache.felix.useradmin.impl.role.UserImpl#hasCredential(java.lang.String, java.lang.Object)}.
+     */
+    public void testHasExistingCredentialOk() {
+        UserImpl user = new UserImpl("foo");
+        
+        Dictionary dict = user.getCredentials();
+        dict.put("password", "secret");
+
+        assertTrue(user.hasCredential("password", "secret"));
+    }
+
+    /**
+     * Tests that {@link UserImpl#hashCode()} yields predictable results.
+     */
+    public void testHashCodeOk() {
+        UserImpl user1 = new UserImpl("foo");
+        UserImpl user2 = new UserImpl("foo");
+        UserImpl user3 = new UserImpl("bar");
+        
+        assertTrue(user1.hashCode() == user2.hashCode());
+        assertFalse(user1.hashCode() == user3.hashCode());
+        assertFalse(user2.hashCode() == user3.hashCode());
+    }
+}