filiperochalopes / e-SUS-PEC

Instalação do e-SUS em ambiente docker linux com arquivo jar
8 stars 8 forks source link

Instabilidade e erros difíceis de debugar em conexão com banco de dados externo #13

Open filiperochalopes opened 1 month ago

filiperochalopes commented 1 month ago

Ambientes testados:

Versão da aplicação testada 5.2.38 (maior parte dos testes) e 5.2.28 (poucos testes, porém mesmos erros). Utilizar branch feature/noharm se quiser facilitar a replicação dos testes. Edite .env.external-db, pode adicionar algo na url como argumentos no arquivo build.sh e depois rodar sh build.sh -e -c.

Sistema Operacional

Máquina Ubuntu 24.04 LTS (GNU/Linux 6.8.0-1012-aws x86_64) Container Docker Ubuntu 22.04 com systemd

Banco de dados

Foram utilizados para teste:

Impressão:

Aparentemente o driver JDBC está mal configurado e com retornos difíceis de debugar, aparentemente só podemos usar um usuário chamado postgres e temos limitações quanto a url e uso de SSL na conexão.

Foram realizados testes com diversos clientes psql, jdbc postgres do dbeaver e de script java criado para teste (42.7.3) que retornaram conexões positivas, porém não pelo jdbc utilizado na aplicação.

Também notamos que o driver no pacote parece só fazer requisições "no encryption" fazendo com que tenhamos que desativar o SSL do banco, o que é uma falha de segurança.

* N?vel conectar ao Banco de Dados: FATAL: no pg_hba.conflema(s):
N?o foi poss?vel conectar ao Banco de Dados: FATAL: no pg_hba.conf entry for host "172.31.3.217", user "esus_user", database "esus", no encryption

Passos para o erro:

Rodar comando de instalação do pacote com diferentes url

java -verbose:class -jar eSUS-AB-PEC-5.2.38-Linux64.jar -console -url="jdbc:postgresql://host-de-teste:5432/esus" -username user_example -password pass_example -continue > ./application.log 2>&1

Foi feito teste com -verbose, sem verbose, com -continue e sem, também utilizado parâmetros de configuração SSL e de Debug na url como:

?ssl=true&sslmode=disable
?ssl=true&sslmode=allow
?ssl=true&sslmode=allow&sslfactory=org.postgresql.ssl.NonValidatingFactory

E combinações entre essas variávels

Por questões de segurança credenciais e host não são disponibilizadas na issue

Evidências:

O printscreen enviado pelo suporte usa psql como host, que não é um ip nem um domínio, a utilização de localhost, 127.0.0.1 e domínios AWS retornam alguns erros dependendo da url utilizada, porém nenhum com sucesso:

image image

No foi poss?vel conectar ao Banco de Dados: The connection attempt failed.

image image

*** N?o ? poss?vel prosseguir devido ao(s) seguinte(s) problema(s):
N?o foi poss?vel conectar ao Banco de Dados: The autentication type 10 is not supported. Check that you have configured the pg_hba.conf file to include the client's IP address or subnet, and that it is using an autentication scheme supported by the driver.

Veja esse exemplo, no mesmo ambiente diz que temos uma falha de senha, mas não temos:

sudo java -verbose -jar eSUS-AB-PEC-5.2.38-Linux64.jar -console -url="jdbc:postgres
ql://localhost:5432/esus" -username postgres -password pass

Output:

O Validador da Aplicação foi preparado. - 0%
[Loaded org.apache.commons.io.FileUtils from file:/home/ubuntu/pec/eSUS-AB-PEC-5.2.38-Linux64.jar]
[Loaded java.util.zip.CheckedInputStream from /usr/lib/jvm/zulu-8-amd64/jre/lib/rt.jar]
[Loaded org.apache.commons.io.output.NullOutputStream from file:/home/ubuntu/pec/eSUS-AB-PEC-5.2.38-Linux64.jar]
[Loaded java.nio.channels.Channel from /usr/lib/jvm/zulu-8-amd64/jre/lib/rt.jar]
[Loaded java.nio.channels.ReadableByteChannel from /usr/lib/jvm/zulu-8-amd64/jre/lib/rt.jar]
[Loaded org.apache.commons.io.FileExistsException from file:/home/ubuntu/pec/eSUS-AB-PEC-5.2.38-Linux64.jar]
*** Não é possível prosseguir devido ao(s) seguinte(s) problema(s):
Não foi possível conectar ao Banco de Dados: FATAL: password authentication failed for user "postgres"
[Loaded java.lang.Shutdown from /usr/lib/jvm/zulu-8-amd64/jre/lib/rt.jar]
[Loaded java.lang.Shutdown$Lock from /usr/lib/jvm/zulu-8-amd64/jre/lib/rt.jar]

