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.Template; 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 LdapMapping mapping; // 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 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; } return true; } @Override protected boolean execute() { TarArchiveOutputStream outArchive = null; File keyFile = new File( "/opt/ldadp/configs/" + this.moduleid + ".key.pem" ); File certFile = new File( "/opt/ldadp/configs/" + this.moduleid + ".crt.pem" ); File caFile = new File( "/opt/ldadp/configs/" + this.moduleid + ".ca-bundle.pem" ); String uri = "ldaps://" + this.proxyip + ":" + this.proxyport + "/"; String cacertPath = "/etc/ldap-proxy.pem"; String caPath = ""; final String subject = "/C=DE/ST=Nowhere/L=Springfield/O=Dis/CN=" + this.proxyip; 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" + "%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, this.mapping.toString(), this.proxyport, certFile, keyFile ); // Generic ldap config final Template ldapConf = new Template( "./data/ad/ldap.conf.template" ); ldapConf.replace( "%URI%", uri ); ldapConf.replace( "%SEARCHBASE%", this.searchbase ); ldapConf.replace( "%CACERT%", cacertPath ); // sssd config final Template sssdConf = new Template( "./data/ad/sssd.conf.template" ); sssdConf.replace( "%URI%", uri ); sssdConf.replace( "%SEARCHBASE%", this.searchbase ); sssdConf.replace( "%CACERT%", cacertPath ); // Sharemode config String shareConf = String.format( "SHARE_REMAP_MODE=%d\n" + "SHARE_CREATE_MISSING_REMAP=%d\n" + "SHARE_HOME_DRIVE='%s'\n" + "SHARE_DOCUMENTS=%d\n" + "SHARE_DOWNLOADS=%d\n" + "SHARE_DESKTOP=%d\n" + "SHARE_MEDIA=%d\n" + "SHARE_OTHER=%d\n" + "SHARE_DOMAIN='%s'\n" + "SHARE_CREDENTIAL_PASSTHROUGH=%d\n", this.shareRemapMode, this.shareRemapCreate, escapeBashString( this.shareHomeDrive ), this.shareDocuments, this.shareDownloads, this.shareDesktop, this.shareMedia, this.shareOther, escapeBashString( this.shareDomain ), this.credentialPassthrough ); if ( this.shares != null && !this.shares.isEmpty() ) { int i = 0; for ( Share s : this.shares ) { shareConf += String.format( "SHARE_LINE_%d='%s\t%s\t%s\t%s\t%s'\n", ++i, escapeBashString( s.share ), escapeBashString( s.letter ), escapeBashString( s.shortcut ), escapeBashString( s.user ), escapeBashString( 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, cacertPath, certFile, 0644 ) ) { status.error = "Could not add ldap-proxy.pem to module"; return false; } // nsswitch.conf with ldap enabled if ( !Archive.tarAddFile( outArchive, "/etc/nsswitch.conf", new File( "./data/ad/nsswitch.conf" ), 0644 ) ) { status.error = "Could not add nsswitch.conf to module"; return false; } // All the pam.d common-XXXX files for ( String file : new String[] { "common-auth", "common-account", "common-session", "common-session-noninteractive", "common-password" } ) { if ( !Archive.tarAddFile( outArchive, "/etc/pam.d/" + file, new File( "./data/ad/" + file ), 0644 ) ) { status.error = "Could not add " + file + " to module"; return false; } } // Home if present if ( !Archive.tarAddFile( outArchive, "/opt/openslx/scripts/pam_script_mount_persistent", new File( "./data/ad/mountscript" ), 0644 ) ) { status.error = "Could not add mount script to module"; return false; } boolean ret = Archive.tarCreateFileFromString( outArchive, "/etc/ldap.conf", ldapConf.toString(), 0644 ) && Archive.tarCreateFileFromString( outArchive, "/etc/sssd/sssd.conf", sssdConf.toString(), 0600 ) && Archive.tarCreateFileFromString( outArchive, "/opt/openslx/inc/shares", shareConf, 0644 ) && Archive.tarCreateSymlink( outArchive, "/etc/ldap.conf", "/etc/ldap/ldap.conf" ) && Archive.tarCreateSymlink( outArchive, "/etc/ldap.conf", "/etc/openldap/ldap.conf" ) && Archive.tarCreateSymlink( outArchive, "../sssd.service", "/etc/systemd/system/basic.target.wants/sssd.service" ); if ( !ret ) { status.error = "Could not add ldap configs to module"; } return ret; } catch ( IOException e ) { status.error = e.toString(); return false; } finally { Util.multiClose( outArchive ); } } 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; } }