Repository: karaf
Updated Branches:
  refs/heads/karaf-2.3.x e3c73184a -> cc788d94e


[KARAF-3382] Add role mapping support in LDAP login module


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/cc788d94
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/cc788d94
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/cc788d94

Branch: refs/heads/karaf-2.3.x
Commit: cc788d94eeb01db5c327fec571060d48e2470115
Parents: e3c7318
Author: Jean-Baptiste Onofré <[email protected]>
Authored: Tue Dec 30 09:55:43 2014 +0100
Committer: Jean-Baptiste Onofré <[email protected]>
Committed: Tue Dec 30 09:55:43 2014 +0100

----------------------------------------------------------------------
 .../jaas/modules/ldap/LDAPLoginModule.java      |  69 +++++++++--
 .../jaas/modules/ldap/LdapLoginModuleTest.java  | 114 +++++++++++++++++--
 .../developers-guide/security-framework.conf    |   1 +
 3 files changed, 162 insertions(+), 22 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/cc788d94/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java
----------------------------------------------------------------------
diff --git 
a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java
 
b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java
index 846bcf8..373225c 100644
--- 
a/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java
+++ 
b/jaas/modules/src/main/java/org/apache/karaf/jaas/modules/ldap/LDAPLoginModule.java
@@ -32,9 +32,7 @@ import javax.security.auth.callback.*;
 import javax.security.auth.login.LoginException;
 import java.io.IOException;
 import java.security.Principal;
