package org.openslx.taskmanager.tasks; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.apache.log4j.Logger; import org.openslx.satserver.util.Archive; import org.openslx.satserver.util.Constants; import org.openslx.satserver.util.Exec; import org.openslx.satserver.util.Exec.ExecCallback; import org.openslx.satserver.util.LdapMapping; import org.openslx.satserver.util.Util; import org.openslx.taskmanager.api.AbstractTask; import com.google.gson.annotations.Expose; public class CreateLdapConfig extends AbstractTask { private static final Logger LOGGER = Logger.getLogger( CreateLdapConfig.class ); public static final String DEFAULT_CA_BUNDLE = "/etc/ssl/certs/ca-certificates.crt"; protected static final String[] ALLOWED_DIRS = { "/tmp/", "/opt/openslx/configs/" }; @Expose private int moduleid = 0; @Expose private String filename = null; @Expose private String server = null; @Expose private String searchbase = null; @Expose private String binddn = null; @Expose private String bindpw = null; @Expose private String proxyip = null; @Expose private int proxyport = 0; @Expose private int adport = 0; @Expose private String home = null; @Expose private String homeattr = null; @Expose private String fingerprint; @Expose private String certificate; @Expose private boolean plainldap = false; @Expose private String fixnumeric = null; @Expose private boolean genuid = false; @Expose private LdapMapping mapping; @Expose private String ldapAttrMountOpts; // Share mode stuff @Expose private int shareRemapMode; @Expose private int shareRemapCreate; @Expose private String shareHomeDrive; @Expose private int shareDocuments; @Expose private int shareDownloads; @Expose private int shareDesktop; @Expose private int shareMedia; @Expose private int shareOther; @Expose private List shares; @Expose private String shareDomain; @Expose private String shareHomeMountOpts; @Expose private int credentialPassthrough; private Output status = new Output(); @Override protected boolean initTask() { // TODO: Check path is allowed this.setStatusObject( this.status ); if ( filename == null || server == null || searchbase == null || proxyip == null || proxyport == 0 || moduleid == 0 ) { status.error = "Missing argument to task"; return false; } filename = FilenameUtils.normalize( filename ); if ( !Util.startsWith( filename, ALLOWED_DIRS ) ) { status.error = "Illegal target directory " + filename; return false; } for ( Field field : CreateLdapConfig.class.getDeclaredFields() ) { if ( field.isAnnotationPresent( Expose.class ) && field.getType().equals( String.class ) ) { field.setAccessible( true ); Object ret; try { ret = field.get( this ); } catch ( IllegalArgumentException | IllegalAccessException e1 ) { ret = null; LOGGER.warn( "Cannot get field " + field.getName() ); } if ( ret == null ) { try { field.set( this, "" ); } catch ( IllegalArgumentException | IllegalAccessException e ) { LOGGER.warn( "Cannot set field " + field.getName() ); } } } } if ( mapping == null ) { mapping = new LdapMapping(); } if ( Util.isEmpty( mapping.homemount ) && !Util.isEmpty( this.homeattr ) ) { mapping.homemount = this.homeattr; } if ( Util.isEmpty( mapping.homemount ) && !"homedirectory".equalsIgnoreCase( mapping.localhome ) ) { mapping.homemount = "homeDirectory"; } if (!this.plainldap) { mapping.uidnumber = null; mapping.localhome = null; } return true; } @Override protected boolean execute() { TarArchiveOutputStream outArchive = null; final File keyFile = new File( "/opt/ldadp/configs/" + this.moduleid + ".key.pem" ); final File certFile = new File( "/opt/ldadp/configs/" + this.moduleid + ".crt.pem" ); final File caFile = new File( "/opt/ldadp/configs/" + this.moduleid + ".ca-bundle.pem" ); final String uri = "ldaps://" + this.proxyip + ":" + this.proxyport + "/"; final String clientCacertPath = "/etc/ldap/proxy-" + this.moduleid + ".pem"; final String subject = "/C=DE/ST=Nowhere/L=Springfield/O=Dis/CN=" + this.proxyip; String caPath = ""; try { // If cert already exists, check if the subject (most importantly the CN) matches the desired one if ( certFile.exists() ) { final AtomicBoolean subjectStillGood = new AtomicBoolean( false ); Exec.sync( 4, new ExecCallback() { @Override public void processStdOut( String line ) { if ( line.trim().endsWith( subject ) ) { subjectStillGood.set( true ); } } @Override public void processStdErr( String line ) { } }, "openssl", "x509", "-noout", "-in", certFile.getAbsolutePath(), "-subject" ); if ( !subjectStillGood.get() ) { certFile.delete(); keyFile.delete(); } } // Generate keys if not existent if ( !keyFile.exists() || !certFile.exists() ) { int ret = Exec.sync( 20, "openssl", "req", "-x509", "-new", "-newkey", "rsa:4096", "-keyout", keyFile.getAbsolutePath(), "-out", certFile.getAbsolutePath(), "-days", "5000", "-nodes", "-subj", subject ); if ( ret == -1 ) { status.error = "openssl process didn't finish in time."; } else if ( ret == -2 ) { status.error = "Internal error generating certificate."; } else if ( ret != 0 ) { status.error = "openssl exited with code " + ret; } if ( ret != 0 ) return false; } // Handle ca-bundle; write to file if custom one is passed if ( this.fingerprint.length() > 20 && this.server.matches( "^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$" ) ) { // IP address seems to be given - resort to fingerprint if the certificate doesn't cover // it. } else if ( this.certificate.equals( "default" ) ) { caPath = DEFAULT_CA_BUNDLE; this.fingerprint = ""; } else if ( !this.certificate.isEmpty() && !this.certificate.equals( "false" ) ) { // Write out try { FileUtils.writeStringToFile( caFile, this.certificate, StandardCharsets.UTF_8 ); } catch ( Exception e ) { status.error = "Could not write trusted certificate(s) to file " + caFile.getAbsolutePath(); return false; } caPath = caFile.getAbsolutePath(); this.fingerprint = ""; } // ldadp config String ldadpConf = String.format( "[%s]\n" + "binddn=%s\n" + "bindpw=%s\n" + "base=%s\n" + "home=%s\n" + "port=%s\n" + "fingerprint=%s\n" + "cabundle=%s\n" + "plainldap=%s\n" + "fixnumeric=%s\n" + "genuidnumber=%s\n" + "uidmapstore=/opt/ldadp/pid/map-%d.bin\n" + "%s\n" + "[local]\n" + "port=%s\n" + "cert=%s\n" + "privkey=%s\n" + "\n", this.server, this.binddn, this.bindpw, this.searchbase, this.home, this.adport, this.fingerprint, caPath, Boolean.toString( this.plainldap ), this.fixnumeric == null ? "" : this.fixnumeric, Boolean.toString( this.genuid ), this.moduleid, this.mapping.toString(), this.proxyport, certFile, keyFile ); // Generic ldap config StringBuilder ldapConf = new StringBuilder(); addConfLine( ldapConf, "LDAP_URI", uri ); addConfLine( ldapConf, "LDAP_BASE", this.searchbase ); addConfLine( ldapConf, "LDAP_CACERT", clientCacertPath ); addConfLine( ldapConf, "LDAP_ATTR_MOUNT_OPTS", this.ldapAttrMountOpts ); // Sharemode config addConfLine( ldapConf, "SHARE_HOME_MOUNT_OPTS", this.shareHomeMountOpts ); addConfLine( ldapConf, "SHARE_REMAP_MODE", this.shareRemapMode ); addConfLine( ldapConf, "SHARE_CREATE_MISSING_REMAP", this.shareRemapCreate ); addConfLine( ldapConf, "SHARE_HOME_DRIVE", this.shareHomeDrive ); addConfLine( ldapConf, "SHARE_DOCUMENTS", this.shareDocuments ); addConfLine( ldapConf, "SHARE_DOWNLOADS", this.shareDownloads ); addConfLine( ldapConf, "SHARE_DESKTOP", this.shareDesktop ); addConfLine( ldapConf, "SHARE_MEDIA", this.shareMedia ); addConfLine( ldapConf, "SHARE_OTHER", this.shareOther ); addConfLine( ldapConf, "SHARE_DOMAIN", this.shareDomain ); addConfLine( ldapConf, "SHARE_CREDENTIAL_PASSTHROUGH", this.credentialPassthrough ); if ( this.shares != null && !this.shares.isEmpty() ) { int i = 0; for ( Share s : this.shares ) { ++i; addConfLine( ldapConf, "SHARE_LINE_" + i, String.format( "%s\t%s\t%s\t%s\t%s", s.share, s.letter, s.shortcut, s.user, s.pass ) ); } } // Build tar/config String ldadpConfigPath = "/opt/ldadp/configs/" + this.moduleid + ".cfg"; try { Files.deleteIfExists( Paths.get( this.filename ) ); } catch ( IOException e1 ) { } try { FileUtils.writeStringToFile( new File( ldadpConfigPath ), ldadpConf, StandardCharsets.UTF_8 ); if ( 0 != Exec.sync( 10, "/usr/bin/sudo", "-n", "-u", "root", Constants.BASEDIR + "/scripts/ldadp-setperms", Integer.toString( this.moduleid ) ) ) status.error = "Warning: Could not chown/chmod ldadp config!"; } catch ( IOException e ) { status.error = e.toString(); return false; } try { outArchive = Archive.createTarArchive( this.filename ); } catch ( IOException e ) { status.error = "Could not create archive at " + this.filename; return false; } // The cert we just created if ( !Archive.tarAddFile( outArchive, clientCacertPath, certFile, 0644 ) ) { status.error = "Could not add ldap-proxy.pem to module"; return false; } boolean ret = Archive.tarCreateFileFromString( outArchive, "/opt/openslx/pam/slx-ldap.d/conf-" + this.moduleid, ldapConf.toString(), 0644 ); if ( !ret ) { status.error = "Could not add ldap configs to module"; } return ret; } finally { Util.multiClose( outArchive ); } } private void addConfLine( StringBuilder sb, String varName, int value ) { addConfLine( sb, varName, Integer.toString( value ) ); } private void addConfLine( StringBuilder sb, String varName, String value ) { sb.append( varName ); sb.append( "='" ); sb.append( escapeBashString( value ) ); sb.append( "'\n" ); } private String escapeBashString( String str ) { if ( str == null ) return ""; if ( str.indexOf( '\'' ) != -1 ) { str = str.replace( "'", "'\"'\"'" ); } if (str.indexOf( '\t' ) != -1) { str = str.replace( "\t", " " ); } return str; } /** * Output - contains additional status data of this task */ @SuppressWarnings( "unused" ) private static class Output { protected String error = null; } private static class Share { protected String share; protected String letter; protected String shortcut; protected String user; protected String pass; } }