Porém no mesmo ambiente com psql

image

pg_hba.conf do teste acima image

postgresql.conf image

Olha que bizarro esse outro erro:

$ sudo java -jar eSUS-AB-PEC-5.2.38-Linux64.jar -console -url="jdbc:postgresql://127.0.0.1:5434/esus" -username 
esus_user -password esus_pass

Output:

[...]
*** Não é possível prosseguir devido ao(s) seguinte(s) problema(s):
Não foi possível conectar ao Banco de Dados: FATAL: password authentication failed for user "postgres"

Ele queixa de falha de autenticação do usuário postgres, porém estou usando um usuário chamado esus_user

Algo interessante é que o ambiente executado por sh build.sh -f eSUS-AB-PEC-5.2.38-Linux64.jar funciona, pois usa psql como host, que é um serviço no docker-compose. O que achei interessante é que, no exemplo funcional que o suporte me enviou ele também usa psql como host. Existe alguma limitação no nome do host? algum filtro/parse? Segue imagem do suporte:

image

Tem final bem sucedido com Java 8, Ubuntu 22.04.4 LTS, porém não mostra se o psql nesse caso foi um apelido criado em /etc/hosts ou um serviço docker como meu exemplo que também funciona.

Debugando

Sabemos que o driver está em org.postgresql.Driver.class, porém existem duas referências dessa no archive *.jar. Uma desde a pasta raiz e outra que segue br.org.postgresql.Driver.class, aqui temos alguns arquivos relevantes, assim como o script de conexão utilzando JDBC PostgreSQL que funcionou:

br.org.postgresql.ds.common.BaseDataSource.java

// ...
private String getUrl() {
        StringBuffer sb = new StringBuffer(100);
        sb.append("jdbc:postgresql://");
        sb.append(this.serverName);
        if (this.portNumber != 0) {
            sb.append(":").append(this.portNumber);
        }
        sb.append("/").append(this.databaseName);
        sb.append("?loginTimeout=").append(this.loginTimeout);
        sb.append("&socketTimeout=").append(this.socketTimeout);
        sb.append("&prepareThreshold=").append(this.prepareThreshold);
        sb.append("&unknownLength=").append(this.unknownLength);
        sb.append("&loglevel=").append(this.logLevel);
        if (this.protocolVersion != 0) {
            sb.append("&protocolVersion=").append(this.protocolVersion);
        }
        if (this.ssl) {
            sb.append("&ssl=true");
            if (this.sslfactory != null) {
                sb.append("&sslfactory=").append(this.sslfactory);
            }
        }
        sb.append("&tcpkeepalive=").append(this.tcpKeepAlive);
        if (this.compatible != null) {
            sb.append("&compatible=" + this.compatible);
        }
        return sb.toString();
    }

Meu script utilizando JDBC postgres funcionou normal:

image

Perguntas:

  1. Qual versão do JDBC Postgresql utilizado? (Não encontro nos metafiles)
  2. Existe alguma limitação quanto a versão dos bancos de dados assim como existe do java?
  3. O usuário tem que ser obrigatoriamente postgres?
  4. Como configurar um host diferente de psql? (único que funcionou até agora e não entendi como vc utilizou ele)
  5. Tentei usar o -restore do cli jar porém não funcionou, está funcional?
  6. Existe uma forma melhor para debugar a execução (principalmente a conexão de banco de dados) que não acompanhar o pec.log ou usar o -verbose no comando java?
  7. Como estão as configurações do jdbc? devemos usar algum modo de ssl? o ssl do cliente está ativo? Qual forma de autenticação do banco devemos usar? password? md5? Alguma restrição? scram-sha-256?
  8. Existe alguma documentação para Linux?

Tickets abertos:

https://esusaps.freshdesk.com/support/tickets/58738 - Abril de 2024 https://esusaps.freshdesk.com/support/tickets/64098 - 3 de Agosto de 2024.

filiperochalopes commented 1 month ago

Informações adicionais relevantes, trace do erro:

Arquivo que manda as Exceptions

br.gov.saude.esus.installers.core.EsusValidator.java

