package org.yamcs.security; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.YConfiguration; import javax.naming.AuthenticationException; 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 java.security.cert.X509Certificate; import java.util.Arrays; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; /** * Created by msc on 05/05/15. */ public class LdapRealm implements Realm { public static String tmParaPrivPath; public static String tmParaSetPrivPath; public static String tmPacketPrivPath; public static String tcPrivPath; public static String systemPrivPath; public static String rolePath; public static String userPath; static final Hashtable<String, String> contextEnv = new Hashtable<String, String>(); static Logger log = LoggerFactory.getLogger(LdapRealm.class); static { YConfiguration conf = YConfiguration.getConfiguration("privileges"); String host = conf.getString("ldaphost"); userPath = conf.getString("userPath"); rolePath = conf.getString("rolePath"); systemPrivPath = conf.getString("systemPath"); tmParaPrivPath = conf.getString("tmParameterPath"); tmParaSetPrivPath = conf.getString("tmParameterSetPath"); tmPacketPrivPath = conf.getString("tmPacketPath"); tcPrivPath = conf.getString("tcPath"); contextEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); contextEnv.put(Context.PROVIDER_URL, "ldap://" + host); contextEnv.put("com.sun.jndi.ldap.connect.pool", "true"); } private String getUserDn(User user) { String username = user.getAuthenticationToken().getPrincipal().toString(); if (username == null) return null; return "uid=" + username + "," + userPath; } /** * supports * @param authenticationToken * @return true if the authenticationToken is supported by this realm, false otherwise */ public boolean supports(AuthenticationToken authenticationToken) { return authenticationToken.getClass() == UsernamePasswordToken.class || authenticationToken.getClass() == CertificateToken.class || authenticationToken.getClass() == HqClientMessageToken.class; } @Override public boolean authenticates(AuthenticationToken authenticationToken) { if( authenticationToken == null || authenticationToken.getPrincipal() == null) { return false; } if(authenticationToken.getClass() == UsernamePasswordToken.class || authenticationToken.getClass() == HqClientMessageToken.class) return authenticateUsernamePassword((UsernamePasswordToken)authenticationToken); else if(authenticationToken.getClass() == CertificateToken.class) return authenticateCertificate((CertificateToken) authenticationToken); log.error("Authentication Token of type {} is not supported by LDAP realm.", authenticationToken.getClass()); return false; } private boolean authenticateCertificate(CertificateToken authenticationToken) { try { X509Certificate cert = authenticationToken.getCert(); byte[] encodedCert = null; try { encodedCert = cert.getEncoded(); } catch (java.security.cert.CertificateEncodingException e) { log.warn("got CertificateEncodingException when encoding certificate: {}", cert, e); return false; } DirContext context = new InitialDirContext(contextEnv); try { SearchControls cons = new SearchControls(); cons.setSearchScope(SearchControls.SUBTREE_SCOPE); cons.setReturningAttributes(new String[]{"userCertificate"}); NamingEnumeration<SearchResult> results = context.search(userPath, "userCertificate=*", cons); boolean found = false; String uid = null; while (results.hasMore()) { SearchResult r = results.next(); uid = r.getNameInNamespace(); javax.naming.directory.Attribute a = r.getAttributes().get( "userCertificate;binary"); if (a != null) { for (int i = 0; i < a.size(); i++) { if (Arrays.equals(encodedCert, (byte[]) a.get(i))) { found = true; break; } } } if (found) break; } return found; } finally { context.close(); } } catch (NamingException ne) { log.error("Unable to authenticate this X509Certificate certificate against LDAP.", ne); return false; } } private boolean authenticateUsernamePassword(UsernamePasswordToken usernamePasswordToken) { String username = usernamePasswordToken.getUsername(); String password = usernamePasswordToken.getPasswordS(); DirContext ctx = null; try { String userDn = "uid="+username+","+userPath; Hashtable<String, String> localContextEnv = new Hashtable<>(); localContextEnv.put(Context.INITIAL_CONTEXT_FACTORY, contextEnv.get(Context.INITIAL_CONTEXT_FACTORY)); localContextEnv.put(Context.PROVIDER_URL, contextEnv.get(Context.PROVIDER_URL)); localContextEnv.put("com.sun.jndi.ldap.connect.pool", "true"); localContextEnv.put(Context.SECURITY_AUTHENTICATION, "simple"); localContextEnv.put(Context.SECURITY_PRINCIPAL, userDn); if( password != null ) { localContextEnv.put(Context.SECURITY_CREDENTIALS, password); } ctx = new InitialDirContext( localContextEnv ); ctx.close(); } catch (AuthenticationException e) { log.warn( "User '{}' not authenticated with LDAP; Could not bind with supplied username and password.", username ); return false; } catch (NamingException e) { log.warn( "User '{}' not authenticated with LDAP; An LDAP error was caught: {}", username, e ); return false; } return true; } /** * Loads a user from LDAP together with all the roles and the privileges. * */ @Override public User loadUser(AuthenticationToken authenticationToken) { log.info(""); User u = new User(authenticationToken); u.lastUpdated = System.currentTimeMillis(); // Load privileges DirContext context = null; try { context = new InitialDirContext(contextEnv); } catch (NamingException e) { log.error("", e); return null; } try { String dn = "uid=" + u.getPrincipalName() + "," + userPath; Set<String> ldapRoles = loadRoles(context, dn); u.roles = ldapRolesToRoles(ldapRoles); if (u.roles == null) return u; u.tmParaPrivileges = loadPrivileges(context, ldapRoles, tmParaPrivPath, "groupOfNames", "cn"); u.tmPacketPrivileges = loadPrivileges(context, ldapRoles, tmPacketPrivPath, "groupOfNames", "cn"); u.tcPrivileges = loadPrivileges(context, ldapRoles, tcPrivPath, "groupOfNames", "cn"); u.systemPrivileges = loadPrivileges(context, ldapRoles, systemPrivPath, "groupOfNames", "cn"); // might fail on previous yamcs ldap since this is a new type of privileges: u.tmParaSetPrivileges = loadPrivileges(context, ldapRoles, tmParaSetPrivPath, "groupOfNames", "cn"); } catch (NamingException e) { log.error("", e); } finally { try { context.close(); } catch (NamingException e) { log.error("", e); } } // Check authentication u.setAuthenticated(this.authenticates(authenticationToken)); log.debug("got user from ldap: " + u); return u; } /** * filter roles to remove ldap path * @param ldapRoles * @return */ private Set<String> ldapRolesToRoles(Set<String> ldapRoles) { if(ldapRoles == null) return null; Set<String> roles = new HashSet<>(); for(String ldapRole : ldapRoles) { try { int start = ldapRole.indexOf("cn="); int stop = ldapRole.indexOf(",ou="); roles.add(ldapRole.substring(start + 3, stop)); } catch (Exception e){ log.error("Unable to extract role from LDAP search result", e); } } return roles; } Set<String> loadAssertedIdentities(DirContext context, String dn) throws NamingException { SearchControls cons = new SearchControls(); cons.setSearchScope(SearchControls.SUBTREE_SCOPE); cons.setReturningAttributes(new String[] { "member" }); NamingEnumeration<SearchResult> results; try { results = context.search("cn=assertedIdentities, " + dn, "member=*", cons); } catch (javax.naming.NameNotFoundException e) { // cn=canassertIdentities doesn't even exist for this user return null; } if (!results.hasMore()) return null; HashSet<String> assertedids = new HashSet<String>(); SearchResult r = results.next(); javax.naming.directory.Attribute a = r.getAttributes().get("member"); for (int i = 0; i < a.size(); i++) { assertedids.add((String) a.get(i)); } return assertedids; } Set<String> loadRoles(DirContext context, String dn) throws NamingException { SearchControls cons = new SearchControls(); cons.setSearchScope(SearchControls.SUBTREE_SCOPE); cons.setReturningAttributes(new String[] { "cn" }); NamingEnumeration<SearchResult> results = context.search(rolePath, "member={0}", new String[] { dn }, cons); if (!results.hasMore()) { return null; } HashSet<String> roles = new HashSet<String>(); while (results.hasMore()) { SearchResult r = results.next(); roles.add(r.getNameInNamespace()); } return roles; } Set<String> loadPrivileges(DirContext context, Set<String> roles, String privPath, String objectClass, String attribute) throws NamingException { Set<String> privs = new HashSet<String>(); StringBuffer sb = new StringBuffer(); SearchControls cons = new SearchControls(); cons.setSearchScope(SearchControls.SUBTREE_SCOPE); cons.setReturningAttributes(new String[] { attribute }); sb.append("(&(objectClass=" + objectClass + ")(|"); for (int i = 0; i < roles.size(); i++) { sb.append("(member={" + i + "})"); } sb.append("))"); NamingEnumeration<SearchResult> results = context.search(privPath, sb.toString(), roles.toArray(), cons); while (results.hasMore()) { SearchResult r = results.next(); privs.add((String) r.getAttributes().get("cn").get()); } return privs; } public String getUserPath() { return userPath; } }