Commit 5fcc1d59 authored by Martin Lowe's avatar Martin Lowe 🇨🇦
Browse files

Merge branch 'malowe/1' into 'main'

Initial commit of LDAP report

See merge request !1
parents 7fe629d4 9574a80e
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
/.pnp
.pnp.js
# docker volumes
volumes/
certs
# testing
/coverage
# production
build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.eslintcache
npm-debug.log*
yarn-debug.log*
yarn-error.log*
dist/
npm-debug.log
yarn-error.log
# Editor directories and files
*.suo
*.ntvs*
*.njsproj
*.sln
#Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
release.properties
# Eclipse
.project
.classpath
.settings/
bin/
# IntelliJ
.idea
*.ipr
*.iml
*.iws
# NetBeans
nb-configuration.xml
# Visual Studio Code
.factorypath
# Vim
*.swp
*.swo
# patch
*.orig
*.rej
#environment variables
.env
#Test resources
/.apt_generated/
/.apt_generated_tests/
report.csv
version: '2'
services:
openldap:
image: osixia/openldap:latest
container_name: openldap
environment:
LDAP_LOG_LEVEL: "256"
LDAP_ORGANISATION: "Example Inc."
LDAP_DOMAIN: "example.org"
LDAP_BASE_DN: ""
LDAP_ADMIN_PASSWORD: "admin"
LDAP_CONFIG_PASSWORD: "config"
LDAP_READONLY_USER: "false"
LDAP_RFC2307BIS_SCHEMA: "false"
LDAP_BACKEND: "mdb"
LDAP_TLS: "true"
LDAP_TLS_CRT_FILENAME: "ldap.crt"
LDAP_TLS_KEY_FILENAME: "ldap.key"
LDAP_TLS_CA_CRT_FILENAME: "ca.crt"
LDAP_TLS_ENFORCE: "false"
LDAP_TLS_CIPHER_SUITE: "SECURE256:-VERS-SSL3.0"
LDAP_TLS_PROTOCOL_MIN: "3.1"
LDAP_TLS_VERIFY_CLIENT: "demand"
LDAP_REPLICATION: "false"
KEEP_EXISTING_CONFIG: "false"
LDAP_REMOVE_CONFIG_AFTER_SETUP: "true"
LDAP_SSL_HELPER_PREFIX: "ldap"
tty: true
stdin_open: true
volumes:
- /var/lib/ldap
- /etc/ldap/slapd.d
- /container/service/slapd/assets/certs/
ports:
- "389:389"
- "636:636"
domainname: "example.org"
hostname: "example.org"
phpldapadmin:
image: osixia/phpldapadmin:latest
container_name: phpldapadmin
environment:
PHPLDAPADMIN_LDAP_HOSTS: "openldap"
PHPLDAPADMIN_HTTPS: "false"
ports:
- "8080:80"
depends_on:
- openldap
\ No newline at end of file
#!/bin/bash
java -jar target/quarkus-app/quarkus-run.jar --base=/localdev/standalone-java-reports/report.csv --port=3389
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>standalone-java-reports</groupId>
<artifactId>standalone-java-reports</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<surefire-plugin.version>2.22.0</surefire-plugin.version>
<quarkus.version>2.7.2.Final</quarkus.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.test.skip>true</maven.test.skip>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.parameters>true</maven.compiler.parameters>
<compiler-plugin.version>3.8.1</compiler-plugin.version>
<auto-value.version>1.8.2</auto-value.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-picocli</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value</artifactId>
<version>${auto-value.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<version>${auto-value.version}</version>
</dependency>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.version}</version>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value</artifactId>
<version>${auto-value.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
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();
}
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment