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 |
