diff --git a/pom.xml b/pom.xml index cee6b27..b20750f 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ org.elasticsearch.plugin plugins - 2.3.1 + 2.3.2 4.0.0 @@ -121,7 +121,7 @@ slf4j-log4j12 1.7.12 test - + diff --git a/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosAuthenticationToken.java b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosAuthenticationToken.java index d40cff0..ed1d64b 100644 --- a/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosAuthenticationToken.java +++ b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosAuthenticationToken.java @@ -17,6 +17,8 @@ */ package de.codecentric.elasticsearch.plugin.kerberosrealm.realm; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import org.elasticsearch.common.logging.ESLogger; @@ -29,11 +31,20 @@ public class KerberosAuthenticationToken implements AuthenticationToken { protected final ESLogger logger = Loggers.getLogger(this.getClass()); private byte[] outToken; private final String principal; + private List groups; public KerberosAuthenticationToken(final byte[] outToken, final String principal) { super(); this.outToken = Objects.requireNonNull(outToken); this.principal = Objects.requireNonNull(principal); + this.groups = new ArrayList(); + } + + public KerberosAuthenticationToken(final byte[] outToken, final String principal, final List groups2) { + super(); + this.outToken = Objects.requireNonNull(outToken); + this.principal = Objects.requireNonNull(principal); + this.groups = groups2; } @Override @@ -51,6 +62,10 @@ public Object credentials() { public String principal() { return principal; } + + public List groups() { + return this.groups; + } @Override public String toString() { diff --git a/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosRealm.java b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosRealm.java index c4629e3..fb8c8df 100644 --- a/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosRealm.java +++ b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/KerberosRealm.java @@ -26,11 +26,20 @@ import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Hashtable; import java.util.List; import java.util.Locale; import java.util.Map; +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; import javax.security.auth.Subject; import javax.security.auth.login.LoginException; import javax.xml.bind.DatatypeConverter; @@ -71,9 +80,20 @@ public class KerberosRealm extends Realm { private final boolean stripRealmFromPrincipalName; private final String acceptorPrincipal; private final Path acceptorKeyTabPath; + private final String keyStorePath; + private final String keyStorePassword; + // maps principal string to shield role private final ListMultimap rolesMap = ArrayListMultimap. create(); + // maps group string to shield role + private final ListMultimap groupMap = ArrayListMultimap. create(); private final Environment env; private final boolean mockMode; + + private final String ldapConnectionString; + private final String ldapDomain; + private final String ldapUser; + private final String ldapPassword; + private final String ldapGroupBase; public KerberosRealm(final RealmConfig config) { super(TYPE, config); @@ -81,23 +101,46 @@ public KerberosRealm(final RealmConfig config) { acceptorPrincipal = config.settings().get(SettingConstants.ACCEPTOR_PRINCIPAL, null); final String acceptorKeyTab = config.settings().get(SettingConstants.ACCEPTOR_KEYTAB_PATH, null); + ldapConnectionString = config.settings().get(SettingConstants.LDAP_URL); + ldapDomain = config.settings().get(SettingConstants.LDAP_DOMAIN); + ldapGroupBase = config.settings().get(SettingConstants.LDAP_GROUP_BASE); + ldapUser = config.settings().get(SettingConstants.LDAP_USER, null); + ldapPassword = config.settings().get(SettingConstants.LDAP_PASSWORD, null); + + logger.debug("ldapDomain Path: {}", ldapDomain); + logger.debug("ldapGroupBase: {}", ldapGroupBase); + logger.debug("ldapConnectionString: {}", ldapConnectionString); + + + keyStorePath = config.globalSettings().get(SettingConstants.KEYSTORE_PATH, null); + keyStorePassword = config.globalSettings().get(SettingConstants.KEYSTORE_PASSWORD, null); + + logger.debug("KeyStore Path: {}", keyStorePath); + + //shield.authc.realms.cc-kerberos.roles.: principal1, principal2 //shield.authc.realms.cc-kerberos.roles.: principal1, principal3 ////shield.authc.realms.cc-kerberos.roles.admin: luke@EXAMPLE.COM, vader@EXAMPLE.COM - + Map roleGroups = config.settings().getGroups(SettingConstants.ROLES+"."); - + if(roleGroups != null) { for(String roleGroup:roleGroups.keySet()) { - - for(String principal:config.settings().getAsArray(SettingConstants.ROLES+"."+roleGroup)) { - rolesMap.put(stripRealmName(principal, stripRealmFromPrincipalName), roleGroup); + + for(String principalOrGroup:config.settings().getAsArray(SettingConstants.ROLES+"."+roleGroup)) { + String groupSid = null; + if((groupSid = getSidFromGroupName(principalOrGroup)) != null){ + groupMap.put(principalOrGroup, roleGroup); + logger.debug("Found group {}:{}", principalOrGroup, groupSid); + } else { + rolesMap.put(stripRealmName(principalOrGroup, stripRealmFromPrincipalName), roleGroup); + } } } - } - + } + logger.debug("Parsed roles: {}", rolesMap); - + env = new Environment(config.globalSettings()); mockMode = config.settings().getAsBoolean("mock_mode", false); @@ -108,6 +151,14 @@ public KerberosRealm(final RealmConfig config) { if (acceptorKeyTab == null) { throw new ElasticsearchException("Unconfigured (but required) property: {}", SettingConstants.ACCEPTOR_KEYTAB_PATH); } + + if (keyStorePath == null) { + throw new ElasticsearchException("Unconfigured (but required) property: {}", SettingConstants.KEYSTORE_PATH); + } + + if (keyStorePassword == null) { + throw new ElasticsearchException("Unconfigured (but required) property: {}", SettingConstants.KEYSTORE_PASSWORD); + } acceptorKeyTabPath = env.configFile().resolve(acceptorKeyTab); @@ -120,6 +171,157 @@ public KerberosRealm(final RealmConfig config) { this(config); }*/ + /* + * The binary data is in the form: + * byte[0] - revision level + * byte[1] - count of sub-authorities + * byte[2-7] - 48 bit authority (big-endian) + * and then count x 32 bit sub authorities (little-endian) + * + * The String value is: S-Revision-Authority-SubAuthority[n]... + * + * Based on code from here - http://forums.oracle.com/forums/thread.jspa?threadID=1155740&tstart=0 + */ + public static String decodeSID(byte[] sid) { + + final StringBuilder strSid = new StringBuilder("S-"); + + // get version + final int revision = sid[0]; + strSid.append(Integer.toString(revision)); + + //next byte is the count of sub-authorities + final int countSubAuths = sid[1] & 0xFF; + + //get the authority + long authority = 0; + //String rid = ""; + for(int i = 2; i <= 7; i++) { + authority |= ((long)sid[i]) << (8 * (5 - (i - 2))); + } + strSid.append("-"); + strSid.append(Long.toHexString(authority)); + + //iterate all the sub-auths + int offset = 8; + int size = 4; //4 bytes for each sub auth + for(int j = 0; j < countSubAuths; j++) { + long subAuthority = 0; + for(int k = 0; k < size; k++) { + subAuthority |= (long)(sid[offset + k] & 0xFF) << (8 * k); + } + + strSid.append("-"); + strSid.append(subAuthority); + + offset += size; + } + + return strSid.toString(); + } + + private boolean isInRole(String group, String principal){ + String query = "(&(objectClass=user)(sAMAccountName=" + principal + ")(memberOf:1.2.840.113556.1.4.1941:=CN=" + group + "," + ldapGroupBase + "))"; + NamingEnumeration results = queryLdap(query); + try{ + return results.hasMoreElements(); + }catch(Exception e){ + return false; + } + } + + private NamingEnumeration queryLdap(String query){ + Hashtable env = new Hashtable(11); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + env.put("java.naming.ldap.factory.socket", TrustAllSSLSocketFactory.class.getName()); + env.put("javax.net.ssl.keyStore", this.keyStorePath); + env.put("javax.net.ssl.keyStorePassword", this.keyStorePassword); + + if(ldapUser != null && ldapPassword != null){ + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + env.put(Context.SECURITY_PRINCIPAL, ldapUser); + env.put(Context.SECURITY_CREDENTIALS, ldapPassword); + logger.debug("Connecting to LDAP with username: {}", ldapUser); + }else{ + env.put(Context.SECURITY_AUTHENTICATION, "none"); + logger.debug("Attempting anonymous bind"); + } + + env.put(Context.PROVIDER_URL, ldapConnectionString); + env.put("java.naming.ldap.attributes.binary", "objectSID"); + + List formatedDomain = new ArrayList(); + for(String dc:(ldapDomain.split("\\."))){ + formatedDomain.add("DC=" + dc + ","); + } + String searchBase = ""; + for(int i = 0; i < formatedDomain.size(); i++){ + searchBase += formatedDomain.get(i); + } + searchBase = searchBase.substring(0, searchBase.length()-1); + logger.debug("Search base {}", searchBase); + + // Grab the current classloader to restore after loading custom sockets in JNDI context + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + + DirContext ctx = null; + try { + + Thread.currentThread().setContextClassLoader(TrustAllSSLSocketFactory.class.getClassLoader()); + // Create initial context + ctx = new InitialDirContext(env); + + + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + + NamingEnumeration results = ctx.search(searchBase, query, searchControls); + return results; + + } catch (NamingException e) { + logger.error("Could not connect to LDAP with provided method", e); + } finally { + if(ctx != null){ + try { + ctx.close(); + } catch (NamingException e) { + // pass + } + } + Thread.currentThread().setContextClassLoader(cl); + } + return null; + } + + private SearchResult queryLdapForGroup(String group){ + String query = "(&(objectClass=group)(cn=" + group + "))"; + NamingEnumeration result = queryLdap(query); + if(result != null){ + try{ + return result.nextElement(); + }catch (Exception e){ + return null; + } + } else { + return null; + } + } + + private String getSidFromGroupName(String groupName){ + try { + SearchResult searchResult = queryLdapForGroup(groupName); + if(searchResult != null) { + byte[] sidbytes = null; + sidbytes = (byte[])searchResult.getAttributes().get("objectSid").get(); + String sid = decodeSID(sidbytes); + return sid; + } + } catch (NamingException e) { + logger.error("Error retrieving sid from group name '{}' : {}", groupName,e); + } + return null; + } + @Override public boolean supports(final AuthenticationToken token) { return token instanceof KerberosAuthenticationToken; @@ -173,6 +375,7 @@ private KerberosAuthenticationToken tokenMock(final String authorizationHeader) private KerberosAuthenticationToken tokenKerb(final String authorizationHeader) { Principal principal = null; + List groups = null; if (authorizationHeader != null && acceptorKeyTabPath != null && acceptorPrincipal != null) { @@ -209,6 +412,14 @@ public GSSCredential run() throws GSSException { principal = Subject.doAs(subject, new AuthenticateAction(logger, gssContext, stripRealmFromPrincipalName)); + // find any groups with ldap + groups = new ArrayList(); + for(String group:groupMap.keys()){ + if(!groups.contains(group) && isInRole(group, principal.getName())){ + logger.debug("User {} in LDAP group {}", principal.getName(), group); + groups.add(group); + } + } } catch (final LoginException e) { logger.error("Login exception due to {}", e, e.toString()); throw ExceptionsHelper.convertToRuntime(e); @@ -241,7 +452,7 @@ public GSSCredential run() throws GSSException { } final String username = ((SimpleUserPrincipal) principal).getName(); - return new KerberosAuthenticationToken(outToken, username); + return new KerberosAuthenticationToken(outToken, username, groups); } } else { @@ -270,12 +481,13 @@ public KerberosAuthenticationToken token(final TransportMessage message) { @Override public User authenticate(final KerberosAuthenticationToken token) { - + if(token == KerberosAuthenticationToken.LIVENESS_TOKEN) { return InternalSystemUser.INSTANCE; } - + final String actualUser = token.principal(); + final List actualGroups = token.groups(); if (actualUser == null || actualUser.isEmpty() || token.credentials() == null) { logger.warn("User '{}' cannot be authenticated", actualUser); @@ -284,7 +496,21 @@ public User authenticate(final KerberosAuthenticationToken token) { String[] userRoles = new String[0]; List userRolesList = rolesMap.get(actualUser); - + + + if(actualGroups != null){ + for(String group: actualGroups){ + if(groupMap.containsKey(group)){ + for(String role:groupMap.get(group)){ + if(!userRolesList.contains(role)){ + userRolesList.add(role); + } + } + logger.debug("User '{}' found in AD group {} mapping to shield role {}", actualUser, group, Arrays.toString(groupMap.get(group).toArray(new String[0]))); + } + } + } + if(userRolesList != null && !userRolesList.isEmpty()) { userRoles = userRolesList.toArray(new String[0]); } @@ -364,7 +590,7 @@ private static String getUsernameFromGSSContext(final GSSContext gssContext, fin return null; } - + private static String stripRealmName(String name, boolean strip){ if (strip && name != null) { final int i = name.indexOf('@'); @@ -373,7 +599,7 @@ private static String stripRealmName(String name, boolean strip){ name = name.substring(0, i); } } - + return name; } diff --git a/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/TrustAllSSLSocketFactory.java b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/TrustAllSSLSocketFactory.java new file mode 100644 index 0000000..34794fa --- /dev/null +++ b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/TrustAllSSLSocketFactory.java @@ -0,0 +1,66 @@ +package de.codecentric.elasticsearch.plugin.kerberosrealm.realm; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.SecureRandom; + +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; + +public class TrustAllSSLSocketFactory extends SSLSocketFactory { + private SSLSocketFactory socketFactory; + + public TrustAllSSLSocketFactory() { + try { + SSLContext ctx = SSLContext.getInstance("TLS"); + ctx.init(null, new TrustManager[] { new TrustAllX509TrustManager() }, new SecureRandom()); + socketFactory = ctx.getSocketFactory(); + } catch (Exception ex) { + //ex.printStackTrace(System.err); + /* handle exception */ + } + } + + public static SocketFactory getDefault() { + return new TrustAllSSLSocketFactory(); + } + + @Override + public String[] getDefaultCipherSuites() { + return socketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return socketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String string, int i, boolean bln) throws IOException { + return socketFactory.createSocket(socket, string, i, bln); + } + + @Override + public Socket createSocket(String string, int i) throws IOException, UnknownHostException { + return socketFactory.createSocket(string, i); + } + + @Override + public Socket createSocket(String string, int i, InetAddress ia, int i1) throws IOException, UnknownHostException { + return socketFactory.createSocket(string, i, ia, i1); + } + + @Override + public Socket createSocket(InetAddress ia, int i) throws IOException { + return socketFactory.createSocket(ia, i); + } + + @Override + public Socket createSocket(InetAddress ia, int i, InetAddress ia1, int i1) throws IOException { + return socketFactory.createSocket(ia, i, ia1, i1); + } +} \ No newline at end of file diff --git a/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/TrustAllX509TrustManager.java b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/TrustAllX509TrustManager.java new file mode 100644 index 0000000..ce23812 --- /dev/null +++ b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/realm/TrustAllX509TrustManager.java @@ -0,0 +1,20 @@ +package de.codecentric.elasticsearch.plugin.kerberosrealm.realm; + +import javax.net.ssl.X509TrustManager; +import java.security.cert.X509Certificate; + +public class TrustAllX509TrustManager implements X509TrustManager { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(java.security.cert.X509Certificate[] certs, + String authType) { + } + + public void checkServerTrusted(java.security.cert.X509Certificate[] certs, + String authType) { + } + +} + diff --git a/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/support/SettingConstants.java b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/support/SettingConstants.java index 0271cfc..beb79ee 100644 --- a/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/support/SettingConstants.java +++ b/src/main/java/de/codecentric/elasticsearch/plugin/kerberosrealm/support/SettingConstants.java @@ -30,6 +30,17 @@ public class SettingConstants { public static final String KRB_DEBUG = PREFIX + "krb_debug"; public static final String KRB5_FILE_PATH = PREFIX + "krb5.file_path"; + + public static final String LDAP_URL = "ldap_url"; + public static final String LDAP_DOMAIN = "ldap_domain"; + public static final String LDAP_GROUP_BASE = "ldap_group_base"; + public static final String LDAP_USER = "ldap_user"; + public static final String LDAP_PASSWORD = "ldap_password"; + + public static final String KEYSTORE_PATH = "shield.ssl.keystore.path"; + public static final String KEYSTORE_PASSWORD = "shield.ssl.keystore.password"; + + private SettingConstants() {