Skip to content

Commit

Permalink
Support the sslmode URL query parameter and UDS URLs (#248)
Browse files Browse the repository at this point in the history
* Add support for the sslmode values accepted by libpq, and support for UDS URLs.
* Clean up docs
* Improve code coverage info in CI and fix security issues in test workflow
  • Loading branch information
gwynne authored Jul 27, 2023
1 parent b69f427 commit e0f6bc9
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 87 deletions.
79 changes: 35 additions & 44 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
pull_request: { branches: ['*'] }
push: { branches: ['main'] }
pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
push: { branches: [ main ] }

env:
LOG_LEVEL: info
Expand All @@ -23,39 +23,9 @@ env:
POSTGRES_PASSWORD_B: 'test_password'

jobs:
# Baseline test run for code coverage stats
codecov:
strategy:
matrix: { dbimage: ['postgres:15'], dbauth: ['scram-sha-256'] }
runs-on: ubuntu-latest
container: swift:5.8-jammy
services:
psql-a:
image: ${{ matrix.dbimage }}
env:
POSTGRES_USER: 'test_username'
POSTGRES_DB: 'test_database'
POSTGRES_PASSWORD: 'test_password'
POSTGRES_HOST_AUTH_METHOD: ${{ matrix.dbauth }}
POSTGRES_INITDB_ARGS: --auth-host=${{ matrix.dbauth }}
steps:
- name: Save Postgres version and method to env
run: |
echo POSTGRES_VERSION='${{ matrix.dbimage }}' >> $GITHUB_ENV
echo POSTGRES_AUTH_METHOD='${{ matrix.dbauth }}' >> $GITHUB_ENV
- name: Check out package
uses: actions/checkout@v3
- name: Run local tests with coverage
run: swift test --enable-code-coverage
- name: Submit coverage report to Codecov.io
uses: vapor/[email protected]
with:
cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,POSTGRES_VERSION,POSTGRES_AUTH_METHOD'
cc_fail_ci_if_error: false

# Check for API breakage versus main
api-breakage:
if: github.event_name == 'pull_request'
if: ${{ !(github.event.pull_request.draft || false) }}
runs-on: ubuntu-latest
container: swift:5.8-jammy
steps:
Expand All @@ -67,7 +37,7 @@ jobs:

# Run Linux unit tests against various configurations
linux-unit:
if: github.event_name == 'pull_request'
if: ${{ !(github.event.pull_request.draft || false) }}
strategy:
fail-fast: false
matrix:
Expand All @@ -82,8 +52,8 @@ jobs:
{dbimage: 'postgres:13', dbauth: 'md5'},
{dbimage: 'postgres:11', dbauth: 'trust'}
]
container: ${{ matrix.swiftver }}
runs-on: ubuntu-latest
container: ${{ matrix.swiftver }}
services:
psql-a:
image: ${{ matrix.dbimage }}
Expand All @@ -105,11 +75,23 @@ jobs:
- name: Check out package
uses: actions/checkout@v3
- name: Run local tests
run: swift test
run: swift test --enable-code-coverage
- name: Note Swift version
if: ${{ contains(matrix.swiftver, 'nightly') }}
run: |
echo "SWIFT_PLATFORM=$(. /etc/os-release && echo "${ID}${VERSION_ID}")" >>"${GITHUB_ENV}"
echo "SWIFT_VERSION=$(cat /.swift_tag)" >>"${GITHUB_ENV}"
- name: Upload code coverage
uses: vapor/[email protected]
env:
POSTGRES_VERSION: ${{ matrix.dbimage }}
POSTGRES_AUTH_METHOD: ${{ matrix.dbauth }}
with:
cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,POSTGRES_VERSION,POSTGRES_AUTH_METHOD'

