Skip to content
Snippets Groups Projects

Initial commit of LDAP report

Merged Martin Lowe requested to merge malowe/1 into main
6 files
+ 470
0
Compare changes
  • Side-by-side
  • Inline
Files
6
package org.eclipsefoundation.reports;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.RuntimeJsonMappingException;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.auto.value.AutoValue;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
import picocli.CommandLine;
/**
* Generates an LDAP report
*
* @author Martin Lowe
*
*/
@CommandLine.Command(name = "ldap-report")
public class LDAPReport implements Runnable {
public static final Logger LOGGER = LoggerFactory.getLogger(LDAPReport.class);
// setting for CSV base
@CommandLine.Option(names = { "-b", "--base" }, description = "Path to CSV base of report", required = true)
String path;
// settings for LDAP connection
@CommandLine.Option(names = { "-h", "--host" }, description = "Host for the LDAP", defaultValue = "localhost")
String host;
@CommandLine.Option(names = { "-p", "--port" }, description = "Port for the LDAP connection", defaultValue = "389")
Integer port;
@CommandLine.Option(names = { "-d",
"--basedn" }, description = "Base DN for LDAP query", defaultValue = "dc=eclipse,dc=org")
String baseDN;
@CommandLine.Option(names = { "-B", "--binddn" }, description = "Bind DN for LDAP query")
String bindDN;
@CommandLine.Option(names = { "-P",
"--password" }, description = "Password to authenticate for LDAP query", interactive = true)
String password;
final CsvMapper CSV_MAPPER = (CsvMapper) new CsvMapper().registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
@Override
public void run() {
// attempt to open ldap connection
try (LDAPConnection conn = getConnection();
StringWriter sw = new StringWriter()) {
// read in the base csv
List<FoundationUserEntry> foundationUsers = readInBase();
// use the LDAP connection and foundation users to generate the report
List<ReportEntry> report = generateReport(conn, foundationUsers);
// output the report to the string writer
CSV_MAPPER.writer(CSV_MAPPER.schemaFor(ReportEntry.class).withHeader()).writeValues(sw).writeAll(report);
LOGGER.info("OUTPUT:\n{}", sw);
} catch (LDAPException e) {
LOGGER.error("Error while connecting to LDAP host", e);
} catch (IOException e) {
LOGGER.error("Error while reading the base CSV file", e);
}
}
private LDAPConnection getConnection() throws LDAPException {
return password != null && password != "" ? new LDAPConnection(host, port, bindDN, password)
: new LDAPConnection(host, port);
}
private List<ReportEntry> generateReport(LDAPConnection conn, List<FoundationUserEntry> foundationUsers)
throws LDAPException {
List<ReportEntry> report = new ArrayList<>();
for (FoundationUserEntry user : foundationUsers) {
// query ldap for the current user
SearchResult result = conn.search(new SearchRequest(baseDN, SearchScope.SUB, String
.format("(|(uid=%s)(mail=%s))", user.getPersonId(), user.getEmail())));
List<SearchResultEntry> out = result.getSearchEntries();
// build the report item
ReportEntry.Builder b = ReportEntry.builder();
b.setPersonId(user.getPersonId());
b.setFoundationEmail(user.getEmail());
if (out.isEmpty()) { // if there is no match for current user in LDAP
b.setCode(StatusCode.MISSING);
b.setLdapAccounts(Collections.emptyList());
} else if (out.size() > 1) { // if there is more than one match in LDAP for current user
b.setCode(StatusCode.AMBIGUOUS_ENTRY);
b.setLdapAccounts(out.stream().map(this::buildCompositeLdapAccount).collect(Collectors.toList()));
} else {
// 1 match found, check how well it matches
SearchResultEntry entry = out.get(0);
if (!entry.getAttribute("uid").getValue().equalsIgnoreCase(user.getPersonId())) {
b.setCode(StatusCode.MISMATCH_UID);
} else if (!entry.getAttribute("mail").getValue().equalsIgnoreCase(user.getEmail())) {
b.setCode(StatusCode.MISMATCH_EMAIL);
} else {
b.setCode(StatusCode.OK);
}
b.setLdapAccounts(Arrays.asList(buildCompositeLdapAccount(out.get(0))));
}
report.add(b.build());
}
return report;
}
/**
* Reads in the CSV base file for foundation user accounts. This file should include headers and have columns in the
* following order:
*
* <ul>
* <li>personId
* <li>email
* </ul>
*
* @return the read in list of FoundationDB users to compare to LDAP.
* @throws IllegalStateException if the CSV source is illegible or inaccessible.
*/
private List<FoundationUserEntry> readInBase() {
List<FoundationUserEntry> a = new ArrayList<>();
try (JsonParser parser = CSV_MAPPER.reader(CSV_MAPPER.schemaFor(FoundationUserEntry.class).withHeader())
.createParser(Path.of(path).toFile())) {
Iterator<FoundationUserEntry> entries = parser.readValuesAs(FoundationUserEntry.class);
entries.forEachRemaining(a::add);
} catch (IOException e) {
LOGGER.warn("Encountered an issue while reading in data", e);
throw new IllegalStateException("CSV must be in legible format and accessible to generate report");
} catch (RuntimeJsonMappingException e) {
// Expected, as there seems to be a trailing entity that throws errors because its empty
}
return a;
}
/**
* Formats an LDAP account entry to be in the pattern of <code>&lt;uid&gt;|&lt;mail&gt;</code>.
*
* @param result the LDAP search result to format.
* @return the formatted LDAP account string.
*/
private String buildCompositeLdapAccount(SearchResultEntry result) {
return result.getAttribute("uid").getValue() + "|" + result.getAttribute("mail").getValue();
}
/**
* Status codes for different data states.
*
* @author Martin Lowe
*
*/
public static enum StatusCode {
OK, MISSING, AMBIGUOUS_ENTRY, MISMATCH_UID, MISMATCH_EMAIL;
}
/**
* Data from foundation DB used as a base reference to determine missing higher access users. This data can be
* fetched using the following query:
*
* <code>
* SELECT p.PersonID, p.Email FROM OrganizationContacts oc INNER JOIN People p ON p.PersonID = oc.PersonID WHERE oc.Relation IN ('CR', 'DE', 'MA', 'CRA') GROUP BY p.PersonID
* </code>
*
* @author Martin Lowe
*
*/
@AutoValue
@JsonPropertyOrder({ "personId", "email" })
@JsonDeserialize(builder = AutoValue_LDAPReport_FoundationUserEntry.Builder.class)
public static abstract class FoundationUserEntry {
public abstract String getPersonId();
public abstract String getEmail();
public static Builder builder() {
return new AutoValue_LDAPReport_FoundationUserEntry.Builder();
}
@AutoValue.Builder
@JsonPOJOBuilder(withPrefix = "set")
public abstract static class Builder {
public abstract Builder setPersonId(String personId);
public abstract Builder setEmail(String email);
public abstract FoundationUserEntry build();
}
}
/**
* CSV output row. The JSON property order is how CSV header order is determined.
*
* @author Martin Lowe
*
*/
@AutoValue
@JsonPropertyOrder({ "personId", "foundationEmail", "ldapAccounts", "code" })
@JsonDeserialize(builder = AutoValue_LDAPReport_ReportEntry.Builder.class)
public static abstract class ReportEntry {
public abstract String getPersonId();
public abstract String getFoundationEmail();
public abstract List<String> getLdapAccounts();
public abstract StatusCode getCode();
public static Builder builder() {
return new AutoValue_LDAPReport_ReportEntry.Builder();
}
@AutoValue.Builder
@JsonPOJOBuilder(withPrefix = "set")
public abstract static class Builder {
public abstract Builder setPersonId(String personId);
public abstract Builder setFoundationEmail(String foundationEmail);
public abstract Builder setLdapAccounts(List<String> ldapAccounts);
public abstract Builder setCode(StatusCode code);
public abstract ReportEntry build();
}
}
}
Loading