diff --git a/CHANGELOG.md b/CHANGELOG.md index b53b291a82e..4bbf16a7320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -164,6 +164,8 @@ - [Added `line_endings` and `comment_character` options to `File_Format.Delimited`.][3581] - [Fixed the case of various type names and library paths][3590] +- [Added support for parsing `.pgpass` file and `PG*` environment variables for + the Postgres connection][3593] [debug-shortcuts]: https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug @@ -260,6 +262,7 @@ [3581]: https://github.com/enso-org/enso/pull/3581 [3588]: https://github.com/enso-org/enso/pull/3588 [3590]: https://github.com/enso-org/enso/pull/3590 +[3593]: https://github.com/enso-org/enso/pull/3593 #### Enso Compiler diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/System.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/System.enso index ee37ed2d738..3bc9e0b4fc2 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/System.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/System.enso @@ -50,6 +50,10 @@ nano_time = @Builtin_Method "System.nano_time" os : Text os = @Builtin_Method "System.os" +## Check if the operating system is UNIX. +is_unix : Boolean +is_unix = @Builtin_Method "System.is_unix" + ## PRIVATE Returns the default line separator for the platform that the program is currently running on. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/System/Environment.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/System/Environment.enso index 77a41e23c27..8dec0dc524a 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/System/Environment.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/System/Environment.enso @@ -1,6 +1,6 @@ from Standard.Base import all -polyglot java import java.lang.System +polyglot java import org.enso.base.Environment_Utils ## ALIAS Read Environment UNSTABLE @@ -18,4 +18,24 @@ polyglot java import java.lang.System example_get = Environment.get "PATH" get : Text -> Text | Nothing -get key = System.getenv key +get key = Environment_Utils.get_environment_variable key + +## UNSTABLE + + Returns a value of a specified environment variable or the provided default + value if such variable is not defined. + + Arguments: + - key: The name of the environment variable to look up. + - default: The default fallback value. + + > Example + Look up the value of the `FOOBAR` environment variable. + + import Standard.Base.System.Environment + + example_get_or_else = Environment.get_or_else "FOOBAR" "my default" +get_or_else : Text -> Text -> Text +get_or_else key ~default = case get key of + Nothing -> default + value -> value diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso index c9c14b87cdf..ba2ec03498d 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso @@ -2,6 +2,7 @@ from Standard.Base import all import Standard.Base.System.File.Option import Standard.Base.System.File.Existing_File_Behavior +import Standard.Base.System.File.File_Permissions import Standard.Base.Error.Problem_Behavior import Standard.Base.Data.Text.Matching_Mode import Standard.Base.Data.Text.Text_Sub_Range @@ -368,9 +369,30 @@ type File Builtin method that gets this file's last modified time. Recommended to use `File.last_modified_time` instead which handles potential exceptions. - last_modified_time_builtin : File -> ZonedDateTime + last_modified_time_builtin : ZonedDateTime last_modified_time_builtin = @Builtin_Method "File.last_modified_time_builtin" + ## Gets the POSIX permissions associated with the file. + + > Example + Check if the file is readable by the user's group. + + import Standard.Examples + + example_permissions = Examples.csv.posix_permissions.group_read + posix_permissions : File_Permissions + posix_permissions = + File_Permissions.from_java_set self.posix_permissions_builtin + + ## PRIVATE + + Builtin method that gets this file's POSIX permissions as a Java Set. + Recommended to use `File.posix_permissions` instead which handles + potential exceptions and converts an Enso representation of the + permissions. + posix_permissions_builtin : Set + posix_permissions_builtin = @Builtin_Method "File.posix_permissions_builtin" + ## Checks whether the file exists and is a directory. > Example diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File/File_Permissions.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File/File_Permissions.enso new file mode 100644 index 00000000000..8965fb4d452 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File/File_Permissions.enso @@ -0,0 +1,104 @@ +from Standard.Base import all + +polyglot java import java.nio.file.attribute.PosixFilePermission + +type Permission + ## Permission for read access for a given entity. + type Read + + ## Permission for write access for a given entity. + type Write + + ## Permission for execute access for a given entity. + type Execute + +type File_Permissions + ## Access permissions for a file. + type File_Permissions (owner : Vector Permission) (group : Vector Permission) (others : Vector Permission) + + ## Converts the Enso atom to its Java enum counterpart. + to_java : Vector PosixFilePermission + to_java = + result = Vector.new_builder + if self.owner.contains Read then + result.append PosixFilePermission.OWNER_READ + if self.owner.contains Write then + result.append PosixFilePermission.OWNER_WRITE + if self.owner.contains Execute then + result.append PosixFilePermission.OWNER_EXECUTE + if self.group.contains Read then + result.append PosixFilePermission.GROUP_READ + if self.group.contains Write then + result.append PosixFilePermission.GROUP_WRITE + if self.group.contains Execute then + result.append PosixFilePermission.GROUP_EXECUTE + if self.others.contains Read then + result.append PosixFilePermission.OTHERS_READ + if self.others.contains Write then + result.append PosixFilePermission.OTHERS_WRITE + if self.others.contains Execute then + result.append PosixFilePermission.OTHERS_EXECUTE + result.to_vector + + ## Checks if the given file can be read by the owner. + owner_read : Boolean + owner_read = self.owner.contains Read + + ## Checks if the given file can be written by the owner. + owner_write : Boolean + owner_write = self.owner.contains Write + + ## Checks if the given file can be executed by the owner. + owner_execute : Boolean + owner_execute = self.owner.contains Execute + + ## Checks if the given file can be read by the group. + group_read : Boolean + group_read = self.group.contains Read + + ## Checks if the given file can be written by the group. + group_write : Boolean + group_write = self.group.contains Write + + ## Checks if the given file can be executed by the group. + group_execute : Boolean + group_execute = self.group.contains Execute + + ## Checks if the given file can be read by others. + others_read : Boolean + others_read = self.others.contains Read + + ## Checks if the given file can be written by others. + others_write : Boolean + others_write = self.others.contains Write + + ## Checks if the given file can be executed by others. + others_execute : Boolean + others_execute = self.others.contains Execute + +## Converts a Java `Set` of Java `PosixFilePermission` to `File_Permissions`. +from_java_set java_set = + owner = Vector.new_builder + group = Vector.new_builder + others = Vector.new_builder + + if java_set.contains PosixFilePermission.OWNER_READ then + owner.append Read + if java_set.contains PosixFilePermission.OWNER_WRITE then + owner.append Write + if java_set.contains PosixFilePermission.OWNER_EXECUTE then + owner.append Execute + if java_set.contains PosixFilePermission.GROUP_READ then + group.append Read + if java_set.contains PosixFilePermission.GROUP_WRITE then + group.append Write + if java_set.contains PosixFilePermission.GROUP_EXECUTE then + group.append Execute + if java_set.contains PosixFilePermission.OTHERS_READ then + others.append Read + if java_set.contains PosixFilePermission.OTHERS_WRITE then + others.append Write + if java_set.contains PosixFilePermission.OTHERS_EXECUTE then + others.append Execute + + File_Permissions owner.to_vector group.to_vector others.to_vector diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/System/Platform.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/System/Platform.enso index cdd6e059d28..2079d08646c 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/System/Platform.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/System/Platform.enso @@ -26,6 +26,10 @@ type Os os : Os os = from_text System.os +## Check if the currently running platform is a UNIX platform. +is_unix : Boolean +is_unix = System.is_unix + ## PRIVATE Create an Os object from text. diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Postgres.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Postgres.enso index 911b7c532c7..f2b6d583a72 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Postgres.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Postgres.enso @@ -1,5 +1,7 @@ from Standard.Base import all +from Standard.Base.Data.Numbers import Parse_Error + import Standard.Database.Data.Dialect import Standard.Database.Connection.Connection from Standard.Database.Connection.Credentials as Credentials_Module import Credentials @@ -7,6 +9,7 @@ import Standard.Database.Connection.Connection_Options import Standard.Database.Connection.SSL_Mode from Standard.Database.Connection.SSL_Mode import all import Standard.Database.Connection.Client_Certificate +import Standard.Database.Internal.Postgres.Pgpass polyglot java import org.postgresql.Driver @@ -20,7 +23,7 @@ type Postgres - credentials: The credentials to use for the connection (defaults to PGPass or No Authentication). - use_ssl: Whether to use SSL (defaults to `Prefer`). - client_cert: The client certificate to use or `Nothing` if not needed. - type Postgres (host:Text='localhost') (port:Integer=5432) (database:Text='') (credentials:(Credentials|Nothing)=Nothing) (use_ssl:SSL_Mode=Prefer) (client_cert:(Client_Certificate|Nothing)=Nothing) + type Postgres (host:Text=default_postgres_host) (port:Integer=default_postgres_port) (database:Text=default_postgres_database) (credentials:(Credentials|Nothing)=Nothing) (use_ssl:SSL_Mode=Prefer) (client_cert:(Client_Certificate|Nothing)=Nothing) ## Build the Connection resource. @@ -42,11 +45,22 @@ type Postgres jdbc_properties : [Pair Text Text] jdbc_properties = credentials = case self.credentials of - Nothing -> Postgres.read_pgpass self.host self.port self.database + Nothing -> + env_user = Environment.get "PGUSER" + env_password = Environment.get "PGPASSWORD" + case Pair env_user env_password of + Pair Nothing Nothing -> + Pgpass.read self.host self.port self.database + Pair Nothing _ -> + Error.throw (Illegal_State_Error "PGPASSWORD is set, but PGUSER is not.") + Pair username Nothing -> + Pgpass.read self.host self.port self.database username + Pair username password -> + [Pair 'user' username, Pair 'password' password] Credentials username password -> [Pair 'user' username, Pair 'password' password] - ssl_properties = Postgres.ssl_mode_to_jdbc_properties self.use_ssl + ssl_properties = ssl_mode_to_jdbc_properties self.use_ssl cert_properties = if self.client_cert.is_nothing then [] else self.client_cert.properties @@ -57,31 +71,30 @@ type Postgres dialect : Dialect dialect = Dialect.postgres - ## PRIVATE - static - Read the .pgpass file from the User's home directory and obtain username - and password. https://www.postgresql.org/docs/current/libpq-pgpass.html +## PRIVATE + Given an `SSL_Mode`, create the JDBC properties to secure a Postgres-based + connection. +ssl_mode_to_jdbc_properties : SSL_Mode -> [Pair Text Text] +ssl_mode_to_jdbc_properties use_ssl = case use_ssl of + Disable -> [] + Prefer -> [Pair 'sslmode' 'prefer'] + Require -> [Pair 'sslmode' 'require'] + Verify_CA cert_file -> + if cert_file.is_nothing then [Pair 'sslmode' 'verify-ca'] else + [Pair 'sslmode' 'verify-ca', Pair 'sslrootcert' (File.new cert_file).absolute.path] + Full_Verification cert_file -> + if cert_file.is_nothing then [Pair 'sslmode' 'verify-full'] else + [Pair 'sslmode' 'verify-full', Pair 'sslrootcert' (File.new cert_file).absolute.path] - Arguments: - - host: The hostname of the database server. - - port: The port of the database server. - - database: The database to connect to. - read_pgpass : Text -> Integer -> Text -> [Pair Text Text] - read_pgpass _ _ _ = - ## ToDo: Code not part of the design document. - ## host port database - [] +## PRIVATE +default_postgres_host = Environment.get_or_else "PGHOST" "localhost" - ## PRIVATE - static - Given an `SSL_Mode`, create the JDBC properties to secure a Postgres-based - connection. - ssl_mode_to_jdbc_properties : SSL_Mode -> [Pair Text Text] - ssl_mode_to_jdbc_properties use_ssl = case use_ssl of - Disable -> [] - Prefer -> [Pair 'sslmode' 'prefer'] - Require -> [Pair 'sslmode' 'require'] - Verify_CA cert_file -> - if cert_file.is_nothing then [Pair 'sslmode' 'verify-ca'] else - [Pair 'sslmode' 'verify-ca', Pair 'sslrootcert' (File.new cert_file).absolute.path] - Full_Verification cert_file -> - if cert_file.is_nothing then [Pair 'sslmode' 'verify-full'] else - [Pair 'sslmode' 'verify-full', Pair 'sslrootcert' (File.new cert_file).absolute.path] +## PRIVATE +default_postgres_port = + hardcoded_port = 5432 + case Environment.get "PGPORT" of + Nothing -> hardcoded_port + port -> Integer.parse port . catch Parse_Error (_->hardcoded_port) + +## PRIVATE +default_postgres_database = Environment.get_or_else "PGDATABASE" "" diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Redshift.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Redshift.enso index ec11e488cf0..f1d28645cc4 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Redshift.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Redshift.enso @@ -7,8 +7,7 @@ import Standard.Database.Connection.Connection_Options import Standard.Database.Connection.SSL_Mode from Standard.Database.Connection.SSL_Mode import all import Standard.Database.Connection.Client_Certificate - -import Standard.Database.Connection.Postgres +import Standard.Database.Internal.Postgres.Pgpass polyglot java import com.amazon.redshift.jdbc.Driver polyglot java import java.util.Properties @@ -53,7 +52,7 @@ type Redshift jdbc_properties : [Pair Text Text] jdbc_properties = credentials = case self.credentials of - Nothing -> Postgres.Postgres.read_pgpass self.host self.port self.schema + Nothing -> Pgpass.read self.host self.port self.schema AWS_Profile db_user profile -> [Pair 'user' db_user] + (if profile == '' then [] else [Pair 'profile' profile]) AWS_Key db_user access_key secret_access_key -> @@ -75,7 +74,7 @@ type Redshift type AWS_Profile ## Access Redshift using IAM via an AWS profile. - + Arguments: - db_user: Redshift username to connect as. - profile: AWS profile name (if empty uses default). @@ -83,7 +82,7 @@ type AWS_Profile ## Access Redshift using IAM via an AWS access key ID and secret access key. - + Arguments: - db_user: Redshift username to connect as. - access_key: AWS access key ID. diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/Postgres/Pgpass.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/Postgres/Pgpass.enso new file mode 100644 index 00000000000..6c6aa070b9f --- /dev/null +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/Postgres/Pgpass.enso @@ -0,0 +1,126 @@ +from Standard.Base import all + +import Standard.Base.System.Platform +import Standard.Base.System.File.File_Permissions + +polyglot java import java.lang.StringBuilder as Java_String_Builder + +## PRIVATE + Read the .pgpass file from the User's home directory and obtain username + and password. + + See https://www.postgresql.org/docs/current/libpq-pgpass.html + + On Windows this file is expected to be located at + `%APPDATA%\postgresql\pgpass.conf`. + On Linux and macOS this file is expected to be located at `~/.pgpass` and + it is should be inaccessible by other users and the group - otherwise it + will be ignored. This can be achieved by running `chmod 0600 ~/.pgpass`. + + If `PGPASSFILE` environment variable is set, the provided location is + used instead of the default one. + + Arguments: + - host: The hostname of the database server. + - port: The port of the database server. + - database: The database to connect to. +read : Text -> Integer -> Text -> Text -> [Pair Text Text] +read host port database username=Nothing = + pgpass_file = locate + if pgpass_file.is_nothing || (verify pgpass_file . not) then [] else + entries = parse_file pgpass_file + found = entries.find entry-> + entry.matches host port database username + case found.catch Nothing of + Nothing -> [] + entry -> [Pair 'user' entry.username, Pair 'password' entry.password] + +type Pgpass_Entry + ## PRIVATE + type Pgpass_Entry host port database username password + + ## PRIVATE + matches : Text -> Text|Integer -> Text -> Text -> Boolean + matches host port database username=Nothing = + wildcard='*' + host_match = self.host==wildcard || self.host==host + port_match = self.port==wildcard || + normalized_port = case port of + Integer -> port.to_text + Text -> port + self.port==normalized_port + database_match = self.database==wildcard || self.database==database + username_match = username==Nothing || self.username==wildcard || self.username==username + host_match && port_match && database_match && username_match + +## PRIVATE + Determines the location of the .pgpass file to use. +locate = case Environment.get "PGPASSFILE" of + Nothing -> case Platform.os of + Platform.Windows -> case Environment.get "APPDATA" of + Nothing -> Nothing + appdata -> File.new appdata / "postgresql" / "pgpass.conf" + _ -> case Environment.get "HOME" of + Nothing -> Nothing + home -> File.new home / ".pgpass" + path -> File.new path + +## PRIVATE + Checks if the given .pgpass file can be used. + + The file can be used if it exists and has correct permissions on UNIX systems. +verify file = case Platform.os of + Platform.Windows -> file.exists + _ -> case file.exists of + False -> False + True -> + permissions = file.posix_permissions + can_others_access = permissions.group.not_empty || permissions.others.not_empty + can_others_access.not + +## PRIVATE +parse_file file = + parse line = + if line.starts_with "#" || line.is_empty then Nothing else + elements = parse_line line + if elements.length != 5 then Nothing else + Pgpass_Entry (elements.at 0) (elements.at 1) (elements.at 2) (elements.at 3) (elements.at 4) + + File.read_text file . lines . map parse . filter (x -> x.is_nothing.not) + +## PRIVATE +parse_line line = + existing_entries = Vector.new_builder + current_entry = Java_String_Builder.new + next_entry = + existing_entries.append current_entry.toString + current_entry.setLength 0 + characters = line.characters + go ix is_escape = case ix>=characters.length of + True -> + if is_escape then + # Handle the trailing escape character. + current_entry.append '\\' + next_entry + False -> + c = characters.at ix + case c=='\\' of + True -> + if is_escape then + current_entry.append '\\' + @Tail_Call go (ix+1) is_escape.not + False -> case c==':' of + True -> + case is_escape of + True -> current_entry.append ':' + False -> next_entry + @Tail_Call go (ix+1) False + False -> + if is_escape then + # Handle escape character followed by other characters. + current_entry.append '\\' + # Any other character is just appended and escape is reset. + current_entry.append c + @Tail_Call go (ix+1) False + go 0 False + existing_entries.to_vector diff --git a/distribution/lib/Standard/Test/0.0.0-dev/src/Test_Environment.enso b/distribution/lib/Standard/Test/0.0.0-dev/src/Test_Environment.enso new file mode 100644 index 00000000000..9ee5b4b2507 --- /dev/null +++ b/distribution/lib/Standard/Test/0.0.0-dev/src/Test_Environment.enso @@ -0,0 +1,15 @@ +from Standard.Base import all + +polyglot java import org.enso.base.Environment_Utils + +## UNSTABLE + ADVANCED + + Runs a given action with an environment variable modified to a given value. + The environment variable is restored to its original value after the action. + The environment variable override is only visible to the Enso + `Environment.get` method, the environment as seen from a direct + `System.getenv` Java call remains unchanged. +unsafe_with_environment_override : Text -> Text -> Any -> Any +unsafe_with_environment_override key value ~action = + Environment_Utils.with_environment_variable_override key value (_->action) diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoFile.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoFile.java index f060d24550a..fb1c0a7fa29 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoFile.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoFile.java @@ -19,8 +19,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.CopyOption; import java.nio.file.OpenOption; -import java.time.ZonedDateTime; +import java.nio.file.attribute.PosixFilePermission; import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.util.Set; /** * A wrapper for {@link TruffleFile} objects exposed to the language. For methods documentation @@ -80,6 +82,13 @@ public class EnsoFile implements TruffleObject { return ZonedDateTime.ofInstant(truffleFile.getLastModifiedTime().toInstant(), ZoneOffset.UTC); } + @Builtin.Method(name = "posix_permissions_builtin") + @Builtin.WrapException(from = IOException.class, to = PolyglotError.class, propagate = true) + @Builtin.ReturningGuestObject + public Set getPosixPermissions() throws IOException { + return truffleFile.getPosixPermissions(); + } + @Builtin.Method(name = "parent") public EnsoFile getParent() { return new EnsoFile(this.truffleFile.getParent()); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/System.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/System.java index 3b9c493338d..b24d1e92152 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/System.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/system/System.java @@ -30,6 +30,12 @@ public class System { return UNKNOWN; } + @Builtin.Method(description = "Check if the operating system is UNIX.") + @CompilerDirectives.TruffleBoundary + public static Boolean is_unix() { + return SystemUtils.IS_OS_UNIX; + } + @Builtin.Method(description = "Gets the nanosecond resolution system time.") @CompilerDirectives.TruffleBoundary public static long nanoTime() { diff --git a/std-bits/base/src/main/java/org/enso/base/Environment_Utils.java b/std-bits/base/src/main/java/org/enso/base/Environment_Utils.java new file mode 100644 index 00000000000..93fc49ef14f --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/Environment_Utils.java @@ -0,0 +1,44 @@ +package org.enso.base; + +import java.util.HashMap; +import java.util.function.Function; + +public class Environment_Utils { + /** Gets the environment variable, including any overrides. */ + public static String get_environment_variable(String name) { + String override = overrides.get(name); + if (override != null) { + return override; + } else { + return System.getenv(name); + } + } + + /** + * Calls `action` with the provided environment variable. + * + *

