/*
* Copyright 2016 Studentmediene i Trondheim AS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package no.dusken.momus.ldap;
import no.dusken.momus.model.Person;
import no.dusken.momus.service.repository.PersonRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.ldap.control.PagedResultsDirContextProcessor;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.DefaultTlsDirContextAuthenticationStrategy;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.ldap.LdapContext;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@Service
public class LdapSyncer {
Logger logger = LoggerFactory.getLogger(getClass());
private final int PAGE_SIZE = 1000;
private Long lastId;
@Autowired
LdapTemplate ldapTemplate;
@Autowired
LdapContextSource contextSource;
@Autowired
PersonRepository personRepository;
@Value("${ldap.syncEnabled}")
private boolean enabled;
/**
* This will be run when the server starts
*/
@PostConstruct
public void startUp() {
DefaultTlsDirContextAuthenticationStrategy tls = new DefaultTlsDirContextAuthenticationStrategy();
tls.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
return true;
}
});
contextSource.setAuthenticationStrategy(tls);
sync();
}
/**
* Will pull data from LDAP and update our local copy
* 02:00 each day (second minute hour day month weekdays)
*/
@Scheduled(cron = "0 0 2 * * *")
public void sync() {
if (!enabled) {
logger.info("Not syncing, it is disabled");
return;
}
logger.info("Starting LDAP sync");
long start = System.currentTimeMillis();
syncAllPersonsFromLdap();
long end = System.currentTimeMillis();
long timeUsed = end - start;
logger.info("Done syncing from LDAP, it took {}ms", timeUsed);
}
/**
* Syncs the person database with LDAP
*/
private void syncAllPersonsFromLdap() {
lastId = 0L; // Contains the last granted id, for faster look-up for next free id
// Get all active persons, with base ou=Brukere
List<Person> activePersons = searchForPersons("ou=Brukere", "(objectClass=person)", PAGE_SIZE, true);
//Get all inactive persons, with base ou=DeepDarkVoidOfHell. funny huh
//List<Person> inactivePersons = searchForPersons("ou=DeepDarkVoidOfHell", "(objectClass=person)", PAGE_SIZE, false);
logger.info("Number of users from LDAP: {}", activePersons.size());
logger.info("Number of active: {}", activePersons.size());
}
/**
*
* @param base The base for the ldap search
* @param filter Ldap search filter
* @param pageSize The page size of each search. Must use since ad only allows 1000 results at a time
* @param active Whether or not the user should be flagged as active
* @return A list of the found persons
*/
private List<Person> searchForPersons(String base, String filter, int pageSize, final boolean active){
// For pagination
PagedResultsDirContextProcessor processor = new PagedResultsDirContextProcessor(pageSize, null);
// For searching through subgroups
SearchControls ctrl = new SearchControls();
ctrl.setSearchScope(SearchControls.SUBTREE_SCOPE);
List<Person> persons = new ArrayList<>();
do{
List<Person> result = ldapTemplate.search(base, filter, ctrl, new AttributesMapper<Person>() {
@Override
public Person mapFromAttributes(Attributes attributes) throws NamingException {
Person person = LdapSyncer.this.mapFromAttributes(attributes, active);
personRepository.saveAndFlush(person); //Must save to db per person because of how we give ids
return person;
}
}, processor);
persons.addAll(result);
processor = new PagedResultsDirContextProcessor(pageSize, processor.getCookie());
}while (processor.getCookie().getCookie() != null);
return persons;
}
private Person mapFromAttributes(Attributes attributes, boolean active) throws NamingException {
String firstName = attributes.get("givenName") != null ? (String) attributes.get("givenName").get() : "";
String lastName = attributes.get("sn") != null ? (String) attributes.get("sn").get() : "";
String fullName = firstName + " " + lastName;
String userName = attributes.get("sAMAccountName") != null ? (String) attributes.get("sAMAccountName").get() : "";
String email = attributes.get("mail") != null ? (String) attributes.get("mail").get() : "";
String telephoneNumber = attributes.get("telephoneNumber") != null ? (String) attributes.get("telephoneNumber").get() : "";
Long id = attributes.get("uidNumber") != null ? Long.valueOf((String) attributes.get("uidNumber").get()) : -1L;
id = findId(id, userName);
return new Person(id, userName, firstName, fullName, email, telephoneNumber, active);
}
/**
* This method allocates an ID to the given username if it hasn't one already
* @param id The id from ldap (should be -1 if none was found)
* @param userName The username from ldap
* @return The id granted to the username
*/
private Long findId(Long id, String userName){
// Use id from ldap if one was found
if(id != -1){
return id;
}
Person person = personRepository.findByUsername(userName);
if(person == null){ // New person
id = lastId;
while(personRepository.findOne(id) != null){
id++;
}
lastId = id;
return id;
}
return person.getId();
}
}