-import java.util.HashSet;
-import java.util.Hashtable;
-import java.util.Map;
+import java.util.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -57,6 +55,7 @@ public class LDAPLoginModule extends AbstractKarafLoginModule 
{
     public final static String ROLE_FILTER = "role.filter";
     public final static String ROLE_NAME_ATTRIBUTE = "role.name.attribute";
     public final static String ROLE_SEARCH_SUBTREE = "role.search.subtree";
+    public final static String ROLE_MAPPING = "role.mapping";
     public final static String AUTHENTICATION = "authentication";
     public final static String INITIAL_CONTEXT_FACTORY = 
"initial.context.factory";
     public static final String CONTEXT_PREFIX = "context.";
@@ -81,6 +80,7 @@ public class LDAPLoginModule extends AbstractKarafLoginModule 
{
     private String roleFilter;
     private String roleNameAttribute;
     private boolean roleSearchSubtree = true;
+    private Map<String, Set<String>> roleMapping;
     private String authentication = DEFAULT_AUTHENTICATION;
     private String initialContextFactory = null;
     private boolean ssl;
@@ -97,13 +97,14 @@ public class LDAPLoginModule extends 
AbstractKarafLoginModule {
         connectionURL = (String) options.get(CONNECTION_URL);
         connectionUsername = (String) options.get(CONNECTION_USERNAME);
         connectionPassword = (String) options.get(CONNECTION_PASSWORD);
-        userBaseDN =  (String) options.get(USER_BASE_DN);
+        userBaseDN = (String) options.get(USER_BASE_DN);
         userFilter = (String) options.get(USER_FILTER);
         userSearchSubtree = Boolean.parseBoolean((String) 
options.get(USER_SEARCH_SUBTREE));
         roleBaseDN = (String) options.get(ROLE_BASE_DN);
         roleFilter = (String) options.get(ROLE_FILTER);
         roleNameAttribute = (String) options.get(ROLE_NAME_ATTRIBUTE);
         roleSearchSubtree = Boolean.parseBoolean((String) 
options.get(ROLE_SEARCH_SUBTREE));
+        roleMapping = parseRoleMapping((String) options.get(ROLE_MAPPING));
         initialContextFactory = (String) options.get(INITIAL_CONTEXT_FACTORY);
         if (initialContextFactory == null) {
             initialContextFactory = DEFAULT_INITIAL_CONTEXT_FACTORY;
@@ -133,6 +134,27 @@ public class LDAPLoginModule extends 
AbstractKarafLoginModule {
         }
     }
 
+    private Map<String, Set<String>> parseRoleMapping(String option) {
+        Map<String, Set<String>> roleMapping = new HashMap<String, 
Set<String>>();
+        if (option != null) {
+            logger.debug("Parse role mapping {}", option);
+            String[] mappings = option.split(";");
+            for (String mapping : mappings) {
+                String[] map = mapping.split("=", 2);
+                String ldapRole = map[0];
+                String[] karafRoles = map[1].split(",");
+                if (roleMapping.get(ldapRole) == null) {
+                    roleMapping.put(ldapRole, new HashSet<String>());
+                }
+                final Set<String> karafRolesSet = roleMapping.get(ldapRole);
+                for (String karafRole : karafRoles) {
+                    karafRolesSet.add(karafRole);
+                }
+            }
+        }
+        return roleMapping;
+    }
+
     public boolean login() throws LoginException {
         ClassLoader tccl = Thread.currentThread().getContextClassLoader();
         try {
@@ -159,7 +181,7 @@ public class LDAPLoginModule extends 
AbstractKarafLoginModule {
         user = ((NameCallback) callbacks[0]).getName();
 
         char[] tmpPassword = ((PasswordCallback) callbacks[1]).getPassword();
-        
+
         // If either a username or password is specified don't allow 
authentication = "none".
         // This is to prevent someone from logging into Karaf as any user 
without providing a 
         // valid password (because if authentication = none, the password 
could be any 
@@ -169,7 +191,7 @@ public class LDAPLoginModule extends 
AbstractKarafLoginModule {
             // default to simple so that the provided user/password will get 
checked
             authentication = "simple";
         }
-        
+
         if (tmpPassword == null) {
             tmpPassword = new char[0];
         }
@@ -221,7 +243,7 @@ public class LDAPLoginModule extends 
AbstractKarafLoginModule {
             }
             logger.debug("Get the user DN.");
             SearchResult result = (SearchResult) namingEnumeration.next();
-            
+
             // We need to do the following because slashes are handled badly. 
For example, when searching 
             // for a user with lots of special characters like cn=admin,=+<>#;\
             // SearchResult contains 2 different results:
@@ -288,7 +310,7 @@ public class LDAPLoginModule extends 
AbstractKarafLoginModule {
                 controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
             }
             if (roleNameAttribute != null) {
-                controls.setReturningAttributes(new String[]{ 
roleNameAttribute });
+                controls.setReturningAttributes(new 
String[]{roleNameAttribute});
             }
             logger.debug("Looking for the user roles in LDAP with ");
             logger.debug("  base DN: " + roleBaseDN);
@@ -300,14 +322,23 @@ public class LDAPLoginModule extends 
AbstractKarafLoginModule {
             logger.debug("  filter: " + roleFilter);
             NamingEnumeration namingEnumeration = context.search(roleBaseDN, 
roleFilter, controls);
             while (namingEnumeration.hasMore()) {
-                SearchResult result = (SearchResult)namingEnumeration.next();
+                SearchResult result = (SearchResult) namingEnumeration.next();
                 Attributes attributes = result.getAttributes();
                 Attribute roles = attributes.get(roleNameAttribute);
                 if (roles != null) {
                     for (int i = 0; i < roles.size(); i++) {
-                        String role = (String)roles.get(i);
+                        String role = (String) roles.get(i);
                         if (role != null) {
-                            principals.add(new RolePrincipal(role));
+                            logger.debug("User {} is a member of role {}", 
user, role);
+                            // handle role mapping ...
+                            Set<RolePrincipal> mapped = tryMappingRole(role);
+                            if (mapped.isEmpty()) {
+                                principals.add(new RolePrincipal(role));
+                            } else {
+                                for (RolePrincipal r : mapped) {
+                                    principals.add(r);
+                                }
+                            }
                         }
                     }
                 }
@@ -327,6 +358,22 @@ public class LDAPLoginModule extends 
AbstractKarafLoginModule {
         return true;
     }
 
+    protected Set<RolePrincipal> tryMappingRole(String role) {
+        Set<RolePrincipal> roles = new HashSet<RolePrincipal>();
+        if (roleMapping == null || roleMapping.isEmpty()) {
+            return roles;
+        }
+        Set<String> karafRoles = roleMapping.get(role);
+        if (karafRoles != null) {
+            // add all mapped roles
+            for (String karafRole : karafRoles) {
+                logger.debug("LDAP role {} is mapped to Karaf role {}", role, 
karafRole);
+                roles.add(new RolePrincipal(karafRole));
+            }
+        }
+        return roles;
+    }
+
     protected void setupSsl(Hashtable env) throws LoginException {
         ServiceReference ref = null;
         try {

http://git-wip-us.apache.org/repos/asf/karaf/blob/cc788d94/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java
----------------------------------------------------------------------
diff --git 
a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java
 
b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java
index 01135b0..3addfdc 100644
--- 
a/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java
+++ 
b/jaas/modules/src/test/java/org/apache/karaf/jaas/modules/ldap/LdapLoginModuleTest.java
@@ -13,7 +13,6 @@
  *  limitations under the License.
  *  under the License.
  */
-
 package org.apache.karaf.jaas.modules.ldap;
 
 import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
@@ -35,17 +34,20 @@ import javax.security.auth.callback.*;
 import java.io.File;
 import java.io.IOException;
 import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.assertEquals;
 
-
-@RunWith ( FrameworkRunner.class )
-@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", 
port=9999)})
+@RunWith(FrameworkRunner.class)
+@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP", port = 
9999)})
 @CreateDS(name = "LdapLoginModuleTest-class",
- partitions = { @CreatePartition(name = "example", suffix = 
"dc=example,dc=com") })
+        partitions = {@CreatePartition(name = "example", suffix = 
"dc=example,dc=com")})
 @ApplyLdifFiles(
-   "org/apache/karaf/jaas/modules/ldap/example.com.ldif"
+        "org/apache/karaf/jaas/modules/ldap/example.com.ldif"
 )
 public class LdapLoginModuleTest extends AbstractLdapTestUnit {
 
@@ -88,13 +90,13 @@ public class LdapLoginModuleTest extends 
AbstractLdapTestUnit {
         assertTrue(foundRole);
 
         assertTrue(module.logout());
-        assertEquals("Principals should be gone as the user has logged out", 
0, subject.getPrincipals().size());        
+        assertEquals("Principals should be gone as the user has logged out", 
0, subject.getPrincipals().size());
     }
 
     protected Properties ldapLoginModuleOptions() throws IOException {
         return new Properties(new 
File("src/test/resources/org/apache/karaf/jaas/modules/ldap/ldap.properties"));
     }
-    
+
     @Test
     public void testNonAdminLogin() throws Exception {
         Properties options = ldapLoginModuleOptions();
@@ -135,9 +137,9 @@ public class LdapLoginModuleTest extends 
AbstractLdapTestUnit {
         assertFalse(foundRole);
 
         assertTrue(module.logout());
-        assertEquals("Principals should be gone as the user has logged out", 
0, subject.getPrincipals().size());        
+        assertEquals("Principals should be gone as the user has logged out", 
0, subject.getPrincipals().size());
     }
-    
+
     @Test
     public void testBadPassword() throws Exception {
         Properties options = ldapLoginModuleOptions();
@@ -159,7 +161,7 @@ public class LdapLoginModuleTest extends 
AbstractLdapTestUnit {
         assertEquals("Precondition", 0, subject.getPrincipals().size());
         assertFalse(module.login());
     }
-    
+
     @Test
     public void testUserNotFound() throws Exception {
         Properties options = ldapLoginModuleOptions();
@@ -181,5 +183,95 @@ public class LdapLoginModuleTest extends 
AbstractLdapTestUnit {
         assertEquals("Precondition", 0, subject.getPrincipals().size());
         assertFalse(module.login());
     }
+
+    @Test
+    public void testRoleMappingSimple() throws Exception {
+        Properties options = ldapLoginModuleOptions();
+        options.put(LDAPLoginModule.ROLE_MAPPING, "admin=karaf");
+        LDAPLoginModule module = new LDAPLoginModule();
+        CallbackHandler cb = new CallbackHandler() {
+            public void handle(Callback[] callbacks) throws IOException, 
UnsupportedCallbackException {
+                for (Callback cb : callbacks) {
+                    if (cb instanceof NameCallback) {
+                        ((NameCallback) cb).setName("admin");
+                    } else if (cb instanceof PasswordCallback) {
+                        ((PasswordCallback) 
cb).setPassword("admin123".toCharArray());
+                    }
+                }
+            }
+        };
+        Subject subject = new Subject();
+        module.initialize(subject, cb, null, options);
+
+        assertEquals("Precondition", 0, subject.getPrincipals().size());
+        assertTrue(module.login());
+        assertTrue(module.commit());
+
+        assertEquals(2, subject.getPrincipals().size());
+
+        boolean foundUser = false;
+        boolean foundRole = false;
+        for (Principal pr : subject.getPrincipals()) {
+            if (pr instanceof UserPrincipal) {
+                assertEquals("admin", pr.getName());
+                foundUser = true;
+            } else if (pr instanceof RolePrincipal) {
+                assertEquals("karaf", pr.getName());
+                foundRole = true;
+            }
+        }
+        assertTrue(foundUser);
+        assertTrue(foundRole);
+
+        assertTrue(module.logout());
+        assertEquals("Principals should be gone as the user has logged out", 
0, subject.getPrincipals().size());
+    }
+
+    @Test
+    public void testRoleMappingAdvanced() throws Exception {
+        Properties options = ldapLoginModuleOptions();
+        options.put(LDAPLoginModule.ROLE_MAPPING, 
"admin=karaf,test;admin=another");
+        LDAPLoginModule module = new LDAPLoginModule();
+        CallbackHandler cb = new CallbackHandler() {
+            public void handle(Callback[] callbacks) throws IOException, 
UnsupportedCallbackException {
+                for (Callback cb : callbacks) {
+                    if (cb instanceof NameCallback) {
+                        ((NameCallback) cb).setName("admin");
+                    } else if (cb instanceof PasswordCallback) {
+                        ((PasswordCallback) 
cb).setPassword("admin123".toCharArray());
+                    }
+                }
+            }
+        };
+        Subject subject = new Subject();
+        module.initialize(subject, cb, null, options);
+
+        assertEquals("Precondition", 0, subject.getPrincipals().size());
+        assertTrue(module.login());
+        assertTrue(module.commit());
+
+        assertEquals(4, subject.getPrincipals().size());
+
+        final List<String> roles = new 
ArrayList<String>(Arrays.asList("karaf", "test", "another"));
+
+        boolean foundUser = false;
+        boolean foundRole = false;
+        for (Principal pr : subject.getPrincipals()) {
+            if (pr instanceof UserPrincipal) {
+                assertEquals("admin", pr.getName());
+                foundUser = true;
+            } else if (pr instanceof RolePrincipal) {
+                assertTrue(roles.remove(pr.getName()));
+                foundRole = true;
+            }
+        }
+        assertTrue(foundUser);
+        assertTrue(foundRole);
+        assertTrue(roles.isEmpty());
+
+        assertTrue(module.logout());
+        assertEquals("Principals should be gone as the user has logged out", 
0, subject.getPrincipals().size());
+    }
+
 }
             
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/karaf/blob/cc788d94/manual/src/main/webapp/developers-guide/security-framework.conf
----------------------------------------------------------------------
diff --git a/manual/src/main/webapp/developers-guide/security-framework.conf 
b/manual/src/main/webapp/developers-guide/security-framework.conf
index 3f53b96..6e15be5 100644
--- a/manual/src/main/webapp/developers-guide/security-framework.conf
+++ b/manual/src/main/webapp/developers-guide/security-framework.conf
@@ -313,6 +313,7 @@ The LDAPLoginModule supports the following parameters:
 | {{role.filter}}             | The LDAP filter used to looking for user's 
role, e.g. (member:=uid=%u) |
 | {{role.name.attribute}}     | The LDAP role attribute containing the role 
string used by Karaf, e.g. cn |
 | {{role.search.subtree}}     | If "true", the role lookup will be recursive 
(SUBTREE). If "false", the role lookup will be performed only at the first 
level (ONELEVEL). |
+| {{role.mapping}}            | Define a mapping between roles defined in the 
LDAP directory for the user, and corresponding roles in Karaf. The format is 
ldapRole1=karafRole1,karafRole2;ldapRole2=karafRole3,karafRole4. |
 | {{authentication}}          | Define the authentication backend used on the 
LDAP server. The default is simple. |
 | {{initial.context.factory}} | Define the initial context factory used to 
connect to the LDAP server. The default is com.sun.jndi.ldap.LdapCtxFactory |
 | {{ssl}}                     | If "true" or if the protocol on the 
{{connection.url}} is {{ldaps}}, an SSL connection will be used |

Reply via email to