# Test integration with dependent package on Linux
linux-integration:
if: github.event_name == 'pull_request'
if: ${{ !(github.event.pull_request.draft || false) }}
strategy:
fail-fast: false
matrix:
Expand All @@ -120,8 +102,8 @@ jobs:
'swiftlang/swift:nightly-5.9-jammy',
'swiftlang/swift:nightly-main-jammy'
]
container: ${{ matrix.swiftver }}
runs-on: ubuntu-latest
container: ${{ matrix.swiftver }}
services:
psql-a:
image: ${{ matrix.dbimage }}
Expand Down Expand Up @@ -153,7 +135,7 @@ jobs:

# Run macOS unit tests against various configurations
macos-unit:
if: github.event_name == 'pull_request'
if: ${{ !(github.event.pull_request.draft || false) }}
strategy:
fail-fast: false
matrix:
Expand All @@ -165,20 +147,29 @@ jobs:
env:
POSTGRES_HOSTNAME: 127.0.0.1
POSTGRES_DB: postgres
POSTGRES_HOST_AUTH_METHOD: ${{ matrix.dbauth }}
steps:
- name: Select latest available Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode }}
- name: Install Postgres, setup DB and auth, and wait for server start
env:
POSTGRES_VERSION: ${{ matrix.dbimage }}
POSTGRES_AUTH_METHOD: ${{ matrix.dbauth }}
run: |
export PATH="$(brew --prefix)/opt/${{ matrix.formula }}/bin:$PATH" PGDATA=/tmp/vapor-postgres-test
(brew unlink postgresql || true) && brew install ${{ matrix.dbimage }} && brew link --force ${{ matrix.dbimage }}
initdb --locale=C --auth-host ${{ matrix.dbauth }} -U $POSTGRES_USER --pwfile=<(echo $POSTGRES_PASSWORD)
export PATH="$(brew --prefix)/opt/${POSTGRES_VERSION}/bin:$PATH" PGDATA=/tmp/vapor-postgres-test
(brew unlink postgresql || true) && brew install "${POSTGRES_VERSION}" && brew link --force "${POSTGRES_VERSION}"
initdb --locale=C --auth-host "${POSTGRES_AUTH_METHOD}" -U "${POSTGRES_USER}" --pwfile=<(echo "${POSTGRES_PASSWORD}")
pg_ctl start --wait
timeout-minutes: 2
- name: Checkout code
uses: actions/checkout@v3
- name: Run local tests
run: swift test
run: swift test --enable-code-coverage
- name: Upload code coverage
uses: vapor/[email protected]
env:
POSTGRES_VERSION: ${{ matrix.dbimage }}
POSTGRES_AUTH_METHOD: ${{ matrix.dbauth }}
with:
cc_env_vars: 'MD_APPLE_SDK_ROOT,SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,POSTGRES_VERSION,POSTGRES_AUTH_METHOD'
5 changes: 2 additions & 3 deletions Sources/PostgresKit/Deprecations/PostgresColumnType.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import AsyncKit
import SQLKit