// ...
public boolean validateEnviroment(EsusConfig config) {
        this.errors = new ArrayList<String>();
        if (!Environment.isElevated()) {
            this.errors.add("Esta ferramenta necessita executar com privil\u00e9gios de administrador.");
            return false;
        }
        if (EsusConfig.Action.UNINSTALL.equals((Object)config.getAction())) {
            return true;
        }
        if (!Environment.isUnix() && !Environment.isWindows()) {
            this.errors.add("Esta ferramenta suporta apenas sistemas operacionais das fam\u00edlias Windows e Linux.");
        } else if (EsusConstants.IS_WINDOWS_CONTAINER && !Environment.isWindows()) {
            this.errors.add("Esta ferramenta suporta apenas sistemas operacionais da fam\u00edlia Windows.");
        } else if (!EsusConstants.IS_WINDOWS_CONTAINER && !Environment.isUnix()) {
            this.errors.add("Esta ferramenta suporta apenas sistemas operacionais da fam\u00edlia Linux.");
        }
        if (Environment.isUnix() && !Environment.hasSystemd() && !Boolean.TRUE.equals(config.getCustomDatabase())) {
            this.errors.add("Essa ferramenta suporta apenas distribui\u00e7\u00f5es Linux com sistema de inicializa\u00e7\u00e3o System D.");
        }
        if (!Environment.isX64()) {
            this.errors.add("Esta ferramenta suporta apenas sistemas operacionais de arquitetura x64.");
        }
        return this.errors.isEmpty();
    }

    private boolean validateDatabaseMinimumVersion(DatabaseConfig databaseConfig) {
        this.errors = new ArrayList<String>();
        DatabaseTester database = new DatabaseTester(databaseConfig);
        ResultSet result = database.query("SELECT DS_TEXTO FROM TB_CONFIG_SISTEMA WHERE CO_CONFIG_SISTEMA = 'VERSAOBANCODADOS'");
        try {
            if (result != null && result.next()) {
                Version version = new Version(result.getString("DS_TEXTO"));
                if (!version.isValid() || version.isValid() && version.compareTo(EsusConstants.PEC_MINIMUM_VERSION) < 0) {
                    this.errors.add(String.format("O Banco de dados deve estar pelo menos na vers\u00e3o %s do e-SUS APS PEC. Atualize ou execute o seu PEC e tente novamente.", EsusConstants.PEC_MINIMUM_VERSION));
                }
            } else {
                this.errors.add("N\u00e3o foi poss\u00edvel obter a vers\u00e3o do Banco de Dados.");
            }
        }
        catch (SQLException e) {
            this.errors.add("Falha ao executar a query no Banco de Dados: " + e.getLocalizedMessage());
        }
        database.dispose();
        return this.errors.isEmpty();
    }

    private boolean validateDatabaseConnection(DatabaseConfig databaseConfig) {
        this.errors = new ArrayList<String>();
        DatabaseTester database = new DatabaseTester(databaseConfig);
        if (!database.testConnection()) {
            this.errors.add("N\u00e3o foi poss\u00edvel conectar ao Banco de Dados: " + database.getLastErrorMessage());
        } else if (this.isConnectionInsecure(databaseConfig)) {
            this.errors.add("\u00c9 necess\u00e1rio configurar o seu Banco de Dados para exigir credenciais para a conex\u00e3o.");
        }
        database.dispose();
        return this.errors.isEmpty();
    }

    private boolean isConnectionInsecure(DatabaseConfig databaseConfig) {
        DatabaseConfig databaseConfigInsecure = new DatabaseConfig();
        databaseConfigInsecure.setConnectionUrl(databaseConfig.getConnectionUrl());
        databaseConfigInsecure.setUsername(databaseConfig.getUsername());
        databaseConfigInsecure.setPassword(null);
        DatabaseTester databaseTester = new DatabaseTester(databaseConfigInsecure);
        return databaseTester.testConnection();
    }

    private boolean validatePostgresDefault(EsusConfig config) {
        boolean hasCustomDatabase;
        PostgresConfig pc = new PostgresConfig();
        PecRegistry pecRegistry = PecRegistry.load();
        boolean bl = hasCustomDatabase = pecRegistry != null && pecRegistry.isCustomDatabase();
        if (EsusConfig.Action.UPDATE.equals((Object)config.getAction()) && !hasCustomDatabase) {
            log.info("o PEC utiliza o PostgreSQL padr\u00e3o.");
            if (!pc.autodetect()) {
                this.errors.add("N\u00e3o foi encontrado o PostgreSQL padr\u00e3o.");
                return false;
            }
            PostgresCommander commander = new PostgresCommander(pc);
            if (!NetworkUtils.isTcpPortInUse(pc.getPort()) && !commander.start()) {
                this.errors.add("Falha ao iniciar o PostgreSQL padr\u00e3o.");
                return false;
            }
            log.info("O PostgreSQL padr\u00e3o est\u00e1 em execu\u00e7\u00e3o.");
        }
        return true;
    }

    public boolean validateDatabase(DatabaseConfig databaseConfig) {
        if (!this.validateDatabaseConnection(databaseConfig)) {
            return false;
        }
        this.errors = new ArrayList<String>();
        DatabaseTester database = new DatabaseTester(databaseConfig);
        if (Boolean.TRUE.equals(database.tableExists("TB_CONFIG_SISTEMA")) && this.validateDatabaseMinimumVersion(databaseConfig)) {
            this.validatePendingProcessing(databaseConfig);
        }
        database.dispose();
        return this.errors.isEmpty();
    }
