/*
* RESTHeart - the Web API for MongoDB
* Copyright (C) SoftInstigate Srl
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.restheart.security.impl;
import io.undertow.security.idm.Account;
import io.undertow.security.idm.Credential;
import io.undertow.security.idm.IdentityManager;
import io.undertow.security.idm.PasswordCredential;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.io.*;
import java.util.*;
import java.util.function.Consumer;
/**
*
* @author Jason Brown
*
* I don't know how well this will work in other environments, I built it specifically for mine.
* The principal name isn't consistent across different users within our DC, so I added support for
* multiple suffixes to be added to the username. In addition, we have 3 domain controllers. I
* added support to list them so I don't have to wait for changes to propogate across all DCs before
* the new user can log in. Simply add the "adim" section to security.yml and switch then change
* restheart.yml to use org.restheart.security.impl.ADIdentityManager instead of DBIdentityManager
* or SimpleFileIdentityManager. This still uses the file based access manager, so you'll need to
* choose which AD roles you want to map to Admin and/or other users. The code will use the each
* DC listed, in order (so list your most stable or closest DC first). It will use each suffix in
* the principalNameSuffixes list in order as well, so list your most common one first. It will
* try all suffixes at one DC, before moving on to try all suffixes at the next DC.
* <code>
## Config for AD Identity Manager
adim:
- domainControllers: ldap://eastdc.example.com
principalNameSuffixes: corp.example.com,example.com
* </code>
*/
public final class ADIdentityManager extends AbstractSimpleSecurityManager implements IdentityManager {
private static final Logger LOGGER = LoggerFactory.getLogger(ADIdentityManager.class);
private String[] ldapURLs = null;
private String[] principalNameSuffixes = null;
/**
*
* @param arguments
* @throws FileNotFoundException
*/
public ADIdentityManager(Map<String, Object> arguments) throws FileNotFoundException {
init(arguments, "adim");
}
@Override
Consumer<? super Map<String, Object>> consumeConfiguration() {
return ci -> {
LOGGER.info(ci.keySet().toString());
Object _dc = ci.get("domainControllers");
if (_dc == null || !(_dc instanceof String)) {
throw new IllegalArgumentException("wrong configuration file format. missing domainControllers property, should be a comma separated list of DCs to authenticate against. E.g. 'ldap://eastdc.example.com,ldap://westdc.example.com'");
}
this.ldapURLs = ((String) _dc).split(",");
Object _pns = ci.get("principalNameSuffixes");
if (_pns == null || !(_pns instanceof String)) {
throw new IllegalArgumentException("wrong configuration file format. missing principalNameSuffixes property, should be a comma separated list of suffixes to add to end of username. E.g. 'example.com,corp.example.com,example.net'");
}
this.principalNameSuffixes = ((String) _pns).split(",");
};
}
@Override
public Account verify(Account account) {
return account;
}
@Override
public Account verify(Credential credential) {
return null;
}
@Override
public Account verify(String username, Credential credential) {
if (username == null || credential == null || !(credential instanceof PasswordCredential)){
return null;
}
PasswordCredential pwc = (PasswordCredential) credential;
try {
Set<String> roles = getRoles(username, new String(pwc.getPassword()));
Account acct = new SimpleAccount(username, pwc.getPassword(), roles);
return acct;
} catch (NamingException e) {
LOGGER.error(e.getMessage(), e);
}
return null;
}
private Set<String> getRoles(String username, String password) throws NamingException {
if (password == null || password.trim().length() == 0 || username == null || username.trim().length() == 0){
return null;
}
for (String ldapURL : ldapURLs) {
for (String pns : principalNameSuffixes) {
ldapURL = ldapURL.trim();
pns = pns.trim();
String principalName = username + "@" + pns;
Hashtable props = new Hashtable();
props.put(Context.SECURITY_PRINCIPAL, principalName);
props.put(Context.SECURITY_CREDENTIALS, password);
props.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
props.put(Context.PROVIDER_URL, ldapURL);
try {
LdapContext ldc = new InitialLdapContext(props, null);
LOGGER.info("Connected to " + ldapURL + " as user " + principalName);
return getRoles(ldc, principalName, pns);
} catch (javax.naming.CommunicationException e) {
LOGGER.warn("Failed to connect to " + ldapURL);
} catch (NamingException e) {
LOGGER.warn("Failed to authenticate " + principalName);
}
}
}
LOGGER.error("Failed to connect to any specified DC with any user/suffix combination");
throw new NamingException("Failed to connect to any specified DC with any user/suffix combination");
}
private Set<String> getRoles(LdapContext ldc, String principalName, String principalNameSuffix){
SearchControls sc = new SearchControls();
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
sc.setReturningAttributes(new String[]{"memberOf"});
String searchString = "(&(objectClass=user)(userPrincipalName=" + principalName + "))";
try{
Set<String> roles = new HashSet<>();
NamingEnumeration<SearchResult> results = ldc.search(toDCList(principalNameSuffix), searchString, sc);
if (results.hasMore()){
SearchResult res = results.next();
System.out.println("res.getName = " + res.getName());
Attributes attrs = res.getAttributes();
if (attrs != null){
NamingEnumeration attrEnum = attrs.getAll();
while (attrEnum.hasMore()){
Attribute attr = (Attribute)attrEnum.next();
System.out.println("Attribute : " + attr.getID());
NamingEnumeration sa = attr.getAll();
while (sa.hasMore()){
String[] parts = ((String)sa.next()).split(",");
String[] kv = parts[0].split("=");
roles.add(kv[1]);
}
}
}
}
return roles;
}
catch(NamingException e){
LOGGER.error("Failed to lookup groups for user " + principalName);
}
return null;
}
private static String toDCList(String domainName)
{
StringBuilder sb = new StringBuilder();
for (String p : domainName.split("\\."))
{
if (p.length() > 0) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append("DC=").append(p);
}
}
return sb.toString();
}
}