mirror of
https://github.com/enso-org/enso.git
synced 2024-12-23 18:15:21 +03:00
Add support for .pgpass
to PostgreSQL (#3593)
Implements https://www.pivotaltracker.com/story/show/182582924
This commit is contained in:
parent
7e2998bd27
commit
16fd038c1a
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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.
|
||||
|
@ -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" ""
|
||||
|
@ -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 ->
|
||||
|
@ -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
|
@ -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)
|
@ -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<PosixFilePermission> getPosixPermissions() throws IOException {
|
||||
return truffleFile.getPosixPermissions();
|
||||
}
|
||||
|
||||
@Builtin.Method(name = "parent")
|
||||
public EnsoFile getParent() {
|
||||
return new EnsoFile(this.truffleFile.getParent());
|
||||
|
@ -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() {
|
||||
|
@ -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.
|
||||
*
|
||||
* <p>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}).
|
||||
*
|
||||
* <p>This is an internal function that should be used very carefully and only for testing.
|
||||
*/
|
||||
public static <T> T with_environment_variable_override(
|
||||
String name, String value, Function<Object, T> 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<String, String> overrides = new HashMap<>();
|
||||
}
|
24
test/Table_Tests/data/pgpass.conf
Normal file
24
test/Table_Tests/data/pgpass.conf
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
35
test/Tests/src/System/Environment_Spec.enso
Normal file
35
test/Tests/src/System/Environment_Spec.enso
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user