The override is not persisted (its only visible from within the action called by this + * method) and it is only visible by the Enso `Environment.get` method (backed by {@code + * get_environment_variable}). + * + *

This is an internal function that should be used very carefully and only for testing. + */ + public static T with_environment_variable_override( + String name, String value, Function action) { + String oldValue = overrides.put(name, value); + boolean was_set = oldValue != null; + try { + // Giving 0 here as an argument, as using null would lead to incorrect behaviour, due to some + // weird Truffle peculiarity. + return action.apply(0); + } finally { + if (was_set) { + overrides.put(name, oldValue); + } else { + overrides.remove(name); + } + } + } + + private static final HashMap overrides = new HashMap<>(); +} diff --git a/test/Table_Tests/data/pgpass.conf b/test/Table_Tests/data/pgpass.conf new file mode 100644 index 00000000000..23d59dedfe1 --- /dev/null +++ b/test/Table_Tests/data/pgpass.conf @@ -0,0 +1,24 @@ +#hostname:port:database:username:password +localhost:5432:postgres:postgres:postgres +192.168.4.0:1234:foo:bar:baz +#some interesting comment + +host with \: semicolons in it? what?:*:*:*:well yes, that is possible, the \:password\: can contain those as well + +\::\::\::\::\: + +::wrong amount of entries is skipped + +::::::::::: + +you can escape an escape too\: see \\\\:*:*:*:yes it is possible +other escapes like \n or \? :*:*:*:are just parsed as-is +a trailing escape character:*:*:*:is treated as a regular slash\ +passwords should preserve leading space:*:*:*: pass +\\\::*:*:*:\\\: +#example.com:443:foo:bar:baz + +\:\:1:*:database_name:user_that_has_no_password: + +*:*:*:*:fallback_password +order_matters:1234:this:will_still_match_the_fallback_password:not_this_one diff --git a/test/Table_Tests/src/Database/Postgres_Spec.enso b/test/Table_Tests/src/Database/Postgres_Spec.enso index c7fa4a2034c..06ac417c3dc 100644 --- a/test/Table_Tests/src/Database/Postgres_Spec.enso +++ b/test/Table_Tests/src/Database/Postgres_Spec.enso @@ -1,10 +1,12 @@ from Standard.Base import all import Standard.Base.System.Environment import Standard.Base.Runtime.Ref +import Standard.Base.System.Platform +import Standard.Base.System.Process +from Standard.Base.System.Process.Exit_Code import Exit_Success from Standard.Database import all from Standard.Database.Connection.Connection import Sql_Error -import Standard.Test import Standard.Table as Materialized_Table import project.Database.Common_Spec import project.Database.Helpers.Name_Generator @@ -12,6 +14,10 @@ import project.Common_Table_Spec import project.Aggregate_Spec from Standard.Table.Data.Aggregate_Column import all from Standard.Database.Data.Sql import Sql_Type +from Standard.Database.Internal.Postgres.Pgpass import Pgpass_Entry + +import Standard.Test +import Standard.Test.Test_Environment postgres_specific_spec connection pending = Test.group "[PostgreSQL] Info" pending=pending <| @@ -112,7 +118,7 @@ run_tests connection pending=Nothing = clean_tables tables.to_vector -spec = +table_spec = db_name = Environment.get "ENSO_DATABASE_TEST_DB_NAME" db_host_port = (Environment.get "ENSO_DATABASE_TEST_HOST").if_nothing "localhost" . split ':' db_user = Environment.get "ENSO_DATABASE_TEST_DB_USER" @@ -128,4 +134,133 @@ spec = connection = Database.connect (Postgres (db_host_port.at 0) db_port db_name (Credentials db_user db_password)) run_tests connection +pgpass_file = enso_project.data / "pgpass.conf" + +pgpass_spec = Test.group "[PostgreSQL] .pgpass" <| + make_pair username password = + [Pair "user" username, Pair "password" password] + Test.specify "should correctly parse the file, including escapes, blank lines and comments" <| + result = Pgpass.parse_file pgpass_file + result.length . should_equal 12 + e1 = Pgpass_Entry "localhost" "5432" "postgres" "postgres" "postgres" + e2 = Pgpass_Entry "192.168.4.0" "1234" "foo" "bar" "baz" + e3 = Pgpass_Entry "host with : semicolons in it? what?" "*" "*" "*" "well yes, that is possible, the :password: can contain those as well" + e4 = Pgpass_Entry ":" ":" ":" ":" ":" + e5 = Pgpass_Entry "you can escape an escape too: see \\" "*" "*" "*" "yes it is possible" + e6 = Pgpass_Entry "other escapes like \n or \? " "*" "*" "*" "are just parsed as-is" + e7 = Pgpass_Entry "a trailing escape character" "*" "*" "*" "is treated as a regular slash\" + e8 = Pgpass_Entry "passwords should preserve leading space" "*" "*" "*" " pass" + e9 = Pgpass_Entry "\:" "*" "*" "*" "\:" + e10 = Pgpass_Entry "::1" "*" "database_name" "user_that_has_no_password" "" + e11 = Pgpass_Entry "*" "*" "*" "*" "fallback_password" + e12 = Pgpass_Entry "order_matters" "1234" "this" "will_still_match_the_fallback_password" "not_this_one" + entries = [e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12] + result.should_equal entries + + if Platform.is_unix then + Test.specify "should only accept the .pgpass file if it has correct permissions" <| + Process.run "chmod" ["0777", pgpass_file.absolute.path] . should_equal Exit_Success + Test_Environment.unsafe_with_environment_override "PGPASSFILE" (pgpass_file.absolute.path) <| + Pgpass.verify pgpass_file . should_equal False + Pgpass.read "passwords should preserve leading space" "1" "some database name that is really : weird" . should_equal [] + + Process.run "chmod" ["0400", pgpass_file.absolute.path] . should_equal Exit_Success + Test_Environment.unsafe_with_environment_override "PGPASSFILE" (pgpass_file.absolute.path) <| + Pgpass.verify pgpass_file . should_equal True + Pgpass.read "passwords should preserve leading space" "1" "some database name that is really : weird" . should_equal (make_pair "*" " pass") + + Test.specify "should correctly match wildcards and use the first matching entry" <| + Test_Environment.unsafe_with_environment_override "PGPASSFILE" (pgpass_file.absolute.path) <| + Pgpass.read "localhost" 5432 "postgres" . should_equal (make_pair "postgres" "postgres") + Pgpass.read "192.168.4.0" "1234" "foo" . should_equal (make_pair "bar" "baz") + Pgpass.read "" "" "" . should_equal (make_pair "*" "fallback_password") + Pgpass.read "blah" "5324" "blah" . should_equal (make_pair "*" "fallback_password") + Pgpass.read "::1" "55999" "database_name" . should_equal (make_pair "user_that_has_no_password" "") + Pgpass.read "order_matters" "1234" "this" . should_equal (make_pair "*" "fallback_password") + Pgpass.read "\:" "1234" "blah" . should_equal (make_pair "*" "\:") + Pgpass.read ":" ":" ":" . should_equal (make_pair ":" ":") + +connection_setup_spec = Test.group "[PostgreSQL] Connection setup" <| + Test.specify "should use environment variables as host, port and database defaults and fall back to hardcoded defaults" <| + c1 = Postgres "example.com" 12345 "my_db" + c2 = Postgres + c3 = Test_Environment.unsafe_with_environment_override "PGHOST" "192.168.0.1" <| + Test_Environment.unsafe_with_environment_override "PGPORT" "1000" <| + Test_Environment.unsafe_with_environment_override "PGDATABASE" "ensoDB" <| + Postgres + + c1.host . should_equal "example.com" + c1.port . should_equal 12345 + c1.database . should_equal "my_db" + c1.jdbc_url . should_equal "jdbc:postgresql://example.com:12345/my_db" + + c2.host . should_equal "localhost" + c2.port . should_equal 5432 + c2.database . should_equal "" + c2.jdbc_url . should_equal "jdbc:postgresql://localhost:5432" + + c3.host . should_equal "192.168.0.1" + c3.port . should_equal 1000 + c3.database . should_equal "ensoDB" + c3.jdbc_url . should_equal "jdbc:postgresql://192.168.0.1:1000/ensoDB" + + ## Currently we require the port to be numeric. When we support + Unix-sockets, we may lift that restriction. + c4 = Test_Environment.unsafe_with_environment_override "PGPORT" "foobar" <| + Postgres + c4.host . should_equal "localhost" + c4.port . should_equal 5432 + c4.database . should_equal "" + c4.jdbc_url . should_equal "jdbc:postgresql://localhost:5432" + + add_ssl props = props+[Pair 'sslmode' 'prefer'] + Test.specify "should use the given credentials" <| + c = Postgres credentials=(Credentials "myuser" "mypass") + c.jdbc_url . should_equal "jdbc:postgresql://localhost:5432" + c.jdbc_properties . should_equal <| add_ssl [Pair "user" "myuser", Pair "password" "mypass"] + + Test.specify "should fallback to environment variables and fill-out missing information based on the PGPASS file (if available)" <| + c1 = Postgres + c1.jdbc_url . should_equal "jdbc:postgresql://localhost:5432" + + c1.jdbc_properties . should_equal <| add_ssl [] + Test_Environment.unsafe_with_environment_override "PGPASSWORD" "somepassword" <| + c1.jdbc_properties . should_fail_with Illegal_State_Error + c1.jdbc_properties.catch.message . should_equal "PGPASSWORD is set, but PGUSER is not." + + Test_Environment.unsafe_with_environment_override "PGUSER" "someuser" <| + c1.jdbc_properties . should_equal <| add_ssl [Pair "user" "someuser", Pair "password" "somepassword"] + + c2 = Postgres "192.168.4.0" 1234 "foo" + c3 = Postgres "::1" 55999 "database_name" + c4 = Postgres "::1" 55999 "otherDB" + c2.jdbc_properties . should_equal <| add_ssl [] + c3.jdbc_properties . should_equal <| add_ssl [] + c4.jdbc_properties . should_equal <| add_ssl [] + + Test_Environment.unsafe_with_environment_override "PGPASSFILE" pgpass_file.absolute.path <| + c2.jdbc_properties . should_equal <| add_ssl [Pair "user" "bar", Pair "password" "baz"] + c3.jdbc_properties . should_equal <| add_ssl [Pair "user" "user_that_has_no_password", Pair "password" ""] + c4.jdbc_properties . should_equal <| add_ssl [Pair "user" "*", Pair "password" "fallback_password"] + + Test_Environment.unsafe_with_environment_override "PGUSER" "bar" <| + c2.jdbc_properties . should_equal <| add_ssl [Pair "user" "bar", Pair "password" "baz"] + [c3, c4].each c-> + c.jdbc_properties . should_equal <| + add_ssl [Pair "user" "*", Pair "password" "fallback_password"] + + Test_Environment.unsafe_with_environment_override "PGUSER" "other user" <| + [c2, c3, c4].each c-> + c.jdbc_properties . should_equal <| + add_ssl [Pair "user" "*", Pair "password" "fallback_password"] + + Test_Environment.unsafe_with_environment_override "PGPASSWORD" "other password" <| + [c2, c3, c4].each c-> + c.jdbc_properties . should_equal <| add_ssl [Pair "user" "other user", Pair "password" "other password"] + +spec = + table_spec + pgpass_spec + connection_setup_spec + main = Test.Suite.run_main spec diff --git a/test/Tests/src/Main.enso b/test/Tests/src/Main.enso index 2804553e954..d816482fb14 100644 --- a/test/Tests/src/Main.enso +++ b/test/Tests/src/Main.enso @@ -57,6 +57,7 @@ import project.Resource.Bracket_Spec import project.Runtime.Stack_Traces_Spec import project.Runtime.Lazy_Generator_Spec +import project.System.Environment_Spec import project.System.File_Spec import project.System.Process_Spec import project.System.Reporting_Stream_Decoder_Spec @@ -72,6 +73,7 @@ main = Test.Suite.run_main <| Conversion_Spec.spec Deep_Export_Spec.spec Error_Spec.spec + Environment_Spec.spec File_Spec.spec Reporting_Stream_Decoder_Spec.spec Reporting_Stream_Encoder_Spec.spec diff --git a/test/Tests/src/System/Environment_Spec.enso b/test/Tests/src/System/Environment_Spec.enso new file mode 100644 index 00000000000..324ccb061f3 --- /dev/null +++ b/test/Tests/src/System/Environment_Spec.enso @@ -0,0 +1,35 @@ +from Standard.Base import all + +import Standard.Base.System.Environment +import Standard.Test +import Standard.Test.Test_Environment + +spec = Test.group "Environment" <| + Test.specify "should allow to internally override environment variables for testing purposes" <| + old = Environment.get "foobar" + + result_0 = Test_Environment.unsafe_with_environment_override "foobar" "value1" 23 + result_0 . should_equal 23 + + result_1 = Test_Environment.unsafe_with_environment_override "foobar" "value1" <| + Environment.get "foobar" . should_equal "value1" + 42 + result_2 = Test_Environment.unsafe_with_environment_override "foobar" "other interesting value" <| + Environment.get "foobar" + + result_1 . should_equal 42 + result_2 . should_equal "other interesting value" + Environment.get "foobar" . should_equal old + + result_3 = Test_Environment.unsafe_with_environment_override "foo" "1" <| + Environment.get "foo" . should_equal "1" + x = Test_Environment.unsafe_with_environment_override "foo" "2" <| + Environment.get "foo" . should_equal "2" + Test_Environment.unsafe_with_environment_override "bar" "3" <| + Test_Environment.unsafe_with_environment_override "baz" "4" <| + [Environment.get "foo", Environment.get "bar", Environment.get "baz"] + Environment.get "foo" . should_equal "1" + x + result_3 . should_equal ["2", "3", "4"] + +main = Test.Suite.run_main spec diff --git a/test/Tests/src/System/File_Spec.enso b/test/Tests/src/System/File_Spec.enso index 2e7f3497a30..1c7b5752d19 100644 --- a/test/Tests/src/System/File_Spec.enso +++ b/test/Tests/src/System/File_Spec.enso @@ -4,6 +4,10 @@ from Standard.Base.Data.Text.Encoding as Encoding_Module import Encoding, Encodi import Standard.Base.System.File.Existing_File_Behavior from Standard.Base.System.File import File_Already_Exists_Error from Standard.Base.Error.Problem_Behavior import all +import Standard.Base.System.Platform +import Standard.Base.System.Process +from Standard.Base.System.File.File_Permissions as File_Permissions_Module import all +from Standard.Base.System.Process.Exit_Code import Exit_Success import Standard.Test import Standard.Test.Problems @@ -62,6 +66,23 @@ spec = file = File.new "does_not_exist.txt" file.delete . should_fail_with File.File_Not_Found + if Platform.is_unix then + Test.specify "should allow to check file permissions" <| + f = enso_project.data / "transient" / "permissions.txt" + f.delete_if_exists + "foobar".write f + + Process.run "chmod" ["0777", f.absolute.path] . should_equal Exit_Success + rwx = [Read, Write, Execute] + f.posix_permissions . should_equal <| + File_Permissions rwx rwx rwx + + Process.run "chmod" ["0421", f.absolute.path] . should_equal Exit_Success + f.posix_permissions . should_equal <| + File_Permissions [Read] [Write] [Execute] + + f.delete + Test.group "read_bytes" <| Test.specify "should allow reading a file to byte vector" <| contents = sample_file.read_bytes diff --git a/test/Tests/src/System/System_Spec.enso b/test/Tests/src/System/System_Spec.enso index c4d1c0e67be..f20aba4a6bd 100644 --- a/test/Tests/src/System/System_Spec.enso +++ b/test/Tests/src/System/System_Spec.enso @@ -7,3 +7,5 @@ spec = Test.group "System" <| Test.specify "should provide nanosecond timer" <| result = System.nano_time (result > 0).should_equal True + +main = Test.Suite.run_main spec