/// Postgres-specific column types.
Expand Down Expand Up @@ -250,7 +249,7 @@ public struct PostgresColumnType: SQLExpression, Hashable {
case custom(String) /// User-defined type
indirect case array(of: Primitive) /// Array

/// See ``Swift/CustomStringConvertible/description``.
/// See `CustomStringConvertible.description`.
var description: String {
switch self {
case .bigint: return "BIGINT"
Expand Down Expand Up @@ -301,7 +300,7 @@ public struct PostgresColumnType: SQLExpression, Hashable {
}
}

/// See ``SQLExpression/serialize(to:)``.
// See `SQLExpression.serialize(to:)`.
public func serialize(to serializer: inout SQLSerializer) {
serializer.write(self.primitive.description)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import NIOSSL
import Atomics
import AsyncKit
import Logging
import PostgresNIO
import NIOCore
Expand Down
28 changes: 28 additions & 0 deletions Sources/PostgresKit/Docs.docc/PostgresKit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# ``PostgresKit``

@Metadata {
@TitleHeading(Package)
}

PostgresKit is a library providing an SQLKit driver for PostgresNIO.

## Overview

This package provides the "foundational" level of support for using [Fluent] with PostgreSQL by implementing the requirements of an [SQLKit] driver. It is responsible for:

- Managing the underlying PostgreSQL library ([PostgresNIO]),
- Providing a two-way bridge between PostgresNIO and SQLKit's generic data and metadata formats,
- Presenting an interface for establishing, managing, and interacting with database connections.

> Note: The FluentKit driver for PostgreSQL is provided by the [FluentPostgresDriver] package.
## Version Support

This package uses [PostgresNIO] for all underlying database interactions. It is compatible with all versions of PostgreSQL and all platforms supported by that package.

> Important: There is one exception to the above at the time of this writing: This package requires Swift 5.7 or newer, whereas PostgresNIO continues to support Swift 5.6.
[SQLKit]: https://swiftpackageindex.com/vapor/sql-kit
[PostgresNIO]: https://swiftpackageindex.com/vapor/postgres-nio
[Fluent]: https://swiftpackageindex.com/vapor/fluent-kit
[FluentPostgresDriver]: https://swiftpackageindex.com/vapor/fluent-postgres-driver
3 changes: 0 additions & 3 deletions Sources/PostgresKit/Docs.docc/index.md

This file was deleted.

107 changes: 80 additions & 27 deletions Sources/PostgresKit/SQLPostgresConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public struct SQLPostgresConfiguration {
/// `UInt16(getservbyname("postgresql", "tcp").pointee.s_port).byteSwapped`
public static var ianaPortNumber: Int { 5432 }

/// See ``PostgresNIO/PostgresConnection/Configuration``.
// See `PostgresNIO.PostgresConnection.Configuration`.
public var coreConfiguration: PostgresConnection.Configuration

/// Optional `search_path` to set on new connections.
Expand All @@ -27,40 +27,93 @@ public struct SQLPostgresConfiguration {

/// Create a ``SQLPostgresConfiguration`` from a properly formatted URL.
///
/// The allowed URL format is:
/// The supported URL formats are:
///
/// postgres://username:password@hostname:port/database?tls=mode
/// postgres://username:password@hostname:port/database?tlsmode=mode
/// postgres+tcp://username:password@hostname:port/database?tlsmode=mode
/// postgres+uds://username:password@localhost/path?tlsmode=mode#database
///
/// `hostname` and `username` are required; all other components are optional. For backwards
/// compatibility, `ssl` is treated as an alias of `tls`.
/// The `postgres+tcp` scheme requests a connection over TCP. The `postgres` scheme is an alias
/// for `postgres+tcp`. Only the `hostname` and `username` components are required.
///
/// The allowed `mode` values for `tls` are:
/// - `require` (fail to connect if the server does not support TLS)
/// - `true` (attempt to use TLS but continue anyway if the server doesn't support it)
/// - `false` (do not use TLS even if the server supports it).
/// If `tls` is omitted entirely, the mode defaults to `true`.
/// The `postgres+uds` scheme requests a connection via a UNIX domain socket. The `username` and
/// `path` components are required. The authority must always be empty or `localhost`, and may not
/// specify a port.
///
/// The allowed `mode` values for `tlsmode` are:
///
/// Value|Behavior
/// -|-
/// `disable`|Don't use TLS, even if the server supports it.
/// `prefer`|Use TLS if possible.
/// `require`|Enforce TLS support.
///
/// If no `tlsmode` is specified, the default mode is `prefer` for TCP connections, or `disable`
/// for UDS connections. If more than one mode is specified, the last one wins. Whenever a TLS
/// connection is made, full certificate verification (both chain of trust and hostname match)
/// is always enforced, regardless of the mode used.
///
/// For compatibility with `libpq` and previous versions of this package, any of "`sslmode`",
/// "`tls`", or "`ssl`" may be used instead of "`tlsmode`". There are also various aliases for
/// each of the TLS mode names, as follows:
///
/// - "`disable`": "`false`"
/// - "`prefer`": "`allow`", "`true`"
/// - "`require`": "`verify-ca`", "`verify-full`"
///
/// The aliases always have the same semantics as the "canonical" modes, despite any differences
/// suggested by their names.
///
/// > Note: It is possible to emulate `libpq`'s definitions for `prefer` (TLS if available with
/// > no certificate verification), `require` (TLS enforced, but also without certificate
/// > verification) and `verify-ca` (TLS enforced with no hostname verification) by manually
/// > specifying the TLS configuration instead of using a URL. It is not possible, by design, to
/// > emulate `libpq`'s `allow` mode (TLS only if there is no alternative). It is _strongly_
/// > recommended for both security and privacy reasons to always leave full certificate
/// > verification enabled whenever possible. See NIOSSL's [`TLSConfiguration`](tlsconfig) for
/// > additional information and recommendations.
///
/// [tlsconfig]:
/// https://swiftpackageindex.com/apple/swift-nio-ssl/main/documentation/niossl/tlsconfiguration
public init(url: URL) throws {
guard let comp = URLComponents(url: url, resolvingAgainstBaseURL: true),
comp.scheme?.hasPrefix("postgres") ?? false,
let hostname = comp.host, let username = comp.user
else {
guard let comp = URLComponents(url: url, resolvingAgainstBaseURL: true), let username = comp.user else {
throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString])
}
let password = comp.password, port = comp.port ?? Self.ianaPortNumber
let tls: PostgresConnection.Configuration.TLS
switch (comp.queryItems ?? []).first(where: { ["ssl", "tls"].contains($0.name.lowercased()) })?.value ?? "true" {
case "require": tls = try .require(.init(configuration: .makeClientConfiguration()))
case "true": tls = try .prefer(.init(configuration: .makeClientConfiguration()))
case "false": tls = .disable
default: throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString])

func decideTLSConfig(from queryItems: [URLQueryItem], defaultMode: String) throws -> PostgresConnection.Configuration.TLS {
switch queryItems.last(where: { ["tlsmode", "sslmode", "ssl", "tls"].contains($0.name.lowercased()) })?.value ?? defaultMode {
case "verify-full", "verify-ca", "require":
return try .require(.init(configuration: .makeClientConfiguration()))
case "prefer", "allow", "true":
return try .prefer(.init(configuration: .makeClientConfiguration()))
case "disable", "false":
return .disable
default:
throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString])
}
}

self.init(
hostname: hostname, port: port,
username: username, password: password,
database: url.lastPathComponent,
tls: tls
)
switch comp.scheme {
case "postgres", "postgres+tcp":
guard let hostname = comp.host, !hostname.isEmpty else {
throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString])
}
self.init(
hostname: hostname, port: comp.port ?? Self.ianaPortNumber,
username: username, password: comp.password,
database: url.lastPathComponent.isEmpty ? nil : url.lastPathComponent,
tls: try decideTLSConfig(from: comp.queryItems ?? [], defaultMode: "prefer")
)
case "postgres+uds":
guard (comp.host?.isEmpty ?? true || comp.host == "localhost"), comp.port == nil, !comp.path.isEmpty, comp.path != "/" else {
throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString])
}
var coreConfig = PostgresConnection.Configuration(unixSocketPath: comp.path, username: username, password: comp.password, database: comp.fragment)
coreConfig.tls = try decideTLSConfig(from: comp.queryItems ?? [], defaultMode: "disable")
self.init(coreConfiguration: coreConfig)
default:
throw URLError(.badURL, userInfo: [NSURLErrorFailingURLErrorKey: url, NSURLErrorFailingURLStringErrorKey: url.absoluteString])
}
}

/// Create a ``SQLPostgresConfiguration`` for connecting to a server with a hostname and optional port.
Expand Down
9 changes: 0 additions & 9 deletions Tests/PostgresKitTests/PostgresKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,3 @@ extension Bar: PostgresNonThrowingEncodable, PostgresArrayEncodable, PostgresDec
static var psqlFormat: PostgresFormat { .binary }
static var psqlArrayType: PostgresDataType { .int8Array }
}

let isLoggingConfigured: Bool = {
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .info
return handler
}
return true
}()
Loading

0 comments on commit e0f6bc9

Please sign in to comment.