// ...

Arquivo de configuração do banco de dados br.gov.saude.esus.installer.core.database.DatabaseConfig.java

public class DatabaseConfig {
    private String connectionUrl;
    private String username;
    private String password;
    private String readonlyUsername;
    private String readonlyPassword;
    private boolean training = false;

    public static DatabaseConfig fromRegistry(PecRegistry registry) {
        if (registry == null) {
            return null;
        }
        DatabaseConfig databaseConfig = DatabaseConfig.createDefault(!registry.isProduction());
        if (StringUtils.isNotEmpty(registry.getDatabasePassword())) {
            databaseConfig.setPassword(registry.getDatabasePassword());
        }
        if (StringUtils.isNotEmpty(registry.getDatabaseReadonlyPassword())) {
            databaseConfig.setReadonlyPassword(registry.getDatabaseReadonlyPassword());
        }
        if (registry.isCustomDatabase()) {
            databaseConfig.setConnectionUrl(registry.getDatabaseUrl());
            databaseConfig.setUsername(registry.getDatabaseUsername());
        }
        return databaseConfig;
    }

    public static DatabaseConfig createDefault(boolean training) {
        DatabaseConfig databaseConfig = new DatabaseConfig();
        databaseConfig.setConnectionUrl("jdbc:postgresql://localhost:5433/esus");
        databaseConfig.setUsername("postgres");
        databaseConfig.setPassword("esus");
        databaseConfig.setReadonlyUsername("esus_leitura");
        databaseConfig.setReadonlyPassword("esus");
        databaseConfig.setTraining(training);
        return databaseConfig;
    }
// ...

br.gov.saude.esus.installers.core.database.DatabaseTester.java

// ...
    public boolean testConnection() {
        this.lastErrorMessage = "";
        if (this.config.getType() == null) {
            this.lastErrorMessage = "A URL de conex\u00e3o deve come\u00e7ar com 'jdbc:postgresql' ou 'jdbc:oracle'";
            return false;
        }
        Connection connection = this.getConnection();
        if (connection != null) {
            this.dispose();
        }
        return StringUtils.isEmpty(this.lastErrorMessage);
    }

    private Connection getConnection() {
        try {
            Class.forName(this.getDriverName());
            return DriverManager.getConnection(this.config.getConnectionUrl(), this.config.getUsername(), this.config.getPassword());
        }
        catch (Exception e) {
            this.lastErrorMessage = e.getLocalizedMessage();
            return null;
        }
    }
// ...
    public static boolean isUnix() {
        return SystemUtils.IS_OS_UNIX;
    }

    public static boolean isWindows() {
        return SystemUtils.IS_OS_WINDOWS;
    }

    private static boolean isWindowsX64() {
        String path = System.getenv("SystemRoot");
        if (StringUtils.isEmpty(path)) {
            return false;
        }
        return IO.dirExists(IO.combinePaths(path, "SysWOW64"));
    }

    private static boolean isUnixX64() {
        String output = Shell.getInstance().run("file -L /sbin/init").getOutput();
        return StringUtils.isNotEmpty(output) && output.trim().toLowerCase().contains("64-bit");
    }

    public static boolean isX64() {
        boolean is64bits;
        boolean bl = is64bits = !SystemUtils.OS_ARCH.endsWith("64");
        if (!is64bits) {
            if (Environment.isWindows()) {
                is64bits = Environment.isWindowsX64();
            } else if (Environment.isUnix()) {
                is64bits = Environment.isUnixX64();
            }
        }
        return is64bits;
    }

    public static Version getJvmVersion() {
        return new Version(System.getProperty("java.version"));
    }

    public static boolean isSupported() {
        return Environment.isX64() && (Environment.isUnix() || Environment.isWindows());
    }

    public static String getVariable(String key) {
        return System.getenv(key);
    }

    public static Map<String, String> getVariables() {
        return System.getenv();
    }

    public static boolean hasSystemd() {
        String output = Shell.getInstance().run("ps --no-headers -o comm 1").getOutput();
        return StringUtils.isNotEmpty(output) && output.trim().toLowerCase().equals("systemd");
    }